mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-05-02 06:16:08 -04:00
cli: perform upgrades in-place in Terraform workspace (#2317)
* perform upgrades in-place in terraform workspace Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * update buildfiles Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add iam upgrade apply test Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * update buildfiles Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix linter Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * make config fetcher stubbable Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * change workspace restoring behaviour Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * allow overwriting existing Terraform files Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * allow overwrites of TF variables Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix iam upgrade apply Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix embed directive Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * make loader test less brittle Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * pass upgrade ID to user Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * naming nit Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * use upgradeDir Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * tidy Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> --------- Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>
This commit is contained in:
parent
9c54ff06e0
commit
95cf4bdf21
19 changed files with 410 additions and 286 deletions
|
@ -52,7 +52,6 @@ go_test(
|
|||
"clusterupgrade_test.go",
|
||||
"create_test.go",
|
||||
"iam_test.go",
|
||||
"iamupgrade_test.go",
|
||||
"patch_test.go",
|
||||
"rollback_test.go",
|
||||
"terminate_test.go",
|
||||
|
|
|
@ -45,7 +45,7 @@ type tfIAMClient interface {
|
|||
type tfUpgradePlanner interface {
|
||||
ShowPlan(ctx context.Context, logLevel terraform.LogLevel, output io.Writer) error
|
||||
Plan(ctx context.Context, logLevel terraform.LogLevel) (bool, error)
|
||||
PrepareUpgradeWorkspace(embeddedPath, oldWorkingDir, backupDir string, vars terraform.Variables) error
|
||||
PrepareUpgradeWorkspace(embeddedPath, backupDir string, vars terraform.Variables) error
|
||||
}
|
||||
|
||||
type tfIAMUpgradeClient interface {
|
||||
|
|
|
@ -35,7 +35,7 @@ type ClusterUpgrader struct {
|
|||
func NewClusterUpgrader(ctx context.Context, existingWorkspace, upgradeWorkspace string,
|
||||
logLevel terraform.LogLevel, fileHandler file.Handler,
|
||||
) (*ClusterUpgrader, error) {
|
||||
tfClient, err := terraform.New(ctx, filepath.Join(upgradeWorkspace, constants.TerraformUpgradeWorkingDir))
|
||||
tfClient, err := terraform.New(ctx, constants.TerraformWorkingDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setting up terraform client: %w", err)
|
||||
}
|
||||
|
@ -57,11 +57,17 @@ func (u *ClusterUpgrader) PlanClusterUpgrade(ctx context.Context, outWriter io.W
|
|||
return planUpgrade(
|
||||
ctx, u.tf, u.fileHandler, outWriter, u.logLevel, vars,
|
||||
filepath.Join("terraform", strings.ToLower(csp.String())),
|
||||
u.existingWorkspace,
|
||||
filepath.Join(u.upgradeWorkspace, constants.TerraformUpgradeBackupDir),
|
||||
)
|
||||
}
|
||||
|
||||
// RestoreClusterWorkspace rolls back the existing workspace to the backup directory created when planning an upgrade,
|
||||
// when the user decides to not apply an upgrade after planning it.
|
||||
// Note that this will not apply the restored state from the backup.
|
||||
func (u *ClusterUpgrader) RestoreClusterWorkspace() error {
|
||||
return restoreBackup(u.fileHandler, u.existingWorkspace, filepath.Join(u.upgradeWorkspace, constants.TerraformUpgradeBackupDir))
|
||||
}
|
||||
|
||||
// ApplyClusterUpgrade applies the Terraform migrations planned by PlanClusterUpgrade.
|
||||
// On success, the workspace of the Upgrader replaces the existing Terraform workspace.
|
||||
func (u *ClusterUpgrader) ApplyClusterUpgrade(ctx context.Context, csp cloudprovider.Provider) (terraform.ApplyOutput, error) {
|
||||
|
@ -75,13 +81,5 @@ func (u *ClusterUpgrader) ApplyClusterUpgrade(ctx context.Context, csp cloudprov
|
|||
}
|
||||
}
|
||||
|
||||
if err := moveUpgradeToCurrent(
|
||||
u.fileHandler,
|
||||
u.existingWorkspace,
|
||||
filepath.Join(u.upgradeWorkspace, constants.TerraformUpgradeWorkingDir),
|
||||
); err != nil {
|
||||
return tfOutput, fmt.Errorf("promoting upgrade workspace to current workspace: %w", err)
|
||||
}
|
||||
|
||||
return tfOutput, nil
|
||||
}
|
||||
|
|
|
@ -198,6 +198,6 @@ func (t *tfClusterUpgradeStub) ApplyCluster(_ context.Context, _ cloudprovider.P
|
|||
return terraform.ApplyOutput{}, t.applyErr
|
||||
}
|
||||
|
||||
func (t *tfClusterUpgradeStub) PrepareUpgradeWorkspace(_, _, _ string, _ terraform.Variables) error {
|
||||
func (t *tfClusterUpgradeStub) PrepareUpgradeWorkspace(_, _ string, _ terraform.Variables) error {
|
||||
return t.prepareWorkspaceErr
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ type IAMUpgrader struct {
|
|||
func NewIAMUpgrader(ctx context.Context, existingWorkspace, upgradeWorkspace string,
|
||||
logLevel terraform.LogLevel, fileHandler file.Handler,
|
||||
) (*IAMUpgrader, error) {
|
||||
tfClient, err := terraform.New(ctx, filepath.Join(upgradeWorkspace, constants.TerraformIAMUpgradeWorkingDir))
|
||||
tfClient, err := terraform.New(ctx, constants.TerraformIAMWorkingDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setting up terraform client: %w", err)
|
||||
}
|
||||
|
@ -62,11 +62,17 @@ func (u *IAMUpgrader) PlanIAMUpgrade(ctx context.Context, outWriter io.Writer, v
|
|||
return planUpgrade(
|
||||
ctx, u.tf, u.fileHandler, outWriter, u.logLevel, vars,
|
||||
filepath.Join("terraform", "iam", strings.ToLower(csp.String())),
|
||||
u.existingWorkspace,
|
||||
filepath.Join(u.upgradeWorkspace, constants.TerraformIAMUpgradeBackupDir),
|
||||
)
|
||||
}
|
||||
|
||||
// RestoreIAMWorkspace rolls back the existing workspace to the backup directory created when planning an upgrade,
|
||||
// when the user decides to not apply an upgrade after planning it.
|
||||
// Note that this will not apply the restored state from the backup.
|
||||
func (u *IAMUpgrader) RestoreIAMWorkspace() error {
|
||||
return restoreBackup(u.fileHandler, u.existingWorkspace, filepath.Join(u.upgradeWorkspace, constants.TerraformIAMUpgradeBackupDir))
|
||||
}
|
||||
|
||||
// ApplyIAMUpgrade applies the Terraform IAM migrations planned by PlanIAMUpgrade.
|
||||
// On success, the workspace of the Upgrader replaces the existing Terraform workspace.
|
||||
func (u *IAMUpgrader) ApplyIAMUpgrade(ctx context.Context, csp cloudprovider.Provider) error {
|
||||
|
@ -74,13 +80,5 @@ func (u *IAMUpgrader) ApplyIAMUpgrade(ctx context.Context, csp cloudprovider.Pro
|
|||
return fmt.Errorf("terraform apply: %w", err)
|
||||
}
|
||||
|
||||
if err := moveUpgradeToCurrent(
|
||||
u.fileHandler,
|
||||
u.existingWorkspace,
|
||||
filepath.Join(u.upgradeWorkspace, constants.TerraformIAMUpgradeWorkingDir),
|
||||
); err != nil {
|
||||
return fmt.Errorf("promoting upgrade workspace to current workspace: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,136 +0,0 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package cloudcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIAMMigrate(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
upgradeID := "test-upgrade"
|
||||
upgradeDir := filepath.Join(constants.UpgradeDir, upgradeID, constants.TerraformIAMUpgradeWorkingDir)
|
||||
fs, file := setupMemFSAndFileHandler(t, []string{"terraform.tfvars", "terraform.tfstate"}, []byte("OLD"))
|
||||
csp := cloudprovider.AWS
|
||||
|
||||
// act
|
||||
fakeTfClient := &tfIAMUpgradeStub{upgradeID: upgradeID, file: file}
|
||||
sut := &IAMUpgrader{
|
||||
tf: fakeTfClient,
|
||||
logLevel: terraform.LogLevelDebug,
|
||||
existingWorkspace: constants.TerraformIAMWorkingDir,
|
||||
upgradeWorkspace: filepath.Join(constants.UpgradeDir, upgradeID),
|
||||
fileHandler: file,
|
||||
}
|
||||
hasDiff, err := sut.PlanIAMUpgrade(context.Background(), io.Discard, &terraform.QEMUVariables{}, csp)
|
||||
|
||||
// assert
|
||||
assert.NoError(err)
|
||||
assert.False(hasDiff)
|
||||
assertFileExists(t, fs, filepath.Join(upgradeDir, "terraform.tfvars"))
|
||||
assertFileExists(t, fs, filepath.Join(upgradeDir, "terraform.tfstate"))
|
||||
|
||||
// act
|
||||
err = sut.ApplyIAMUpgrade(context.Background(), csp)
|
||||
assert.NoError(err)
|
||||
|
||||
// assert
|
||||
assertFileReadsContent(t, file, filepath.Join(constants.TerraformIAMWorkingDir, "terraform.tfvars"), "NEW")
|
||||
assertFileReadsContent(t, file, filepath.Join(constants.TerraformIAMWorkingDir, "terraform.tfstate"), "NEW")
|
||||
assertFileDoesntExist(t, fs, filepath.Join(upgradeDir))
|
||||
}
|
||||
|
||||
func assertFileReadsContent(t *testing.T, file file.Handler, path string, expectedContent string) {
|
||||
t.Helper()
|
||||
bt, err := file.Read(path)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedContent, string(bt))
|
||||
}
|
||||
|
||||
func assertFileExists(t *testing.T, fs afero.Fs, path string) {
|
||||
t.Helper()
|
||||
res, err := fs.Stat(path)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, res)
|
||||
}
|
||||
|
||||
func assertFileDoesntExist(t *testing.T, fs afero.Fs, path string) {
|
||||
t.Helper()
|
||||
res, err := fs.Stat(path)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
}
|
||||
|
||||
// setupMemFSAndFileHandler sets up a file handler with a memory file system and writes the given files with the given content.
|
||||
func setupMemFSAndFileHandler(t *testing.T, files []string, content []byte) (afero.Fs, file.Handler) {
|
||||
fs := afero.NewMemMapFs()
|
||||
file := file.NewHandler(fs)
|
||||
err := file.MkdirAll(constants.TerraformIAMWorkingDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, f := range files {
|
||||
err := file.Write(filepath.Join(constants.TerraformIAMWorkingDir, f), content)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
return fs, file
|
||||
}
|
||||
|
||||
type tfIAMUpgradeStub struct {
|
||||
upgradeID string
|
||||
file file.Handler
|
||||
applyErr error
|
||||
planErr error
|
||||
planDiff bool
|
||||
showErr error
|
||||
prepareWorkspaceErr error
|
||||
}
|
||||
|
||||
func (t *tfIAMUpgradeStub) Plan(_ context.Context, _ terraform.LogLevel) (bool, error) {
|
||||
return t.planDiff, t.planErr
|
||||
}
|
||||
|
||||
func (t *tfIAMUpgradeStub) ShowPlan(_ context.Context, _ terraform.LogLevel, _ io.Writer) error {
|
||||
return t.showErr
|
||||
}
|
||||
|
||||
func (t *tfIAMUpgradeStub) ApplyIAM(_ context.Context, _ cloudprovider.Provider, _ terraform.LogLevel) (terraform.IAMOutput, error) {
|
||||
if t.applyErr != nil {
|
||||
return terraform.IAMOutput{}, t.applyErr
|
||||
}
|
||||
|
||||
upgradeDir := filepath.Join(constants.UpgradeDir, t.upgradeID, constants.TerraformIAMUpgradeWorkingDir)
|
||||
if err := t.file.Write(filepath.Join(upgradeDir, "terraform.tfvars"), []byte("NEW"), file.OptOverwrite); err != nil {
|
||||
return terraform.IAMOutput{}, err
|
||||
}
|
||||
if err := t.file.Write(filepath.Join(upgradeDir, "terraform.tfstate"), []byte("NEW"), file.OptOverwrite); err != nil {
|
||||
return terraform.IAMOutput{}, err
|
||||
}
|
||||
return terraform.IAMOutput{}, nil
|
||||
}
|
||||
|
||||
func (t *tfIAMUpgradeStub) PrepareUpgradeWorkspace(_, _, _ string, _ terraform.Variables) error {
|
||||
if t.prepareWorkspaceErr != nil {
|
||||
return t.prepareWorkspaceErr
|
||||
}
|
||||
|
||||
upgradeDir := filepath.Join(constants.UpgradeDir, t.upgradeID, constants.TerraformIAMUpgradeWorkingDir)
|
||||
if err := t.file.Write(filepath.Join(upgradeDir, "terraform.tfvars"), []byte("OLD")); err != nil {
|
||||
return err
|
||||
}
|
||||
return t.file.Write(filepath.Join(upgradeDir, "terraform.tfstate"), []byte("OLD"))
|
||||
}
|
|
@ -21,16 +21,15 @@ import (
|
|||
func planUpgrade(
|
||||
ctx context.Context, tfClient tfUpgradePlanner, fileHandler file.Handler,
|
||||
outWriter io.Writer, logLevel terraform.LogLevel, vars terraform.Variables,
|
||||
templateDir, existingWorkspace, backupDir string,
|
||||
templateDir, backupDir string,
|
||||
) (bool, error) {
|
||||
if err := ensureFileNotExist(fileHandler, backupDir); err != nil {
|
||||
return false, fmt.Errorf("workspace is not clean: %w", err)
|
||||
return false, fmt.Errorf("backup directory %s already exists: %w", backupDir, err)
|
||||
}
|
||||
|
||||
// Prepare the new Terraform workspace and backup the old one
|
||||
// Backup the old Terraform workspace and move the embedded Terraform files into the workspace.
|
||||
err := tfClient.PrepareUpgradeWorkspace(
|
||||
templateDir,
|
||||
existingWorkspace,
|
||||
backupDir,
|
||||
vars,
|
||||
)
|
||||
|
@ -52,20 +51,20 @@ func planUpgrade(
|
|||
return hasDiff, nil
|
||||
}
|
||||
|
||||
// moveUpgradeToCurrent replaces the an existing Terraform workspace with a workspace holding migrated Terraform resources.
|
||||
func moveUpgradeToCurrent(fileHandler file.Handler, existingWorkspace, upgradeWorkingDir string) error {
|
||||
if err := fileHandler.RemoveAll(existingWorkspace); err != nil {
|
||||
return fmt.Errorf("removing old terraform directory: %w", err)
|
||||
// restoreBackup replaces the existing Terraform workspace with the backup.
|
||||
func restoreBackup(fileHandler file.Handler, workingDir, backupDir string) error {
|
||||
if err := fileHandler.RemoveAll(workingDir); err != nil {
|
||||
return fmt.Errorf("removing existing workspace: %w", err)
|
||||
}
|
||||
if err := fileHandler.CopyDir(
|
||||
upgradeWorkingDir,
|
||||
existingWorkspace,
|
||||
backupDir,
|
||||
workingDir,
|
||||
); err != nil {
|
||||
return fmt.Errorf("replacing old terraform directory with new one: %w", err)
|
||||
return fmt.Errorf("replacing terraform workspace with backup: %w", err)
|
||||
}
|
||||
|
||||
if err := fileHandler.RemoveAll(upgradeWorkingDir); err != nil {
|
||||
return fmt.Errorf("removing terraform upgrade directory: %w", err)
|
||||
if err := fileHandler.RemoveAll(backupDir); err != nil {
|
||||
return fmt.Errorf("removing backup directory: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -87,7 +87,7 @@ func TestPlanUpgrade(t *testing.T) {
|
|||
hasDiff, err := planUpgrade(
|
||||
context.Background(), tc.tf, fs, io.Discard, terraform.LogLevelDebug,
|
||||
&terraform.QEMUVariables{},
|
||||
"existing", "upgrade", "backup",
|
||||
"test", "backup",
|
||||
)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
|
@ -99,9 +99,9 @@ func TestPlanUpgrade(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMoveUpgradeToCurrent(t *testing.T) {
|
||||
func TestRestoreBackup(t *testing.T) {
|
||||
existingWorkspace := "foo"
|
||||
upgradeWorkingDir := "bar"
|
||||
backupDir := "bar"
|
||||
|
||||
testCases := map[string]struct {
|
||||
prepareFs func(require *require.Assertions) file.Handler
|
||||
|
@ -111,18 +111,18 @@ func TestMoveUpgradeToCurrent(t *testing.T) {
|
|||
prepareFs: func(require *require.Assertions) file.Handler {
|
||||
fs := file.NewHandler(afero.NewMemMapFs())
|
||||
require.NoError(fs.MkdirAll(existingWorkspace))
|
||||
require.NoError(fs.MkdirAll(upgradeWorkingDir))
|
||||
require.NoError(fs.MkdirAll(backupDir))
|
||||
return fs
|
||||
},
|
||||
},
|
||||
"old workspace does not exist": {
|
||||
"existing workspace does not exist": {
|
||||
prepareFs: func(require *require.Assertions) file.Handler {
|
||||
fs := file.NewHandler(afero.NewMemMapFs())
|
||||
require.NoError(fs.MkdirAll(upgradeWorkingDir))
|
||||
require.NoError(fs.MkdirAll(backupDir))
|
||||
return fs
|
||||
},
|
||||
},
|
||||
"upgrade working dir does not exist": {
|
||||
"backup dir does not exist": {
|
||||
prepareFs: func(require *require.Assertions) file.Handler {
|
||||
fs := file.NewHandler(afero.NewMemMapFs())
|
||||
require.NoError(fs.MkdirAll(existingWorkspace))
|
||||
|
@ -135,8 +135,7 @@ func TestMoveUpgradeToCurrent(t *testing.T) {
|
|||
memFS := afero.NewMemMapFs()
|
||||
fs := file.NewHandler(memFS)
|
||||
require.NoError(fs.MkdirAll(existingWorkspace))
|
||||
require.NoError(fs.MkdirAll(upgradeWorkingDir))
|
||||
|
||||
require.NoError(fs.MkdirAll(backupDir))
|
||||
return file.NewHandler(afero.NewReadOnlyFs(memFS))
|
||||
},
|
||||
wantErr: true,
|
||||
|
@ -148,7 +147,7 @@ func TestMoveUpgradeToCurrent(t *testing.T) {
|
|||
assert := assert.New(t)
|
||||
fs := tc.prepareFs(require.New(t))
|
||||
|
||||
err := moveUpgradeToCurrent(fs, existingWorkspace, upgradeWorkingDir)
|
||||
err := restoreBackup(fs, existingWorkspace, backupDir)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
|
@ -209,7 +208,7 @@ type stubUpgradePlanner struct {
|
|||
showPlanErr error
|
||||
}
|
||||
|
||||
func (s *stubUpgradePlanner) PrepareUpgradeWorkspace(_, _ string, _ string, _ terraform.Variables) error {
|
||||
func (s *stubUpgradePlanner) PrepareUpgradeWorkspace(_, _ string, _ terraform.Variables) error {
|
||||
return s.prepareWorkspaceErr
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue