This commit is contained in:
Adrian Stobbe 2023-07-15 07:29:32 +02:00
parent 7fcf7e15c6
commit ad56360d5f
10 changed files with 272 additions and 3 deletions

View File

@ -111,6 +111,12 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command, fileHandler file.Hand
return fmt.Errorf("upgrading measurements: %w", err)
}
migrateIAM := getIAMMigrateCmd(cmd, u.upgrader.GetTerraformUpgrader(), conf, flags, u.upgrader.GetUpgradeID())
if err := u.executeMigration(cmd, fileHandler, migrateIAM, flags); err != nil {
return fmt.Errorf("executing IAM migration: %w", err)
}
//
if err := u.migrateTerraform(cmd, fileHandler, u.imageFetcher, conf, flags); err != nil {
return fmt.Errorf("performing Terraform migrations: %w", err)
}
@ -211,6 +217,60 @@ func (u *upgradeApplyCmd) migrateTerraform(cmd *cobra.Command, file file.Handler
return nil
}
func getIAMMigrateCmd(cmd *cobra.Command, tfClient *terraform.Client, conf *config.Config, flags upgradeApplyFlags, upgradeID string) *terraform.IAMMigrateCmd {
// Check if there are any Terraform migrations to apply
outWriter := cmd.OutOrStdout()
migrateCmd := terraform.NewIAMMigrateCmd(
tfClient,
upgradeID,
conf.GetProvider(),
flags.terraformLogLevel,
outWriter,
)
return migrateCmd
}
func (u *upgradeApplyCmd) executeMigration(cmd *cobra.Command, file file.Handler, migrateCmd terraform.MigrationCmd, flags upgradeApplyFlags) error {
hasDiff, err := migrateCmd.Plan(cmd.Context()) // u.upgrader.PlanTerraformMigrations(cmd.Context(), opts)
if err != nil {
return fmt.Errorf("planning terraform migrations: %w", err)
}
if hasDiff {
// If there are any Terraform migrations to apply, ask for confirmation
fmt.Fprintf(cmd.OutOrStdout(), "The %s upgrade requires a migration of Constellation cloud resources by applying an updated Terraform template. Please manually review the suggested changes below.\n", migrateCmd.String())
if !flags.yes {
ok, err := askToConfirm(cmd, fmt.Sprintf("Do you want to apply the %s?", migrateCmd.String()))
if err != nil {
return fmt.Errorf("asking for confirmation: %w", err)
}
if !ok {
cmd.Println("Aborting upgrade.")
if err := u.upgrader.CleanUpTerraformMigrations(file); err != nil {
return fmt.Errorf("cleaning up workspace: %w", err)
}
return fmt.Errorf("aborted by user")
}
}
u.log.Debugf("Applying Terraform %s migrations", migrateCmd.String())
// .ApplyMigration()
err := migrateCmd.Apply(cmd.Context(), file) // u.upgrader.ApplyTerraformMigrations(cmd.Context(), file, opts)
if err != nil {
return fmt.Errorf("applying terraform migrations: %w", err)
}
// TODO write this outside of apply migration
upgradeOutputFile := constants.TerraformMigrationOutputFile
cmd.Printf("Terraform migrations applied successfully and output written to: %s\n"+
"A backup of the pre-upgrade Terraform state has been written to: %s\n",
upgradeOutputFile, filepath.Join(constants.UpgradeDir, constants.TerraformUpgradeBackupDir)) // TODO include log of file write where the action happens?
} else {
u.log.Debugf("No Terraform diff detected")
}
return nil
}
// parseTerraformUpgradeVars parses the variables required to execute the Terraform script with.
func parseTerraformUpgradeVars(cmd *cobra.Command, conf *config.Config, fetcher imageFetcher) (terraform.Variables, error) {
// Fetch variables to execute Terraform script with
@ -446,6 +506,8 @@ type cloudUpgrader interface {
CheckTerraformMigrations(fileHandler file.Handler) error
CleanUpTerraformMigrations(fileHandler file.Handler) error
AddManualStateMigration(migration terraform.StateMigration)
GetTerraformUpgrader() *terraform.Client
GetUpgradeID() string
}
func toPtr[T any](v T) *T {

View File

@ -153,6 +153,14 @@ func NewUpgrader(ctx context.Context, outWriter io.Writer, log debugLog, upgrade
return u, nil
}
func (u *Upgrader) GetTerraformUpgrader() *terraform.Client {
return u.tfClient
}
func (u *Upgrader) GetUpgradeID() string {
return u.upgradeID
}
// AddManualStateMigration adds a manual state migration to the Terraform client.
// TODO(AB#3248): Remove this method after we can assume that all existing clusters have been migrated.
func (u *Upgrader) AddManualStateMigration(migration terraform.StateMigration) {
@ -175,10 +183,15 @@ func (u *Upgrader) CleanUpTerraformMigrations(fileHandler file.Handler) error {
// If a diff exists, it's being written to the upgrader's output writer. It also returns
// a bool indicating whether a diff exists.
func (u *Upgrader) PlanTerraformMigrations(ctx context.Context, opts upgrade.TerraformUpgradeOptions) (bool, error) {
return u.tfUpgrader.PlanTerraformMigrations(ctx, opts, u.upgradeID)
hasDiff, err := u.tfUpgrader.PlanIAMMigration(ctx, opts.CSP, opts.LogLevel, u.upgradeID)
if err != nil {
return false, fmt.Errorf("planning terraform migrations: %w", err)
}
return hasDiff, nil
// return u.tfUpgrader.PlanTerraformMigrations(ctx, opts, u.upgradeID)
}
// ApplyTerraformMigrations applies the migerations planned by PlanTerraformMigrations.
// ApplyTerraformMigrations applies the migrations planned by PlanTerraformMigrations.
// If PlanTerraformMigrations has not been executed before, it will return an error.
// In case of a successful upgrade, the output will be written to the specified file and the old Terraform directory is replaced
// By the new one.

View File

@ -4,6 +4,7 @@ load("//bazel/go:go_test.bzl", "go_test")
go_library(
name = "terraform",
srcs = [
"iammigrate.go",
"loader.go",
"logging.go",
"terraform.go",

View File

@ -0,0 +1,98 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package terraform
import (
"context"
"fmt"
"io"
"path/filepath"
"strings"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
)
type tfClient interface {
PrepareIAMUpgradeWorkspace(rootDir, workingDir, newWorkingDir, backupDir string) error
Plan(ctx context.Context, logLevel LogLevel, planFile string) (bool, error)
ShowPlan(ctx context.Context, logLevel LogLevel, planFile string, outWriter io.Writer) error
CreateIAMConfig(ctx context.Context, csp cloudprovider.Provider, logLevel LogLevel) (IAMOutput, error)
}
type MigrationCmd interface {
Plan(ctx context.Context) (bool, error)
Apply(ctx context.Context, fileHandler file.Handler) error
String() string
}
type IAMMigrateCmd struct {
tf tfClient
upgradeID string
csp cloudprovider.Provider
logLevel LogLevel
outWriter io.Writer
}
func NewIAMMigrateCmd(tf tfClient, upgradeID string, csp cloudprovider.Provider, logLevel LogLevel, outWriter io.Writer) *IAMMigrateCmd {
return &IAMMigrateCmd{
tf: tf,
upgradeID: upgradeID,
csp: csp,
logLevel: logLevel,
outWriter: outWriter,
}
}
func (c *IAMMigrateCmd) String() string {
return "iam migration"
}
func (c *IAMMigrateCmd) Plan(ctx context.Context) (bool, error) {
err := c.tf.PrepareIAMUpgradeWorkspace(
filepath.Join("terraform", "iam", strings.ToLower(c.csp.String())),
constants.TerraformIAMWorkingDir,
filepath.Join(constants.UpgradeDir, c.upgradeID, constants.TerraformIAMUpgradeWorkingDir),
filepath.Join(constants.UpgradeDir, c.upgradeID, constants.TerraformUpgradeBackupDir), // TODO: use IAM backup dir
)
if err != nil {
return false, fmt.Errorf("preparing terraform workspace: %w", err)
}
hasDiff, err := c.tf.Plan(ctx, c.logLevel, constants.TerraformUpgradePlanFile)
if err != nil {
return false, fmt.Errorf("terraform plan: %w", err)
}
if hasDiff {
if err := c.tf.ShowPlan(ctx, c.logLevel, constants.TerraformUpgradePlanFile, c.outWriter); err != nil {
return false, fmt.Errorf("terraform show plan: %w", err)
}
}
return hasDiff, nil
}
func (c *IAMMigrateCmd) Apply(ctx context.Context, fileHandler file.Handler) error {
_, err := c.tf.CreateIAMConfig(ctx, c.csp, c.logLevel)
// TODO: put in template?
if err := fileHandler.RemoveAll(constants.TerraformIAMWorkingDir); err != nil {
return fmt.Errorf("removing old terraform directory: %w", err)
}
if err := fileHandler.CopyDir(filepath.Join(constants.UpgradeDir, c.upgradeID, constants.TerraformIAMUpgradeWorkingDir), constants.TerraformIAMWorkingDir); err != nil {
return fmt.Errorf("replacing old terraform directory with new one: %w", err)
}
if err := fileHandler.RemoveAll(filepath.Join(constants.UpgradeDir, c.upgradeID, constants.TerraformIAMUpgradeWorkingDir)); err != nil {
return fmt.Errorf("removing terraform upgrade directory: %w", err)
}
return err
}

View File

@ -102,6 +102,18 @@ func (c *Client) PrepareUpgradeWorkspace(path, oldWorkingDir, newWorkingDir, bac
return c.writeVars(vars)
}
// PrepareIAMUpgradeWorkspace prepares a Terraform workspace for a Constellation IAM upgrade.
func (c *Client) PrepareIAMUpgradeWorkspace(path, oldWorkingDir, newWorkingDir, backupDir string) error {
if err := prepareUpgradeWorkspace(path, c.file, oldWorkingDir, newWorkingDir, backupDir); err != nil {
return fmt.Errorf("prepare upgrade workspace: %w", err)
}
// copy the vars file from the old working dir to the new working dir
if err := c.file.CopyFile(filepath.Join(oldWorkingDir, terraformVarsFile), filepath.Join(newWorkingDir, terraformVarsFile)); err != nil {
return fmt.Errorf("copying vars file: %w", err)
}
return nil
}
// CreateCluster creates a Constellation cluster using Terraform.
func (c *Client) CreateCluster(ctx context.Context, logLevel LogLevel) (CreateOutput, error) {
if err := c.setLogLevel(logLevel); err != nil {

View File

@ -0,0 +1,20 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "main_lib",
srcs = ["main.go"],
importpath = "github.com/edgelesssys/constellation/v2/cli/internal/upgrade/main",
visibility = ["//visibility:private"],
deps = [
"//cli/internal/terraform",
"//cli/internal/upgrade",
"//internal/cloud/cloudprovider",
"//internal/constants",
],
)
go_binary(
name = "main",
embed = [":main_lib"],
visibility = ["//cli:__subpackages__"],
)

BIN
cli/internal/upgrade/main/main Executable file

Binary file not shown.

View File

@ -0,0 +1,35 @@
package main
import (
"bytes"
"context"
"fmt"
"path/filepath"
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/cli/internal/upgrade"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/constants"
)
func main() {
ctx := context.Background()
tfClient, err := terraform.New(ctx, filepath.Join(constants.UpgradeDir, "test", constants.TerraformUpgradeWorkingDir))
if err != nil {
panic(fmt.Errorf("setting up terraform client: %w", err))
}
// give me a writer
outWriter := bytes.NewBuffer(nil)
tfUpgrader, err := upgrade.NewTerraformUpgrader(tfClient, outWriter)
if err != nil {
panic(fmt.Errorf("setting up terraform upgrader: %w", err))
}
diff, err := tfUpgrader.PlanIAMMigration(ctx, upgrade.TerraformUpgradeOptions{
CSP: cloudprovider.AWS,
LogLevel: terraform.LogLevelDebug,
}, "test")
if err != nil {
panic(fmt.Errorf("planning terraform migrations: %w", err))
}
fmt.Println(diff)
}

View File

@ -85,6 +85,31 @@ func checkFileExists(fileHandler file.Handler, existingFiles *[]string, filename
return nil
}
func (u *TerraformUpgrader) PlanIAMMigration(ctx context.Context, csp cloudprovider.Provider, logLevel terraform.LogLevel, upgradeID string) (bool, error) {
err := u.tf.PrepareIAMUpgradeWorkspace(
filepath.Join("terraform", "iam", strings.ToLower(csp.String())),
constants.TerraformIAMWorkingDir,
filepath.Join(constants.UpgradeDir, upgradeID, constants.TerraformUpgradeWorkingDir),
filepath.Join(constants.UpgradeDir, upgradeID, constants.TerraformUpgradeBackupDir),
)
if err != nil {
return false, fmt.Errorf("preparing terraform workspace: %w", err)
}
hasDiff, err := u.tf.Plan(ctx, logLevel, constants.TerraformUpgradePlanFile)
if err != nil {
return false, fmt.Errorf("terraform plan: %w", err)
}
if hasDiff {
if err := u.tf.ShowPlan(ctx, logLevel, constants.TerraformUpgradePlanFile, u.outWriter); err != nil {
return false, fmt.Errorf("terraform show plan: %w", err)
}
}
return hasDiff, nil
}
// PlanTerraformMigrations prepares the upgrade workspace and plans the Terraform migrations for the Constellation upgrade.
// If a diff exists, it's being written to the upgrader's output writer. It also returns
// a bool indicating whether a diff exists.
@ -130,7 +155,7 @@ func (u *TerraformUpgrader) CleanUpTerraformMigrations(fileHandler file.Handler,
return nil
}
// ApplyTerraformMigrations applies the migerations planned by PlanTerraformMigrations.
// ApplyTerraformMigrations applies the migrations planned by PlanTerraformMigrations.
// If PlanTerraformMigrations has not been executed before, it will return an error.
// In case of a successful upgrade, the output will be written to the specified file and the old Terraform directory is replaced
// By the new one.
@ -176,6 +201,7 @@ func (u *TerraformUpgrader) ApplyTerraformMigrations(ctx context.Context, fileHa
// a tfClient performs the Terraform interactions in an upgrade.
type tfClient interface {
PrepareIAMUpgradeWorkspace(path, oldWorkingDir, newWorkingDir, backupDir string) error
PrepareUpgradeWorkspace(path, oldWorkingDir, newWorkingDir, upgradeID string, vars terraform.Variables) error
ShowPlan(ctx context.Context, logLevel terraform.LogLevel, planFilePath string, output io.Writer) error
Plan(ctx context.Context, logLevel terraform.LogLevel, planFile string) (bool, error)

View File

@ -154,6 +154,8 @@ const (
TerraformUpgradePlanFile = "plan.zip"
// TerraformUpgradeWorkingDir is the directory name for the Terraform workspace being used in an upgrade.
TerraformUpgradeWorkingDir = "terraform"
// TerraformIAMUpgradeWorkingDir is the directory name for the Terraform IAM workspace being used in an upgrade.
TerraformIAMUpgradeWorkingDir = "terraform-iam"
// TerraformUpgradeBackupDir is the directory name being used to backup the pre-upgrade state in an upgrade.
TerraformUpgradeBackupDir = "terraform-backup"
// TerraformMigrationOutputFile is the file name of the output file created by a successful Terraform migration.