Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli: remove/refactor upgrade package #2266

Merged
merged 8 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion cli/internal/cloudcmd/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ go_library(
srcs = [
"clients.go",
"cloudcmd.go",
"clusterupgrade.go",
"create.go",
"iam.go",
"iamupgrade.go",
"patch.go",
"rollback.go",
"serviceaccount.go",
"terminate.go",
"terraform.go",
"tfupgrade.go",
"tfvars.go",
"validators.go",
],
importpath = "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd",
Expand All @@ -31,6 +34,7 @@ go_library(
"//internal/cloud/gcpshared",
"//internal/cloud/openstack",
"//internal/config",
"//internal/constants",
"//internal/file",
"//internal/imagefetcher",
"//internal/role",
Expand All @@ -45,11 +49,14 @@ go_test(
name = "cloudcmd_test",
srcs = [
"clients_test.go",
"clusterupgrade_test.go",
"create_test.go",
"iam_test.go",
"iamupgrade_test.go",
"patch_test.go",
"rollback_test.go",
"terminate_test.go",
"tfupgrade_test.go",
"validators_test.go",
],
embed = [":cloudcmd"],
Expand All @@ -60,6 +67,9 @@ go_test(
"//internal/cloud/cloudprovider",
"//internal/cloud/gcpshared",
"//internal/config",
"//internal/constants",
"//internal/file",
"@com_github_spf13_afero//:afero",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
"@org_uber_go_goleak//:goleak",
Expand Down
16 changes: 16 additions & 0 deletions cli/internal/cloudcmd/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,22 @@ type tfIAMClient interface {
ShowIAM(ctx context.Context, provider cloudprovider.Provider) (terraform.IAMOutput, error)
}

type tfUpgradePlanner interface {
ShowPlan(ctx context.Context, logLevel terraform.LogLevel, output io.Writer) error
Plan(ctx context.Context, logLevel terraform.LogLevel) (bool, error)
PrepareUpgradeWorkspace(embeddedPath, oldWorkingDir, backupDir string, vars terraform.Variables) error
}

type tfIAMUpgradeClient interface {
tfUpgradePlanner
ApplyIAM(ctx context.Context, csp cloudprovider.Provider, logLevel terraform.LogLevel) (terraform.IAMOutput, error)
}

type tfClusterUpgradeClient interface {
tfUpgradePlanner
ApplyCluster(ctx context.Context, provider cloudprovider.Provider, logLevel terraform.LogLevel) (terraform.ApplyOutput, error)
}

type libvirtRunner interface {
Start(ctx context.Context, containerName, imageName string) error
Stop(ctx context.Context) error
Expand Down
87 changes: 87 additions & 0 deletions cli/internal/cloudcmd/clusterupgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
Copyright (c) Edgeless Systems GmbH

SPDX-License-Identifier: AGPL-3.0-only
*/

package cloudcmd
elchead marked this conversation as resolved.
Show resolved Hide resolved

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 changes: 203 additions & 0 deletions cli/internal/cloudcmd/clusterupgrade_test.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading