From f4d7938547b0cde37ebd68b8747df1cdb76b0f8a Mon Sep 17 00:00:00 2001 From: Adrian Stobbe Date: Thu, 7 Sep 2023 15:55:19 +0200 Subject: [PATCH] write infrastructure state during create --- cli/internal/cloudcmd/BUILD.bazel | 2 +- cli/internal/cloudcmd/create.go | 46 ++++++++++++++++++---------- cli/internal/cloudcmd/create_test.go | 3 +- cli/internal/cmd/BUILD.bazel | 2 ++ cli/internal/cmd/cloud.go | 4 +-- cli/internal/cmd/cloud_test.go | 11 +++---- cli/internal/cmd/create.go | 23 +++++++++++++- cli/internal/cmd/create_test.go | 19 +++++++++--- cli/internal/state/BUILD.bazel | 8 +++++ cli/internal/state/state.go | 41 +++++++++++++++++++++++++ internal/constants/constants.go | 2 ++ 11 files changed, 129 insertions(+), 32 deletions(-) create mode 100644 cli/internal/state/BUILD.bazel create mode 100644 cli/internal/state/state.go diff --git a/cli/internal/cloudcmd/BUILD.bazel b/cli/internal/cloudcmd/BUILD.bazel index 115e37fd5d..7649a25a7d 100644 --- a/cli/internal/cloudcmd/BUILD.bazel +++ b/cli/internal/cloudcmd/BUILD.bazel @@ -21,9 +21,9 @@ go_library( importpath = "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd", visibility = ["//cli:__subpackages__"], deps = [ - "//cli/internal/clusterid", "//cli/internal/cmd/pathprefix", "//cli/internal/libvirt", + "//cli/internal/state", "//cli/internal/terraform", "//internal/atls", "//internal/attestation/choose", diff --git a/cli/internal/cloudcmd/create.go b/cli/internal/cloudcmd/create.go index 1f323ba2e7..17b7aa00f0 100644 --- a/cli/internal/cloudcmd/create.go +++ b/cli/internal/cloudcmd/create.go @@ -18,8 +18,8 @@ import ( "runtime" "strings" - "github.com/edgelesssys/constellation/v2/cli/internal/clusterid" "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" @@ -64,19 +64,19 @@ type CreateOptions struct { } // Create creates the handed amount of instances and all the needed resources. -func (c *Creator) Create(ctx context.Context, opts CreateOptions) (clusterid.File, error) { +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 clusterid.File{}, fmt.Errorf("fetching image reference: %w", err) + return state.Infrastructure{}, fmt.Errorf("fetching image reference: %w", err) } opts.image = image cl, err := c.newTerraformClient(ctx, opts.TFWorkspace) if err != nil { - return clusterid.File{}, err + return state.Infrastructure{}, err } defer cl.RemoveInstaller() @@ -96,7 +96,7 @@ func (c *Creator) Create(ctx context.Context, opts CreateOptions) (clusterid.Fil 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) + 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{ @@ -106,23 +106,37 @@ func (c *Creator) Create(ctx context.Context, opts CreateOptions) (clusterid.Fil tfOutput, err = c.createQEMU(ctx, cl, lv, qemuOpts) default: - return clusterid.File{}, fmt.Errorf("unsupported cloud provider: %s", opts.Provider) + return state.Infrastructure{}, fmt.Errorf("unsupported cloud provider: %s", opts.Provider) } if err != nil { - return clusterid.File{}, fmt.Errorf("creating cluster: %w", err) + return state.Infrastructure{}, fmt.Errorf("creating cluster: %w", err) } - res := clusterid.File{ - CloudProvider: opts.Provider, - IP: tfOutput.IP, - APIServerCertSANs: tfOutput.APIServerCertSANs, - InitSecret: []byte(tfOutput.Secret), - UID: tfOutput.UID, + return convertToInfrastructure(tfOutput), nil +} + +func convertToInfrastructure(applyOutput terraform.ApplyOutput) state.Infrastructure { + var infra state.Infrastructure + infra.UID = applyOutput.UID + infra.PublicIP = applyOutput.IP + infra.InitSecret = applyOutput.Secret + infra.APIServerCertSANs = applyOutput.APIServerCertSANs + + if applyOutput.Azure != nil { + infra.Azure.ResourceGroup = applyOutput.Azure.ResourceGroup + infra.Azure.SubscriptionID = applyOutput.Azure.SubscriptionID + infra.Azure.NetworkSecurityGroupName = applyOutput.Azure.NetworkSecurityGroupName + infra.Azure.LoadBalancerName = applyOutput.Azure.LoadBalancerName + infra.Azure.UserAssignedIdentity = applyOutput.Azure.UserAssignedIdentity + infra.Azure.AttestationURL = applyOutput.Azure.AttestationURL } - if tfOutput.Azure != nil { - res.AttestationURL = tfOutput.Azure.AttestationURL + + if applyOutput.GCP != nil { + infra.GCP.ProjectID = applyOutput.GCP.ProjectID + infra.GCP.IPCidrNode = applyOutput.GCP.IPCidrNode + infra.GCP.IPCidrPod = applyOutput.GCP.IPCidrPod } - return res, nil + return infra } func (c *Creator) createAWS(ctx context.Context, cl tfResourceClient, opts CreateOptions) (tfOutput terraform.ApplyOutput, retErr error) { diff --git a/cli/internal/cloudcmd/create_test.go b/cli/internal/cloudcmd/create_test.go index 327f3b438d..ded75d5f19 100644 --- a/cli/internal/cloudcmd/create_test.go +++ b/cli/internal/cloudcmd/create_test.go @@ -238,8 +238,7 @@ func TestCreator(t *testing.T) { } } else { assert.NoError(err) - assert.Equal(tc.provider, idFile.CloudProvider) - assert.Equal(ip, idFile.IP) + assert.Equal(ip, idFile.PublicIP) } }) } diff --git a/cli/internal/cmd/BUILD.bazel b/cli/internal/cmd/BUILD.bazel index 2428cc7bc2..c0379cf00c 100644 --- a/cli/internal/cmd/BUILD.bazel +++ b/cli/internal/cmd/BUILD.bazel @@ -46,6 +46,7 @@ go_library( "//cli/internal/helm", "//cli/internal/kubecmd", "//cli/internal/libvirt", + "//cli/internal/state", "//cli/internal/terraform", "//disk-mapper/recoverproto", "//internal/api/attestationconfigapi", @@ -136,6 +137,7 @@ go_test( "//cli/internal/cmd/pathprefix", "//cli/internal/helm", "//cli/internal/kubecmd", + "//cli/internal/state", "//cli/internal/terraform", "//disk-mapper/recoverproto", "//internal/api/attestationconfigapi", diff --git a/cli/internal/cmd/cloud.go b/cli/internal/cmd/cloud.go index f4c9439225..688b948474 100644 --- a/cli/internal/cmd/cloud.go +++ b/cli/internal/cmd/cloud.go @@ -10,7 +10,7 @@ import ( "context" "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" - "github.com/edgelesssys/constellation/v2/cli/internal/clusterid" + "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/cloud/gcpshared" @@ -20,7 +20,7 @@ type cloudCreator interface { Create( ctx context.Context, opts cloudcmd.CreateOptions, - ) (clusterid.File, error) + ) (state.Infrastructure, error) } type cloudIAMCreator interface { diff --git a/cli/internal/cmd/cloud_test.go b/cli/internal/cmd/cloud_test.go index dc1bfe2ff6..ece543f38e 100644 --- a/cli/internal/cmd/cloud_test.go +++ b/cli/internal/cmd/cloud_test.go @@ -11,7 +11,7 @@ import ( "testing" "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" - "github.com/edgelesssys/constellation/v2/cli/internal/clusterid" + "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/cloud/gcpshared" @@ -27,17 +27,16 @@ func TestMain(m *testing.M) { type stubCloudCreator struct { createCalled bool - id clusterid.File + state state.Infrastructure createErr error } func (c *stubCloudCreator) Create( _ context.Context, - opts cloudcmd.CreateOptions, -) (clusterid.File, error) { + _ cloudcmd.CreateOptions, +) (state.Infrastructure, error) { c.createCalled = true - c.id.CloudProvider = opts.Provider - return c.id, c.createErr + return c.state, c.createErr } type stubCloudTerminator struct { diff --git a/cli/internal/cmd/create.go b/cli/internal/cmd/create.go index 4962d34394..00e38be070 100644 --- a/cli/internal/cmd/create.go +++ b/cli/internal/cmd/create.go @@ -12,11 +12,14 @@ import ( "io/fs" "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" + "github.com/edgelesssys/constellation/v2/cli/internal/clusterid" "github.com/edgelesssys/constellation/v2/cli/internal/cmd/pathprefix" + "github.com/edgelesssys/constellation/v2/cli/internal/state" "github.com/edgelesssys/constellation/v2/cli/internal/terraform" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/api/versionsapi" "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/file" @@ -167,21 +170,39 @@ func (c *createCmd) create(cmd *cobra.Command, creator cloudCreator, fileHandler TFLogLevel: flags.tfLogLevel, TFWorkspace: constants.TerraformWorkingDir, } - idFile, err := creator.Create(cmd.Context(), opts) + infraState, err := creator.Create(cmd.Context(), opts) spinner.Stop() if err != nil { return translateCreateErrors(cmd, c.pf, err) } c.log.Debugf("Successfully created the cloud resources for the cluster") + idFile := convertToIDFile(infraState, provider) if err := fileHandler.WriteJSON(constants.ClusterIDsFilename, idFile, file.OptNone); err != nil { return err } + state := state.State{ + Version: "v1", + Infrastructure: infraState, + } + if err := fileHandler.WriteYAML(constants.StateFilename, state, file.OptNone); err != nil { + return err + } cmd.Println("Your Constellation cluster was created successfully.") return nil } +func convertToIDFile(infra state.Infrastructure, provider cloudprovider.Provider) clusterid.File { + var file clusterid.File + file.CloudProvider = provider + file.IP = infra.PublicIP + file.APIServerCertSANs = infra.APIServerCertSANs + file.InitSecret = []byte(infra.InitSecret) // Convert string to []byte + file.UID = infra.UID + return file +} + // parseCreateFlags parses the flags of the create command. func (c *createCmd) parseCreateFlags(cmd *cobra.Command) (createFlags, error) { yes, err := cmd.Flags().GetBool("yes") diff --git a/cli/internal/cmd/create_test.go b/cli/internal/cmd/create_test.go index 128bd61f08..1bcb6e68cb 100644 --- a/cli/internal/cmd/create_test.go +++ b/cli/internal/cmd/create_test.go @@ -13,6 +13,7 @@ import ( "testing" "github.com/edgelesssys/constellation/v2/cli/internal/clusterid" + "github.com/edgelesssys/constellation/v2/cli/internal/state" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/constants" @@ -31,7 +32,7 @@ func TestCreate(t *testing.T) { require.NoError(file.WriteYAML(constants.ConfigFilename, defaultConfigWithExpectedMeasurements(t, config.Default(), provider))) return fs } - idFile := clusterid.File{IP: "192.0.2.1"} + idFile := state.Infrastructure{PublicIP: "192.0.2.1"} someErr := errors.New("failed") testCases := map[string]struct { @@ -47,7 +48,7 @@ func TestCreate(t *testing.T) { }{ "create": { setupFs: fsWithDefaultConfig, - creator: &stubCloudCreator{id: idFile}, + creator: &stubCloudCreator{state: idFile}, provider: cloudprovider.GCP, controllerCountFlag: intPtr(1), workerCountFlag: intPtr(2), @@ -55,7 +56,7 @@ func TestCreate(t *testing.T) { }, "interactive": { setupFs: fsWithDefaultConfig, - creator: &stubCloudCreator{id: idFile}, + creator: &stubCloudCreator{state: idFile}, provider: cloudprovider.Azure, controllerCountFlag: intPtr(2), workerCountFlag: intPtr(1), @@ -211,9 +212,19 @@ func TestCreate(t *testing.T) { var gotIDFile clusterid.File require.NoError(fileHandler.ReadJSON(constants.ClusterIDsFilename, &gotIDFile)) assert.Equal(gotIDFile, clusterid.File{ - IP: idFile.IP, + IP: idFile.PublicIP, CloudProvider: tc.provider, }) + + var gotState state.State + expectedState := state.Infrastructure{ + PublicIP: "192.0.2.1", + APIServerCertSANs: []string{}, + } + require.NoError(fileHandler.ReadYAML(constants.StateFilename, &gotState)) + assert.Equal("v1", gotState.Version) + assert.Equal(expectedState, gotState.Infrastructure) + } } }) diff --git a/cli/internal/state/BUILD.bazel b/cli/internal/state/BUILD.bazel new file mode 100644 index 0000000000..c2cc74e8a8 --- /dev/null +++ b/cli/internal/state/BUILD.bazel @@ -0,0 +1,8 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "state", + srcs = ["state.go"], + importpath = "github.com/edgelesssys/constellation/v2/cli/internal/state", + visibility = ["//cli:__subpackages__"], +) diff --git a/cli/internal/state/state.go b/cli/internal/state/state.go new file mode 100644 index 0000000000..8ba9230bdb --- /dev/null +++ b/cli/internal/state/state.go @@ -0,0 +1,41 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +// package state defines the structure of the Constellation state file. +package state + +// State describe the entire state to describe a Constellation cluster. +type State struct { + Version string `yaml:"version"` + Infrastructure `yaml:"infrastructure"` +} + +// Infrastructure describe the state related to the cloud resources of the cluster. +type Infrastructure struct { + UID string `yaml:"uid"` + PublicIP string `yaml:"publicIP"` + InitSecret string `yaml:"initSecret"` + APIServerCertSANs []string `yaml:"apiServerCertSANs"` + Azure AzureState `yaml:"azure"` + GCP GCPState `yaml:"gcp"` +} + +// GCPState describes the infra state related to GCP. +type GCPState struct { + ProjectID string `yaml:"projectID"` + IPCidrNode string `yaml:"ipCidrNode"` + IPCidrPod string `yaml:"ipCidrPod"` +} + +// AzureState describes the infra state related to Azure. +type AzureState struct { + ResourceGroup string `yaml:"resourceGroup"` + SubscriptionID string `yaml:"subscriptionID"` + NetworkSecurityGroupName string `yaml:"networkSecurityGroupName"` + LoadBalancerName string `yaml:"loadBalancerName"` + UserAssignedIdentity string `yaml:"userAssignedIdentity"` + AttestationURL string `yaml:"attestationURL"` +} diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 57a35cdf43..565584cf78 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -76,6 +76,8 @@ const ( // ClusterIDsFilename filename that contains Constellation clusterID and IP. ClusterIDsFilename = "constellation-id.json" + // StateFilename filename that contains the entire state of the Constellation cluster. + StateFilename = "constellation-state.yaml" // ConfigFilename filename of Constellation config file. ConfigFilename = "constellation-conf.yaml" // LicenseFilename filename of Constellation license file.