From cf822f7eee715fa93e04d08a1aed68e06601c63c Mon Sep 17 00:00:00 2001 From: Otto Bittner Date: Fri, 21 Jul 2023 10:04:29 +0200 Subject: [PATCH] cli: unify terraform variable creation (#2119) Before we defined the variables twice. Once for upgrades, once for create. Also move default node group names into a constant --- cli/internal/cloudcmd/BUILD.bazel | 1 + cli/internal/cloudcmd/create.go | 363 +++++------------- cli/internal/cloudcmd/create_test.go | 25 +- cli/internal/cloudcmd/terraform.go | 209 ++++++++++ cli/internal/cmd/BUILD.bazel | 1 - cli/internal/cmd/upgradeapply.go | 124 +----- cli/internal/cmd/upgradecheck.go | 8 +- cli/internal/terraform/variables.go | 111 ++++-- cli/internal/terraform/variables_test.go | 29 +- internal/constants/constants.go | 4 + .../internal/cloud/aws/client/BUILD.bazel | 2 + .../internal/cloud/aws/client/scalinggroup.go | 5 +- .../cloud/aws/client/scalinggroup_test.go | 5 +- .../internal/cloud/azure/client/BUILD.bazel | 2 + .../cloud/azure/client/scalinggroup.go | 5 +- .../cloud/azure/client/scalinggroup_test.go | 7 +- .../internal/cloud/gcp/client/BUILD.bazel | 2 + .../internal/cloud/gcp/client/scalinggroup.go | 5 +- .../cloud/gcp/client/scalinggroup_test.go | 7 +- 19 files changed, 464 insertions(+), 451 deletions(-) create mode 100644 cli/internal/cloudcmd/terraform.go diff --git a/cli/internal/cloudcmd/BUILD.bazel b/cli/internal/cloudcmd/BUILD.bazel index 89f54a418..c1316fa54 100644 --- a/cli/internal/cloudcmd/BUILD.bazel +++ b/cli/internal/cloudcmd/BUILD.bazel @@ -11,6 +11,7 @@ go_library( "patch.go", "rollback.go", "terminate.go", + "terraform.go", "validators.go", ], importpath = "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd", diff --git a/cli/internal/cloudcmd/create.go b/cli/internal/cloudcmd/create.go index a98cc2b75..3a5e069cf 100644 --- a/cli/internal/cloudcmd/create.go +++ b/cli/internal/cloudcmd/create.go @@ -21,12 +21,10 @@ import ( "github.com/edgelesssys/constellation/v2/cli/internal/clusterid" "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/imagefetcher" - "github.com/edgelesssys/constellation/v2/internal/role" ) // Creator creates cloud resources. @@ -79,201 +77,47 @@ func (c *Creator) Create(ctx context.Context, opts CreateOptions) (clusterid.Fil } opts.image = image + cl, err := c.newTerraformClient(ctx) + if err != nil { + return clusterid.File{}, err + } + defer cl.RemoveInstaller() + + var tfOutput terraform.CreateOutput switch opts.Provider { case cloudprovider.AWS: - cl, err := c.newTerraformClient(ctx) - if err != nil { - return clusterid.File{}, err - } - defer cl.RemoveInstaller() - return c.createAWS(ctx, cl, opts) + + tfOutput, err = c.createAWS(ctx, cl, opts) case cloudprovider.GCP: - cl, err := c.newTerraformClient(ctx) - if err != nil { - return clusterid.File{}, err - } - defer cl.RemoveInstaller() - return c.createGCP(ctx, cl, opts) + + tfOutput, err = c.createGCP(ctx, cl, opts) case cloudprovider.Azure: - cl, err := c.newTerraformClient(ctx) - if err != nil { - return clusterid.File{}, err - } - defer cl.RemoveInstaller() - return c.createAzure(ctx, cl, opts) + + tfOutput, err = c.createAzure(ctx, cl, opts) case cloudprovider.OpenStack: - cl, err := c.newTerraformClient(ctx) - if err != nil { - return clusterid.File{}, err - } - defer cl.RemoveInstaller() - return c.createOpenStack(ctx, cl, opts) + + tfOutput, err = c.createOpenStack(ctx, cl, opts) case cloudprovider.QEMU: if runtime.GOARCH != "amd64" || runtime.GOOS != "linux" { return clusterid.File{}, fmt.Errorf("creation of a QEMU based Constellation is not supported for %s/%s", runtime.GOOS, runtime.GOARCH) } - cl, err := c.newTerraformClient(ctx) - if err != nil { - return clusterid.File{}, err - } - defer cl.RemoveInstaller() lv := c.newLibvirtRunner() qemuOpts := qemuCreateOptions{ source: image, CreateOptions: opts, } - return c.createQEMU(ctx, cl, lv, qemuOpts) + + tfOutput, err = c.createQEMU(ctx, cl, lv, qemuOpts) default: return clusterid.File{}, fmt.Errorf("unsupported cloud provider: %s", opts.Provider) } -} -func (c *Creator) createAWS(ctx context.Context, cl terraformClient, opts CreateOptions) (idFile clusterid.File, retErr error) { - vars := terraform.AWSClusterVariables{ - Name: opts.Config.Name, - NodeGroups: map[string]terraform.AWSNodeGroup{ - "control_plane_default": { - Role: role.ControlPlane.TFString(), - StateDiskSizeGB: opts.Config.StateDiskSizeGB, - InitialCount: opts.ControlPlaneCount, - Zone: opts.Config.Provider.AWS.Zone, - InstanceType: opts.InsType, - DiskType: opts.Config.Provider.AWS.StateDiskType, - }, - "worker_default": { - Role: role.Worker.TFString(), - StateDiskSizeGB: opts.Config.StateDiskSizeGB, - InitialCount: opts.WorkerCount, - Zone: opts.Config.Provider.AWS.Zone, - InstanceType: opts.InsType, - DiskType: opts.Config.Provider.AWS.StateDiskType, - }, - }, - Region: opts.Config.Provider.AWS.Region, - Zone: opts.Config.Provider.AWS.Zone, - AMIImageID: opts.image, - IAMProfileControlPlane: opts.Config.Provider.AWS.IAMProfileControlPlane, - IAMProfileWorkerNodes: opts.Config.Provider.AWS.IAMProfileWorkerNodes, - Debug: opts.Config.IsDebugCluster(), - EnableSNP: opts.Config.GetAttestationConfig().GetVariant().Equal(variant.AWSSEVSNP{}), - } - - if err := cl.PrepareWorkspace(path.Join("terraform", strings.ToLower(cloudprovider.AWS.String())), &vars); err != nil { - return clusterid.File{}, err - } - - defer rollbackOnError(c.out, &retErr, &rollbackerTerraform{client: cl}, opts.TFLogLevel) - tfOutput, err := cl.CreateCluster(ctx, opts.TFLogLevel) if err != nil { - return clusterid.File{}, err + return clusterid.File{}, fmt.Errorf("creating cluster: %w", err) } return clusterid.File{ - CloudProvider: cloudprovider.AWS, - InitSecret: []byte(tfOutput.Secret), - IP: tfOutput.IP, - UID: tfOutput.UID, - }, nil -} - -func (c *Creator) createGCP(ctx context.Context, cl terraformClient, opts CreateOptions) (idFile clusterid.File, retErr error) { - vars := terraform.GCPClusterVariables{ - Name: opts.Config.Name, - NodeGroups: map[string]terraform.GCPNodeGroup{ - "control_plane_default": { - Role: role.ControlPlane.TFString(), - StateDiskSizeGB: opts.Config.StateDiskSizeGB, - InitialCount: opts.ControlPlaneCount, - Zone: opts.Config.Provider.GCP.Zone, - InstanceType: opts.InsType, - DiskType: opts.Config.Provider.GCP.StateDiskType, - }, - "worker_default": { - Role: role.Worker.TFString(), - StateDiskSizeGB: opts.Config.StateDiskSizeGB, - InitialCount: opts.WorkerCount, - Zone: opts.Config.Provider.GCP.Zone, - InstanceType: opts.InsType, - DiskType: opts.Config.Provider.GCP.StateDiskType, - }, - }, - Project: opts.Config.Provider.GCP.Project, - Region: opts.Config.Provider.GCP.Region, - Zone: opts.Config.Provider.GCP.Zone, - ImageID: opts.image, - Debug: opts.Config.IsDebugCluster(), - } - - if err := cl.PrepareWorkspace(path.Join("terraform", strings.ToLower(cloudprovider.GCP.String())), &vars); err != nil { - return clusterid.File{}, err - } - - defer rollbackOnError(c.out, &retErr, &rollbackerTerraform{client: cl}, opts.TFLogLevel) - tfOutput, err := cl.CreateCluster(ctx, opts.TFLogLevel) - if err != nil { - return clusterid.File{}, err - } - - return clusterid.File{ - CloudProvider: cloudprovider.GCP, - InitSecret: []byte(tfOutput.Secret), - IP: tfOutput.IP, - UID: tfOutput.UID, - }, nil -} - -func (c *Creator) createAzure(ctx context.Context, cl terraformClient, opts CreateOptions) (idFile clusterid.File, retErr error) { - vars := terraform.AzureClusterVariables{ - Name: opts.Config.Name, - NodeGroups: map[string]terraform.AzureNodeGroup{ - "control_plane_default": { - Role: role.ControlPlane.TFString(), - InitialCount: toPtr(opts.ControlPlaneCount), - InstanceType: opts.InsType, - DiskSizeGB: opts.Config.StateDiskSizeGB, - DiskType: opts.Config.Provider.Azure.StateDiskType, - Zones: nil, // TODO(elchead): support zones AB#3225 - }, - "worker_default": { - Role: role.Worker.TFString(), - InitialCount: toPtr(opts.WorkerCount), - InstanceType: opts.InsType, - DiskSizeGB: opts.Config.StateDiskSizeGB, - DiskType: opts.Config.Provider.Azure.StateDiskType, - Zones: nil, - }, - }, - Location: opts.Config.Provider.Azure.Location, - ImageID: opts.image, - CreateMAA: toPtr(opts.Config.GetAttestationConfig().GetVariant().Equal(variant.AzureSEVSNP{})), - Debug: toPtr(opts.Config.IsDebugCluster()), - ConfidentialVM: toPtr(opts.Config.GetAttestationConfig().GetVariant().Equal(variant.AzureSEVSNP{})), - SecureBoot: opts.Config.Provider.Azure.SecureBoot, - UserAssignedIdentity: opts.Config.Provider.Azure.UserAssignedIdentity, - ResourceGroup: opts.Config.Provider.Azure.ResourceGroup, - } - - vars = normalizeAzureURIs(vars) - - if err := cl.PrepareWorkspace(path.Join("terraform", strings.ToLower(cloudprovider.Azure.String())), &vars); err != nil { - return clusterid.File{}, err - } - - defer rollbackOnError(c.out, &retErr, &rollbackerTerraform{client: cl}, opts.TFLogLevel) - tfOutput, err := cl.CreateCluster(ctx, opts.TFLogLevel) - if err != nil { - return clusterid.File{}, err - } - - if vars.CreateMAA != nil && *vars.CreateMAA { - // Patch the attestation policy to allow the cluster to boot while having secure boot disabled. - if err := c.policyPatcher.Patch(ctx, tfOutput.AttestationURL); err != nil { - return clusterid.File{}, err - } - } - - return clusterid.File{ - CloudProvider: cloudprovider.Azure, + CloudProvider: opts.Provider, IP: tfOutput.IP, InitSecret: []byte(tfOutput.Secret), UID: tfOutput.UID, @@ -281,6 +125,46 @@ func (c *Creator) createAzure(ctx context.Context, cl terraformClient, opts Crea }, nil } +func (c *Creator) createAWS(ctx context.Context, cl terraformClient, opts CreateOptions) (tfOutput terraform.CreateOutput, retErr error) { + vars := awsTerraformVars(opts.Config, opts.image, &opts.ControlPlaneCount, &opts.WorkerCount) + + tfOutput, err := runTerraformCreate(ctx, cl, cloudprovider.AWS, vars, c.out, opts.TFLogLevel) + if err != nil { + return terraform.CreateOutput{}, err + } + + return tfOutput, nil +} + +func (c *Creator) createGCP(ctx context.Context, cl terraformClient, opts CreateOptions) (tfOutput terraform.CreateOutput, retErr error) { + vars := gcpTerraformVars(opts.Config, opts.image, &opts.ControlPlaneCount, &opts.WorkerCount) + + tfOutput, err := runTerraformCreate(ctx, cl, cloudprovider.GCP, vars, c.out, opts.TFLogLevel) + if err != nil { + return terraform.CreateOutput{}, err + } + + return tfOutput, nil +} + +func (c *Creator) createAzure(ctx context.Context, cl terraformClient, opts CreateOptions) (tfOutput terraform.CreateOutput, retErr error) { + vars := azureTerraformVars(opts.Config, opts.image, &opts.ControlPlaneCount, &opts.WorkerCount) + + tfOutput, err := runTerraformCreate(ctx, cl, cloudprovider.Azure, vars, c.out, opts.TFLogLevel) + if err != nil { + return terraform.CreateOutput{}, err + } + + if vars.GetCreateMAA() { + // Patch the attestation policy to allow the cluster to boot while having secure boot disabled. + if err := c.policyPatcher.Patch(ctx, tfOutput.AttestationURL); err != nil { + return terraform.CreateOutput{}, 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 @@ -299,7 +183,7 @@ var ( caseInsensitiveVersionsRegExp = regexp.MustCompile(`(?i)\/versions\/`) ) -func normalizeAzureURIs(vars terraform.AzureClusterVariables) terraform.AzureClusterVariables { +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/") @@ -312,13 +196,13 @@ func normalizeAzureURIs(vars terraform.AzureClusterVariables) terraform.AzureClu return vars } -func (c *Creator) createOpenStack(ctx context.Context, cl terraformClient, opts CreateOptions) (idFile clusterid.File, retErr error) { +func (c *Creator) createOpenStack(ctx context.Context, cl terraformClient, opts CreateOptions) (tfOutput terraform.CreateOutput, retErr error) { // TODO(malt3): Remove this once OpenStack is supported. if os.Getenv("CONSTELLATION_OPENSTACK_DEV") != "1" { - return clusterid.File{}, errors.New("OpenStack isn't supported yet") + return terraform.CreateOutput{}, errors.New("OpenStack isn't supported yet") } if _, hasOSAuthURL := os.LookupEnv("OS_AUTH_URL"); !hasOSAuthURL && opts.Config.Provider.OpenStack.Cloud == "" { - return clusterid.File{}, errors.New( + return terraform.CreateOutput{}, 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\". " + @@ -326,51 +210,28 @@ func (c *Creator) createOpenStack(ctx context.Context, cl terraformClient, opts ) } - vars := terraform.OpenStackClusterVariables{ - Name: opts.Config.Name, - Cloud: toPtr(opts.Config.Provider.OpenStack.Cloud), - FlavorID: opts.Config.Provider.OpenStack.FlavorID, - FloatingIPPoolID: opts.Config.Provider.OpenStack.FloatingIPPoolID, - ImageURL: opts.image, - DirectDownload: *opts.Config.Provider.OpenStack.DirectDownload, - OpenstackUserDomainName: opts.Config.Provider.OpenStack.UserDomainName, - OpenstackUsername: opts.Config.Provider.OpenStack.Username, - OpenstackPassword: opts.Config.Provider.OpenStack.Password, - Debug: opts.Config.IsDebugCluster(), - NodeGroups: map[string]terraform.OpenStackNodeGroup{ - "control_plane_default": { - Role: role.ControlPlane.TFString(), - InitialCount: opts.ControlPlaneCount, - Zone: opts.Config.Provider.OpenStack.AvailabilityZone, // TODO(elchead): make configurable AB#3225 - StateDiskType: opts.Config.Provider.OpenStack.StateDiskType, - StateDiskSizeGB: opts.Config.StateDiskSizeGB, - }, - "worker_default": { - Role: role.Worker.TFString(), - InitialCount: opts.WorkerCount, - Zone: opts.Config.Provider.OpenStack.AvailabilityZone, // TODO(elchead): make configurable AB#3225 - StateDiskType: opts.Config.Provider.OpenStack.StateDiskType, - StateDiskSizeGB: opts.Config.StateDiskSizeGB, - }, - }, - } + vars := openStackTerraformVars(opts.Config, opts.image, &opts.ControlPlaneCount, &opts.WorkerCount) - if err := cl.PrepareWorkspace(path.Join("terraform", strings.ToLower(cloudprovider.OpenStack.String())), &vars); err != nil { - return clusterid.File{}, err - } - - defer rollbackOnError(c.out, &retErr, &rollbackerTerraform{client: cl}, opts.TFLogLevel) - tfOutput, err := cl.CreateCluster(ctx, opts.TFLogLevel) + tfOutput, err := runTerraformCreate(ctx, cl, cloudprovider.OpenStack, vars, c.out, opts.TFLogLevel) if err != nil { - return clusterid.File{}, err + return terraform.CreateOutput{}, err } - return clusterid.File{ - CloudProvider: cloudprovider.OpenStack, - IP: tfOutput.IP, - InitSecret: []byte(tfOutput.Secret), - UID: tfOutput.UID, - }, nil + return tfOutput, nil +} + +func runTerraformCreate(ctx context.Context, cl terraformClient, provider cloudprovider.Provider, vars terraform.Variables, outWriter io.Writer, loglevel terraform.LogLevel) (output terraform.CreateOutput, retErr error) { + if err := cl.PrepareWorkspace(path.Join("terraform", strings.ToLower(provider.String())), vars); err != nil { + return terraform.CreateOutput{}, err + } + + defer rollbackOnError(outWriter, &retErr, &rollbackerTerraform{client: cl}, loglevel) + tfOutput, err := cl.CreateCluster(ctx, loglevel) + if err != nil { + return terraform.CreateOutput{}, err + } + + return tfOutput, nil } type qemuCreateOptions struct { @@ -378,7 +239,7 @@ type qemuCreateOptions struct { CreateOptions } -func (c *Creator) createQEMU(ctx context.Context, cl terraformClient, lv libvirtRunner, opts qemuCreateOptions) (idFile clusterid.File, retErr error) { +func (c *Creator) createQEMU(ctx context.Context, cl terraformClient, lv libvirtRunner, opts qemuCreateOptions) (tfOutput terraform.CreateOutput, retErr error) { qemuRollbacker := &rollbackerQEMU{client: cl, libvirt: lv, createdWorkspace: false} defer rollbackOnError(c.out, &retErr, qemuRollbacker, opts.TFLogLevel) @@ -386,7 +247,7 @@ func (c *Creator) createQEMU(ctx context.Context, cl terraformClient, lv libvirt downloader := c.newRawDownloader() imagePath, err := downloader.Download(ctx, c.out, false, opts.source, opts.Config.Image) if err != nil { - return clusterid.File{}, fmt.Errorf("download raw image: %w", err) + return terraform.CreateOutput{}, fmt.Errorf("download raw image: %w", err) } libvirtURI := opts.Config.Provider.QEMU.LibvirtURI @@ -396,7 +257,7 @@ func (c *Creator) createQEMU(ctx context.Context, cl terraformClient, lv libvirt // 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 clusterid.File{}, fmt.Errorf("start libvirt container: %w", err) + return terraform.CreateOutput{}, fmt.Errorf("start libvirt container: %w", err) } libvirtURI = libvirt.LibvirtTCPConnectURI @@ -412,11 +273,11 @@ func (c *Creator) createQEMU(ctx context.Context, cl terraformClient, lv libvirt case strings.HasPrefix(libvirtURI, "qemu+unix://"): unixURI, err := url.Parse(strings.TrimPrefix(libvirtURI, "qemu+unix://")) if err != nil { - return clusterid.File{}, err + return terraform.CreateOutput{}, err } libvirtSocketPath = unixURI.Query().Get("socket") if libvirtSocketPath == "" { - return clusterid.File{}, fmt.Errorf("socket path not specified in qemu+unix URI: %s", libvirtURI) + return terraform.CreateOutput{}, fmt.Errorf("socket path not specified in qemu+unix URI: %s", libvirtURI) } } @@ -425,63 +286,25 @@ func (c *Creator) createQEMU(ctx context.Context, cl terraformClient, lv libvirt metadataLibvirtURI = "qemu:///system" } - vars := terraform.QEMUVariables{ - Name: opts.Config.Name, - LibvirtURI: libvirtURI, - LibvirtSocketPath: libvirtSocketPath, - // TODO(malt3): auto select boot mode based on attestation variant. - // requires image info v2. - BootMode: "uefi", - ImagePath: imagePath, - ImageFormat: opts.Config.Provider.QEMU.ImageFormat, - NodeGroups: map[string]terraform.QEMUNodeGroup{ - "control_plane_default": { - Role: role.ControlPlane.TFString(), - InitialCount: opts.ControlPlaneCount, - DiskSize: opts.Config.StateDiskSizeGB, - CPUCount: opts.Config.Provider.QEMU.VCPUs, - MemorySize: opts.Config.Provider.QEMU.Memory, - }, - "worker_default": { - Role: role.Worker.TFString(), - InitialCount: opts.WorkerCount, - DiskSize: opts.Config.StateDiskSizeGB, - CPUCount: opts.Config.Provider.QEMU.VCPUs, - MemorySize: opts.Config.Provider.QEMU.Memory, - }, - }, - Machine: "q35", // TODO(elchead): make configurable AB#3225 - MetadataAPIImage: opts.Config.Provider.QEMU.MetadataAPIImage, - MetadataLibvirtURI: metadataLibvirtURI, - NVRAM: opts.Config.Provider.QEMU.NVRAM, - // TODO(malt3) enable once we have a way to auto-select values for these - // requires image info v2. - // BzImagePath: placeholder, - // InitrdPath: placeholder, - // KernelCmdline: placeholder, - } + vars := qemuTerraformVars(opts.Config, imagePath, &opts.ControlPlaneCount, &opts.WorkerCount, libvirtURI, libvirtSocketPath, metadataLibvirtURI) + if opts.Config.Provider.QEMU.Firmware != "" { vars.Firmware = toPtr(opts.Config.Provider.QEMU.Firmware) } - if err := cl.PrepareWorkspace(path.Join("terraform", strings.ToLower(cloudprovider.QEMU.String())), &vars); err != nil { - return clusterid.File{}, fmt.Errorf("prepare workspace: %w", err) + if err := cl.PrepareWorkspace(path.Join("terraform", strings.ToLower(cloudprovider.QEMU.String())), vars); err != nil { + return terraform.CreateOutput{}, fmt.Errorf("prepare workspace: %w", err) } // Allow rollback of QEMU Terraform workspace from this point on qemuRollbacker.createdWorkspace = true - tfOutput, err := cl.CreateCluster(ctx, opts.TFLogLevel) + tfOutput, err = cl.CreateCluster(ctx, opts.TFLogLevel) if err != nil { - return clusterid.File{}, fmt.Errorf("create cluster: %w", err) + return terraform.CreateOutput{}, fmt.Errorf("create cluster: %w", err) } - return clusterid.File{ - CloudProvider: cloudprovider.QEMU, - InitSecret: []byte(tfOutput.Secret), - IP: tfOutput.IP, - UID: tfOutput.UID, - }, nil + return tfOutput, nil } func toPtr[T any](v T) *T { diff --git a/cli/internal/cloudcmd/create_test.go b/cli/internal/cloudcmd/create_test.go index eded26230..b8b5fb6f9 100644 --- a/cli/internal/cloudcmd/create_test.go +++ b/cli/internal/cloudcmd/create_test.go @@ -188,6 +188,7 @@ func TestCreator(t *testing.T) { wantErr: true, }, "unknown provider": { + tfClient: &stubTerraformClient{}, provider: cloudprovider.Unknown, config: config.Default(), wantErr: true, @@ -257,43 +258,43 @@ func (s stubPolicyPatcher) Patch(_ context.Context, _ string) error { func TestNormalizeAzureURIs(t *testing.T) { testCases := map[string]struct { - in terraform.AzureClusterVariables - want terraform.AzureClusterVariables + in *terraform.AzureClusterVariables + want *terraform.AzureClusterVariables }{ "empty": { - in: terraform.AzureClusterVariables{}, - want: terraform.AzureClusterVariables{}, + in: &terraform.AzureClusterVariables{}, + want: &terraform.AzureClusterVariables{}, }, "no change": { - in: terraform.AzureClusterVariables{ + in: &terraform.AzureClusterVariables{ ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0", }, - want: terraform.AzureClusterVariables{ + want: &terraform.AzureClusterVariables{ ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0", }, }, "fix image id": { - in: terraform.AzureClusterVariables{ + in: &terraform.AzureClusterVariables{ ImageID: "/CommunityGalleries/foo/Images/constellation/Versions/2.1.0", }, - want: terraform.AzureClusterVariables{ + want: &terraform.AzureClusterVariables{ ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0", }, }, "fix resource group": { - in: terraform.AzureClusterVariables{ + in: &terraform.AzureClusterVariables{ UserAssignedIdentity: "/subscriptions/foo/resourcegroups/test/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uai", }, - want: terraform.AzureClusterVariables{ + want: &terraform.AzureClusterVariables{ UserAssignedIdentity: "/subscriptions/foo/resourceGroups/test/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uai", }, }, "fix arbitrary casing": { - in: terraform.AzureClusterVariables{ + 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{ + want: &terraform.AzureClusterVariables{ ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0", UserAssignedIdentity: "/subscriptions/foo/resourceGroups/test/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uai", }, diff --git a/cli/internal/cloudcmd/terraform.go b/cli/internal/cloudcmd/terraform.go new file mode 100644 index 000000000..bf972aa5e --- /dev/null +++ b/cli/internal/cloudcmd/terraform.go @@ -0,0 +1,209 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package cloudcmd + +import ( + "fmt" + + "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/role" +) + +// TerraformUpgradeVars returns variables required to execute the Terraform scripts. +func TerraformUpgradeVars(conf *config.Config, imageRef string) (terraform.Variables, error) { + switch conf.GetProvider() { + case cloudprovider.AWS: + vars := awsTerraformVars(conf, imageRef, nil, nil) + return vars, nil + case cloudprovider.Azure: + vars := azureTerraformVars(conf, imageRef, nil, nil) + return vars, nil + case cloudprovider.GCP: + vars := gcpTerraformVars(conf, imageRef, nil, nil) + return vars, nil + default: + return nil, fmt.Errorf("unsupported provider: %s", conf.GetProvider()) + } +} + +// awsTerraformVars provides variables required to execute the Terraform scripts. +// It should be the only place to declare the AWS variables. +func awsTerraformVars(conf *config.Config, imageRef string, controlPlaneCount, workerCount *int) *terraform.AWSClusterVariables { + return &terraform.AWSClusterVariables{ + Name: conf.Name, + NodeGroups: map[string]terraform.AWSNodeGroup{ + constants.ControlPlaneDefault: { + Role: role.ControlPlane.TFString(), + StateDiskSizeGB: conf.StateDiskSizeGB, + InitialCount: controlPlaneCount, + Zone: conf.Provider.AWS.Zone, + InstanceType: conf.Provider.AWS.InstanceType, + DiskType: conf.Provider.AWS.StateDiskType, + }, + constants.WorkerDefault: { + Role: role.Worker.TFString(), + StateDiskSizeGB: conf.StateDiskSizeGB, + InitialCount: workerCount, + Zone: conf.Provider.AWS.Zone, + InstanceType: conf.Provider.AWS.InstanceType, + DiskType: conf.Provider.AWS.StateDiskType, + }, + }, + Region: conf.Provider.AWS.Region, + Zone: conf.Provider.AWS.Zone, + AMIImageID: imageRef, + IAMProfileControlPlane: conf.Provider.AWS.IAMProfileControlPlane, + IAMProfileWorkerNodes: conf.Provider.AWS.IAMProfileWorkerNodes, + Debug: conf.IsDebugCluster(), + EnableSNP: conf.GetAttestationConfig().GetVariant().Equal(variant.AWSSEVSNP{}), + } +} + +// 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, controlPlaneCount, workerCount *int) *terraform.AzureClusterVariables { + vars := &terraform.AzureClusterVariables{ + Name: conf.Name, + NodeGroups: map[string]terraform.AzureNodeGroup{ + constants.ControlPlaneDefault: { + Role: "control-plane", + InitialCount: controlPlaneCount, + InstanceType: conf.Provider.Azure.InstanceType, + DiskSizeGB: conf.StateDiskSizeGB, + DiskType: conf.Provider.Azure.StateDiskType, + Zones: nil, // TODO(elchead): support zones AB#3225. check if lifecycle arg is required. + }, + constants.WorkerDefault: { + Role: "worker", + InitialCount: workerCount, + InstanceType: conf.Provider.Azure.InstanceType, + DiskSizeGB: conf.StateDiskSizeGB, + DiskType: conf.Provider.Azure.StateDiskType, + Zones: nil, + }, + }, + Location: conf.Provider.Azure.Location, + ImageID: imageRef, + CreateMAA: toPtr(conf.GetAttestationConfig().GetVariant().Equal(variant.AzureSEVSNP{})), + Debug: toPtr(conf.IsDebugCluster()), + ConfidentialVM: toPtr(conf.GetAttestationConfig().GetVariant().Equal(variant.AzureSEVSNP{})), + SecureBoot: conf.Provider.Azure.SecureBoot, + UserAssignedIdentity: conf.Provider.Azure.UserAssignedIdentity, + ResourceGroup: conf.Provider.Azure.ResourceGroup, + } + + vars = normalizeAzureURIs(vars) + return vars +} + +// gcpTerraformVars provides variables required to execute the Terraform scripts. +// It should be the only place to declare the GCP variables. +func gcpTerraformVars(conf *config.Config, imageRef string, controlPlaneCount, workerCount *int) *terraform.GCPClusterVariables { + return &terraform.GCPClusterVariables{ + Name: conf.Name, + NodeGroups: map[string]terraform.GCPNodeGroup{ + constants.ControlPlaneDefault: { + Role: role.ControlPlane.TFString(), + StateDiskSizeGB: conf.StateDiskSizeGB, + InitialCount: controlPlaneCount, + Zone: conf.Provider.GCP.Zone, + InstanceType: conf.Provider.GCP.InstanceType, + DiskType: conf.Provider.GCP.StateDiskType, + }, + constants.WorkerDefault: { + Role: role.Worker.TFString(), + StateDiskSizeGB: conf.StateDiskSizeGB, + InitialCount: workerCount, + Zone: conf.Provider.GCP.Zone, + InstanceType: conf.Provider.GCP.InstanceType, + DiskType: conf.Provider.GCP.StateDiskType, + }, + }, + Project: conf.Provider.GCP.Project, + Region: conf.Provider.GCP.Region, + Zone: conf.Provider.GCP.Zone, + ImageID: imageRef, + Debug: conf.IsDebugCluster(), + } +} + +// 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, controlPlaneCount, workerCount *int) *terraform.OpenStackClusterVariables { + return &terraform.OpenStackClusterVariables{ + Name: conf.Name, + Cloud: toPtr(conf.Provider.OpenStack.Cloud), + FlavorID: conf.Provider.OpenStack.FlavorID, + FloatingIPPoolID: conf.Provider.OpenStack.FloatingIPPoolID, + ImageURL: imageRef, + DirectDownload: *conf.Provider.OpenStack.DirectDownload, + OpenstackUserDomainName: conf.Provider.OpenStack.UserDomainName, + OpenstackUsername: conf.Provider.OpenStack.Username, + OpenstackPassword: conf.Provider.OpenStack.Password, + Debug: conf.IsDebugCluster(), + NodeGroups: map[string]terraform.OpenStackNodeGroup{ + constants.ControlPlaneDefault: { + Role: role.ControlPlane.TFString(), + InitialCount: controlPlaneCount, + Zone: conf.Provider.OpenStack.AvailabilityZone, // TODO(elchead): make configurable AB#3225 + StateDiskType: conf.Provider.OpenStack.StateDiskType, + StateDiskSizeGB: conf.StateDiskSizeGB, + }, + constants.WorkerDefault: { + Role: role.Worker.TFString(), + InitialCount: workerCount, + Zone: conf.Provider.OpenStack.AvailabilityZone, // TODO(elchead): make configurable AB#3225 + StateDiskType: conf.Provider.OpenStack.StateDiskType, + StateDiskSizeGB: conf.StateDiskSizeGB, + }, + }, + } +} + +// 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, controlPlaneCount, workerCount *int, libvirtURI, libvirtSocketPath, metadataLibvirtURI string) *terraform.QEMUVariables { + return &terraform.QEMUVariables{ + Name: conf.Name, + LibvirtURI: libvirtURI, + LibvirtSocketPath: libvirtSocketPath, + // TODO(malt3): auto select boot mode based on attestation variant. + // requires image info v2. + BootMode: "uefi", + ImagePath: imageRef, + ImageFormat: conf.Provider.QEMU.ImageFormat, + NodeGroups: map[string]terraform.QEMUNodeGroup{ + constants.ControlPlaneDefault: { + Role: role.ControlPlane.TFString(), + InitialCount: controlPlaneCount, + DiskSize: conf.StateDiskSizeGB, + CPUCount: conf.Provider.QEMU.VCPUs, + MemorySize: conf.Provider.QEMU.Memory, + }, + constants.WorkerDefault: { + Role: role.Worker.TFString(), + InitialCount: workerCount, + DiskSize: conf.StateDiskSizeGB, + CPUCount: conf.Provider.QEMU.VCPUs, + MemorySize: conf.Provider.QEMU.Memory, + }, + }, + Machine: "q35", // TODO(elchead): make configurable AB#3225 + MetadataAPIImage: conf.Provider.QEMU.MetadataAPIImage, + MetadataLibvirtURI: metadataLibvirtURI, + NVRAM: conf.Provider.QEMU.NVRAM, + // TODO(malt3) enable once we have a way to auto-select values for these + // requires image info v2. + // BzImagePath: placeholder, + // InitrdPath: placeholder, + // KernelCmdline: placeholder, + } +} diff --git a/cli/internal/cmd/BUILD.bazel b/cli/internal/cmd/BUILD.bazel index 579552b21..7d71d9f47 100644 --- a/cli/internal/cmd/BUILD.bazel +++ b/cli/internal/cmd/BUILD.bazel @@ -76,7 +76,6 @@ go_library( "//internal/license", "//internal/logger", "//internal/retry", - "//internal/role", "//internal/semver", "//internal/sigstore", "//internal/versions", diff --git a/cli/internal/cmd/upgradeapply.go b/cli/internal/cmd/upgradeapply.go index 1df995e44..ae7cf2fbd 100644 --- a/cli/internal/cmd/upgradeapply.go +++ b/cli/internal/cmd/upgradeapply.go @@ -11,9 +11,9 @@ import ( "errors" "fmt" "path/filepath" - "strings" "time" + "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" "github.com/edgelesssys/constellation/v2/cli/internal/clusterid" "github.com/edgelesssys/constellation/v2/cli/internal/helm" "github.com/edgelesssys/constellation/v2/cli/internal/kubernetes" @@ -27,7 +27,6 @@ import ( "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/role" "github.com/edgelesssys/constellation/v2/internal/versions" "github.com/spf13/afero" "github.com/spf13/cobra" @@ -141,6 +140,14 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command, fileHandler file.Hand return nil } +func getImage(ctx context.Context, conf *config.Config, fetcher imageFetcher) (string, error) { + // Fetch variables to execute Terraform script with + provider := conf.GetProvider() + attestationVariant := conf.GetAttestationConfig().GetVariant() + region := conf.GetRegion() + return fetcher.FetchReference(ctx, provider, attestationVariant, conf.Image, region) +} + // migrateTerraform checks if the Constellation version the cluster is being upgraded to requires a migration // of cloud resources with Terraform. If so, the migration is performed. func (u *upgradeApplyCmd) migrateTerraform(cmd *cobra.Command, fetcher imageFetcher, conf *config.Config, flags upgradeApplyFlags) error { @@ -161,7 +168,12 @@ func (u *upgradeApplyCmd) migrateTerraform(cmd *cobra.Command, fetcher imageFetc u.upgrader.AddManualStateMigration(migration) } - vars, err := parseTerraformUpgradeVars(cmd, conf, fetcher) + imageRef, err := getImage(cmd.Context(), conf, fetcher) + if err != nil { + return fmt.Errorf("fetching image reference: %w", err) + } + + vars, err := cloudcmd.TerraformUpgradeVars(conf, imageRef) if err != nil { return fmt.Errorf("parsing upgrade variables: %w", err) } @@ -210,108 +222,6 @@ func (u *upgradeApplyCmd) migrateTerraform(cmd *cobra.Command, fetcher imageFetc return nil } -// parseTerraformUpgradeVars parses the variables required to execute the Terraform script with. -func parseTerraformUpgradeVars(cmd *cobra.Command, conf *config.Config, fetcher imageFetcher) (terraform.Variables, error) { - // Fetch variables to execute Terraform script with - provider := conf.GetProvider() - attestationVariant := conf.GetAttestationConfig().GetVariant() - region := conf.GetRegion() - imageRef, err := fetcher.FetchReference(cmd.Context(), provider, attestationVariant, conf.Image, region) - if err != nil { - return nil, fmt.Errorf("fetching image reference: %w", err) - } - - switch conf.GetProvider() { - case cloudprovider.AWS: - vars := &terraform.AWSClusterVariables{ - Name: conf.Name, - NodeGroups: map[string]terraform.AWSNodeGroup{ - "control_plane_default": { - Role: role.ControlPlane.TFString(), - StateDiskSizeGB: conf.StateDiskSizeGB, - Zone: conf.Provider.AWS.Zone, - InstanceType: conf.Provider.AWS.InstanceType, - DiskType: conf.Provider.AWS.StateDiskType, - }, - "worker_default": { - Role: role.Worker.TFString(), - StateDiskSizeGB: conf.StateDiskSizeGB, - Zone: conf.Provider.AWS.Zone, - InstanceType: conf.Provider.AWS.InstanceType, - DiskType: conf.Provider.AWS.StateDiskType, - }, - }, - Region: conf.Provider.AWS.Region, - Zone: conf.Provider.AWS.Zone, - AMIImageID: imageRef, - IAMProfileControlPlane: conf.Provider.AWS.IAMProfileControlPlane, - IAMProfileWorkerNodes: conf.Provider.AWS.IAMProfileWorkerNodes, - Debug: conf.IsDebugCluster(), - } - return vars, nil - case cloudprovider.Azure: - // Azure Terraform provider is very strict about it's casing - imageRef = strings.Replace(imageRef, "CommunityGalleries", "communityGalleries", 1) - imageRef = strings.Replace(imageRef, "Images", "images", 1) - imageRef = strings.Replace(imageRef, "Versions", "versions", 1) - - vars := &terraform.AzureClusterVariables{ - Name: conf.Name, - NodeGroups: map[string]terraform.AzureNodeGroup{ - "control_plane_default": { - Role: "control-plane", - InstanceType: conf.Provider.Azure.InstanceType, - DiskSizeGB: conf.StateDiskSizeGB, - DiskType: conf.Provider.Azure.StateDiskType, - }, - "worker_default": { - Role: "worker", - InstanceType: conf.Provider.Azure.InstanceType, - DiskSizeGB: conf.StateDiskSizeGB, - DiskType: conf.Provider.Azure.StateDiskType, - }, - }, - Location: conf.Provider.Azure.Location, - ImageID: imageRef, - CreateMAA: toPtr(conf.GetAttestationConfig().GetVariant().Equal(variant.AzureSEVSNP{})), - Debug: toPtr(conf.IsDebugCluster()), - ConfidentialVM: toPtr(conf.GetAttestationConfig().GetVariant().Equal(variant.AzureSEVSNP{})), - SecureBoot: conf.Provider.Azure.SecureBoot, - UserAssignedIdentity: conf.Provider.Azure.UserAssignedIdentity, - ResourceGroup: conf.Provider.Azure.ResourceGroup, - } - return vars, nil - case cloudprovider.GCP: - vars := &terraform.GCPClusterVariables{ - Name: conf.Name, - NodeGroups: map[string]terraform.GCPNodeGroup{ - "control_plane_default": { - Role: role.ControlPlane.TFString(), - StateDiskSizeGB: conf.StateDiskSizeGB, - Zone: conf.Provider.GCP.Zone, - InstanceType: conf.Provider.GCP.InstanceType, - DiskType: conf.Provider.GCP.StateDiskType, - }, - "worker_default": { - Role: role.Worker.TFString(), - StateDiskSizeGB: conf.StateDiskSizeGB, - Zone: conf.Provider.GCP.Zone, - InstanceType: conf.Provider.GCP.InstanceType, - DiskType: conf.Provider.GCP.StateDiskType, - }, - }, - Project: conf.Provider.GCP.Project, - Region: conf.Provider.GCP.Region, - Zone: conf.Provider.GCP.Zone, - ImageID: imageRef, - Debug: conf.IsDebugCluster(), - } - return vars, nil - default: - return nil, fmt.Errorf("unsupported provider: %s", conf.GetProvider()) - } -} - // handleInvalidK8sPatchVersion checks if the Kubernetes patch version is supported and asks for confirmation if not. func handleInvalidK8sPatchVersion(cmd *cobra.Command, version string, yes bool) error { _, err := versions.NewValidK8sVersion(version, true) @@ -447,7 +357,3 @@ type cloudUpgrader interface { CleanUpTerraformMigrations() error AddManualStateMigration(migration terraform.StateMigration) } - -func toPtr[T any](v T) *T { - return &v -} diff --git a/cli/internal/cmd/upgradecheck.go b/cli/internal/cmd/upgradecheck.go index af2d61d3d..0237cebd8 100644 --- a/cli/internal/cmd/upgradecheck.go +++ b/cli/internal/cmd/upgradecheck.go @@ -15,6 +15,7 @@ import ( "sort" "strings" + "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" "github.com/edgelesssys/constellation/v2/cli/internal/featureset" "github.com/edgelesssys/constellation/v2/cli/internal/helm" "github.com/edgelesssys/constellation/v2/cli/internal/kubernetes" @@ -219,7 +220,12 @@ func (u *upgradeCheckCmd) upgradeCheck(cmd *cobra.Command, fileHandler file.Hand return fmt.Errorf("checking workspace: %w", err) } - vars, err := parseTerraformUpgradeVars(cmd, conf, u.imagefetcher) + imageRef, err := getImage(cmd.Context(), conf, u.imagefetcher) + if err != nil { + return fmt.Errorf("fetching image reference: %w", err) + } + + vars, err := cloudcmd.TerraformUpgradeVars(conf, imageRef) if err != nil { return fmt.Errorf("parsing upgrade variables: %w", err) } diff --git a/cli/internal/terraform/variables.go b/cli/internal/terraform/variables.go index 7751f9101..d8f82ac72 100644 --- a/cli/internal/terraform/variables.go +++ b/cli/internal/terraform/variables.go @@ -19,6 +19,17 @@ type Variables interface { fmt.Stringer } +// ClusterVariables should be used in places where a cluster is created. +type ClusterVariables interface { + Variables + // TODO (derpsteb): Rename this function once we have introduced an interface for config.Config. + // GetCreateMAA does not follow Go's naming convention because we need to keep the CreateMAA property public for now. + // There are functions creating Variables objects outside of this package. + // These functions can only be moved into this package once we have introduced an interface for config.Config, + // since we do not want to introduce a dependency on config.Config in this package. + GetCreateMAA() bool +} + // CommonVariables is user configuration for creating a cluster with Terraform. type CommonVariables struct { // Name of the cluster. @@ -64,6 +75,19 @@ type AWSClusterVariables struct { NodeGroups map[string]AWSNodeGroup `hcl:"node_groups" cty:"node_groups"` } +// GetCreateMAA gets the CreateMAA variable. +// TODO (derpsteb): Rename this function once we have introduced an interface for config.Config. +func (a *AWSClusterVariables) GetCreateMAA() bool { + return false +} + +// String returns a string representation of the variables, formatted as Terraform variables. +func (a *AWSClusterVariables) String() string { + f := hclwrite.NewEmptyFile() + gohcl.EncodeIntoBody(a, f.Body()) + return string(f.Bytes()) +} + // AWSNodeGroup is a node group to create on AWS. type AWSNodeGroup struct { // Role is the role of the node group. @@ -71,7 +95,8 @@ type AWSNodeGroup struct { // StateDiskSizeGB is the size of the state disk to allocate to each node, in GB. StateDiskSizeGB int `hcl:"disk_size" cty:"disk_size"` // InitialCount is the initial number of nodes to create in the node group. - InitialCount int `hcl:"initial_count" cty:"initial_count"` + // During upgrades this value is not set. + InitialCount *int `hcl:"initial_count" cty:"initial_count"` // Zone is the AWS availability-zone to use in the given region. Zone string `hcl:"zone" cty:"zone"` // InstanceType is the type of the EC2 instance to use. @@ -80,12 +105,6 @@ type AWSNodeGroup struct { DiskType string `hcl:"disk_type" cty:"disk_type"` } -func (v *AWSClusterVariables) String() string { - f := hclwrite.NewEmptyFile() - gohcl.EncodeIntoBody(v, f.Body()) - return string(f.Bytes()) -} - // AWSIAMVariables is user configuration for creating the IAM configuration with Terraform on Microsoft Azure. type AWSIAMVariables struct { // Region is the AWS location to use. (e.g. us-east-2) @@ -121,6 +140,19 @@ type GCPClusterVariables struct { NodeGroups map[string]GCPNodeGroup `hcl:"node_groups" cty:"node_groups"` } +// GetCreateMAA gets the CreateMAA variable. +// TODO (derpsteb): Rename this function once we have introduced an interface for config.Config. +func (g *GCPClusterVariables) GetCreateMAA() bool { + return false +} + +// String returns a string representation of the variables, formatted as Terraform variables. +func (g *GCPClusterVariables) String() string { + f := hclwrite.NewEmptyFile() + gohcl.EncodeIntoBody(g, f.Body()) + return string(f.Bytes()) +} + // GCPNodeGroup is a node group to create on GCP. type GCPNodeGroup struct { // Role is the role of the node group. @@ -128,19 +160,13 @@ type GCPNodeGroup struct { // StateDiskSizeGB is the size of the state disk to allocate to each node, in GB. StateDiskSizeGB int `hcl:"disk_size" cty:"disk_size"` // InitialCount is the initial number of nodes to create in the node group. - InitialCount int `hcl:"initial_count" cty:"initial_count"` + // During upgrades this value is not set. + InitialCount *int `hcl:"initial_count" cty:"initial_count"` Zone string `hcl:"zone" cty:"zone"` InstanceType string `hcl:"instance_type" cty:"instance_type"` DiskType string `hcl:"disk_type" cty:"disk_type"` } -// String returns a string representation of the variables, formatted as Terraform variables. -func (v *GCPClusterVariables) String() string { - f := hclwrite.NewEmptyFile() - gohcl.EncodeIntoBody(v, f.Body()) - return string(f.Bytes()) -} - // GCPIAMVariables is user configuration for creating the IAM confioguration with Terraform on GCP. type GCPIAMVariables struct { // Project is the ID of the GCP project to use. @@ -188,10 +214,20 @@ type AzureClusterVariables struct { NodeGroups map[string]AzureNodeGroup `hcl:"node_groups" cty:"node_groups"` } +// GetCreateMAA gets the CreateMAA variable. +// TODO (derpsteb): Rename this function once we have introduced an interface for config.Config. +func (a *AzureClusterVariables) GetCreateMAA() bool { + if a.CreateMAA == nil { + return false + } + + return *a.CreateMAA +} + // String returns a string representation of the variables, formatted as Terraform variables. -func (v *AzureClusterVariables) String() string { +func (a *AzureClusterVariables) String() string { f := hclwrite.NewEmptyFile() - gohcl.EncodeIntoBody(v, f.Body()) + gohcl.EncodeIntoBody(a, f.Body()) return string(f.Bytes()) } @@ -253,12 +289,26 @@ type OpenStackClusterVariables struct { Debug bool `hcl:"debug" cty:"debug"` } +// GetCreateMAA gets the CreateMAA variable. +// TODO (derpsteb): Rename this function once we have introduced an interface for config.Config. +func (o *OpenStackClusterVariables) GetCreateMAA() bool { + return false +} + +// String returns a string representation of the variables, formatted as Terraform variables. +func (o *OpenStackClusterVariables) String() string { + f := hclwrite.NewEmptyFile() + gohcl.EncodeIntoBody(o, f.Body()) + return string(f.Bytes()) +} + // OpenStackNodeGroup is a node group to create on OpenStack. type OpenStackNodeGroup struct { // Role is the role of the node group. Role string `hcl:"role" cty:"role"` // InitialCount is the number of instances to create. - InitialCount int `hcl:"initial_count" cty:"initial_count"` + // InitialCount is optional for upgrades. OpenStack does not support upgrades yet but might in the future. + InitialCount *int `hcl:"initial_count" cty:"initial_count"` // Zone is the OpenStack availability zone to use. Zone string `hcl:"zone" cty:"zone"` // StateDiskType is the OpenStack disk type to use for the state disk. @@ -267,13 +317,6 @@ type OpenStackNodeGroup struct { StateDiskSizeGB int `hcl:"state_disk_size" cty:"state_disk_size"` } -// String returns a string representation of the variables, formatted as Terraform variables. -func (v *OpenStackClusterVariables) String() string { - f := hclwrite.NewEmptyFile() - gohcl.EncodeIntoBody(v, f.Body()) - return string(f.Bytes()) -} - // TODO(malt3): Add support for OpenStack IAM variables. // QEMUVariables is user configuration for creating a QEMU cluster with Terraform. @@ -313,10 +356,16 @@ type QEMUVariables struct { KernelCmdline *string `hcl:"constellation_cmdline" cty:"constellation_cmdline"` } +// GetCreateMAA gets the CreateMAA variable. +// TODO (derpsteb): Rename this function once we have introduced an interface for config.Config. +func (q *QEMUVariables) GetCreateMAA() bool { + return false +} + // String returns a string representation of the variables, formatted as Terraform variables. -func (v *QEMUVariables) String() string { +func (q *QEMUVariables) String() string { // copy v object - vCopy := *v + vCopy := *q switch vCopy.NVRAM { case "production": vCopy.NVRAM = "/usr/share/OVMF/constellation_vars.production.fd" @@ -333,7 +382,9 @@ type QEMUNodeGroup struct { // Role is the role of the node group. Role string `hcl:"role" cty:"role"` // InitialCount is the number of instances to create. - InitialCount int `hcl:"initial_count" cty:"initial_count"` + // InitialCount is optional for upgrades. + // Upgrades are not implemented for QEMU. The type is similar to other NodeGroup types for consistency. + InitialCount *int `hcl:"initial_count" cty:"initial_count"` // DiskSize is the size of the disk to allocate to each node, in GiB. DiskSize int `hcl:"disk_size" cty:"disk_size"` // CPUCount is the number of CPUs to allocate to each node. @@ -346,3 +397,7 @@ func writeLinef(builder *strings.Builder, format string, a ...any) { builder.WriteString(fmt.Sprintf(format, a...)) builder.WriteByte('\n') } + +func toPtr[T any](v T) *T { + return &v +} diff --git a/cli/internal/terraform/variables_test.go b/cli/internal/terraform/variables_test.go index 6a3f47198..00efb329a 100644 --- a/cli/internal/terraform/variables_test.go +++ b/cli/internal/terraform/variables_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/role" "github.com/stretchr/testify/assert" ) @@ -18,18 +19,18 @@ func TestAWSClusterVariables(t *testing.T) { vars := AWSClusterVariables{ Name: "cluster-name", NodeGroups: map[string]AWSNodeGroup{ - "control_plane_default": { + constants.ControlPlaneDefault: { Role: role.ControlPlane.TFString(), StateDiskSizeGB: 30, - InitialCount: 1, + InitialCount: toPtr(1), Zone: "eu-central-1b", InstanceType: "x1.foo", DiskType: "foodisk", }, - "worker_default": { + constants.WorkerDefault: { Role: role.Worker.TFString(), StateDiskSizeGB: 30, - InitialCount: 2, + InitialCount: toPtr(2), Zone: "eu-central-1c", InstanceType: "x1.bar", DiskType: "bardisk", @@ -99,18 +100,18 @@ func TestGCPClusterVariables(t *testing.T) { ImageID: "image-0123456789abcdef", Debug: true, NodeGroups: map[string]GCPNodeGroup{ - "control_plane_default": { + constants.ControlPlaneDefault: { Role: "control-plane", StateDiskSizeGB: 30, - InitialCount: 1, + InitialCount: toPtr(1), Zone: "eu-central-1a", InstanceType: "n2d-standard-4", DiskType: "pd-ssd", }, - "worker_default": { + constants.WorkerDefault: { Role: "worker", StateDiskSizeGB: 10, - InitialCount: 1, + InitialCount: toPtr(1), Zone: "eu-central-1b", InstanceType: "n2d-standard-8", DiskType: "pd-ssd", @@ -170,7 +171,7 @@ func TestAzureClusterVariables(t *testing.T) { vars := AzureClusterVariables{ Name: "cluster-name", NodeGroups: map[string]AzureNodeGroup{ - "control_plane_default": { + constants.ControlPlaneDefault: { Role: "ControlPlane", InitialCount: to.Ptr(1), InstanceType: "Standard_D2s_v3", @@ -240,9 +241,9 @@ func TestOpenStackClusterVariables(t *testing.T) { OpenstackPassword: "my-password", Debug: true, NodeGroups: map[string]OpenStackNodeGroup{ - "control_plane_default": { + constants.ControlPlaneDefault: { Role: "control-plane", - InitialCount: 1, + InitialCount: toPtr(1), Zone: "az-01", StateDiskType: "performance-8", StateDiskSizeGB: 30, @@ -281,7 +282,7 @@ func TestQEMUClusterVariables(t *testing.T) { NodeGroups: map[string]QEMUNodeGroup{ "control-plane": { Role: role.ControlPlane.TFString(), - InitialCount: 1, + InitialCount: toPtr(1), DiskSize: 30, CPUCount: 4, MemorySize: 8192, @@ -326,7 +327,3 @@ constellation_cmdline = "console=ttyS0,115200n8" got := vars.String() assert.Equal(t, want, got) } - -func toPtr[T any](v T) *T { - return &v -} diff --git a/internal/constants/constants.go b/internal/constants/constants.go index b3413daca..21ccf595d 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -158,6 +158,10 @@ const ( TerraformUpgradeBackupDir = "terraform-backup" // UpgradeDir is the name of the directory being used for cluster upgrades. UpgradeDir = "constellation-upgrade" + // ControlPlaneDefault is the name of the default control plane worker group. + ControlPlaneDefault = "control_plane_default" + // WorkerDefault is the name of the default worker group. + WorkerDefault = "worker_default" // // Kubernetes. diff --git a/operators/constellation-node-operator/internal/cloud/aws/client/BUILD.bazel b/operators/constellation-node-operator/internal/cloud/aws/client/BUILD.bazel index 5dc373eaa..cd09820ba 100644 --- a/operators/constellation-node-operator/internal/cloud/aws/client/BUILD.bazel +++ b/operators/constellation-node-operator/internal/cloud/aws/client/BUILD.bazel @@ -14,6 +14,7 @@ go_library( importpath = "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/internal/cloud/aws/client", visibility = ["//operators/constellation-node-operator:__subpackages__"], deps = [ + "//internal/constants", "//operators/constellation-node-operator/api/v1alpha1", "//operators/constellation-node-operator/internal/cloud/api", "@com_github_aws_aws_sdk_go_v2_config//:config", @@ -36,6 +37,7 @@ go_test( ], embed = [":client"], deps = [ + "//internal/constants", "//operators/constellation-node-operator/api/v1alpha1", "//operators/constellation-node-operator/internal/cloud/api", "@com_github_aws_aws_sdk_go_v2_service_autoscaling//:autoscaling", diff --git a/operators/constellation-node-operator/internal/cloud/aws/client/scalinggroup.go b/operators/constellation-node-operator/internal/cloud/aws/client/scalinggroup.go index 6cb320d47..a7f4c2a00 100644 --- a/operators/constellation-node-operator/internal/cloud/aws/client/scalinggroup.go +++ b/operators/constellation-node-operator/internal/cloud/aws/client/scalinggroup.go @@ -16,6 +16,7 @@ import ( scalingtypes "github.com/aws/aws-sdk-go-v2/service/autoscaling/types" "github.com/aws/aws-sdk-go-v2/service/ec2" ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/edgelesssys/constellation/v2/internal/constants" updatev1alpha1 "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/api/v1alpha1" cspapi "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/internal/cloud/api" ) @@ -185,9 +186,9 @@ func (c *Client) ListScalingGroups(ctx context.Context, uid string) ([]cspapi.Sc if nodeGroupName == "" { switch role { case updatev1alpha1.ControlPlaneRole: - nodeGroupName = "control_plane_default" + nodeGroupName = constants.ControlPlaneDefault case updatev1alpha1.WorkerRole: - nodeGroupName = "worker_default" + nodeGroupName = constants.WorkerDefault } } diff --git a/operators/constellation-node-operator/internal/cloud/aws/client/scalinggroup_test.go b/operators/constellation-node-operator/internal/cloud/aws/client/scalinggroup_test.go index 0282eaddf..6f9c4cb49 100644 --- a/operators/constellation-node-operator/internal/cloud/aws/client/scalinggroup_test.go +++ b/operators/constellation-node-operator/internal/cloud/aws/client/scalinggroup_test.go @@ -14,6 +14,7 @@ import ( scalingtypes "github.com/aws/aws-sdk-go-v2/service/autoscaling/types" "github.com/aws/aws-sdk-go-v2/service/ec2" ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/edgelesssys/constellation/v2/internal/constants" cspapi "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/internal/cloud/api" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -279,14 +280,14 @@ func TestListScalingGroups(t *testing.T) { wantGroups: []cspapi.ScalingGroup{ { Name: "control-plane-asg", - NodeGroupName: "control_plane_default", + NodeGroupName: constants.ControlPlaneDefault, GroupID: "control-plane-asg", AutoscalingGroupName: "control-plane-asg", Role: "ControlPlane", }, { Name: "worker-asg", - NodeGroupName: "worker_default", + NodeGroupName: constants.WorkerDefault, GroupID: "worker-asg", AutoscalingGroupName: "worker-asg", Role: "Worker", diff --git a/operators/constellation-node-operator/internal/cloud/azure/client/BUILD.bazel b/operators/constellation-node-operator/internal/cloud/azure/client/BUILD.bazel index f59970f68..844c8b7bb 100644 --- a/operators/constellation-node-operator/internal/cloud/azure/client/BUILD.bazel +++ b/operators/constellation-node-operator/internal/cloud/azure/client/BUILD.bazel @@ -18,6 +18,7 @@ go_library( importpath = "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/internal/cloud/azure/client", visibility = ["//operators/constellation-node-operator:__subpackages__"], deps = [ + "//internal/constants", "//operators/constellation-node-operator/api/v1alpha1", "//operators/constellation-node-operator/internal/cloud/api", "//operators/constellation-node-operator/internal/poller", @@ -44,6 +45,7 @@ go_test( ], embed = [":client"], deps = [ + "//internal/constants", "//operators/constellation-node-operator/api/v1alpha1", "//operators/constellation-node-operator/internal/cloud/api", "//operators/constellation-node-operator/internal/poller", diff --git a/operators/constellation-node-operator/internal/cloud/azure/client/scalinggroup.go b/operators/constellation-node-operator/internal/cloud/azure/client/scalinggroup.go index 19ea2e1d3..5928bea48 100644 --- a/operators/constellation-node-operator/internal/cloud/azure/client/scalinggroup.go +++ b/operators/constellation-node-operator/internal/cloud/azure/client/scalinggroup.go @@ -13,6 +13,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4" + "github.com/edgelesssys/constellation/v2/internal/constants" updatev1alpha1 "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/api/v1alpha1" cspapi "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/internal/cloud/api" ) @@ -117,9 +118,9 @@ func (c *Client) ListScalingGroups(ctx context.Context, uid string) ([]cspapi.Sc if nodeGroupName == "" { switch role { case updatev1alpha1.ControlPlaneRole: - nodeGroupName = "control_plane_default" + nodeGroupName = constants.ControlPlaneDefault case updatev1alpha1.WorkerRole: - nodeGroupName = "worker_default" + nodeGroupName = constants.WorkerDefault } } diff --git a/operators/constellation-node-operator/internal/cloud/azure/client/scalinggroup_test.go b/operators/constellation-node-operator/internal/cloud/azure/client/scalinggroup_test.go index 9ff8f7dfd..cc2adb5c7 100644 --- a/operators/constellation-node-operator/internal/cloud/azure/client/scalinggroup_test.go +++ b/operators/constellation-node-operator/internal/cloud/azure/client/scalinggroup_test.go @@ -13,6 +13,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4" + "github.com/edgelesssys/constellation/v2/internal/constants" cspapi "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/internal/cloud/api" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -202,7 +203,7 @@ func TestListScalingGroups(t *testing.T) { wantGroups: []cspapi.ScalingGroup{ { Name: "constellation-scale-set-control-planes-uid", - NodeGroupName: "control_plane_default", + NodeGroupName: constants.ControlPlaneDefault, GroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/constellation-scale-set-control-planes-uid", AutoscalingGroupName: "constellation-scale-set-control-planes-uid", Role: "ControlPlane", @@ -221,7 +222,7 @@ func TestListScalingGroups(t *testing.T) { wantGroups: []cspapi.ScalingGroup{ { Name: "constellation-scale-set-workers-uid", - NodeGroupName: "worker_default", + NodeGroupName: constants.WorkerDefault, GroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/constellation-scale-set-workers-uid", AutoscalingGroupName: "constellation-scale-set-workers-uid", Role: "Worker", @@ -240,7 +241,7 @@ func TestListScalingGroups(t *testing.T) { wantGroups: []cspapi.ScalingGroup{ { Name: "some-scale-set", - NodeGroupName: "control_plane_default", + NodeGroupName: constants.ControlPlaneDefault, GroupID: "/subscriptions/subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachineScaleSets/some-scale-set", AutoscalingGroupName: "some-scale-set", Role: "ControlPlane", diff --git a/operators/constellation-node-operator/internal/cloud/gcp/client/BUILD.bazel b/operators/constellation-node-operator/internal/cloud/gcp/client/BUILD.bazel index 83711d1ba..35b2e375d 100644 --- a/operators/constellation-node-operator/internal/cloud/gcp/client/BUILD.bazel +++ b/operators/constellation-node-operator/internal/cloud/gcp/client/BUILD.bazel @@ -22,6 +22,7 @@ go_library( importpath = "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/internal/cloud/gcp/client", visibility = ["//operators/constellation-node-operator:__subpackages__"], deps = [ + "//internal/constants", "//operators/constellation-node-operator/api/v1alpha1", "//operators/constellation-node-operator/internal/cloud/api", "@com_github_googleapis_gax_go_v2//:gax-go", @@ -51,6 +52,7 @@ go_test( ], embed = [":client"], deps = [ + "//internal/constants", "//operators/constellation-node-operator/api/v1alpha1", "//operators/constellation-node-operator/internal/cloud/api", "@com_github_googleapis_gax_go_v2//:gax-go", diff --git a/operators/constellation-node-operator/internal/cloud/gcp/client/scalinggroup.go b/operators/constellation-node-operator/internal/cloud/gcp/client/scalinggroup.go index 49554722b..d69b5ed6d 100644 --- a/operators/constellation-node-operator/internal/cloud/gcp/client/scalinggroup.go +++ b/operators/constellation-node-operator/internal/cloud/gcp/client/scalinggroup.go @@ -13,6 +13,7 @@ import ( "strings" "cloud.google.com/go/compute/apiv1/computepb" + "github.com/edgelesssys/constellation/v2/internal/constants" updatev1alpha1 "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/api/v1alpha1" cspapi "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/internal/cloud/api" "google.golang.org/api/iterator" @@ -164,9 +165,9 @@ func (c *Client) ListScalingGroups(ctx context.Context, uid string) ([]cspapi.Sc if nodeGroupName == "" { switch role { case updatev1alpha1.ControlPlaneRole: - nodeGroupName = "control_plane_default" + nodeGroupName = constants.ControlPlaneDefault case updatev1alpha1.WorkerRole: - nodeGroupName = "worker_default" + nodeGroupName = constants.WorkerDefault } } diff --git a/operators/constellation-node-operator/internal/cloud/gcp/client/scalinggroup_test.go b/operators/constellation-node-operator/internal/cloud/gcp/client/scalinggroup_test.go index c02a066bd..4662d26ef 100644 --- a/operators/constellation-node-operator/internal/cloud/gcp/client/scalinggroup_test.go +++ b/operators/constellation-node-operator/internal/cloud/gcp/client/scalinggroup_test.go @@ -12,6 +12,7 @@ import ( "testing" "cloud.google.com/go/compute/apiv1/computepb" + "github.com/edgelesssys/constellation/v2/internal/constants" cspapi "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/internal/cloud/api" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -356,7 +357,7 @@ func TestListScalingGroups(t *testing.T) { wantGroups: []cspapi.ScalingGroup{ { Name: "test-control-plane-uid", - NodeGroupName: "control_plane_default", + NodeGroupName: constants.ControlPlaneDefault, GroupID: "projects/project/zones/zone/instanceGroupManagers/test-control-plane-uid", AutoscalingGroupName: "https://www.googleapis.com/compute/v1/projects/project/zones/zone/instanceGroups/test-control-plane-uid", Role: "ControlPlane", @@ -374,7 +375,7 @@ func TestListScalingGroups(t *testing.T) { wantGroups: []cspapi.ScalingGroup{ { Name: "test-worker-uid", - NodeGroupName: "worker_default", + NodeGroupName: constants.WorkerDefault, GroupID: "projects/project/zones/zone/instanceGroupManagers/test-worker-uid", AutoscalingGroupName: "https://www.googleapis.com/compute/v1/projects/project/zones/zone/instanceGroups/test-worker-uid", Role: "Worker", @@ -411,7 +412,7 @@ func TestListScalingGroups(t *testing.T) { wantGroups: []cspapi.ScalingGroup{ { Name: "some-instance-group-manager", - NodeGroupName: "control_plane_default", + NodeGroupName: constants.ControlPlaneDefault, GroupID: "projects/project/zones/zone/instanceGroupManagers/some-instance-group-manager", AutoscalingGroupName: "https://www.googleapis.com/compute/v1/projects/project/zones/zone/instanceGroups/some-instance-group-manager", Role: "ControlPlane",