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:
Daniel Weiße 2023-08-23 10:35:42 +02:00 committed by GitHub
parent 3d5d291891
commit 0a911806d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1197 additions and 1194 deletions

View File

@ -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",

View File

@ -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

View 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
}

View 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
}

View 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
}

View 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"))
}

View 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)
}

View 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
}

View File

@ -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 {

View File

@ -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",

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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")
}

View File

@ -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)
}
}

View File

@ -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",

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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",
],
)

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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