diff --git a/cli/internal/cmd/iamupgradeapply.go b/cli/internal/cmd/iamupgradeapply.go index 1c694a9fa..2c3b1bd68 100644 --- a/cli/internal/cmd/iamupgradeapply.go +++ b/cli/internal/cmd/iamupgradeapply.go @@ -8,8 +8,6 @@ package cmd import ( "errors" "fmt" - "strings" - "time" "github.com/edgelesssys/constellation/v2/cli/internal/terraform" "github.com/edgelesssys/constellation/v2/cli/internal/upgrade" @@ -18,7 +16,6 @@ import ( "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" - "github.com/google/uuid" "github.com/spf13/afero" "github.com/spf13/cobra" ) @@ -70,7 +67,8 @@ func runIAMUpgradeApply(cmd *cobra.Command, _ []string) error { if err != nil { return err } - upgradeID := "iam-" + time.Now().Format("20060102150405") + "-" + strings.Split(uuid.New().String(), "-")[0] + + upgradeID := generateUpgradeID(upgradeCmdKindIAM) iamMigrateCmd, err := upgrade.NewIAMMigrateCmd(cmd.Context(), constants.TerraformIAMWorkingDir, constants.UpgradeDir, upgradeID, conf.GetProvider(), terraform.LogLevelDebug) if err != nil { return fmt.Errorf("setting up IAM migration command: %w", err) @@ -86,10 +84,10 @@ func runIAMUpgradeApply(cmd *cobra.Command, _ []string) error { if err != nil { return err } - err = migrator.applyMigration(cmd, constants.UpgradeDir, file.NewHandler(afero.NewOsFs()), iamMigrateCmd, yes) - if err != nil { + if err := migrator.applyMigration(cmd, constants.UpgradeDir, file.NewHandler(afero.NewOsFs()), iamMigrateCmd, yes); err != nil { return fmt.Errorf("applying IAM migration: %w", err) } + cmd.Println("IAM profile successfully applied.") return nil } diff --git a/cli/internal/cmd/upgrade.go b/cli/internal/cmd/upgrade.go index 9af5f3ccb..21addcb06 100644 --- a/cli/internal/cmd/upgrade.go +++ b/cli/internal/cmd/upgrade.go @@ -7,6 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only package cmd import ( + "strings" + "time" + + "github.com/google/uuid" "github.com/spf13/cobra" ) @@ -23,3 +27,31 @@ func NewUpgradeCmd() *cobra.Command { cmd.AddCommand(newUpgradeApplyCmd()) return cmd } + +// upgradeCmdKind is the kind of the upgrade command (check, apply). +type upgradeCmdKind int + +const ( + // upgradeCmdKindCheck corresponds to the upgrade check command. + upgradeCmdKindCheck upgradeCmdKind = iota + // upgradeCmdKindApply corresponds to the upgrade apply command. + upgradeCmdKindApply + // upgradeCmdKindIAM corresponds to the IAM upgrade command. + upgradeCmdKindIAM +) + +func generateUpgradeID(kind upgradeCmdKind) string { + upgradeID := time.Now().Format("20060102150405") + "-" + strings.Split(uuid.New().String(), "-")[0] + switch kind { + case upgradeCmdKindCheck: + // When performing an upgrade check, the upgrade directory will only be used temporarily to store the + // Terraform state. The directory is deleted after the check is finished. + // Therefore, add a tmp-suffix to the upgrade ID to indicate that the directory will be cleared after the check. + upgradeID = "upgrade-" + upgradeID + "-tmp" + case upgradeCmdKindApply: + upgradeID = "upgrade-" + upgradeID + case upgradeCmdKindIAM: + upgradeID = "iam-" + upgradeID + } + return upgradeID +} diff --git a/cli/internal/cmd/upgradeapply.go b/cli/internal/cmd/upgradeapply.go index 2775a058d..58a672f62 100644 --- a/cli/internal/cmd/upgradeapply.go +++ b/cli/internal/cmd/upgradeapply.go @@ -28,6 +28,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/kms/uri" + "github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl" "github.com/edgelesssys/constellation/v2/internal/versions" "github.com/rogpeppe/go-internal/diff" "github.com/spf13/afero" @@ -64,33 +65,54 @@ func runUpgradeApply(cmd *cobra.Command, _ []string) error { return fmt.Errorf("creating logger: %w", err) } defer log.Sync() - fileHandler := file.NewHandler(afero.NewOsFs()) - upgrader, err := kubernetes.NewUpgrader( - cmd.Context(), cmd.OutOrStdout(), - constants.UpgradeDir, constants.AdminConfFilename, - fileHandler, log, kubernetes.UpgradeCmdKindApply, - ) + fileHandler := file.NewHandler(afero.NewOsFs()) + upgradeID := generateUpgradeID(upgradeCmdKindApply) + + kubeUpgrader, err := kubernetes.NewUpgrader(cmd.OutOrStdout(), constants.AdminConfFilename, log) if err != nil { return err } + helmUpgrader, err := helm.NewUpgradeClient(kubectl.New(), constants.UpgradeDir, constants.AdminConfFilename, constants.HelmNamespace, log) + if err != nil { + return fmt.Errorf("setting up helm client: %w", err) + } + configFetcher := attestationconfigapi.NewFetcher() - tfClient, err := terraform.New(cmd.Context(), constants.TerraformWorkingDir) + + // Set up two Terraform clients. They need to be configured with different workspaces + // One for upgrading existing resources + tfUpgrader, err := terraform.New(cmd.Context(), filepath.Join(constants.UpgradeDir, upgradeID, constants.TerraformUpgradeWorkingDir)) + if err != nil { + return fmt.Errorf("setting up terraform client: %w", err) + } + // And one for showing existing resources + tfShower, err := terraform.New(cmd.Context(), constants.TerraformWorkingDir) if err != nil { return fmt.Errorf("setting up terraform client: %w", err) } - applyCmd := upgradeApplyCmd{upgrader: upgrader, log: log, configFetcher: configFetcher, clusterShower: tfClient, fileHandler: fileHandler} + applyCmd := upgradeApplyCmd{ + helmUpgrader: helmUpgrader, + kubeUpgrader: kubeUpgrader, + terraformUpgrader: upgrade.NewTerraformUpgrader(tfUpgrader, cmd.OutOrStdout(), fileHandler, upgradeID), + configFetcher: configFetcher, + clusterShower: tfShower, + fileHandler: fileHandler, + log: log, + } return applyCmd.upgradeApply(cmd) } type upgradeApplyCmd struct { - upgrader cloudUpgrader - configFetcher attestationconfigapi.Fetcher - clusterShower clusterShower - fileHandler file.Handler - log debugLog + helmUpgrader helmUpgrader + kubeUpgrader kubernetesUpgrader + terraformUpgrader terraformUpgrader + configFetcher attestationconfigapi.Fetcher + clusterShower clusterShower + fileHandler file.Handler + log debugLog } func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command) error { @@ -131,7 +153,7 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command) error { } if idFile.MeasurementSalt == nil { // TODO(elchead): remove after 2.10, since 2.9 does not yet save it in the idfile - measurementSalt, err := u.upgrader.GetMeasurementSalt(cmd.Context()) + measurementSalt, err := u.kubeUpgrader.GetMeasurementSalt(cmd.Context()) if err != nil { return fmt.Errorf("getting join-config: %w", err) } @@ -161,7 +183,7 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command) error { // - fallback endpoint // - custom (user-provided) endpoint sans := append([]string{idFile.IP, conf.CustomEndpoint}, idFile.APIServerCertSANs...) - if err := u.upgrader.ExtendClusterConfigCertSANs(cmd.Context(), sans); err != nil { + if err := u.kubeUpgrader.ExtendClusterConfigCertSANs(cmd.Context(), sans); err != nil { return fmt.Errorf("extending cert SANs: %w", err) } @@ -177,7 +199,7 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command) error { return fmt.Errorf("upgrading services: %w", err) } - err = u.upgrader.UpgradeNodeVersion(cmd.Context(), conf, flags.force) + err = u.kubeUpgrader.UpgradeNodeVersion(cmd.Context(), conf, flags.force) switch { case errors.Is(err, kubernetes.ErrInProgress): cmd.PrintErrln("Skipping image and Kubernetes upgrades. Another upgrade is in progress.") @@ -214,7 +236,7 @@ func (u *upgradeApplyCmd) migrateTerraform( ) (res terraform.ApplyOutput, err error) { u.log.Debugf("Planning Terraform migrations") - if err := u.upgrader.CheckTerraformMigrations(constants.UpgradeDir); err != nil { + if err := u.terraformUpgrader.CheckTerraformMigrations(constants.UpgradeDir); err != nil { return res, fmt.Errorf("checking workspace: %w", err) } @@ -242,7 +264,7 @@ func (u *upgradeApplyCmd) migrateTerraform( // u.upgrader.AddManualStateMigration(migration) // } - hasDiff, err := u.upgrader.PlanTerraformMigrations(cmd.Context(), opts) + hasDiff, err := u.terraformUpgrader.PlanTerraformMigrations(cmd.Context(), opts) if err != nil { return res, fmt.Errorf("planning terraform migrations: %w", err) } @@ -257,14 +279,14 @@ func (u *upgradeApplyCmd) migrateTerraform( } if !ok { cmd.Println("Aborting upgrade.") - if err := u.upgrader.CleanUpTerraformMigrations(constants.UpgradeDir); err != nil { + if err := u.terraformUpgrader.CleanUpTerraformMigrations(constants.UpgradeDir); err != nil { return res, fmt.Errorf("cleaning up workspace: %w", err) } return res, fmt.Errorf("aborted by user") } } u.log.Debugf("Applying Terraform migrations") - tfOutput, err := u.upgrader.ApplyTerraformMigrations(cmd.Context(), opts) + tfOutput, err := u.terraformUpgrader.ApplyTerraformMigrations(cmd.Context(), opts) if err != nil { return tfOutput, fmt.Errorf("applying terraform migrations: %w", err) } @@ -277,7 +299,9 @@ func (u *upgradeApplyCmd) migrateTerraform( cmd.Printf("Terraform migrations applied successfully and output written to: %s\n"+ "A backup of the pre-upgrade state has been written to: %s\n", - flags.pf.PrefixPath(constants.ClusterIDsFilename), flags.pf.PrefixPath(filepath.Join(opts.UpgradeWorkspace, u.upgrader.GetUpgradeID(), constants.TerraformUpgradeBackupDir))) + flags.pf.PrefixPath(constants.ClusterIDsFilename), + flags.pf.PrefixPath(filepath.Join(opts.UpgradeWorkspace, u.terraformUpgrader.UpgradeID(), constants.TerraformUpgradeBackupDir)), + ) } else { u.log.Debugf("No Terraform diff detected") } @@ -327,7 +351,7 @@ func validK8sVersion(cmd *cobra.Command, version string, yes bool) (validVersion // confirmIfUpgradeAttestConfigHasDiff checks if the locally configured measurements are different from the cluster's measurements. // If so the function will ask the user to confirm (if --yes is not set). func (u *upgradeApplyCmd) confirmIfUpgradeAttestConfigHasDiff(cmd *cobra.Command, newConfig config.AttestationCfg, flags upgradeApplyFlags) error { - clusterAttestationConfig, err := u.upgrader.GetClusterAttestationConfig(cmd.Context(), newConfig.GetVariant()) + clusterAttestationConfig, err := u.kubeUpgrader.GetClusterAttestationConfig(cmd.Context(), newConfig.GetVariant()) if err != nil { return fmt.Errorf("getting cluster attestation config: %w", err) } @@ -357,10 +381,10 @@ func (u *upgradeApplyCmd) confirmIfUpgradeAttestConfigHasDiff(cmd *cobra.Command } } // TODO(elchead): move this outside this function to remove the side effect. - if err := u.upgrader.BackupConfigMap(cmd.Context(), constants.JoinConfigMap); err != nil { + if err := u.kubeUpgrader.BackupConfigMap(cmd.Context(), constants.JoinConfigMap); err != nil { return fmt.Errorf("backing up join-config: %w", err) } - if err := u.upgrader.UpdateAttestationConfig(cmd.Context(), newConfig); err != nil { + if err := u.kubeUpgrader.UpdateAttestationConfig(cmd.Context(), newConfig); err != nil { return fmt.Errorf("updating attestation config: %w", err) } return nil @@ -375,7 +399,11 @@ func (u *upgradeApplyCmd) handleServiceUpgrade(cmd *cobra.Command, conf *config. if err != nil { return fmt.Errorf("getting service account URI: %w", err) } - err = u.upgrader.UpgradeHelmServices(cmd.Context(), conf, idFile, flags.upgradeTimeout, helm.DenyDestructive, flags.force, flags.conformance, flags.helmWaitMode, secret, serviceAccURI, validK8sVersion, tfOutput) + err = u.helmUpgrader.Upgrade( + cmd.Context(), conf, idFile, + flags.upgradeTimeout, helm.DenyDestructive, flags.force, u.terraformUpgrader.UpgradeID(), + flags.conformance, flags.helmWaitMode, secret, serviceAccURI, validK8sVersion, tfOutput, + ) if errors.Is(err, helm.ErrConfirmationMissing) { if !flags.yes { cmd.PrintErrln("WARNING: Upgrading cert-manager will destroy all custom resources you have manually created that are based on the current version of cert-manager.") @@ -388,7 +416,11 @@ func (u *upgradeApplyCmd) handleServiceUpgrade(cmd *cobra.Command, conf *config. return nil } } - err = u.upgrader.UpgradeHelmServices(cmd.Context(), conf, idFile, flags.upgradeTimeout, helm.AllowDestructive, flags.force, flags.conformance, flags.helmWaitMode, secret, serviceAccURI, validK8sVersion, tfOutput) + err = u.helmUpgrader.Upgrade( + cmd.Context(), conf, idFile, + flags.upgradeTimeout, helm.AllowDestructive, flags.force, u.terraformUpgrader.UpgradeID(), + flags.conformance, flags.helmWaitMode, secret, serviceAccURI, validK8sVersion, tfOutput, + ) } return err @@ -470,17 +502,27 @@ type upgradeApplyFlags struct { helmWaitMode helm.WaitMode } -type cloudUpgrader interface { +type kubernetesUpgrader interface { UpgradeNodeVersion(ctx context.Context, conf *config.Config, force bool) error - UpgradeHelmServices(ctx context.Context, config *config.Config, idFile clusterid.File, timeout time.Duration, allowDestructive bool, force bool, conformance bool, helmWaitMode helm.WaitMode, masterSecret uri.MasterSecret, serviceAccURI string, validK8sVersion versions.ValidK8sVersion, tfOutput terraform.ApplyOutput) error ExtendClusterConfigCertSANs(ctx context.Context, alternativeNames []string) error GetClusterAttestationConfig(ctx context.Context, variant variant.Variant) (config.AttestationCfg, error) UpdateAttestationConfig(ctx context.Context, newAttestConfig config.AttestationCfg) error GetMeasurementSalt(ctx context.Context) ([]byte, error) + BackupConfigMap(ctx context.Context, name string) error +} + +type helmUpgrader interface { + Upgrade( + ctx context.Context, config *config.Config, idFile clusterid.File, timeout time.Duration, + allowDestructive, force bool, upgradeID string, conformance bool, helmWaitMode helm.WaitMode, + masterSecret uri.MasterSecret, serviceAccURI string, validK8sVersion versions.ValidK8sVersion, tfOutput terraform.ApplyOutput, + ) error +} + +type terraformUpgrader interface { PlanTerraformMigrations(ctx context.Context, opts upgrade.TerraformUpgradeOptions) (bool, error) ApplyTerraformMigrations(ctx context.Context, opts upgrade.TerraformUpgradeOptions) (terraform.ApplyOutput, error) CheckTerraformMigrations(upgradeWorkspace string) error CleanUpTerraformMigrations(upgradeWorkspace string) error - GetUpgradeID() string - BackupConfigMap(ctx context.Context, name string) error + UpgradeID() string } diff --git a/cli/internal/cmd/upgradeapply_test.go b/cli/internal/cmd/upgradeapply_test.go index e38962695..fed612220 100644 --- a/cli/internal/cmd/upgradeapply_test.go +++ b/cli/internal/cmd/upgradeapply_test.go @@ -9,7 +9,6 @@ package cmd import ( "bytes" "context" - "errors" "testing" "time" @@ -32,87 +31,106 @@ import ( ) func TestUpgradeApply(t *testing.T) { - someErr := errors.New("some error") testCases := map[string]struct { - upgrader *stubUpgrader + helmUpgrader *stubHelmUpgrader + kubeUpgrader *stubKubernetesUpgrader + terraformUpgrader *stubTerraformUpgrader wantErr bool yesFlag bool dontWantJoinConfigBackup bool stdin string }{ "success": { - upgrader: &stubUpgrader{currentConfig: config.DefaultForAzureSEVSNP()}, - yesFlag: true, + kubeUpgrader: &stubKubernetesUpgrader{currentConfig: config.DefaultForAzureSEVSNP()}, + helmUpgrader: &stubHelmUpgrader{}, + terraformUpgrader: &stubTerraformUpgrader{}, + yesFlag: true, }, "nodeVersion some error": { - upgrader: &stubUpgrader{ + kubeUpgrader: &stubKubernetesUpgrader{ currentConfig: config.DefaultForAzureSEVSNP(), - nodeVersionErr: someErr, + nodeVersionErr: assert.AnError, }, - wantErr: true, - yesFlag: true, + helmUpgrader: &stubHelmUpgrader{}, + terraformUpgrader: &stubTerraformUpgrader{}, + wantErr: true, + yesFlag: true, }, "nodeVersion in progress error": { - upgrader: &stubUpgrader{ + kubeUpgrader: &stubKubernetesUpgrader{ currentConfig: config.DefaultForAzureSEVSNP(), nodeVersionErr: kubernetes.ErrInProgress, }, - yesFlag: true, + helmUpgrader: &stubHelmUpgrader{}, + terraformUpgrader: &stubTerraformUpgrader{}, + yesFlag: true, }, "helm other error": { - upgrader: &stubUpgrader{ + kubeUpgrader: &stubKubernetesUpgrader{ currentConfig: config.DefaultForAzureSEVSNP(), - helmErr: someErr, }, - wantErr: true, - yesFlag: true, + helmUpgrader: &stubHelmUpgrader{err: assert.AnError}, + terraformUpgrader: &stubTerraformUpgrader{}, + wantErr: true, + yesFlag: true, }, "check terraform error": { - upgrader: &stubUpgrader{ - currentConfig: config.DefaultForAzureSEVSNP(), - checkTerraformErr: someErr, + kubeUpgrader: &stubKubernetesUpgrader{ + currentConfig: config.DefaultForAzureSEVSNP(), }, - wantErr: true, - yesFlag: true, + helmUpgrader: &stubHelmUpgrader{}, + terraformUpgrader: &stubTerraformUpgrader{checkTerraformErr: assert.AnError}, + wantErr: true, + yesFlag: true, }, "abort": { - upgrader: &stubUpgrader{ + kubeUpgrader: &stubKubernetesUpgrader{ currentConfig: config.DefaultForAzureSEVSNP(), - terraformDiff: true, }, - wantErr: true, - stdin: "no\n", + helmUpgrader: &stubHelmUpgrader{}, + terraformUpgrader: &stubTerraformUpgrader{terraformDiff: true}, + wantErr: true, + stdin: "no\n", }, "clean terraform error": { - upgrader: &stubUpgrader{ - currentConfig: config.DefaultForAzureSEVSNP(), - cleanTerraformErr: someErr, + kubeUpgrader: &stubKubernetesUpgrader{ + currentConfig: config.DefaultForAzureSEVSNP(), + }, + helmUpgrader: &stubHelmUpgrader{}, + terraformUpgrader: &stubTerraformUpgrader{ + cleanTerraformErr: assert.AnError, terraformDiff: true, }, wantErr: true, stdin: "no\n", }, "plan terraform error": { - upgrader: &stubUpgrader{ - currentConfig: config.DefaultForAzureSEVSNP(), - planTerraformErr: someErr, + kubeUpgrader: &stubKubernetesUpgrader{ + currentConfig: config.DefaultForAzureSEVSNP(), }, - wantErr: true, - yesFlag: true, + helmUpgrader: &stubHelmUpgrader{}, + terraformUpgrader: &stubTerraformUpgrader{planTerraformErr: assert.AnError}, + wantErr: true, + yesFlag: true, }, "apply terraform error": { - upgrader: &stubUpgrader{ - currentConfig: config.DefaultForAzureSEVSNP(), - applyTerraformErr: someErr, + kubeUpgrader: &stubKubernetesUpgrader{ + currentConfig: config.DefaultForAzureSEVSNP(), + }, + helmUpgrader: &stubHelmUpgrader{}, + terraformUpgrader: &stubTerraformUpgrader{ + applyTerraformErr: assert.AnError, terraformDiff: true, }, wantErr: true, yesFlag: true, }, "do no backup join-config when remote attestation config is the same": { - upgrader: &stubUpgrader{ + kubeUpgrader: &stubKubernetesUpgrader{ currentConfig: fakeAzureAttestationConfigFromCluster(context.Background(), t, cloudprovider.Azure), }, + helmUpgrader: &stubHelmUpgrader{}, + terraformUpgrader: &stubTerraformUpgrader{}, yesFlag: true, dontWantJoinConfigBackup: true, }, @@ -141,7 +159,15 @@ func TestUpgradeApply(t *testing.T) { require.NoError(handler.WriteJSON(constants.ClusterIDsFilename, clusterid.File{})) require.NoError(handler.WriteJSON(constants.MasterSecretFilename, uri.MasterSecret{})) - upgrader := upgradeApplyCmd{upgrader: tc.upgrader, log: logger.NewTest(t), configFetcher: stubAttestationFetcher{}, clusterShower: &stubShowCluster{}, fileHandler: handler} + upgrader := upgradeApplyCmd{ + kubeUpgrader: tc.kubeUpgrader, + helmUpgrader: tc.helmUpgrader, + terraformUpgrader: tc.terraformUpgrader, + log: logger.NewTest(t), + configFetcher: stubAttestationFetcher{}, + clusterShower: &stubShowCluster{}, + fileHandler: handler, + } err := upgrader.upgradeApply(cmd) if tc.wantErr { @@ -149,70 +175,79 @@ func TestUpgradeApply(t *testing.T) { return } assert.NoError(err) - assert.Equal(!tc.dontWantJoinConfigBackup, tc.upgrader.backupWasCalled) + assert.Equal(!tc.dontWantJoinConfigBackup, tc.kubeUpgrader.backupWasCalled) }) } } -type stubUpgrader struct { - currentConfig config.AttestationCfg - nodeVersionErr error - helmErr error +type stubHelmUpgrader struct { + err error +} + +func (u stubHelmUpgrader) Upgrade( + _ context.Context, _ *config.Config, _ clusterid.File, _ time.Duration, _, _ bool, _ string, _ bool, + _ helm.WaitMode, _ uri.MasterSecret, _ string, _ versions.ValidK8sVersion, _ terraform.ApplyOutput, +) error { + return u.err +} + +type stubKubernetesUpgrader struct { + backupWasCalled bool + nodeVersionErr error + currentConfig config.AttestationCfg +} + +func (u stubKubernetesUpgrader) GetMeasurementSalt(_ context.Context) ([]byte, error) { + return []byte{}, nil +} + +func (u *stubKubernetesUpgrader) BackupConfigMap(_ context.Context, _ string) error { + u.backupWasCalled = true + return nil +} + +func (u stubKubernetesUpgrader) UpgradeNodeVersion(_ context.Context, _ *config.Config, _ bool) error { + return u.nodeVersionErr +} + +func (u stubKubernetesUpgrader) UpdateAttestationConfig(_ context.Context, _ config.AttestationCfg) error { + return nil +} + +func (u stubKubernetesUpgrader) GetClusterAttestationConfig(_ context.Context, _ variant.Variant) (config.AttestationCfg, error) { + return u.currentConfig, nil +} + +func (u stubKubernetesUpgrader) ExtendClusterConfigCertSANs(_ context.Context, _ []string) error { + return nil +} + +type stubTerraformUpgrader struct { terraformDiff bool planTerraformErr error checkTerraformErr error applyTerraformErr error cleanTerraformErr error - backupWasCalled bool } -func (u stubUpgrader) GetMeasurementSalt(_ context.Context) ([]byte, error) { - return []byte{}, nil -} - -func (u stubUpgrader) GetUpgradeID() string { - return "test-upgrade" -} - -func (u *stubUpgrader) BackupConfigMap(_ context.Context, _ string) error { - u.backupWasCalled = true - return nil -} - -func (u stubUpgrader) UpgradeNodeVersion(_ context.Context, _ *config.Config, _ bool) error { - return u.nodeVersionErr -} - -func (u stubUpgrader) UpgradeHelmServices(_ context.Context, _ *config.Config, _ clusterid.File, _ time.Duration, _, _, _ bool, _ helm.WaitMode, _ uri.MasterSecret, _ string, _ versions.ValidK8sVersion, _ terraform.ApplyOutput) error { - return u.helmErr -} - -func (u stubUpgrader) UpdateAttestationConfig(_ context.Context, _ config.AttestationCfg) error { - return nil -} - -func (u stubUpgrader) GetClusterAttestationConfig(_ context.Context, _ variant.Variant) (config.AttestationCfg, error) { - return u.currentConfig, nil -} - -func (u stubUpgrader) CheckTerraformMigrations(_ string) error { +func (u stubTerraformUpgrader) CheckTerraformMigrations(_ string) error { return u.checkTerraformErr } -func (u stubUpgrader) CleanUpTerraformMigrations(_ string) error { +func (u stubTerraformUpgrader) CleanUpTerraformMigrations(_ string) error { return u.cleanTerraformErr } -func (u stubUpgrader) PlanTerraformMigrations(context.Context, upgrade.TerraformUpgradeOptions) (bool, error) { +func (u stubTerraformUpgrader) PlanTerraformMigrations(context.Context, upgrade.TerraformUpgradeOptions) (bool, error) { return u.terraformDiff, u.planTerraformErr } -func (u stubUpgrader) ApplyTerraformMigrations(context.Context, upgrade.TerraformUpgradeOptions) (terraform.ApplyOutput, error) { +func (u stubTerraformUpgrader) ApplyTerraformMigrations(context.Context, upgrade.TerraformUpgradeOptions) (terraform.ApplyOutput, error) { return terraform.ApplyOutput{}, u.applyTerraformErr } -func (u stubUpgrader) ExtendClusterConfigCertSANs(_ context.Context, _ []string) error { - return nil +func (u stubTerraformUpgrader) UpgradeID() string { + return "test-upgrade" } func fakeAzureAttestationConfigFromCluster(ctx context.Context, t *testing.T, provider cloudprovider.Provider) config.AttestationCfg { diff --git a/cli/internal/cmd/upgradecheck.go b/cli/internal/cmd/upgradecheck.go index ee967bcce..a745e3cbc 100644 --- a/cli/internal/cmd/upgradecheck.go +++ b/cli/internal/cmd/upgradecheck.go @@ -12,6 +12,7 @@ import ( "fmt" "io" "net/http" + "path/filepath" "sort" "strings" @@ -71,11 +72,14 @@ func runUpgradeCheck(cmd *cobra.Command, _ []string) error { } fileHandler := file.NewHandler(afero.NewOsFs()) - checker, err := kubernetes.NewUpgrader( - cmd.Context(), cmd.OutOrStdout(), - constants.UpgradeDir, constants.AdminConfFilename, - fileHandler, log, kubernetes.UpgradeCmdKindCheck, - ) + upgradeID := generateUpgradeID(upgradeCmdKindCheck) + + tfClient, err := terraform.New(cmd.Context(), filepath.Join(constants.UpgradeDir, upgradeID, constants.TerraformUpgradeWorkingDir)) + if err != nil { + return fmt.Errorf("setting up terraform client: %w", err) + } + + kubeChecker, err := kubernetes.NewUpgrader(cmd.OutOrStdout(), constants.AdminConfFilename, log) if err != nil { return fmt.Errorf("setting up Kubernetes upgrader: %w", err) } @@ -89,7 +93,7 @@ func runUpgradeCheck(cmd *cobra.Command, _ []string) error { canUpgradeCheck: featureset.CanUpgradeCheck, collect: &versionCollector{ writer: cmd.OutOrStderr(), - checker: checker, + kubeChecker: kubeChecker, verListFetcher: versionfetcher, fileHandler: fileHandler, client: http.DefaultClient, @@ -99,8 +103,8 @@ func runUpgradeCheck(cmd *cobra.Command, _ []string) error { log: log, versionsapi: versionfetcher, }, - checker: checker, - log: log, + terraformChecker: upgrade.NewTerraformUpgrader(tfClient, cmd.OutOrStdout(), fileHandler, upgradeID), + log: log, } return up.upgradeCheck(cmd, fileHandler, attestationconfigapi.NewFetcher(), flags) @@ -143,10 +147,10 @@ func parseUpgradeCheckFlags(cmd *cobra.Command) (upgradeCheckFlags, error) { } type upgradeCheckCmd struct { - canUpgradeCheck bool - collect collector - checker upgradeChecker - log debugLog + canUpgradeCheck bool + collect collector + terraformChecker terraformChecker + log debugLog } // upgradePlan plans an upgrade of a Constellation cluster. @@ -212,7 +216,7 @@ func (u *upgradeCheckCmd) upgradeCheck(cmd *cobra.Command, fileHandler file.Hand // u.upgrader.AddManualStateMigration(migration) // } - if err := u.checker.CheckTerraformMigrations(constants.UpgradeDir); err != nil { + if err := u.terraformChecker.CheckTerraformMigrations(constants.UpgradeDir); err != nil { return fmt.Errorf("checking workspace: %w", err) } @@ -233,12 +237,12 @@ func (u *upgradeCheckCmd) upgradeCheck(cmd *cobra.Command, fileHandler file.Hand cmd.Println("The following Terraform migrations are available with this CLI:") // Check if there are any Terraform migrations - hasDiff, err := u.checker.PlanTerraformMigrations(cmd.Context(), opts) + hasDiff, err := u.terraformChecker.PlanTerraformMigrations(cmd.Context(), opts) if err != nil { return fmt.Errorf("planning terraform migrations: %w", err) } defer func() { - if err := u.checker.CleanUpTerraformMigrations(constants.UpgradeDir); err != nil { + if err := u.terraformChecker.CleanUpTerraformMigrations(constants.UpgradeDir); err != nil { u.log.Debugf("Failed to clean up Terraform migrations: %v", err) } }() @@ -323,7 +327,7 @@ type collector interface { type versionCollector struct { writer io.Writer - checker upgradeChecker + kubeChecker kubernetesChecker verListFetcher versionListFetcher fileHandler file.Handler client *http.Client @@ -382,12 +386,12 @@ func (v *versionCollector) currentVersions(ctx context.Context) (currentVersionI return currentVersionInfo{}, fmt.Errorf("getting service versions: %w", err) } - imageVersion, err := getCurrentImageVersion(ctx, v.checker) + imageVersion, err := getCurrentImageVersion(ctx, v.kubeChecker) if err != nil { return currentVersionInfo{}, fmt.Errorf("getting image version: %w", err) } - k8sVersion, err := getCurrentKubernetesVersion(ctx, v.checker) + k8sVersion, err := getCurrentKubernetesVersion(ctx, v.kubeChecker) if err != nil { return currentVersionInfo{}, fmt.Errorf("getting Kubernetes version: %w", err) } @@ -588,7 +592,7 @@ func (v *versionUpgrade) writeConfig(conf *config.Config, fileHandler file.Handl // getCurrentImageVersion retrieves the semantic version of the image currently installed in the cluster. // If the cluster is not using a release image, an error is returned. -func getCurrentImageVersion(ctx context.Context, checker upgradeChecker) (string, error) { +func getCurrentImageVersion(ctx context.Context, checker kubernetesChecker) (string, error) { imageVersion, err := checker.CurrentImage(ctx) if err != nil { return "", err @@ -602,7 +606,7 @@ func getCurrentImageVersion(ctx context.Context, checker upgradeChecker) (string } // getCurrentKubernetesVersion retrieves the semantic version of Kubernetes currently installed in the cluster. -func getCurrentKubernetesVersion(ctx context.Context, checker upgradeChecker) (string, error) { +func getCurrentKubernetesVersion(ctx context.Context, checker kubernetesChecker) (string, error) { k8sVersion, err := checker.CurrentKubernetesVersion(ctx) if err != nil { return "", err @@ -745,9 +749,12 @@ type upgradeCheckFlags struct { terraformLogLevel terraform.LogLevel } -type upgradeChecker interface { +type kubernetesChecker interface { CurrentImage(ctx context.Context) (string, error) CurrentKubernetesVersion(ctx context.Context) (string, error) +} + +type terraformChecker interface { PlanTerraformMigrations(ctx context.Context, opts upgrade.TerraformUpgradeOptions) (bool, error) CheckTerraformMigrations(upgradeWorkspace string) error CleanUpTerraformMigrations(upgradeWorkspace string) error diff --git a/cli/internal/cmd/upgradecheck_test.go b/cli/internal/cmd/upgradecheck_test.go index b5445641e..e10d84dc5 100644 --- a/cli/internal/cmd/upgradecheck_test.go +++ b/cli/internal/cmd/upgradecheck_test.go @@ -110,22 +110,22 @@ func TestBuildString(t *testing.T) { func TestGetCurrentImageVersion(t *testing.T) { testCases := map[string]struct { - stubUpgradeChecker stubUpgradeChecker - wantErr bool + stubKubernetesChecker stubKubernetesChecker + wantErr bool }{ "valid version": { - stubUpgradeChecker: stubUpgradeChecker{ + stubKubernetesChecker: stubKubernetesChecker{ image: "v1.0.0", }, }, "invalid version": { - stubUpgradeChecker: stubUpgradeChecker{ + stubKubernetesChecker: stubKubernetesChecker{ image: "invalid", }, wantErr: true, }, "GetCurrentImage error": { - stubUpgradeChecker: stubUpgradeChecker{ + stubKubernetesChecker: stubKubernetesChecker{ err: errors.New("error"), }, wantErr: true, @@ -136,7 +136,7 @@ func TestGetCurrentImageVersion(t *testing.T) { t.Run(name, func(t *testing.T) { assert := assert.New(t) - version, err := getCurrentImageVersion(context.Background(), tc.stubUpgradeChecker) + version, err := getCurrentImageVersion(context.Background(), tc.stubKubernetesChecker) if tc.wantErr { assert.Error(err) return @@ -215,19 +215,19 @@ func TestUpgradeCheck(t *testing.T) { testCases := map[string]struct { collector stubVersionCollector csp cloudprovider.Provider - checker stubUpgradeChecker + checker stubTerraformChecker cliVersion string wantError bool }{ "upgrades gcp": { collector: collector, - checker: stubUpgradeChecker{}, + checker: stubTerraformChecker{}, csp: cloudprovider.GCP, cliVersion: "v1.0.0", }, "terraform err": { collector: collector, - checker: stubUpgradeChecker{ + checker: stubTerraformChecker{ err: assert.AnError, }, csp: cloudprovider.GCP, @@ -245,10 +245,10 @@ func TestUpgradeCheck(t *testing.T) { require.NoError(fileHandler.WriteYAML(constants.ConfigFilename, cfg)) checkCmd := upgradeCheckCmd{ - canUpgradeCheck: true, - collect: &tc.collector, - checker: tc.checker, - log: logger.NewTest(t), + canUpgradeCheck: true, + collect: &tc.collector, + terraformChecker: tc.checker, + log: logger.NewTest(t), } cmd := newUpgradeCheckCmd() @@ -317,31 +317,35 @@ func (s *stubVersionCollector) filterCompatibleCLIVersions(_ context.Context, _ return s.newCompatibleCLIVersionsList, nil } -type stubUpgradeChecker struct { +type stubKubernetesChecker struct { image string k8sVersion string - tfDiff bool err error } -func (u stubUpgradeChecker) CurrentImage(context.Context) (string, error) { - return u.image, u.err +func (s stubKubernetesChecker) CurrentImage(context.Context) (string, error) { + return s.image, s.err } -func (u stubUpgradeChecker) CurrentKubernetesVersion(context.Context) (string, error) { - return u.k8sVersion, u.err +func (s stubKubernetesChecker) CurrentKubernetesVersion(context.Context) (string, error) { + return s.k8sVersion, s.err } -func (u stubUpgradeChecker) PlanTerraformMigrations(context.Context, upgrade.TerraformUpgradeOptions) (bool, error) { - return u.tfDiff, u.err +type stubTerraformChecker struct { + tfDiff bool + err error } -func (u stubUpgradeChecker) CheckTerraformMigrations(_ string) error { - return u.err +func (s stubTerraformChecker) PlanTerraformMigrations(context.Context, upgrade.TerraformUpgradeOptions) (bool, error) { + return s.tfDiff, s.err } -func (u stubUpgradeChecker) CleanUpTerraformMigrations(_ string) error { - return u.err +func (s stubTerraformChecker) CheckTerraformMigrations(_ string) error { + return s.err +} + +func (s stubTerraformChecker) CleanUpTerraformMigrations(_ string) error { + return s.err } func TestNewCLIVersions(t *testing.T) { diff --git a/cli/internal/kubernetes/BUILD.bazel b/cli/internal/kubernetes/BUILD.bazel index 62fc2180a..19ffe7bd8 100644 --- a/cli/internal/kubernetes/BUILD.bazel +++ b/cli/internal/kubernetes/BUILD.bazel @@ -11,25 +11,17 @@ go_library( importpath = "github.com/edgelesssys/constellation/v2/cli/internal/kubernetes", visibility = ["//cli:__subpackages__"], deps = [ - "//cli/internal/clusterid", - "//cli/internal/helm", - "//cli/internal/terraform", - "//cli/internal/upgrade", "//internal/api/versionsapi", "//internal/attestation/variant", "//internal/cloud/cloudprovider", "//internal/compatibility", "//internal/config", "//internal/constants", - "//internal/file", "//internal/imagefetcher", - "//internal/kms/uri", "//internal/kubernetes", - "//internal/kubernetes/kubectl", "//internal/versions", "//internal/versions/components", "//operators/constellation-node-operator/api/v1alpha1", - "@com_github_google_uuid//:uuid", "@io_k8s_api//core/v1:core", "@io_k8s_apimachinery//pkg/api/errors", "@io_k8s_apimachinery//pkg/apis/meta/v1:meta", diff --git a/cli/internal/kubernetes/upgrade.go b/cli/internal/kubernetes/upgrade.go index a04b79e0f..d8dd7688c 100644 --- a/cli/internal/kubernetes/upgrade.go +++ b/cli/internal/kubernetes/upgrade.go @@ -12,30 +12,20 @@ import ( "errors" "fmt" "io" - "path/filepath" "sort" "strings" - "time" - "github.com/edgelesssys/constellation/v2/cli/internal/clusterid" - "github.com/edgelesssys/constellation/v2/cli/internal/helm" - "github.com/edgelesssys/constellation/v2/cli/internal/terraform" - "github.com/edgelesssys/constellation/v2/cli/internal/upgrade" "github.com/edgelesssys/constellation/v2/internal/api/versionsapi" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/compatibility" "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/constants" - "github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/imagefetcher" - "github.com/edgelesssys/constellation/v2/internal/kms/uri" internalk8s "github.com/edgelesssys/constellation/v2/internal/kubernetes" - "github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl" "github.com/edgelesssys/constellation/v2/internal/versions" "github.com/edgelesssys/constellation/v2/internal/versions/components" updatev1alpha1 "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/api/v1alpha1" - "github.com/google/uuid" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -49,16 +39,6 @@ import ( "sigs.k8s.io/yaml" ) -// UpgradeCmdKind is the kind of the upgrade command (check, apply). -type UpgradeCmdKind int - -const ( - // UpgradeCmdKindCheck corresponds to the upgrade check command. - UpgradeCmdKindCheck UpgradeCmdKind = iota - // UpgradeCmdKindApply corresponds to the upgrade apply command. - UpgradeCmdKindApply -) - // ErrInProgress signals that an upgrade is in progress inside the cluster. var ErrInProgress = errors.New("upgrade in progress") @@ -91,69 +71,36 @@ func (e *applyError) Error() string { type Upgrader struct { stableInterface StableInterface dynamicInterface DynamicInterface - helmClient helmInterface imageFetcher imageFetcher outWriter io.Writer - tfUpgrader *upgrade.TerraformUpgrader log debugLog - upgradeID string } // NewUpgrader returns a new Upgrader. -func NewUpgrader( - ctx context.Context, outWriter io.Writer, upgradeWorkspace, kubeConfigPath string, - fileHandler file.Handler, log debugLog, upgradeCmdKind UpgradeCmdKind, -) (*Upgrader, error) { - upgradeID := "upgrade-" + time.Now().Format("20060102150405") + "-" + strings.Split(uuid.New().String(), "-")[0] - if upgradeCmdKind == UpgradeCmdKindCheck { - // When performing an upgrade check, the upgrade directory will only be used temporarily to store the - // Terraform state. The directory is deleted after the check is finished. - // Therefore, add a tmp-suffix to the upgrade ID to indicate that the directory will be cleared after the check. - upgradeID += "-tmp" - } - - u := &Upgrader{ - imageFetcher: imagefetcher.New(), - outWriter: outWriter, - log: log, - upgradeID: upgradeID, - } - +func NewUpgrader(outWriter io.Writer, kubeConfigPath string, log debugLog) (*Upgrader, error) { kubeClient, err := newClient(kubeConfigPath) if err != nil { return nil, err } - u.stableInterface = &stableClient{client: kubeClient} // use unstructured client to avoid importing the operator packages kubeConfig, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath) if err != nil { return nil, fmt.Errorf("building kubernetes config: %w", err) } + unstructuredClient, err := dynamic.NewForConfig(kubeConfig) if err != nil { return nil, fmt.Errorf("setting up custom resource client: %w", err) } - u.dynamicInterface = &NodeVersionClient{client: unstructuredClient} - helmClient, err := helm.NewUpgradeClient(kubectl.New(), upgradeWorkspace, kubeConfigPath, constants.HelmNamespace, log) - if err != nil { - return nil, fmt.Errorf("setting up helm client: %w", err) - } - u.helmClient = helmClient - - tfClient, err := terraform.New(ctx, filepath.Join(upgradeWorkspace, upgradeID, constants.TerraformUpgradeWorkingDir)) - if err != nil { - return nil, fmt.Errorf("setting up terraform client: %w", err) - } - - tfUpgrader, err := upgrade.NewTerraformUpgrader(tfClient, outWriter, fileHandler) - if err != nil { - return nil, fmt.Errorf("setting up terraform upgrader: %w", err) - } - u.tfUpgrader = tfUpgrader - - return u, nil + return &Upgrader{ + imageFetcher: imagefetcher.New(), + outWriter: outWriter, + log: log, + stableInterface: &stableClient{client: kubeClient}, + dynamicInterface: &NodeVersionClient{client: unstructuredClient}, + }, nil } // GetMeasurementSalt returns the measurementSalt from the join-config. @@ -169,47 +116,6 @@ func (u *Upgrader) GetMeasurementSalt(ctx context.Context) ([]byte, error) { return salt, nil } -// GetUpgradeID returns the upgrade ID. -func (u *Upgrader) GetUpgradeID() string { - return u.upgradeID -} - -// CheckTerraformMigrations checks whether Terraform migrations are possible in the current workspace. -// If the files that will be written during the upgrade already exist, it returns an error. -func (u *Upgrader) CheckTerraformMigrations(upgradeWorkspace string) error { - return u.tfUpgrader.CheckTerraformMigrations(upgradeWorkspace, u.upgradeID, constants.TerraformUpgradeBackupDir) -} - -// CleanUpTerraformMigrations cleans up the Terraform migration workspace, for example when an upgrade is -// aborted by the user. -func (u *Upgrader) CleanUpTerraformMigrations(upgradeWorkspace string) error { - return u.tfUpgrader.CleanUpTerraformMigrations(upgradeWorkspace, u.upgradeID) -} - -// 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. -func (u *Upgrader) PlanTerraformMigrations(ctx context.Context, opts upgrade.TerraformUpgradeOptions) (bool, error) { - return u.tfUpgrader.PlanTerraformMigrations(ctx, opts, u.upgradeID) -} - -// 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. -func (u *Upgrader) ApplyTerraformMigrations(ctx context.Context, opts upgrade.TerraformUpgradeOptions) (terraform.ApplyOutput, error) { - return u.tfUpgrader.ApplyTerraformMigrations(ctx, opts, u.upgradeID) -} - -// UpgradeHelmServices upgrade helm services. -func (u *Upgrader) UpgradeHelmServices(ctx context.Context, config *config.Config, idFile clusterid.File, timeout time.Duration, - allowDestructive bool, force bool, conformance bool, helmWaitMode helm.WaitMode, masterSecret uri.MasterSecret, serviceAccURI string, - validK8sVersion versions.ValidK8sVersion, output terraform.ApplyOutput, -) error { - return u.helmClient.Upgrade(ctx, config, idFile, timeout, allowDestructive, force, u.upgradeID, conformance, - helmWaitMode, masterSecret, serviceAccURI, validK8sVersion, output) -} - // UpgradeNodeVersion upgrades the cluster's NodeVersion object and in turn triggers image & k8s version upgrades. // The versions set in the config are validated against the versions running in the cluster. func (u *Upgrader) UpgradeNodeVersion(ctx context.Context, conf *config.Config, force bool) error { @@ -569,10 +475,6 @@ func upgradeInProgress(nodeVersion updatev1alpha1.NodeVersion) bool { return false } -type helmInterface interface { - Upgrade(ctx context.Context, config *config.Config, idFile clusterid.File, timeout time.Duration, allowDestructive, force bool, upgradeID string, conformance bool, helmWaitMode helm.WaitMode, masterSecret uri.MasterSecret, serviceAccURI string, validK8sVersion versions.ValidK8sVersion, output terraform.ApplyOutput) error -} - type debugLog interface { Debugf(format string, args ...any) Sync() diff --git a/cli/internal/upgrade/terraform.go b/cli/internal/upgrade/terraform.go index 8484275b9..cf0745a4b 100644 --- a/cli/internal/upgrade/terraform.go +++ b/cli/internal/upgrade/terraform.go @@ -21,24 +21,6 @@ import ( "github.com/edgelesssys/constellation/v2/internal/file" ) -// NewTerraformUpgrader returns a new TerraformUpgrader. -func NewTerraformUpgrader(tfClient tfResourceClient, outWriter io.Writer, fileHandler file.Handler) (*TerraformUpgrader, error) { - return &TerraformUpgrader{ - tf: tfClient, - policyPatcher: cloudcmd.NewAzurePolicyPatcher(), - outWriter: outWriter, - fileHandler: fileHandler, - }, nil -} - -// TerraformUpgrader is responsible for performing Terraform migrations on cluster upgrades. -type TerraformUpgrader struct { - tf tfResourceClient - policyPatcher policyPatcher - outWriter io.Writer - fileHandler file.Handler -} - // TerraformUpgradeOptions are the options used for the Terraform upgrade. type TerraformUpgradeOptions struct { // LogLevel is the log level used for Terraform. @@ -51,55 +33,43 @@ type TerraformUpgradeOptions struct { UpgradeWorkspace string } -// CheckTerraformMigrations checks whether Terraform migrations are possible in the current workspace. -func checkTerraformMigrations(file file.Handler, upgradeWorkspace, upgradeID, upgradeSubDir string) error { - var existingFiles []string - filesToCheck := []string{ - filepath.Join(upgradeWorkspace, upgradeID, upgradeSubDir), - } +// TerraformUpgrader is responsible for performing Terraform migrations on cluster upgrades. +type TerraformUpgrader struct { + tf tfResourceClient + policyPatcher policyPatcher + outWriter io.Writer + fileHandler file.Handler + upgradeID string +} - for _, f := range filesToCheck { - if err := checkFileExists(file, &existingFiles, f); err != nil { - return fmt.Errorf("checking terraform migrations: %w", err) - } +// NewTerraformUpgrader returns a new TerraformUpgrader. +func NewTerraformUpgrader(tfClient tfResourceClient, outWriter io.Writer, fileHandler file.Handler, upgradeID string, +) *TerraformUpgrader { + return &TerraformUpgrader{ + tf: tfClient, + policyPatcher: cloudcmd.NewAzurePolicyPatcher(), + outWriter: outWriter, + fileHandler: fileHandler, + upgradeID: upgradeID, } - - if len(existingFiles) > 0 { - return fmt.Errorf("file(s) %s already exist", strings.Join(existingFiles, ", ")) - } - return nil } // CheckTerraformMigrations checks whether Terraform migrations are possible in the current workspace. // If the files that will be written during the upgrade already exist, it returns an error. -func (u *TerraformUpgrader) CheckTerraformMigrations(upgradeWorkspace, upgradeID, upgradeSubDir string) error { - return checkTerraformMigrations(u.fileHandler, upgradeWorkspace, upgradeID, upgradeSubDir) -} - -// checkFileExists checks whether a file exists and adds it to the existingFiles slice if it does. -func checkFileExists(fileHandler file.Handler, existingFiles *[]string, filename string) error { - _, err := fileHandler.Stat(filename) - if err != nil { - if !os.IsNotExist(err) { - return fmt.Errorf("checking %s: %w", filename, err) - } - return nil - } - - *existingFiles = append(*existingFiles, filename) - return nil +func (u *TerraformUpgrader) CheckTerraformMigrations(upgradeWorkspace string) error { + return checkTerraformMigrations(u.fileHandler, upgradeWorkspace, u.upgradeID, constants.TerraformUpgradeBackupDir) } // 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. -func (u *TerraformUpgrader) PlanTerraformMigrations(ctx context.Context, opts TerraformUpgradeOptions, upgradeID string) (bool, error) { +func (u *TerraformUpgrader) PlanTerraformMigrations(ctx context.Context, opts TerraformUpgradeOptions) (bool, error) { // Prepare the new Terraform workspace and backup the old one err := u.tf.PrepareUpgradeWorkspace( filepath.Join("terraform", strings.ToLower(opts.CSP.String())), opts.TFWorkspace, - filepath.Join(opts.UpgradeWorkspace, upgradeID, constants.TerraformUpgradeWorkingDir), - filepath.Join(opts.UpgradeWorkspace, upgradeID, constants.TerraformUpgradeBackupDir), + filepath.Join(opts.UpgradeWorkspace, u.upgradeID, constants.TerraformUpgradeWorkingDir), + filepath.Join(opts.UpgradeWorkspace, u.upgradeID, constants.TerraformUpgradeBackupDir), opts.Vars, ) if err != nil { @@ -122,24 +92,15 @@ func (u *TerraformUpgrader) PlanTerraformMigrations(ctx context.Context, opts Te // CleanUpTerraformMigrations cleans up the Terraform migration workspace, for example when an upgrade is // aborted by the user. -func (u *TerraformUpgrader) CleanUpTerraformMigrations(upgradeWorkspace, upgradeID string) error { - return CleanUpTerraformMigrations(upgradeWorkspace, upgradeID, u.fileHandler) -} - -// CleanUpTerraformMigrations cleans up the Terraform upgrade directory. -func CleanUpTerraformMigrations(upgradeWorkspace, upgradeID string, fileHandler file.Handler) error { - upgradeDir := filepath.Join(upgradeWorkspace, upgradeID) - if err := fileHandler.RemoveAll(upgradeDir); err != nil { - return fmt.Errorf("cleaning up file %s: %w", upgradeDir, err) - } - return nil +func (u *TerraformUpgrader) CleanUpTerraformMigrations(upgradeWorkspace string) error { + return CleanUpTerraformMigrations(upgradeWorkspace, u.upgradeID, u.fileHandler) } // 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. -func (u *TerraformUpgrader) ApplyTerraformMigrations(ctx context.Context, opts TerraformUpgradeOptions, upgradeID string) (terraform.ApplyOutput, error) { +func (u *TerraformUpgrader) ApplyTerraformMigrations(ctx context.Context, opts TerraformUpgradeOptions) (terraform.ApplyOutput, error) { tfOutput, err := u.tf.CreateCluster(ctx, opts.CSP, opts.LogLevel) if err != nil { return tfOutput, fmt.Errorf("terraform apply: %w", err) @@ -154,17 +115,64 @@ func (u *TerraformUpgrader) ApplyTerraformMigrations(ctx context.Context, opts T } if err := u.fileHandler.CopyDir( - filepath.Join(opts.UpgradeWorkspace, upgradeID, constants.TerraformUpgradeWorkingDir), + filepath.Join(opts.UpgradeWorkspace, u.upgradeID, constants.TerraformUpgradeWorkingDir), opts.TFWorkspace, ); err != nil { return tfOutput, fmt.Errorf("replacing old terraform directory with new one: %w", err) } - if err := u.fileHandler.RemoveAll(filepath.Join(opts.UpgradeWorkspace, upgradeID, constants.TerraformUpgradeWorkingDir)); err != nil { + if err := u.fileHandler.RemoveAll(filepath.Join(opts.UpgradeWorkspace, u.upgradeID, constants.TerraformUpgradeWorkingDir)); err != nil { return tfOutput, fmt.Errorf("removing terraform upgrade directory: %w", err) } return tfOutput, nil } +// UpgradeID returns the ID of the upgrade. +func (u *TerraformUpgrader) UpgradeID() string { + return u.upgradeID +} + +// CleanUpTerraformMigrations cleans up the Terraform upgrade directory. +func CleanUpTerraformMigrations(upgradeWorkspace, upgradeID string, fileHandler file.Handler) error { + upgradeDir := filepath.Join(upgradeWorkspace, upgradeID) + if err := fileHandler.RemoveAll(upgradeDir); err != nil { + return fmt.Errorf("cleaning up file %s: %w", upgradeDir, err) + } + return nil +} + +// CheckTerraformMigrations checks whether Terraform migrations are possible in the current workspace. +func checkTerraformMigrations(file file.Handler, upgradeWorkspace, upgradeID, upgradeSubDir string) error { + var existingFiles []string + filesToCheck := []string{ + filepath.Join(upgradeWorkspace, upgradeID, upgradeSubDir), + } + + for _, f := range filesToCheck { + if err := checkFileExists(file, &existingFiles, f); err != nil { + return fmt.Errorf("checking terraform migrations: %w", err) + } + } + + if len(existingFiles) > 0 { + return fmt.Errorf("file(s) %s already exist", strings.Join(existingFiles, ", ")) + } + return nil +} + +// checkFileExists checks whether a file exists and adds it to the existingFiles slice if it does. +func checkFileExists(fileHandler file.Handler, existingFiles *[]string, filename string) error { + _, err := fileHandler.Stat(filename) + if err != nil { + if !os.IsNotExist(err) { + return fmt.Errorf("checking %s: %w", filename, err) + } + return nil + } + + *existingFiles = append(*existingFiles, filename) + return nil +} + type tfClientCommon interface { ShowPlan(ctx context.Context, logLevel terraform.LogLevel, output io.Writer) error Plan(ctx context.Context, logLevel terraform.LogLevel) (bool, error) diff --git a/cli/internal/upgrade/terraform_test.go b/cli/internal/upgrade/terraform_test.go index 515e6cf89..a985cdb0c 100644 --- a/cli/internal/upgrade/terraform_test.go +++ b/cli/internal/upgrade/terraform_test.go @@ -23,13 +23,6 @@ import ( ) func TestCheckTerraformMigrations(t *testing.T) { - upgrader := func(fileHandler file.Handler) *TerraformUpgrader { - u, err := NewTerraformUpgrader(&stubTerraformClient{}, bytes.NewBuffer(nil), fileHandler) - require.NoError(t, err) - - return u - } - workspace := func(existingFiles []string) file.Handler { fs := afero.NewMemMapFs() for _, f := range existingFiles { @@ -57,8 +50,9 @@ func TestCheckTerraformMigrations(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { - u := upgrader(tc.workspace) - err := u.CheckTerraformMigrations(constants.UpgradeDir, tc.upgradeID, constants.TerraformUpgradeBackupDir) + u := NewTerraformUpgrader(&stubTerraformClient{}, bytes.NewBuffer(nil), tc.workspace, tc.upgradeID) + + err := u.CheckTerraformMigrations(constants.UpgradeDir) if tc.wantErr { require.Error(t, err) return @@ -70,12 +64,6 @@ func TestCheckTerraformMigrations(t *testing.T) { } func TestPlanTerraformMigrations(t *testing.T) { - upgrader := func(tf tfResourceClient, fileHandler file.Handler) *TerraformUpgrader { - u, err := NewTerraformUpgrader(tf, bytes.NewBuffer(nil), fileHandler) - require.NoError(t, err) - - return u - } workspace := func(existingFiles []string) file.Handler { fs := afero.NewMemMapFs() for _, f := range existingFiles { @@ -142,7 +130,7 @@ func TestPlanTerraformMigrations(t *testing.T) { t.Run(name, func(t *testing.T) { require := require.New(t) - u := upgrader(tc.tf, tc.workspace) + u := NewTerraformUpgrader(tc.tf, bytes.NewBuffer(nil), tc.workspace, tc.upgradeID) opts := TerraformUpgradeOptions{ LogLevel: terraform.LogLevelDebug, @@ -150,7 +138,7 @@ func TestPlanTerraformMigrations(t *testing.T) { Vars: &terraform.QEMUVariables{}, } - diff, err := u.PlanTerraformMigrations(context.Background(), opts, tc.upgradeID) + diff, err := u.PlanTerraformMigrations(context.Background(), opts) if tc.wantErr { require.Error(err) } else { @@ -162,13 +150,6 @@ func TestPlanTerraformMigrations(t *testing.T) { } func TestApplyTerraformMigrations(t *testing.T) { - upgrader := func(tf tfResourceClient, fileHandler file.Handler) *TerraformUpgrader { - u, err := NewTerraformUpgrader(tf, bytes.NewBuffer(nil), fileHandler) - require.NoError(t, err) - - return u - } - fileHandler := func(upgradeID string, existingFiles ...string) file.Handler { fh := file.NewHandler(afero.NewMemMapFs()) @@ -211,7 +192,7 @@ func TestApplyTerraformMigrations(t *testing.T) { t.Run(name, func(t *testing.T) { require := require.New(t) - u := upgrader(tc.tf, tc.fs) + u := NewTerraformUpgrader(tc.tf, bytes.NewBuffer(nil), tc.fs, tc.upgradeID) opts := TerraformUpgradeOptions{ LogLevel: terraform.LogLevelDebug, @@ -221,7 +202,7 @@ func TestApplyTerraformMigrations(t *testing.T) { UpgradeWorkspace: constants.UpgradeDir, } - _, err := u.ApplyTerraformMigrations(context.Background(), opts, tc.upgradeID) + _, err := u.ApplyTerraformMigrations(context.Background(), opts) if tc.wantErr { require.Error(err) } else { @@ -232,13 +213,6 @@ func TestApplyTerraformMigrations(t *testing.T) { } func TestCleanUpTerraformMigrations(t *testing.T) { - upgrader := func(fileHandler file.Handler) *TerraformUpgrader { - u, err := NewTerraformUpgrader(&stubTerraformClient{}, bytes.NewBuffer(nil), fileHandler) - require.NoError(t, err) - - return u - } - workspace := func(existingFiles []string) file.Handler { fs := afero.NewMemMapFs() for _, f := range existingFiles { @@ -299,9 +273,9 @@ func TestCleanUpTerraformMigrations(t *testing.T) { require := require.New(t) workspace := workspace(tc.workspaceFiles) - u := upgrader(workspace) + u := NewTerraformUpgrader(&stubTerraformClient{}, bytes.NewBuffer(nil), workspace, tc.upgradeID) - err := u.CleanUpTerraformMigrations(constants.UpgradeDir, tc.upgradeID) + err := u.CleanUpTerraformMigrations(constants.UpgradeDir) if tc.wantErr { require.Error(err) return