mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-11-12 00:26:35 -05:00
cli: unify cloudcmd create and upgrade code (#2513)
* Unify cloudcmd create and upgrade code * Make libvirt runner code a bit more idempotent --------- Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
parent
e8cf0f59bd
commit
625dc26644
33 changed files with 1041 additions and 1120 deletions
|
|
@ -4,16 +4,15 @@ load("//bazel/go:go_test.bzl", "go_test")
|
|||
go_library(
|
||||
name = "cloudcmd",
|
||||
srcs = [
|
||||
"apply.go",
|
||||
"clients.go",
|
||||
"cloudcmd.go",
|
||||
"clusterupgrade.go",
|
||||
"create.go",
|
||||
"iam.go",
|
||||
"iamupgrade.go",
|
||||
"rollback.go",
|
||||
"serviceaccount.go",
|
||||
"terminate.go",
|
||||
"tfupgrade.go",
|
||||
"tfplan.go",
|
||||
"tfvars.go",
|
||||
"validators.go",
|
||||
],
|
||||
|
|
@ -44,13 +43,13 @@ go_library(
|
|||
go_test(
|
||||
name = "cloudcmd_test",
|
||||
srcs = [
|
||||
"apply_test.go",
|
||||
"clients_test.go",
|
||||
"clusterupgrade_test.go",
|
||||
"create_test.go",
|
||||
"iam_test.go",
|
||||
"rollback_test.go",
|
||||
"terminate_test.go",
|
||||
"tfupgrade_test.go",
|
||||
"tfplan_test.go",
|
||||
"tfvars_test.go",
|
||||
"validators_test.go",
|
||||
],
|
||||
embed = [":cloudcmd"],
|
||||
|
|
|
|||
154
cli/internal/cloudcmd/apply.go
Normal file
154
cli/internal/cloudcmd/apply.go
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
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/libvirt"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/state"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||
"github.com/edgelesssys/constellation/v2/internal/config"
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||
"github.com/edgelesssys/constellation/v2/internal/imagefetcher"
|
||||
"github.com/edgelesssys/constellation/v2/internal/maa"
|
||||
)
|
||||
|
||||
const (
|
||||
// WithRollbackOnError indicates a rollback should be performed on error.
|
||||
WithRollbackOnError RollbackBehavior = true
|
||||
// WithoutRollbackOnError indicates a rollback should not be performed on error.
|
||||
WithoutRollbackOnError RollbackBehavior = false
|
||||
)
|
||||
|
||||
// RollbackBehavior is a boolean flag that indicates whether a rollback should be performed.
|
||||
type RollbackBehavior bool
|
||||
|
||||
// Applier creates or updates cloud resources.
|
||||
type Applier struct {
|
||||
fileHandler file.Handler
|
||||
imageFetcher imageFetcher
|
||||
libvirtRunner libvirtRunner
|
||||
rawDownloader rawDownloader
|
||||
policyPatcher policyPatcher
|
||||
terraformClient tfResourceClient
|
||||
logLevel terraform.LogLevel
|
||||
|
||||
workingDir string
|
||||
backupDir string
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
// NewApplier creates a new Applier.
|
||||
func NewApplier(
|
||||
ctx context.Context, out io.Writer, workingDir, backupDir string,
|
||||
logLevel terraform.LogLevel, fileHandler file.Handler,
|
||||
) (*Applier, func(), error) {
|
||||
tfClient, err := terraform.New(ctx, workingDir)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("setting up terraform client: %w", err)
|
||||
}
|
||||
|
||||
return &Applier{
|
||||
fileHandler: fileHandler,
|
||||
imageFetcher: imagefetcher.New(),
|
||||
libvirtRunner: libvirt.New(),
|
||||
rawDownloader: imagefetcher.NewDownloader(),
|
||||
policyPatcher: maa.NewAzurePolicyPatcher(),
|
||||
terraformClient: tfClient,
|
||||
logLevel: logLevel,
|
||||
workingDir: workingDir,
|
||||
backupDir: backupDir,
|
||||
out: out,
|
||||
}, tfClient.RemoveInstaller, nil
|
||||
}
|
||||
|
||||
// Plan plans the given configuration and prepares the Terraform workspace.
|
||||
func (a *Applier) Plan(ctx context.Context, conf *config.Config) (bool, error) {
|
||||
vars, err := a.terraformApplyVars(ctx, conf)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("creating terraform variables: %w", err)
|
||||
}
|
||||
|
||||
return plan(
|
||||
ctx, a.terraformClient, a.fileHandler, a.out, a.logLevel, vars,
|
||||
filepath.Join(constants.TerraformEmbeddedDir, strings.ToLower(conf.GetProvider().String())),
|
||||
a.workingDir,
|
||||
filepath.Join(a.backupDir, constants.TerraformUpgradeBackupDir),
|
||||
)
|
||||
}
|
||||
|
||||
// Apply applies the prepared configuration by creating or updating cloud resources.
|
||||
func (a *Applier) Apply(ctx context.Context, csp cloudprovider.Provider, withRollback RollbackBehavior) (infra state.Infrastructure, retErr error) {
|
||||
if withRollback {
|
||||
var rollbacker rollbacker
|
||||
switch csp {
|
||||
case cloudprovider.QEMU:
|
||||
rollbacker = &rollbackerQEMU{client: a.terraformClient, libvirt: a.libvirtRunner}
|
||||
default:
|
||||
rollbacker = &rollbackerTerraform{client: a.terraformClient}
|
||||
}
|
||||
defer rollbackOnError(a.out, &retErr, rollbacker, a.logLevel)
|
||||
}
|
||||
|
||||
infraState, err := a.terraformClient.ApplyCluster(ctx, csp, a.logLevel)
|
||||
if err != nil {
|
||||
return infraState, fmt.Errorf("terraform apply: %w", err)
|
||||
}
|
||||
if csp == cloudprovider.Azure && infraState.Azure != nil {
|
||||
if err := a.policyPatcher.Patch(ctx, infraState.Azure.AttestationURL); err != nil {
|
||||
return infraState, fmt.Errorf("patching policies: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return infraState, nil
|
||||
}
|
||||
|
||||
// RestoreWorkspace rolls back the existing workspace to the backup directory created when planning an action,
|
||||
// and the user decides to not apply it.
|
||||
// Note that this will not apply the restored state from the backup.
|
||||
func (a *Applier) RestoreWorkspace() error {
|
||||
return restoreBackup(a.fileHandler, a.workingDir, filepath.Join(a.backupDir, constants.TerraformUpgradeBackupDir))
|
||||
}
|
||||
|
||||
func (a *Applier) terraformApplyVars(ctx context.Context, conf *config.Config) (terraform.Variables, error) {
|
||||
imageRef, err := a.imageFetcher.FetchReference(
|
||||
ctx,
|
||||
conf.GetProvider(),
|
||||
conf.GetAttestationConfig().GetVariant(),
|
||||
conf.Image, conf.GetRegion(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching image reference: %w", err)
|
||||
}
|
||||
|
||||
switch conf.GetProvider() {
|
||||
case cloudprovider.AWS:
|
||||
return awsTerraformVars(conf, imageRef), nil
|
||||
case cloudprovider.Azure:
|
||||
return azureTerraformVars(conf, imageRef), nil
|
||||
case cloudprovider.GCP:
|
||||
return gcpTerraformVars(conf, imageRef), nil
|
||||
case cloudprovider.OpenStack:
|
||||
return openStackTerraformVars(conf, imageRef)
|
||||
case cloudprovider.QEMU:
|
||||
return qemuTerraformVars(ctx, conf, imageRef, a.libvirtRunner, a.rawDownloader)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported provider: %s", conf.GetProvider())
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
371
cli/internal/cloudcmd/apply_test.go
Normal file
371
cli/internal/cloudcmd/apply_test.go
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package cloudcmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||
"github.com/edgelesssys/constellation/v2/internal/config"
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||
)
|
||||
|
||||
func TestApplier(t *testing.T) {
|
||||
t.Setenv("CONSTELLATION_OPENSTACK_DEV", "1")
|
||||
failOnNonAMD64 := (runtime.GOARCH != "amd64") || (runtime.GOOS != "linux")
|
||||
ip := "192.0.2.1"
|
||||
configWithProvider := func(provider cloudprovider.Provider) *config.Config {
|
||||
cfg := config.Default()
|
||||
cfg.RemoveProviderAndAttestationExcept(provider)
|
||||
return cfg
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
tfClient tfResourceClient
|
||||
newTfClientErr error
|
||||
libvirt *stubLibvirtRunner
|
||||
provider cloudprovider.Provider
|
||||
config *config.Config
|
||||
policyPatcher *stubPolicyPatcher
|
||||
wantErr bool
|
||||
wantRollback bool // Use only together with stubClients.
|
||||
wantTerraformRollback bool // When libvirt fails, don't call into Terraform.
|
||||
}{
|
||||
"gcp": {
|
||||
tfClient: &stubTerraformClient{ip: ip},
|
||||
provider: cloudprovider.GCP,
|
||||
config: configWithProvider(cloudprovider.GCP),
|
||||
},
|
||||
"gcp create cluster error": {
|
||||
tfClient: &stubTerraformClient{applyClusterErr: assert.AnError},
|
||||
provider: cloudprovider.GCP,
|
||||
config: configWithProvider(cloudprovider.GCP),
|
||||
wantErr: true,
|
||||
wantRollback: true,
|
||||
wantTerraformRollback: true,
|
||||
},
|
||||
"azure": {
|
||||
tfClient: &stubTerraformClient{ip: ip},
|
||||
provider: cloudprovider.Azure,
|
||||
config: configWithProvider(cloudprovider.Azure),
|
||||
policyPatcher: &stubPolicyPatcher{},
|
||||
},
|
||||
"azure trusted launch": {
|
||||
tfClient: &stubTerraformClient{ip: ip},
|
||||
provider: cloudprovider.Azure,
|
||||
config: func() *config.Config {
|
||||
cfg := config.Default()
|
||||
cfg.RemoveProviderAndAttestationExcept(cloudprovider.Azure)
|
||||
cfg.Attestation = config.AttestationConfig{
|
||||
AzureTrustedLaunch: &config.AzureTrustedLaunch{},
|
||||
}
|
||||
return cfg
|
||||
}(),
|
||||
policyPatcher: &stubPolicyPatcher{},
|
||||
},
|
||||
"azure new policy patch error": {
|
||||
tfClient: &stubTerraformClient{ip: ip},
|
||||
provider: cloudprovider.Azure,
|
||||
config: configWithProvider(cloudprovider.Azure),
|
||||
policyPatcher: &stubPolicyPatcher{assert.AnError},
|
||||
wantErr: true,
|
||||
},
|
||||
"azure create cluster error": {
|
||||
tfClient: &stubTerraformClient{applyClusterErr: assert.AnError},
|
||||
provider: cloudprovider.Azure,
|
||||
config: configWithProvider(cloudprovider.Azure),
|
||||
policyPatcher: &stubPolicyPatcher{},
|
||||
wantErr: true,
|
||||
wantRollback: true,
|
||||
wantTerraformRollback: true,
|
||||
},
|
||||
"openstack": {
|
||||
tfClient: &stubTerraformClient{ip: ip},
|
||||
libvirt: &stubLibvirtRunner{},
|
||||
provider: cloudprovider.OpenStack,
|
||||
config: func() *config.Config {
|
||||
cfg := config.Default()
|
||||
cfg.RemoveProviderAndAttestationExcept(cloudprovider.OpenStack)
|
||||
cfg.Provider.OpenStack.Cloud = "testcloud"
|
||||
return cfg
|
||||
}(),
|
||||
},
|
||||
"openstack without clouds.yaml": {
|
||||
tfClient: &stubTerraformClient{ip: ip},
|
||||
libvirt: &stubLibvirtRunner{},
|
||||
provider: cloudprovider.OpenStack,
|
||||
config: configWithProvider(cloudprovider.OpenStack),
|
||||
wantErr: true,
|
||||
},
|
||||
"openstack create cluster error": {
|
||||
tfClient: &stubTerraformClient{applyClusterErr: assert.AnError},
|
||||
libvirt: &stubLibvirtRunner{},
|
||||
provider: cloudprovider.OpenStack,
|
||||
config: func() *config.Config {
|
||||
cfg := config.Default()
|
||||
cfg.RemoveProviderAndAttestationExcept(cloudprovider.OpenStack)
|
||||
cfg.Provider.OpenStack.Cloud = "testcloud"
|
||||
return cfg
|
||||
}(),
|
||||
wantErr: true,
|
||||
wantRollback: true,
|
||||
wantTerraformRollback: true,
|
||||
},
|
||||
"qemu": {
|
||||
tfClient: &stubTerraformClient{ip: ip},
|
||||
libvirt: &stubLibvirtRunner{},
|
||||
provider: cloudprovider.QEMU,
|
||||
config: configWithProvider(cloudprovider.QEMU),
|
||||
wantErr: failOnNonAMD64,
|
||||
},
|
||||
"qemu create cluster error": {
|
||||
tfClient: &stubTerraformClient{applyClusterErr: assert.AnError},
|
||||
libvirt: &stubLibvirtRunner{},
|
||||
provider: cloudprovider.QEMU,
|
||||
config: configWithProvider(cloudprovider.QEMU),
|
||||
wantErr: true,
|
||||
wantRollback: !failOnNonAMD64, // if we run on non-AMD64/linux, we don't get to a point where rollback is needed
|
||||
wantTerraformRollback: true,
|
||||
},
|
||||
"qemu start libvirt error": {
|
||||
tfClient: &stubTerraformClient{ip: ip},
|
||||
libvirt: &stubLibvirtRunner{startErr: assert.AnError},
|
||||
provider: cloudprovider.QEMU,
|
||||
config: configWithProvider(cloudprovider.QEMU),
|
||||
wantRollback: !failOnNonAMD64,
|
||||
wantTerraformRollback: false,
|
||||
wantErr: true,
|
||||
},
|
||||
"unknown provider": {
|
||||
tfClient: &stubTerraformClient{},
|
||||
provider: cloudprovider.Unknown,
|
||||
config: func() *config.Config {
|
||||
cfg := config.Default()
|
||||
cfg.RemoveProviderAndAttestationExcept(cloudprovider.AWS)
|
||||
cfg.Provider.AWS = nil
|
||||
return cfg
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
applier := &Applier{
|
||||
fileHandler: file.NewHandler(afero.NewMemMapFs()),
|
||||
imageFetcher: &stubImageFetcher{
|
||||
reference: "some-image",
|
||||
},
|
||||
terraformClient: tc.tfClient,
|
||||
libvirtRunner: tc.libvirt,
|
||||
rawDownloader: &stubRawDownloader{
|
||||
destination: "some-destination",
|
||||
},
|
||||
policyPatcher: tc.policyPatcher,
|
||||
logLevel: terraform.LogLevelNone,
|
||||
workingDir: "test",
|
||||
backupDir: "test-backup",
|
||||
out: &bytes.Buffer{},
|
||||
}
|
||||
|
||||
diff, err := applier.Plan(context.Background(), tc.config)
|
||||
if err != nil {
|
||||
assert.True(tc.wantErr, "unexpected error: %s", err)
|
||||
return
|
||||
}
|
||||
assert.False(diff)
|
||||
|
||||
idFile, err := applier.Apply(context.Background(), tc.provider, true)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
if tc.wantRollback {
|
||||
cl := tc.tfClient.(*stubTerraformClient)
|
||||
if tc.wantTerraformRollback {
|
||||
assert.True(cl.destroyCalled)
|
||||
}
|
||||
assert.True(cl.cleanUpWorkspaceCalled)
|
||||
if tc.provider == cloudprovider.QEMU {
|
||||
assert.True(tc.libvirt.stopCalled)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
assert.Equal(ip, idFile.ClusterEndpoint)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlan(t *testing.T) {
|
||||
setUpFilesystem := func(existingFiles []string) file.Handler {
|
||||
fs := file.NewHandler(afero.NewMemMapFs())
|
||||
require.NoError(t, fs.Write("test/terraform.tfstate", []byte{}, file.OptMkdirAll))
|
||||
for _, f := range existingFiles {
|
||||
require.NoError(t, fs.Write(f, []byte{}))
|
||||
}
|
||||
return fs
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
upgradeID string
|
||||
tf *stubTerraformClient
|
||||
fs file.Handler
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
"success no diff": {
|
||||
upgradeID: "1234",
|
||||
tf: &stubTerraformClient{},
|
||||
fs: setUpFilesystem([]string{}),
|
||||
},
|
||||
"success diff": {
|
||||
upgradeID: "1234",
|
||||
tf: &stubTerraformClient{
|
||||
planDiff: true,
|
||||
},
|
||||
fs: setUpFilesystem([]string{}),
|
||||
want: true,
|
||||
},
|
||||
"prepare workspace error": {
|
||||
upgradeID: "1234",
|
||||
tf: &stubTerraformClient{
|
||||
prepareWorkspaceErr: assert.AnError,
|
||||
},
|
||||
fs: setUpFilesystem([]string{}),
|
||||
wantErr: true,
|
||||
},
|
||||
"plan error": {
|
||||
tf: &stubTerraformClient{
|
||||
planErr: assert.AnError,
|
||||
},
|
||||
fs: setUpFilesystem([]string{}),
|
||||
wantErr: true,
|
||||
},
|
||||
"show plan error no diff": {
|
||||
upgradeID: "1234",
|
||||
tf: &stubTerraformClient{
|
||||
showPlanErr: assert.AnError,
|
||||
},
|
||||
fs: setUpFilesystem([]string{}),
|
||||
},
|
||||
"show plan error diff": {
|
||||
upgradeID: "1234",
|
||||
tf: &stubTerraformClient{
|
||||
showPlanErr: assert.AnError,
|
||||
planDiff: true,
|
||||
},
|
||||
fs: setUpFilesystem([]string{}),
|
||||
wantErr: true,
|
||||
},
|
||||
"workspace not clean": {
|
||||
upgradeID: "1234",
|
||||
tf: &stubTerraformClient{},
|
||||
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 := &Applier{
|
||||
terraformClient: tc.tf,
|
||||
policyPatcher: stubPolicyPatcher{},
|
||||
fileHandler: tc.fs,
|
||||
imageFetcher: &stubImageFetcher{reference: "some-image"},
|
||||
rawDownloader: &stubRawDownloader{destination: "some-destination"},
|
||||
libvirtRunner: &stubLibvirtRunner{},
|
||||
logLevel: terraform.LogLevelDebug,
|
||||
backupDir: filepath.Join(constants.UpgradeDir, tc.upgradeID),
|
||||
workingDir: "test",
|
||||
out: io.Discard,
|
||||
}
|
||||
|
||||
cfg := config.Default()
|
||||
cfg.RemoveProviderAndAttestationExcept(cloudprovider.QEMU)
|
||||
|
||||
diff, err := u.Plan(context.Background(), cfg)
|
||||
if tc.wantErr {
|
||||
require.Error(err)
|
||||
} else {
|
||||
require.NoError(err)
|
||||
require.Equal(tc.want, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
upgradeID string
|
||||
tf *stubTerraformClient
|
||||
policyPatcher stubPolicyPatcher
|
||||
fs file.Handler
|
||||
wantErr bool
|
||||
}{
|
||||
"success": {
|
||||
upgradeID: "1234",
|
||||
tf: &stubTerraformClient{},
|
||||
policyPatcher: stubPolicyPatcher{},
|
||||
},
|
||||
"apply error": {
|
||||
upgradeID: "1234",
|
||||
tf: &stubTerraformClient{
|
||||
applyClusterErr: assert.AnError,
|
||||
},
|
||||
policyPatcher: stubPolicyPatcher{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := require.New(t)
|
||||
|
||||
u := &Applier{
|
||||
terraformClient: tc.tf,
|
||||
logLevel: terraform.LogLevelDebug,
|
||||
libvirtRunner: &stubLibvirtRunner{},
|
||||
policyPatcher: stubPolicyPatcher{},
|
||||
fileHandler: tc.fs,
|
||||
backupDir: filepath.Join(constants.UpgradeDir, tc.upgradeID),
|
||||
workingDir: "test",
|
||||
out: io.Discard,
|
||||
}
|
||||
|
||||
_, err := u.Apply(context.Background(), cloudprovider.QEMU, WithoutRollbackOnError)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type stubPolicyPatcher struct {
|
||||
patchErr error
|
||||
}
|
||||
|
||||
func (s stubPolicyPatcher) Patch(_ context.Context, _ string) error {
|
||||
return s.patchErr
|
||||
}
|
||||
|
|
@ -24,39 +24,34 @@ type imageFetcher interface {
|
|||
) (string, error)
|
||||
}
|
||||
|
||||
type tfCommonClient interface {
|
||||
type tfDestroyer interface {
|
||||
CleanUpWorkspace() error
|
||||
Destroy(ctx context.Context, logLevel terraform.LogLevel) error
|
||||
PrepareWorkspace(path string, input terraform.Variables) error
|
||||
RemoveInstaller()
|
||||
}
|
||||
|
||||
type tfResourceClient interface {
|
||||
tfCommonClient
|
||||
ApplyCluster(ctx context.Context, provider cloudprovider.Provider, logLevel terraform.LogLevel) (state.Infrastructure, error)
|
||||
ShowInfrastructure(ctx context.Context, provider cloudprovider.Provider) (state.Infrastructure, error)
|
||||
}
|
||||
|
||||
type tfIAMClient interface {
|
||||
tfCommonClient
|
||||
ApplyIAM(ctx context.Context, provider cloudprovider.Provider, logLevel terraform.LogLevel) (terraform.IAMOutput, error)
|
||||
ShowIAM(ctx context.Context, provider cloudprovider.Provider) (terraform.IAMOutput, error)
|
||||
}
|
||||
|
||||
type tfUpgradePlanner interface {
|
||||
type tfPlanner interface {
|
||||
ShowPlan(ctx context.Context, logLevel terraform.LogLevel, output io.Writer) error
|
||||
Plan(ctx context.Context, logLevel terraform.LogLevel) (bool, error)
|
||||
PrepareWorkspace(path string, vars terraform.Variables) error
|
||||
}
|
||||
|
||||
type tfIAMUpgradeClient interface {
|
||||
tfUpgradePlanner
|
||||
ApplyIAM(ctx context.Context, csp cloudprovider.Provider, logLevel terraform.LogLevel) (terraform.IAMOutput, error)
|
||||
type tfResourceClient interface {
|
||||
tfDestroyer
|
||||
tfPlanner
|
||||
ApplyCluster(ctx context.Context, provider cloudprovider.Provider, logLevel terraform.LogLevel) (state.Infrastructure, error)
|
||||
}
|
||||
|
||||
type tfClusterUpgradeClient interface {
|
||||
tfUpgradePlanner
|
||||
ApplyCluster(ctx context.Context, provider cloudprovider.Provider, logLevel terraform.LogLevel) (state.Infrastructure, error)
|
||||
type tfIAMClient interface {
|
||||
tfDestroyer
|
||||
PrepareWorkspace(path string, vars terraform.Variables) error
|
||||
ApplyIAM(ctx context.Context, provider cloudprovider.Provider, logLevel terraform.LogLevel) (terraform.IAMOutput, error)
|
||||
ShowIAM(ctx context.Context, provider cloudprovider.Provider) (terraform.IAMOutput, error)
|
||||
}
|
||||
|
||||
type tfIAMUpgradeClient interface {
|
||||
tfPlanner
|
||||
ApplyIAM(ctx context.Context, csp cloudprovider.Provider, logLevel terraform.LogLevel) (terraform.IAMOutput, error)
|
||||
}
|
||||
|
||||
type libvirtRunner interface {
|
||||
|
|
|
|||
|
|
@ -37,12 +37,16 @@ type stubTerraformClient struct {
|
|||
removeInstallerCalled bool
|
||||
destroyCalled bool
|
||||
showCalled bool
|
||||
createClusterErr error
|
||||
applyClusterErr error
|
||||
destroyErr error
|
||||
prepareWorkspaceErr error
|
||||
cleanUpWorkspaceErr error
|
||||
iamOutputErr error
|
||||
showErr error
|
||||
showInfrastructureErr error
|
||||
showIAMErr error
|
||||
planDiff bool
|
||||
planErr error
|
||||
showPlanErr error
|
||||
}
|
||||
|
||||
func (c *stubTerraformClient) ApplyCluster(_ context.Context, _ cloudprovider.Provider, _ terraform.LogLevel) (state.Infrastructure, error) {
|
||||
|
|
@ -53,7 +57,7 @@ func (c *stubTerraformClient) ApplyCluster(_ context.Context, _ cloudprovider.Pr
|
|||
Azure: &state.Azure{
|
||||
AttestationURL: c.attestationURL,
|
||||
},
|
||||
}, c.createClusterErr
|
||||
}, c.applyClusterErr
|
||||
}
|
||||
|
||||
func (c *stubTerraformClient) ApplyIAM(_ context.Context, _ cloudprovider.Provider, _ terraform.LogLevel) (terraform.IAMOutput, error) {
|
||||
|
|
@ -80,12 +84,20 @@ func (c *stubTerraformClient) RemoveInstaller() {
|
|||
|
||||
func (c *stubTerraformClient) ShowInfrastructure(_ context.Context, _ cloudprovider.Provider) (state.Infrastructure, error) {
|
||||
c.showCalled = true
|
||||
return c.infraState, c.showErr
|
||||
return c.infraState, c.showInfrastructureErr
|
||||
}
|
||||
|
||||
func (c *stubTerraformClient) ShowIAM(_ context.Context, _ cloudprovider.Provider) (terraform.IAMOutput, error) {
|
||||
c.showCalled = true
|
||||
return c.iamOutput, c.showErr
|
||||
return c.iamOutput, c.showIAMErr
|
||||
}
|
||||
|
||||
func (c *stubTerraformClient) Plan(_ context.Context, _ terraform.LogLevel) (bool, error) {
|
||||
return c.planDiff, c.planErr
|
||||
}
|
||||
|
||||
func (c *stubTerraformClient) ShowPlan(_ context.Context, _ terraform.LogLevel, _ io.Writer) error {
|
||||
return c.showPlanErr
|
||||
}
|
||||
|
||||
type stubLibvirtRunner struct {
|
||||
|
|
|
|||
|
|
@ -1,88 +0,0 @@
|
|||
/*
|
||||
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/state"
|
||||
"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/edgelesssys/constellation/v2/internal/maa"
|
||||
)
|
||||
|
||||
// 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, existingWorkspace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setting up terraform client: %w", err)
|
||||
}
|
||||
|
||||
return &ClusterUpgrader{
|
||||
tf: tfClient,
|
||||
policyPatcher: maa.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(constants.TerraformEmbeddedDir, strings.ToLower(csp.String())),
|
||||
u.existingWorkspace,
|
||||
filepath.Join(u.upgradeWorkspace, constants.TerraformUpgradeBackupDir),
|
||||
)
|
||||
}
|
||||
|
||||
// RestoreClusterWorkspace rolls back the existing workspace to the backup directory created when planning an upgrade,
|
||||
// when the user decides to not apply an upgrade after planning it.
|
||||
// Note that this will not apply the restored state from the backup.
|
||||
func (u *ClusterUpgrader) RestoreClusterWorkspace() error {
|
||||
return restoreBackup(u.fileHandler, u.existingWorkspace, filepath.Join(u.upgradeWorkspace, constants.TerraformUpgradeBackupDir))
|
||||
}
|
||||
|
||||
// ApplyClusterUpgrade applies the Terraform migrations planned by PlanClusterUpgrade.
|
||||
// On success, the workspace of the Upgrader replaces the existing Terraform workspace.
|
||||
func (u *ClusterUpgrader) ApplyClusterUpgrade(ctx context.Context, csp cloudprovider.Provider) (state.Infrastructure, error) {
|
||||
infraState, err := u.tf.ApplyCluster(ctx, csp, u.logLevel)
|
||||
if err != nil {
|
||||
return infraState, fmt.Errorf("terraform apply: %w", err)
|
||||
}
|
||||
if infraState.Azure != nil {
|
||||
if err := u.policyPatcher.Patch(ctx, infraState.Azure.AttestationURL); err != nil {
|
||||
return infraState, fmt.Errorf("patching policies: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return infraState, nil
|
||||
}
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package cloudcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/state"
|
||||
"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 := file.NewHandler(afero.NewMemMapFs())
|
||||
require.NoError(t, fs.MkdirAll("test"))
|
||||
for _, f := range existingFiles {
|
||||
require.NoError(t, fs.Write(f, []byte{}))
|
||||
}
|
||||
return 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) (state.Infrastructure, error) {
|
||||
return state.Infrastructure{}, t.applyErr
|
||||
}
|
||||
|
||||
func (t *tfClusterUpgradeStub) PrepareWorkspace(_ string, _ terraform.Variables) error {
|
||||
return t.prepareWorkspaceErr
|
||||
}
|
||||
|
|
@ -1,303 +0,0 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package cloudcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/libvirt"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/state"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||
"github.com/edgelesssys/constellation/v2/internal/config"
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"github.com/edgelesssys/constellation/v2/internal/imagefetcher"
|
||||
"github.com/edgelesssys/constellation/v2/internal/maa"
|
||||
)
|
||||
|
||||
// Creator creates cloud resources.
|
||||
type Creator struct {
|
||||
out io.Writer
|
||||
image imageFetcher
|
||||
newTerraformClient func(ctx context.Context, workspace string) (tfResourceClient, error)
|
||||
newLibvirtRunner func() libvirtRunner
|
||||
newRawDownloader func() rawDownloader
|
||||
policyPatcher policyPatcher
|
||||
}
|
||||
|
||||
// NewCreator creates a new creator.
|
||||
func NewCreator(out io.Writer) *Creator {
|
||||
return &Creator{
|
||||
out: out,
|
||||
image: imagefetcher.New(),
|
||||
newTerraformClient: func(ctx context.Context, workspace string) (tfResourceClient, error) {
|
||||
return terraform.New(ctx, workspace)
|
||||
},
|
||||
newLibvirtRunner: func() libvirtRunner {
|
||||
return libvirt.New()
|
||||
},
|
||||
newRawDownloader: func() rawDownloader {
|
||||
return imagefetcher.NewDownloader()
|
||||
},
|
||||
policyPatcher: maa.NewAzurePolicyPatcher(),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateOptions are the options for creating a Constellation cluster.
|
||||
type CreateOptions struct {
|
||||
Provider cloudprovider.Provider
|
||||
Config *config.Config
|
||||
TFWorkspace string
|
||||
image string
|
||||
TFLogLevel terraform.LogLevel
|
||||
}
|
||||
|
||||
// Create creates the handed amount of instances and all the needed resources.
|
||||
func (c *Creator) Create(ctx context.Context, opts CreateOptions) (state.Infrastructure, error) {
|
||||
provider := opts.Config.GetProvider()
|
||||
attestationVariant := opts.Config.GetAttestationConfig().GetVariant()
|
||||
region := opts.Config.GetRegion()
|
||||
image, err := c.image.FetchReference(ctx, provider, attestationVariant, opts.Config.Image, region)
|
||||
if err != nil {
|
||||
return state.Infrastructure{}, fmt.Errorf("fetching image reference: %w", err)
|
||||
}
|
||||
opts.image = image
|
||||
|
||||
cl, err := c.newTerraformClient(ctx, opts.TFWorkspace)
|
||||
if err != nil {
|
||||
return state.Infrastructure{}, err
|
||||
}
|
||||
defer cl.RemoveInstaller()
|
||||
|
||||
var infraState state.Infrastructure
|
||||
switch opts.Provider {
|
||||
case cloudprovider.AWS:
|
||||
|
||||
infraState, err = c.createAWS(ctx, cl, opts)
|
||||
case cloudprovider.GCP:
|
||||
|
||||
infraState, err = c.createGCP(ctx, cl, opts)
|
||||
case cloudprovider.Azure:
|
||||
|
||||
infraState, err = c.createAzure(ctx, cl, opts)
|
||||
case cloudprovider.OpenStack:
|
||||
|
||||
infraState, err = c.createOpenStack(ctx, cl, opts)
|
||||
case cloudprovider.QEMU:
|
||||
if runtime.GOARCH != "amd64" || runtime.GOOS != "linux" {
|
||||
return state.Infrastructure{}, fmt.Errorf("creation of a QEMU based Constellation is not supported for %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
lv := c.newLibvirtRunner()
|
||||
qemuOpts := qemuCreateOptions{
|
||||
source: image,
|
||||
CreateOptions: opts,
|
||||
}
|
||||
|
||||
infraState, err = c.createQEMU(ctx, cl, lv, qemuOpts)
|
||||
default:
|
||||
return state.Infrastructure{}, fmt.Errorf("unsupported cloud provider: %s", opts.Provider)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return state.Infrastructure{}, fmt.Errorf("creating cluster: %w", err)
|
||||
}
|
||||
return infraState, nil
|
||||
}
|
||||
|
||||
func (c *Creator) createAWS(ctx context.Context, cl tfResourceClient, opts CreateOptions) (tfOutput state.Infrastructure, retErr error) {
|
||||
vars := awsTerraformVars(opts.Config, opts.image)
|
||||
|
||||
tfOutput, err := runTerraformCreate(ctx, cl, cloudprovider.AWS, vars, c.out, opts.TFLogLevel)
|
||||
if err != nil {
|
||||
return state.Infrastructure{}, err
|
||||
}
|
||||
|
||||
return tfOutput, nil
|
||||
}
|
||||
|
||||
func (c *Creator) createGCP(ctx context.Context, cl tfResourceClient, opts CreateOptions) (tfOutput state.Infrastructure, retErr error) {
|
||||
vars := gcpTerraformVars(opts.Config, opts.image)
|
||||
|
||||
tfOutput, err := runTerraformCreate(ctx, cl, cloudprovider.GCP, vars, c.out, opts.TFLogLevel)
|
||||
if err != nil {
|
||||
return state.Infrastructure{}, err
|
||||
}
|
||||
|
||||
return tfOutput, nil
|
||||
}
|
||||
|
||||
func (c *Creator) createAzure(ctx context.Context, cl tfResourceClient, opts CreateOptions) (tfOutput state.Infrastructure, retErr error) {
|
||||
vars := azureTerraformVars(opts.Config, opts.image)
|
||||
|
||||
tfOutput, err := runTerraformCreate(ctx, cl, cloudprovider.Azure, vars, c.out, opts.TFLogLevel)
|
||||
if err != nil {
|
||||
return state.Infrastructure{}, err
|
||||
}
|
||||
|
||||
if vars.GetCreateMAA() {
|
||||
// Patch the attestation policy to allow the cluster to boot while having secure boot disabled.
|
||||
if tfOutput.Azure == nil {
|
||||
return state.Infrastructure{}, errors.New("no Terraform Azure output found")
|
||||
}
|
||||
if err := c.policyPatcher.Patch(ctx, tfOutput.Azure.AttestationURL); err != nil {
|
||||
return state.Infrastructure{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return tfOutput, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// The azurerm Terraform provider enforces its own convention of case sensitivity for Azure URIs which Azure's API itself does not enforce or, even worse, actually returns.
|
||||
// Let's go loco with case insensitive Regexp here and fix the user input here to be compliant with this arbitrary design decision.
|
||||
var (
|
||||
caseInsensitiveSubscriptionsRegexp = regexp.MustCompile(`(?i)\/subscriptions\/`)
|
||||
caseInsensitiveResourceGroupRegexp = regexp.MustCompile(`(?i)\/resourcegroups\/`)
|
||||
caseInsensitiveProvidersRegexp = regexp.MustCompile(`(?i)\/providers\/`)
|
||||
caseInsensitiveUserAssignedIdentitiesRegexp = regexp.MustCompile(`(?i)\/userassignedidentities\/`)
|
||||
caseInsensitiveMicrosoftManagedIdentity = regexp.MustCompile(`(?i)\/microsoft.managedidentity\/`)
|
||||
caseInsensitiveCommunityGalleriesRegexp = regexp.MustCompile(`(?i)\/communitygalleries\/`)
|
||||
caseInsensitiveImagesRegExp = regexp.MustCompile(`(?i)\/images\/`)
|
||||
caseInsensitiveVersionsRegExp = regexp.MustCompile(`(?i)\/versions\/`)
|
||||
)
|
||||
|
||||
func normalizeAzureURIs(vars *terraform.AzureClusterVariables) *terraform.AzureClusterVariables {
|
||||
vars.UserAssignedIdentity = caseInsensitiveSubscriptionsRegexp.ReplaceAllString(vars.UserAssignedIdentity, "/subscriptions/")
|
||||
vars.UserAssignedIdentity = caseInsensitiveResourceGroupRegexp.ReplaceAllString(vars.UserAssignedIdentity, "/resourceGroups/")
|
||||
vars.UserAssignedIdentity = caseInsensitiveProvidersRegexp.ReplaceAllString(vars.UserAssignedIdentity, "/providers/")
|
||||
vars.UserAssignedIdentity = caseInsensitiveUserAssignedIdentitiesRegexp.ReplaceAllString(vars.UserAssignedIdentity, "/userAssignedIdentities/")
|
||||
vars.UserAssignedIdentity = caseInsensitiveMicrosoftManagedIdentity.ReplaceAllString(vars.UserAssignedIdentity, "/Microsoft.ManagedIdentity/")
|
||||
vars.ImageID = caseInsensitiveCommunityGalleriesRegexp.ReplaceAllString(vars.ImageID, "/communityGalleries/")
|
||||
vars.ImageID = caseInsensitiveImagesRegExp.ReplaceAllString(vars.ImageID, "/images/")
|
||||
vars.ImageID = caseInsensitiveVersionsRegExp.ReplaceAllString(vars.ImageID, "/versions/")
|
||||
|
||||
return vars
|
||||
}
|
||||
|
||||
func (c *Creator) createOpenStack(ctx context.Context, cl tfResourceClient, opts CreateOptions) (infraState state.Infrastructure, retErr error) {
|
||||
if os.Getenv("CONSTELLATION_OPENSTACK_DEV") != "1" {
|
||||
return state.Infrastructure{}, errors.New("Constellation must be fine-tuned to your OpenStack deployment. Please create an issue or contact Edgeless Systems at https://edgeless.systems/contact/")
|
||||
}
|
||||
if _, hasOSAuthURL := os.LookupEnv("OS_AUTH_URL"); !hasOSAuthURL && opts.Config.Provider.OpenStack.Cloud == "" {
|
||||
return state.Infrastructure{}, errors.New(
|
||||
"neither environment variable OS_AUTH_URL nor cloud name for \"clouds.yaml\" is set. OpenStack authentication requires a set of " +
|
||||
"OS_* environment variables that are typically sourced into the current shell with an openrc file " +
|
||||
"or a cloud name for \"clouds.yaml\". " +
|
||||
"See https://docs.openstack.org/openstacksdk/latest/user/config/configuration.html for more information",
|
||||
)
|
||||
}
|
||||
|
||||
vars := openStackTerraformVars(opts.Config, opts.image)
|
||||
|
||||
infraState, err := runTerraformCreate(ctx, cl, cloudprovider.OpenStack, vars, c.out, opts.TFLogLevel)
|
||||
if err != nil {
|
||||
return state.Infrastructure{}, err
|
||||
}
|
||||
|
||||
return infraState, nil
|
||||
}
|
||||
|
||||
func runTerraformCreate(ctx context.Context, cl tfResourceClient, provider cloudprovider.Provider, vars terraform.Variables, outWriter io.Writer, loglevel terraform.LogLevel) (output state.Infrastructure, retErr error) {
|
||||
if err := cl.PrepareWorkspace(path.Join(constants.TerraformEmbeddedDir, strings.ToLower(provider.String())), vars); err != nil {
|
||||
return state.Infrastructure{}, err
|
||||
}
|
||||
|
||||
defer rollbackOnError(outWriter, &retErr, &rollbackerTerraform{client: cl}, loglevel)
|
||||
tfOutput, err := cl.ApplyCluster(ctx, provider, loglevel)
|
||||
if err != nil {
|
||||
return state.Infrastructure{}, err
|
||||
}
|
||||
|
||||
return tfOutput, nil
|
||||
}
|
||||
|
||||
type qemuCreateOptions struct {
|
||||
source string
|
||||
CreateOptions
|
||||
}
|
||||
|
||||
func (c *Creator) createQEMU(ctx context.Context, cl tfResourceClient, lv libvirtRunner, opts qemuCreateOptions) (tfOutput state.Infrastructure, retErr error) {
|
||||
qemuRollbacker := &rollbackerQEMU{client: cl, libvirt: lv}
|
||||
defer rollbackOnError(c.out, &retErr, qemuRollbacker, opts.TFLogLevel)
|
||||
|
||||
// TODO(malt3): render progress bar
|
||||
downloader := c.newRawDownloader()
|
||||
imagePath, err := downloader.Download(ctx, c.out, false, opts.source, opts.Config.Image)
|
||||
if err != nil {
|
||||
return state.Infrastructure{}, fmt.Errorf("download raw image: %w", err)
|
||||
}
|
||||
|
||||
libvirtURI := opts.Config.Provider.QEMU.LibvirtURI
|
||||
libvirtSocketPath := "."
|
||||
|
||||
switch {
|
||||
// if no libvirt URI is specified, start a libvirt container
|
||||
case libvirtURI == "":
|
||||
if err := lv.Start(ctx, opts.Config.Name, opts.Config.Provider.QEMU.LibvirtContainerImage); err != nil {
|
||||
return state.Infrastructure{}, fmt.Errorf("start libvirt container: %w", err)
|
||||
}
|
||||
libvirtURI = libvirt.LibvirtTCPConnectURI
|
||||
|
||||
// socket for system URI should be in /var/run/libvirt/libvirt-sock
|
||||
case libvirtURI == "qemu:///system":
|
||||
libvirtSocketPath = "/var/run/libvirt/libvirt-sock"
|
||||
|
||||
// socket for session URI should be in /run/user/<uid>/libvirt/libvirt-sock
|
||||
case libvirtURI == "qemu:///session":
|
||||
libvirtSocketPath = fmt.Sprintf("/run/user/%d/libvirt/libvirt-sock", os.Getuid())
|
||||
|
||||
// if a unix socket is specified we need to parse the URI to get the socket path
|
||||
case strings.HasPrefix(libvirtURI, "qemu+unix://"):
|
||||
unixURI, err := url.Parse(strings.TrimPrefix(libvirtURI, "qemu+unix://"))
|
||||
if err != nil {
|
||||
return state.Infrastructure{}, err
|
||||
}
|
||||
libvirtSocketPath = unixURI.Query().Get("socket")
|
||||
if libvirtSocketPath == "" {
|
||||
return state.Infrastructure{}, fmt.Errorf("socket path not specified in qemu+unix URI: %s", libvirtURI)
|
||||
}
|
||||
}
|
||||
|
||||
metadataLibvirtURI := libvirtURI
|
||||
if libvirtSocketPath != "." {
|
||||
metadataLibvirtURI = "qemu:///system"
|
||||
}
|
||||
|
||||
vars := qemuTerraformVars(opts.Config, imagePath, libvirtURI, libvirtSocketPath, metadataLibvirtURI)
|
||||
|
||||
if opts.Config.Provider.QEMU.Firmware != "" {
|
||||
vars.Firmware = toPtr(opts.Config.Provider.QEMU.Firmware)
|
||||
}
|
||||
|
||||
if err := cl.PrepareWorkspace(path.Join(constants.TerraformEmbeddedDir, strings.ToLower(cloudprovider.QEMU.String())), vars); err != nil {
|
||||
return state.Infrastructure{}, fmt.Errorf("prepare workspace: %w", err)
|
||||
}
|
||||
|
||||
tfOutput, err = cl.ApplyCluster(ctx, opts.Provider, opts.TFLogLevel)
|
||||
if err != nil {
|
||||
return state.Infrastructure{}, fmt.Errorf("create cluster: %w", err)
|
||||
}
|
||||
|
||||
return tfOutput, nil
|
||||
}
|
||||
|
||||
func toPtr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
|
@ -1,306 +0,0 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package cloudcmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||
"github.com/edgelesssys/constellation/v2/internal/config"
|
||||
)
|
||||
|
||||
func TestCreator(t *testing.T) {
|
||||
t.Setenv("CONSTELLATION_OPENSTACK_DEV", "1")
|
||||
failOnNonAMD64 := (runtime.GOARCH != "amd64") || (runtime.GOOS != "linux")
|
||||
ip := "192.0.2.1"
|
||||
someErr := errors.New("failed")
|
||||
|
||||
testCases := map[string]struct {
|
||||
tfClient tfResourceClient
|
||||
newTfClientErr error
|
||||
libvirt *stubLibvirtRunner
|
||||
provider cloudprovider.Provider
|
||||
config *config.Config
|
||||
policyPatcher *stubPolicyPatcher
|
||||
wantErr bool
|
||||
wantRollback bool // Use only together with stubClients.
|
||||
wantTerraformRollback bool // When libvirt fails, don't call into Terraform.
|
||||
}{
|
||||
"gcp": {
|
||||
tfClient: &stubTerraformClient{ip: ip},
|
||||
provider: cloudprovider.GCP,
|
||||
config: config.Default(),
|
||||
},
|
||||
"gcp newTerraformClient error": {
|
||||
newTfClientErr: someErr,
|
||||
provider: cloudprovider.GCP,
|
||||
config: config.Default(),
|
||||
wantErr: true,
|
||||
},
|
||||
"gcp create cluster error": {
|
||||
tfClient: &stubTerraformClient{createClusterErr: someErr},
|
||||
provider: cloudprovider.GCP,
|
||||
config: config.Default(),
|
||||
wantErr: true,
|
||||
wantRollback: true,
|
||||
wantTerraformRollback: true,
|
||||
},
|
||||
"azure": {
|
||||
tfClient: &stubTerraformClient{ip: ip},
|
||||
provider: cloudprovider.Azure,
|
||||
config: func() *config.Config {
|
||||
cfg := config.Default()
|
||||
cfg.RemoveProviderAndAttestationExcept(cloudprovider.Azure)
|
||||
return cfg
|
||||
}(),
|
||||
policyPatcher: &stubPolicyPatcher{},
|
||||
},
|
||||
"azure trusted launch": {
|
||||
tfClient: &stubTerraformClient{ip: ip},
|
||||
provider: cloudprovider.Azure,
|
||||
config: func() *config.Config {
|
||||
cfg := config.Default()
|
||||
cfg.Attestation = config.AttestationConfig{
|
||||
AzureTrustedLaunch: &config.AzureTrustedLaunch{},
|
||||
}
|
||||
return cfg
|
||||
}(),
|
||||
policyPatcher: &stubPolicyPatcher{},
|
||||
},
|
||||
"azure new policy patch error": {
|
||||
tfClient: &stubTerraformClient{ip: ip},
|
||||
provider: cloudprovider.Azure,
|
||||
config: func() *config.Config {
|
||||
cfg := config.Default()
|
||||
cfg.RemoveProviderAndAttestationExcept(cloudprovider.Azure)
|
||||
return cfg
|
||||
}(),
|
||||
policyPatcher: &stubPolicyPatcher{someErr},
|
||||
wantErr: true,
|
||||
},
|
||||
"azure newTerraformClient error": {
|
||||
newTfClientErr: someErr,
|
||||
provider: cloudprovider.Azure,
|
||||
config: func() *config.Config {
|
||||
cfg := config.Default()
|
||||
cfg.RemoveProviderAndAttestationExcept(cloudprovider.Azure)
|
||||
return cfg
|
||||
}(),
|
||||
policyPatcher: &stubPolicyPatcher{},
|
||||
wantErr: true,
|
||||
},
|
||||
"azure create cluster error": {
|
||||
tfClient: &stubTerraformClient{createClusterErr: someErr},
|
||||
provider: cloudprovider.Azure,
|
||||
config: func() *config.Config {
|
||||
cfg := config.Default()
|
||||
cfg.RemoveProviderAndAttestationExcept(cloudprovider.Azure)
|
||||
return cfg
|
||||
}(),
|
||||
policyPatcher: &stubPolicyPatcher{},
|
||||
wantErr: true,
|
||||
wantRollback: true,
|
||||
wantTerraformRollback: true,
|
||||
},
|
||||
"openstack": {
|
||||
tfClient: &stubTerraformClient{ip: ip},
|
||||
libvirt: &stubLibvirtRunner{},
|
||||
provider: cloudprovider.OpenStack,
|
||||
config: func() *config.Config {
|
||||
cfg := config.Default()
|
||||
cfg.Provider.OpenStack.Cloud = "testcloud"
|
||||
return cfg
|
||||
}(),
|
||||
},
|
||||
"openstack without clouds.yaml": {
|
||||
tfClient: &stubTerraformClient{ip: ip},
|
||||
libvirt: &stubLibvirtRunner{},
|
||||
provider: cloudprovider.OpenStack,
|
||||
config: config.Default(),
|
||||
wantErr: true,
|
||||
},
|
||||
"openstack newTerraformClient error": {
|
||||
newTfClientErr: someErr,
|
||||
libvirt: &stubLibvirtRunner{},
|
||||
provider: cloudprovider.OpenStack,
|
||||
config: func() *config.Config {
|
||||
cfg := config.Default()
|
||||
cfg.Provider.OpenStack.Cloud = "testcloud"
|
||||
return cfg
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
"openstack create cluster error": {
|
||||
tfClient: &stubTerraformClient{createClusterErr: someErr},
|
||||
libvirt: &stubLibvirtRunner{},
|
||||
provider: cloudprovider.OpenStack,
|
||||
config: func() *config.Config {
|
||||
cfg := config.Default()
|
||||
cfg.Provider.OpenStack.Cloud = "testcloud"
|
||||
return cfg
|
||||
}(),
|
||||
wantErr: true,
|
||||
wantRollback: true,
|
||||
wantTerraformRollback: true,
|
||||
},
|
||||
"qemu": {
|
||||
tfClient: &stubTerraformClient{ip: ip},
|
||||
libvirt: &stubLibvirtRunner{},
|
||||
provider: cloudprovider.QEMU,
|
||||
config: config.Default(),
|
||||
wantErr: failOnNonAMD64,
|
||||
},
|
||||
"qemu newTerraformClient error": {
|
||||
newTfClientErr: someErr,
|
||||
libvirt: &stubLibvirtRunner{},
|
||||
provider: cloudprovider.QEMU,
|
||||
config: config.Default(),
|
||||
wantErr: true,
|
||||
},
|
||||
"qemu create cluster error": {
|
||||
tfClient: &stubTerraformClient{createClusterErr: someErr},
|
||||
libvirt: &stubLibvirtRunner{},
|
||||
provider: cloudprovider.QEMU,
|
||||
config: config.Default(),
|
||||
wantErr: true,
|
||||
wantRollback: !failOnNonAMD64, // if we run on non-AMD64/linux, we don't get to a point where rollback is needed
|
||||
wantTerraformRollback: true,
|
||||
},
|
||||
"qemu start libvirt error": {
|
||||
tfClient: &stubTerraformClient{ip: ip},
|
||||
libvirt: &stubLibvirtRunner{startErr: someErr},
|
||||
provider: cloudprovider.QEMU,
|
||||
config: config.Default(),
|
||||
wantRollback: !failOnNonAMD64,
|
||||
wantTerraformRollback: false,
|
||||
wantErr: true,
|
||||
},
|
||||
"unknown provider": {
|
||||
tfClient: &stubTerraformClient{},
|
||||
provider: cloudprovider.Unknown,
|
||||
config: config.Default(),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
creator := &Creator{
|
||||
out: &bytes.Buffer{},
|
||||
image: &stubImageFetcher{
|
||||
reference: "some-image",
|
||||
},
|
||||
newTerraformClient: func(_ context.Context, _ string) (tfResourceClient, error) {
|
||||
return tc.tfClient, tc.newTfClientErr
|
||||
},
|
||||
newLibvirtRunner: func() libvirtRunner {
|
||||
return tc.libvirt
|
||||
},
|
||||
newRawDownloader: func() rawDownloader {
|
||||
return &stubRawDownloader{
|
||||
destination: "some-destination",
|
||||
}
|
||||
},
|
||||
policyPatcher: tc.policyPatcher,
|
||||
}
|
||||
|
||||
opts := CreateOptions{
|
||||
Provider: tc.provider,
|
||||
Config: tc.config,
|
||||
TFLogLevel: terraform.LogLevelNone,
|
||||
}
|
||||
idFile, err := creator.Create(context.Background(), opts)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
if tc.wantRollback {
|
||||
cl := tc.tfClient.(*stubTerraformClient)
|
||||
if tc.wantTerraformRollback {
|
||||
assert.True(cl.destroyCalled)
|
||||
}
|
||||
assert.True(cl.cleanUpWorkspaceCalled)
|
||||
if tc.provider == cloudprovider.QEMU {
|
||||
assert.True(tc.libvirt.stopCalled)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
assert.Equal(ip, idFile.ClusterEndpoint)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type stubPolicyPatcher struct {
|
||||
patchErr error
|
||||
}
|
||||
|
||||
func (s stubPolicyPatcher) Patch(_ context.Context, _ string) error {
|
||||
return s.patchErr
|
||||
}
|
||||
|
||||
func TestNormalizeAzureURIs(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
in *terraform.AzureClusterVariables
|
||||
want *terraform.AzureClusterVariables
|
||||
}{
|
||||
"empty": {
|
||||
in: &terraform.AzureClusterVariables{},
|
||||
want: &terraform.AzureClusterVariables{},
|
||||
},
|
||||
"no change": {
|
||||
in: &terraform.AzureClusterVariables{
|
||||
ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0",
|
||||
},
|
||||
want: &terraform.AzureClusterVariables{
|
||||
ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0",
|
||||
},
|
||||
},
|
||||
"fix image id": {
|
||||
in: &terraform.AzureClusterVariables{
|
||||
ImageID: "/CommunityGalleries/foo/Images/constellation/Versions/2.1.0",
|
||||
},
|
||||
want: &terraform.AzureClusterVariables{
|
||||
ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0",
|
||||
},
|
||||
},
|
||||
"fix resource group": {
|
||||
in: &terraform.AzureClusterVariables{
|
||||
UserAssignedIdentity: "/subscriptions/foo/resourcegroups/test/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uai",
|
||||
},
|
||||
want: &terraform.AzureClusterVariables{
|
||||
UserAssignedIdentity: "/subscriptions/foo/resourceGroups/test/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uai",
|
||||
},
|
||||
},
|
||||
"fix arbitrary casing": {
|
||||
in: &terraform.AzureClusterVariables{
|
||||
ImageID: "/CoMMUnitygaLLeries/foo/iMAges/constellation/vERsions/2.1.0",
|
||||
UserAssignedIdentity: "/subsCRiptions/foo/resoURCegroups/test/proViDers/MICROsoft.mANAgedIdentity/USerASsignediDENtities/uai",
|
||||
},
|
||||
want: &terraform.AzureClusterVariables{
|
||||
ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0",
|
||||
UserAssignedIdentity: "/subscriptions/foo/resourceGroups/test/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uai",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
out := normalizeAzureURIs(tc.in)
|
||||
assert.Equal(tc.want, out)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -234,7 +234,7 @@ func TestGetTfstateServiceAccountKey(t *testing.T) {
|
|||
},
|
||||
"show error": {
|
||||
cl: &stubTerraformClient{
|
||||
showErr: assert.AnError,
|
||||
showIAMErr: assert.AnError,
|
||||
},
|
||||
wantErr: true,
|
||||
wantShowCalled: true,
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ func NewIAMUpgrader(ctx context.Context, existingWorkspace, upgradeWorkspace str
|
|||
// 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(
|
||||
return plan(
|
||||
ctx, u.tf, u.fileHandler, outWriter, u.logLevel, vars,
|
||||
filepath.Join(constants.TerraformEmbeddedDir, "iam", strings.ToLower(csp.String())),
|
||||
u.existingWorkspace,
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ func rollbackOnError(w io.Writer, onErr *error, roll rollbacker, logLevel terraf
|
|||
}
|
||||
|
||||
type rollbackerTerraform struct {
|
||||
client tfCommonClient
|
||||
client tfDestroyer
|
||||
}
|
||||
|
||||
func (r *rollbackerTerraform) rollback(ctx context.Context, w io.Writer, logLevel terraform.LogLevel) error {
|
||||
|
|
@ -50,7 +50,7 @@ func (r *rollbackerTerraform) rollback(ctx context.Context, w io.Writer, logLeve
|
|||
}
|
||||
|
||||
type rollbackerQEMU struct {
|
||||
client tfResourceClient
|
||||
client tfDestroyer
|
||||
libvirt libvirtRunner
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,14 +15,14 @@ import (
|
|||
|
||||
// Terminator deletes cloud provider resources.
|
||||
type Terminator struct {
|
||||
newTerraformClient func(ctx context.Context, tfWorkspace string) (tfResourceClient, error)
|
||||
newTerraformClient func(ctx context.Context, tfWorkspace string) (tfDestroyer, error)
|
||||
newLibvirtRunner func() libvirtRunner
|
||||
}
|
||||
|
||||
// NewTerminator create a new cloud terminator.
|
||||
func NewTerminator() *Terminator {
|
||||
return &Terminator{
|
||||
newTerraformClient: func(ctx context.Context, tfWorkspace string) (tfResourceClient, error) {
|
||||
newTerraformClient: func(ctx context.Context, tfWorkspace string) (tfDestroyer, error) {
|
||||
return terraform.New(ctx, tfWorkspace)
|
||||
},
|
||||
newLibvirtRunner: func() libvirtRunner {
|
||||
|
|
@ -48,7 +48,7 @@ func (t *Terminator) Terminate(ctx context.Context, tfWorkspace string, logLevel
|
|||
return t.terminateTerraform(ctx, cl, logLevel)
|
||||
}
|
||||
|
||||
func (t *Terminator) terminateTerraform(ctx context.Context, cl tfResourceClient, logLevel terraform.LogLevel) error {
|
||||
func (t *Terminator) terminateTerraform(ctx context.Context, cl tfDestroyer, logLevel terraform.LogLevel) error {
|
||||
if err := cl.Destroy(ctx, logLevel); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ func TestTerminator(t *testing.T) {
|
|||
someErr := errors.New("failed")
|
||||
|
||||
testCases := map[string]struct {
|
||||
tfClient tfResourceClient
|
||||
tfClient tfDestroyer
|
||||
newTfClientErr error
|
||||
libvirt *stubLibvirtRunner
|
||||
wantErr bool
|
||||
|
|
@ -55,7 +55,7 @@ func TestTerminator(t *testing.T) {
|
|||
assert := assert.New(t)
|
||||
|
||||
terminator := &Terminator{
|
||||
newTerraformClient: func(_ context.Context, _ string) (tfResourceClient, error) {
|
||||
newTerraformClient: func(_ context.Context, _ string) (tfDestroyer, error) {
|
||||
return tc.tfClient, tc.newTfClientErr
|
||||
},
|
||||
newLibvirtRunner: func() libvirtRunner {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ package cloudcmd
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
|
@ -16,20 +17,30 @@ import (
|
|||
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||
)
|
||||
|
||||
// planUpgrade prepares a workspace and plans the possible Terraform migrations.
|
||||
// plan prepares a workspace and plans the possible Terraform actions.
|
||||
// This will either create a new workspace or update an existing one.
|
||||
// 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,
|
||||
func plan(
|
||||
ctx context.Context, tfClient tfPlanner, 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("backup directory %s already exists: %w", backupDir, err)
|
||||
isNewWorkspace, err := fileHandler.IsEmpty(existingWorkspace)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return false, fmt.Errorf("checking if workspace is empty: %w", err)
|
||||
}
|
||||
isNewWorkspace = true
|
||||
}
|
||||
|
||||
// Backup old workspace
|
||||
if err := fileHandler.CopyDir(existingWorkspace, backupDir); err != nil {
|
||||
return false, fmt.Errorf("backing up old workspace: %w", err)
|
||||
// Backup old workspace if it exists
|
||||
if !isNewWorkspace {
|
||||
if err := ensureFileNotExist(fileHandler, backupDir); err != nil {
|
||||
return false, fmt.Errorf("backup directory %s already exists: %w", backupDir, err)
|
||||
}
|
||||
if err := fileHandler.CopyDir(existingWorkspace, backupDir); err != nil {
|
||||
return false, fmt.Errorf("backing up old workspace: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Move the new embedded Terraform files into the workspace.
|
||||
|
|
@ -42,12 +53,16 @@ func planUpgrade(
|
|||
return false, fmt.Errorf("terraform plan: %w", err)
|
||||
}
|
||||
|
||||
// If we are planning in a new workspace, we don't want to show a diff
|
||||
if isNewWorkspace {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if hasDiff {
|
||||
if err := tfClient.ShowPlan(ctx, logLevel, outWriter); err != nil {
|
||||
return false, fmt.Errorf("terraform show plan: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return hasDiff, nil
|
||||
}
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPlanUpgrade(t *testing.T) {
|
||||
func TestTFPlan(t *testing.T) {
|
||||
const (
|
||||
templateDir = "templateDir"
|
||||
existingWorkspace = "existing"
|
||||
|
|
@ -28,36 +28,37 @@ func TestPlanUpgrade(t *testing.T) {
|
|||
)
|
||||
fsWithWorkspace := func(require *require.Assertions) file.Handler {
|
||||
fs := file.NewHandler(afero.NewMemMapFs())
|
||||
require.NoError(fs.MkdirAll(existingWorkspace))
|
||||
require.NoError(fs.Write(filepath.Join(existingWorkspace, testFile), []byte{}))
|
||||
require.NoError(fs.Write(filepath.Join(existingWorkspace, testFile), []byte{}, file.OptMkdirAll))
|
||||
return fs
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
prepareFs func(require *require.Assertions) file.Handler
|
||||
tf *stubUpgradePlanner
|
||||
wantDiff bool
|
||||
wantErr bool
|
||||
prepareFs func(require *require.Assertions) file.Handler
|
||||
tf *stubUpgradePlanner
|
||||
wantDiff bool
|
||||
wantBackup bool
|
||||
wantErr bool
|
||||
}{
|
||||
"success no diff": {
|
||||
prepareFs: fsWithWorkspace,
|
||||
tf: &stubUpgradePlanner{},
|
||||
prepareFs: fsWithWorkspace,
|
||||
tf: &stubUpgradePlanner{},
|
||||
wantBackup: true,
|
||||
},
|
||||
"success diff": {
|
||||
prepareFs: fsWithWorkspace,
|
||||
tf: &stubUpgradePlanner{
|
||||
planDiff: true,
|
||||
},
|
||||
wantDiff: true,
|
||||
wantDiff: true,
|
||||
wantBackup: true,
|
||||
},
|
||||
"workspace does not exist": {
|
||||
"workspace is empty": {
|
||||
prepareFs: func(require *require.Assertions) file.Handler {
|
||||
return file.NewHandler(afero.NewMemMapFs())
|
||||
},
|
||||
tf: &stubUpgradePlanner{},
|
||||
wantErr: true,
|
||||
tf: &stubUpgradePlanner{},
|
||||
},
|
||||
"workspace not clean": {
|
||||
"backup dir already exists": {
|
||||
prepareFs: func(require *require.Assertions) file.Handler {
|
||||
fs := fsWithWorkspace(require)
|
||||
require.NoError(fs.MkdirAll(backupDir))
|
||||
|
|
@ -71,14 +72,16 @@ func TestPlanUpgrade(t *testing.T) {
|
|||
tf: &stubUpgradePlanner{
|
||||
prepareWorkspaceErr: assert.AnError,
|
||||
},
|
||||
wantErr: true,
|
||||
wantBackup: true,
|
||||
wantErr: true,
|
||||
},
|
||||
"plan error": {
|
||||
prepareFs: fsWithWorkspace,
|
||||
tf: &stubUpgradePlanner{
|
||||
planErr: assert.AnError,
|
||||
},
|
||||
wantErr: true,
|
||||
wantErr: true,
|
||||
wantBackup: true,
|
||||
},
|
||||
"show plan error": {
|
||||
prepareFs: fsWithWorkspace,
|
||||
|
|
@ -86,7 +89,8 @@ func TestPlanUpgrade(t *testing.T) {
|
|||
planDiff: true,
|
||||
showPlanErr: assert.AnError,
|
||||
},
|
||||
wantErr: true,
|
||||
wantErr: true,
|
||||
wantBackup: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -95,19 +99,23 @@ func TestPlanUpgrade(t *testing.T) {
|
|||
assert := assert.New(t)
|
||||
fs := tc.prepareFs(require.New(t))
|
||||
|
||||
hasDiff, err := planUpgrade(
|
||||
hasDiff, planErr := plan(
|
||||
context.Background(), tc.tf, fs, io.Discard, terraform.LogLevelDebug,
|
||||
&terraform.QEMUVariables{},
|
||||
templateDir, existingWorkspace, backupDir,
|
||||
)
|
||||
|
||||
if tc.wantBackup {
|
||||
_, err := fs.Stat(filepath.Join(backupDir, testFile))
|
||||
assert.NoError(err)
|
||||
}
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
assert.Error(planErr)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
assert.NoError(planErr)
|
||||
assert.Equal(tc.wantDiff, hasDiff)
|
||||
_, err = fs.Stat(filepath.Join(backupDir, testFile))
|
||||
assert.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -7,10 +7,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
package cloudcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/libvirt"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||
|
|
@ -20,27 +27,18 @@ import (
|
|||
"github.com/edgelesssys/constellation/v2/internal/role"
|
||||
)
|
||||
|
||||
// TerraformUpgradeVars returns variables required to execute the Terraform scripts.
|
||||
func TerraformUpgradeVars(conf *config.Config) (terraform.Variables, error) {
|
||||
// Note that we don't pass any real image as imageRef, as we ignore changes to the image in the terraform.
|
||||
// The image is updates via our operator.
|
||||
// Still, the terraform variable verification must accept the values.
|
||||
// For AWS, we enforce some basic constraints on the image variable.
|
||||
// For Azure, the provider enforces the format below.
|
||||
// For GCP, any placeholder works.
|
||||
var vars terraform.Variables
|
||||
switch conf.GetProvider() {
|
||||
case cloudprovider.AWS:
|
||||
vars = awsTerraformVars(conf, "ami-placeholder")
|
||||
case cloudprovider.Azure:
|
||||
vars = azureTerraformVars(conf, "/communityGalleries/myGalleryName/images/myImageName/versions/latest")
|
||||
case cloudprovider.GCP:
|
||||
vars = gcpTerraformVars(conf, "placeholder")
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported provider: %s", conf.GetProvider())
|
||||
}
|
||||
return vars, nil
|
||||
}
|
||||
// The azurerm Terraform provider enforces its own convention of case sensitivity for Azure URIs which Azure's API itself does not enforce or, even worse, actually returns.
|
||||
// These regular expression are used to make sure that the URIs we pass to Terraform are in the format that the provider expects.
|
||||
var (
|
||||
caseInsensitiveSubscriptionsRegexp = regexp.MustCompile(`(?i)\/subscriptions\/`)
|
||||
caseInsensitiveResourceGroupRegexp = regexp.MustCompile(`(?i)\/resourcegroups\/`)
|
||||
caseInsensitiveProvidersRegexp = regexp.MustCompile(`(?i)\/providers\/`)
|
||||
caseInsensitiveUserAssignedIdentitiesRegexp = regexp.MustCompile(`(?i)\/userassignedidentities\/`)
|
||||
caseInsensitiveMicrosoftManagedIdentity = regexp.MustCompile(`(?i)\/microsoft.managedidentity\/`)
|
||||
caseInsensitiveCommunityGalleriesRegexp = regexp.MustCompile(`(?i)\/communitygalleries\/`)
|
||||
caseInsensitiveImagesRegExp = regexp.MustCompile(`(?i)\/images\/`)
|
||||
caseInsensitiveVersionsRegExp = regexp.MustCompile(`(?i)\/versions\/`)
|
||||
)
|
||||
|
||||
// TerraformIAMUpgradeVars returns variables required to execute IAM upgrades with Terraform.
|
||||
func TerraformIAMUpgradeVars(conf *config.Config, fileHandler file.Handler) (terraform.Variables, error) {
|
||||
|
|
@ -114,6 +112,19 @@ func awsTerraformIAMVars(conf *config.Config, oldVars terraform.AWSIAMVariables)
|
|||
}
|
||||
}
|
||||
|
||||
func normalizeAzureURIs(vars *terraform.AzureClusterVariables) *terraform.AzureClusterVariables {
|
||||
vars.UserAssignedIdentity = caseInsensitiveSubscriptionsRegexp.ReplaceAllString(vars.UserAssignedIdentity, "/subscriptions/")
|
||||
vars.UserAssignedIdentity = caseInsensitiveResourceGroupRegexp.ReplaceAllString(vars.UserAssignedIdentity, "/resourceGroups/")
|
||||
vars.UserAssignedIdentity = caseInsensitiveProvidersRegexp.ReplaceAllString(vars.UserAssignedIdentity, "/providers/")
|
||||
vars.UserAssignedIdentity = caseInsensitiveUserAssignedIdentitiesRegexp.ReplaceAllString(vars.UserAssignedIdentity, "/userAssignedIdentities/")
|
||||
vars.UserAssignedIdentity = caseInsensitiveMicrosoftManagedIdentity.ReplaceAllString(vars.UserAssignedIdentity, "/Microsoft.ManagedIdentity/")
|
||||
vars.ImageID = caseInsensitiveCommunityGalleriesRegexp.ReplaceAllString(vars.ImageID, "/communityGalleries/")
|
||||
vars.ImageID = caseInsensitiveImagesRegExp.ReplaceAllString(vars.ImageID, "/images/")
|
||||
vars.ImageID = caseInsensitiveVersionsRegExp.ReplaceAllString(vars.ImageID, "/versions/")
|
||||
|
||||
return vars
|
||||
}
|
||||
|
||||
// azureTerraformVars provides variables required to execute the Terraform scripts.
|
||||
// It should be the only place to declare the Azure variables.
|
||||
func azureTerraformVars(conf *config.Config, imageRef string) *terraform.AzureClusterVariables {
|
||||
|
|
@ -197,7 +208,19 @@ func gcpTerraformIAMVars(conf *config.Config, oldVars terraform.GCPIAMVariables)
|
|||
|
||||
// openStackTerraformVars provides variables required to execute the Terraform scripts.
|
||||
// It should be the only place to declare the OpenStack variables.
|
||||
func openStackTerraformVars(conf *config.Config, imageRef string) *terraform.OpenStackClusterVariables {
|
||||
func openStackTerraformVars(conf *config.Config, imageRef string) (*terraform.OpenStackClusterVariables, error) {
|
||||
if os.Getenv("CONSTELLATION_OPENSTACK_DEV") != "1" {
|
||||
return nil, errors.New("Constellation must be fine-tuned to your OpenStack deployment. Please create an issue or contact Edgeless Systems at https://edgeless.systems/contact/")
|
||||
}
|
||||
if _, hasOSAuthURL := os.LookupEnv("OS_AUTH_URL"); !hasOSAuthURL && conf.Provider.OpenStack.Cloud == "" {
|
||||
return nil, errors.New(
|
||||
"neither environment variable OS_AUTH_URL nor cloud name for \"clouds.yaml\" is set. OpenStack authentication requires a set of " +
|
||||
"OS_* environment variables that are typically sourced into the current shell with an openrc file " +
|
||||
"or a cloud name for \"clouds.yaml\". " +
|
||||
"See https://docs.openstack.org/openstacksdk/latest/user/config/configuration.html for more information",
|
||||
)
|
||||
}
|
||||
|
||||
nodeGroups := make(map[string]terraform.OpenStackNodeGroup)
|
||||
for groupName, group := range conf.NodeGroups {
|
||||
nodeGroups[groupName] = terraform.OpenStackNodeGroup{
|
||||
|
|
@ -222,12 +245,65 @@ func openStackTerraformVars(conf *config.Config, imageRef string) *terraform.Ope
|
|||
NodeGroups: nodeGroups,
|
||||
CustomEndpoint: conf.CustomEndpoint,
|
||||
InternalLoadBalancer: conf.InternalLoadBalancer,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// qemuTerraformVars provides variables required to execute the Terraform scripts.
|
||||
// It should be the only place to declare the QEMU variables.
|
||||
func qemuTerraformVars(conf *config.Config, imageRef string, libvirtURI, libvirtSocketPath, metadataLibvirtURI string) *terraform.QEMUVariables {
|
||||
func qemuTerraformVars(
|
||||
ctx context.Context, conf *config.Config, imageRef string,
|
||||
lv libvirtRunner, downloader rawDownloader,
|
||||
) (*terraform.QEMUVariables, error) {
|
||||
if runtime.GOARCH != "amd64" || runtime.GOOS != "linux" {
|
||||
return nil, fmt.Errorf("creation of a QEMU based Constellation is not supported for %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
||||
imagePath, err := downloader.Download(ctx, nil, false, imageRef, conf.Image)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("download raw image: %w", err)
|
||||
}
|
||||
|
||||
libvirtURI := conf.Provider.QEMU.LibvirtURI
|
||||
libvirtSocketPath := "."
|
||||
|
||||
switch {
|
||||
// if no libvirt URI is specified, start a libvirt container
|
||||
case libvirtURI == "":
|
||||
if err := lv.Start(ctx, conf.Name, conf.Provider.QEMU.LibvirtContainerImage); err != nil {
|
||||
return nil, fmt.Errorf("start libvirt container: %w", err)
|
||||
}
|
||||
libvirtURI = libvirt.LibvirtTCPConnectURI
|
||||
|
||||
// socket for system URI should be in /var/run/libvirt/libvirt-sock
|
||||
case libvirtURI == "qemu:///system":
|
||||
libvirtSocketPath = "/var/run/libvirt/libvirt-sock"
|
||||
|
||||
// socket for session URI should be in /run/user/<uid>/libvirt/libvirt-sock
|
||||
case libvirtURI == "qemu:///session":
|
||||
libvirtSocketPath = fmt.Sprintf("/run/user/%d/libvirt/libvirt-sock", os.Getuid())
|
||||
|
||||
// if a unix socket is specified we need to parse the URI to get the socket path
|
||||
case strings.HasPrefix(libvirtURI, "qemu+unix://"):
|
||||
unixURI, err := url.Parse(strings.TrimPrefix(libvirtURI, "qemu+unix://"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
libvirtSocketPath = unixURI.Query().Get("socket")
|
||||
if libvirtSocketPath == "" {
|
||||
return nil, fmt.Errorf("socket path not specified in qemu+unix URI: %s", libvirtURI)
|
||||
}
|
||||
}
|
||||
|
||||
metadataLibvirtURI := libvirtURI
|
||||
if libvirtSocketPath != "." {
|
||||
metadataLibvirtURI = "qemu:///system"
|
||||
}
|
||||
|
||||
var firmware *string
|
||||
if conf.Provider.QEMU.Firmware != "" {
|
||||
firmware = &conf.Provider.QEMU.Firmware
|
||||
}
|
||||
|
||||
nodeGroups := make(map[string]terraform.QEMUNodeGroup)
|
||||
for groupName, group := range conf.NodeGroups {
|
||||
nodeGroups[groupName] = terraform.QEMUNodeGroup{
|
||||
|
|
@ -245,17 +321,22 @@ func qemuTerraformVars(conf *config.Config, imageRef string, libvirtURI, libvirt
|
|||
// TODO(malt3): auto select boot mode based on attestation variant.
|
||||
// requires image info v2.
|
||||
BootMode: "uefi",
|
||||
ImagePath: imageRef,
|
||||
ImagePath: imagePath,
|
||||
ImageFormat: conf.Provider.QEMU.ImageFormat,
|
||||
NodeGroups: nodeGroups,
|
||||
Machine: "q35", // TODO(elchead): make configurable AB#3225
|
||||
MetadataAPIImage: conf.Provider.QEMU.MetadataAPIImage,
|
||||
MetadataLibvirtURI: metadataLibvirtURI,
|
||||
NVRAM: conf.Provider.QEMU.NVRAM,
|
||||
Firmware: firmware,
|
||||
// TODO(malt3) enable once we have a way to auto-select values for these
|
||||
// requires image info v2.
|
||||
// BzImagePath: placeholder,
|
||||
// InitrdPath: placeholder,
|
||||
// KernelCmdline: placeholder,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toPtr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
|
|
|||
68
cli/internal/cloudcmd/tfvars_test.go
Normal file
68
cli/internal/cloudcmd/tfvars_test.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package cloudcmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNormalizeAzureURIs(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
in *terraform.AzureClusterVariables
|
||||
want *terraform.AzureClusterVariables
|
||||
}{
|
||||
"empty": {
|
||||
in: &terraform.AzureClusterVariables{},
|
||||
want: &terraform.AzureClusterVariables{},
|
||||
},
|
||||
"no change": {
|
||||
in: &terraform.AzureClusterVariables{
|
||||
ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0",
|
||||
},
|
||||
want: &terraform.AzureClusterVariables{
|
||||
ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0",
|
||||
},
|
||||
},
|
||||
"fix image id": {
|
||||
in: &terraform.AzureClusterVariables{
|
||||
ImageID: "/CommunityGalleries/foo/Images/constellation/Versions/2.1.0",
|
||||
},
|
||||
want: &terraform.AzureClusterVariables{
|
||||
ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0",
|
||||
},
|
||||
},
|
||||
"fix resource group": {
|
||||
in: &terraform.AzureClusterVariables{
|
||||
UserAssignedIdentity: "/subscriptions/foo/resourcegroups/test/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uai",
|
||||
},
|
||||
want: &terraform.AzureClusterVariables{
|
||||
UserAssignedIdentity: "/subscriptions/foo/resourceGroups/test/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uai",
|
||||
},
|
||||
},
|
||||
"fix arbitrary casing": {
|
||||
in: &terraform.AzureClusterVariables{
|
||||
ImageID: "/CoMMUnitygaLLeries/foo/iMAges/constellation/vERsions/2.1.0",
|
||||
UserAssignedIdentity: "/subsCRiptions/foo/resoURCegroups/test/proViDers/MICROsoft.mANAgedIdentity/USerASsignediDENtities/uai",
|
||||
},
|
||||
want: &terraform.AzureClusterVariables{
|
||||
ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0",
|
||||
UserAssignedIdentity: "/subscriptions/foo/resourceGroups/test/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uai",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
out := normalizeAzureURIs(tc.in)
|
||||
assert.Equal(tc.want, out)
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue