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