From 1d5a8283e04d485d4099bab112537cb3422196ee Mon Sep 17 00:00:00 2001 From: Otto Bittner Date: Tue, 25 Jul 2023 14:20:25 +0200 Subject: [PATCH] cli: use Semver type to represent microservice versions (#2125) Previously we used strings to pass microservice versions. This invited bugs due to missing input validation. --- bootstrapper/cmd/bootstrapper/run.go | 2 +- cli/internal/cmd/BUILD.bazel | 1 + cli/internal/cmd/create.go | 22 +-- cli/internal/cmd/create_test.go | 37 ++--- cli/internal/cmd/init.go | 2 +- cli/internal/cmd/init_test.go | 2 +- cli/internal/cmd/status_test.go | 5 +- cli/internal/cmd/upgradecheck.go | 86 ++++++----- cli/internal/cmd/upgradecheck_test.go | 59 +++---- cli/internal/cmd/version.go | 2 +- cli/internal/cmd/version_test.go | 2 +- cli/internal/helm/BUILD.bazel | 1 + cli/internal/helm/client.go | 71 ++++----- cli/internal/helm/client_test.go | 5 +- cli/internal/helm/loader.go | 16 +- disk-mapper/cmd/main.go | 2 +- e2e/internal/upgrade/BUILD.bazel | 1 + e2e/internal/upgrade/helm.go | 17 ++- e2e/internal/upgrade/upgrade_test.go | 14 +- internal/config/BUILD.bazel | 1 + internal/config/config.go | 24 ++- internal/config/config_doc.go | 2 +- internal/config/config_test.go | 20 ++- internal/config/migration/BUILD.bazel | 1 + internal/config/migration/migration.go | 12 +- internal/config/validation.go | 90 ++++++----- internal/config/validation_test.go | 47 +++++- internal/constants/BUILD.bazel | 1 + internal/constants/constants.go | 15 +- internal/semver/BUILD.bazel | 3 +- internal/semver/semver.go | 158 +++++++++++++++---- internal/semver/semver_test.go | 203 ++++++++++++++++++------- joinservice/cmd/main.go | 2 +- keyservice/cmd/main.go | 2 +- verify/cmd/main.go | 2 +- 35 files changed, 612 insertions(+), 318 deletions(-) diff --git a/bootstrapper/cmd/bootstrapper/run.go b/bootstrapper/cmd/bootstrapper/run.go index 7916e3436..d45ea9828 100644 --- a/bootstrapper/cmd/bootstrapper/run.go +++ b/bootstrapper/cmd/bootstrapper/run.go @@ -33,7 +33,7 @@ func run(issuer atls.Issuer, openDevice vtpm.TPMOpenFunc, fileHandler file.Handl ) { defer cloudLogger.Close() - log.With(zap.String("version", constants.VersionInfo())).Infof("Starting bootstrapper") + log.With(zap.String("version", constants.BinaryVersion().String())).Infof("Starting bootstrapper") cloudLogger.Disclose("bootstrapper started running...") uuid, err := getDiskUUID() diff --git a/cli/internal/cmd/BUILD.bazel b/cli/internal/cmd/BUILD.bazel index 651365bbc..bede0f3fd 100644 --- a/cli/internal/cmd/BUILD.bazel +++ b/cli/internal/cmd/BUILD.bazel @@ -157,6 +157,7 @@ go_test( "//internal/kms/uri", "//internal/license", "//internal/logger", + "//internal/semver", "//internal/versions", "//operators/constellation-node-operator/api/v1alpha1", "//verify/verifyproto", diff --git a/cli/internal/cmd/create.go b/cli/internal/cmd/create.go index 1bcf610cb..92dec0b94 100644 --- a/cli/internal/cmd/create.go +++ b/cli/internal/cmd/create.go @@ -86,7 +86,7 @@ func (c *createCmd) create(cmd *cobra.Command, creator cloudCreator, fileHandler return err } if !flags.force { - if err := validateCLIandConstellationVersionAreEqual(constants.VersionInfo(), conf.Image, conf.MicroserviceVersion); err != nil { + if err := validateCLIandConstellationVersionAreEqual(constants.BinaryVersion(), conf.Image, conf.MicroserviceVersion); err != nil { return err } } @@ -301,7 +301,7 @@ func must(err error) { } // 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 { +func validateCLIandConstellationVersionAreEqual(cliVersion semver.Semver, imageVersion string, microserviceVersion semver.Semver) error { parsedImageVersion, err := versionsapi.NewVersionFromShortPath(imageVersion, versionsapi.VersionKindImage) if err != nil { return fmt.Errorf("parsing image version: %w", err) @@ -312,21 +312,11 @@ func validateCLIandConstellationVersionAreEqual(cliVersion, imageVersion, micros 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) + if !cliVersion.MajorMinorEqual(semImage) { + return fmt.Errorf("image version %q does not match the major and minor version of the cli version %q", semImage.String(), cliVersion.String()) } - - 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()) + if cliVersion.Compare(microserviceVersion) != 0 { + return fmt.Errorf("cli version %q does not match microservice version %q", cliVersion.String(), microserviceVersion.String()) } return nil } diff --git a/cli/internal/cmd/create_test.go b/cli/internal/cmd/create_test.go index 984679fe7..bbdaae424 100644 --- a/cli/internal/cmd/create_test.go +++ b/cli/internal/cmd/create_test.go @@ -18,6 +18,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/logger" + consemver "github.com/edgelesssys/constellation/v2/internal/semver" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -273,52 +274,52 @@ func TestCheckDirClean(t *testing.T) { func TestValidateCLIandConstellationVersionCompatibility(t *testing.T) { testCases := map[string]struct { imageVersion string - microServiceVersion string - cliVersion string + microServiceVersion consemver.Semver + cliVersion consemver.Semver wantErr bool }{ "empty": { imageVersion: "", - microServiceVersion: "", - cliVersion: "", + microServiceVersion: consemver.Semver{}, + cliVersion: consemver.Semver{}, wantErr: true, }, "invalid when image < CLI": { imageVersion: "v2.7.1", - microServiceVersion: "v2.8.0", - cliVersion: "v2.8.0", + microServiceVersion: consemver.NewFromInt(2, 8, 0, ""), + cliVersion: consemver.NewFromInt(2, 8, 0, ""), wantErr: true, }, "invalid when microservice < CLI": { imageVersion: "v2.8.0", - microServiceVersion: "v2.7.1", - cliVersion: "v2.8.0", + microServiceVersion: consemver.NewFromInt(2, 7, 1, ""), + cliVersion: consemver.NewFromInt(2, 8, 0, ""), wantErr: true, }, "valid release version": { imageVersion: "v2.9.0", - microServiceVersion: "v2.9.0", - cliVersion: "2.9.0", + microServiceVersion: consemver.NewFromInt(2, 9, 0, ""), + cliVersion: consemver.NewFromInt(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", + microServiceVersion: consemver.NewFromInt(2, 9, 0, "pre.0.20230626150512-0a36ce61719f"), + cliVersion: consemver.NewFromInt(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", + microServiceVersion: consemver.NewFromInt(2, 9, 0, "pre.0.20230626150512-0a36ce61719f"), + cliVersion: consemver.NewFromInt(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", + microServiceVersion: consemver.NewFromInt(2, 9, 0, "pre.0.20230626150512-0a36ce61719f"), + cliVersion: consemver.NewFromInt(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", + microServiceVersion: consemver.NewFromInt(2, 9, 0, "pre.0.19990626150512-9z36ce61799z"), + cliVersion: consemver.NewFromInt(2, 9, 0, "pre.0.20230626150512-0a36ce61719f"), wantErr: true, }, } diff --git a/cli/internal/cmd/init.go b/cli/internal/cmd/init.go index 8b57222db..b5cf3af63 100644 --- a/cli/internal/cmd/init.go +++ b/cli/internal/cmd/init.go @@ -124,7 +124,7 @@ func (i *initCmd) initialize(cmd *cobra.Command, newDialer func(validator atls.V return err } if !flags.force { - if err := validateCLIandConstellationVersionAreEqual(constants.VersionInfo(), conf.Image, conf.MicroserviceVersion); err != nil { + if err := validateCLIandConstellationVersionAreEqual(constants.BinaryVersion(), conf.Image, conf.MicroserviceVersion); err != nil { return err } } diff --git a/cli/internal/cmd/init_test.go b/cli/internal/cmd/init_test.go index 07d20f949..ab3869282 100644 --- a/cli/internal/cmd/init_test.go +++ b/cli/internal/cmd/init_test.go @@ -592,7 +592,7 @@ func (m *stubMerger) kubeconfigEnvVar() string { func defaultConfigWithExpectedMeasurements(t *testing.T, conf *config.Config, csp cloudprovider.Provider) *config.Config { t.Helper() - conf.Image = "v" + constants.VersionInfo() + conf.Image = constants.BinaryVersion().String() conf.Name = "kubernetes" switch csp { diff --git a/cli/internal/cmd/status_test.go b/cli/internal/cmd/status_test.go index 2ebd46c3f..b56016f56 100644 --- a/cli/internal/cmd/status_test.go +++ b/cli/internal/cmd/status_test.go @@ -12,6 +12,7 @@ import ( "github.com/edgelesssys/constellation/v2/cli/internal/helm" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + consemver "github.com/edgelesssys/constellation/v2/internal/semver" updatev1alpha1 "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/api/v1alpha1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -117,7 +118,7 @@ func TestStatus(t *testing.T) { }, }, helmClient: stubHelmClient{ - serviceVersions: helm.NewServiceVersions("v1.0.0", "v1.0.0", "v1.1.0", "v1.1.0"), + serviceVersions: helm.NewServiceVersions(consemver.NewFromInt(1, 0, 0, ""), consemver.NewFromInt(1, 0, 0, ""), consemver.NewFromInt(1, 1, 0, ""), consemver.NewFromInt(1, 1, 0, "")), }, nodeVersion: updatev1alpha1.NodeVersion{ Spec: updatev1alpha1.NodeVersionSpec{ @@ -167,7 +168,7 @@ func TestStatus(t *testing.T) { }, }, helmClient: stubHelmClient{ - serviceVersions: helm.NewServiceVersions("v1.0.0", "v1.0.0", "v1.1.0", "v1.1.0"), + serviceVersions: helm.NewServiceVersions(consemver.NewFromInt(1, 0, 0, ""), consemver.NewFromInt(1, 0, 0, ""), consemver.NewFromInt(1, 1, 0, ""), consemver.NewFromInt(1, 1, 0, "")), }, nodeVersion: updatev1alpha1.NodeVersion{ Spec: updatev1alpha1.NodeVersionSpec{ diff --git a/cli/internal/cmd/upgradecheck.go b/cli/internal/cmd/upgradecheck.go index 52d8e3ade..4cf7b6332 100644 --- a/cli/internal/cmd/upgradecheck.go +++ b/cli/internal/cmd/upgradecheck.go @@ -33,7 +33,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/imagefetcher" "github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl" - conSemver "github.com/edgelesssys/constellation/v2/internal/semver" + consemver "github.com/edgelesssys/constellation/v2/internal/semver" "github.com/edgelesssys/constellation/v2/internal/sigstore" "github.com/edgelesssys/constellation/v2/internal/versions" "github.com/siderolabs/talos/pkg/machinery/config/encoder" @@ -93,7 +93,7 @@ func runUpgradeCheck(cmd *cobra.Command, _ []string) error { cosign: sigstore.CosignVerifier{}, rekor: rekor, flags: flags, - cliVersion: compatibility.EnsurePrefixV(constants.VersionInfo()), + cliVersion: constants.BinaryVersion(), log: log, versionsapi: versionfetcher, }, @@ -202,9 +202,9 @@ func (u *upgradeCheckCmd) upgradeCheck(cmd *cobra.Command, fileHandler file.Hand // Filter versions to only include upgrades newServices := supported.service - if err := compatibility.IsValidUpgrade(current.service, supported.service); err != nil { - newServices = "" - u.log.Debugf("No valid service upgrades are available from %q to %q. The minor version can only drift by 1.\n", current.service, supported.service) + if err := supported.service.IsUpgradeTo(current.service); err != nil { + newServices = consemver.Semver{} + u.log.Debugf("No valid service upgrades are available from %q to %q. The minor version can only drift by 1.\n", current.service.String(), supported.service.String()) } newKubernetes := filterK8sUpgrades(current.k8s, supported.k8s) @@ -352,8 +352,8 @@ type collector interface { newImages(ctx context.Context, version string) ([]versionsapi.Version, error) newMeasurements(ctx context.Context, csp cloudprovider.Provider, attestationVariant variant.Variant, images []versionsapi.Version) (map[string]measurements.M, error) newerVersions(ctx context.Context, allowedVersions []string) ([]versionsapi.Version, error) - newCLIVersions(ctx context.Context) ([]string, error) - filterCompatibleCLIVersions(ctx context.Context, cliPatchVersions []string, currentK8sVersion string) ([]string, error) + newCLIVersions(ctx context.Context) ([]consemver.Semver, error) + filterCompatibleCLIVersions(ctx context.Context, cliPatchVersions []consemver.Semver, currentK8sVersion string) ([]consemver.Semver, error) } type versionCollector struct { @@ -366,7 +366,7 @@ type versionCollector struct { rekor rekorVerifier flags upgradeCheckFlags versionsapi versionFetcher - cliVersion string + cliVersion consemver.Semver log debugLog } @@ -382,10 +382,10 @@ func (v *versionCollector) newMeasurements(ctx context.Context, csp cloudprovide } type currentVersionInfo struct { - service string + service consemver.Semver image string k8s string - cli string + cli consemver.Semver } func (v *versionCollector) currentVersions(ctx context.Context) (currentVersionInfo, error) { @@ -418,20 +418,18 @@ func (v *versionCollector) currentVersions(ctx context.Context) (currentVersionI } type supportedVersionInfo struct { - service string + service consemver.Semver image []versionsapi.Version k8s []string // CLI versions including those incompatible with the current Kubernetes version. - cli []string + cli []consemver.Semver // CLI versions compatible with the current Kubernetes version. - compatibleCLI []string + compatibleCLI []consemver.Semver } // supportedVersions returns slices of supported versions. func (v *versionCollector) supportedVersions(ctx context.Context, version, currentK8sVersion string) (supportedVersionInfo, error) { k8sVersions := versions.SupportedK8sVersions() - // Each CLI comes with a set of services that have the same version as the CLI. - serviceVersion := compatibility.EnsurePrefixV(constants.VersionInfo()) imageVersions, err := v.newImages(ctx, version) if err != nil { @@ -447,7 +445,8 @@ func (v *versionCollector) supportedVersions(ctx context.Context, version, curre } return supportedVersionInfo{ - service: serviceVersion, + // Each CLI comes with a set of services that have the same version as the CLI. + service: constants.BinaryVersion(), image: imageVersions, k8s: k8sVersions, cli: cliVersions, @@ -462,7 +461,7 @@ func (v *versionCollector) newImages(ctx context.Context, version string) ([]ver // additionally, we allow updates to the next minor version (e.g. 0.1.0 -> 0.2.0) // if the CLI minor version is newer than the cluster minor version currentImageMinorVer := semver.MajorMinor(version) - currentCLIMinorVer := semver.MajorMinor(v.cliVersion) + currentCLIMinorVer := semver.MajorMinor(v.cliVersion.String()) nextImageMinorVer, err := compatibility.NextMinorVersion(currentImageMinorVer) if err != nil { return nil, fmt.Errorf("calculating next image minor version: %w", err) @@ -522,15 +521,15 @@ func (v *versionCollector) newerVersions(ctx context.Context, allowedVersions [] } type versionUpgrade struct { - newServices string + newServices consemver.Semver newImages map[string]measurements.M newKubernetes []string - newCLI []string - newCompatibleCLI []string - currentServices string + newCLI []consemver.Semver + newCompatibleCLI []consemver.Semver + currentServices consemver.Semver currentImage string currentKubernetes string - currentCLI string + currentCLI consemver.Semver } func (v *versionUpgrade) buildString() (string, error) { @@ -560,7 +559,7 @@ func (v *versionUpgrade) buildString() (string, error) { fmt.Fprintln(&upgradeMsg, "") } - if v.newServices != "" { + if v.newServices != (consemver.Semver{}) { upgradeMsg.WriteString(fmt.Sprintf(" Services: %s --> %s\n", v.currentServices, v.newServices)) } @@ -572,12 +571,12 @@ func (v *versionUpgrade) buildString() (string, error) { } // no upgrades available - if v.newServices == "" && len(v.newImages) == 0 { + if v.newServices == (consemver.Semver{}) && len(v.newImages) == 0 { if len(v.newCompatibleCLI) > 0 { - result.WriteString(fmt.Sprintf("Newer CLI versions that are compatible with your cluster are: %s\n", strings.Join(v.newCompatibleCLI, " "))) + result.WriteString(fmt.Sprintf("Newer CLI versions that are compatible with your cluster are: %s\n", strings.Join(consemver.ToStrings(v.newCompatibleCLI), " "))) return result.String(), nil } else if len(v.newCLI) > 0 { - result.WriteString(fmt.Sprintf("There are newer CLIs available (%s), however, you need to upgrade your cluster's Kubernetes version first.\n", strings.Join(v.newCLI, " "))) + result.WriteString(fmt.Sprintf("There are newer CLIs available (%s), however, you need to upgrade your cluster's Kubernetes version first.\n", strings.Join(consemver.ToStrings(v.newCLI), " "))) return result.String(), nil } } @@ -589,7 +588,7 @@ func (v *versionUpgrade) buildString() (string, error) { func (v *versionUpgrade) writeConfig(conf *config.Config, fileHandler file.Handler, configPath string) error { // can't sort image map because maps are unsorted. services is only one string, k8s versions are sorted. - if v.newServices != "" { + if v.newServices != (consemver.Semver{}) { conf.MicroserviceVersion = v.newServices } if len(v.newKubernetes) > 0 { @@ -685,16 +684,12 @@ type versionFetcher interface { } // newCLIVersions returns a list of versions of the CLI which are a valid upgrade. -func (v *versionCollector) newCLIVersions(ctx context.Context) ([]string, error) { - cliVersion, err := conSemver.New(constants.VersionInfo()) - if err != nil { - return nil, fmt.Errorf("parsing current CLI version: %w", err) - } +func (v *versionCollector) newCLIVersions(ctx context.Context) ([]consemver.Semver, error) { list := versionsapi.List{ Ref: v.flags.ref, Stream: v.flags.stream, Granularity: versionsapi.GranularityMajor, - Base: fmt.Sprintf("v%d", cliVersion.Major), + Base: fmt.Sprintf("v%d", constants.BinaryVersion().Major()), Kind: versionsapi.VersionKindCLI, } minorList, err := v.versionsapi.FetchVersionList(ctx, list) @@ -704,7 +699,11 @@ func (v *versionCollector) newCLIVersions(ctx context.Context) ([]string, error) var patchVersions []string for _, version := range minorList.Versions { - if err := compatibility.IsValidUpgrade(v.cliVersion, version); err != nil { + target, err := consemver.New(version) + if err != nil { + return nil, fmt.Errorf("parsing version %s: %w", version, err) + } + if err := target.IsUpgradeTo(v.cliVersion); err != nil { v.log.Debugf("Skipping incompatible minor version %q: %s", version, err) continue } @@ -722,24 +721,29 @@ func (v *versionCollector) newCLIVersions(ctx context.Context) ([]string, error) patchVersions = append(patchVersions, patchList.Versions...) } - semver.Sort(patchVersions) + out, err := consemver.NewSlice(patchVersions) + if err != nil { + return nil, fmt.Errorf("parsing versions: %w", err) + } - return patchVersions, nil + consemver.Sort(out) + + return out, nil } // filterCompatibleCLIVersions filters a list of CLI versions which are compatible with the current Kubernetes version. -func (v *versionCollector) filterCompatibleCLIVersions(ctx context.Context, cliPatchVersions []string, currentK8sVersion string) ([]string, error) { +func (v *versionCollector) filterCompatibleCLIVersions(ctx context.Context, cliPatchVersions []consemver.Semver, currentK8sVersion string) ([]consemver.Semver, error) { // filter out invalid upgrades and versions which are not compatible with the current Kubernetes version - var compatibleVersions []string + var compatibleVersions []consemver.Semver for _, version := range cliPatchVersions { - if err := compatibility.IsValidUpgrade(v.cliVersion, version); err != nil { + if err := version.IsUpgradeTo(v.cliVersion); err != nil { v.log.Debugf("Skipping incompatible patch version %q: %s", version, err) continue } req := versionsapi.CLIInfo{ Ref: v.flags.ref, Stream: v.flags.stream, - Version: version, + Version: version.String(), } info, err := v.versionsapi.FetchCLIInfo(ctx, req) if err != nil { @@ -753,7 +757,7 @@ func (v *versionCollector) filterCompatibleCLIVersions(ctx context.Context, cliP } } - semver.Sort(compatibleVersions) + consemver.Sort(compatibleVersions) return compatibleVersions, nil } diff --git a/cli/internal/cmd/upgradecheck_test.go b/cli/internal/cmd/upgradecheck_test.go index f874bf0e7..4ada414ec 100644 --- a/cli/internal/cmd/upgradecheck_test.go +++ b/cli/internal/cmd/upgradecheck_test.go @@ -25,6 +25,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/logger" + consemver "github.com/edgelesssys/constellation/v2/internal/semver" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -40,30 +41,30 @@ func TestBuildString(t *testing.T) { }{ "update everything": { upgrade: versionUpgrade{ - newServices: "v2.5.0", + newServices: consemver.NewFromInt(2, 5, 0, ""), newImages: map[string]measurements.M{ "v2.5.0": measurements.DefaultsFor(cloudprovider.QEMU, variant.QEMUVTPM{}), }, newKubernetes: []string{"v1.24.12", "v1.25.6"}, - newCLI: []string{"v2.5.0", "v2.6.0"}, - currentServices: "v2.4.0", + newCLI: []consemver.Semver{consemver.NewFromInt(2, 5, 0, ""), consemver.NewFromInt(2, 6, 0, "")}, + currentServices: consemver.NewFromInt(2, 4, 0, ""), currentImage: "v2.4.0", currentKubernetes: "v1.24.5", - currentCLI: "v2.4.0", + currentCLI: consemver.NewFromInt(2, 4, 0, ""), }, expected: "The following updates are available with this CLI:\n Kubernetes: v1.24.5 --> v1.24.12 v1.25.6\n Images:\n v2.4.0 --> v2.5.0\n Includes these measurements:\n 4:\n expected: \"1234123412341234123412341234123412341234123412341234123412341234\"\n warnOnly: false\n 8:\n expected: \"0000000000000000000000000000000000000000000000000000000000000000\"\n warnOnly: false\n 9:\n expected: \"1234123412341234123412341234123412341234123412341234123412341234\"\n warnOnly: false\n 11:\n expected: \"0000000000000000000000000000000000000000000000000000000000000000\"\n warnOnly: false\n 12:\n expected: \"1234123412341234123412341234123412341234123412341234123412341234\"\n warnOnly: false\n 13:\n expected: \"0000000000000000000000000000000000000000000000000000000000000000\"\n warnOnly: false\n 15:\n expected: \"0000000000000000000000000000000000000000000000000000000000000000\"\n warnOnly: false\n \n Services: v2.4.0 --> v2.5.0\n", }, "cli incompatible with K8s": { upgrade: versionUpgrade{ - newCLI: []string{"v2.5.0", "v2.6.0"}, - currentCLI: "v2.4.0", + newCLI: []consemver.Semver{consemver.NewFromInt(2, 5, 0, ""), consemver.NewFromInt(2, 6, 0, "")}, + currentCLI: consemver.NewFromInt(2, 4, 0, ""), }, expected: "There are newer CLIs available (v2.5.0 v2.6.0), however, you need to upgrade your cluster's Kubernetes version first.\n", }, "cli compatible with K8s": { upgrade: versionUpgrade{ - newCompatibleCLI: []string{"v2.5.0", "v2.6.0"}, - currentCLI: "v2.4.0", + newCompatibleCLI: []consemver.Semver{consemver.NewFromInt(2, 5, 0, ""), consemver.NewFromInt(2, 6, 0, "")}, + currentCLI: consemver.NewFromInt(2, 4, 0, ""), }, expected: "Newer CLI versions that are compatible with your cluster are: v2.5.0 v2.6.0\n", }, @@ -76,14 +77,14 @@ func TestBuildString(t *testing.T) { }, "no upgrades": { upgrade: versionUpgrade{ - newServices: "", + newServices: consemver.Semver{}, newImages: map[string]measurements.M{}, newKubernetes: []string{}, - newCLI: []string{}, - currentServices: "v2.5.0", + newCLI: []consemver.Semver{}, + currentServices: consemver.NewFromInt(2, 5, 0, ""), currentImage: "v2.5.0", currentKubernetes: "v1.25.6", - currentCLI: "v2.5.0", + currentCLI: consemver.NewFromInt(2, 5, 0, ""), }, expected: "You are up to date.\n", }, @@ -227,18 +228,18 @@ func TestUpgradeCheck(t *testing.T) { Kind: versionsapi.VersionKindImage, } collector := stubVersionCollector{ - supportedServicesVersions: "v2.5.0", + supportedServicesVersions: consemver.NewFromInt(2, 5, 0, ""), supportedImages: []versionsapi.Version{v2_3}, supportedImageVersions: map[string]measurements.M{ "v2.3.0": measurements.DefaultsFor(cloudprovider.GCP, variant.GCPSEVES{}), }, supportedK8sVersions: []string{"v1.24.5", "v1.24.12", "v1.25.6"}, - currentServicesVersions: "v2.4.0", + currentServicesVersions: consemver.NewFromInt(2, 4, 0, ""), currentImageVersion: "v2.4.0", currentK8sVersion: "v1.24.5", - currentCLIVersion: "v2.4.0", + currentCLIVersion: consemver.NewFromInt(2, 4, 0, ""), images: []versionsapi.Version{v2_5}, - newCLIVersionsList: []string{"v2.5.0", "v2.6.0"}, + newCLIVersionsList: []consemver.Semver{consemver.NewFromInt(2, 5, 0, ""), consemver.NewFromInt(2, 6, 0, "")}, } testCases := map[string]struct { @@ -305,18 +306,18 @@ func TestUpgradeCheck(t *testing.T) { } type stubVersionCollector struct { - supportedServicesVersions string + supportedServicesVersions consemver.Semver supportedImages []versionsapi.Version supportedImageVersions map[string]measurements.M supportedK8sVersions []string - supportedCLIVersions []string - currentServicesVersions string + supportedCLIVersions []consemver.Semver + currentServicesVersions consemver.Semver currentImageVersion string currentK8sVersion string - currentCLIVersion string + currentCLIVersion consemver.Semver images []versionsapi.Version - newCLIVersionsList []string - newCompatibleCLIVersionsList []string + newCLIVersionsList []consemver.Semver + newCompatibleCLIVersionsList []consemver.Semver someErr error } @@ -350,11 +351,11 @@ func (s *stubVersionCollector) newerVersions(_ context.Context, _ []string) ([]v return s.images, nil } -func (s *stubVersionCollector) newCLIVersions(_ context.Context) ([]string, error) { +func (s *stubVersionCollector) newCLIVersions(_ context.Context) ([]consemver.Semver, error) { return s.newCLIVersionsList, nil } -func (s *stubVersionCollector) filterCompatibleCLIVersions(_ context.Context, _ []string, _ string) ([]string, error) { +func (s *stubVersionCollector) filterCompatibleCLIVersions(_ context.Context, _ []consemver.Semver, _ string) ([]consemver.Semver, error) { return s.newCompatibleCLIVersionsList, nil } @@ -408,7 +409,7 @@ func TestNewCLIVersions(t *testing.T) { } verCollector := func(minorList, patchList versionsapi.List, verListErr error) versionCollector { return versionCollector{ - cliVersion: "v0.1.0", + cliVersion: consemver.NewFromInt(0, 1, 0, ""), versionsapi: stubVersionFetcher{ minorList: minorList, patchList: patchList, @@ -451,7 +452,7 @@ func TestFilterCompatibleCLIVersions(t *testing.T) { someErr := errors.New("some error") verCollector := func(cliInfoErr error) versionCollector { return versionCollector{ - cliVersion: "v0.1.0", + cliVersion: consemver.NewFromInt(0, 1, 0, ""), versionsapi: stubVersionFetcher{ cliInfoErr: cliInfoErr, }, @@ -460,16 +461,16 @@ func TestFilterCompatibleCLIVersions(t *testing.T) { testCases := map[string]struct { verCollector versionCollector - cliPatchVersions []string + cliPatchVersions []consemver.Semver wantErr bool }{ "works": { verCollector: verCollector(nil), - cliPatchVersions: []string{"v0.1.1"}, + cliPatchVersions: []consemver.Semver{consemver.NewFromInt(0, 1, 1, "")}, }, "cli info error": { verCollector: verCollector(someErr), - cliPatchVersions: []string{"v0.1.1"}, + cliPatchVersions: []consemver.Semver{consemver.NewFromInt(0, 1, 1, "")}, wantErr: true, }, } diff --git a/cli/internal/cmd/version.go b/cli/internal/cmd/version.go index 33464dff8..a61aee437 100644 --- a/cli/internal/cmd/version.go +++ b/cli/internal/cmd/version.go @@ -35,7 +35,7 @@ func runVersion(cmd *cobra.Command, _ []string) { commit, state, date, goVersion, compiler, platform = parseStamp() } - cmd.Printf("Version:\t%s (%s)\n", constants.VersionInfo(), constants.VersionBuild) + cmd.Printf("Version:\t%s (%s)\n", constants.BinaryVersion().String(), constants.VersionBuild) cmd.Printf("GitCommit:\t%s\n", commit) cmd.Printf("GitTreeState:\t%s\n", state) cmd.Printf("BuildDate:\t%s\n", date) diff --git a/cli/internal/cmd/version_test.go b/cli/internal/cmd/version_test.go index 9e7879ab1..646244423 100644 --- a/cli/internal/cmd/version_test.go +++ b/cli/internal/cmd/version_test.go @@ -30,7 +30,7 @@ func TestVersionCmd(t *testing.T) { s, err := io.ReadAll(b) assert.NoError(err) - assert.Contains(string(s), constants.VersionInfo()) + assert.Contains(string(s), constants.BinaryVersion().String()) } func TestParseBuildInfo(t *testing.T) { diff --git a/cli/internal/helm/BUILD.bazel b/cli/internal/helm/BUILD.bazel index 96e97a2ed..05b8d3279 100644 --- a/cli/internal/helm/BUILD.bazel +++ b/cli/internal/helm/BUILD.bazel @@ -459,6 +459,7 @@ go_test( "//internal/deploy/helm", "//internal/file", "//internal/logger", + "//internal/semver", "@com_github_pkg_errors//:errors", "@com_github_spf13_afero//:afero", "@com_github_stretchr_testify//assert", diff --git a/cli/internal/helm/client.go b/cli/internal/helm/client.go index 563fb99f8..4f6f238dd 100644 --- a/cli/internal/helm/client.go +++ b/cli/internal/helm/client.go @@ -77,7 +77,7 @@ func NewClient(client crdClient, kubeConfigPath, helmNamespace string, log debug return &Client{kubectl: client, fs: fileHandler, actions: actions{config: actionConfig}, log: log}, nil } -func (c *Client) shouldUpgrade(releaseName, newVersion string, force bool) error { +func (c *Client) shouldUpgrade(releaseName string, newVersion semver.Semver, force bool) error { currentVersion, err := c.currentVersion(releaseName) if err != nil { return fmt.Errorf("getting version for %s: %w", releaseName, err) @@ -88,14 +88,15 @@ func (c *Client) shouldUpgrade(releaseName, newVersion string, force bool) error // This may break for cert-manager or cilium if we decide to upgrade more than one minor version at a time. // Leaving it as is since it is not clear to me what kind of sanity check we could do. if !force { - if err := compatibility.IsValidUpgrade(currentVersion, newVersion); err != nil { + if err := newVersion.IsUpgradeTo(currentVersion); err != nil { return err } } + cliVersion := constants.BinaryVersion() // at this point we conclude that the release should be upgraded. check that this CLI supports the upgrade. if releaseName == constellationOperatorsInfo.releaseName || releaseName == constellationServicesInfo.releaseName { - if compatibility.EnsurePrefixV(constants.VersionInfo()) != compatibility.EnsurePrefixV(newVersion) { - return fmt.Errorf("this CLI only supports microservice version %s for upgrading", constants.VersionInfo()) + if cliVersion.Compare(newVersion) != 0 { + return fmt.Errorf("this CLI only supports microservice version %s for upgrading", cliVersion.String()) } } c.log.Debugf("Upgrading %s from %s to %s", releaseName, currentVersion, newVersion) @@ -118,13 +119,17 @@ func (c *Client) Upgrade(ctx context.Context, config *config.Config, idFile clus } // define target version the chart is upgraded to - var upgradeVersion string + var upgradeVersion semver.Semver if info == constellationOperatorsInfo || info == constellationServicesInfo { // ensure that the services chart has the same version as the CLI - updateVersions(chart, compatibility.EnsurePrefixV(constants.VersionInfo())) + updateVersions(chart, constants.BinaryVersion()) upgradeVersion = config.MicroserviceVersion } else { - upgradeVersion = chart.Metadata.Version + chartVersion, err := semver.New(chart.Metadata.Version) + if err != nil { + return fmt.Errorf("parsing chart version: %w", err) + } + upgradeVersion = chartVersion } var invalidUpgrade *compatibility.InvalidUpgradeError @@ -222,49 +227,51 @@ func (c *Client) Versions() (ServiceVersions, error) { } res := ServiceVersions{ - cilium: compatibility.EnsurePrefixV(ciliumVersion), - certManager: compatibility.EnsurePrefixV(certManagerVersion), - constellationOperators: compatibility.EnsurePrefixV(operatorsVersion), - constellationServices: compatibility.EnsurePrefixV(servicesVersion), + cilium: ciliumVersion, + certManager: certManagerVersion, + constellationOperators: operatorsVersion, + constellationServices: servicesVersion, + awsLBController: awsLBVersion, } - if awsLBVersion != "" { - res.awsLBController = compatibility.EnsurePrefixV(awsLBVersion) + if awsLBVersion != (semver.Semver{}) { + res.awsLBController = awsLBVersion } + return res, nil } // currentVersion returns the version of the currently installed helm release. -func (c *Client) currentVersion(release string) (string, error) { +func (c *Client) currentVersion(release string) (semver.Semver, error) { rel, err := c.actions.listAction(release) if err != nil { - return "", err + return semver.Semver{}, err } if len(rel) == 0 { - return "", errReleaseNotFound + return semver.Semver{}, errReleaseNotFound } if len(rel) > 1 { - return "", fmt.Errorf("multiple releases found for %s", release) + return semver.Semver{}, fmt.Errorf("multiple releases found for %s", release) } if rel[0] == nil || rel[0].Chart == nil || rel[0].Chart.Metadata == nil { - return "", fmt.Errorf("received invalid release %s", release) + return semver.Semver{}, fmt.Errorf("received invalid release %s", release) } - return rel[0].Chart.Metadata.Version, nil + return semver.New(rel[0].Chart.Metadata.Version) } // ServiceVersions bundles the versions of all services that are part of Constellation. type ServiceVersions struct { - cilium string - certManager string - constellationOperators string - constellationServices string - awsLBController string + cilium semver.Semver + certManager semver.Semver + constellationOperators semver.Semver + constellationServices semver.Semver + awsLBController semver.Semver } // NewServiceVersions returns a new ServiceVersions struct. -func NewServiceVersions(cilium, certManager, constellationOperators, constellationServices string) ServiceVersions { +func NewServiceVersions(cilium, certManager, constellationOperators, constellationServices semver.Semver) ServiceVersions { return ServiceVersions{ cilium: cilium, certManager: certManager, @@ -274,22 +281,22 @@ func NewServiceVersions(cilium, certManager, constellationOperators, constellati } // Cilium returns the version of the Cilium release. -func (s ServiceVersions) Cilium() string { +func (s ServiceVersions) Cilium() semver.Semver { return s.cilium } // CertManager returns the version of the cert-manager release. -func (s ServiceVersions) CertManager() string { +func (s ServiceVersions) CertManager() semver.Semver { return s.certManager } // ConstellationOperators returns the version of the constellation-operators release. -func (s ServiceVersions) ConstellationOperators() string { +func (s ServiceVersions) ConstellationOperators() semver.Semver { return s.constellationOperators } // ConstellationServices returns the version of the constellation-services release. -func (s ServiceVersions) ConstellationServices() string { +func (s ServiceVersions) ConstellationServices() semver.Semver { return s.constellationServices } @@ -385,12 +392,8 @@ func (c *Client) applyMigrations(ctx context.Context, releaseName string, values if err != nil { return fmt.Errorf("getting %s version: %w", releaseName, err) } - currentV, err := semver.New(current) - if err != nil { - return fmt.Errorf("parsing current version: %w", err) - } - if currentV.Major == 2 && currentV.Minor == 8 { + if current.Major() == 2 && current.Minor() == 8 { // Rename/change the following function to implement any necessary migrations. return migrateFrom2_8(ctx, values, conf, c.kubectl) } diff --git a/cli/internal/helm/client_test.go b/cli/internal/helm/client_test.go index ea61f8fe8..285def10a 100644 --- a/cli/internal/helm/client_test.go +++ b/cli/internal/helm/client_test.go @@ -15,6 +15,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/compatibility" "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/edgelesssys/constellation/v2/internal/semver" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "helm.sh/helm/v3/pkg/chart" @@ -49,7 +50,9 @@ func TestShouldUpgrade(t *testing.T) { chart, err := loadChartsDir(helmFS, certManagerInfo.path) require.NoError(err) - err = client.shouldUpgrade(certManagerInfo.releaseName, chart.Metadata.Version, false) + chartVersion, err := semver.New(chart.Metadata.Version) + require.NoError(err) + err = client.shouldUpgrade(certManagerInfo.releaseName, chartVersion, false) if tc.wantError { tc.assertCorrectError(t, err) return diff --git a/cli/internal/helm/loader.go b/cli/internal/helm/loader.go index 28c9ca30c..d646b6224 100644 --- a/cli/internal/helm/loader.go +++ b/cli/internal/helm/loader.go @@ -25,10 +25,10 @@ import ( "github.com/edgelesssys/constellation/v2/cli/internal/helm/imageversion" "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/deploy/helm" + "github.com/edgelesssys/constellation/v2/internal/semver" "github.com/edgelesssys/constellation/v2/internal/versions" ) @@ -175,15 +175,15 @@ func (i *ChartLoader) loadRelease(info chartInfo, helmWaitMode helm.WaitMode) (h case certManagerInfo.releaseName: values = i.loadCertManagerValues() case constellationOperatorsInfo.releaseName: - updateVersions(chart, compatibility.EnsurePrefixV(constants.VersionInfo())) + updateVersions(chart, constants.BinaryVersion()) values = i.loadOperatorsValues() case constellationServicesInfo.releaseName: - updateVersions(chart, compatibility.EnsurePrefixV(constants.VersionInfo())) + updateVersions(chart, constants.BinaryVersion()) values = i.loadConstellationServicesValues() case awsLBControllerInfo.releaseName: values = i.loadAWSLBControllerValues() case csiInfo.releaseName: - updateVersions(chart, compatibility.EnsurePrefixV(constants.VersionInfo())) + updateVersions(chart, constants.BinaryVersion()) values = i.loadCSIValues() } @@ -378,19 +378,19 @@ func extendConstellationServicesValues( } // updateVersions changes all versions of direct dependencies that are set to "0.0.0" to newVersion. -func updateVersions(chart *chart.Chart, newVersion string) { - chart.Metadata.Version = newVersion +func updateVersions(chart *chart.Chart, newVersion semver.Semver) { + chart.Metadata.Version = newVersion.String() selectedDeps := chart.Metadata.Dependencies for i := range selectedDeps { if selectedDeps[i].Version == "0.0.0" { - selectedDeps[i].Version = newVersion + selectedDeps[i].Version = newVersion.String() } } deps := chart.Dependencies() for i := range deps { if deps[i].Metadata.Version == "0.0.0" { - deps[i].Metadata.Version = newVersion + deps[i].Metadata.Version = newVersion.String() } } } diff --git a/disk-mapper/cmd/main.go b/disk-mapper/cmd/main.go index d6d3efc88..415b1c56c 100644 --- a/disk-mapper/cmd/main.go +++ b/disk-mapper/cmd/main.go @@ -52,7 +52,7 @@ func main() { flag.Parse() log := logger.New(logger.JSONLog, logger.VerbosityFromInt(*verbosity)) - log.With(zap.String("version", constants.VersionInfo()), zap.String("cloudProvider", *csp)). + log.With(zap.String("version", constants.BinaryVersion().String()), zap.String("cloudProvider", *csp)). Infof("Starting disk-mapper") // set up quote issuer for aTLS connections diff --git a/e2e/internal/upgrade/BUILD.bazel b/e2e/internal/upgrade/BUILD.bazel index 3549227d6..2d16064bd 100644 --- a/e2e/internal/upgrade/BUILD.bazel +++ b/e2e/internal/upgrade/BUILD.bazel @@ -12,6 +12,7 @@ go_library( deps = [ "//internal/constants", "//internal/logger", + "//internal/semver", "@sh_helm_helm_v3//pkg/action", "@sh_helm_helm_v3//pkg/cli", ], diff --git a/e2e/internal/upgrade/helm.go b/e2e/internal/upgrade/helm.go index a2fd4acbb..b0fc498fd 100644 --- a/e2e/internal/upgrade/helm.go +++ b/e2e/internal/upgrade/helm.go @@ -14,41 +14,42 @@ import ( "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/edgelesssys/constellation/v2/internal/semver" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/cli" ) -func servicesVersion(t *testing.T) (string, error) { +func servicesVersion(t *testing.T) (semver.Semver, error) { t.Helper() log := logger.NewTest(t) settings := cli.New() settings.KubeConfig = "constellation-admin.conf" actionConfig := &action.Configuration{} if err := actionConfig.Init(settings.RESTClientGetter(), constants.HelmNamespace, "secret", log.Infof); err != nil { - return "", fmt.Errorf("initializing config: %w", err) + return semver.Semver{}, fmt.Errorf("initializing config: %w", err) } return currentVersion(actionConfig, "constellation-services") } -func currentVersion(cfg *action.Configuration, release string) (string, error) { +func currentVersion(cfg *action.Configuration, release string) (semver.Semver, error) { action := action.NewList(cfg) action.Filter = release rel, err := action.Run() if err != nil { - return "", err + return semver.Semver{}, err } if len(rel) == 0 { - return "", fmt.Errorf("release %s not found", release) + return semver.Semver{}, fmt.Errorf("release %s not found", release) } if len(rel) > 1 { - return "", fmt.Errorf("multiple releases found for %s", release) + return semver.Semver{}, fmt.Errorf("multiple releases found for %s", release) } if rel[0] == nil || rel[0].Chart == nil || rel[0].Chart.Metadata == nil { - return "", fmt.Errorf("received invalid release %s", release) + return semver.Semver{}, fmt.Errorf("received invalid release %s", release) } - return rel[0].Chart.Metadata.Version, nil + return semver.New(rel[0].Chart.Metadata.Version) } diff --git a/e2e/internal/upgrade/upgrade_test.go b/e2e/internal/upgrade/upgrade_test.go index 0620b8b3a..9aca68e11 100644 --- a/e2e/internal/upgrade/upgrade_test.go +++ b/e2e/internal/upgrade/upgrade_test.go @@ -293,11 +293,13 @@ func writeUpgradeConfig(require *require.Assertions, image string, kubernetes st require.NoError(err) } - var microserviceVersion string + var microserviceVersion semver.Semver if microservices == "" { microserviceVersion = defaultConfig.MicroserviceVersion } else { - microserviceVersion = microservices + version, err := semver.New(microservices) + require.NoError(err) + microserviceVersion = version } log.Printf("Setting K8s version: %s\n", kubernetesVersion.String()) @@ -330,10 +332,8 @@ func runUpgradeCheck(require *require.Assertions, cli, targetKubernetes string) require.Contains(string(stdout), targetKubernetes, fmt.Sprintf("Expected Kubernetes version %s in output.", targetKubernetes)) } - cliVersion, err := semver.New(constants.VersionInfo()) - require.NoError(err) require.Contains(string(stdout), "Services:") - require.Contains(string(stdout), fmt.Sprintf("--> %s", cliVersion.String())) + require.Contains(string(stdout), fmt.Sprintf("--> %s", constants.BinaryVersion().String())) log.Println(string(stdout)) } @@ -403,7 +403,7 @@ func testStatusEventuallyWorks(t *testing.T, cli string, timeout time.Duration) }, timeout, time.Minute) } -func testMicroservicesEventuallyHaveVersion(t *testing.T, wantMicroserviceVersion string, timeout time.Duration) { +func testMicroservicesEventuallyHaveVersion(t *testing.T, wantMicroserviceVersion semver.Semver, timeout time.Duration) { require.Eventually(t, func() bool { version, err := servicesVersion(t) if err != nil { @@ -460,7 +460,7 @@ func testNodesEventuallyHaveVersion(t *testing.T, k *kubernetes.Clientset, targe type versionContainer struct { imageRef string kubernetes semver.Semver - microservices string + microservices semver.Semver } // runCommandWithSeparateOutputs runs the given command while separating buffers for diff --git a/internal/config/BUILD.bazel b/internal/config/BUILD.bazel index 0104d7cfd..340ad682e 100644 --- a/internal/config/BUILD.bazel +++ b/internal/config/BUILD.bazel @@ -29,6 +29,7 @@ go_library( "//internal/config/instancetypes", "//internal/constants", "//internal/file", + "//internal/semver", "//internal/versions", "@com_github_go_playground_locales//en", "@com_github_go_playground_universal_translator//:universal-translator", diff --git a/internal/config/config.go b/internal/config/config.go index 7c71cdeaf..25fa57067 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -40,10 +40,10 @@ import ( "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" "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/imageversion" "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" ) @@ -63,7 +63,7 @@ type Config struct { Version string `yaml:"version" validate:"eq=v3"` // description: | // Machine image version used to create Constellation nodes. - Image string `yaml:"image" validate:"required,version_compatibility"` + Image string `yaml:"image" validate:"required,image_compatibility"` // description: | // Name of the cluster. Name string `yaml:"name" validate:"valid_name,required"` @@ -75,7 +75,7 @@ type Config struct { KubernetesVersion string `yaml:"kubernetesVersion" validate:"required,supported_k8s_version"` // description: | // Microservice version to be installed into the cluster. Defaults to the version of the CLI. - MicroserviceVersion string `yaml:"microserviceVersion" validate:"required,version_compatibility"` + MicroserviceVersion semver.Semver `yaml:"microserviceVersion" validate:"required"` // description: | // DON'T USE IN PRODUCTION: enable debug mode and use debug images. DebugCluster *bool `yaml:"debugCluster" validate:"required"` @@ -315,7 +315,7 @@ func Default() *Config { Version: Version3, Image: defaultImage, Name: defaultName, - MicroserviceVersion: compatibility.EnsurePrefixV(constants.VersionInfo()), + MicroserviceVersion: constants.BinaryVersion(), KubernetesVersion: string(versions.Default), StateDiskSizeGB: 30, DebugCluster: toPtr(false), @@ -725,7 +725,7 @@ func (c *Config) Validate(force bool) error { return err } - if err := validate.RegisterTranslation("version_compatibility", trans, registerVersionCompatibilityError, translateVersionCompatibilityError); err != nil { + if err := validate.RegisterTranslation("image_compatibility", trans, registerImageCompatibilityError, translateImageCompatibilityError); err != nil { return err } @@ -750,7 +750,7 @@ func (c *Config) Validate(force bool) error { if force { versionCompatibilityValidator = returnsTrue } - if err := validate.RegisterValidation("version_compatibility", versionCompatibilityValidator); err != nil { + if err := validate.RegisterValidation("image_compatibility", versionCompatibilityValidator); err != nil { return err } @@ -800,6 +800,18 @@ func (c *Config) Validate(force bool) error { validate.RegisterStructValidation(validateMeasurement, measurements.Measurement{}) validate.RegisterStructValidation(validateAttestation, AttestationConfig{}) + if !force { + // Validating MicroserviceVersion separately is required since it is a custom type. + // The validation pkg we use does not allow accessing the field name during struct validation. + // Because of this we can't print the offending field name in the error message, resulting in + // suboptimal UX. Adding the field name to the struct validation of Semver would make it + // impossible to use Semver for other fields. + if err := validateMicroserviceVersion(constants.BinaryVersion(), c.MicroserviceVersion); err != nil { + msg := "microserviceVersion: " + msgFromCompatibilityError(err, constants.BinaryVersion().String(), c.MicroserviceVersion.String()) + return &ValidationError{validationErrMsgs: []string{msg}} + } + } + err := validate.Struct(c) if err == nil { return nil diff --git a/internal/config/config_doc.go b/internal/config/config_doc.go index 4a38dcbf8..79a9d8437 100644 --- a/internal/config/config_doc.go +++ b/internal/config/config_doc.go @@ -61,7 +61,7 @@ func init() { ConfigDoc.Fields[4].Description = "Kubernetes version to be installed into the cluster." ConfigDoc.Fields[4].Comments[encoder.LineComment] = "Kubernetes version to be installed into the cluster." ConfigDoc.Fields[5].Name = "microserviceVersion" - ConfigDoc.Fields[5].Type = "string" + ConfigDoc.Fields[5].Type = "Semver" ConfigDoc.Fields[5].Note = "" ConfigDoc.Fields[5].Description = "Microservice version to be installed into the cluster. Defaults to the version of the CLI." ConfigDoc.Fields[5].Comments[encoder.LineComment] = "Microservice version to be installed into the cluster. Defaults to the version of the CLI." diff --git a/internal/config/config_test.go b/internal/config/config_test.go index d67fc6810..720b5b532 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -140,7 +140,7 @@ func TestNew(t *testing.T) { func modifyConfigForAzureToPassValidate(c *Config) { c.RemoveProviderAndAttestationExcept(cloudprovider.Azure) - c.Image = "v" + constants.VersionInfo() + c.Image = constants.BinaryVersion().String() c.Provider.Azure.SubscriptionID = "11111111-1111-1111-1111-111111111111" c.Provider.Azure.TenantID = "11111111-1111-1111-1111-111111111111" c.Provider.Azure.Location = "westus" @@ -308,13 +308,25 @@ func TestValidate(t *testing.T) { cnf.Image = "" ver, err := semver.New(versions.SupportedK8sVersions()[0]) require.NoError(t, err) - ver.Patch = ver.Patch - 1 + 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() + cnf.Image = "" + cliVersion := constants.BinaryVersion() + cnf.MicroserviceVersion = semver.NewFromInt(cliVersion.Major()+2, cliVersion.Minor(), cliVersion.Patch(), "") + return cnf + }(), + wantErr: true, + // This is a very different value from the other error counts because of the way we are checking MicroserviceVersions. + wantErrCount: 1, + }, "v0 is one error": { cnf: func() *Config { cnf := Default() @@ -350,7 +362,7 @@ func TestValidate(t *testing.T) { cnf: func() *Config { cnf := Default() cnf.RemoveProviderAndAttestationExcept(cloudprovider.Azure) - cnf.Image = "v" + constants.VersionInfo() + cnf.Image = constants.BinaryVersion().String() az := cnf.Provider.Azure az.SubscriptionID = "01234567-0123-0123-0123-0123456789ab" az.TenantID = "01234567-0123-0123-0123-0123456789ab" @@ -411,7 +423,7 @@ func TestValidate(t *testing.T) { cnf: func() *Config { cnf := Default() cnf.RemoveProviderAndAttestationExcept(cloudprovider.GCP) - cnf.Image = "v" + constants.VersionInfo() + cnf.Image = constants.BinaryVersion().String() gcp := cnf.Provider.GCP gcp.Region = "test-region" gcp.Project = "test-project" diff --git a/internal/config/migration/BUILD.bazel b/internal/config/migration/BUILD.bazel index e6946ba62..cb4e27da7 100644 --- a/internal/config/migration/BUILD.bazel +++ b/internal/config/migration/BUILD.bazel @@ -11,5 +11,6 @@ go_library( "//internal/attestation/variant", "//internal/config", "//internal/file", + "//internal/semver", ], ) diff --git a/internal/config/migration/migration.go b/internal/config/migration/migration.go index 5b780a7ee..4ebee195c 100644 --- a/internal/config/migration/migration.go +++ b/internal/config/migration/migration.go @@ -18,6 +18,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/file" + "github.com/edgelesssys/constellation/v2/internal/semver" ) const ( @@ -28,11 +29,11 @@ const ( // Config defines configuration used by CLI. type Config struct { Version string `yaml:"version" validate:"eq=v2"` - Image string `yaml:"image" validate:"required,version_compatibility"` + Image string `yaml:"image" validate:"required,image_compatibility"` Name string `yaml:"name" validate:"valid_name,required"` StateDiskSizeGB int `yaml:"stateDiskSizeGB" validate:"min=0"` KubernetesVersion string `yaml:"kubernetesVersion" validate:"required,supported_k8s_version"` - MicroserviceVersion string `yaml:"microserviceVersion" validate:"required,version_compatibility"` + MicroserviceVersion string `yaml:"microserviceVersion" validate:"required"` DebugCluster *bool `yaml:"debugCluster" validate:"required"` AttestationVariant string `yaml:"attestationVariant,omitempty" validate:"valid_attestation_variant"` Provider ProviderConfig `yaml:"provider" validate:"dive"` @@ -195,6 +196,11 @@ func V2ToV3(path string, fileHandler file.Handler) error { return fmt.Errorf("reading config file %s using v2 format: %w", path, err) } + microserviceVersion, err := semver.New(cfgV2.MicroserviceVersion) + if err != nil { + return fmt.Errorf("parsing microservice version: %w", err) + } + // Migrate to new format var cfgV3 config.Config cfgV3.Version = config.Version3 @@ -202,7 +208,7 @@ func V2ToV3(path string, fileHandler file.Handler) error { cfgV3.Name = cfgV2.Name cfgV3.StateDiskSizeGB = cfgV2.StateDiskSizeGB cfgV3.KubernetesVersion = cfgV2.KubernetesVersion - cfgV3.MicroserviceVersion = cfgV2.MicroserviceVersion + cfgV3.MicroserviceVersion = microserviceVersion cfgV3.DebugCluster = cfgV2.DebugCluster switch { diff --git a/internal/config/validation.go b/internal/config/validation.go index 115dd0b09..d6a138929 100644 --- a/internal/config/validation.go +++ b/internal/config/validation.go @@ -27,6 +27,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/compatibility" "github.com/edgelesssys/constellation/v2/internal/config/instancetypes" "github.com/edgelesssys/constellation/v2/internal/constants" + consemver "github.com/edgelesssys/constellation/v2/internal/semver" "github.com/edgelesssys/constellation/v2/internal/versions" ) @@ -86,11 +87,11 @@ func translateInvalidK8sVersionError(ut ut.Translator, fe validator.FieldError) configured = compatibility.EnsurePrefixV(configured) switch { case !semver.IsValid(configured): - errorMsg = "The configured version is not a valid semantic version" + errorMsg = "The configured version is not a valid semantic version\n" case semver.Compare(configured, minVersion) == -1: - errorMsg = fmt.Sprintf("The configured version %s is older than the oldest version supported by this CLI: %s", configured, minVersion) + errorMsg = fmt.Sprintf("The configured version %s is older than the oldest version supported by this CLI: %s\n", configured, minVersion) case semver.Compare(configured, maxVersion) == 1: - errorMsg = fmt.Sprintf("The configured version %s is newer than the newest version supported by this CLI: %s", configured, maxVersion) + errorMsg = fmt.Sprintf("The configured version %s is newer than the newest version supported by this CLI: %s\n", configured, maxVersion) } errorMsg = errorMsg + fmt.Sprintf("Supported versions: %s", strings.Join(validVersionsSorted, " ")) @@ -492,44 +493,21 @@ func K8sVersionFromMajorMinor(version string) string { } } -func registerVersionCompatibilityError(ut ut.Translator) error { - return ut.Add("version_compatibility", "{0} specifies an invalid version: {1}", true) -} - -func translateVersionCompatibilityError(ut ut.Translator, fe validator.FieldError) string { - binaryVersion := constants.VersionInfo() - err := validateVersionCompatibilityHelper(binaryVersion, fe.Field(), fe.Value().(string)) - var msg string - - switch { - case errors.Is(err, compatibility.ErrSemVer): - msg = fmt.Sprintf("configured version (%s) does not adhere to SemVer syntax", fe.Value().(string)) - case errors.Is(err, compatibility.ErrMajorMismatch): - msg = fmt.Sprintf("the CLI's major version (%s) has to match your configured major version (%s). Use --force to ignore the version mismatch.", constants.VersionInfo(), fe.Value().(string)) - case errors.Is(err, compatibility.ErrMinorDrift): - msg = fmt.Sprintf("the CLI's minor version (%s) and the configured version (%s) are more than one minor version apart. Use --force to ignore the version mismatch.", constants.VersionInfo(), fe.Value().(string)) - case errors.Is(err, compatibility.ErrOutdatedCLI): - msg = fmt.Sprintf("the CLI's version (%s) is older than the configured version (%s). Use --force to ignore the version mismatch.", constants.VersionInfo(), fe.Value().(string)) - default: - msg = err.Error() - } - - t, _ := ut.T("version_compatibility", fe.Field(), msg) - - return t +func registerImageCompatibilityError(ut ut.Translator) error { + return ut.Add("image_compatibility", "{0} specifies an invalid version: {1}", true) } // Check that the validated field and the CLI version are not more than one minor version apart. func validateVersionCompatibility(fl validator.FieldLevel) bool { - binaryVersion := constants.VersionInfo() - if err := validateVersionCompatibilityHelper(binaryVersion, fl.FieldName(), fl.Field().String()); err != nil { + binaryVersion := constants.BinaryVersion() + if err := validateImageCompatibilityHelper(binaryVersion, fl.FieldName(), fl.Field().String()); err != nil { return false } return true } -func validateVersionCompatibilityHelper(binaryVersion, fieldName, configuredVersion string) error { +func validateImageCompatibilityHelper(binaryVersion consemver.Semver, fieldName, configuredVersion string) error { if fieldName == "image" { imageVersion, err := versionsapi.NewVersionFromShortPath(configuredVersion, versionsapi.VersionKindImage) if err != nil { @@ -538,15 +516,51 @@ func validateVersionCompatibilityHelper(binaryVersion, fieldName, configuredVers configuredVersion = imageVersion.Version } - if fieldName == "microserviceVersion" { - cliVersion := compatibility.EnsurePrefixV(binaryVersion) - serviceVersion := compatibility.EnsurePrefixV(configuredVersion) - if semver.Compare(cliVersion, serviceVersion) == -1 { - return fmt.Errorf("the CLI's version (%s) is older than the configured version (%s)", cliVersion, serviceVersion) - } + return compatibility.BinaryWith(binaryVersion.String(), configuredVersion) +} + +func translateImageCompatibilityError(ut ut.Translator, fe validator.FieldError) string { + binaryVersion := constants.BinaryVersion() + err := validateImageCompatibilityHelper(binaryVersion, fe.Field(), fe.Value().(string)) + + msg := msgFromCompatibilityError(err, binaryVersion.String(), fe.Value().(string)) + + t, _ := ut.T("image_compatibility", fe.Field(), msg) + + return t +} + +// msgFromCompatibilityError translates compatibility errors into user-facing error messages. +func msgFromCompatibilityError(err error, binaryVersion, fieldValue string) string { + switch { + case errors.Is(err, compatibility.ErrSemVer): + return fmt.Sprintf("configured version (%s) does not adhere to SemVer syntax", fieldValue) + case errors.Is(err, compatibility.ErrMajorMismatch): + return fmt.Sprintf("the CLI's major version (%s) has to match your configured major version (%s). Use --force to ignore the version mismatch.", binaryVersion, fieldValue) + case errors.Is(err, compatibility.ErrMinorDrift): + return fmt.Sprintf("the CLI's minor version (%s) and the configured version (%s) are more than one minor version apart. Use --force to ignore the version mismatch.", binaryVersion, fieldValue) + case errors.Is(err, compatibility.ErrOutdatedCLI): + return fmt.Sprintf("the CLI's version (%s) is older than the configured version (%s). Use --force to ignore the version mismatch.", binaryVersion, fieldValue) + default: + return err.Error() + } +} + +func validateMicroserviceVersion(binaryVersion, version consemver.Semver) error { + // Major versions always have to match. + if binaryVersion.Major() != version.Major() { + return compatibility.ErrMajorMismatch + } + // Allow newer CLIs (for upgrades), but dissallow newer service versions. + if binaryVersion.Compare(version) == -1 { + return compatibility.ErrOutdatedCLI + } + // Abort if minor version drift between CLI and versionA value is greater than 1. + if binaryVersion.Minor()-version.Minor() > 1 { + return compatibility.ErrMinorDrift } - return compatibility.BinaryWith(binaryVersion, configuredVersion) + return nil } func returnsTrue(_ validator.FieldLevel) bool { diff --git a/internal/config/validation_test.go b/internal/config/validation_test.go index ff3639668..f6a5bde95 100644 --- a/internal/config/validation_test.go +++ b/internal/config/validation_test.go @@ -9,26 +9,27 @@ package config import ( "testing" + "github.com/edgelesssys/constellation/v2/internal/semver" "github.com/stretchr/testify/assert" ) // TestValidateVersionCompatibilityHelper checks that basic version and image short paths are correctly validated. func TestValidateVersionCompatibilityHelper(t *testing.T) { testCases := map[string]struct { - cli string + cli semver.Semver target string wantError bool }{ "full version works": { - cli: "0.1.0", + cli: semver.NewFromInt(0, 1, 0, ""), target: "v0.0.0", }, "short path works": { - cli: "0.1.0", + cli: semver.NewFromInt(0, 1, 0, ""), target: "ref/main/stream/debug/v0.0.0-pre.0.20230109121528-d24fac00f018", }, "minor version difference > 1": { - cli: "0.0.0", + cli: semver.NewFromInt(0, 0, 0, ""), target: "ref/main/stream/debug/v0.2.0-pre.0.20230109121528-d24fac00f018", wantError: true, }, @@ -38,7 +39,43 @@ func TestValidateVersionCompatibilityHelper(t *testing.T) { t.Run(name, func(t *testing.T) { assert := assert.New(t) - err := validateVersionCompatibilityHelper(tc.cli, "image", tc.target) + err := validateImageCompatibilityHelper(tc.cli, "image", tc.target) + if tc.wantError { + assert.Error(err) + return + } + assert.NoError(err) + }) + } +} + +func TestValidateMicroserviceVersion(t *testing.T) { + testCases := map[string]struct { + cli semver.Semver + services semver.Semver + wantError bool + }{ + "success": { + cli: semver.NewFromInt(0, 1, 0, ""), + services: semver.NewFromInt(0, 0, 0, ""), + }, + "minor version difference > 1": { + cli: semver.NewFromInt(0, 0, 0, ""), + services: semver.NewFromInt(0, 2, 0, "pre.0.20230109121528-d24fac00f018"), + wantError: true, + }, + "major version difference": { + cli: semver.NewFromInt(0, 0, 0, ""), + services: semver.NewFromInt(1, 0, 0, ""), + wantError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + err := validateMicroserviceVersion(tc.cli, tc.services) if tc.wantError { assert.Error(err) return diff --git a/internal/constants/BUILD.bazel b/internal/constants/BUILD.bazel index 9ff968694..ba157792b 100644 --- a/internal/constants/BUILD.bazel +++ b/internal/constants/BUILD.bazel @@ -17,4 +17,5 @@ go_library( "timestamp": "{STABLE_STAMP_TIME}", "versionInfo": "{STABLE_STAMP_VERSION}", }, + deps = ["//internal/semver"], ) diff --git a/internal/constants/constants.go b/internal/constants/constants.go index a720f2d01..c08ee2653 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -11,7 +11,10 @@ Constants should never be overwritable by command line flags or configuration fi package constants import ( + "fmt" "time" + + "github.com/edgelesssys/constellation/v2/internal/semver" ) const ( @@ -231,9 +234,15 @@ b92PDCpM7FZAINQF88s1TZS/HmRXYk62UJ4eqPduvUnJmXhNikhLbMi6fw== ` ) -// VersionInfo returns the version of a binary. -func VersionInfo() string { - return versionInfo +// BinaryVersion returns the version of this Binary. +func BinaryVersion() semver.Semver { + version, err := semver.New(versionInfo) + if err != nil { + // This is not user input, unrecoverable, should never happen. + panic(fmt.Sprintf("parsing embedded version information: %s", err)) + } + + return version } // Timestamp returns the commit timestamp of a binary. diff --git a/internal/semver/BUILD.bazel b/internal/semver/BUILD.bazel index b16e43c95..7db0e0efa 100644 --- a/internal/semver/BUILD.bazel +++ b/internal/semver/BUILD.bazel @@ -7,7 +7,7 @@ go_library( importpath = "github.com/edgelesssys/constellation/v2/internal/semver", visibility = ["//:__subpackages__"], deps = [ - "//internal/constants", + "//internal/compatibility", "@org_golang_x_mod//semver", ], ) @@ -19,5 +19,6 @@ go_test( deps = [ "@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//require", + "@in_gopkg_yaml_v3//:yaml_v3", ], ) diff --git a/internal/semver/semver.go b/internal/semver/semver.go index e189d97db..9c91dcd7e 100644 --- a/internal/semver/semver.go +++ b/internal/semver/semver.go @@ -6,24 +6,37 @@ SPDX-License-Identifier: AGPL-3.0-only /* Package semver provides functionality to parse and process semantic versions, as they are used in multiple components of Constellation. + +The official [semantic versioning specification] disallows leading "v" prefixes. +However, the Constellation config uses the "v" prefix for versions to make version strings more recognizable. +This package bridges the gap between Go's semver pkg (doesn't allow "v" prefix) and the Constellation config (requires "v" prefix). + +[semantic versioning specification]: https://semver.org/ */ package semver import ( "encoding/json" + "errors" "fmt" + "sort" "strings" - "github.com/edgelesssys/constellation/v2/internal/constants" + "github.com/edgelesssys/constellation/v2/internal/compatibility" "golang.org/x/mod/semver" ) +// Sort sorts a list of semantic version strings using [ByVersion]. +func Sort(list []Semver) { + sort.Sort(byVersion(list)) +} + // Semver represents a semantic version. type Semver struct { - Major int - Minor int - Patch int - Prerelease string + major int + minor int + patch int + prerelease string } // New returns a Version from a string. @@ -47,18 +60,72 @@ func New(version string) (Semver, error) { } return Semver{ - Major: major, - Minor: minor, - Patch: patch, - Prerelease: pre, + major: major, + minor: minor, + patch: patch, + prerelease: pre, }, nil } +// NewFromInt constructs a new Semver from three integers and prerelease string: MAJOR.MINOR.PATCH-PRERELEASE. +func NewFromInt(major, minor, patch int, prerelease string) Semver { + return Semver{ + major: major, + minor: minor, + patch: patch, + prerelease: prerelease, + } +} + +// NewSlice returns a slice of Semver from a slice of strings. +func NewSlice(in []string) ([]Semver, error) { + var out []Semver + for _, version := range in { + semVersion, err := New(version) + if err != nil { + return nil, fmt.Errorf("parsing version %s: %w", version, err) + } + out = append(out, semVersion) + } + + return out, nil +} + +// ToStrings converts a slice of Semver to a slice of strings. +func ToStrings(in []Semver) []string { + var out []string + for _, v := range in { + out = append(out, v.String()) + } + + return out +} + +// Major returns the major version of the object. +func (v Semver) Major() int { + return v.major +} + +// Minor returns the minor version of the object. +func (v Semver) Minor() int { + return v.minor +} + +// Patch returns the patch version of the object. +func (v Semver) Patch() int { + return v.patch +} + +// Prerelease returns the prerelease section of the object. +func (v Semver) Prerelease() string { + return v.prerelease +} + // String returns the string representation of the version. func (v Semver) String() string { - version := fmt.Sprintf("v%d.%d.%d", v.Major, v.Minor, v.Patch) - if v.Prerelease != "" { - return fmt.Sprintf("%s-%s", version, v.Prerelease) + version := fmt.Sprintf("v%d.%d.%d", v.major, v.minor, v.patch) + if v.prerelease != "" { + return fmt.Sprintf("%s-%s", version, v.prerelease) } return version } @@ -70,29 +137,51 @@ func (v Semver) Compare(other Semver) int { // 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 + 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 { - return v.Compare(other) > 0 && v.Major == other.Major && v.Minor-other.Minor <= 1 -} - -// CompatibleWithBinary returns if a version is compatible version of the current built binary. -// It checks if the version of the binary is equal or greater than the current version and allows a drift of at most one minor version. -func (v Semver) CompatibleWithBinary() bool { - binaryVersion, err := New(constants.VersionInfo()) - if err != nil { - return false +func (v Semver) IsUpgradeTo(other Semver) error { + if v.Compare(other) <= 0 { + return compatibility.NewInvalidUpgradeError(v.String(), other.String(), errors.New("current version newer than or equal to new version")) + } + if v.major != other.major { + return compatibility.NewInvalidUpgradeError(v.String(), other.String(), compatibility.ErrMajorMismatch) } - return v.Compare(binaryVersion) == 0 || binaryVersion.IsUpgradeTo(v) + if v.minor-other.minor > 1 { + return compatibility.NewInvalidUpgradeError(v.String(), other.String(), compatibility.ErrMinorDrift) + } + + return nil } -// NextMinor returns the next minor version in the format "vMAJOR.MINOR". +// NextMinor returns the next minor version in the format "vMAJOR.MINOR+1". func (v Semver) NextMinor() string { - return fmt.Sprintf("v%d.%d", v.Major, v.Minor+1) + return fmt.Sprintf("v%d.%d", v.major, v.minor+1) +} + +// MarshalYAML implements the yaml.Marshaller interface. +func (v Semver) MarshalYAML() (any, error) { + return v.String(), nil +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (v *Semver) UnmarshalYAML(unmarshal func(any) error) error { + var raw string + if err := unmarshal(&raw); err != nil { + return fmt.Errorf("unmarshalling to string: %w", err) + } + + version, err := New(raw) + if err != nil { + return fmt.Errorf("parsing semantic version: %w", err) + } + + *v = version + + return nil } // MarshalJSON implements the json.Marshaler interface. @@ -115,3 +204,20 @@ func (v *Semver) UnmarshalJSON(data []byte) error { *v = version return nil } + +// byVersion implements [sort.Interface] for sorting semantic version strings. +// Copied from Go's semver pkg with minimal modification. +// https://cs.opensource.google/go/x/mod/+/master:semver/semver.go +type byVersion []Semver + +func (vs byVersion) Len() int { return len(vs) } +func (vs byVersion) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] } +func (vs byVersion) Less(i, j int) bool { + cmp := vs[i].Compare(vs[j]) + if cmp != 0 { + return cmp < 0 + } + + // if versions are equal, sort by lexicographic order + return vs[i].String() < vs[j].String() +} diff --git a/internal/semver/semver_test.go b/internal/semver/semver_test.go index 02584032b..30b798514 100644 --- a/internal/semver/semver_test.go +++ b/internal/semver/semver_test.go @@ -7,20 +7,22 @@ SPDX-License-Identifier: AGPL-3.0-only package semver import ( + "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) var ( - v1_18_0 = Semver{Major: 1, Minor: 18, Patch: 0} - v1_18_0Pre = Semver{Major: 1, Minor: 18, Patch: 0, Prerelease: "pre"} - v1_18_0PreExtra = Semver{Major: 1, Minor: 18, Patch: 0, Prerelease: "pre.1"} - v1_19_0 = Semver{Major: 1, Minor: 19, Patch: 0} - v1_18_1 = Semver{Major: 1, Minor: 18, Patch: 1} - v1_20_0 = Semver{Major: 1, Minor: 20, Patch: 0} - v2_0_0 = Semver{Major: 2, Minor: 0, Patch: 0} + v1_18_0 = Semver{major: 1, minor: 18, patch: 0} + v1_18_0Pre = Semver{major: 1, minor: 18, patch: 0, prerelease: "pre"} + v1_18_0PreExtra = Semver{major: 1, minor: 18, patch: 0, prerelease: "pre.1"} + v1_19_0 = Semver{major: 1, minor: 19, patch: 0} + v1_18_1 = Semver{major: 1, minor: 18, patch: 1} + v1_20_0 = Semver{major: 1, minor: 20, patch: 0} + v2_0_0 = Semver{major: 2, minor: 0, patch: 0} ) func TestNewVersion(t *testing.T) { @@ -32,19 +34,19 @@ func TestNewVersion(t *testing.T) { "valid version": { version: "v1.18.0", want: Semver{ - Major: 1, - Minor: 18, - Patch: 0, + major: 1, + minor: 18, + patch: 0, }, wantErr: false, }, "valid version prerelease": { version: "v1.18.0-pre+yyyymmddhhmmss-abcdefabcdef", want: Semver{ - Major: 1, - Minor: 18, - Patch: 0, - Prerelease: "pre", + major: 1, + minor: 18, + patch: 0, + prerelease: "pre", }, wantErr: false, }, @@ -53,27 +55,27 @@ func TestNewVersion(t *testing.T) { "add prefix": { version: "1.18.0", want: Semver{ - Major: 1, - Minor: 18, - Patch: 0, + major: 1, + minor: 18, + patch: 0, }, wantErr: false, }, "only major.minor": { version: "v1.18", want: Semver{ - Major: 1, - Minor: 18, - Patch: 0, + major: 1, + minor: 18, + patch: 0, }, wantErr: false, }, "only major": { version: "v1", want: Semver{ - Major: 1, - Minor: 0, - Patch: 0, + major: 1, + minor: 0, + patch: 0, }, wantErr: false, }, @@ -216,60 +218,59 @@ func TestComparison(t *testing.T) { func TestCanUpgrade(t *testing.T) { testCases := map[string]struct { - version1 Semver - version2 Semver - want bool - wantErr bool + version1 Semver + version2 Semver + wantUpgrade bool }{ "equal": { - version1: v1_18_0, - version2: v1_18_0, - want: false, + version1: v1_18_0, + version2: v1_18_0, + wantUpgrade: false, }, "patch less than": { - version1: v1_18_0, - version2: v1_18_1, - want: true, + version1: v1_18_0, + version2: v1_18_1, + wantUpgrade: true, }, "minor less then": { - version1: v1_18_0, - version2: v1_19_0, - want: true, + version1: v1_18_0, + version2: v1_19_0, + wantUpgrade: true, }, "minor too big drift": { - version1: v1_18_0, - version2: v1_20_0, - want: false, + version1: v1_18_0, + version2: v1_20_0, + wantUpgrade: false, }, "major too big drift": { - version1: v1_18_0, - version2: v2_0_0, - want: false, + version1: v1_18_0, + version2: v2_0_0, + wantUpgrade: false, }, "greater than": { - version1: v1_18_1, - version2: v1_18_0, - want: false, + version1: v1_18_1, + version2: v1_18_0, + wantUpgrade: false, }, "prerelease less than": { - version1: v1_18_0Pre, - version2: v1_18_0, - want: true, + version1: v1_18_0Pre, + version2: v1_18_0, + wantUpgrade: true, }, "prerelease greater than": { - version1: v1_18_0, - version2: v1_18_0Pre, - want: false, + version1: v1_18_0, + version2: v1_18_0Pre, + wantUpgrade: false, }, "prerelease equal": { - version1: v1_18_0Pre, - version2: v1_18_0Pre, - want: false, + version1: v1_18_0Pre, + version2: v1_18_0Pre, + wantUpgrade: false, }, "prerelease extra": { - version1: v1_18_0Pre, - version2: v1_18_0PreExtra, - want: true, + version1: v1_18_0Pre, + version2: v1_18_0PreExtra, + wantUpgrade: true, }, } @@ -277,7 +278,7 @@ func TestCanUpgrade(t *testing.T) { t.Run(name, func(t *testing.T) { assert := assert.New(t) - assert.Equal(tc.want, tc.version2.IsUpgradeTo(tc.version1)) + assert.Equal(tc.wantUpgrade, tc.version2.IsUpgradeTo(tc.version1) == nil) }) } } @@ -304,3 +305,89 @@ func TestNextMinor(t *testing.T) { }) } } + +func TestVersionMarshalYAML(t *testing.T) { + testCases := map[string]struct { + version Semver + want string + }{ + "simple": { + version: Semver{ + major: 1, + minor: 18, + patch: 0, + prerelease: "", + }, + want: "v1.18.0\n", + }, + "with prerelease": { + version: Semver{ + major: 1, + minor: 18, + patch: 0, + prerelease: "pre", + }, + want: "v1.18.0-pre\n", + }, + "empty semver": { + version: Semver{}, + want: "v0.0.0\n", + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + marshalled, err := yaml.Marshal(tc.version) + + require.NoError(t, err) + require.Equal(t, tc.want, string(marshalled)) + + var unmarshalled Semver + err = yaml.Unmarshal(marshalled, &unmarshalled) + require.NoError(t, err) + require.Equal(t, tc.version, unmarshalled) + }) + } +} + +func TestVersionUnmarshalYAML(t *testing.T) { + testCases := map[string]struct { + version []byte + want Semver + wantError bool + }{ + "empty string": { + version: []byte(""), + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + var actual Semver + err := yaml.Unmarshal(tc.version, &actual) + if tc.wantError { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tc.want.Compare(actual), 0, fmt.Sprintf("expected %s, got %s", tc.want, actual)) + }) + } +} + +func TestSort(t *testing.T) { + testCases := map[string]struct { + input []Semver + want []Semver + }{ + "": { + input: []Semver{NewFromInt(2, 0, 0, ""), NewFromInt(0, 0, 0, ""), NewFromInt(1, 5, 0, "aa"), NewFromInt(1, 5, 0, "bb"), NewFromInt(1, 0, 0, "")}, + want: []Semver{NewFromInt(0, 0, 0, ""), NewFromInt(1, 0, 0, ""), NewFromInt(1, 5, 0, "aa"), NewFromInt(1, 5, 0, "bb"), NewFromInt(2, 0, 0, "")}, + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + Sort(tc.input) + require.Equal(t, tc.want, tc.input, fmt.Sprintf("expected %s, got %s", tc.want, tc.input)) + }) + } +} diff --git a/joinservice/cmd/main.go b/joinservice/cmd/main.go index 779fe7250..c8b1bf8d3 100644 --- a/joinservice/cmd/main.go +++ b/joinservice/cmd/main.go @@ -49,7 +49,7 @@ func main() { log := logger.New(logger.JSONLog, logger.VerbosityFromInt(*verbosity)) log.With( - zap.String("version", constants.VersionInfo()), + zap.String("version", constants.BinaryVersion().String()), zap.String("cloudProvider", *provider), zap.String("attestationVariant", *attestationVariant), ).Infof("Constellation Node Join Service") diff --git a/keyservice/cmd/main.go b/keyservice/cmd/main.go index 24c378cb0..d58464da4 100644 --- a/keyservice/cmd/main.go +++ b/keyservice/cmd/main.go @@ -34,7 +34,7 @@ func main() { flag.Parse() log := logger.New(logger.JSONLog, logger.VerbosityFromInt(*verbosity)) - log.With(zap.String("version", constants.VersionInfo())). + log.With(zap.String("version", constants.BinaryVersion().String())). Infof("Constellation Key Management Service") // read master secret and salt diff --git a/verify/cmd/main.go b/verify/cmd/main.go index 9dcbd7159..cd131eabd 100644 --- a/verify/cmd/main.go +++ b/verify/cmd/main.go @@ -26,7 +26,7 @@ func main() { flag.Parse() log := logger.New(logger.JSONLog, logger.VerbosityFromInt(*verbosity)) - log.With(zap.String("version", constants.VersionInfo()), zap.String("attestationVariant", *attestationVariant)). + log.With(zap.String("version", constants.BinaryVersion().String()), zap.String("attestationVariant", *attestationVariant)). Infof("Constellation Verification Service") variant, err := variant.FromString(*attestationVariant)