cli: fail fast when CLI and Constellation versions don't match (#1972)

* fail on version mismatch

* rename to validateCLIandConstellationVersionAreEqual

* fix test

* image version must only be major,minor patch equal (ignore suffix)

* add version support doc

* fix: do not check patch version equality for image and cli

* skip validate on force
This commit is contained in:
Adrian Stobbe 2023-06-27 18:24:35 +02:00 committed by GitHub
parent 90ffcd17e8
commit 1edbe962c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 140 additions and 2 deletions

View File

@ -14,11 +14,13 @@ import (
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"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"
"github.com/edgelesssys/constellation/v2/internal/semver"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)
@ -83,6 +85,11 @@ func (c *createCmd) create(cmd *cobra.Command, creator cloudCreator, fileHandler
if err != nil {
return err
}
if !flags.force {
if err := validateCLIandConstellationVersionAreEqual(constants.VersionInfo(), conf.Image, conf.MicroserviceVersion); err != nil {
return err
}
}
c.log.Debugf("Checking configuration for warnings")
var printedAWarning bool
@ -292,3 +299,34 @@ func must(err error) {
panic(err)
}
}
// validateCLIandConstellationVersionAreEqual checks if the image and microservice version are equal (down to patch level) to the CLI version.
func validateCLIandConstellationVersionAreEqual(cliVersion, imageVersion, microserviceVersion string) error {
parsedImageVersion, err := versionsapi.NewVersionFromShortPath(imageVersion, versionsapi.VersionKindImage)
if err != nil {
return fmt.Errorf("parsing image version: %w", err)
}
semImage, err := semver.New(parsedImageVersion.Version)
if err != nil {
return fmt.Errorf("parsing image semantical version: %w", err)
}
semMicro, err := semver.New(microserviceVersion)
if err != nil {
return fmt.Errorf("parsing microservice version: %w", err)
}
semCLI, err := semver.New(cliVersion)
if err != nil {
return fmt.Errorf("parsing binary version: %w", err)
}
if !semCLI.MajorMinorEqual(semImage) {
return fmt.Errorf("image version %q does not match the major and minor version of the cli version %q", semImage.String(), semCLI.String())
}
if semCLI.Compare(semMicro) != 0 {
return fmt.Errorf("cli version %q does not match microservice version %q", semCLI.String(), semMicro.String())
}
return nil
}

View File

@ -270,6 +270,73 @@ func TestCheckDirClean(t *testing.T) {
}
}
func TestValidateCLIandConstellationVersionCompatibility(t *testing.T) {
testCases := map[string]struct {
imageVersion string
microServiceVersion string
cliVersion string
wantErr bool
}{
"empty": {
imageVersion: "",
microServiceVersion: "",
cliVersion: "",
wantErr: true,
},
"invalid when image < CLI": {
imageVersion: "v2.7.1",
microServiceVersion: "v2.8.0",
cliVersion: "v2.8.0",
wantErr: true,
},
"invalid when microservice < CLI": {
imageVersion: "v2.8.0",
microServiceVersion: "v2.7.1",
cliVersion: "v2.8.0",
wantErr: true,
},
"valid release version": {
imageVersion: "v2.9.0",
microServiceVersion: "v2.9.0",
cliVersion: "2.9.0",
},
"valid pre-version": {
imageVersion: "ref/main/stream/nightly/v2.9.0-pre.0.20230626150512-0a36ce61719f",
microServiceVersion: "v2.9.0-pre.0.20230626150512-0a36ce61719f",
cliVersion: "2.9.0-pre.0.20230626150512-0a36ce61719f",
},
"image version suffix need not be equal to CLI version": {
imageVersion: "ref/main/stream/nightly/v2.9.0-pre.0.19990626150512-9z36ce61799z",
microServiceVersion: "v2.9.0-pre.0.20230626150512-0a36ce61719f",
cliVersion: "2.9.0-pre.0.20230626150512-0a36ce61719f",
},
"image version can have different patch version": {
imageVersion: "ref/main/stream/nightly/v2.9.1-pre.0.19990626150512-9z36ce61799z",
microServiceVersion: "v2.9.0-pre.0.20230626150512-0a36ce61719f",
cliVersion: "2.9.0-pre.0.20230626150512-0a36ce61719f",
},
"microService version suffix must be equal to CLI version": {
imageVersion: "ref/main/stream/nightly/v2.9.0-pre.0.20230626150512-0a36ce61719f",
microServiceVersion: "v2.9.0-pre.0.19990626150512-9z36ce61799z",
cliVersion: "2.9.0-pre.0.20230626150512-0a36ce61719f",
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
err := validateCLIandConstellationVersionAreEqual(tc.cliVersion, tc.imageVersion, tc.microServiceVersion)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}
func intPtr(i int) *int {
return &i
}

View File

@ -121,7 +121,11 @@ func (i *initCmd) initialize(cmd *cobra.Command, newDialer func(validator atls.V
if err != nil {
return err
}
if !flags.force {
if err := validateCLIandConstellationVersionAreEqual(constants.VersionInfo(), conf.Image, conf.MicroserviceVersion); err != nil {
return err
}
}
if conf.GetAttestationConfig().GetVariant().Equal(variant.AWSSEVSNP{}) {
cmd.PrintErrln("WARNING: SNP based attestation is still under active development. Please do not use in production.")
}

View File

@ -494,7 +494,7 @@ func TestAttestation(t *testing.T) {
require.NoError(fileHandler.WriteJSON(constants.ClusterIDsFileName, existingIDFile, file.OptNone))
cfg := config.Default()
cfg.Image = "image"
cfg.Image = "v0.0.0" // is the default version of the the CLI (before build injects the real version)
cfg.RemoveProviderAndAttestationExcept(cloudprovider.QEMU)
cfg.Attestation.QEMUVTPM.Measurements[0] = measurements.WithAllBytes(0x00, measurements.Enforce, measurements.PCRMeasurementLength)
cfg.Attestation.QEMUVTPM.Measurements[1] = measurements.WithAllBytes(0x11, measurements.Enforce, measurements.PCRMeasurementLength)

View File

@ -0,0 +1,24 @@
# Versions support
This explains the support and compatibility of the different versions that are part of the config.
## Microservice version
(the version that is referenced in the config during constellation init or referenced as target version during upgrade apply) must match the CLI version exactly (down to the version suffix). When planning an upgrade, the source version (current version before the upgrade is applied) may be up to one minor version older (allow upgrades from v2.8.x to v2.9.x).
The reason is this: The CLI embeds exactly one version of the helm charts (the version of the CLI). So whenever we deploy the helm charts, we can only use the embedded chart version.
The source version can only be older by one minor version to ensure we can perform safe upgrades where we can reasonably test what microservice versions will interact with other versions of CLI, OS images, other microservices.
## Kubernetes version
(the version that is referenced in the config during constellation init or referenced as target version during upgrade apply) must match one of the embedded k8s versions. We have a hardcoded list that is embedded in the CLI.
When planning an upgrade, the source k8s version may be up to one minor version older (allow upgrades from k8s 1.25.x to 1.26.x).
The reason is this: The CLI embeds components for kubernetes version for the hardcoded list of supported k8s versions. So whenever we deploy a k8s version (during constellation init or constellation upgrade apply), the k8s components for the target version need to be available.
The source version drift is a general k8s recommendation: k8s was designed to withstand a version drift of one minor version in each component during a rolling upgrade.
## Image version
(the version that is referenced in the config during constellation init or referenced as target version during upgrade apply) must have the same major and minor version (but can have a different patch or suffix) when upgrading compared to the cli. Examples: CLI v2.8.2 could correctly upgrade to OS image v2.8.1 or v2.8.2-nightly-foo-bar.
When planning an upgrade, the source image version may be up to one minor version older (allow upgrades from v2.8.x to 2.9.x).
The reason is this: The CLI uses an API endpoint to query configuration and measurements for a configured target image. This generally allows for arbitrary image versions to be referenced/deployed by the CLI. However, to ensure safe upgrades, we always upgrade by at most one minor version and ensure that the CLI is at the same minor version that we target.
This allows us to test upgrades with only a reasonable amount of moving parts.
Patch version upgrades of OS images are not allowed to have any breaking changes. They are designed to deliver bug and security fixes only and should otherwise be 1:1 compatible.

View File

@ -68,6 +68,11 @@ func (v Semver) Compare(other Semver) int {
return semver.Compare(v.String(), other.String())
}
// MajorMinorEqual returns if the major and minor version of two versions are equal.
func (v Semver) MajorMinorEqual(other Semver) bool {
return v.Major == other.Major && v.Minor == other.Minor
}
// IsUpgradeTo returns if a version is an upgrade to another version.
// It checks if the version of v is greater than the version of other and allows a drift of at most one minor version.
func (v Semver) IsUpgradeTo(other Semver) bool {