diff --git a/cli/internal/cmd/upgradeapply.go b/cli/internal/cmd/upgradeapply.go index a83166b4d..c4753ad7f 100644 --- a/cli/internal/cmd/upgradeapply.go +++ b/cli/internal/cmd/upgradeapply.go @@ -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 { diff --git a/cli/internal/kubernetes/upgrade.go b/cli/internal/kubernetes/upgrade.go index 5ccb3b7e2..98e41b8fa 100644 --- a/cli/internal/kubernetes/upgrade.go +++ b/cli/internal/kubernetes/upgrade.go @@ -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. diff --git a/cli/internal/terraform/BUILD.bazel b/cli/internal/terraform/BUILD.bazel index 208ee0ecb..edf5e3ee8 100644 --- a/cli/internal/terraform/BUILD.bazel +++ b/cli/internal/terraform/BUILD.bazel @@ -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", diff --git a/cli/internal/terraform/iammigrate.go b/cli/internal/terraform/iammigrate.go new file mode 100644 index 000000000..ced2efdc6 --- /dev/null +++ b/cli/internal/terraform/iammigrate.go @@ -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 +} diff --git a/cli/internal/terraform/terraform.go b/cli/internal/terraform/terraform.go index 0058512e3..dfb40bb0c 100644 --- a/cli/internal/terraform/terraform.go +++ b/cli/internal/terraform/terraform.go @@ -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 { diff --git a/cli/internal/upgrade/main/BUILD.bazel b/cli/internal/upgrade/main/BUILD.bazel new file mode 100644 index 000000000..c06a85b16 --- /dev/null +++ b/cli/internal/upgrade/main/BUILD.bazel @@ -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__"], +) diff --git a/cli/internal/upgrade/main/main b/cli/internal/upgrade/main/main new file mode 100755 index 000000000..cd4203303 Binary files /dev/null and b/cli/internal/upgrade/main/main differ diff --git a/cli/internal/upgrade/main/main.go b/cli/internal/upgrade/main/main.go new file mode 100644 index 000000000..ab16ebfdd --- /dev/null +++ b/cli/internal/upgrade/main/main.go @@ -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) +} diff --git a/cli/internal/upgrade/terraform.go b/cli/internal/upgrade/terraform.go index b14b2c28f..fe1614cc7 100644 --- a/cli/internal/upgrade/terraform.go +++ b/cli/internal/upgrade/terraform.go @@ -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) diff --git a/internal/constants/constants.go b/internal/constants/constants.go index d9a37dd08..3040eb802 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -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.