mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-01-24 14:22:14 -05:00
cli: remove/refactor upgrade package (#2266)
* Move IAM migration client to cloudcmd package * Move Terraform Cluster upgrade client to cloudcmd package * Use hcl for creating Terraform IAM variables files * Unify terraform upgrade code * Rename some cloudcmd files for better clarity --------- Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
parent
3d5d291891
commit
0a911806d1
@ -6,13 +6,16 @@ go_library(
|
||||
srcs = [
|
||||
"clients.go",
|
||||
"cloudcmd.go",
|
||||
"clusterupgrade.go",
|
||||
"create.go",
|
||||
"iam.go",
|
||||
"iamupgrade.go",
|
||||
"patch.go",
|
||||
"rollback.go",
|
||||
"serviceaccount.go",
|
||||
"terminate.go",
|
||||
"terraform.go",
|
||||
"tfupgrade.go",
|
||||
"tfvars.go",
|
||||
"validators.go",
|
||||
],
|
||||
importpath = "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd",
|
||||
@ -31,6 +34,7 @@ go_library(
|
||||
"//internal/cloud/gcpshared",
|
||||
"//internal/cloud/openstack",
|
||||
"//internal/config",
|
||||
"//internal/constants",
|
||||
"//internal/file",
|
||||
"//internal/imagefetcher",
|
||||
"//internal/role",
|
||||
@ -45,11 +49,14 @@ go_test(
|
||||
name = "cloudcmd_test",
|
||||
srcs = [
|
||||
"clients_test.go",
|
||||
"clusterupgrade_test.go",
|
||||
"create_test.go",
|
||||
"iam_test.go",
|
||||
"iamupgrade_test.go",
|
||||
"patch_test.go",
|
||||
"rollback_test.go",
|
||||
"terminate_test.go",
|
||||
"tfupgrade_test.go",
|
||||
"validators_test.go",
|
||||
],
|
||||
embed = [":cloudcmd"],
|
||||
@ -60,6 +67,9 @@ go_test(
|
||||
"//internal/cloud/cloudprovider",
|
||||
"//internal/cloud/gcpshared",
|
||||
"//internal/config",
|
||||
"//internal/constants",
|
||||
"//internal/file",
|
||||
"@com_github_spf13_afero//:afero",
|
||||
"@com_github_stretchr_testify//assert",
|
||||
"@com_github_stretchr_testify//require",
|
||||
"@org_uber_go_goleak//:goleak",
|
||||
|
@ -42,6 +42,22 @@ type tfIAMClient interface {
|
||||
ShowIAM(ctx context.Context, provider cloudprovider.Provider) (terraform.IAMOutput, error)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type tfIAMUpgradeClient interface {
|
||||
tfUpgradePlanner
|
||||
ApplyIAM(ctx context.Context, csp cloudprovider.Provider, logLevel terraform.LogLevel) (terraform.IAMOutput, error)
|
||||
}
|
||||
|
||||
type tfClusterUpgradeClient interface {
|
||||
tfUpgradePlanner
|
||||
ApplyCluster(ctx context.Context, provider cloudprovider.Provider, logLevel terraform.LogLevel) (terraform.ApplyOutput, error)
|
||||
}
|
||||
|
||||
type libvirtRunner interface {
|
||||
Start(ctx context.Context, containerName, imageName string) error
|
||||
Stop(ctx context.Context) error
|
||||
|
87
cli/internal/cloudcmd/clusterupgrade.go
Normal file
87
cli/internal/cloudcmd/clusterupgrade.go
Normal file
@ -0,0 +1,87 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package cloudcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// ClusterUpgrader is responsible for performing Terraform migrations on cluster upgrades.
|
||||
type ClusterUpgrader struct {
|
||||
tf tfClusterUpgradeClient
|
||||
policyPatcher policyPatcher
|
||||
fileHandler file.Handler
|
||||
existingWorkspace string
|
||||
upgradeWorkspace string
|
||||
logLevel terraform.LogLevel
|
||||
}
|
||||
|
||||
// NewClusterUpgrader initializes and returns a new ClusterUpgrader.
|
||||
// existingWorkspace is the directory holding the existing Terraform resources.
|
||||
// upgradeWorkspace is the directory to use for holding temporary files and resources required to apply the upgrade.
|
||||
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))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setting up terraform client: %w", err)
|
||||
}
|
||||
|
||||
return &ClusterUpgrader{
|
||||
tf: tfClient,
|
||||
policyPatcher: NewAzurePolicyPatcher(),
|
||||
fileHandler: fileHandler,
|
||||
existingWorkspace: existingWorkspace,
|
||||
upgradeWorkspace: upgradeWorkspace,
|
||||
logLevel: logLevel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PlanClusterUpgrade prepares the upgrade workspace and plans the possible Terraform migrations for Constellation's cluster resources (Loadbalancers, VMs, networks etc.).
|
||||
// In case of possible migrations, the diff is written to outWriter and this function returns true.
|
||||
func (u *ClusterUpgrader) PlanClusterUpgrade(ctx context.Context, outWriter io.Writer, vars terraform.Variables, csp cloudprovider.Provider,
|
||||
) (bool, error) {
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
tfOutput, err := u.tf.ApplyCluster(ctx, csp, u.logLevel)
|
||||
if err != nil {
|
||||
return tfOutput, fmt.Errorf("terraform apply: %w", err)
|
||||
}
|
||||
if tfOutput.Azure != nil {
|
||||
if err := u.policyPatcher.Patch(ctx, tfOutput.Azure.AttestationURL); err != nil {
|
||||
return tfOutput, fmt.Errorf("patching policies: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
203
cli/internal/cloudcmd/clusterupgrade_test.go
Normal file
203
cli/internal/cloudcmd/clusterupgrade_test.go
Normal file
@ -0,0 +1,203 @@
|
||||
/*
|
||||
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 TestPlanClusterUpgrade(t *testing.T) {
|
||||
setUpFilesystem := 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 {
|
||||
upgradeID string
|
||||
tf *tfClusterUpgradeStub
|
||||
fs file.Handler
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
"success no diff": {
|
||||
upgradeID: "1234",
|
||||
tf: &tfClusterUpgradeStub{},
|
||||
fs: setUpFilesystem([]string{}),
|
||||
},
|
||||
"success diff": {
|
||||
upgradeID: "1234",
|
||||
tf: &tfClusterUpgradeStub{
|
||||
planDiff: true,
|
||||
},
|
||||
fs: setUpFilesystem([]string{}),
|
||||
want: true,
|
||||
},
|
||||
"prepare workspace error": {
|
||||
upgradeID: "1234",
|
||||
tf: &tfClusterUpgradeStub{
|
||||
prepareWorkspaceErr: assert.AnError,
|
||||
},
|
||||
fs: setUpFilesystem([]string{}),
|
||||
wantErr: true,
|
||||
},
|
||||
"plan error": {
|
||||
tf: &tfClusterUpgradeStub{
|
||||
planErr: assert.AnError,
|
||||
},
|
||||
fs: setUpFilesystem([]string{}),
|
||||
wantErr: true,
|
||||
},
|
||||
"show plan error no diff": {
|
||||
upgradeID: "1234",
|
||||
tf: &tfClusterUpgradeStub{
|
||||
showErr: assert.AnError,
|
||||
},
|
||||
fs: setUpFilesystem([]string{}),
|
||||
},
|
||||
"show plan error diff": {
|
||||
upgradeID: "1234",
|
||||
tf: &tfClusterUpgradeStub{
|
||||
showErr: assert.AnError,
|
||||
planDiff: true,
|
||||
},
|
||||
fs: setUpFilesystem([]string{}),
|
||||
wantErr: true,
|
||||
},
|
||||
"workspace not clean": {
|
||||
upgradeID: "1234",
|
||||
tf: &tfClusterUpgradeStub{},
|
||||
fs: setUpFilesystem([]string{filepath.Join(constants.UpgradeDir, "1234", constants.TerraformUpgradeBackupDir)}),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
u := &ClusterUpgrader{
|
||||
tf: tc.tf,
|
||||
policyPatcher: stubPolicyPatcher{},
|
||||
fileHandler: tc.fs,
|
||||
upgradeWorkspace: filepath.Join(constants.UpgradeDir, tc.upgradeID),
|
||||
existingWorkspace: "test",
|
||||
logLevel: terraform.LogLevelDebug,
|
||||
}
|
||||
|
||||
diff, err := u.PlanClusterUpgrade(context.Background(), io.Discard, &terraform.QEMUVariables{}, cloudprovider.Unknown)
|
||||
if tc.wantErr {
|
||||
require.Error(err)
|
||||
} else {
|
||||
require.NoError(err)
|
||||
require.Equal(tc.want, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyClusterUpgrade(t *testing.T) {
|
||||
setUpFilesystem := func(upgradeID string, existingFiles ...string) file.Handler {
|
||||
fh := file.NewHandler(afero.NewMemMapFs())
|
||||
|
||||
require.NoError(t,
|
||||
fh.Write(
|
||||
filepath.Join(constants.UpgradeDir, upgradeID, constants.TerraformUpgradeWorkingDir, "someFile"),
|
||||
[]byte("some content"),
|
||||
))
|
||||
for _, f := range existingFiles {
|
||||
require.NoError(t, fh.Write(f, []byte("some content")))
|
||||
}
|
||||
return fh
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
upgradeID string
|
||||
tf *tfClusterUpgradeStub
|
||||
policyPatcher stubPolicyPatcher
|
||||
fs file.Handler
|
||||
wantErr bool
|
||||
}{
|
||||
"success": {
|
||||
upgradeID: "1234",
|
||||
tf: &tfClusterUpgradeStub{},
|
||||
fs: setUpFilesystem("1234"),
|
||||
policyPatcher: stubPolicyPatcher{},
|
||||
},
|
||||
"apply error": {
|
||||
upgradeID: "1234",
|
||||
tf: &tfClusterUpgradeStub{
|
||||
applyErr: assert.AnError,
|
||||
},
|
||||
fs: setUpFilesystem("1234"),
|
||||
policyPatcher: stubPolicyPatcher{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
tc.tf.file = tc.fs
|
||||
u := &ClusterUpgrader{
|
||||
tf: tc.tf,
|
||||
policyPatcher: stubPolicyPatcher{},
|
||||
fileHandler: tc.fs,
|
||||
upgradeWorkspace: filepath.Join(constants.UpgradeDir, tc.upgradeID),
|
||||
existingWorkspace: "test",
|
||||
logLevel: terraform.LogLevelDebug,
|
||||
}
|
||||
|
||||
_, err := u.ApplyClusterUpgrade(context.Background(), cloudprovider.Unknown)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type tfClusterUpgradeStub struct {
|
||||
file file.Handler
|
||||
applyErr error
|
||||
planErr error
|
||||
planDiff bool
|
||||
showErr error
|
||||
prepareWorkspaceErr error
|
||||
}
|
||||
|
||||
func (t *tfClusterUpgradeStub) Plan(_ context.Context, _ terraform.LogLevel) (bool, error) {
|
||||
return t.planDiff, t.planErr
|
||||
}
|
||||
|
||||
func (t *tfClusterUpgradeStub) ShowPlan(_ context.Context, _ terraform.LogLevel, _ io.Writer) error {
|
||||
return t.showErr
|
||||
}
|
||||
|
||||
func (t *tfClusterUpgradeStub) ApplyCluster(_ context.Context, _ cloudprovider.Provider, _ terraform.LogLevel) (terraform.ApplyOutput, error) {
|
||||
return terraform.ApplyOutput{}, t.applyErr
|
||||
}
|
||||
|
||||
func (t *tfClusterUpgradeStub) PrepareUpgradeWorkspace(_, _, _ string, _ terraform.Variables) error {
|
||||
return t.prepareWorkspaceErr
|
||||
}
|
78
cli/internal/cloudcmd/iamupgrade.go
Normal file
78
cli/internal/cloudcmd/iamupgrade.go
Normal file
@ -0,0 +1,78 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package cloudcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// IAMUpgrader handles upgrades to IAM resources required by Constellation.
|
||||
type IAMUpgrader struct {
|
||||
tf tfIAMUpgradeClient
|
||||
existingWorkspace string
|
||||
upgradeWorkspace string
|
||||
fileHandler file.Handler
|
||||
logLevel terraform.LogLevel
|
||||
}
|
||||
|
||||
// NewIAMUpgrader creates and initializes a new IAMUpgrader.
|
||||
// existingWorkspace is the directory holding the existing Terraform resources.
|
||||
// upgradeWorkspace is the directory to use for holding temporary files and resources required to apply the upgrade.
|
||||
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))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setting up terraform client: %w", err)
|
||||
}
|
||||
|
||||
return &IAMUpgrader{
|
||||
tf: tfClient,
|
||||
existingWorkspace: existingWorkspace,
|
||||
upgradeWorkspace: upgradeWorkspace,
|
||||
fileHandler: fileHandler,
|
||||
logLevel: logLevel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PlanIAMUpgrade prepares the upgrade workspace and plans the possible Terraform migrations for Constellation's IAM resources (service accounts, permissions etc.).
|
||||
// In case of possible migrations, the diff is written to outWriter and this function returns true.
|
||||
func (u *IAMUpgrader) PlanIAMUpgrade(ctx context.Context, outWriter io.Writer, vars terraform.Variables, csp cloudprovider.Provider) (bool, error) {
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if _, err := u.tf.ApplyIAM(ctx, csp, u.logLevel); err != nil {
|
||||
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
|
||||
}
|
136
cli/internal/cloudcmd/iamupgrade_test.go
Normal file
136
cli/internal/cloudcmd/iamupgrade_test.go
Normal file
@ -0,0 +1,136 @@
|
||||
/*
|
||||
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"))
|
||||
}
|
82
cli/internal/cloudcmd/tfupgrade.go
Normal file
82
cli/internal/cloudcmd/tfupgrade.go
Normal file
@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package cloudcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||
)
|
||||
|
||||
// planUpgrade prepares a workspace and plans the possible Terraform migrations.
|
||||
// In case of possible migrations, the diff is written to outWriter and this function returns true.
|
||||
func planUpgrade(
|
||||
ctx context.Context, tfClient tfUpgradePlanner, fileHandler file.Handler,
|
||||
outWriter io.Writer, logLevel terraform.LogLevel, vars terraform.Variables,
|
||||
templateDir, existingWorkspace, backupDir string,
|
||||
) (bool, error) {
|
||||
if err := ensureFileNotExist(fileHandler, backupDir); err != nil {
|
||||
return false, fmt.Errorf("workspace is not clean: %w", err)
|
||||
}
|
||||
|
||||
// Prepare the new Terraform workspace and backup the old one
|
||||
err := tfClient.PrepareUpgradeWorkspace(
|
||||
templateDir,
|
||||
existingWorkspace,
|
||||
backupDir,
|
||||
vars,
|
||||
)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("preparing terraform workspace: %w", err)
|
||||
}
|
||||
|
||||
hasDiff, err := tfClient.Plan(ctx, logLevel)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("terraform plan: %w", err)
|
||||
}
|
||||
|
||||
if hasDiff {
|
||||
if err := tfClient.ShowPlan(ctx, logLevel, outWriter); err != nil {
|
||||
return false, fmt.Errorf("terraform show plan: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if err := fileHandler.CopyDir(
|
||||
upgradeWorkingDir,
|
||||
existingWorkspace,
|
||||
); err != nil {
|
||||
return fmt.Errorf("replacing old terraform directory with new one: %w", err)
|
||||
}
|
||||
|
||||
if err := fileHandler.RemoveAll(upgradeWorkingDir); err != nil {
|
||||
return fmt.Errorf("removing terraform upgrade directory: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureFileNotExist checks if a single file or directory does not exist, returning an error if it does.
|
||||
func ensureFileNotExist(fileHandler file.Handler, fileName string) error {
|
||||
if _, err := fileHandler.Stat(fileName); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("checking %q: %w", fileName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%q already exists", fileName)
|
||||
}
|
222
cli/internal/cloudcmd/tfupgrade_test.go
Normal file
222
cli/internal/cloudcmd/tfupgrade_test.go
Normal file
@ -0,0 +1,222 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package cloudcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPlanUpgrade(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
prepareFs func(require *require.Assertions) file.Handler
|
||||
tf *stubUpgradePlanner
|
||||
wantDiff bool
|
||||
wantErr bool
|
||||
}{
|
||||
"success no diff": {
|
||||
prepareFs: func(require *require.Assertions) file.Handler {
|
||||
return file.NewHandler(afero.NewMemMapFs())
|
||||
},
|
||||
tf: &stubUpgradePlanner{},
|
||||
},
|
||||
"success diff": {
|
||||
prepareFs: func(require *require.Assertions) file.Handler {
|
||||
return file.NewHandler(afero.NewMemMapFs())
|
||||
},
|
||||
tf: &stubUpgradePlanner{
|
||||
planDiff: true,
|
||||
},
|
||||
wantDiff: true,
|
||||
},
|
||||
"workspace not clean": {
|
||||
prepareFs: func(require *require.Assertions) file.Handler {
|
||||
fs := file.NewHandler(afero.NewMemMapFs())
|
||||
require.NoError(fs.MkdirAll("backup"))
|
||||
return fs
|
||||
},
|
||||
tf: &stubUpgradePlanner{},
|
||||
wantErr: true,
|
||||
},
|
||||
"prepare workspace error": {
|
||||
prepareFs: func(require *require.Assertions) file.Handler {
|
||||
return file.NewHandler(afero.NewMemMapFs())
|
||||
},
|
||||
tf: &stubUpgradePlanner{
|
||||
prepareWorkspaceErr: assert.AnError,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"plan error": {
|
||||
prepareFs: func(require *require.Assertions) file.Handler {
|
||||
return file.NewHandler(afero.NewMemMapFs())
|
||||
},
|
||||
tf: &stubUpgradePlanner{
|
||||
planErr: assert.AnError,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"show plan error": {
|
||||
prepareFs: func(require *require.Assertions) file.Handler {
|
||||
return file.NewHandler(afero.NewMemMapFs())
|
||||
},
|
||||
tf: &stubUpgradePlanner{
|
||||
planDiff: true,
|
||||
showPlanErr: assert.AnError,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
fs := tc.prepareFs(require.New(t))
|
||||
|
||||
hasDiff, err := planUpgrade(
|
||||
context.Background(), tc.tf, fs, io.Discard, terraform.LogLevelDebug,
|
||||
&terraform.QEMUVariables{},
|
||||
"existing", "upgrade", "backup",
|
||||
)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
assert.Equal(tc.wantDiff, hasDiff)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoveUpgradeToCurrent(t *testing.T) {
|
||||
existingWorkspace := "foo"
|
||||
upgradeWorkingDir := "bar"
|
||||
|
||||
testCases := map[string]struct {
|
||||
prepareFs func(require *require.Assertions) file.Handler
|
||||
wantErr bool
|
||||
}{
|
||||
"success": {
|
||||
prepareFs: func(require *require.Assertions) file.Handler {
|
||||
fs := file.NewHandler(afero.NewMemMapFs())
|
||||
require.NoError(fs.MkdirAll(existingWorkspace))
|
||||
require.NoError(fs.MkdirAll(upgradeWorkingDir))
|
||||
return fs
|
||||
},
|
||||
},
|
||||
"old workspace does not exist": {
|
||||
prepareFs: func(require *require.Assertions) file.Handler {
|
||||
fs := file.NewHandler(afero.NewMemMapFs())
|
||||
require.NoError(fs.MkdirAll(upgradeWorkingDir))
|
||||
return fs
|
||||
},
|
||||
},
|
||||
"upgrade working dir does not exist": {
|
||||
prepareFs: func(require *require.Assertions) file.Handler {
|
||||
fs := file.NewHandler(afero.NewMemMapFs())
|
||||
require.NoError(fs.MkdirAll(existingWorkspace))
|
||||
return fs
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"read only file system": {
|
||||
prepareFs: func(require *require.Assertions) file.Handler {
|
||||
memFS := afero.NewMemMapFs()
|
||||
fs := file.NewHandler(memFS)
|
||||
require.NoError(fs.MkdirAll(existingWorkspace))
|
||||
require.NoError(fs.MkdirAll(upgradeWorkingDir))
|
||||
|
||||
return file.NewHandler(afero.NewReadOnlyFs(memFS))
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
fs := tc.prepareFs(require.New(t))
|
||||
|
||||
err := moveUpgradeToCurrent(fs, existingWorkspace, upgradeWorkingDir)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureFileNotExist(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
fs file.Handler
|
||||
fileName string
|
||||
wantErr bool
|
||||
}{
|
||||
"file does not exist": {
|
||||
fs: file.NewHandler(afero.NewMemMapFs()),
|
||||
fileName: "foo",
|
||||
},
|
||||
"file exists": {
|
||||
fs: func() file.Handler {
|
||||
fs := file.NewHandler(afero.NewMemMapFs())
|
||||
err := fs.Write("foo", []byte{})
|
||||
require.NoError(t, err)
|
||||
return fs
|
||||
}(),
|
||||
fileName: "foo",
|
||||
wantErr: true,
|
||||
},
|
||||
"directory exists": {
|
||||
fs: func() file.Handler {
|
||||
fs := file.NewHandler(afero.NewMemMapFs())
|
||||
err := fs.MkdirAll("foo/bar")
|
||||
require.NoError(t, err)
|
||||
return fs
|
||||
}(),
|
||||
fileName: "foo/bar",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := ensureFileNotExist(tc.fs, tc.fileName)
|
||||
if tc.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type stubUpgradePlanner struct {
|
||||
prepareWorkspaceErr error
|
||||
planDiff bool
|
||||
planErr error
|
||||
showPlanErr error
|
||||
}
|
||||
|
||||
func (s *stubUpgradePlanner) PrepareUpgradeWorkspace(_, _ string, _ string, _ terraform.Variables) error {
|
||||
return s.prepareWorkspaceErr
|
||||
}
|
||||
|
||||
func (s *stubUpgradePlanner) Plan(_ context.Context, _ terraform.LogLevel) (bool, error) {
|
||||
return s.planDiff, s.planErr
|
||||
}
|
||||
|
||||
func (s *stubUpgradePlanner) ShowPlan(_ context.Context, _ terraform.LogLevel, _ io.Writer) error {
|
||||
return s.showPlanErr
|
||||
}
|
@ -8,12 +8,15 @@ package cloudcmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||
"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/role"
|
||||
)
|
||||
|
||||
@ -25,19 +28,54 @@ func TerraformUpgradeVars(conf *config.Config) (terraform.Variables, error) {
|
||||
// For AWS, we enforce some basic constraints on the image variable.
|
||||
// For Azure, the provider enforces the format below.
|
||||
// For GCP, any placeholder works.
|
||||
var vars terraform.Variables
|
||||
switch conf.GetProvider() {
|
||||
case cloudprovider.AWS:
|
||||
vars := awsTerraformVars(conf, "ami-placeholder")
|
||||
return vars, nil
|
||||
vars = awsTerraformVars(conf, "ami-placeholder")
|
||||
case cloudprovider.Azure:
|
||||
vars := azureTerraformVars(conf, "/communityGalleries/myGalleryName/images/myImageName/versions/latest")
|
||||
return vars, nil
|
||||
vars = azureTerraformVars(conf, "/communityGalleries/myGalleryName/images/myImageName/versions/latest")
|
||||
case cloudprovider.GCP:
|
||||
vars := gcpTerraformVars(conf, "placeholder")
|
||||
return vars, nil
|
||||
vars = gcpTerraformVars(conf, "placeholder")
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported provider: %s", conf.GetProvider())
|
||||
}
|
||||
return vars, nil
|
||||
}
|
||||
|
||||
// TerraformIAMUpgradeVars returns variables required to execute IAM upgrades with Terraform.
|
||||
func TerraformIAMUpgradeVars(conf *config.Config, fileHandler file.Handler) (terraform.Variables, error) {
|
||||
// Load the tfvars of the existing IAM workspace.
|
||||
// Ideally we would only load values from the config file, but this currently does not hold all values required.
|
||||
// This should be refactored in the future.
|
||||
oldVarBytes, err := fileHandler.Read(filepath.Join(constants.TerraformIAMWorkingDir, "terraform.tfvars"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading existing IAM workspace: %w", err)
|
||||
}
|
||||
|
||||
var vars terraform.Variables
|
||||
switch conf.GetProvider() {
|
||||
case cloudprovider.AWS:
|
||||
var oldVars terraform.AWSIAMVariables
|
||||
if err := terraform.VariablesFromBytes(oldVarBytes, &oldVars); err != nil {
|
||||
return nil, fmt.Errorf("parsing existing IAM workspace: %w", err)
|
||||
}
|
||||
vars = awsTerraformIAMVars(conf, oldVars)
|
||||
case cloudprovider.Azure:
|
||||
var oldVars terraform.AzureIAMVariables
|
||||
if err := terraform.VariablesFromBytes(oldVarBytes, &oldVars); err != nil {
|
||||
return nil, fmt.Errorf("parsing existing IAM workspace: %w", err)
|
||||
}
|
||||
vars = azureTerraformIAMVars(conf, oldVars)
|
||||
case cloudprovider.GCP:
|
||||
var oldVars terraform.GCPIAMVariables
|
||||
if err := terraform.VariablesFromBytes(oldVarBytes, &oldVars); err != nil {
|
||||
return nil, fmt.Errorf("parsing existing IAM workspace: %w", err)
|
||||
}
|
||||
vars = gcpTerraformIAMVars(conf, oldVars)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported provider: %s", conf.GetProvider())
|
||||
}
|
||||
return vars, nil
|
||||
}
|
||||
|
||||
// awsTerraformVars provides variables required to execute the Terraform scripts.
|
||||
@ -68,6 +106,13 @@ func awsTerraformVars(conf *config.Config, imageRef string) *terraform.AWSCluste
|
||||
}
|
||||
}
|
||||
|
||||
func awsTerraformIAMVars(conf *config.Config, oldVars terraform.AWSIAMVariables) *terraform.AWSIAMVariables {
|
||||
return &terraform.AWSIAMVariables{
|
||||
Region: conf.Provider.AWS.Region,
|
||||
Prefix: oldVars.Prefix,
|
||||
}
|
||||
}
|
||||
|
||||
// azureTerraformVars provides variables required to execute the Terraform scripts.
|
||||
// It should be the only place to declare the Azure variables.
|
||||
func azureTerraformVars(conf *config.Config, imageRef string) *terraform.AzureClusterVariables {
|
||||
@ -104,6 +149,14 @@ func azureTerraformVars(conf *config.Config, imageRef string) *terraform.AzureCl
|
||||
return vars
|
||||
}
|
||||
|
||||
func azureTerraformIAMVars(conf *config.Config, oldVars terraform.AzureIAMVariables) *terraform.AzureIAMVariables {
|
||||
return &terraform.AzureIAMVariables{
|
||||
Region: conf.Provider.Azure.Location,
|
||||
ServicePrincipal: oldVars.ServicePrincipal,
|
||||
ResourceGroup: conf.Provider.Azure.ResourceGroup,
|
||||
}
|
||||
}
|
||||
|
||||
// gcpTerraformVars provides variables required to execute the Terraform scripts.
|
||||
// It should be the only place to declare the GCP variables.
|
||||
func gcpTerraformVars(conf *config.Config, imageRef string) *terraform.GCPClusterVariables {
|
||||
@ -130,6 +183,15 @@ func gcpTerraformVars(conf *config.Config, imageRef string) *terraform.GCPCluste
|
||||
}
|
||||
}
|
||||
|
||||
func gcpTerraformIAMVars(conf *config.Config, oldVars terraform.GCPIAMVariables) *terraform.GCPIAMVariables {
|
||||
return &terraform.GCPIAMVariables{
|
||||
Project: conf.Provider.GCP.Project,
|
||||
Region: conf.Provider.GCP.Region,
|
||||
Zone: conf.Provider.GCP.Zone,
|
||||
ServiceAccountID: oldVars.ServiceAccountID,
|
||||
}
|
||||
}
|
||||
|
||||
// openStackTerraformVars provides variables required to execute the Terraform scripts.
|
||||
// It should be the only place to declare the OpenStack variables.
|
||||
func openStackTerraformVars(conf *config.Config, imageRef string) *terraform.OpenStackClusterVariables {
|
@ -27,7 +27,6 @@ go_library(
|
||||
"spinner.go",
|
||||
"status.go",
|
||||
"terminate.go",
|
||||
"tfmigrationclient.go",
|
||||
"upgrade.go",
|
||||
"upgradeapply.go",
|
||||
"upgradecheck.go",
|
||||
@ -48,7 +47,6 @@ go_library(
|
||||
"//cli/internal/kubecmd",
|
||||
"//cli/internal/libvirt",
|
||||
"//cli/internal/terraform",
|
||||
"//cli/internal/upgrade",
|
||||
"//disk-mapper/recoverproto",
|
||||
"//internal/api/attestationconfigapi",
|
||||
"//internal/api/fetcher",
|
||||
@ -139,7 +137,6 @@ go_test(
|
||||
"//cli/internal/helm",
|
||||
"//cli/internal/kubecmd",
|
||||
"//cli/internal/terraform",
|
||||
"//cli/internal/upgrade",
|
||||
"//disk-mapper/recoverproto",
|
||||
"//internal/api/attestationconfigapi",
|
||||
"//internal/api/versionsapi",
|
||||
|
@ -6,11 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/upgrade"
|
||||
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
|
||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||
"github.com/edgelesssys/constellation/v2/internal/config"
|
||||
@ -50,6 +53,12 @@ func newIAMUpgradeApplyCmd() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
type iamUpgradeApplyCmd struct {
|
||||
fileHandler file.Handler
|
||||
configFetcher attestationconfigapi.Fetcher
|
||||
log debugLog
|
||||
}
|
||||
|
||||
func runIAMUpgradeApply(cmd *cobra.Command, _ []string) error {
|
||||
force, err := cmd.Flags().GetBool("force")
|
||||
if err != nil {
|
||||
@ -57,17 +66,16 @@ func runIAMUpgradeApply(cmd *cobra.Command, _ []string) error {
|
||||
}
|
||||
fileHandler := file.NewHandler(afero.NewOsFs())
|
||||
configFetcher := attestationconfigapi.NewFetcher()
|
||||
conf, err := config.New(fileHandler, constants.ConfigFilename, configFetcher, force)
|
||||
var configValidationErr *config.ValidationError
|
||||
if errors.As(err, &configValidationErr) {
|
||||
cmd.PrintErrln(configValidationErr.LongMessage())
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
upgradeID := generateUpgradeID(upgradeCmdKindIAM)
|
||||
iamMigrateCmd, err := upgrade.NewIAMMigrateCmd(cmd.Context(), constants.TerraformIAMWorkingDir, constants.UpgradeDir, upgradeID, conf.GetProvider(), terraform.LogLevelDebug)
|
||||
upgradeDir := filepath.Join(constants.UpgradeDir, upgradeID)
|
||||
iamMigrateCmd, err := cloudcmd.NewIAMUpgrader(
|
||||
cmd.Context(),
|
||||
constants.TerraformIAMWorkingDir,
|
||||
upgradeDir,
|
||||
terraform.LogLevelDebug,
|
||||
fileHandler,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up IAM migration command: %w", err)
|
||||
}
|
||||
@ -76,16 +84,71 @@ func runIAMUpgradeApply(cmd *cobra.Command, _ []string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up logger: %w", err)
|
||||
}
|
||||
migrator := &tfMigrationClient{log}
|
||||
|
||||
yes, err := cmd.Flags().GetBool("yes")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := migrator.applyMigration(cmd, constants.UpgradeDir, file.NewHandler(afero.NewOsFs()), iamMigrateCmd, yes); err != nil {
|
||||
return fmt.Errorf("applying IAM migration: %w", err)
|
||||
|
||||
i := iamUpgradeApplyCmd{
|
||||
fileHandler: fileHandler,
|
||||
configFetcher: configFetcher,
|
||||
log: log,
|
||||
}
|
||||
|
||||
return i.iamUpgradeApply(cmd, iamMigrateCmd, upgradeDir, force, yes)
|
||||
}
|
||||
|
||||
func (i iamUpgradeApplyCmd) iamUpgradeApply(cmd *cobra.Command, iamUpgrader iamUpgrader, upgradeDir string, force, yes bool) error {
|
||||
conf, err := config.New(i.fileHandler, constants.ConfigFilename, i.configFetcher, force)
|
||||
var configValidationErr *config.ValidationError
|
||||
if errors.As(err, &configValidationErr) {
|
||||
cmd.PrintErrln(configValidationErr.LongMessage())
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vars, err := cloudcmd.TerraformIAMUpgradeVars(conf, i.fileHandler)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting terraform variables: %w", err)
|
||||
}
|
||||
hasDiff, err := iamUpgrader.PlanIAMUpgrade(cmd.Context(), cmd.OutOrStderr(), vars, conf.GetProvider())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasDiff && !force {
|
||||
cmd.Println("No IAM migrations necessary.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// If there are any Terraform migrations to apply, ask for confirmation
|
||||
cmd.Println("The IAM upgrade requires a migration by applying an updated Terraform template. Please manually review the suggested changes.")
|
||||
if !yes {
|
||||
ok, err := askToConfirm(cmd, "Do you want to apply the IAM upgrade?")
|
||||
if err != nil {
|
||||
return fmt.Errorf("asking for confirmation: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
cmd.Println("Aborting upgrade.")
|
||||
// Remove the upgrade directory
|
||||
if err := i.fileHandler.RemoveAll(upgradeDir); err != nil {
|
||||
return fmt.Errorf("cleaning up upgrade directory %s: %w", upgradeDir, err)
|
||||
}
|
||||
return errors.New("IAM upgrade aborted by user")
|
||||
}
|
||||
}
|
||||
i.log.Debugf("Applying Terraform IAM migrations")
|
||||
if err := iamUpgrader.ApplyIAMUpgrade(cmd.Context(), conf.GetProvider()); err != nil {
|
||||
return fmt.Errorf("applying terraform migrations: %w", err)
|
||||
}
|
||||
|
||||
cmd.Println("IAM profile successfully applied.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type iamUpgrader interface {
|
||||
PlanIAMUpgrade(ctx context.Context, outWriter io.Writer, vars terraform.Variables, csp cloudprovider.Provider) (bool, error)
|
||||
ApplyIAMUpgrade(ctx context.Context, csp cloudprovider.Provider) error
|
||||
}
|
||||
|
@ -53,10 +53,8 @@ func runStatus(cmd *cobra.Command, _ []string) error {
|
||||
|
||||
fileHandler := file.NewHandler(afero.NewOsFs())
|
||||
|
||||
// need helm client to fetch service versions.
|
||||
// The client used here, doesn't need to know the current workspace.
|
||||
// It may be refactored in the future for easier usage.
|
||||
helmClient, err := helm.NewUpgradeClient(kubectl.NewUninitialized(), constants.UpgradeDir, constants.AdminConfFilename, constants.HelmNamespace, log)
|
||||
// set up helm client to fetch service versions
|
||||
helmClient, err := helm.NewUpgradeClient(kubectl.NewUninitialized(), constants.AdminConfFilename, constants.HelmNamespace, log)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up helm client: %w", err)
|
||||
}
|
||||
|
@ -1,79 +0,0 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/upgrade"
|
||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// tfMigrationClient is a client for planning and applying Terraform migrations.
|
||||
type tfMigrationClient struct {
|
||||
log debugLog
|
||||
}
|
||||
|
||||
// planMigration checks for Terraform migrations and asks for confirmation if there are any. The user input is returned as confirmedDiff.
|
||||
// adapted from migrateTerraform().
|
||||
func (u *tfMigrationClient) planMigration(cmd *cobra.Command, file file.Handler, migrateCmd tfMigrationCmd) (hasDiff bool, err error) {
|
||||
u.log.Debugf("Planning %s", migrateCmd.String())
|
||||
if err := migrateCmd.CheckTerraformMigrations(file); err != nil {
|
||||
return false, fmt.Errorf("checking workspace: %w", err)
|
||||
}
|
||||
hasDiff, err = migrateCmd.Plan(cmd.Context(), file, cmd.OutOrStdout())
|
||||
if err != nil {
|
||||
return hasDiff, fmt.Errorf("planning terraform migrations: %w", err)
|
||||
}
|
||||
return hasDiff, nil
|
||||
}
|
||||
|
||||
// applyMigration plans and then applies the Terraform migration. The user is asked for confirmation if there are any changes.
|
||||
// adapted from migrateTerraform().
|
||||
func (u *tfMigrationClient) applyMigration(cmd *cobra.Command, upgradeWorkspace string, file file.Handler, migrateCmd tfMigrationCmd, yesFlag bool) error {
|
||||
hasDiff, err := u.planMigration(cmd, file, migrateCmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasDiff {
|
||||
// If there are any Terraform migrations to apply, ask for confirmation
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "The %s upgrade requires a migration by applying an updated Terraform template. Please manually review the suggested changes.\n", migrateCmd.String())
|
||||
if !yesFlag {
|
||||
ok, err := askToConfirm(cmd, fmt.Sprintf("Do you want to apply the %s?", migrateCmd.String()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("asking for confirmation: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
cmd.Println("Aborting upgrade.")
|
||||
if err := upgrade.CleanUpTerraformMigrations(upgradeWorkspace, migrateCmd.UpgradeID(), file); err != nil {
|
||||
return fmt.Errorf("cleaning up workspace: %w", err)
|
||||
}
|
||||
return fmt.Errorf("aborted by user")
|
||||
}
|
||||
}
|
||||
u.log.Debugf("Applying Terraform %s migrations", migrateCmd.String())
|
||||
err := migrateCmd.Apply(cmd.Context(), file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("applying terraform migrations: %w", err)
|
||||
}
|
||||
} else {
|
||||
u.log.Debugf("No Terraform diff detected")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tfMigrationCmd is an interface for all terraform upgrade / migration commands.
|
||||
type tfMigrationCmd interface {
|
||||
CheckTerraformMigrations(file file.Handler) error
|
||||
Plan(ctx context.Context, file file.Handler, outWriter io.Writer) (bool, error)
|
||||
Apply(ctx context.Context, fileHandler file.Handler) error
|
||||
String() string
|
||||
UpgradeID() string
|
||||
}
|
@ -10,6 +10,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
@ -19,7 +20,6 @@ import (
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/helm"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/kubecmd"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/upgrade"
|
||||
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
|
||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||
@ -61,6 +61,11 @@ func newUpgradeApplyCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
func runUpgradeApply(cmd *cobra.Command, _ []string) error {
|
||||
flags, err := parseUpgradeApplyFlags(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
log, err := newCLILogger(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating logger: %w", err)
|
||||
@ -75,53 +80,55 @@ func runUpgradeApply(cmd *cobra.Command, _ []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
helmUpgrader, err := helm.NewUpgradeClient(kubectl.NewUninitialized(), constants.UpgradeDir, constants.AdminConfFilename, constants.HelmNamespace, log)
|
||||
helmUpgrader, err := helm.NewUpgradeClient(kubectl.NewUninitialized(), constants.AdminConfFilename, constants.HelmNamespace, log)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up helm client: %w", err)
|
||||
}
|
||||
|
||||
configFetcher := attestationconfigapi.NewFetcher()
|
||||
|
||||
// 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))
|
||||
// Set up terraform upgrader
|
||||
upgradeDir := filepath.Join(constants.UpgradeDir, upgradeID)
|
||||
clusterUpgrader, err := cloudcmd.NewClusterUpgrader(
|
||||
cmd.Context(),
|
||||
constants.TerraformWorkingDir,
|
||||
upgradeDir,
|
||||
flags.terraformLogLevel,
|
||||
fileHandler,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up terraform client: %w", err)
|
||||
return fmt.Errorf("setting up cluster upgrader: %w", err)
|
||||
}
|
||||
// And one for showing existing resources
|
||||
|
||||
// Set up terraform client to show existing cluster resources and information required for Helm upgrades
|
||||
tfShower, err := terraform.New(cmd.Context(), constants.TerraformWorkingDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up terraform client: %w", err)
|
||||
}
|
||||
|
||||
applyCmd := upgradeApplyCmd{
|
||||
helmUpgrader: helmUpgrader,
|
||||
kubeUpgrader: kubeUpgrader,
|
||||
terraformUpgrader: upgrade.NewTerraformUpgrader(tfUpgrader, cmd.OutOrStdout(), fileHandler, upgradeID),
|
||||
configFetcher: configFetcher,
|
||||
clusterShower: tfShower,
|
||||
fileHandler: fileHandler,
|
||||
log: log,
|
||||
helmUpgrader: helmUpgrader,
|
||||
kubeUpgrader: kubeUpgrader,
|
||||
clusterUpgrader: clusterUpgrader,
|
||||
configFetcher: configFetcher,
|
||||
clusterShower: tfShower,
|
||||
fileHandler: fileHandler,
|
||||
log: log,
|
||||
}
|
||||
return applyCmd.upgradeApply(cmd)
|
||||
return applyCmd.upgradeApply(cmd, upgradeDir, flags)
|
||||
}
|
||||
|
||||
type upgradeApplyCmd struct {
|
||||
helmUpgrader helmUpgrader
|
||||
kubeUpgrader kubernetesUpgrader
|
||||
terraformUpgrader terraformUpgrader
|
||||
configFetcher attestationconfigapi.Fetcher
|
||||
clusterShower clusterShower
|
||||
fileHandler file.Handler
|
||||
log debugLog
|
||||
helmUpgrader helmUpgrader
|
||||
kubeUpgrader kubernetesUpgrader
|
||||
clusterUpgrader clusterUpgrader
|
||||
configFetcher attestationconfigapi.Fetcher
|
||||
clusterShower clusterShower
|
||||
fileHandler file.Handler
|
||||
log debugLog
|
||||
}
|
||||
|
||||
func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command) error {
|
||||
flags, err := parseUpgradeApplyFlags(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command, upgradeDir string, flags upgradeApplyFlags) error {
|
||||
conf, err := config.New(u.fileHandler, constants.ConfigFilename, u.configFetcher, flags.force)
|
||||
var configValidationErr *config.ValidationError
|
||||
if errors.As(err, &configValidationErr) {
|
||||
@ -166,8 +173,7 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command) error {
|
||||
return fmt.Errorf("upgrading measurements: %w", err)
|
||||
}
|
||||
|
||||
// not moving existing Terraform migrator because of planned apply refactor
|
||||
tfOutput, err := u.migrateTerraform(cmd, conf, flags)
|
||||
tfOutput, err := u.migrateTerraform(cmd, conf, upgradeDir, flags)
|
||||
if err != nil {
|
||||
return fmt.Errorf("performing Terraform migrations: %w", err)
|
||||
}
|
||||
@ -192,7 +198,7 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command) error {
|
||||
}
|
||||
|
||||
var upgradeErr *compatibility.InvalidUpgradeError
|
||||
err = u.handleServiceUpgrade(cmd, conf, idFile, tfOutput, validK8sVersion, flags)
|
||||
err = u.handleServiceUpgrade(cmd, conf, idFile, tfOutput, validK8sVersion, upgradeDir, flags)
|
||||
switch {
|
||||
case errors.As(err, &upgradeErr):
|
||||
cmd.PrintErrln(err)
|
||||
@ -231,29 +237,16 @@ func diffAttestationCfg(currentAttestationCfg config.AttestationCfg, newAttestat
|
||||
|
||||
// migrateTerraform checks if the Constellation version the cluster is being upgraded to requires a migration
|
||||
// of cloud resources with Terraform. If so, the migration is performed.
|
||||
func (u *upgradeApplyCmd) migrateTerraform(
|
||||
cmd *cobra.Command, conf *config.Config, flags upgradeApplyFlags,
|
||||
func (u *upgradeApplyCmd) migrateTerraform(cmd *cobra.Command, conf *config.Config, upgradeDir string, flags upgradeApplyFlags,
|
||||
) (res terraform.ApplyOutput, err error) {
|
||||
u.log.Debugf("Planning Terraform migrations")
|
||||
|
||||
if err := u.terraformUpgrader.CheckTerraformMigrations(constants.UpgradeDir); err != nil {
|
||||
return res, fmt.Errorf("checking workspace: %w", err)
|
||||
}
|
||||
|
||||
vars, err := cloudcmd.TerraformUpgradeVars(conf)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("parsing upgrade variables: %w", err)
|
||||
}
|
||||
u.log.Debugf("Using Terraform variables:\n%v", vars)
|
||||
|
||||
opts := upgrade.TerraformUpgradeOptions{
|
||||
LogLevel: flags.terraformLogLevel,
|
||||
CSP: conf.GetProvider(),
|
||||
Vars: vars,
|
||||
TFWorkspace: constants.TerraformWorkingDir,
|
||||
UpgradeWorkspace: constants.UpgradeDir,
|
||||
}
|
||||
|
||||
// Check if there are any Terraform migrations to apply
|
||||
|
||||
// Add manual migrations here if required
|
||||
@ -264,7 +257,7 @@ func (u *upgradeApplyCmd) migrateTerraform(
|
||||
// u.upgrader.AddManualStateMigration(migration)
|
||||
// }
|
||||
|
||||
hasDiff, err := u.terraformUpgrader.PlanTerraformMigrations(cmd.Context(), opts)
|
||||
hasDiff, err := u.clusterUpgrader.PlanClusterUpgrade(cmd.Context(), cmd.OutOrStdout(), vars, conf.GetProvider())
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("planning terraform migrations: %w", err)
|
||||
}
|
||||
@ -279,28 +272,28 @@ func (u *upgradeApplyCmd) migrateTerraform(
|
||||
}
|
||||
if !ok {
|
||||
cmd.Println("Aborting upgrade.")
|
||||
if err := u.terraformUpgrader.CleanUpTerraformMigrations(constants.UpgradeDir); err != nil {
|
||||
return res, fmt.Errorf("cleaning up workspace: %w", err)
|
||||
// Remove the upgrade directory
|
||||
if err := u.fileHandler.RemoveAll(upgradeDir); err != nil {
|
||||
return res, fmt.Errorf("cleaning up upgrade directory %s: %w", upgradeDir, err)
|
||||
}
|
||||
return res, fmt.Errorf("aborted by user")
|
||||
return res, fmt.Errorf("cluster upgrade aborted by user")
|
||||
}
|
||||
}
|
||||
u.log.Debugf("Applying Terraform migrations")
|
||||
tfOutput, err := u.terraformUpgrader.ApplyTerraformMigrations(cmd.Context(), opts)
|
||||
tfOutput, err := u.clusterUpgrader.ApplyClusterUpgrade(cmd.Context(), conf.GetProvider())
|
||||
if err != nil {
|
||||
return tfOutput, fmt.Errorf("applying terraform migrations: %w", err)
|
||||
}
|
||||
|
||||
// Patch MAA policy if we applied an Azure upgrade.
|
||||
newIDFile := newIDFile(opts, tfOutput)
|
||||
if err := mergeClusterIDFile(constants.ClusterIDsFilename, newIDFile, u.fileHandler); err != nil {
|
||||
// Apply possible updates to cluster ID file
|
||||
if err := updateClusterIDFile(tfOutput, u.fileHandler); err != nil {
|
||||
return tfOutput, fmt.Errorf("merging cluster ID files: %w", err)
|
||||
}
|
||||
|
||||
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.PrefixPrintablePath(constants.ClusterIDsFilename),
|
||||
flags.pf.PrefixPrintablePath(filepath.Join(opts.UpgradeWorkspace, u.terraformUpgrader.UpgradeID(), constants.TerraformUpgradeBackupDir)),
|
||||
flags.pf.PrefixPrintablePath(filepath.Join(upgradeDir, constants.TerraformUpgradeBackupDir)),
|
||||
)
|
||||
} else {
|
||||
u.log.Debugf("No Terraform diff detected")
|
||||
@ -313,20 +306,6 @@ func (u *upgradeApplyCmd) migrateTerraform(
|
||||
return tfOutput, nil
|
||||
}
|
||||
|
||||
func newIDFile(opts upgrade.TerraformUpgradeOptions, tfOutput terraform.ApplyOutput) clusterid.File {
|
||||
newIDFile := clusterid.File{
|
||||
CloudProvider: opts.CSP,
|
||||
InitSecret: []byte(tfOutput.Secret),
|
||||
IP: tfOutput.IP,
|
||||
APIServerCertSANs: tfOutput.APIServerCertSANs,
|
||||
UID: tfOutput.UID,
|
||||
}
|
||||
if tfOutput.Azure != nil {
|
||||
newIDFile.AttestationURL = tfOutput.Azure.AttestationURL
|
||||
}
|
||||
return newIDFile
|
||||
}
|
||||
|
||||
// validK8sVersion checks if the Kubernetes patch version is supported and asks for confirmation if not.
|
||||
func validK8sVersion(cmd *cobra.Command, version string, yes bool) (validVersion versions.ValidK8sVersion, err error) {
|
||||
validVersion, err = versions.NewValidK8sVersion(version, true)
|
||||
@ -390,7 +369,10 @@ func (u *upgradeApplyCmd) confirmAndUpgradeAttestationConfig(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *upgradeApplyCmd) handleServiceUpgrade(cmd *cobra.Command, conf *config.Config, idFile clusterid.File, tfOutput terraform.ApplyOutput, validK8sVersion versions.ValidK8sVersion, flags upgradeApplyFlags) error {
|
||||
func (u *upgradeApplyCmd) handleServiceUpgrade(
|
||||
cmd *cobra.Command, conf *config.Config, idFile clusterid.File, tfOutput terraform.ApplyOutput,
|
||||
validK8sVersion versions.ValidK8sVersion, upgradeDir string, flags upgradeApplyFlags,
|
||||
) error {
|
||||
var secret uri.MasterSecret
|
||||
if err := u.fileHandler.ReadJSON(constants.MasterSecretFilename, &secret); err != nil {
|
||||
return fmt.Errorf("reading master secret: %w", err)
|
||||
@ -401,7 +383,7 @@ func (u *upgradeApplyCmd) handleServiceUpgrade(cmd *cobra.Command, conf *config.
|
||||
}
|
||||
err = u.helmUpgrader.Upgrade(
|
||||
cmd.Context(), conf, idFile,
|
||||
flags.upgradeTimeout, helm.DenyDestructive, flags.force, u.terraformUpgrader.UpgradeID(),
|
||||
flags.upgradeTimeout, helm.DenyDestructive, flags.force, upgradeDir,
|
||||
flags.conformance, flags.helmWaitMode, secret, serviceAccURI, validK8sVersion, tfOutput,
|
||||
)
|
||||
if errors.Is(err, helm.ErrConfirmationMissing) {
|
||||
@ -418,7 +400,7 @@ func (u *upgradeApplyCmd) handleServiceUpgrade(cmd *cobra.Command, conf *config.
|
||||
}
|
||||
err = u.helmUpgrader.Upgrade(
|
||||
cmd.Context(), conf, idFile,
|
||||
flags.upgradeTimeout, helm.AllowDestructive, flags.force, u.terraformUpgrader.UpgradeID(),
|
||||
flags.upgradeTimeout, helm.AllowDestructive, flags.force, upgradeDir,
|
||||
flags.conformance, flags.helmWaitMode, secret, serviceAccURI, validK8sVersion, tfOutput,
|
||||
)
|
||||
}
|
||||
@ -507,14 +489,24 @@ func parseUpgradeApplyFlags(cmd *cobra.Command) (upgradeApplyFlags, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mergeClusterIDFile(clusterIDPath string, newIDFile clusterid.File, fileHandler file.Handler) error {
|
||||
idFile := &clusterid.File{}
|
||||
if err := fileHandler.ReadJSON(clusterIDPath, idFile); err != nil {
|
||||
return fmt.Errorf("reading %s: %w", clusterIDPath, err)
|
||||
func updateClusterIDFile(tfOutput terraform.ApplyOutput, fileHandler file.Handler) error {
|
||||
newIDFile := clusterid.File{
|
||||
InitSecret: []byte(tfOutput.Secret),
|
||||
IP: tfOutput.IP,
|
||||
APIServerCertSANs: tfOutput.APIServerCertSANs,
|
||||
UID: tfOutput.UID,
|
||||
}
|
||||
if tfOutput.Azure != nil {
|
||||
newIDFile.AttestationURL = tfOutput.Azure.AttestationURL
|
||||
}
|
||||
|
||||
if err := fileHandler.WriteJSON(clusterIDPath, idFile.Merge(newIDFile), file.OptOverwrite); err != nil {
|
||||
return fmt.Errorf("writing %s: %w", clusterIDPath, err)
|
||||
idFile := &clusterid.File{}
|
||||
if err := fileHandler.ReadJSON(constants.ClusterIDsFilename, idFile); err != nil {
|
||||
return fmt.Errorf("reading %s: %w", constants.ClusterIDsFilename, err)
|
||||
}
|
||||
|
||||
if err := fileHandler.WriteJSON(constants.ClusterIDsFilename, idFile.Merge(newIDFile), file.OptOverwrite); err != nil {
|
||||
return fmt.Errorf("writing %s: %w", constants.ClusterIDsFilename, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -544,15 +536,12 @@ type kubernetesUpgrader interface {
|
||||
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,
|
||||
allowDestructive, force bool, upgradeDir 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
|
||||
UpgradeID() string
|
||||
type clusterUpgrader interface {
|
||||
PlanClusterUpgrade(ctx context.Context, outWriter io.Writer, vars terraform.Variables, csp cloudprovider.Provider) (bool, error)
|
||||
ApplyClusterUpgrade(ctx context.Context, csp cloudprovider.Provider) (terraform.ApplyOutput, error)
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -16,7 +17,6 @@ import (
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/helm"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/kubecmd"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/upgrade"
|
||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||
"github.com/edgelesssys/constellation/v2/internal/config"
|
||||
@ -35,15 +35,15 @@ func TestUpgradeApply(t *testing.T) {
|
||||
helmUpgrader *stubHelmUpgrader
|
||||
kubeUpgrader *stubKubernetesUpgrader
|
||||
terraformUpgrader *stubTerraformUpgrader
|
||||
flags upgradeApplyFlags
|
||||
wantErr bool
|
||||
yesFlag bool
|
||||
stdin string
|
||||
}{
|
||||
"success": {
|
||||
kubeUpgrader: &stubKubernetesUpgrader{currentConfig: config.DefaultForAzureSEVSNP()},
|
||||
helmUpgrader: &stubHelmUpgrader{},
|
||||
terraformUpgrader: &stubTerraformUpgrader{},
|
||||
yesFlag: true,
|
||||
flags: upgradeApplyFlags{yes: true},
|
||||
},
|
||||
"nodeVersion some error": {
|
||||
kubeUpgrader: &stubKubernetesUpgrader{
|
||||
@ -53,7 +53,7 @@ func TestUpgradeApply(t *testing.T) {
|
||||
helmUpgrader: &stubHelmUpgrader{},
|
||||
terraformUpgrader: &stubTerraformUpgrader{},
|
||||
wantErr: true,
|
||||
yesFlag: true,
|
||||
flags: upgradeApplyFlags{yes: true},
|
||||
},
|
||||
"nodeVersion in progress error": {
|
||||
kubeUpgrader: &stubKubernetesUpgrader{
|
||||
@ -62,7 +62,7 @@ func TestUpgradeApply(t *testing.T) {
|
||||
},
|
||||
helmUpgrader: &stubHelmUpgrader{},
|
||||
terraformUpgrader: &stubTerraformUpgrader{},
|
||||
yesFlag: true,
|
||||
flags: upgradeApplyFlags{yes: true},
|
||||
},
|
||||
"helm other error": {
|
||||
kubeUpgrader: &stubKubernetesUpgrader{
|
||||
@ -71,16 +71,7 @@ func TestUpgradeApply(t *testing.T) {
|
||||
helmUpgrader: &stubHelmUpgrader{err: assert.AnError},
|
||||
terraformUpgrader: &stubTerraformUpgrader{},
|
||||
wantErr: true,
|
||||
yesFlag: true,
|
||||
},
|
||||
"check terraform error": {
|
||||
kubeUpgrader: &stubKubernetesUpgrader{
|
||||
currentConfig: config.DefaultForAzureSEVSNP(),
|
||||
},
|
||||
helmUpgrader: &stubHelmUpgrader{},
|
||||
terraformUpgrader: &stubTerraformUpgrader{checkTerraformErr: assert.AnError},
|
||||
wantErr: true,
|
||||
yesFlag: true,
|
||||
flags: upgradeApplyFlags{yes: true},
|
||||
},
|
||||
"abort": {
|
||||
kubeUpgrader: &stubKubernetesUpgrader{
|
||||
@ -91,18 +82,6 @@ func TestUpgradeApply(t *testing.T) {
|
||||
wantErr: true,
|
||||
stdin: "no\n",
|
||||
},
|
||||
"clean terraform error": {
|
||||
kubeUpgrader: &stubKubernetesUpgrader{
|
||||
currentConfig: config.DefaultForAzureSEVSNP(),
|
||||
},
|
||||
helmUpgrader: &stubHelmUpgrader{},
|
||||
terraformUpgrader: &stubTerraformUpgrader{
|
||||
cleanTerraformErr: assert.AnError,
|
||||
terraformDiff: true,
|
||||
},
|
||||
wantErr: true,
|
||||
stdin: "no\n",
|
||||
},
|
||||
"plan terraform error": {
|
||||
kubeUpgrader: &stubKubernetesUpgrader{
|
||||
currentConfig: config.DefaultForAzureSEVSNP(),
|
||||
@ -110,7 +89,7 @@ func TestUpgradeApply(t *testing.T) {
|
||||
helmUpgrader: &stubHelmUpgrader{},
|
||||
terraformUpgrader: &stubTerraformUpgrader{planTerraformErr: assert.AnError},
|
||||
wantErr: true,
|
||||
yesFlag: true,
|
||||
flags: upgradeApplyFlags{yes: true},
|
||||
},
|
||||
"apply terraform error": {
|
||||
kubeUpgrader: &stubKubernetesUpgrader{
|
||||
@ -122,15 +101,7 @@ func TestUpgradeApply(t *testing.T) {
|
||||
terraformDiff: true,
|
||||
},
|
||||
wantErr: true,
|
||||
yesFlag: true,
|
||||
},
|
||||
"do no backup join-config when remote attestation config is the same": {
|
||||
kubeUpgrader: &stubKubernetesUpgrader{
|
||||
currentConfig: fakeAzureAttestationConfigFromCluster(context.Background(), t, cloudprovider.Azure),
|
||||
},
|
||||
helmUpgrader: &stubHelmUpgrader{},
|
||||
terraformUpgrader: &stubTerraformUpgrader{},
|
||||
yesFlag: true,
|
||||
flags: upgradeApplyFlags{yes: true},
|
||||
},
|
||||
}
|
||||
|
||||
@ -140,14 +111,6 @@ func TestUpgradeApply(t *testing.T) {
|
||||
require := require.New(t)
|
||||
cmd := newUpgradeApplyCmd()
|
||||
cmd.SetIn(bytes.NewBufferString(tc.stdin))
|
||||
cmd.Flags().String("workspace", "", "") // register persistent flag manually
|
||||
cmd.Flags().Bool("force", true, "") // register persistent flag manually
|
||||
cmd.Flags().String("tf-log", "DEBUG", "") // register persistent flag manually
|
||||
|
||||
if tc.yesFlag {
|
||||
err := cmd.Flags().Set("yes", "true")
|
||||
require.NoError(err)
|
||||
}
|
||||
|
||||
handler := file.NewHandler(afero.NewMemMapFs())
|
||||
|
||||
@ -158,16 +121,16 @@ func TestUpgradeApply(t *testing.T) {
|
||||
require.NoError(handler.WriteJSON(constants.MasterSecretFilename, uri.MasterSecret{}))
|
||||
|
||||
upgrader := upgradeApplyCmd{
|
||||
kubeUpgrader: tc.kubeUpgrader,
|
||||
helmUpgrader: tc.helmUpgrader,
|
||||
terraformUpgrader: tc.terraformUpgrader,
|
||||
log: logger.NewTest(t),
|
||||
configFetcher: stubAttestationFetcher{},
|
||||
clusterShower: &stubShowCluster{},
|
||||
fileHandler: handler,
|
||||
kubeUpgrader: tc.kubeUpgrader,
|
||||
helmUpgrader: tc.helmUpgrader,
|
||||
clusterUpgrader: tc.terraformUpgrader,
|
||||
log: logger.NewTest(t),
|
||||
configFetcher: stubAttestationFetcher{},
|
||||
clusterShower: &stubShowCluster{},
|
||||
fileHandler: handler,
|
||||
}
|
||||
|
||||
err := upgrader.upgradeApply(cmd)
|
||||
err := upgrader.upgradeApply(cmd, "test", tc.flags)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
@ -222,35 +185,13 @@ func (u stubKubernetesUpgrader) RemoveHelmKeepAnnotation(_ context.Context) erro
|
||||
type stubTerraformUpgrader struct {
|
||||
terraformDiff bool
|
||||
planTerraformErr error
|
||||
checkTerraformErr error
|
||||
applyTerraformErr error
|
||||
cleanTerraformErr error
|
||||
}
|
||||
|
||||
func (u stubTerraformUpgrader) CheckTerraformMigrations(_ string) error {
|
||||
return u.checkTerraformErr
|
||||
}
|
||||
|
||||
func (u stubTerraformUpgrader) CleanUpTerraformMigrations(_ string) error {
|
||||
return u.cleanTerraformErr
|
||||
}
|
||||
|
||||
func (u stubTerraformUpgrader) PlanTerraformMigrations(context.Context, upgrade.TerraformUpgradeOptions) (bool, error) {
|
||||
func (u stubTerraformUpgrader) PlanClusterUpgrade(_ context.Context, _ io.Writer, _ terraform.Variables, _ cloudprovider.Provider) (bool, error) {
|
||||
return u.terraformDiff, u.planTerraformErr
|
||||
}
|
||||
|
||||
func (u stubTerraformUpgrader) ApplyTerraformMigrations(context.Context, upgrade.TerraformUpgradeOptions) (terraform.ApplyOutput, error) {
|
||||
func (u stubTerraformUpgrader) ApplyClusterUpgrade(_ context.Context, _ cloudprovider.Provider) (terraform.ApplyOutput, error) {
|
||||
return terraform.ApplyOutput{}, u.applyTerraformErr
|
||||
}
|
||||
|
||||
func (u stubTerraformUpgrader) UpgradeID() string {
|
||||
return "test-upgrade"
|
||||
}
|
||||
|
||||
func fakeAzureAttestationConfigFromCluster(ctx context.Context, t *testing.T, provider cloudprovider.Provider) config.AttestationCfg {
|
||||
cpCfg := defaultConfigWithExpectedMeasurements(t, config.Default(), provider)
|
||||
// the cluster attestation config needs to have real version numbers that are translated from "latest" as defined in config.Default()
|
||||
err := cpCfg.Attestation.AzureSEVSNP.FetchAndSetLatestVersionNumbers(ctx, stubAttestationFetcher{}, time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC))
|
||||
require.NoError(t, err)
|
||||
return cpCfg.GetAttestationConfig()
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import (
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/helm"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/kubecmd"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/upgrade"
|
||||
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
|
||||
"github.com/edgelesssys/constellation/v2/internal/api/fetcher"
|
||||
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
|
||||
@ -74,9 +73,16 @@ func runUpgradeCheck(cmd *cobra.Command, _ []string) error {
|
||||
fileHandler := file.NewHandler(afero.NewOsFs())
|
||||
upgradeID := generateUpgradeID(upgradeCmdKindCheck)
|
||||
|
||||
tfClient, err := terraform.New(cmd.Context(), filepath.Join(constants.UpgradeDir, upgradeID, constants.TerraformUpgradeWorkingDir))
|
||||
upgradeDir := filepath.Join(constants.UpgradeDir, upgradeID)
|
||||
tfClient, err := cloudcmd.NewClusterUpgrader(
|
||||
cmd.Context(),
|
||||
constants.TerraformWorkingDir,
|
||||
upgradeDir,
|
||||
flags.terraformLogLevel,
|
||||
fileHandler,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up terraform client: %w", err)
|
||||
return fmt.Errorf("setting up Terraform upgrader: %w", err)
|
||||
}
|
||||
|
||||
kubeChecker, err := kubecmd.New(cmd.OutOrStdout(), constants.AdminConfFilename, log)
|
||||
@ -103,11 +109,12 @@ func runUpgradeCheck(cmd *cobra.Command, _ []string) error {
|
||||
log: log,
|
||||
versionsapi: versionfetcher,
|
||||
},
|
||||
terraformChecker: upgrade.NewTerraformUpgrader(tfClient, cmd.OutOrStdout(), fileHandler, upgradeID),
|
||||
terraformChecker: tfClient,
|
||||
fileHandler: fileHandler,
|
||||
log: log,
|
||||
}
|
||||
|
||||
return up.upgradeCheck(cmd, fileHandler, attestationconfigapi.NewFetcher(), flags)
|
||||
return up.upgradeCheck(cmd, attestationconfigapi.NewFetcher(), upgradeDir, flags)
|
||||
}
|
||||
|
||||
func parseUpgradeCheckFlags(cmd *cobra.Command) (upgradeCheckFlags, error) {
|
||||
@ -150,12 +157,13 @@ type upgradeCheckCmd struct {
|
||||
canUpgradeCheck bool
|
||||
collect collector
|
||||
terraformChecker terraformChecker
|
||||
fileHandler file.Handler
|
||||
log debugLog
|
||||
}
|
||||
|
||||
// upgradePlan plans an upgrade of a Constellation cluster.
|
||||
func (u *upgradeCheckCmd) upgradeCheck(cmd *cobra.Command, fileHandler file.Handler, fetcher attestationconfigapi.Fetcher, flags upgradeCheckFlags) error {
|
||||
conf, err := config.New(fileHandler, constants.ConfigFilename, fetcher, flags.force)
|
||||
func (u *upgradeCheckCmd) upgradeCheck(cmd *cobra.Command, fetcher attestationconfigapi.Fetcher, upgradeDir string, flags upgradeCheckFlags) error {
|
||||
conf, err := config.New(u.fileHandler, constants.ConfigFilename, fetcher, flags.force)
|
||||
var configValidationErr *config.ValidationError
|
||||
if errors.As(err, &configValidationErr) {
|
||||
cmd.PrintErrln(configValidationErr.LongMessage())
|
||||
@ -216,34 +224,21 @@ func (u *upgradeCheckCmd) upgradeCheck(cmd *cobra.Command, fileHandler file.Hand
|
||||
// u.upgrader.AddManualStateMigration(migration)
|
||||
// }
|
||||
|
||||
if err := u.terraformChecker.CheckTerraformMigrations(constants.UpgradeDir); err != nil {
|
||||
return fmt.Errorf("checking workspace: %w", err)
|
||||
}
|
||||
|
||||
vars, err := cloudcmd.TerraformUpgradeVars(conf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing upgrade variables: %w", err)
|
||||
}
|
||||
u.log.Debugf("Using Terraform variables:\n%v", vars)
|
||||
|
||||
opts := upgrade.TerraformUpgradeOptions{
|
||||
LogLevel: flags.terraformLogLevel,
|
||||
CSP: conf.GetProvider(),
|
||||
Vars: vars,
|
||||
TFWorkspace: constants.TerraformWorkingDir,
|
||||
UpgradeWorkspace: constants.UpgradeDir,
|
||||
}
|
||||
|
||||
cmd.Println("The following Terraform migrations are available with this CLI:")
|
||||
|
||||
// Check if there are any Terraform migrations
|
||||
hasDiff, err := u.terraformChecker.PlanTerraformMigrations(cmd.Context(), opts)
|
||||
hasDiff, err := u.terraformChecker.PlanClusterUpgrade(cmd.Context(), cmd.OutOrStdout(), vars, conf.GetProvider())
|
||||
if err != nil {
|
||||
return fmt.Errorf("planning terraform migrations: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := u.terraformChecker.CleanUpTerraformMigrations(constants.UpgradeDir); err != nil {
|
||||
u.log.Debugf("Failed to clean up Terraform migrations: %v", err)
|
||||
// Remove the upgrade directory
|
||||
if err := u.fileHandler.RemoveAll(upgradeDir); err != nil {
|
||||
u.log.Debugf("Failed to clean up Terraform migrations: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
@ -271,7 +266,7 @@ func (u *upgradeCheckCmd) upgradeCheck(cmd *cobra.Command, fileHandler file.Hand
|
||||
cmd.Print(updateMsg)
|
||||
|
||||
if flags.updateConfig {
|
||||
if err := upgrade.writeConfig(conf, fileHandler, constants.ConfigFilename); err != nil {
|
||||
if err := upgrade.writeConfig(conf, u.fileHandler, constants.ConfigFilename); err != nil {
|
||||
return fmt.Errorf("writing config: %w", err)
|
||||
}
|
||||
cmd.Println("Config updated successfully.")
|
||||
@ -376,7 +371,7 @@ type currentVersionInfo struct {
|
||||
}
|
||||
|
||||
func (v *versionCollector) currentVersions(ctx context.Context) (currentVersionInfo, error) {
|
||||
helmClient, err := helm.NewUpgradeClient(kubectl.NewUninitialized(), constants.UpgradeDir, constants.AdminConfFilename, constants.HelmNamespace, v.log)
|
||||
helmClient, err := helm.NewUpgradeClient(kubectl.NewUninitialized(), constants.AdminConfFilename, constants.HelmNamespace, v.log)
|
||||
if err != nil {
|
||||
return currentVersionInfo{}, fmt.Errorf("setting up helm client: %w", err)
|
||||
}
|
||||
@ -727,9 +722,7 @@ type kubernetesChecker interface {
|
||||
}
|
||||
|
||||
type terraformChecker interface {
|
||||
PlanTerraformMigrations(ctx context.Context, opts upgrade.TerraformUpgradeOptions) (bool, error)
|
||||
CheckTerraformMigrations(upgradeWorkspace string) error
|
||||
CleanUpTerraformMigrations(upgradeWorkspace string) error
|
||||
PlanClusterUpgrade(ctx context.Context, outWriter io.Writer, vars terraform.Variables, csp cloudprovider.Provider) (bool, error)
|
||||
}
|
||||
|
||||
type versionListFetcher interface {
|
||||
|
@ -15,7 +15,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/upgrade"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
||||
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
|
||||
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
|
||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||
@ -207,12 +207,13 @@ func TestUpgradeCheck(t *testing.T) {
|
||||
canUpgradeCheck: true,
|
||||
collect: &tc.collector,
|
||||
terraformChecker: tc.checker,
|
||||
fileHandler: fileHandler,
|
||||
log: logger.NewTest(t),
|
||||
}
|
||||
|
||||
cmd := newUpgradeCheckCmd()
|
||||
|
||||
err := checkCmd.upgradeCheck(cmd, fileHandler, stubAttestationFetcher{}, upgradeCheckFlags{})
|
||||
err := checkCmd.upgradeCheck(cmd, stubAttestationFetcher{}, "test", upgradeCheckFlags{})
|
||||
if tc.wantError {
|
||||
assert.Error(err)
|
||||
return
|
||||
@ -281,18 +282,10 @@ type stubTerraformChecker struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (s stubTerraformChecker) PlanTerraformMigrations(context.Context, upgrade.TerraformUpgradeOptions) (bool, error) {
|
||||
func (s stubTerraformChecker) PlanClusterUpgrade(_ context.Context, _ io.Writer, _ terraform.Variables, _ cloudprovider.Provider) (bool, error) {
|
||||
return s.tfDiff, s.err
|
||||
}
|
||||
|
||||
func (s stubTerraformChecker) CheckTerraformMigrations(_ string) error {
|
||||
return s.err
|
||||
}
|
||||
|
||||
func (s stubTerraformChecker) CleanUpTerraformMigrations(_ string) error {
|
||||
return s.err
|
||||
}
|
||||
|
||||
func TestNewCLIVersions(t *testing.T) {
|
||||
someErr := errors.New("some error")
|
||||
minorList := func() versionsapi.List {
|
||||
|
@ -17,14 +17,14 @@ import (
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
func (c *UpgradeClient) backupCRDs(ctx context.Context, upgradeID string) ([]apiextensionsv1.CustomResourceDefinition, error) {
|
||||
func (c *UpgradeClient) backupCRDs(ctx context.Context, upgradeDir string) ([]apiextensionsv1.CustomResourceDefinition, error) {
|
||||
c.log.Debugf("Starting CRD backup")
|
||||
crds, err := c.kubectl.ListCRDs(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting CRDs: %w", err)
|
||||
}
|
||||
|
||||
crdBackupFolder := c.crdBackupFolder(upgradeID)
|
||||
crdBackupFolder := c.crdBackupFolder(upgradeDir)
|
||||
if err := c.fs.MkdirAll(crdBackupFolder); err != nil {
|
||||
return nil, fmt.Errorf("creating backup dir: %w", err)
|
||||
}
|
||||
@ -98,10 +98,10 @@ func (c *UpgradeClient) backupCRs(ctx context.Context, crds []apiextensionsv1.Cu
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *UpgradeClient) backupFolder(upgradeID string) string {
|
||||
return filepath.Join(c.upgradeWorkspace, upgradeID, "backups") + string(filepath.Separator)
|
||||
func (c *UpgradeClient) backupFolder(upgradeDir string) string {
|
||||
return filepath.Join(upgradeDir, "backups")
|
||||
}
|
||||
|
||||
func (c *UpgradeClient) crdBackupFolder(upgradeID string) string {
|
||||
return filepath.Join(c.backupFolder(upgradeID), "crds") + string(filepath.Separator)
|
||||
func (c *UpgradeClient) crdBackupFolder(upgradeDir string) string {
|
||||
return filepath.Join(c.backupFolder(upgradeDir), "crds")
|
||||
}
|
||||
|
@ -47,16 +47,15 @@ var errReleaseNotFound = errors.New("release not found")
|
||||
|
||||
// UpgradeClient handles interaction with helm and the cluster.
|
||||
type UpgradeClient struct {
|
||||
config *action.Configuration
|
||||
kubectl crdClient
|
||||
fs file.Handler
|
||||
actions actionWrapper
|
||||
upgradeWorkspace string
|
||||
log debugLog
|
||||
config *action.Configuration
|
||||
kubectl crdClient
|
||||
fs file.Handler
|
||||
actions actionWrapper
|
||||
log debugLog
|
||||
}
|
||||
|
||||
// NewUpgradeClient returns a newly initialized UpgradeClient for the given namespace.
|
||||
func NewUpgradeClient(client crdClient, upgradeWorkspace, kubeConfigPath, helmNamespace string, log debugLog) (*UpgradeClient, error) {
|
||||
func NewUpgradeClient(client crdClient, kubeConfigPath, helmNamespace string, log debugLog) (*UpgradeClient, error) {
|
||||
settings := cli.New()
|
||||
settings.KubeConfig = kubeConfigPath
|
||||
|
||||
@ -77,11 +76,10 @@ func NewUpgradeClient(client crdClient, upgradeWorkspace, kubeConfigPath, helmNa
|
||||
}
|
||||
|
||||
return &UpgradeClient{
|
||||
kubectl: client,
|
||||
fs: fileHandler,
|
||||
actions: actions{config: actionConfig},
|
||||
upgradeWorkspace: upgradeWorkspace,
|
||||
log: log,
|
||||
kubectl: client,
|
||||
fs: fileHandler,
|
||||
actions: actions{config: actionConfig},
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -115,7 +113,7 @@ func (c *UpgradeClient) shouldUpgrade(releaseName string, newVersion semver.Semv
|
||||
// If the CLI receives an interrupt signal it will cancel the context.
|
||||
// Canceling the context will prompt helm to abort and roll back the ongoing upgrade.
|
||||
func (c *UpgradeClient) Upgrade(ctx context.Context, config *config.Config, idFile clusterid.File, timeout time.Duration,
|
||||
allowDestructive, force bool, upgradeID string, conformance bool, helmWaitMode WaitMode, masterSecret uri.MasterSecret,
|
||||
allowDestructive, force bool, upgradeDir string, conformance bool, helmWaitMode WaitMode, masterSecret uri.MasterSecret,
|
||||
serviceAccURI string, validK8sVersion versions.ValidK8sVersion, output terraform.ApplyOutput,
|
||||
) error {
|
||||
upgradeErrs := []error{}
|
||||
@ -174,11 +172,11 @@ func (c *UpgradeClient) Upgrade(ctx context.Context, config *config.Config, idFi
|
||||
// Backup CRDs and CRs if we are upgrading anything.
|
||||
if len(upgradeReleases) != 0 {
|
||||
c.log.Debugf("Creating backup of CRDs and CRs")
|
||||
crds, err := c.backupCRDs(ctx, upgradeID)
|
||||
crds, err := c.backupCRDs(ctx, upgradeDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating CRD backup: %w", err)
|
||||
}
|
||||
if err := c.backupCRs(ctx, crds, upgradeID); err != nil {
|
||||
if err := c.backupCRs(ctx, crds, upgradeDir); err != nil {
|
||||
return fmt.Errorf("creating CR backup: %w", err)
|
||||
}
|
||||
}
|
||||
|
@ -83,7 +83,9 @@ go_library(
|
||||
"@com_github_hashicorp_hc_install//product",
|
||||
"@com_github_hashicorp_hc_install//releases",
|
||||
"@com_github_hashicorp_hc_install//src",
|
||||
"@com_github_hashicorp_hcl_v2//:hcl",
|
||||
"@com_github_hashicorp_hcl_v2//gohcl",
|
||||
"@com_github_hashicorp_hcl_v2//hclsyntax",
|
||||
"@com_github_hashicorp_hcl_v2//hclwrite",
|
||||
"@com_github_hashicorp_terraform_exec//tfexec",
|
||||
"@com_github_hashicorp_terraform_json//:terraform-json",
|
||||
|
@ -47,18 +47,6 @@ const (
|
||||
terraformUpgradePlanFile = "plan.zip"
|
||||
)
|
||||
|
||||
// PrepareIAMUpgradeWorkspace prepares a Terraform workspace for a Constellation IAM upgrade.
|
||||
func PrepareIAMUpgradeWorkspace(file file.Handler, path, oldWorkingDir, newWorkingDir, backupDir string) error {
|
||||
if err := prepareUpgradeWorkspace(path, file, oldWorkingDir, newWorkingDir, backupDir); err != nil {
|
||||
return fmt.Errorf("prepare upgrade workspace: %w", err)
|
||||
}
|
||||
// copy the vars file from the old working dir to the new working dir
|
||||
if err := file.CopyFile(filepath.Join(oldWorkingDir, terraformVarsFile), filepath.Join(newWorkingDir, terraformVarsFile)); err != nil {
|
||||
return fmt.Errorf("copying vars file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ErrTerraformWorkspaceExistsWithDifferentVariables is returned when existing Terraform files differ from the version the CLI wants to extract.
|
||||
var ErrTerraformWorkspaceExistsWithDifferentVariables = errors.New("creating cluster: a Terraform workspace already exists with different variables")
|
||||
|
||||
@ -347,10 +335,12 @@ func (c *Client) PrepareWorkspace(path string, vars Variables) error {
|
||||
return c.writeVars(vars)
|
||||
}
|
||||
|
||||
// PrepareUpgradeWorkspace prepares a Terraform workspace for a Constellation version upgrade.
|
||||
// It copies the Terraform state from the old working dir and the embedded Terraform files into the new working dir.
|
||||
func (c *Client) PrepareUpgradeWorkspace(path, oldWorkingDir, newWorkingDir, backupDir string, vars Variables) error {
|
||||
if err := prepareUpgradeWorkspace(path, c.file, oldWorkingDir, newWorkingDir, backupDir); err != nil {
|
||||
// PrepareUpgradeWorkspace prepares a Terraform workspace for an upgrade.
|
||||
// It copies the Terraform state from the old working dir and the embedded Terraform files
|
||||
// into the working dir of the Terraform client.
|
||||
// Additionally, a backup of the old working dir is created in the backup dir.
|
||||
func (c *Client) PrepareUpgradeWorkspace(path, oldWorkingDir, backupDir string, vars Variables) error {
|
||||
if err := prepareUpgradeWorkspace(path, c.file, oldWorkingDir, c.workingDir, backupDir); err != nil {
|
||||
return fmt.Errorf("prepare upgrade workspace: %w", err)
|
||||
}
|
||||
|
||||
|
@ -8,9 +8,10 @@ package terraform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/gohcl"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/hashicorp/hcl/v2/hclwrite"
|
||||
)
|
||||
|
||||
@ -30,6 +31,19 @@ type ClusterVariables interface {
|
||||
GetCreateMAA() bool
|
||||
}
|
||||
|
||||
// VariablesFromBytes parses the given bytes into the given variables struct.
|
||||
func VariablesFromBytes[T any](b []byte, vars *T) error {
|
||||
file, err := hclsyntax.ParseConfig(b, "", hcl.Pos{Line: 1, Column: 1})
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing variables: %w", err)
|
||||
}
|
||||
|
||||
if err := gohcl.DecodeBody(file.Body, nil, vars); err != nil {
|
||||
return fmt.Errorf("decoding variables: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AWSClusterVariables is user configuration for creating a cluster with Terraform on AWS.
|
||||
type AWSClusterVariables struct {
|
||||
// Name of the cluster.
|
||||
@ -87,18 +101,16 @@ type AWSNodeGroup struct {
|
||||
// AWSIAMVariables is user configuration for creating the IAM configuration with Terraform on Microsoft Azure.
|
||||
type AWSIAMVariables struct {
|
||||
// Region is the AWS location to use. (e.g. us-east-2)
|
||||
Region string
|
||||
Region string `hcl:"region" cty:"region"`
|
||||
// Prefix is the name prefix of the resources to use.
|
||||
Prefix string
|
||||
Prefix string `hcl:"name_prefix" cty:"name_prefix"`
|
||||
}
|
||||
|
||||
// String returns a string representation of the IAM-specific variables, formatted as Terraform variables.
|
||||
func (v *AWSIAMVariables) String() string {
|
||||
b := &strings.Builder{}
|
||||
writeLinef(b, "name_prefix = %q", v.Prefix)
|
||||
writeLinef(b, "region = %q", v.Region)
|
||||
|
||||
return b.String()
|
||||
f := hclwrite.NewEmptyFile()
|
||||
gohcl.EncodeIntoBody(v, f.Body())
|
||||
return string(f.Bytes())
|
||||
}
|
||||
|
||||
// GCPClusterVariables is user configuration for creating resources with Terraform on GCP.
|
||||
@ -151,24 +163,20 @@ type GCPNodeGroup struct {
|
||||
// GCPIAMVariables is user configuration for creating the IAM confioguration with Terraform on GCP.
|
||||
type GCPIAMVariables struct {
|
||||
// Project is the ID of the GCP project to use.
|
||||
Project string
|
||||
Project string `hcl:"project_id" cty:"project_id"`
|
||||
// Region is the GCP region to use.
|
||||
Region string
|
||||
Region string `hcl:"region" cty:"region"`
|
||||
// Zone is the GCP zone to use.
|
||||
Zone string
|
||||
Zone string `hcl:"zone" cty:"zone"`
|
||||
// ServiceAccountID is the ID of the service account to use.
|
||||
ServiceAccountID string
|
||||
ServiceAccountID string `hcl:"service_account_id" cty:"service_account_id"`
|
||||
}
|
||||
|
||||
// String returns a string representation of the IAM-specific variables, formatted as Terraform variables.
|
||||
func (v *GCPIAMVariables) String() string {
|
||||
b := &strings.Builder{}
|
||||
writeLinef(b, "project_id = %q", v.Project)
|
||||
writeLinef(b, "region = %q", v.Region)
|
||||
writeLinef(b, "zone = %q", v.Zone)
|
||||
writeLinef(b, "service_account_id = %q", v.ServiceAccountID)
|
||||
|
||||
return b.String()
|
||||
f := hclwrite.NewEmptyFile()
|
||||
gohcl.EncodeIntoBody(v, f.Body())
|
||||
return string(f.Bytes())
|
||||
}
|
||||
|
||||
// AzureClusterVariables is user configuration for creating a cluster with Terraform on Azure.
|
||||
@ -229,21 +237,18 @@ type AzureNodeGroup struct {
|
||||
// AzureIAMVariables is user configuration for creating the IAM configuration with Terraform on Microsoft Azure.
|
||||
type AzureIAMVariables struct {
|
||||
// Region is the Azure region to use. (e.g. westus)
|
||||
Region string
|
||||
Region string `hcl:"region" cty:"region"`
|
||||
// ServicePrincipal is the name of the service principal to use.
|
||||
ServicePrincipal string
|
||||
ServicePrincipal string `hcl:"service_principal_name" cty:"service_principal_name"`
|
||||
// ResourceGroup is the name of the resource group to use.
|
||||
ResourceGroup string
|
||||
ResourceGroup string `hcl:"resource_group_name" cty:"resource_group_name"`
|
||||
}
|
||||
|
||||
// String returns a string representation of the IAM-specific variables, formatted as Terraform variables.
|
||||
func (v *AzureIAMVariables) String() string {
|
||||
b := &strings.Builder{}
|
||||
writeLinef(b, "service_principal_name = %q", v.ServicePrincipal)
|
||||
writeLinef(b, "region = %q", v.Region)
|
||||
writeLinef(b, "resource_group_name = %q", v.ResourceGroup)
|
||||
|
||||
return b.String()
|
||||
f := hclwrite.NewEmptyFile()
|
||||
gohcl.EncodeIntoBody(v, f.Body())
|
||||
return string(f.Bytes())
|
||||
}
|
||||
|
||||
// OpenStackClusterVariables is user configuration for creating a cluster with Terraform on OpenStack.
|
||||
@ -380,11 +385,6 @@ type QEMUNodeGroup struct {
|
||||
MemorySize int `hcl:"memory" cty:"memory"`
|
||||
}
|
||||
|
||||
func writeLinef(builder *strings.Builder, format string, a ...any) {
|
||||
builder.WriteString(fmt.Sprintf(format, a...))
|
||||
builder.WriteByte('\n')
|
||||
}
|
||||
|
||||
func toPtr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
@ -86,8 +86,8 @@ func TestAWSIAMVariables(t *testing.T) {
|
||||
}
|
||||
|
||||
// test that the variables are correctly rendered
|
||||
want := `name_prefix = "my-prefix"
|
||||
region = "eu-central-1"
|
||||
want := `region = "eu-central-1"
|
||||
name_prefix = "my-prefix"
|
||||
`
|
||||
got := vars.String()
|
||||
assert.Equal(t, want, got)
|
||||
@ -162,9 +162,9 @@ func TestGCPIAMVariables(t *testing.T) {
|
||||
}
|
||||
|
||||
// test that the variables are correctly rendered
|
||||
want := `project_id = "my-project"
|
||||
region = "eu-central-1"
|
||||
zone = "eu-central-1a"
|
||||
want := `project_id = "my-project"
|
||||
region = "eu-central-1"
|
||||
zone = "eu-central-1a"
|
||||
service_account_id = "my-service-account"
|
||||
`
|
||||
got := vars.String()
|
||||
@ -226,9 +226,9 @@ func TestAzureIAMVariables(t *testing.T) {
|
||||
}
|
||||
|
||||
// test that the variables are correctly rendered
|
||||
want := `service_principal_name = "my-service-principal"
|
||||
region = "eu-central-1"
|
||||
resource_group_name = "my-resource-group"
|
||||
want := `region = "eu-central-1"
|
||||
service_principal_name = "my-service-principal"
|
||||
resource_group_name = "my-resource-group"
|
||||
`
|
||||
got := vars.String()
|
||||
assert.Equal(t, want, got)
|
||||
@ -337,3 +337,34 @@ custom_endpoint = "example.com"
|
||||
got := vars.String()
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
func TestVariablesFromBytes(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
awsVars := AWSIAMVariables{
|
||||
Region: "test",
|
||||
}
|
||||
var loadedAWSVars AWSIAMVariables
|
||||
err := VariablesFromBytes([]byte(awsVars.String()), &loadedAWSVars)
|
||||
assert.NoError(err)
|
||||
assert.Equal(awsVars, loadedAWSVars)
|
||||
|
||||
azureVars := AzureIAMVariables{
|
||||
Region: "test",
|
||||
}
|
||||
var loadedAzureVars AzureIAMVariables
|
||||
err = VariablesFromBytes([]byte(azureVars.String()), &loadedAzureVars)
|
||||
assert.NoError(err)
|
||||
assert.Equal(azureVars, loadedAzureVars)
|
||||
|
||||
gcpVars := GCPIAMVariables{
|
||||
Region: "test",
|
||||
}
|
||||
var loadedGCPVars GCPIAMVariables
|
||||
err = VariablesFromBytes([]byte(gcpVars.String()), &loadedGCPVars)
|
||||
assert.NoError(err)
|
||||
assert.Equal(gcpVars, loadedGCPVars)
|
||||
|
||||
err = VariablesFromBytes([]byte("invalid"), &loadedGCPVars)
|
||||
assert.Error(err)
|
||||
}
|
||||
|
@ -1,38 +0,0 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
load("//bazel/go:go_test.bzl", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "upgrade",
|
||||
srcs = [
|
||||
"iammigrate.go",
|
||||
"terraform.go",
|
||||
"upgrade.go",
|
||||
],
|
||||
importpath = "github.com/edgelesssys/constellation/v2/cli/internal/upgrade",
|
||||
visibility = ["//cli:__subpackages__"],
|
||||
deps = [
|
||||
"//cli/internal/cloudcmd",
|
||||
"//cli/internal/terraform",
|
||||
"//internal/cloud/cloudprovider",
|
||||
"//internal/constants",
|
||||
"//internal/file",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "upgrade_test",
|
||||
srcs = [
|
||||
"iammigrate_test.go",
|
||||
"terraform_test.go",
|
||||
],
|
||||
embed = [":upgrade"],
|
||||
deps = [
|
||||
"//cli/internal/terraform",
|
||||
"//internal/cloud/cloudprovider",
|
||||
"//internal/constants",
|
||||
"//internal/file",
|
||||
"@com_github_spf13_afero//:afero",
|
||||
"@com_github_stretchr_testify//assert",
|
||||
"@com_github_stretchr_testify//require",
|
||||
],
|
||||
)
|
@ -1,110 +0,0 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// IAMMigrateCmd is a terraform migration command for IAM. Which is used for the tfMigrationClient.
|
||||
type IAMMigrateCmd struct {
|
||||
tf tfIAMClient
|
||||
upgradeID string
|
||||
iamWorkspace string
|
||||
upgradeWorkspace string
|
||||
csp cloudprovider.Provider
|
||||
logLevel terraform.LogLevel
|
||||
}
|
||||
|
||||
// NewIAMMigrateCmd creates a new IAMMigrateCmd.
|
||||
func NewIAMMigrateCmd(ctx context.Context, iamWorkspace, upgradeWorkspace, upgradeID string, csp cloudprovider.Provider, logLevel terraform.LogLevel) (*IAMMigrateCmd, error) {
|
||||
tfClient, err := terraform.New(ctx, filepath.Join(upgradeWorkspace, upgradeID, constants.TerraformIAMUpgradeWorkingDir))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setting up terraform client: %w", err)
|
||||
}
|
||||
return &IAMMigrateCmd{
|
||||
tf: tfClient,
|
||||
upgradeID: upgradeID,
|
||||
iamWorkspace: iamWorkspace,
|
||||
upgradeWorkspace: upgradeWorkspace,
|
||||
csp: csp,
|
||||
logLevel: logLevel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// String returns the name of the command.
|
||||
func (c *IAMMigrateCmd) String() string {
|
||||
return "iam migration"
|
||||
}
|
||||
|
||||
// UpgradeID returns the upgrade ID.
|
||||
func (c *IAMMigrateCmd) UpgradeID() string {
|
||||
return c.upgradeID
|
||||
}
|
||||
|
||||
// CheckTerraformMigrations checks whether Terraform migrations are possible in the current workspace.
|
||||
func (c *IAMMigrateCmd) CheckTerraformMigrations(file file.Handler) error {
|
||||
return checkTerraformMigrations(file, c.upgradeWorkspace, c.upgradeID, constants.TerraformIAMUpgradeBackupDir)
|
||||
}
|
||||
|
||||
// Plan prepares the upgrade workspace and plans the Terraform migrations for the Constellation upgrade, writing the plan to the outWriter.
|
||||
func (c *IAMMigrateCmd) Plan(ctx context.Context, file file.Handler, outWriter io.Writer) (bool, error) {
|
||||
templateDir := filepath.Join("terraform", "iam", strings.ToLower(c.csp.String()))
|
||||
if err := terraform.PrepareIAMUpgradeWorkspace(file,
|
||||
templateDir,
|
||||
c.iamWorkspace,
|
||||
filepath.Join(c.upgradeWorkspace, c.upgradeID, constants.TerraformIAMUpgradeWorkingDir),
|
||||
filepath.Join(c.upgradeWorkspace, c.upgradeID, constants.TerraformIAMUpgradeBackupDir),
|
||||
); err != nil {
|
||||
return false, fmt.Errorf("preparing terraform workspace: %w", err)
|
||||
}
|
||||
|
||||
hasDiff, err := c.tf.Plan(ctx, c.logLevel)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("terraform plan: %w", err)
|
||||
}
|
||||
|
||||
if hasDiff {
|
||||
if err := c.tf.ShowPlan(ctx, c.logLevel, outWriter); err != nil {
|
||||
return false, fmt.Errorf("terraform show plan: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return hasDiff, nil
|
||||
}
|
||||
|
||||
// Apply applies the Terraform IAM migrations for the Constellation upgrade.
|
||||
func (c *IAMMigrateCmd) Apply(ctx context.Context, fileHandler file.Handler) error {
|
||||
if _, err := c.tf.ApplyIAM(ctx, c.csp, c.logLevel); err != nil {
|
||||
return fmt.Errorf("terraform apply: %w", err)
|
||||
}
|
||||
|
||||
if err := fileHandler.RemoveAll(c.iamWorkspace); err != nil {
|
||||
return fmt.Errorf("removing old terraform directory: %w", err)
|
||||
}
|
||||
if err := fileHandler.CopyDir(
|
||||
filepath.Join(c.upgradeWorkspace, c.upgradeID, constants.TerraformIAMUpgradeWorkingDir),
|
||||
c.iamWorkspace,
|
||||
); err != nil {
|
||||
return fmt.Errorf("replacing old terraform directory with new one: %w", err)
|
||||
}
|
||||
|
||||
if err := fileHandler.RemoveAll(filepath.Join(c.upgradeWorkspace, c.upgradeID, constants.TerraformIAMUpgradeWorkingDir)); err != nil {
|
||||
return fmt.Errorf("removing terraform upgrade directory: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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) {
|
||||
upgradeID := "test-upgrade"
|
||||
upgradeDir := filepath.Join(constants.UpgradeDir, upgradeID, constants.TerraformIAMUpgradeWorkingDir)
|
||||
fs, file := setupMemFSAndFileHandler(t, []string{"terraform.tfvars", "terraform.tfstate"}, []byte("OLD"))
|
||||
// act
|
||||
fakeTfClient := &tfClientStub{upgradeID, file}
|
||||
sut := &IAMMigrateCmd{
|
||||
tf: fakeTfClient,
|
||||
upgradeID: upgradeID,
|
||||
csp: cloudprovider.AWS,
|
||||
logLevel: terraform.LogLevelDebug,
|
||||
iamWorkspace: constants.TerraformIAMWorkingDir,
|
||||
upgradeWorkspace: constants.UpgradeDir,
|
||||
}
|
||||
hasDiff, err := sut.Plan(context.Background(), file, bytes.NewBuffer(nil))
|
||||
// assert
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, hasDiff)
|
||||
assertFileExists(fs, filepath.Join(upgradeDir, "terraform.tfvars"), t)
|
||||
assertFileExists(fs, filepath.Join(upgradeDir, "terraform.tfstate"), t)
|
||||
|
||||
// act
|
||||
err = sut.Apply(context.Background(), file)
|
||||
assert.NoError(t, err)
|
||||
// assert
|
||||
assertFileReadsContent(file, filepath.Join(constants.TerraformIAMWorkingDir, "terraform.tfvars"), "NEW", t)
|
||||
assertFileReadsContent(file, filepath.Join(constants.TerraformIAMWorkingDir, "terraform.tfstate"), "NEW", t)
|
||||
assertFileDoesntExist(fs, filepath.Join(upgradeDir), t)
|
||||
}
|
||||
|
||||
func assertFileReadsContent(file file.Handler, path string, expectedContent string, t *testing.T) {
|
||||
bt, err := file.Read(path)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedContent, string(bt))
|
||||
}
|
||||
|
||||
func assertFileExists(fs afero.Fs, path string, t *testing.T) {
|
||||
res, err := fs.Stat(path)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, res)
|
||||
}
|
||||
|
||||
func assertFileDoesntExist(fs afero.Fs, path string, t *testing.T) {
|
||||
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 tfClientStub struct {
|
||||
upgradeID string
|
||||
file file.Handler
|
||||
}
|
||||
|
||||
func (t *tfClientStub) Plan(_ context.Context, _ terraform.LogLevel) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (t *tfClientStub) ShowPlan(_ context.Context, _ terraform.LogLevel, _ io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tfClientStub) ApplyIAM(_ context.Context, _ cloudprovider.Provider, _ terraform.LogLevel) (terraform.IAMOutput, error) {
|
||||
upgradeDir := filepath.Join(constants.UpgradeDir, t.upgradeID, constants.TerraformIAMUpgradeWorkingDir)
|
||||
err := t.file.Remove(filepath.Join(upgradeDir, "terraform.tfvars"))
|
||||
if err != nil {
|
||||
return terraform.IAMOutput{}, err
|
||||
}
|
||||
err = t.file.Write(filepath.Join(upgradeDir, "terraform.tfvars"), []byte("NEW"))
|
||||
if err != nil {
|
||||
return terraform.IAMOutput{}, err
|
||||
}
|
||||
err = t.file.Remove(filepath.Join(upgradeDir, "terraform.tfstate"))
|
||||
if err != nil {
|
||||
return terraform.IAMOutput{}, err
|
||||
}
|
||||
err = t.file.Write(filepath.Join(upgradeDir, "terraform.tfstate"), []byte("NEW"))
|
||||
if err != nil {
|
||||
return terraform.IAMOutput{}, err
|
||||
}
|
||||
return terraform.IAMOutput{}, nil
|
||||
}
|
@ -1,197 +0,0 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
|
||||
"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"
|
||||
)
|
||||
|
||||
// TerraformUpgradeOptions are the options used for the Terraform upgrade.
|
||||
type TerraformUpgradeOptions struct {
|
||||
// LogLevel is the log level used for Terraform.
|
||||
LogLevel terraform.LogLevel
|
||||
// CSP is the cloud provider to perform the upgrade on.
|
||||
CSP cloudprovider.Provider
|
||||
// Vars are the Terraform variables used for the upgrade.
|
||||
Vars terraform.Variables
|
||||
TFWorkspace string
|
||||
UpgradeWorkspace string
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 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) (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, u.upgradeID, constants.TerraformUpgradeWorkingDir),
|
||||
filepath.Join(opts.UpgradeWorkspace, u.upgradeID, constants.TerraformUpgradeBackupDir),
|
||||
opts.Vars,
|
||||
)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("preparing terraform workspace: %w", err)
|
||||
}
|
||||
|
||||
hasDiff, err := u.tf.Plan(ctx, opts.LogLevel)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("terraform plan: %w", err)
|
||||
}
|
||||
|
||||
if hasDiff {
|
||||
if err := u.tf.ShowPlan(ctx, opts.LogLevel, u.outWriter); err != nil {
|
||||
return false, fmt.Errorf("terraform show plan: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return hasDiff, nil
|
||||
}
|
||||
|
||||
// CleanUpTerraformMigrations cleans up the Terraform migration workspace, for example when an upgrade is
|
||||
// aborted by the user.
|
||||
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) (terraform.ApplyOutput, error) {
|
||||
tfOutput, err := u.tf.ApplyCluster(ctx, opts.CSP, opts.LogLevel)
|
||||
if err != nil {
|
||||
return tfOutput, fmt.Errorf("terraform apply: %w", err)
|
||||
}
|
||||
if tfOutput.Azure != nil {
|
||||
if err := u.policyPatcher.Patch(ctx, tfOutput.Azure.AttestationURL); err != nil {
|
||||
return tfOutput, fmt.Errorf("patching policies: %w", err)
|
||||
}
|
||||
}
|
||||
if err := u.fileHandler.RemoveAll(opts.TFWorkspace); err != nil {
|
||||
return tfOutput, fmt.Errorf("removing old terraform directory: %w", err)
|
||||
}
|
||||
|
||||
if err := u.fileHandler.CopyDir(
|
||||
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, 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)
|
||||
}
|
||||
|
||||
// tfResourceClient is a Terraform client for managing cluster resources.
|
||||
type tfResourceClient interface {
|
||||
PrepareUpgradeWorkspace(embeddedPath, oldWorkingDir, newWorkingDir, backupDir string, vars terraform.Variables) error
|
||||
ApplyCluster(ctx context.Context, provider cloudprovider.Provider, logLevel terraform.LogLevel) (terraform.ApplyOutput, error)
|
||||
tfClientCommon
|
||||
}
|
||||
|
||||
// tfIAMClient is a Terraform client for managing IAM resources.
|
||||
type tfIAMClient interface {
|
||||
ApplyIAM(ctx context.Context, csp cloudprovider.Provider, logLevel terraform.LogLevel) (terraform.IAMOutput, error)
|
||||
tfClientCommon
|
||||
}
|
||||
|
||||
// policyPatcher interacts with the CSP (currently only applies for Azure) to update the attestation policy.
|
||||
type policyPatcher interface {
|
||||
Patch(ctx context.Context, attestationURL string) error
|
||||
}
|
@ -1,331 +0,0 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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 TestCheckTerraformMigrations(t *testing.T) {
|
||||
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 {
|
||||
upgradeID string
|
||||
workspace file.Handler
|
||||
wantErr bool
|
||||
}{
|
||||
"success": {
|
||||
upgradeID: "1234",
|
||||
workspace: workspace(nil),
|
||||
},
|
||||
"terraform backup dir already exists": {
|
||||
upgradeID: "1234",
|
||||
workspace: workspace([]string{filepath.Join(constants.UpgradeDir, "1234", constants.TerraformUpgradeBackupDir)}),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
u := NewTerraformUpgrader(&stubTerraformClient{}, bytes.NewBuffer(nil), tc.workspace, tc.upgradeID)
|
||||
|
||||
err := u.CheckTerraformMigrations(constants.UpgradeDir)
|
||||
if tc.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanTerraformMigrations(t *testing.T) {
|
||||
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 {
|
||||
upgradeID string
|
||||
tf tfResourceClient
|
||||
workspace file.Handler
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
"success no diff": {
|
||||
upgradeID: "1234",
|
||||
tf: &stubTerraformClient{},
|
||||
workspace: workspace([]string{}),
|
||||
},
|
||||
"success diff": {
|
||||
upgradeID: "1234",
|
||||
tf: &stubTerraformClient{
|
||||
hasDiff: true,
|
||||
},
|
||||
workspace: workspace([]string{}),
|
||||
want: true,
|
||||
},
|
||||
"prepare workspace error": {
|
||||
upgradeID: "1234",
|
||||
tf: &stubTerraformClient{
|
||||
prepareWorkspaceErr: assert.AnError,
|
||||
},
|
||||
workspace: workspace([]string{}),
|
||||
wantErr: true,
|
||||
},
|
||||
"plan error": {
|
||||
tf: &stubTerraformClient{
|
||||
planErr: assert.AnError,
|
||||
},
|
||||
workspace: workspace([]string{}),
|
||||
wantErr: true,
|
||||
},
|
||||
"show plan error no diff": {
|
||||
upgradeID: "1234",
|
||||
tf: &stubTerraformClient{
|
||||
showErr: assert.AnError,
|
||||
},
|
||||
workspace: workspace([]string{}),
|
||||
},
|
||||
"show plan error diff": {
|
||||
upgradeID: "1234",
|
||||
tf: &stubTerraformClient{
|
||||
showErr: assert.AnError,
|
||||
hasDiff: true,
|
||||
},
|
||||
workspace: workspace([]string{}),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
u := NewTerraformUpgrader(tc.tf, bytes.NewBuffer(nil), tc.workspace, tc.upgradeID)
|
||||
|
||||
opts := TerraformUpgradeOptions{
|
||||
LogLevel: terraform.LogLevelDebug,
|
||||
CSP: cloudprovider.Unknown,
|
||||
Vars: &terraform.QEMUVariables{},
|
||||
}
|
||||
|
||||
diff, err := u.PlanTerraformMigrations(context.Background(), opts)
|
||||
if tc.wantErr {
|
||||
require.Error(err)
|
||||
} else {
|
||||
require.NoError(err)
|
||||
require.Equal(tc.want, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyTerraformMigrations(t *testing.T) {
|
||||
fileHandler := func(upgradeID string, existingFiles ...string) file.Handler {
|
||||
fh := file.NewHandler(afero.NewMemMapFs())
|
||||
|
||||
require.NoError(t,
|
||||
fh.Write(
|
||||
filepath.Join(constants.UpgradeDir, upgradeID, constants.TerraformUpgradeWorkingDir, "someFile"),
|
||||
[]byte("some content"),
|
||||
))
|
||||
for _, f := range existingFiles {
|
||||
require.NoError(t, fh.Write(f, []byte("some content")))
|
||||
}
|
||||
return fh
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
upgradeID string
|
||||
tf tfResourceClient
|
||||
policyPatcher stubPolicyPatcher
|
||||
fs file.Handler
|
||||
wantErr bool
|
||||
}{
|
||||
"success": {
|
||||
upgradeID: "1234",
|
||||
tf: &stubTerraformClient{},
|
||||
fs: fileHandler("1234"),
|
||||
policyPatcher: stubPolicyPatcher{},
|
||||
},
|
||||
"create cluster error": {
|
||||
upgradeID: "1234",
|
||||
tf: &stubTerraformClient{
|
||||
CreateClusterErr: assert.AnError,
|
||||
},
|
||||
fs: fileHandler("1234"),
|
||||
policyPatcher: stubPolicyPatcher{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
u := NewTerraformUpgrader(tc.tf, bytes.NewBuffer(nil), tc.fs, tc.upgradeID)
|
||||
|
||||
opts := TerraformUpgradeOptions{
|
||||
LogLevel: terraform.LogLevelDebug,
|
||||
CSP: cloudprovider.Unknown,
|
||||
Vars: &terraform.QEMUVariables{},
|
||||
TFWorkspace: "test",
|
||||
UpgradeWorkspace: constants.UpgradeDir,
|
||||
}
|
||||
|
||||
_, err := u.ApplyTerraformMigrations(context.Background(), opts)
|
||||
if tc.wantErr {
|
||||
require.Error(err)
|
||||
} else {
|
||||
require.NoError(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanUpTerraformMigrations(t *testing.T) {
|
||||
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 {
|
||||
upgradeID string
|
||||
workspaceFiles []string
|
||||
wantFiles []string
|
||||
wantErr bool
|
||||
}{
|
||||
"no files": {
|
||||
upgradeID: "1234",
|
||||
workspaceFiles: nil,
|
||||
wantFiles: []string{},
|
||||
},
|
||||
"clean backup dir": {
|
||||
upgradeID: "1234",
|
||||
workspaceFiles: []string{
|
||||
filepath.Join(constants.UpgradeDir, "1234", constants.TerraformUpgradeBackupDir),
|
||||
},
|
||||
wantFiles: []string{},
|
||||
},
|
||||
"clean working dir": {
|
||||
upgradeID: "1234",
|
||||
workspaceFiles: []string{
|
||||
filepath.Join(constants.UpgradeDir, "1234", constants.TerraformUpgradeWorkingDir),
|
||||
},
|
||||
wantFiles: []string{},
|
||||
},
|
||||
"clean all": {
|
||||
upgradeID: "1234",
|
||||
workspaceFiles: []string{
|
||||
filepath.Join(constants.UpgradeDir, "1234", constants.TerraformUpgradeBackupDir),
|
||||
filepath.Join(constants.UpgradeDir, "1234", constants.TerraformUpgradeWorkingDir),
|
||||
filepath.Join(constants.UpgradeDir, "1234", "abc"),
|
||||
},
|
||||
wantFiles: []string{},
|
||||
},
|
||||
"leave other files": {
|
||||
upgradeID: "1234",
|
||||
workspaceFiles: []string{
|
||||
filepath.Join(constants.UpgradeDir, "1234", constants.TerraformUpgradeBackupDir),
|
||||
filepath.Join(constants.UpgradeDir, "other"),
|
||||
},
|
||||
wantFiles: []string{
|
||||
filepath.Join(constants.UpgradeDir, "other"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
workspace := workspace(tc.workspaceFiles)
|
||||
u := NewTerraformUpgrader(&stubTerraformClient{}, bytes.NewBuffer(nil), workspace, tc.upgradeID)
|
||||
|
||||
err := u.CleanUpTerraformMigrations(constants.UpgradeDir)
|
||||
if tc.wantErr {
|
||||
require.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
|
||||
for _, haveFile := range tc.workspaceFiles {
|
||||
for _, wantFile := range tc.wantFiles {
|
||||
if haveFile == wantFile {
|
||||
_, err := workspace.Stat(wantFile)
|
||||
require.NoError(err, "file %s should exist", wantFile)
|
||||
} else {
|
||||
_, err := workspace.Stat(haveFile)
|
||||
require.Error(err, "file %s should not exist", haveFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type stubTerraformClient struct {
|
||||
hasDiff bool
|
||||
prepareWorkspaceErr error
|
||||
showErr error
|
||||
planErr error
|
||||
CreateClusterErr error
|
||||
}
|
||||
|
||||
func (u *stubTerraformClient) PrepareUpgradeWorkspace(_, _, _, _ string, _ terraform.Variables) error {
|
||||
return u.prepareWorkspaceErr
|
||||
}
|
||||
|
||||
func (u *stubTerraformClient) ShowPlan(_ context.Context, _ terraform.LogLevel, _ io.Writer) error {
|
||||
return u.showErr
|
||||
}
|
||||
|
||||
func (u *stubTerraformClient) Plan(_ context.Context, _ terraform.LogLevel) (bool, error) {
|
||||
return u.hasDiff, u.planErr
|
||||
}
|
||||
|
||||
func (u *stubTerraformClient) ApplyCluster(_ context.Context, _ cloudprovider.Provider, _ terraform.LogLevel) (terraform.ApplyOutput, error) {
|
||||
return terraform.ApplyOutput{}, u.CreateClusterErr
|
||||
}
|
||||
|
||||
type stubPolicyPatcher struct {
|
||||
patchErr error
|
||||
}
|
||||
|
||||
func (p *stubPolicyPatcher) PatchPolicy(_ context.Context, _ string) error {
|
||||
return p.patchErr
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/*
|
||||
Package upgrade provides functionality to upgrade the cluster and it's resources.
|
||||
|
||||
TODO: Remove this package in favour of adding splitting its functionality onto the kubernetes, helm, and terraform packages.
|
||||
There should be no additions to this package at the current time.
|
||||
If you need to make larger changes to existing code, consider refactoring and moving relevant code to the kubernetes, helm, or terraform packages.
|
||||
*/
|
||||
package upgrade
|
Loading…
Reference in New Issue
Block a user