cli: skip k8s upgrade in case of outdated version (#1864)

If an unsupported, outdated k8s patch version is used,
the user should still be able to run upgrade apply.
This commit is contained in:
Otto Bittner 2023-06-05 09:13:02 +02:00 committed by GitHub
parent eb9bea1cff
commit 6bda62d397
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 107 additions and 16 deletions

View file

@ -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) 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 { 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) i.log.Debugf("Validated k8s version as %s", k8sVersion)
if versions.IsPreviewK8sVersion(k8sVersion) { if versions.IsPreviewK8sVersion(k8sVersion) {

View file

@ -27,6 +27,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/imagefetcher" "github.com/edgelesssys/constellation/v2/internal/imagefetcher"
"github.com/edgelesssys/constellation/v2/internal/variant" "github.com/edgelesssys/constellation/v2/internal/variant"
"github.com/edgelesssys/constellation/v2/internal/versions"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -94,6 +95,10 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command, fileHandler file.Hand
return err return err
} }
if err := handleInvalidK8sPatchVersion(cmd, conf.KubernetesVersion, flags.yes); err != nil {
return err
}
var idFile clusterid.File var idFile clusterid.File
if err := fileHandler.ReadJSON(constants.ClusterIDsFileName, &idFile); err != nil { if err := fileHandler.ReadJSON(constants.ClusterIDsFileName, &idFile); err != nil {
return fmt.Errorf("reading cluster ID file: %w", err) 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 { type imageFetcher interface {
FetchReference(ctx context.Context, FetchReference(ctx context.Context,
provider cloudprovider.Provider, attestationVariant variant.Variant, provider cloudprovider.Provider, attestationVariant variant.Variant,

View file

@ -247,9 +247,9 @@ func (c *Client) upgradeRelease(
) error { ) error {
// We need to load all values that can be statically loaded before merging them with the cluster // 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. // values. Otherwise the templates are not rendered correctly.
k8sVersion, err := versions.NewValidK8sVersion(conf.KubernetesVersion) k8sVersion, err := versions.NewValidK8sVersion(conf.KubernetesVersion, true)
if err != nil { 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) loader := NewLoader(conf.GetProvider(), k8sVersion)

View file

@ -13,6 +13,7 @@ import (
"fmt" "fmt"
"io" "io"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"github.com/edgelesssys/constellation/v2/cli/internal/helm" "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) 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) nodeVersion, err := u.checkClusterStatus(ctx)
if err != nil { if err != nil {
return err return err
@ -199,7 +194,18 @@ func (u *Upgrader) UpgradeNodeVersion(ctx context.Context, conf *config.Config)
return fmt.Errorf("updating image version: %w", err) 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 { switch {
case err == nil: case err == nil:
err := u.applyComponentsCM(ctx, components) err := u.applyComponentsCM(ctx, components)

View file

@ -195,6 +195,27 @@ func TestUpgradeNodeVersion(t *testing.T) {
return assert.ErrorAs(t, err, &target) 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 { for name, tc := range testCases {

View file

@ -58,7 +58,9 @@ go_test(
"//internal/config/instancetypes", "//internal/config/instancetypes",
"//internal/constants", "//internal/constants",
"//internal/file", "//internal/file",
"//internal/semver",
"//internal/variant", "//internal/variant",
"//internal/versions",
"@com_github_go_playground_locales//en", "@com_github_go_playground_locales//en",
"@com_github_go_playground_universal_translator//:universal-translator", "@com_github_go_playground_universal_translator//:universal-translator",
"@com_github_go_playground_validator_v10//:validator", "@com_github_go_playground_validator_v10//:validator",

View file

@ -27,6 +27,8 @@ import (
"github.com/edgelesssys/constellation/v2/internal/config/instancetypes" "github.com/edgelesssys/constellation/v2/internal/config/instancetypes"
"github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file" "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) { func TestMain(m *testing.M) {
@ -294,6 +296,19 @@ func TestValidate(t *testing.T) {
wantErr: true, wantErr: true,
wantErrCount: defaultErrCount, 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": { "v0 is one error": {
cnf: func() *Config { cnf: func() *Config {
cnf := Default() cnf := Default()

View file

@ -390,8 +390,10 @@ func getPlaceholderEntries(m measurements.M) []uint32 {
return placeholders return placeholders
} }
// validateK8sVersion does not check the patch version.
func (c *Config) validateK8sVersion(fl validator.FieldLevel) bool { 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 // K8sVersionFromMajorMinor takes a semver in format MAJOR.MINOR

View file

@ -40,15 +40,35 @@ func SupportedK8sVersions() []string {
type ValidK8sVersion string type ValidK8sVersion string
// NewValidK8sVersion validates the given string and produces a new ValidK8sVersion object. // NewValidK8sVersion validates the given string and produces a new ValidK8sVersion object.
func NewValidK8sVersion(k8sVersion string) (ValidK8sVersion, error) { // Returns an empty string if the given version is invalid.
if IsSupportedK8sVersion(k8sVersion) { // 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 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. // 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() { for _, valid := range SupportedK8sVersions() {
if valid == version { if valid == version {
return true return true