diff --git a/cli/internal/cmd/init.go b/cli/internal/cmd/init.go index 75debea48..53ee5f9cf 100644 --- a/cli/internal/cmd/init.go +++ b/cli/internal/cmd/init.go @@ -127,9 +127,11 @@ func (i *initCmd) initialize(cmd *cobra.Command, newDialer func(validator atls.V return fmt.Errorf("reading cluster ID file: %w", err) } - k8sVersion, err := versions.NewValidK8sVersion(compatibility.EnsurePrefixV(conf.KubernetesVersion)) + // 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 fmt.Errorf("validating kubernetes version: %w", err) + return fmt.Errorf("invalid Kubernetes version: %s", conf.KubernetesVersion) } i.log.Debugf("Validated k8s version as %s", k8sVersion) if versions.IsPreviewK8sVersion(k8sVersion) { diff --git a/cli/internal/cmd/upgradeapply.go b/cli/internal/cmd/upgradeapply.go index a70eca176..2dc57c473 100644 --- a/cli/internal/cmd/upgradeapply.go +++ b/cli/internal/cmd/upgradeapply.go @@ -27,6 +27,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/imagefetcher" "github.com/edgelesssys/constellation/v2/internal/variant" + "github.com/edgelesssys/constellation/v2/internal/versions" "github.com/spf13/afero" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" @@ -94,6 +95,10 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command, fileHandler file.Hand return err } + if err := handleInvalidK8sPatchVersion(cmd, conf.KubernetesVersion, flags.yes); err != nil { + return err + } + var idFile clusterid.File if err := fileHandler.ReadJSON(constants.ClusterIDsFileName, &idFile); err != nil { return fmt.Errorf("reading cluster ID file: %w", err) @@ -269,6 +274,24 @@ func (u *upgradeApplyCmd) parseUpgradeVars(cmd *cobra.Command, conf *config.Conf } } +// 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) + valid := err == nil + + if !valid && !yes { + confirmed, err := askToConfirm(cmd, fmt.Sprintf("WARNING: The Kubernetes patch version %s is not supported. If you continue, Kubernetes upgrades will be skipped. Do you want to continue anyway?", version)) + if err != nil { + return fmt.Errorf("asking for confirmation: %w", err) + } + if !confirmed { + return fmt.Errorf("aborted by user") + } + } + + return nil +} + type imageFetcher interface { FetchReference(ctx context.Context, provider cloudprovider.Provider, attestationVariant variant.Variant, diff --git a/cli/internal/helm/client.go b/cli/internal/helm/client.go index ae6bf0131..9ab693ddc 100644 --- a/cli/internal/helm/client.go +++ b/cli/internal/helm/client.go @@ -247,9 +247,9 @@ func (c *Client) upgradeRelease( ) error { // We need to load all values that can be statically loaded before merging them with the cluster // values. Otherwise the templates are not rendered correctly. - k8sVersion, err := versions.NewValidK8sVersion(conf.KubernetesVersion) + k8sVersion, err := versions.NewValidK8sVersion(conf.KubernetesVersion, true) if err != nil { - return fmt.Errorf("invalid k8s version: %w", err) + return fmt.Errorf("validating k8s version: %s", conf.KubernetesVersion) } loader := NewLoader(conf.GetProvider(), k8sVersion) diff --git a/cli/internal/kubernetes/upgrade.go b/cli/internal/kubernetes/upgrade.go index 308081fa3..4457338c8 100644 --- a/cli/internal/kubernetes/upgrade.go +++ b/cli/internal/kubernetes/upgrade.go @@ -13,6 +13,7 @@ import ( "fmt" "io" "path/filepath" + "strings" "time" "github.com/edgelesssys/constellation/v2/cli/internal/helm" @@ -178,12 +179,6 @@ func (u *Upgrader) UpgradeNodeVersion(ctx context.Context, conf *config.Config) return fmt.Errorf("parsing version from image short path: %w", err) } - currentK8sVersion, err := versions.NewValidK8sVersion(conf.KubernetesVersion) - if err != nil { - return fmt.Errorf("getting Kubernetes version: %w", err) - } - versionConfig := versions.VersionConfigs[currentK8sVersion] - nodeVersion, err := u.checkClusterStatus(ctx) if err != nil { return err @@ -199,7 +194,18 @@ func (u *Upgrader) UpgradeNodeVersion(ctx context.Context, conf *config.Config) return fmt.Errorf("updating image version: %w", err) } - components, err := u.updateK8s(&nodeVersion, versionConfig.ClusterVersion, versionConfig.KubernetesComponents) + // 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) + 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) + } else { + versionConfig := versions.VersionConfigs[currentK8sVersion] + components, err = u.updateK8s(&nodeVersion, versionConfig.ClusterVersion, versionConfig.KubernetesComponents) + } + switch { case err == nil: err := u.applyComponentsCM(ctx, components) diff --git a/cli/internal/kubernetes/upgrade_test.go b/cli/internal/kubernetes/upgrade_test.go index fd0e3bf5e..af068803a 100644 --- a/cli/internal/kubernetes/upgrade_test.go +++ b/cli/internal/kubernetes/upgrade_test.go @@ -195,6 +195,27 @@ func TestUpgradeNodeVersion(t *testing.T) { return assert.ErrorAs(t, err, &target) }, }, + "outdated k8s version skips k8s upgrade": { + conf: func() *config.Config { + conf := config.Default() + conf.Image = "v1.2.2" + conf.KubernetesVersion = "v1.25.8" + return conf + }(), + currentImageVersion: "v1.2.2", + currentClusterVersion: versions.SupportedK8sVersions()[0], + stable: &stubStableClient{ + configMaps: map[string]*corev1.ConfigMap{ + constants.JoinConfigMap: newJoinConfigMap(`{"0":{"expected":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","warnOnly":false}}`), + }, + }, + wantUpdate: false, + wantErr: true, + assertCorrectError: func(t *testing.T, err error) bool { + var upgradeErr *compatibility.InvalidUpgradeError + return assert.ErrorAs(t, err, &upgradeErr) + }, + }, } for name, tc := range testCases { diff --git a/internal/config/BUILD.bazel b/internal/config/BUILD.bazel index d6d0d894c..b83c98da7 100644 --- a/internal/config/BUILD.bazel +++ b/internal/config/BUILD.bazel @@ -58,7 +58,9 @@ go_test( "//internal/config/instancetypes", "//internal/constants", "//internal/file", + "//internal/semver", "//internal/variant", + "//internal/versions", "@com_github_go_playground_locales//en", "@com_github_go_playground_universal_translator//:universal-translator", "@com_github_go_playground_validator_v10//:validator", diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 3655ac93c..ea684fcb6 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -27,6 +27,8 @@ import ( "github.com/edgelesssys/constellation/v2/internal/config/instancetypes" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" + "github.com/edgelesssys/constellation/v2/internal/semver" + "github.com/edgelesssys/constellation/v2/internal/versions" ) func TestMain(m *testing.M) { @@ -294,6 +296,19 @@ 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.Patch = ver.Patch - 1 + cnf.KubernetesVersion = ver.String() + return cnf + }(), + wantErr: true, + wantErrCount: defaultErrCount, + }, "v0 is one error": { cnf: func() *Config { cnf := Default() diff --git a/internal/config/validation.go b/internal/config/validation.go index 9c9df8a4f..6e954507c 100644 --- a/internal/config/validation.go +++ b/internal/config/validation.go @@ -390,8 +390,10 @@ func getPlaceholderEntries(m measurements.M) []uint32 { return placeholders } +// validateK8sVersion does not check the patch version. func (c *Config) validateK8sVersion(fl validator.FieldLevel) bool { - return versions.IsSupportedK8sVersion(compatibility.EnsurePrefixV(fl.Field().String())) + _, err := versions.NewValidK8sVersion(compatibility.EnsurePrefixV(fl.Field().String()), false) + return err == nil } // K8sVersionFromMajorMinor takes a semver in format MAJOR.MINOR diff --git a/internal/versions/versions.go b/internal/versions/versions.go index 461206bbc..6159bc852 100644 --- a/internal/versions/versions.go +++ b/internal/versions/versions.go @@ -40,15 +40,35 @@ func SupportedK8sVersions() []string { type ValidK8sVersion string // NewValidK8sVersion validates the given string and produces a new ValidK8sVersion object. -func NewValidK8sVersion(k8sVersion string) (ValidK8sVersion, error) { - if IsSupportedK8sVersion(k8sVersion) { +// Returns an empty string if the given version is invalid. +// strict controls whether the patch version is checked or not. +func NewValidK8sVersion(k8sVersion string, strict bool) (ValidK8sVersion, error) { + var supported bool + if strict { + supported = isSupportedK8sVersionStrict(k8sVersion) + } else { + supported = isSupportedK8sVersion(k8sVersion) + } + + if supported { return ValidK8sVersion(k8sVersion), nil } - return "", fmt.Errorf("invalid k8sVersion supplied: %s", k8sVersion) + return "", fmt.Errorf("invalid Kubernetes version: %s", k8sVersion) +} + +// IsSupportedK8sVersion checks if a given Kubernetes minor version is supported by Constellation. +// Note: the patch version is not checked! +func isSupportedK8sVersion(version string) bool { + for _, valid := range SupportedK8sVersions() { + if semver.MajorMinor(valid) == semver.MajorMinor(version) { + return true + } + } + return false } // IsSupportedK8sVersion checks if a given Kubernetes version is supported by Constellation. -func IsSupportedK8sVersion(version string) bool { +func isSupportedK8sVersionStrict(version string) bool { for _, valid := range SupportedK8sVersions() { if valid == version { return true