/* 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/terraform" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/constellation/state" "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, attestation variant.Variant, 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 && attestation.Equal(variant.AzureSEVSNP{}) && 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)) } // WorkingDirIsEmpty returns true if the working directory of the Applier is empty. func (a *Applier) WorkingDirIsEmpty() (bool, error) { return a.fileHandler.IsEmpty(a.workingDir) } 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(), conf.UseMarketplaceImage(), ) 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) 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 }