diff --git a/cli/internal/cmd/configgenerate.go b/cli/internal/cmd/configgenerate.go index 6e05a6b92..316157adb 100644 --- a/cli/internal/cmd/configgenerate.go +++ b/cli/internal/cmd/configgenerate.go @@ -13,7 +13,6 @@ import ( "github.com/edgelesssys/constellation/v2/cli/internal/cmd/pathprefix" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" - "github.com/edgelesssys/constellation/v2/internal/compatibility" "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" @@ -35,7 +34,7 @@ func newConfigGenerateCmd() *cobra.Command { ValidArgsFunction: generateCompletion, RunE: runConfigGenerate, } - cmd.Flags().StringP("kubernetes", "k", semver.MajorMinor(config.Default().KubernetesVersion), "Kubernetes version to use in format MAJOR.MINOR") + cmd.Flags().StringP("kubernetes", "k", semver.MajorMinor(string(config.Default().KubernetesVersion)), "Kubernetes version to use in format MAJOR.MINOR") cmd.Flags().StringP("attestation", "a", "", fmt.Sprintf("attestation variant to use %s. If not specified, the default for the cloud provider is used", printFormattedSlice(variant.GetAvailableAttestationVariants()))) return cmd @@ -43,7 +42,7 @@ func newConfigGenerateCmd() *cobra.Command { type generateFlags struct { pf pathprefix.PathPrefixer - k8sVersion string + k8sVersion versions.ValidK8sVersion attestationVariant variant.Variant } @@ -124,19 +123,6 @@ func createConfig(provider cloudprovider.Provider) *config.Config { return res } -// supportedVersions prints the supported version without v prefix and without patch version. -// Should only be used when accepting Kubernetes versions from --kubernetes. -func supportedVersions() string { - builder := strings.Builder{} - for i, version := range versions.SupportedK8sVersions() { - if i > 0 { - builder.WriteString(" ") - } - builder.WriteString(strings.TrimPrefix(semver.MajorMinor(version), "v")) - } - return builder.String() -} - func parseGenerateFlags(cmd *cobra.Command) (generateFlags, error) { workDir, err := cmd.Flags().GetString("workspace") if err != nil { @@ -144,11 +130,15 @@ func parseGenerateFlags(cmd *cobra.Command) (generateFlags, error) { } k8sVersion, err := cmd.Flags().GetString("kubernetes") if err != nil { - return generateFlags{}, fmt.Errorf("parsing kuberentes flag: %w", err) + return generateFlags{}, fmt.Errorf("parsing Kubernetes flag: %w", err) } - resolvedVersion, err := resolveK8sVersion(k8sVersion) + resolvedVersion, err := versions.ResolveK8sPatchVersion(k8sVersion) if err != nil { - return generateFlags{}, fmt.Errorf("resolving kuberentes version from flag: %w", err) + return generateFlags{}, fmt.Errorf("resolving kubernetes patch version from flag: %w", err) + } + validK8sVersion, err := versions.NewValidK8sVersion(resolvedVersion, true) + if err != nil { + return generateFlags{}, fmt.Errorf("resolving Kubernetes version from flag: %w", err) } attestationString, err := cmd.Flags().GetString("attestation") @@ -168,7 +158,7 @@ func parseGenerateFlags(cmd *cobra.Command) (generateFlags, error) { } return generateFlags{ pf: pathprefix.New(workDir), - k8sVersion: resolvedVersion, + k8sVersion: validK8sVersion, attestationVariant: attestationVariant, }, nil } @@ -184,22 +174,6 @@ func generateCompletion(_ *cobra.Command, args []string, _ string) ([]string, co } } -// resolveK8sVersion takes the user input from --kubernetes and transforms a MAJOR.MINOR definition into a supported -// MAJOR.MINOR.PATCH release. -func resolveK8sVersion(k8sVersion string) (string, error) { - prefixedVersion := compatibility.EnsurePrefixV(k8sVersion) - if !semver.IsValid(prefixedVersion) { - return "", fmt.Errorf("kubernetes flag does not specify a valid semantic version: %s", k8sVersion) - } - - extendedVersion := config.K8sVersionFromMajorMinor(prefixedVersion) - if extendedVersion == "" { - return "", fmt.Errorf("--kubernetes (%s) does not specify a valid Kubernetes version. Supported versions: %s", strings.TrimPrefix(k8sVersion, "v"), supportedVersions()) - } - - return extendedVersion, nil -} - func printFormattedSlice[T any](input []T) string { return fmt.Sprintf("{%s}", strings.Join(toString(input), "|")) } diff --git a/cli/internal/cmd/configgenerate_test.go b/cli/internal/cmd/configgenerate_test.go index d11cd5d80..9a07c7ad0 100644 --- a/cli/internal/cmd/configgenerate_test.go +++ b/cli/internal/cmd/configgenerate_test.go @@ -8,6 +8,7 @@ package cmd import ( "fmt" + "strings" "testing" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" @@ -29,9 +30,29 @@ func TestConfigGenerateKubernetesVersion(t *testing.T) { version string wantErr bool }{ - "success": { + "default version": { + version: "", + }, + "without v prefix": { + version: strings.TrimPrefix(string(versions.Default), "v"), + }, + "K8s version without patch version": { version: semver.MajorMinor(string(versions.Default)), }, + "K8s version with patch version": { + version: string(versions.Default), + }, + "K8s version with invalid patch version": { + version: func() string { + s := string(versions.Default) + return s[:len(s)-1] + "99" + }(), + wantErr: true, + }, + "outdated K8s version": { + version: "v1.0.0", + wantErr: true, + }, "no semver": { version: "asdf", wantErr: true, @@ -50,11 +71,13 @@ func TestConfigGenerateKubernetesVersion(t *testing.T) { fileHandler := file.NewHandler(afero.NewMemMapFs()) cmd := newConfigGenerateCmd() cmd.Flags().String("workspace", "", "") // register persistent flag manually - err := cmd.Flags().Set("kubernetes", tc.version) - require.NoError(err) + if tc.version != "" { + err := cmd.Flags().Set("kubernetes", tc.version) + require.NoError(err) + } cg := &configGenerateCmd{log: logger.NewTest(t)} - err = cg.configGenerate(cmd, fileHandler, cloudprovider.Unknown, "") + err := cg.configGenerate(cmd, fileHandler, cloudprovider.Unknown, "") if tc.wantErr { assert.Error(err) diff --git a/cli/internal/cmd/init.go b/cli/internal/cmd/init.go index 1781b13a5..8663ec0fb 100644 --- a/cli/internal/cmd/init.go +++ b/cli/internal/cmd/init.go @@ -24,7 +24,6 @@ import ( "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/atls" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" - "github.com/edgelesssys/constellation/v2/internal/compatibility" "github.com/spf13/afero" "github.com/spf13/cobra" @@ -153,6 +152,11 @@ func (i *initCmd) initialize( if err != nil { return err } + // cfg validation does not check k8s patch version since upgrade may accept an outdated patch version. + k8sVersion, err := versions.NewValidK8sVersion(string(conf.KubernetesVersion), true) + if err != nil { + return err + } if !flags.force { if err := validateCLIandConstellationVersionAreEqual(constants.BinaryVersion(), conf.Image, conf.MicroserviceVersion); err != nil { return err @@ -168,12 +172,6 @@ func (i *initCmd) initialize( return fmt.Errorf("reading cluster ID file: %w", err) } - // config validation does not check k8s patch version since upgrade may accept an outdated patch version. - // init only supported up-to-date versions. - k8sVersion, err := versions.NewValidK8sVersion(compatibility.EnsurePrefixV(conf.KubernetesVersion), true) - if err != nil { - return err - } i.log.Debugf("Validated k8s version as %s", k8sVersion) if versions.IsPreviewK8sVersion(k8sVersion) { cmd.PrintErrf("Warning: Constellation with Kubernetes %v is still in preview. Use only for evaluation purposes.\n", k8sVersion) @@ -275,7 +273,7 @@ func (i *initCmd) initialize( if err != nil { return fmt.Errorf("creating Helm client: %w", err) } - executor, includesUpgrades, err := helmApplier.PrepareApply(conf, k8sVersion, idFile, options, output, + executor, includesUpgrades, err := helmApplier.PrepareApply(conf, idFile, options, output, serviceAccURI, masterSecret) if err != nil { return fmt.Errorf("getting Helm chart executor: %w", err) @@ -629,7 +627,7 @@ type attestationConfigApplier interface { } type helmApplier interface { - PrepareApply(conf *config.Config, validK8sversion versions.ValidK8sVersion, idFile clusterid.File, flags helm.Options, tfOutput terraform.ApplyOutput, serviceAccURI string, masterSecret uri.MasterSecret) (helm.Applier, bool, error) + PrepareApply(conf *config.Config, idFile clusterid.File, flags helm.Options, tfOutput terraform.ApplyOutput, serviceAccURI string, masterSecret uri.MasterSecret) (helm.Applier, bool, error) } type clusterShower interface { diff --git a/cli/internal/cmd/init_test.go b/cli/internal/cmd/init_test.go index 13ae7eb4a..a6a75202d 100644 --- a/cli/internal/cmd/init_test.go +++ b/cli/internal/cmd/init_test.go @@ -38,6 +38,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/kms/uri" "github.com/edgelesssys/constellation/v2/internal/license" "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/edgelesssys/constellation/v2/internal/semver" "github.com/edgelesssys/constellation/v2/internal/versions" "github.com/spf13/afero" "github.com/stretchr/testify/assert" @@ -73,7 +74,6 @@ func TestInitialize(t *testing.T) { ClusterId: []byte("clusterID"), } serviceAccPath := "/test/service-account.json" - someErr := errors.New("failed") testCases := map[string]struct { provider cloudprovider.Provider @@ -105,7 +105,7 @@ func TestInitialize(t *testing.T) { "non retriable error": { provider: cloudprovider.QEMU, idFile: &clusterid.File{IP: "192.0.2.1"}, - initServerAPI: &stubInitServer{initErr: &nonRetriableError{someErr}}, + initServerAPI: &stubInitServer{initErr: &nonRetriableError{assert.AnError}}, retriable: false, masterSecretShouldExist: true, wantErr: true, @@ -125,7 +125,7 @@ func TestInitialize(t *testing.T) { "init call fails": { provider: cloudprovider.GCP, idFile: &clusterid.File{IP: "192.0.2.1"}, - initServerAPI: &stubInitServer{initErr: someErr}, + initServerAPI: &stubInitServer{initErr: assert.AnError}, retriable: true, wantErr: true, }, @@ -133,7 +133,24 @@ func TestInitialize(t *testing.T) { provider: cloudprovider.Azure, idFile: &clusterid.File{IP: "192.0.2.1"}, initServerAPI: &stubInitServer{res: &initproto.InitResponse{Kind: &initproto.InitResponse_InitSuccess{InitSuccess: testInitResp}}}, - configMutator: func(c *config.Config) { c.KubernetesVersion = strings.TrimPrefix(string(versions.Default), "v") }, + configMutator: func(c *config.Config) { + res, err := versions.NewValidK8sVersion(strings.TrimPrefix(string(versions.Default), "v"), true) + require.NoError(t, err) + c.KubernetesVersion = res + }, + }, + "outdated k8s patch version doesn't work": { + provider: cloudprovider.Azure, + idFile: &clusterid.File{IP: "192.0.2.1"}, + initServerAPI: &stubInitServer{res: &initproto.InitResponse{Kind: &initproto.InitResponse_InitSuccess{InitSuccess: testInitResp}}}, + configMutator: func(c *config.Config) { + v, err := semver.New(versions.SupportedK8sVersions()[0]) + require.NoError(t, err) + outdatedPatchVer := semver.NewFromInt(v.Major(), v.Minor(), v.Patch()-1, "").String() + c.KubernetesVersion = versions.ValidK8sVersion(outdatedPatchVer) + }, + wantErr: true, + retriable: true, // doesn't need to show retriable error message }, } @@ -222,7 +239,7 @@ type stubApplier struct { err error } -func (s stubApplier) PrepareApply(_ *config.Config, _ versions.ValidK8sVersion, _ clusterid.File, _ helm.Options, _ terraform.ApplyOutput, _ string, _ uri.MasterSecret) (helm.Applier, bool, error) { +func (s stubApplier) PrepareApply(_ *config.Config, _ clusterid.File, _ helm.Options, _ terraform.ApplyOutput, _ string, _ uri.MasterSecret) (helm.Applier, bool, error) { return stubRunner{}, false, s.err } diff --git a/cli/internal/cmd/upgradeapply.go b/cli/internal/cmd/upgradeapply.go index 7f1d371c2..d15eb1273 100644 --- a/cli/internal/cmd/upgradeapply.go +++ b/cli/internal/cmd/upgradeapply.go @@ -166,7 +166,7 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command, upgradeDir string, fl } } } - validK8sVersion, err := validK8sVersion(cmd, conf.KubernetesVersion, flags.yes) + conf.KubernetesVersion, err = validK8sVersion(cmd, string(conf.KubernetesVersion), flags.yes) if err != nil { return err } @@ -223,7 +223,7 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command, upgradeDir string, fl var upgradeErr *compatibility.InvalidUpgradeError if !flags.skipPhases.contains(skipHelmPhase) { - err = u.handleServiceUpgrade(cmd, conf, idFile, tfOutput, validK8sVersion, upgradeDir, flags) + err = u.handleServiceUpgrade(cmd, conf, idFile, tfOutput, upgradeDir, flags) switch { case errors.As(err, &upgradeErr): cmd.PrintErrln(err) @@ -405,7 +405,7 @@ func (u *upgradeApplyCmd) confirmAndUpgradeAttestationConfig( func (u *upgradeApplyCmd) handleServiceUpgrade( cmd *cobra.Command, conf *config.Config, idFile clusterid.File, tfOutput terraform.ApplyOutput, - validK8sVersion versions.ValidK8sVersion, upgradeDir string, flags upgradeApplyFlags, + upgradeDir string, flags upgradeApplyFlags, ) error { var secret uri.MasterSecret if err := u.fileHandler.ReadJSON(constants.MasterSecretFilename, &secret); err != nil { @@ -423,7 +423,7 @@ func (u *upgradeApplyCmd) handleServiceUpgrade( prepareApply := func(allowDestructive bool) (helm.Applier, bool, error) { options.AllowDestructive = allowDestructive - executor, includesUpgrades, err := u.helmApplier.PrepareApply(conf, validK8sVersion, idFile, options, + executor, includesUpgrades, err := u.helmApplier.PrepareApply(conf, idFile, options, tfOutput, serviceAccURI, secret) var upgradeErr *compatibility.InvalidUpgradeError switch { diff --git a/cli/internal/cmd/upgradeapply_test.go b/cli/internal/cmd/upgradeapply_test.go index 29ec6bf4c..6a885791d 100644 --- a/cli/internal/cmd/upgradeapply_test.go +++ b/cli/internal/cmd/upgradeapply_test.go @@ -23,6 +23,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/kms/uri" "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/edgelesssys/constellation/v2/internal/semver" "github.com/edgelesssys/constellation/v2/internal/versions" "github.com/spf13/afero" "github.com/stretchr/testify/assert" @@ -37,6 +38,7 @@ func TestUpgradeApply(t *testing.T) { kubeUpgrader *stubKubernetesUpgrader terraformUpgrader clusterUpgrader wantErr bool + customK8sVersion string flags upgradeApplyFlags stdin string }{ @@ -113,6 +115,30 @@ func TestUpgradeApply(t *testing.T) { wantErr: true, flags: upgradeApplyFlags{yes: true}, }, + "outdated K8s patch version": { + kubeUpgrader: &stubKubernetesUpgrader{ + currentConfig: config.DefaultForAzureSEVSNP(), + }, + helmUpgrader: stubApplier{}, + terraformUpgrader: &stubTerraformUpgrader{}, + customK8sVersion: func() string { + v, err := semver.New(versions.SupportedK8sVersions()[0]) + require.NoError(t, err) + return semver.NewFromInt(v.Major(), v.Minor(), v.Patch()-1, "").String() + }(), + flags: upgradeApplyFlags{yes: true}, + wantErr: false, + }, + "outdated K8s version": { + kubeUpgrader: &stubKubernetesUpgrader{ + currentConfig: config.DefaultForAzureSEVSNP(), + }, + helmUpgrader: stubApplier{}, + terraformUpgrader: &stubTerraformUpgrader{}, + customK8sVersion: "v1.20.0", + flags: upgradeApplyFlags{yes: true}, + wantErr: true, + }, "skip all upgrade phases": { kubeUpgrader: &stubKubernetesUpgrader{ currentConfig: config.DefaultForAzureSEVSNP(), @@ -147,7 +173,9 @@ func TestUpgradeApply(t *testing.T) { handler := file.NewHandler(afero.NewMemMapFs()) cfg := defaultConfigWithExpectedMeasurements(t, config.Default(), cloudprovider.Azure) - + if tc.customK8sVersion != "" { + cfg.KubernetesVersion = versions.ValidK8sVersion(tc.customK8sVersion) + } require.NoError(handler.WriteYAML(constants.ConfigFilename, cfg)) require.NoError(handler.WriteJSON(constants.ClusterIDsFilename, clusterid.File{MeasurementSalt: []byte("measurementSalt")})) require.NoError(handler.WriteJSON(constants.MasterSecretFilename, uri.MasterSecret{})) @@ -270,7 +298,7 @@ type mockApplier struct { mock.Mock } -func (m *mockApplier) PrepareApply(cfg *config.Config, k8sVersion versions.ValidK8sVersion, clusterID clusterid.File, helmOpts helm.Options, terraformOut terraform.ApplyOutput, str string, masterSecret uri.MasterSecret) (helm.Applier, bool, error) { - args := m.Called(cfg, k8sVersion, clusterID, helmOpts, terraformOut, str, masterSecret) +func (m *mockApplier) PrepareApply(cfg *config.Config, clusterID clusterid.File, helmOpts helm.Options, terraformOut terraform.ApplyOutput, str string, masterSecret uri.MasterSecret) (helm.Applier, bool, error) { + args := m.Called(cfg, clusterID, helmOpts, terraformOut, str, masterSecret) return args.Get(0).(helm.Applier), args.Bool(1), args.Error(2) } diff --git a/cli/internal/cmd/upgradecheck.go b/cli/internal/cmd/upgradecheck.go index 5eba7a66f..b81ff8a34 100644 --- a/cli/internal/cmd/upgradecheck.go +++ b/cli/internal/cmd/upgradecheck.go @@ -583,7 +583,11 @@ func (v *versionUpgrade) writeConfig(conf *config.Config, fileHandler file.Handl conf.MicroserviceVersion = v.newServices } if len(v.newKubernetes) > 0 { - conf.KubernetesVersion = v.newKubernetes[0] + var err error + conf.KubernetesVersion, err = versions.NewValidK8sVersion(v.newKubernetes[0], true) + if err != nil { + return fmt.Errorf("parsing Kubernetes version: %w", err) + } } if len(v.newImages) > 0 { imageUpgrade := sortedMapKeys(v.newImages)[0] diff --git a/cli/internal/helm/BUILD.bazel b/cli/internal/helm/BUILD.bazel index 29a19ea4b..be59cb0e9 100644 --- a/cli/internal/helm/BUILD.bazel +++ b/cli/internal/helm/BUILD.bazel @@ -473,7 +473,6 @@ go_test( "//internal/kms/uri", "//internal/logger", "//internal/semver", - "//internal/versions", "@com_github_pkg_errors//:errors", "@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//mock", diff --git a/cli/internal/helm/helm.go b/cli/internal/helm/helm.go index 5c285cc87..060253851 100644 --- a/cli/internal/helm/helm.go +++ b/cli/internal/helm/helm.go @@ -40,7 +40,6 @@ import ( "github.com/edgelesssys/constellation/v2/internal/kms/uri" "github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl" "github.com/edgelesssys/constellation/v2/internal/semver" - "github.com/edgelesssys/constellation/v2/internal/versions" ) const ( @@ -87,11 +86,10 @@ type Options struct { // PrepareApply loads the charts and returns the executor to apply them. // TODO(elchead): remove validK8sVersion by putting ValidK8sVersion into config.Config, see AB#3374. -func (h Client) PrepareApply( - conf *config.Config, validK8sversion versions.ValidK8sVersion, idFile clusterid.File, - flags Options, tfOutput terraform.ApplyOutput, serviceAccURI string, masterSecret uri.MasterSecret, +func (h Client) PrepareApply(conf *config.Config, idFile clusterid.File, flags Options, tfOutput terraform.ApplyOutput, + serviceAccURI string, masterSecret uri.MasterSecret, ) (Applier, bool, error) { - releases, err := h.loadReleases(conf, masterSecret, validK8sversion, idFile, flags, tfOutput, serviceAccURI) + releases, err := h.loadReleases(conf, masterSecret, idFile, flags, tfOutput, serviceAccURI) if err != nil { return nil, false, fmt.Errorf("loading Helm releases: %w", err) } @@ -100,11 +98,10 @@ func (h Client) PrepareApply( return &ChartApplyExecutor{actions: actions, log: h.log}, includesUpgrades, err } -func (h Client) loadReleases( - conf *config.Config, secret uri.MasterSecret, validK8sVersion versions.ValidK8sVersion, - idFile clusterid.File, flags Options, tfOutput terraform.ApplyOutput, serviceAccURI string, +func (h Client) loadReleases(conf *config.Config, secret uri.MasterSecret, idFile clusterid.File, flags Options, + tfOutput terraform.ApplyOutput, serviceAccURI string, ) ([]Release, error) { - helmLoader := newLoader(conf, idFile, validK8sVersion, h.cliVersion) + helmLoader := newLoader(conf, idFile, h.cliVersion) h.log.Debugf("Created new Helm loader") return helmLoader.loadReleases(flags.Conformance, flags.HelmWaitMode, secret, serviceAccURI, tfOutput) diff --git a/cli/internal/helm/helm_test.go b/cli/internal/helm/helm_test.go index c39551e8e..b160c35cf 100644 --- a/cli/internal/helm/helm_test.go +++ b/cli/internal/helm/helm_test.go @@ -18,7 +18,6 @@ import ( "github.com/edgelesssys/constellation/v2/internal/kms/uri" "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/semver" - "github.com/edgelesssys/constellation/v2/internal/versions" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "helm.sh/helm/v3/pkg/action" @@ -208,7 +207,7 @@ func TestHelmApply(t *testing.T) { helmListVersion(lister, "aws-load-balancer-controller", awsLbVersion) options.AllowDestructive = tc.allowDestructive - ex, includesUpgrade, err := sut.PrepareApply(cfg, versions.ValidK8sVersion("v1.27.4"), + ex, includesUpgrade, err := sut.PrepareApply(cfg, clusterid.File{UID: "testuid", MeasurementSalt: []byte("measurementSalt")}, options, fakeTerraformOutput(csp), fakeServiceAccURI(csp), uri.MasterSecret{Key: []byte("secret"), Salt: []byte("masterSalt")}) diff --git a/cli/internal/helm/loader.go b/cli/internal/helm/loader.go index acdbc6c30..8992dd670 100644 --- a/cli/internal/helm/loader.go +++ b/cli/internal/helm/loader.go @@ -77,11 +77,12 @@ type chartLoader struct { } // newLoader creates a new ChartLoader. -func newLoader(config *config.Config, idFile clusterid.File, k8sVersion versions.ValidK8sVersion, cliVersion semver.Semver) *chartLoader { +func newLoader(config *config.Config, idFile clusterid.File, cliVersion semver.Semver) *chartLoader { // TODO(malt3): Allow overriding container image registry + prefix for all images // (e.g. for air-gapped environments). var ccmImage, cnmImage string csp := config.GetProvider() + k8sVersion := config.KubernetesVersion switch csp { case cloudprovider.AWS: ccmImage = versions.VersionConfigs[k8sVersion].CloudControllerManagerImageAWS diff --git a/cli/internal/helm/loader_test.go b/cli/internal/helm/loader_test.go index 1eb385662..ba1122ea2 100644 --- a/cli/internal/helm/loader_test.go +++ b/cli/internal/helm/loader_test.go @@ -31,7 +31,6 @@ import ( "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/kms/uri" "github.com/edgelesssys/constellation/v2/internal/semver" - "github.com/edgelesssys/constellation/v2/internal/versions" ) func fakeServiceAccURI(provider cloudprovider.Provider) string { @@ -67,9 +66,8 @@ func TestLoadReleases(t *testing.T) { assert := assert.New(t) require := require.New(t) config := &config.Config{Provider: config.ProviderConfig{GCP: &config.GCPConfig{}}} - k8sVersion := versions.ValidK8sVersion("v1.27.4") chartLoader := newLoader(config, clusterid.File{UID: "testuid", MeasurementSalt: []byte("measurementSalt")}, - k8sVersion, semver.NewFromInt(2, 10, 0, "")) + semver.NewFromInt(2, 10, 0, "")) helmReleases, err := chartLoader.loadReleases( true, WaitModeAtomic, uri.MasterSecret{Key: []byte("secret"), Salt: []byte("masterSalt")}, diff --git a/cli/internal/kubecmd/kubecmd.go b/cli/internal/kubecmd/kubecmd.go index fee16e32e..0b93119b2 100644 --- a/cli/internal/kubecmd/kubecmd.go +++ b/cli/internal/kubecmd/kubecmd.go @@ -138,13 +138,21 @@ func (k *KubeCmd) UpgradeNodeVersion(ctx context.Context, conf *config.Config, f // We have to allow users to specify outdated k8s patch versions. // Therefore, this code has to skip k8s updates if a user configures an outdated (i.e. invalid) k8s version. var components *corev1.ConfigMap - currentK8sVersion, err := versions.NewValidK8sVersion(conf.KubernetesVersion, true) + _, err = versions.NewValidK8sVersion(string(conf.KubernetesVersion), true) if err != nil { - innerErr := fmt.Errorf("unsupported Kubernetes version, supported versions are %s", strings.Join(versions.SupportedK8sVersions(), ", ")) - err = compatibility.NewInvalidUpgradeError(nodeVersion.Spec.KubernetesClusterVersion, conf.KubernetesVersion, innerErr) + innerErr := fmt.Errorf("unsupported Kubernetes version, supported versions are %s", + strings.Join(versions.SupportedK8sVersions(), ", ")) + err = compatibility.NewInvalidUpgradeError(nodeVersion.Spec.KubernetesClusterVersion, + string(conf.KubernetesVersion), innerErr) } else { - versionConfig := versions.VersionConfigs[currentK8sVersion] - components, err = k.updateK8s(&nodeVersion, versionConfig.ClusterVersion, versionConfig.KubernetesComponents, force) + versionConfig, ok := versions.VersionConfigs[conf.KubernetesVersion] + if !ok { + err = compatibility.NewInvalidUpgradeError(nodeVersion.Spec.KubernetesClusterVersion, + string(conf.KubernetesVersion), fmt.Errorf("no version config matching K8s %s", conf.KubernetesVersion)) + } else { + components, err = k.prepareUpdateK8s(&nodeVersion, versionConfig.ClusterVersion, + versionConfig.KubernetesComponents, force) + } } switch { @@ -159,7 +167,6 @@ func (k *KubeCmd) UpgradeNodeVersion(ctx context.Context, conf *config.Config, f return fmt.Errorf("updating Kubernetes version: %w", err) } } - if len(upgradeErrs) == 2 { return errors.Join(upgradeErrs...) } @@ -421,7 +428,7 @@ func (k *KubeCmd) isValidImageUpgrade(nodeVersion updatev1alpha1.NodeVersion, ne return nil } -func (k *KubeCmd) updateK8s(nodeVersion *updatev1alpha1.NodeVersion, newClusterVersion string, components components.Components, force bool) (*corev1.ConfigMap, error) { +func (k *KubeCmd) prepareUpdateK8s(nodeVersion *updatev1alpha1.NodeVersion, newClusterVersion string, components components.Components, force bool) (*corev1.ConfigMap, error) { configMap, err := internalk8s.ConstructK8sComponentsCM(components, newClusterVersion) if err != nil { return nil, fmt.Errorf("constructing k8s-components ConfigMap: %w", err) diff --git a/cli/internal/kubecmd/kubecmd_test.go b/cli/internal/kubecmd/kubecmd_test.go index ec24a274b..9d1d37896 100644 --- a/cli/internal/kubecmd/kubecmd_test.go +++ b/cli/internal/kubecmd/kubecmd_test.go @@ -43,7 +43,7 @@ func TestUpgradeNodeVersion(t *testing.T) { currentImageVersion string newImageReference string badImageVersion string - currentClusterVersion string + currentClusterVersion versions.ValidK8sVersion conf *config.Config force bool getCRErr error @@ -56,11 +56,11 @@ func TestUpgradeNodeVersion(t *testing.T) { conf: func() *config.Config { conf := config.Default() conf.Image = "v1.2.3" - conf.KubernetesVersion = versions.SupportedK8sVersions()[1] + conf.KubernetesVersion = supportedValidK8sVersions()[1] return conf }(), currentImageVersion: "v1.2.2", - currentClusterVersion: versions.SupportedK8sVersions()[0], + currentClusterVersion: supportedValidK8sVersions()[0], kubectl: &stubKubectl{ configMaps: map[string]*corev1.ConfigMap{ constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`), @@ -72,11 +72,11 @@ func TestUpgradeNodeVersion(t *testing.T) { conf: func() *config.Config { conf := config.Default() conf.Image = "v1.2.2" - conf.KubernetesVersion = versions.SupportedK8sVersions()[1] + conf.KubernetesVersion = supportedValidK8sVersions()[1] return conf }(), currentImageVersion: "v1.2.2", - currentClusterVersion: versions.SupportedK8sVersions()[0], + currentClusterVersion: supportedValidK8sVersions()[0], kubectl: &stubKubectl{ configMaps: map[string]*corev1.ConfigMap{ constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`), @@ -93,11 +93,11 @@ func TestUpgradeNodeVersion(t *testing.T) { conf: func() *config.Config { conf := config.Default() conf.Image = "v1.2.3" - conf.KubernetesVersion = versions.SupportedK8sVersions()[0] + conf.KubernetesVersion = supportedValidK8sVersions()[0] return conf }(), currentImageVersion: "v1.2.2", - currentClusterVersion: versions.SupportedK8sVersions()[0], + currentClusterVersion: supportedValidK8sVersions()[0], kubectl: &stubKubectl{ configMaps: map[string]*corev1.ConfigMap{ constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`), @@ -114,11 +114,11 @@ func TestUpgradeNodeVersion(t *testing.T) { conf: func() *config.Config { conf := config.Default() conf.Image = "v1.2.2" - conf.KubernetesVersion = versions.SupportedK8sVersions()[0] + conf.KubernetesVersion = supportedValidK8sVersions()[0] return conf }(), currentImageVersion: "v1.2.2", - currentClusterVersion: versions.SupportedK8sVersions()[0], + currentClusterVersion: supportedValidK8sVersions()[0], kubectl: &stubKubectl{}, wantErr: true, assertCorrectError: func(t *testing.T, err error) bool { @@ -130,7 +130,7 @@ func TestUpgradeNodeVersion(t *testing.T) { conf: func() *config.Config { conf := config.Default() conf.Image = "v1.2.3" - conf.KubernetesVersion = versions.SupportedK8sVersions()[1] + conf.KubernetesVersion = supportedValidK8sVersions()[1] return conf }(), conditions: []metav1.Condition{{ @@ -138,7 +138,7 @@ func TestUpgradeNodeVersion(t *testing.T) { Status: metav1.ConditionTrue, }}, currentImageVersion: "v1.2.2", - currentClusterVersion: versions.SupportedK8sVersions()[0], + currentClusterVersion: supportedValidK8sVersions()[0], kubectl: &stubKubectl{}, wantErr: true, assertCorrectError: func(t *testing.T, err error) bool { @@ -149,7 +149,7 @@ func TestUpgradeNodeVersion(t *testing.T) { conf: func() *config.Config { conf := config.Default() conf.Image = "v1.2.3" - conf.KubernetesVersion = versions.SupportedK8sVersions()[1] + conf.KubernetesVersion = supportedValidK8sVersions()[1] return conf }(), conditions: []metav1.Condition{{ @@ -157,7 +157,7 @@ func TestUpgradeNodeVersion(t *testing.T) { Status: metav1.ConditionTrue, }}, currentImageVersion: "v1.2.2", - currentClusterVersion: versions.SupportedK8sVersions()[0], + currentClusterVersion: supportedValidK8sVersions()[0], kubectl: &stubKubectl{}, force: true, wantUpdate: true, @@ -166,11 +166,11 @@ func TestUpgradeNodeVersion(t *testing.T) { conf: func() *config.Config { conf := config.Default() conf.Image = "v1.2.3" - conf.KubernetesVersion = versions.SupportedK8sVersions()[1] + conf.KubernetesVersion = supportedValidK8sVersions()[1] return conf }(), currentImageVersion: "v1.2.2", - currentClusterVersion: versions.SupportedK8sVersions()[0], + currentClusterVersion: supportedValidK8sVersions()[0], kubectl: &stubKubectl{ configMaps: map[string]*corev1.ConfigMap{ constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`), @@ -186,12 +186,12 @@ func TestUpgradeNodeVersion(t *testing.T) { conf: func() *config.Config { conf := config.Default() conf.Image = "v1.4.2" - conf.KubernetesVersion = versions.SupportedK8sVersions()[1] + conf.KubernetesVersion = supportedValidK8sVersions()[1] return conf }(), newImageReference: "path/to/image:v1.4.2", currentImageVersion: "v1.2.2", - currentClusterVersion: versions.SupportedK8sVersions()[0], + currentClusterVersion: supportedValidK8sVersions()[0], kubectl: &stubKubectl{ configMaps: map[string]*corev1.ConfigMap{ constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":true}}`), @@ -208,12 +208,12 @@ func TestUpgradeNodeVersion(t *testing.T) { conf: func() *config.Config { conf := config.Default() conf.Image = "v1.4.2" - conf.KubernetesVersion = versions.SupportedK8sVersions()[1] + conf.KubernetesVersion = supportedValidK8sVersions()[1] return conf }(), newImageReference: "path/to/image:v1.4.2", currentImageVersion: "v1.2.2", - currentClusterVersion: versions.SupportedK8sVersions()[0], + currentClusterVersion: supportedValidK8sVersions()[0], kubectl: &stubKubectl{ configMaps: map[string]*corev1.ConfigMap{ constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`), @@ -226,11 +226,11 @@ func TestUpgradeNodeVersion(t *testing.T) { conf: func() *config.Config { conf := config.Default() conf.Image = "v1.2.3" - conf.KubernetesVersion = versions.SupportedK8sVersions()[1] + conf.KubernetesVersion = supportedValidK8sVersions()[1] return conf }(), currentImageVersion: "v1.2.2", - currentClusterVersion: versions.SupportedK8sVersions()[0], + currentClusterVersion: supportedValidK8sVersions()[0], badImageVersion: "v3.2.1", kubectl: &stubKubectl{ configMaps: map[string]*corev1.ConfigMap{ @@ -252,7 +252,7 @@ func TestUpgradeNodeVersion(t *testing.T) { return conf }(), currentImageVersion: "v1.2.2", - currentClusterVersion: versions.SupportedK8sVersions()[0], + currentClusterVersion: supportedValidK8sVersions()[0], kubectl: &stubKubectl{ configMaps: map[string]*corev1.ConfigMap{ constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`), @@ -269,11 +269,11 @@ func TestUpgradeNodeVersion(t *testing.T) { conf: func() *config.Config { conf := config.Default() conf.Image = "v1.2.3" - conf.KubernetesVersion = versions.SupportedK8sVersions()[1] + conf.KubernetesVersion = supportedValidK8sVersions()[1] return conf }(), currentImageVersion: "v1.2.2", - currentClusterVersion: versions.SupportedK8sVersions()[0], + currentClusterVersion: supportedValidK8sVersions()[0], kubectl: &stubKubectl{ configMaps: map[string]*corev1.ConfigMap{ constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`), @@ -298,7 +298,7 @@ func TestUpgradeNodeVersion(t *testing.T) { nodeVersion := updatev1alpha1.NodeVersion{ Spec: updatev1alpha1.NodeVersionSpec{ ImageVersion: tc.currentImageVersion, - KubernetesClusterVersion: tc.currentClusterVersion, + KubernetesClusterVersion: string(tc.currentClusterVersion), }, Status: updatev1alpha1.NodeVersionStatus{ Conditions: tc.conditions, @@ -444,7 +444,7 @@ func TestUpdateK8s(t *testing.T) { }, } - _, err := upgrader.updateK8s(&nodeVersion, tc.newClusterVersion, components.Components{}, false) + _, err := upgrader.prepareUpdateK8s(&nodeVersion, tc.newClusterVersion, components.Components{}, false) if tc.wantErr { assert.Error(err) @@ -818,3 +818,11 @@ func (f *fakeConfigMapClient) CreateConfigMap(_ context.Context, configMap *core f.configMaps[configMap.ObjectMeta.Name] = configMap return nil } + +// supportedValidK8sVersions returns a typed list of supported Kubernetes versions. +func supportedValidK8sVersions() (res []versions.ValidK8sVersion) { + for _, v := range versions.SupportedK8sVersions() { + res = append(res, versions.ValidK8sVersion(v)) + } + return +} diff --git a/e2e/internal/upgrade/upgrade_test.go b/e2e/internal/upgrade/upgrade_test.go index 1c5521767..b72d252f8 100644 --- a/e2e/internal/upgrade/upgrade_test.go +++ b/e2e/internal/upgrade/upgrade_test.go @@ -273,13 +273,11 @@ func writeUpgradeConfig(require *require.Assertions, image string, kubernetes st cfg.Image = image defaultConfig := config.Default() - var kubernetesVersion semver.Semver + var kubernetesVersion versions.ValidK8sVersion if kubernetes == "" { - kubernetesVersion, err = semver.New(defaultConfig.KubernetesVersion) - require.NoError(err) + kubernetesVersion = defaultConfig.KubernetesVersion } else { - kubernetesVersion, err = semver.New(kubernetes) - require.NoError(err) + kubernetesVersion = versions.ValidK8sVersion(kubernetes) // ignore validation because the config is only written to file } var microserviceVersion semver.Semver @@ -291,8 +289,8 @@ func writeUpgradeConfig(require *require.Assertions, image string, kubernetes st microserviceVersion = version } - log.Printf("Setting K8s version: %s\n", kubernetesVersion.String()) - cfg.KubernetesVersion = kubernetesVersion.String() + log.Printf("Setting K8s version: %s\n", kubernetesVersion) + cfg.KubernetesVersion = kubernetesVersion log.Printf("Setting microservice version: %s\n", microserviceVersion) cfg.MicroserviceVersion = microserviceVersion @@ -432,13 +430,13 @@ func testNodesEventuallyHaveVersion(t *testing.T, k *kubernetes.Clientset, targe } kubeletVersion := node.Status.NodeInfo.KubeletVersion - if kubeletVersion != targetVersions.kubernetes.String() { - log.Printf("\t%s: K8s (Kubelet) %s, want %s\n", node.Name, kubeletVersion, targetVersions.kubernetes.String()) + if kubeletVersion != string(targetVersions.kubernetes) { + log.Printf("\t%s: K8s (Kubelet) %s, want %s\n", node.Name, kubeletVersion, targetVersions.kubernetes) allUpdated = false } kubeProxyVersion := node.Status.NodeInfo.KubeProxyVersion - if kubeProxyVersion != targetVersions.kubernetes.String() { - log.Printf("\t%s: K8s (Proxy) %s, want %s\n", node.Name, kubeProxyVersion, targetVersions.kubernetes.String()) + if kubeProxyVersion != string(targetVersions.kubernetes) { + log.Printf("\t%s: K8s (Proxy) %s, want %s\n", node.Name, kubeProxyVersion, targetVersions.kubernetes) allUpdated = false } } @@ -449,7 +447,7 @@ func testNodesEventuallyHaveVersion(t *testing.T, k *kubernetes.Clientset, targe type versionContainer struct { imageRef string - kubernetes semver.Semver + kubernetes versions.ValidK8sVersion microservices semver.Semver } diff --git a/internal/config/BUILD.bazel b/internal/config/BUILD.bazel index 4385f89a4..7c94b72fa 100644 --- a/internal/config/BUILD.bazel +++ b/internal/config/BUILD.bazel @@ -70,6 +70,7 @@ go_test( "@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//require", "@in_gopkg_yaml_v3//:yaml_v3", + "@org_golang_x_mod//semver", "@org_uber_go_goleak//:goleak", ], ) diff --git a/internal/config/config.go b/internal/config/config.go index 8abbdd11a..3c9995bfb 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -69,7 +69,7 @@ type Config struct { Name string `yaml:"name" validate:"valid_name,required"` // description: | // Kubernetes version to be installed into the cluster. - KubernetesVersion string `yaml:"kubernetesVersion" validate:"required,supported_k8s_version"` + KubernetesVersion versions.ValidK8sVersion `yaml:"kubernetesVersion" validate:"required,supported_k8s_version"` // description: | // Microservice version to be installed into the cluster. Defaults to the version of the CLI. MicroserviceVersion semver.Semver `yaml:"microserviceVersion" validate:"required"` @@ -315,7 +315,7 @@ func Default() *Config { Image: defaultImage, Name: defaultName, MicroserviceVersion: constants.BinaryVersion(), - KubernetesVersion: string(versions.Default), + KubernetesVersion: versions.Default, DebugCluster: toPtr(false), Provider: ProviderConfig{ AWS: &AWSConfig{ diff --git a/internal/config/config_doc.go b/internal/config/config_doc.go index fbaa881f9..f47adb876 100644 --- a/internal/config/config_doc.go +++ b/internal/config/config_doc.go @@ -52,7 +52,7 @@ func init() { ConfigDoc.Fields[2].Description = "Name of the cluster." ConfigDoc.Fields[2].Comments[encoder.LineComment] = "Name of the cluster." ConfigDoc.Fields[3].Name = "kubernetesVersion" - ConfigDoc.Fields[3].Type = "string" + ConfigDoc.Fields[3].Type = "ValidK8sVersion" ConfigDoc.Fields[3].Note = "" ConfigDoc.Fields[3].Description = "Kubernetes version to be installed into the cluster." ConfigDoc.Fields[3].Comments[encoder.LineComment] = "Kubernetes version to be installed into the cluster." diff --git a/internal/config/config_test.go b/internal/config/config_test.go index c066b92cf..ca4e51056 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -30,6 +30,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/semver" "github.com/edgelesssys/constellation/v2/internal/versions" + gosemver "golang.org/x/mod/semver" ) func TestMain(m *testing.M) { @@ -187,6 +188,44 @@ func TestReadConfigFile(t *testing.T) { configName: constants.ConfigFilename, wantErr: true, }, + "outdated k8s patch version is allowed": { + config: func() configMap { + conf := Default() + ver, err := semver.New(versions.SupportedK8sVersions()[0]) + require.NoError(t, err) + conf.KubernetesVersion = versions.ValidK8sVersion(semver.NewFromInt(ver.Major(), ver.Minor(), ver.Patch()-1, "").String()) + m := getConfigAsMap(conf, t) + return m + }(), + wantResult: func() *Config { + conf := Default() + ver, err := semver.New(versions.SupportedK8sVersions()[0]) + require.NoError(t, err) + conf.KubernetesVersion = versions.ValidK8sVersion(semver.NewFromInt(ver.Major(), ver.Minor(), ver.Patch()-1, "").String()) + return conf + }(), + configName: constants.ConfigFilename, + }, + "outdated k8s version is not allowed": { + config: func() configMap { + conf := Default() + conf.KubernetesVersion = versions.ValidK8sVersion("v1.0.0") + m := getConfigAsMap(conf, t) + return m + }(), + wantErr: true, + configName: constants.ConfigFilename, + }, + "a k8s version without specified patch is not allowed": { + config: func() configMap { + conf := Default() + conf.KubernetesVersion = versions.ValidK8sVersion(gosemver.MajorMinor(string(versions.Default))) + m := getConfigAsMap(conf, t) + return m + }(), + wantErr: true, + configName: constants.ConfigFilename, + }, "error on entering app client id": { config: func() configMap { conf := Default() @@ -249,16 +288,6 @@ func TestFromFile(t *testing.T) { configName: "wrong-name.yaml", wantErr: true, }, - "custom config from default file": { - config: &Config{ - Version: Version4, - }, - configName: constants.ConfigFilename, - wantResult: &Config{ - Version: Version4, - NodeGroups: map[string]NodeGroup{}, - }, - }, "modify default config": { config: func() *Config { conf := Default() @@ -321,19 +350,6 @@ func TestValidate(t *testing.T) { wantErr: true, wantErrCount: defaultErrCount, }, - "outdated k8s patch version is allowed": { - cnf: func() *Config { - cnf := Default() - cnf.Image = "" - ver, err := semver.New(versions.SupportedK8sVersions()[0]) - require.NoError(t, err) - ver = semver.NewFromInt(ver.Major(), ver.Minor(), ver.Patch()-1, "") - cnf.KubernetesVersion = ver.String() - return cnf - }(), - wantErr: true, - wantErrCount: defaultErrCount, - }, "microservices violate version drift": { cnf: func() *Config { cnf := Default() diff --git a/internal/config/migration/BUILD.bazel b/internal/config/migration/BUILD.bazel index 76c70f423..a321caeba 100644 --- a/internal/config/migration/BUILD.bazel +++ b/internal/config/migration/BUILD.bazel @@ -12,5 +12,6 @@ go_library( "//internal/file", "//internal/role", "//internal/semver", + "//internal/versions", ], ) diff --git a/internal/config/migration/migration.go b/internal/config/migration/migration.go index b11b86053..8b8661033 100644 --- a/internal/config/migration/migration.go +++ b/internal/config/migration/migration.go @@ -21,6 +21,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/role" "github.com/edgelesssys/constellation/v2/internal/semver" + "github.com/edgelesssys/constellation/v2/internal/versions" ) const ( @@ -335,7 +336,7 @@ func V3ToV4(path string, fileHandler file.Handler) error { cfgV4.Version = config.Version4 cfgV4.Image = cfgV3.Image cfgV4.Name = cfgV3.Name - cfgV4.KubernetesVersion = cfgV3.KubernetesVersion + cfgV4.KubernetesVersion = versions.ValidK8sVersion(cfgV3.KubernetesVersion) cfgV4.MicroserviceVersion = cfgV3.MicroserviceVersion cfgV4.DebugCluster = cfgV3.DebugCluster diff --git a/internal/config/validation.go b/internal/config/validation.go index 3f82e81e7..54289dee0 100644 --- a/internal/config/validation.go +++ b/internal/config/validation.go @@ -637,22 +637,6 @@ func (c *Config) validateK8sVersion(fl validator.FieldLevel) bool { return err == nil } -// K8sVersionFromMajorMinor takes a semver in format MAJOR.MINOR -// and returns the version in format MAJOR.MINOR.PATCH with the -// supported patch version as PATCH. -func K8sVersionFromMajorMinor(version string) string { - switch version { - case semver.MajorMinor(string(versions.V1_26)): - return string(versions.V1_26) - case semver.MajorMinor(string(versions.V1_27)): - return string(versions.V1_27) - case semver.MajorMinor(string(versions.V1_28)): - return string(versions.V1_28) - default: - return "" - } -} - func registerImageCompatibilityError(ut ut.Translator) error { return ut.Add("image_compatibility", "{0} specifies an invalid version: {1}", true) } diff --git a/internal/versions/versions.go b/internal/versions/versions.go index 38fdb06d4..c01b35b38 100644 --- a/internal/versions/versions.go +++ b/internal/versions/versions.go @@ -40,25 +40,87 @@ func SupportedK8sVersions() []string { type ValidK8sVersion string // NewValidK8sVersion validates the given string and produces a new ValidK8sVersion object. +// It accepts a full version (e.g. 1.26.7) and validates it. // Returns an empty string if the given version is invalid. // strict controls whether the patch version is checked or not. -// If strict is false, the patch version is ignored and the returned -// ValidK8sVersion is a supported patch version for the given major.minor version. +// If strict is false, the patch version validation is skipped. func NewValidK8sVersion(k8sVersion string, strict bool) (ValidK8sVersion, error) { + prefixedVersion := compatibility.EnsurePrefixV(k8sVersion) var supported bool if strict { - supported = isSupportedK8sVersionStrict(k8sVersion) + supported = isSupportedK8sVersionStrict(prefixedVersion) } else { - supported = isSupportedK8sVersion(k8sVersion) + supported = isSupportedK8sVersion(prefixedVersion) } if !supported { - return "", fmt.Errorf("invalid Kubernetes version: %s; supported versions are %v", k8sVersion, SupportedK8sVersions()) + return "", fmt.Errorf("invalid Kubernetes version: %s; supported versions are %v", prefixedVersion, SupportedK8sVersions()) } - if !strict { - k8sVersion, _ = supportedVersionForMajorMinor(k8sVersion) + return ValidK8sVersion(prefixedVersion), nil +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (v *ValidK8sVersion) UnmarshalYAML(unmarshal func(interface{}) error) error { + var version string + if err := unmarshal(&version); err != nil { + return err + } + if !hasPatchVersion(version) { + return fmt.Errorf("Kubernetes version %s does not specify a patch, supported versions are %s", version, strings.Join(SupportedK8sVersions(), ", ")) + } + valid, err := NewValidK8sVersion(version, false) // allow any patch version to not force K8s patch upgrades + if err != nil { + return fmt.Errorf("unsupported Kubernetes version %s, supported versions are %s", version, strings.Join(SupportedK8sVersions(), ", ")) + } + *v = valid + return nil +} + +// ResolveK8sPatchVersion transforms a MAJOR.MINOR definition into a supported +// MAJOR.MINOR.PATCH release. +func ResolveK8sPatchVersion(k8sVersion string) (string, error) { + k8sVersion = compatibility.EnsurePrefixV(k8sVersion) + if !semver.IsValid(k8sVersion) { + return "", fmt.Errorf("Kubernetes version does not specify a valid semantic version: %s", k8sVersion) + } + if hasPatchVersion(k8sVersion) { + return k8sVersion, nil + } + extendedVersion := k8sVersionFromMajorMinor(k8sVersion) + if extendedVersion == "" { + return "", fmt.Errorf("Kubernetes version %s is not valid. Supported versions: %s", + strings.TrimPrefix(k8sVersion, "v"), supportedVersions()) } - return ValidK8sVersion(k8sVersion), nil + return extendedVersion, nil +} + +// k8sVersionFromMajorMinor takes a semver in format MAJOR.MINOR +// and returns the version in format MAJOR.MINOR.PATCH with the +// supported patch version as PATCH. +func k8sVersionFromMajorMinor(version string) string { + switch version { + case semver.MajorMinor(string(V1_26)): + return string(V1_26) + case semver.MajorMinor(string(V1_27)): + return string(V1_27) + case semver.MajorMinor(string(V1_28)): + return string(V1_28) + default: + return "" + } +} + +// supportedVersions prints the supported version without v prefix and without patch version. +// Should only be used when accepting Kubernetes versions from --kubernetes. +func supportedVersions() string { + builder := strings.Builder{} + for i, version := range SupportedK8sVersions() { + if i > 0 { + builder.WriteString(" ") + } + builder.WriteString(strings.TrimPrefix(semver.MajorMinor(version), "v")) + } + return builder.String() } // IsSupportedK8sVersion checks if a given Kubernetes minor version is supported by Constellation. @@ -87,14 +149,9 @@ func IsPreviewK8sVersion(_ ValidK8sVersion) bool { return false } -func supportedVersionForMajorMinor(majorMinor string) (string, bool) { - majorMinor = semver.MajorMinor(majorMinor) - for _, valid := range SupportedK8sVersions() { - if semver.MajorMinor(valid) == majorMinor { - return valid, true - } - } - return "", false +// hasPatchVersion returns if the given version has specified a patch version. +func hasPatchVersion(version string) bool { + return semver.MajorMinor(version) != version } const (