diff --git a/cli/internal/cmd/apply.go b/cli/internal/cmd/apply.go index 99cc9d0b612..5ad8c8e396b 100644 --- a/cli/internal/cmd/apply.go +++ b/cli/internal/cmd/apply.go @@ -497,11 +497,13 @@ func (a *applyCmd) validateInputs(cmd *cobra.Command, configFetcher attestationc } } - // Constellation on QEMU or OpenStack don't support upgrades - // If using one of those providers, make sure the command is only used to initialize a cluster + // Constellation does not support image upgrades on all CSPs. Not supported are: QEMU, OpenStack + // If using one of those providers, print a warning when trying to upgrade the image if !(conf.GetProvider() == cloudprovider.AWS || conf.GetProvider() == cloudprovider.Azure || conf.GetProvider() == cloudprovider.GCP) && - (!a.flags.skipPhases.contains(skipImagePhase) && a.flags.skipPhases.contains(skipInitPhase)) { - return nil, nil, fmt.Errorf("image upgrades are not supported for provider %s", conf.GetProvider()) + !a.flags.skipPhases.contains(skipImagePhase) { + cmd.PrintErrf("Image upgrades are not supported for provider %s", conf.GetProvider()) + cmd.PrintErrln("Image phase will be skipped") + a.flags.skipPhases.add(skipImagePhase) } // Check if Terraform state exists @@ -530,12 +532,12 @@ func (a *applyCmd) validateInputs(cmd *cobra.Command, configFetcher attestationc } } - // TODO: Run validation on state file depending on what phases have to be run + // TODO(AB#3503): Run validation on state file depending on what phases have to be run return conf, stateFile, nil } -// applyJoincConfig creates or updates the cluster's join config. +// applyJoinConfig creates or updates the cluster's join config. // If the config already exists, and is different from the new config, the user is asked to confirm the upgrade. func (a *applyCmd) applyJoinConfig( cmd *cobra.Command, kubeUpgrader kubernetesUpgrader, newConfig config.AttestationCfg, measurementSalt []byte, diff --git a/cli/internal/cmd/apply_test.go b/cli/internal/cmd/apply_test.go index 4d8a56f3858..63eb973e74d 100644 --- a/cli/internal/cmd/apply_test.go +++ b/cli/internal/cmd/apply_test.go @@ -7,14 +7,23 @@ SPDX-License-Identifier: AGPL-3.0-only package cmd import ( + "bytes" "context" + "errors" "fmt" + "path/filepath" "strings" "testing" "time" "github.com/edgelesssys/constellation/v2/cli/internal/helm" + "github.com/edgelesssys/constellation/v2/cli/internal/state" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" + "github.com/edgelesssys/constellation/v2/internal/cloud/gcpshared" + "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/kms/uri" "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/spf13/afero" "github.com/spf13/pflag" @@ -176,3 +185,197 @@ func TestSkipPhases(t *testing.T) { assert.False(phases.contains(skipAttestationConfigPhase, skipInitPhase)) assert.False(phases.contains(skipInitPhase, skipInfrastructurePhase)) } + +func TestValidateInputs(t *testing.T) { + defaultConfig := func(csp cloudprovider.Provider) func(require *require.Assertions, fh file.Handler) { + return func(require *require.Assertions, fh file.Handler) { + cfg := defaultConfigWithExpectedMeasurements(t, config.Default(), csp) + + if csp == cloudprovider.GCP { + require.NoError(fh.WriteJSON("saKey.json", &gcpshared.ServiceAccountKey{ + Type: "service_account", + ProjectID: "project_id", + PrivateKeyID: "key_id", + PrivateKey: "key", + ClientEmail: "client_email", + ClientID: "client_id", + AuthURI: "auth_uri", + TokenURI: "token_uri", + AuthProviderX509CertURL: "cert", + ClientX509CertURL: "client_cert", + })) + cfg.Provider.GCP.ServiceAccountKeyPath = "saKey.json" + } + + require.NoError(fh.WriteYAML(constants.ConfigFilename, cfg)) + } + } + defaultState := func(require *require.Assertions, fh file.Handler) { + require.NoError(fh.WriteYAML(constants.StateFilename, &state.State{})) + } + defaultMasterSecret := func(require *require.Assertions, fh file.Handler) { + require.NoError(fh.WriteJSON(constants.MasterSecretFilename, &uri.MasterSecret{})) + } + defaultAdminConfig := func(require *require.Assertions, fh file.Handler) { + require.NoError(fh.Write(constants.AdminConfFilename, []byte("admin config"))) + } + defaultTfState := func(require *require.Assertions, fh file.Handler) { + require.NoError(fh.Write(filepath.Join(constants.TerraformWorkingDir, "tfvars"), []byte("tf state"))) + } + + testCases := map[string]struct { + createConfig func(require *require.Assertions, fh file.Handler) + createState func(require *require.Assertions, fh file.Handler) + createMasterSecret func(require *require.Assertions, fh file.Handler) + createAdminConfig func(require *require.Assertions, fh file.Handler) + createTfState func(require *require.Assertions, fh file.Handler) + stdin string + flags applyFlags + wantPhases skipPhases + wantErr bool + }{ + "gcp: all files exist": { + createConfig: defaultConfig(cloudprovider.GCP), + createState: defaultState, + createMasterSecret: defaultMasterSecret, + createAdminConfig: defaultAdminConfig, + createTfState: defaultTfState, + flags: applyFlags{}, + wantPhases: skipPhases{ + skipInitPhase: struct{}{}, + }, + }, + "aws: all files exist": { + createConfig: defaultConfig(cloudprovider.AWS), + createState: defaultState, + createMasterSecret: defaultMasterSecret, + createAdminConfig: defaultAdminConfig, + createTfState: defaultTfState, + flags: applyFlags{}, + wantPhases: skipPhases{ + skipInitPhase: struct{}{}, + }, + }, + "azure: all files exist": { + createConfig: defaultConfig(cloudprovider.Azure), + createState: defaultState, + createMasterSecret: defaultMasterSecret, + createAdminConfig: defaultAdminConfig, + createTfState: defaultTfState, + flags: applyFlags{}, + wantPhases: skipPhases{ + skipInitPhase: struct{}{}, + }, + }, + "qemu: all files exist": { + createConfig: defaultConfig(cloudprovider.QEMU), + createState: defaultState, + createMasterSecret: defaultMasterSecret, + createAdminConfig: defaultAdminConfig, + createTfState: defaultTfState, + flags: applyFlags{}, + wantPhases: skipPhases{ + skipInitPhase: struct{}{}, + skipImagePhase: struct{}{}, // No image upgrades on QEMU + }, + }, + "no config file": { + createConfig: func(require *require.Assertions, fh file.Handler) {}, + createState: defaultState, + createMasterSecret: defaultMasterSecret, + createAdminConfig: defaultAdminConfig, + createTfState: defaultTfState, + flags: applyFlags{}, + wantErr: true, + }, + "no admin config file, but mastersecret file exists": { + createConfig: defaultConfig(cloudprovider.GCP), + createState: defaultState, + createMasterSecret: defaultMasterSecret, + createAdminConfig: func(require *require.Assertions, fh file.Handler) {}, + createTfState: defaultTfState, + flags: applyFlags{}, + wantErr: true, + }, + "no admin config file, no master secret": { + createConfig: defaultConfig(cloudprovider.GCP), + createState: defaultState, + createMasterSecret: func(require *require.Assertions, fh file.Handler) {}, + createAdminConfig: func(require *require.Assertions, fh file.Handler) {}, + createTfState: defaultTfState, + flags: applyFlags{}, + }, + "no tf state, but admin config exists": { + createConfig: defaultConfig(cloudprovider.GCP), + createState: defaultState, + createMasterSecret: defaultMasterSecret, + createAdminConfig: defaultAdminConfig, + createTfState: func(require *require.Assertions, fh file.Handler) {}, + flags: applyFlags{}, + wantErr: true, + }, + "only config file": { + createConfig: defaultConfig(cloudprovider.GCP), + createState: func(require *require.Assertions, fh file.Handler) {}, + createMasterSecret: func(require *require.Assertions, fh file.Handler) {}, + createAdminConfig: func(require *require.Assertions, fh file.Handler) {}, + createTfState: func(require *require.Assertions, fh file.Handler) {}, + flags: applyFlags{}, + }, + "skip terraform": { + createConfig: defaultConfig(cloudprovider.GCP), + createState: defaultState, + createMasterSecret: func(require *require.Assertions, fh file.Handler) {}, + createAdminConfig: func(require *require.Assertions, fh file.Handler) {}, + createTfState: func(require *require.Assertions, fh file.Handler) {}, + flags: applyFlags{ + skipPhases: skipPhases{ + skipInfrastructurePhase: struct{}{}, + }, + }, + wantPhases: skipPhases{ + skipInfrastructurePhase: struct{}{}, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + fileHandler := file.NewHandler(afero.NewMemMapFs()) + tc.createConfig(require, fileHandler) + tc.createState(require, fileHandler) + tc.createMasterSecret(require, fileHandler) + tc.createAdminConfig(require, fileHandler) + tc.createTfState(require, fileHandler) + + cmd := NewApplyCmd() + var out bytes.Buffer + cmd.SetOut(&out) + var errOut bytes.Buffer + cmd.SetErr(&errOut) + cmd.SetIn(bytes.NewBufferString(tc.stdin)) + + a := applyCmd{ + log: logger.NewTest(t), + fileHandler: fileHandler, + flags: tc.flags, + quotaChecker: &stubLicenseClient{}, + } + + _, _, err := a.validateInputs(cmd, &stubAttestationFetcher{}) + if tc.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + var cfgErr *config.ValidationError + if errors.As(err, &cfgErr) { + t.Log(cfgErr.LongMessage()) + } + assert.Equal(tc.wantPhases, a.flags.skipPhases) + }) + } +} diff --git a/cli/internal/cmd/init_test.go b/cli/internal/cmd/init_test.go index 407b1b8d771..4174130aacd 100644 --- a/cli/internal/cmd/init_test.go +++ b/cli/internal/cmd/init_test.go @@ -152,7 +152,7 @@ func TestInitialize(t *testing.T) { /* Tests currently disabled since we don't actually have validation for the state file yet These tests cases only passed in the past because of unrelated errors in the test setup - TODO(AB#3492): Re-enable tests once state file validation is implemented + TODO(AB#3503): Re-enable tests once state file validation is implemented "state file with only version": { provider: cloudprovider.GCP, @@ -174,13 +174,6 @@ func TestInitialize(t *testing.T) { wantErr: true, }, */ - "no state file": { - provider: cloudprovider.GCP, - configMutator: func(c *config.Config) { c.Provider.GCP.ServiceAccountKeyPath = serviceAccPath }, - serviceAccKey: gcpServiceAccKey, - retriable: true, - wantErr: false, // TODO: Reenable once we have validation for the state file - }, "init call fails": { provider: cloudprovider.GCP, configMutator: func(c *config.Config) { c.Provider.GCP.ServiceAccountKeyPath = serviceAccPath }, @@ -734,6 +727,17 @@ func defaultConfigWithExpectedMeasurements(t *testing.T, conf *config.Config, cs var zone, instanceType, diskType string switch csp { + case cloudprovider.AWS: + conf.Provider.AWS.Region = "test-region-2" + conf.Provider.AWS.Zone = "test-zone-2c" + conf.Provider.AWS.IAMProfileControlPlane = "test-iam-profile" + conf.Provider.AWS.IAMProfileWorkerNodes = "test-iam-profile" + conf.Attestation.AWSSEVSNP.Measurements[4] = measurements.WithAllBytes(0x44, measurements.Enforce, measurements.PCRMeasurementLength) + conf.Attestation.AWSSEVSNP.Measurements[9] = measurements.WithAllBytes(0x11, measurements.Enforce, measurements.PCRMeasurementLength) + conf.Attestation.AWSSEVSNP.Measurements[12] = measurements.WithAllBytes(0xcc, measurements.Enforce, measurements.PCRMeasurementLength) + zone = "test-zone-2c" + instanceType = "c6a.xlarge" + diskType = "gp3" case cloudprovider.Azure: conf.Provider.Azure.SubscriptionID = "01234567-0123-0123-0123-0123456789ab" conf.Provider.Azure.TenantID = "01234567-0123-0123-0123-0123456789ab"