cli: write Terraform migration output directly to constellation-id.json (#2107)

* backup `constellation-id.json` before upgrade

* remove superfluous `file.Handler` arguments

* merge `constellation-id.json` on upgrade

* fix typo
This commit is contained in:
Moritz Sanft 2023-07-18 09:33:42 +02:00 committed by GitHub
parent 5cbdb3a519
commit 5f71934f56
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 255 additions and 120 deletions

View file

@ -1,4 +1,5 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library") load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("//bazel/go:go_test.bzl", "go_test")
go_library( go_library(
name = "clusterid", name = "clusterid",
@ -7,3 +8,10 @@ go_library(
visibility = ["//cli:__subpackages__"], visibility = ["//cli:__subpackages__"],
deps = ["//internal/cloud/cloudprovider"], deps = ["//internal/cloud/cloudprovider"],
) )
go_test(
name = "clusterid_test",
srcs = ["id_test.go"],
embed = [":clusterid"],
deps = ["@com_github_stretchr_testify//require"],
)

View file

@ -28,3 +28,38 @@ type File struct {
// It is only set if the cluster is created on Azure. // It is only set if the cluster is created on Azure.
AttestationURL string `json:"attestationURL,omitempty"` AttestationURL string `json:"attestationURL,omitempty"`
} }
// Merge merges the other file into the current file and returns the result.
// If a field is set in both files, the value of the other file is used.
// This does in-place changes on the current file.
func (f *File) Merge(other File) *File {
if other.ClusterID != "" {
f.ClusterID = other.ClusterID
}
if other.OwnerID != "" {
f.OwnerID = other.OwnerID
}
if other.UID != "" {
f.UID = other.UID
}
if other.CloudProvider != cloudprovider.Unknown {
f.CloudProvider = other.CloudProvider
}
if other.IP != "" {
f.IP = other.IP
}
if other.InitSecret != nil {
f.InitSecret = other.InitSecret
}
if other.AttestationURL != "" {
f.AttestationURL = other.AttestationURL
}
return f
}

View file

@ -0,0 +1,63 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package clusterid
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestMerge(t *testing.T) {
testCases := map[string]struct {
current File
other File
want File
}{
"empty": {
current: File{},
other: File{},
want: File{},
},
"current empty": {
current: File{},
other: File{
ClusterID: "clusterID",
},
want: File{
ClusterID: "clusterID",
},
},
"other empty": {
current: File{
ClusterID: "clusterID",
},
other: File{},
want: File{
ClusterID: "clusterID",
},
},
"both set": {
current: File{
ClusterID: "clusterID",
},
other: File{
ClusterID: "otherClusterID",
},
want: File{
ClusterID: "otherClusterID",
},
},
}
for _, tc := range testCases {
require := require.New(t)
ret := tc.current.Merge(tc.other)
require.Equal(tc.want, *ret)
}
}

View file

@ -63,7 +63,7 @@ func runUpgradeApply(cmd *cobra.Command, _ []string) error {
defer log.Sync() defer log.Sync()
fileHandler := file.NewHandler(afero.NewOsFs()) fileHandler := file.NewHandler(afero.NewOsFs())
upgrader, err := kubernetes.NewUpgrader(cmd.Context(), cmd.OutOrStdout(), log, kubernetes.UpgradeCmdKindApply) upgrader, err := kubernetes.NewUpgrader(cmd.Context(), cmd.OutOrStdout(), fileHandler, log, kubernetes.UpgradeCmdKindApply)
if err != nil { if err != nil {
return err return err
} }
@ -146,7 +146,7 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command, fileHandler file.Hand
func (u *upgradeApplyCmd) migrateTerraform(cmd *cobra.Command, file file.Handler, fetcher imageFetcher, conf *config.Config, flags upgradeApplyFlags) error { func (u *upgradeApplyCmd) migrateTerraform(cmd *cobra.Command, file file.Handler, fetcher imageFetcher, conf *config.Config, flags upgradeApplyFlags) error {
u.log.Debugf("Planning Terraform migrations") u.log.Debugf("Planning Terraform migrations")
if err := u.upgrader.CheckTerraformMigrations(file); err != nil { if err := u.upgrader.CheckTerraformMigrations(); err != nil {
return fmt.Errorf("checking workspace: %w", err) return fmt.Errorf("checking workspace: %w", err)
} }
@ -168,10 +168,9 @@ func (u *upgradeApplyCmd) migrateTerraform(cmd *cobra.Command, file file.Handler
u.log.Debugf("Using Terraform variables:\n%v", vars) u.log.Debugf("Using Terraform variables:\n%v", vars)
opts := upgrade.TerraformUpgradeOptions{ opts := upgrade.TerraformUpgradeOptions{
LogLevel: flags.terraformLogLevel, LogLevel: flags.terraformLogLevel,
CSP: conf.GetProvider(), CSP: conf.GetProvider(),
Vars: vars, Vars: vars,
OutputFile: constants.TerraformMigrationOutputFile,
} }
// Check if there are any Terraform migrations to apply // Check if there are any Terraform migrations to apply
@ -190,20 +189,20 @@ func (u *upgradeApplyCmd) migrateTerraform(cmd *cobra.Command, file file.Handler
} }
if !ok { if !ok {
cmd.Println("Aborting upgrade.") cmd.Println("Aborting upgrade.")
if err := u.upgrader.CleanUpTerraformMigrations(file); err != nil { if err := u.upgrader.CleanUpTerraformMigrations(); err != nil {
return fmt.Errorf("cleaning up workspace: %w", err) return fmt.Errorf("cleaning up workspace: %w", err)
} }
return fmt.Errorf("aborted by user") return fmt.Errorf("aborted by user")
} }
} }
u.log.Debugf("Applying Terraform migrations") u.log.Debugf("Applying Terraform migrations")
err := u.upgrader.ApplyTerraformMigrations(cmd.Context(), file, opts) err := u.upgrader.ApplyTerraformMigrations(cmd.Context(), opts)
if err != nil { if err != nil {
return fmt.Errorf("applying terraform migrations: %w", err) return fmt.Errorf("applying terraform migrations: %w", err)
} }
cmd.Printf("Terraform migrations applied successfully and output written to: %s\n"+ 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", "A backup of the pre-upgrade state has been written to: %s\n",
opts.OutputFile, filepath.Join(constants.UpgradeDir, constants.TerraformUpgradeBackupDir)) constants.ClusterIDsFileName, filepath.Join(constants.UpgradeDir, constants.TerraformUpgradeBackupDir))
} else { } else {
u.log.Debugf("No Terraform diff detected") u.log.Debugf("No Terraform diff detected")
} }
@ -442,9 +441,9 @@ type cloudUpgrader interface {
UpdateAttestationConfig(ctx context.Context, newConfig config.AttestationCfg) error UpdateAttestationConfig(ctx context.Context, newConfig config.AttestationCfg) error
GetClusterAttestationConfig(ctx context.Context, variant variant.Variant) (config.AttestationCfg, *corev1.ConfigMap, error) GetClusterAttestationConfig(ctx context.Context, variant variant.Variant) (config.AttestationCfg, *corev1.ConfigMap, error)
PlanTerraformMigrations(ctx context.Context, opts upgrade.TerraformUpgradeOptions) (bool, error) PlanTerraformMigrations(ctx context.Context, opts upgrade.TerraformUpgradeOptions) (bool, error)
ApplyTerraformMigrations(ctx context.Context, fileHandler file.Handler, opts upgrade.TerraformUpgradeOptions) error ApplyTerraformMigrations(ctx context.Context, opts upgrade.TerraformUpgradeOptions) error
CheckTerraformMigrations(fileHandler file.Handler) error CheckTerraformMigrations() error
CleanUpTerraformMigrations(fileHandler file.Handler) error CleanUpTerraformMigrations() error
AddManualStateMigration(migration terraform.StateMigration) AddManualStateMigration(migration terraform.StateMigration)
} }

View file

@ -181,11 +181,11 @@ func (u stubUpgrader) GetClusterAttestationConfig(_ context.Context, _ variant.V
return u.currentConfig, &corev1.ConfigMap{}, nil return u.currentConfig, &corev1.ConfigMap{}, nil
} }
func (u stubUpgrader) CheckTerraformMigrations(file.Handler) error { func (u stubUpgrader) CheckTerraformMigrations() error {
return u.checkTerraformErr return u.checkTerraformErr
} }
func (u stubUpgrader) CleanUpTerraformMigrations(file.Handler) error { func (u stubUpgrader) CleanUpTerraformMigrations() error {
return u.cleanTerraformErr return u.cleanTerraformErr
} }
@ -193,7 +193,7 @@ func (u stubUpgrader) PlanTerraformMigrations(context.Context, upgrade.Terraform
return u.terraformDiff, u.planTerraformErr return u.terraformDiff, u.planTerraformErr
} }
func (u stubUpgrader) ApplyTerraformMigrations(context.Context, file.Handler, upgrade.TerraformUpgradeOptions) error { func (u stubUpgrader) ApplyTerraformMigrations(context.Context, upgrade.TerraformUpgradeOptions) error {
return u.applyTerraformErr return u.applyTerraformErr
} }

View file

@ -68,7 +68,7 @@ func runUpgradeCheck(cmd *cobra.Command, _ []string) error {
if err != nil { if err != nil {
return err return err
} }
checker, err := kubernetes.NewUpgrader(cmd.Context(), cmd.OutOrStdout(), log, kubernetes.UpgradeCmdKindCheck) checker, err := kubernetes.NewUpgrader(cmd.Context(), cmd.OutOrStdout(), fileHandler, log, kubernetes.UpgradeCmdKindCheck)
if err != nil { if err != nil {
return err return err
} }
@ -215,7 +215,7 @@ func (u *upgradeCheckCmd) upgradeCheck(cmd *cobra.Command, fileHandler file.Hand
u.checker.AddManualStateMigration(migration) u.checker.AddManualStateMigration(migration)
} }
if err := u.checker.CheckTerraformMigrations(fileHandler); err != nil { if err := u.checker.CheckTerraformMigrations(); err != nil {
return fmt.Errorf("checking workspace: %w", err) return fmt.Errorf("checking workspace: %w", err)
} }
@ -226,13 +226,12 @@ func (u *upgradeCheckCmd) upgradeCheck(cmd *cobra.Command, fileHandler file.Hand
u.log.Debugf("Using Terraform variables:\n%v", vars) u.log.Debugf("Using Terraform variables:\n%v", vars)
opts := upgrade.TerraformUpgradeOptions{ opts := upgrade.TerraformUpgradeOptions{
LogLevel: flags.terraformLogLevel, LogLevel: flags.terraformLogLevel,
CSP: conf.GetProvider(), CSP: conf.GetProvider(),
Vars: vars, Vars: vars,
OutputFile: constants.TerraformMigrationOutputFile,
} }
cmd.Println("The following Teraform migrations are available with this CLI:") cmd.Println("The following Terraform migrations are available with this CLI:")
// Check if there are any Terraform migrations // Check if there are any Terraform migrations
hasDiff, err := u.checker.PlanTerraformMigrations(cmd.Context(), opts) hasDiff, err := u.checker.PlanTerraformMigrations(cmd.Context(), opts)
@ -240,7 +239,7 @@ func (u *upgradeCheckCmd) upgradeCheck(cmd *cobra.Command, fileHandler file.Hand
return fmt.Errorf("planning terraform migrations: %w", err) return fmt.Errorf("planning terraform migrations: %w", err)
} }
defer func() { defer func() {
if err := u.checker.CleanUpTerraformMigrations(fileHandler); err != nil { if err := u.checker.CleanUpTerraformMigrations(); err != nil {
u.log.Debugf("Failed to clean up Terraform migrations: %v", err) u.log.Debugf("Failed to clean up Terraform migrations: %v", err)
} }
}() }()
@ -738,8 +737,8 @@ type upgradeChecker interface {
CurrentImage(ctx context.Context) (string, error) CurrentImage(ctx context.Context) (string, error)
CurrentKubernetesVersion(ctx context.Context) (string, error) CurrentKubernetesVersion(ctx context.Context) (string, error)
PlanTerraformMigrations(ctx context.Context, opts upgrade.TerraformUpgradeOptions) (bool, error) PlanTerraformMigrations(ctx context.Context, opts upgrade.TerraformUpgradeOptions) (bool, error)
CheckTerraformMigrations(fileHandler file.Handler) error CheckTerraformMigrations() error
CleanUpTerraformMigrations(fileHandler file.Handler) error CleanUpTerraformMigrations() error
AddManualStateMigration(migration terraform.StateMigration) AddManualStateMigration(migration terraform.StateMigration)
} }

View file

@ -377,11 +377,11 @@ func (u stubUpgradeChecker) PlanTerraformMigrations(context.Context, upgrade.Ter
return u.tfDiff, u.err return u.tfDiff, u.err
} }
func (u stubUpgradeChecker) CheckTerraformMigrations(file.Handler) error { func (u stubUpgradeChecker) CheckTerraformMigrations() error {
return u.err return u.err
} }
func (u stubUpgradeChecker) CleanUpTerraformMigrations(file.Handler) error { func (u stubUpgradeChecker) CleanUpTerraformMigrations() error {
return u.err return u.err
} }

View file

@ -97,7 +97,7 @@ type Upgrader struct {
} }
// NewUpgrader returns a new Upgrader. // NewUpgrader returns a new Upgrader.
func NewUpgrader(ctx context.Context, outWriter io.Writer, log debugLog, upgradeCmdKind UpgradeCmdKind) (*Upgrader, error) { func NewUpgrader(ctx context.Context, outWriter io.Writer, fileHandler file.Handler, log debugLog, upgradeCmdKind UpgradeCmdKind) (*Upgrader, error) {
upgradeID := "upgrade-" + time.Now().Format("20060102150405") + "-" + strings.Split(uuid.New().String(), "-")[0] upgradeID := "upgrade-" + time.Now().Format("20060102150405") + "-" + strings.Split(uuid.New().String(), "-")[0]
if upgradeCmdKind == UpgradeCmdKindCheck { if upgradeCmdKind == UpgradeCmdKindCheck {
// When performing an upgrade check, the upgrade directory will only be used temporarily to store the // When performing an upgrade check, the upgrade directory will only be used temporarily to store the
@ -143,7 +143,7 @@ func NewUpgrader(ctx context.Context, outWriter io.Writer, log debugLog, upgrade
} }
u.tfClient = tfClient u.tfClient = tfClient
tfUpgrader, err := upgrade.NewTerraformUpgrader(tfClient, outWriter) tfUpgrader, err := upgrade.NewTerraformUpgrader(tfClient, outWriter, fileHandler)
if err != nil { if err != nil {
return nil, fmt.Errorf("setting up terraform upgrader: %w", err) return nil, fmt.Errorf("setting up terraform upgrader: %w", err)
} }
@ -160,14 +160,14 @@ func (u *Upgrader) AddManualStateMigration(migration terraform.StateMigration) {
// CheckTerraformMigrations checks whether Terraform migrations are possible in the current workspace. // 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. // If the files that will be written during the upgrade already exist, it returns an error.
func (u *Upgrader) CheckTerraformMigrations(fileHandler file.Handler) error { func (u *Upgrader) CheckTerraformMigrations() error {
return u.tfUpgrader.CheckTerraformMigrations(fileHandler, u.upgradeID) return u.tfUpgrader.CheckTerraformMigrations(u.upgradeID)
} }
// CleanUpTerraformMigrations cleans up the Terraform migration workspace, for example when an upgrade is // CleanUpTerraformMigrations cleans up the Terraform migration workspace, for example when an upgrade is
// aborted by the user. // aborted by the user.
func (u *Upgrader) CleanUpTerraformMigrations(fileHandler file.Handler) error { func (u *Upgrader) CleanUpTerraformMigrations() error {
return u.tfUpgrader.CleanUpTerraformMigrations(fileHandler, u.upgradeID) return u.tfUpgrader.CleanUpTerraformMigrations(u.upgradeID)
} }
// PlanTerraformMigrations prepares the upgrade workspace and plans the Terraform migrations for the Constellation upgrade. // PlanTerraformMigrations prepares the upgrade workspace and plans the Terraform migrations for the Constellation upgrade.
@ -181,8 +181,8 @@ func (u *Upgrader) PlanTerraformMigrations(ctx context.Context, opts upgrade.Ter
// If PlanTerraformMigrations has not been executed before, it will return an error. // 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 // 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. // By the new one.
func (u *Upgrader) ApplyTerraformMigrations(ctx context.Context, fileHandler file.Handler, opts upgrade.TerraformUpgradeOptions) error { func (u *Upgrader) ApplyTerraformMigrations(ctx context.Context, opts upgrade.TerraformUpgradeOptions) error {
return u.tfUpgrader.ApplyTerraformMigrations(ctx, fileHandler, opts, u.upgradeID) return u.tfUpgrader.ApplyTerraformMigrations(ctx, opts, u.upgradeID)
} }
// UpgradeHelmServices upgrade helm services. // UpgradeHelmServices upgrade helm services.

View file

@ -23,11 +23,12 @@ import (
) )
// NewTerraformUpgrader returns a new TerraformUpgrader. // NewTerraformUpgrader returns a new TerraformUpgrader.
func NewTerraformUpgrader(tfClient tfClient, outWriter io.Writer) (*TerraformUpgrader, error) { func NewTerraformUpgrader(tfClient tfClient, outWriter io.Writer, fileHandler file.Handler) (*TerraformUpgrader, error) {
return &TerraformUpgrader{ return &TerraformUpgrader{
tf: tfClient, tf: tfClient,
policyPatcher: cloudcmd.NewAzurePolicyPatcher(), policyPatcher: cloudcmd.NewAzurePolicyPatcher(),
outWriter: outWriter, outWriter: outWriter,
fileHandler: fileHandler,
}, nil }, nil
} }
@ -36,6 +37,7 @@ type TerraformUpgrader struct {
tf tfClient tf tfClient
policyPatcher policyPatcher policyPatcher policyPatcher
outWriter io.Writer outWriter io.Writer
fileHandler file.Handler
} }
// TerraformUpgradeOptions are the options used for the Terraform upgrade. // TerraformUpgradeOptions are the options used for the Terraform upgrade.
@ -46,21 +48,18 @@ type TerraformUpgradeOptions struct {
CSP cloudprovider.Provider CSP cloudprovider.Provider
// Vars are the Terraform variables used for the upgrade. // Vars are the Terraform variables used for the upgrade.
Vars terraform.Variables Vars terraform.Variables
// OutputFile is the file to write the Terraform output to.
OutputFile string
} }
// CheckTerraformMigrations checks whether Terraform migrations are possible in the current workspace. // 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. // If the files that will be written during the upgrade already exist, it returns an error.
func (u *TerraformUpgrader) CheckTerraformMigrations(fileHandler file.Handler, upgradeID string) error { func (u *TerraformUpgrader) CheckTerraformMigrations(upgradeID string) error {
var existingFiles []string var existingFiles []string
filesToCheck := []string{ filesToCheck := []string{
constants.TerraformMigrationOutputFile,
filepath.Join(constants.UpgradeDir, upgradeID, constants.TerraformUpgradeBackupDir), filepath.Join(constants.UpgradeDir, upgradeID, constants.TerraformUpgradeBackupDir),
} }
for _, f := range filesToCheck { for _, f := range filesToCheck {
if err := checkFileExists(fileHandler, &existingFiles, f); err != nil { if err := checkFileExists(u.fileHandler, &existingFiles, f); err != nil {
return fmt.Errorf("checking terraform migrations: %w", err) return fmt.Errorf("checking terraform migrations: %w", err)
} }
} }
@ -89,6 +88,7 @@ func checkFileExists(fileHandler file.Handler, existingFiles *[]string, filename
// If a diff exists, it's being written to the upgrader's output writer. It also returns // If a diff exists, it's being written to the upgrader's output writer. It also returns
// a bool indicating whether a diff exists. // 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, upgradeID string) (bool, error) {
// Prepare the new Terraform workspace and backup the old one
err := u.tf.PrepareUpgradeWorkspace( err := u.tf.PrepareUpgradeWorkspace(
filepath.Join("terraform", strings.ToLower(opts.CSP.String())), filepath.Join("terraform", strings.ToLower(opts.CSP.String())),
constants.TerraformWorkingDir, constants.TerraformWorkingDir,
@ -100,6 +100,14 @@ func (u *TerraformUpgrader) PlanTerraformMigrations(ctx context.Context, opts Te
return false, fmt.Errorf("preparing terraform workspace: %w", err) return false, fmt.Errorf("preparing terraform workspace: %w", err)
} }
// Backup the old constellation-id.json file
if err := u.fileHandler.CopyFile(
constants.ClusterIDsFileName,
filepath.Join(constants.UpgradeDir, upgradeID, constants.ClusterIDsFileName+".old"),
); err != nil {
return false, fmt.Errorf("backing up %s: %w", constants.ClusterIDsFileName, err)
}
hasDiff, err := u.tf.Plan(ctx, opts.LogLevel, constants.TerraformUpgradePlanFile) hasDiff, err := u.tf.Plan(ctx, opts.LogLevel, constants.TerraformUpgradePlanFile)
if err != nil { if err != nil {
return false, fmt.Errorf("terraform plan: %w", err) return false, fmt.Errorf("terraform plan: %w", err)
@ -116,13 +124,13 @@ func (u *TerraformUpgrader) PlanTerraformMigrations(ctx context.Context, opts Te
// CleanUpTerraformMigrations cleans up the Terraform migration workspace, for example when an upgrade is // CleanUpTerraformMigrations cleans up the Terraform migration workspace, for example when an upgrade is
// aborted by the user. // aborted by the user.
func (u *TerraformUpgrader) CleanUpTerraformMigrations(fileHandler file.Handler, upgradeID string) error { func (u *TerraformUpgrader) CleanUpTerraformMigrations(upgradeID string) error {
cleanupFiles := []string{ cleanupFiles := []string{
filepath.Join(constants.UpgradeDir, upgradeID), filepath.Join(constants.UpgradeDir, upgradeID),
} }
for _, f := range cleanupFiles { for _, f := range cleanupFiles {
if err := fileHandler.RemoveAll(f); err != nil { if err := u.fileHandler.RemoveAll(f); err != nil {
return fmt.Errorf("cleaning up file %s: %w", f, err) return fmt.Errorf("cleaning up file %s: %w", f, err)
} }
} }
@ -134,7 +142,7 @@ func (u *TerraformUpgrader) CleanUpTerraformMigrations(fileHandler file.Handler,
// If PlanTerraformMigrations has not been executed before, it will return an error. // 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 // 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. // By the new one.
func (u *TerraformUpgrader) ApplyTerraformMigrations(ctx context.Context, fileHandler file.Handler, opts TerraformUpgradeOptions, upgradeID string) error { func (u *TerraformUpgrader) ApplyTerraformMigrations(ctx context.Context, opts TerraformUpgradeOptions, upgradeID string) error {
tfOutput, err := u.tf.CreateCluster(ctx, opts.LogLevel) tfOutput, err := u.tf.CreateCluster(ctx, opts.LogLevel)
if err != nil { if err != nil {
return fmt.Errorf("terraform apply: %w", err) return fmt.Errorf("terraform apply: %w", err)
@ -155,20 +163,34 @@ func (u *TerraformUpgrader) ApplyTerraformMigrations(ctx context.Context, fileHa
AttestationURL: tfOutput.AttestationURL, AttestationURL: tfOutput.AttestationURL,
} }
if err := fileHandler.RemoveAll(constants.TerraformWorkingDir); err != nil { if err := u.fileHandler.RemoveAll(constants.TerraformWorkingDir); err != nil {
return fmt.Errorf("removing old terraform directory: %w", err) return fmt.Errorf("removing old terraform directory: %w", err)
} }
if err := fileHandler.CopyDir(filepath.Join(constants.UpgradeDir, upgradeID, constants.TerraformUpgradeWorkingDir), constants.TerraformWorkingDir); err != nil { if err := u.fileHandler.CopyDir(filepath.Join(constants.UpgradeDir, upgradeID, constants.TerraformUpgradeWorkingDir), constants.TerraformWorkingDir); err != nil {
return fmt.Errorf("replacing old terraform directory with new one: %w", err) return fmt.Errorf("replacing old terraform directory with new one: %w", err)
} }
if err := fileHandler.RemoveAll(filepath.Join(constants.UpgradeDir, upgradeID, constants.TerraformUpgradeWorkingDir)); err != nil { if err := u.fileHandler.RemoveAll(filepath.Join(constants.UpgradeDir, upgradeID, constants.TerraformUpgradeWorkingDir)); err != nil {
return fmt.Errorf("removing terraform upgrade directory: %w", err) return fmt.Errorf("removing terraform upgrade directory: %w", err)
} }
if err := fileHandler.WriteJSON(opts.OutputFile, outputFileContents); err != nil { if err := u.mergeClusterIDFile(outputFileContents); err != nil {
return fmt.Errorf("writing terraform output to file: %w", err) return fmt.Errorf("merging migration output into %s: %w", constants.ClusterIDsFileName, err)
}
return nil
}
// mergeClusterIDFile merges the output of the migration into the constellation-id.json file.
func (u *TerraformUpgrader) mergeClusterIDFile(migrationOutput clusterid.File) error {
idFile := &clusterid.File{}
if err := u.fileHandler.ReadJSON(constants.ClusterIDsFileName, idFile); err != nil {
return fmt.Errorf("reading %s: %w", constants.ClusterIDsFileName, err)
}
if err := u.fileHandler.WriteJSON(constants.ClusterIDsFileName, idFile.Merge(migrationOutput), file.OptOverwrite); err != nil {
return fmt.Errorf("writing %s: %w", constants.ClusterIDsFileName, err)
} }
return nil return nil

View file

@ -23,8 +23,8 @@ import (
) )
func TestCheckTerraformMigrations(t *testing.T) { func TestCheckTerraformMigrations(t *testing.T) {
upgrader := func() *TerraformUpgrader { upgrader := func(fileHandler file.Handler) *TerraformUpgrader {
u, err := NewTerraformUpgrader(&stubTerraformClient{}, bytes.NewBuffer(nil)) u, err := NewTerraformUpgrader(&stubTerraformClient{}, bytes.NewBuffer(nil), fileHandler)
require.NoError(t, err) require.NoError(t, err)
return u return u
@ -48,11 +48,6 @@ func TestCheckTerraformMigrations(t *testing.T) {
upgradeID: "1234", upgradeID: "1234",
workspace: workspace(nil), workspace: workspace(nil),
}, },
"migration output file already exists": {
upgradeID: "1234",
workspace: workspace([]string{constants.TerraformMigrationOutputFile}),
wantErr: true,
},
"terraform backup dir already exists": { "terraform backup dir already exists": {
upgradeID: "1234", upgradeID: "1234",
workspace: workspace([]string{filepath.Join(constants.UpgradeDir, "1234", constants.TerraformUpgradeBackupDir)}), workspace: workspace([]string{filepath.Join(constants.UpgradeDir, "1234", constants.TerraformUpgradeBackupDir)}),
@ -62,8 +57,8 @@ func TestCheckTerraformMigrations(t *testing.T) {
for name, tc := range testCases { for name, tc := range testCases {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
u := upgrader() u := upgrader(tc.workspace)
err := u.CheckTerraformMigrations(tc.workspace, tc.upgradeID) err := u.CheckTerraformMigrations(tc.upgradeID)
if tc.wantErr { if tc.wantErr {
require.Error(t, err) require.Error(t, err)
return return
@ -75,48 +70,74 @@ func TestCheckTerraformMigrations(t *testing.T) {
} }
func TestPlanTerraformMigrations(t *testing.T) { func TestPlanTerraformMigrations(t *testing.T) {
upgrader := func(tf tfClient) *TerraformUpgrader { upgrader := func(tf tfClient, fileHandler file.Handler) *TerraformUpgrader {
u, err := NewTerraformUpgrader(tf, bytes.NewBuffer(nil)) u, err := NewTerraformUpgrader(tf, bytes.NewBuffer(nil), fileHandler)
require.NoError(t, err) require.NoError(t, err)
return u return u
} }
workspace := func(existingFiles []string) file.Handler {
fs := afero.NewMemMapFs()
for _, f := range existingFiles {
require.NoError(t, afero.WriteFile(fs, f, []byte{}, 0o644))
}
return file.NewHandler(fs)
}
testCases := map[string]struct { testCases := map[string]struct {
upgradeID string upgradeID string
tf tfClient tf tfClient
workspace file.Handler
want bool want bool
wantErr bool wantErr bool
}{ }{
"success no diff": { "success no diff": {
upgradeID: "1234", upgradeID: "1234",
tf: &stubTerraformClient{}, tf: &stubTerraformClient{},
workspace: workspace([]string{constants.ClusterIDsFileName}),
}, },
"success diff": { "success diff": {
upgradeID: "1234", upgradeID: "1234",
tf: &stubTerraformClient{ tf: &stubTerraformClient{
hasDiff: true, hasDiff: true,
}, },
want: true, workspace: workspace([]string{constants.ClusterIDsFileName}),
want: true,
}, },
"prepare workspace error": { "prepare workspace error": {
upgradeID: "1234", upgradeID: "1234",
tf: &stubTerraformClient{ tf: &stubTerraformClient{
prepareWorkspaceErr: assert.AnError, prepareWorkspaceErr: assert.AnError,
}, },
wantErr: true, workspace: workspace([]string{constants.ClusterIDsFileName}),
wantErr: true,
},
"constellation-id.json does not exist": {
upgradeID: "1234",
tf: &stubTerraformClient{},
workspace: workspace(nil),
wantErr: true,
},
"constellation-id backup already exists": {
upgradeID: "1234",
tf: &stubTerraformClient{},
workspace: workspace([]string{filepath.Join(constants.UpgradeDir, "1234", constants.ClusterIDsFileName+".old")}),
wantErr: true,
}, },
"plan error": { "plan error": {
tf: &stubTerraformClient{ tf: &stubTerraformClient{
planErr: assert.AnError, planErr: assert.AnError,
}, },
wantErr: true, workspace: workspace([]string{constants.ClusterIDsFileName}),
wantErr: true,
}, },
"show plan error no diff": { "show plan error no diff": {
upgradeID: "1234", upgradeID: "1234",
tf: &stubTerraformClient{ tf: &stubTerraformClient{
showErr: assert.AnError, showErr: assert.AnError,
}, },
workspace: workspace([]string{constants.ClusterIDsFileName}),
}, },
"show plan error diff": { "show plan error diff": {
upgradeID: "1234", upgradeID: "1234",
@ -124,7 +145,8 @@ func TestPlanTerraformMigrations(t *testing.T) {
showErr: assert.AnError, showErr: assert.AnError,
hasDiff: true, hasDiff: true,
}, },
wantErr: true, workspace: workspace([]string{constants.ClusterIDsFileName}),
wantErr: true,
}, },
} }
@ -132,7 +154,7 @@ func TestPlanTerraformMigrations(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
require := require.New(t) require := require.New(t)
u := upgrader(tc.tf) u := upgrader(tc.tf, tc.workspace)
opts := TerraformUpgradeOptions{ opts := TerraformUpgradeOptions{
LogLevel: terraform.LogLevelDebug, LogLevel: terraform.LogLevelDebug,
@ -152,8 +174,8 @@ func TestPlanTerraformMigrations(t *testing.T) {
} }
func TestApplyTerraformMigrations(t *testing.T) { func TestApplyTerraformMigrations(t *testing.T) {
upgrader := func(tf tfClient) *TerraformUpgrader { upgrader := func(tf tfClient, fileHandler file.Handler) *TerraformUpgrader {
u, err := NewTerraformUpgrader(tf, bytes.NewBuffer(nil)) u, err := NewTerraformUpgrader(tf, bytes.NewBuffer(nil), fileHandler)
require.NoError(t, err) require.NoError(t, err)
return u return u
@ -161,6 +183,7 @@ func TestApplyTerraformMigrations(t *testing.T) {
fileHandler := func(upgradeID string, existingFiles ...string) file.Handler { fileHandler := func(upgradeID string, existingFiles ...string) file.Handler {
fh := file.NewHandler(afero.NewMemMapFs()) fh := file.NewHandler(afero.NewMemMapFs())
require.NoError(t, require.NoError(t,
fh.Write( fh.Write(
filepath.Join(constants.UpgradeDir, upgradeID, constants.TerraformUpgradeWorkingDir, "someFile"), filepath.Join(constants.UpgradeDir, upgradeID, constants.TerraformUpgradeWorkingDir, "someFile"),
@ -173,54 +196,35 @@ func TestApplyTerraformMigrations(t *testing.T) {
} }
testCases := map[string]struct { testCases := map[string]struct {
upgradeID string upgradeID string
tf tfClient tf tfClient
policyPatcher stubPolicyPatcher policyPatcher stubPolicyPatcher
fs file.Handler fs file.Handler
outputFileName string skipIDFileCreation bool // if true, do not create the constellation-id.json file
wantErr bool wantErr bool
}{ }{
"success": { "success": {
upgradeID: "1234", upgradeID: "1234",
tf: &stubTerraformClient{}, tf: &stubTerraformClient{},
fs: fileHandler("1234"), fs: fileHandler("1234"),
policyPatcher: stubPolicyPatcher{}, policyPatcher: stubPolicyPatcher{},
outputFileName: "test.json",
}, },
"create cluster error": { "create cluster error": {
upgradeID: "1234", upgradeID: "1234",
tf: &stubTerraformClient{ tf: &stubTerraformClient{
CreateClusterErr: assert.AnError, CreateClusterErr: assert.AnError,
}, },
fs: fileHandler("1234"), fs: fileHandler("1234"),
policyPatcher: stubPolicyPatcher{}, policyPatcher: stubPolicyPatcher{},
outputFileName: "test.json", wantErr: true,
wantErr: true,
}, },
"patch error": { "constellation-id.json does not exist": {
upgradeID: "1234", upgradeID: "1234",
tf: &stubTerraformClient{}, tf: &stubTerraformClient{},
fs: fileHandler("1234"), fs: fileHandler("1234"),
policyPatcher: stubPolicyPatcher{ policyPatcher: stubPolicyPatcher{},
patchErr: assert.AnError, skipIDFileCreation: true,
}, wantErr: true,
wantErr: true,
},
"empty file name": {
upgradeID: "1234",
tf: &stubTerraformClient{},
fs: fileHandler("1234"),
policyPatcher: stubPolicyPatcher{},
outputFileName: "",
wantErr: true,
},
"file already exists": {
upgradeID: "1234",
tf: &stubTerraformClient{},
fs: fileHandler("1234", "test.json"),
policyPatcher: stubPolicyPatcher{},
outputFileName: "test.json",
wantErr: true,
}, },
} }
@ -228,16 +232,23 @@ func TestApplyTerraformMigrations(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
require := require.New(t) require := require.New(t)
u := upgrader(tc.tf) u := upgrader(tc.tf, tc.fs)
opts := TerraformUpgradeOptions{ if !tc.skipIDFileCreation {
LogLevel: terraform.LogLevelDebug, require.NoError(
CSP: cloudprovider.Unknown, tc.fs.Write(
Vars: &terraform.QEMUVariables{}, filepath.Join(constants.ClusterIDsFileName),
OutputFile: tc.outputFileName, []byte("{}"),
))
} }
err := u.ApplyTerraformMigrations(context.Background(), tc.fs, opts, tc.upgradeID) opts := TerraformUpgradeOptions{
LogLevel: terraform.LogLevelDebug,
CSP: cloudprovider.Unknown,
Vars: &terraform.QEMUVariables{},
}
err := u.ApplyTerraformMigrations(context.Background(), opts, tc.upgradeID)
if tc.wantErr { if tc.wantErr {
require.Error(err) require.Error(err)
} else { } else {
@ -248,8 +259,8 @@ func TestApplyTerraformMigrations(t *testing.T) {
} }
func TestCleanUpTerraformMigrations(t *testing.T) { func TestCleanUpTerraformMigrations(t *testing.T) {
upgrader := func() *TerraformUpgrader { upgrader := func(fileHandler file.Handler) *TerraformUpgrader {
u, err := NewTerraformUpgrader(&stubTerraformClient{}, bytes.NewBuffer(nil)) u, err := NewTerraformUpgrader(&stubTerraformClient{}, bytes.NewBuffer(nil), fileHandler)
require.NoError(t, err) require.NoError(t, err)
return u return u
@ -315,9 +326,9 @@ func TestCleanUpTerraformMigrations(t *testing.T) {
require := require.New(t) require := require.New(t)
workspace := workspace(tc.workspaceFiles) workspace := workspace(tc.workspaceFiles)
u := upgrader() u := upgrader(workspace)
err := u.CleanUpTerraformMigrations(workspace, tc.upgradeID) err := u.CleanUpTerraformMigrations(tc.upgradeID)
if tc.wantErr { if tc.wantErr {
require.Error(err) require.Error(err)
return return

View file

@ -156,8 +156,6 @@ const (
TerraformUpgradeWorkingDir = "terraform" TerraformUpgradeWorkingDir = "terraform"
// TerraformUpgradeBackupDir is the directory name being used to backup the pre-upgrade state in an upgrade. // TerraformUpgradeBackupDir is the directory name being used to backup the pre-upgrade state in an upgrade.
TerraformUpgradeBackupDir = "terraform-backup" TerraformUpgradeBackupDir = "terraform-backup"
// TerraformMigrationOutputFile is the file name of the output file created by a successful Terraform migration.
TerraformMigrationOutputFile = "terraform-migration-output.json"
// UpgradeDir is the name of the directory being used for cluster upgrades. // UpgradeDir is the name of the directory being used for cluster upgrades.
UpgradeDir = "constellation-upgrade" UpgradeDir = "constellation-upgrade"