cli: add version validation and force flag

Version validation checks that the configured versions
are not more than one minor version below the CLI's version.
The validation can be disabled using --force.
This is necessary for now during development as the CLI
does not have a prerelease version, as our images do.
This commit is contained in:
Otto Bittner 2023-01-31 11:45:31 +01:00
parent 3a7b829107
commit f204c24174
29 changed files with 590 additions and 61 deletions

View File

@ -155,7 +155,7 @@ runs:
echo "Creating cluster using config:"
cat constellation-conf.yaml
sudo sh -c 'echo "127.0.0.1 license.confidential.cloud" >> /etc/hosts' || true
constellation create -c ${{ inputs.controlNodesCount }} -w ${{ inputs.workerNodesCount }} --name e2e-test -y
constellation create -c ${{ inputs.controlNodesCount }} -w ${{ inputs.workerNodesCount }} --name e2e-test -y --force
- name: Cdbg deploy
if: inputs.isDebugImage == 'true'
@ -173,14 +173,15 @@ runs:
--info logcollect.github.run-attempt="${{ github.run_attempt }}" \
--info logcollect.github.ref-name="${{ github.ref_name }}" \
--info logcollect.github.sha="${{ github.sha }}" \
--info logcollect.github.runner-os="${{ runner.os }}"
--info logcollect.github.runner-os="${{ runner.os }}" \
--force
echo "::endgroup::"
- name: Constellation init
id: constellation-init
shell: bash
run: |
constellation init
constellation init --force
echo "KUBECONFIG=$(pwd)/constellation-admin.conf" >> $GITHUB_OUTPUT
echo "MASTERSECRET=$(pwd)/constellation-mastersecret.json" >> $GITHUB_OUTPUT

View File

@ -62,7 +62,7 @@ add_custom_target(debugd ALL
# cdbg
#
add_custom_target(cdbg ALL
CGO_ENABLED=0 go build -o ${CMAKE_BINARY_DIR}/cdbg -buildvcs=false -ldflags "-buildid=''"
CGO_ENABLED=0 go build -o ${CMAKE_BINARY_DIR}/cdbg -buildvcs=false -ldflags "-buildid='' -X github.com/edgelesssys/constellation/v2/internal/constants.VersionInfo=${PROJECT_VERSION}"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/debugd/cmd/cdbg
BYPRODUCTS cdbg
)

View File

@ -47,6 +47,7 @@ func NewRootCmd() *cobra.Command {
must(rootCmd.MarkPersistentFlagFilename("config", "yaml"))
rootCmd.PersistentFlags().Bool("debug", false, "enable debug logging")
rootCmd.PersistentFlags().Bool("force", false, "disables version validation errors - might result in corrupted clusters")
rootCmd.AddCommand(cmd.NewConfigCmd())
rootCmd.AddCommand(cmd.NewCreateCmd())

View File

@ -45,6 +45,7 @@ type fetchMeasurementsFlags struct {
measurementsURL *url.URL
signatureURL *url.URL
configPath string
force bool
}
type configFetchMeasurementsCmd struct {
@ -78,9 +79,9 @@ func (cfm *configFetchMeasurementsCmd) configFetchMeasurements(
cfm.log.Debugf("Using flags %v", flags)
cfm.log.Debugf("Loading configuration file from %q", flags.configPath)
conf, err := config.New(fileHandler, flags.configPath)
conf, err := config.New(fileHandler, flags.configPath, flags.force)
if err != nil {
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
return config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
}
if !conf.IsReleaseImage() {
@ -161,10 +162,16 @@ func (cfm *configFetchMeasurementsCmd) parseFetchMeasurementsFlags(cmd *cobra.Co
}
cfm.log.Debugf("Configuration path is %q", config)
force, err := cmd.Flags().GetBool("force")
if err != nil {
return &fetchMeasurementsFlags{}, fmt.Errorf("parsing force argument: %w", err)
}
return &fetchMeasurementsFlags{
measurementsURL: measurementsURL,
signatureURL: measurementsSignatureURL,
configPath: config,
force: force,
}, nil
}

View File

@ -36,6 +36,7 @@ func TestParseFetchMeasurementsFlags(t *testing.T) {
urlFlag string
signatureURLFlag string
configFlag string
forceFlag bool
wantFlags *fetchMeasurementsFlags
wantErr bool
}{
@ -74,6 +75,7 @@ func TestParseFetchMeasurementsFlags(t *testing.T) {
cmd := newConfigFetchMeasurementsCmd()
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
cmd.Flags().Bool("force", false, "") // register persistent flag manually
if tc.urlFlag != "" {
require.NoError(cmd.Flags().Set("url", tc.urlFlag))
@ -243,10 +245,12 @@ func TestConfigFetchMeasurements(t *testing.T) {
cmd := newConfigFetchMeasurementsCmd()
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
cmd.Flags().Bool("force", true, "") // register persistent flag manually
fileHandler := file.NewHandler(afero.NewMemMapFs())
gcpConfig := defaultConfigWithExpectedMeasurements(t, config.Default(), cloudprovider.GCP)
gcpConfig.Image = "v999.999.999"
constants.VersionInfo = "v999.999.999"
err := fileHandler.WriteYAML(constants.ConfigFilename, gcpConfig, file.OptMkdirAll)
require.NoError(err)

View File

@ -1,28 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"errors"
"fmt"
"io"
"go.uber.org/multierr"
)
func displayConfigValidationErrors(errWriter io.Writer, configError error) error {
errs := multierr.Errors(configError)
if errs != nil {
fmt.Fprintln(errWriter, "Problems validating config file:")
for _, err := range errs {
fmt.Fprintln(errWriter, "\t"+err.Error())
}
fmt.Fprintln(errWriter, "Fix the invalid entries or generate a new configuration using `constellation config generate`")
return errors.New("invalid configuration")
}
return nil
}

View File

@ -73,9 +73,9 @@ func (c *createCmd) create(cmd *cobra.Command, creator cloudCreator, fileHandler
}
c.log.Debugf("Loading configuration file from %q", flags.configPath)
conf, err := config.New(fileHandler, flags.configPath)
conf, err := config.New(fileHandler, flags.configPath, flags.force)
if err != nil {
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
return config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
}
c.log.Debugf("Checking configuration for warnings")
@ -201,11 +201,18 @@ func (c *createCmd) parseCreateFlags(cmd *cobra.Command) (createFlags, error) {
}
c.log.Debugf("Configuration path flag is %q", configPath)
force, err := cmd.Flags().GetBool("force")
if err != nil {
return createFlags{}, fmt.Errorf("parsing force argument: %w", err)
}
c.log.Debugf("force flag is %t", force)
return createFlags{
controllerCount: controllerCount,
workerCount: workerCount,
name: name,
configPath: configPath,
force: force,
yes: yes,
}, nil
}
@ -216,6 +223,7 @@ type createFlags struct {
workerCount int
name string
configPath string
force bool
yes bool
}

View File

@ -195,6 +195,8 @@ func TestCreate(t *testing.T) {
cmd.SetErr(&bytes.Buffer{})
cmd.SetIn(bytes.NewBufferString(tc.stdin))
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
cmd.Flags().Bool("force", true, "") // register persistent flag manually
if tc.yesFlag {
require.NoError(cmd.Flags().Set("yes", "true"))
}

View File

@ -93,9 +93,9 @@ func (i *initCmd) initialize(cmd *cobra.Command, newDialer func(validator *cloud
}
i.log.Debugf("Using flags: %+v", flags)
i.log.Debugf("Loading configuration file from %q", flags.configPath)
conf, err := config.New(fileHandler, flags.configPath)
conf, err := config.New(fileHandler, flags.configPath, flags.force)
if err != nil {
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
return config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
}
i.log.Debugf("Checking cluster ID file")
@ -282,10 +282,17 @@ func (i *initCmd) evalFlagArgs(cmd *cobra.Command) (initFlags, error) {
}
i.log.Debugf("Configuration path flag is %q", configPath)
force, err := cmd.Flags().GetBool("force")
if err != nil {
return initFlags{}, fmt.Errorf("parsing force argument: %w", err)
}
i.log.Debugf("force flag is %t", configPath)
return initFlags{
configPath: configPath,
conformance: conformance,
masterSecretPath: masterSecretPath,
force: force,
}, nil
}
@ -294,6 +301,7 @@ type initFlags struct {
configPath string
masterSecretPath string
conformance bool
force bool
}
// readOrGenerateMasterSecret reads a base64 encoded master secret from file or generates a new 32 byte secret.

View File

@ -145,6 +145,7 @@ func TestInitialize(t *testing.T) {
// Flags
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
cmd.Flags().Bool("force", true, "") // register persistent flag manually
// File system preparation
fs := afero.NewMemMapFs()
@ -390,6 +391,7 @@ func TestAttestation(t *testing.T) {
cmd := NewInitCmd()
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
cmd.Flags().Bool("force", true, "") // register persistent flag manually
var out bytes.Buffer
cmd.SetOut(&out)
var errOut bytes.Buffer

View File

@ -179,11 +179,16 @@ func (m *miniUpCmd) prepareConfig(cmd *cobra.Command, fileHandler file.Handler)
if err != nil {
return nil, err
}
force, err := cmd.Flags().GetBool("force")
if err != nil {
return nil, fmt.Errorf("parsing force argument: %w", err)
}
// check for existing config
if configPath != "" {
conf, err := config.New(fileHandler, configPath)
conf, err := config.New(fileHandler, configPath, force)
if err != nil {
return nil, displayConfigValidationErrors(cmd.ErrOrStderr(), err)
return nil, config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
}
if conf.GetProvider() != cloudprovider.QEMU {
return nil, errors.New("invalid provider for MiniConstellation cluster")

View File

@ -79,9 +79,9 @@ func (r *recoverCmd) recover(
}
r.log.Debugf("Loading configuration file from %q", flags.configPath)
conf, err := config.New(fileHandler, flags.configPath)
conf, err := config.New(fileHandler, flags.configPath, flags.force)
if err != nil {
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
return config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
}
provider := conf.GetProvider()
r.log.Debugf("Got provider %s", provider.String())
@ -202,6 +202,7 @@ type recoverFlags struct {
endpoint string
secretPath string
configPath string
force bool
}
func (r *recoverCmd) parseRecoverFlags(cmd *cobra.Command, fileHandler file.Handler) (recoverFlags, error) {
@ -232,10 +233,16 @@ func (r *recoverCmd) parseRecoverFlags(cmd *cobra.Command, fileHandler file.Hand
}
r.log.Debugf("Configuration path flag is %s", configPath)
force, err := cmd.Flags().GetBool("force")
if err != nil {
return recoverFlags{}, fmt.Errorf("parsing force argument: %w", err)
}
return recoverFlags{
endpoint: endpoint,
secretPath: masterSecretPath,
configPath: configPath,
force: force,
}, nil
}

View File

@ -140,6 +140,7 @@ func TestRecover(t *testing.T) {
cmd := NewRecoverCmd()
cmd.SetContext(context.Background())
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
cmd.Flags().Bool("force", true, "") // register persistent flag manually
out := &bytes.Buffer{}
cmd.SetOut(out)
cmd.SetErr(out)
@ -225,6 +226,7 @@ func TestParseRecoverFlags(t *testing.T) {
cmd := NewRecoverCmd()
cmd.Flags().String("config", "", "") // register persistent flag manually
cmd.Flags().Bool("force", false, "") // register persistent flag manually
require.NoError(cmd.ParseFlags(tc.args))
fileHandler := file.NewHandler(afero.NewMemMapFs())

View File

@ -69,9 +69,9 @@ func upgradeExecute(cmd *cobra.Command, imageFetcher imageFetcher, upgrader clou
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
conf, err := config.New(fileHandler, flags.configPath)
conf, err := config.New(fileHandler, flags.configPath, flags.force)
if err != nil {
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
return config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
}
if flags.helmFlag {
@ -130,7 +130,13 @@ func parseUpgradeExecuteFlags(cmd *cobra.Command) (upgradeExecuteFlags, error) {
if err != nil {
return upgradeExecuteFlags{}, err
}
return upgradeExecuteFlags{configPath: configPath, helmFlag: helmFlag, yes: yes, upgradeTimeout: timeout}, nil
force, err := cmd.Flags().GetBool("force")
if err != nil {
return upgradeExecuteFlags{}, fmt.Errorf("parsing force argument: %w", err)
}
return upgradeExecuteFlags{configPath: configPath, helmFlag: helmFlag, yes: yes, upgradeTimeout: timeout, force: force}, nil
}
type upgradeExecuteFlags struct {
@ -138,6 +144,7 @@ type upgradeExecuteFlags struct {
helmFlag bool
yes bool
upgradeTimeout time.Duration
force bool
}
type cloudUpgrader interface {

View File

@ -51,6 +51,7 @@ func TestUpgradeExecute(t *testing.T) {
require := require.New(t)
cmd := newUpgradeExecuteCmd()
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
cmd.Flags().Bool("force", true, "") // register persistent flag manually
handler := file.NewHandler(afero.NewMemMapFs())
cfg := defaultConfigWithExpectedMeasurements(t, config.Default(), cloudprovider.Azure)

View File

@ -79,9 +79,9 @@ func (up *upgradePlanCmd) upgradePlan(cmd *cobra.Command, planner upgradePlanner
fileHandler file.Handler, client *http.Client, rekor rekorVerifier, flags upgradePlanFlags,
cliVersion string,
) error {
conf, err := config.New(fileHandler, flags.configPath)
conf, err := config.New(fileHandler, flags.configPath, true)
if err != nil {
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
return config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
}
up.log.Debugf("Read configuration from %q", flags.configPath)
// get current image version of the cluster

View File

@ -75,9 +75,9 @@ func (v *verifyCmd) verify(cmd *cobra.Command, fileHandler file.Handler, verifyC
v.log.Debugf("Using flags: %+v", flags)
v.log.Debugf("Loading configuration file from %q", flags.configPath)
conf, err := config.New(fileHandler, flags.configPath)
conf, err := config.New(fileHandler, flags.configPath, flags.force)
if err != nil {
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
return config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
}
provider := conf.GetProvider()
@ -133,6 +133,12 @@ func (v *verifyCmd) parseVerifyFlags(cmd *cobra.Command, fileHandler file.Handle
}
v.log.Debugf("'node-endpoint' flag is %q", endpoint)
force, err := cmd.Flags().GetBool("force")
if err != nil {
return verifyFlags{}, fmt.Errorf("parsing force argument: %w", err)
}
v.log.Debugf("'force' flag is %t", force)
// Get empty values from ID file
emptyEndpoint := endpoint == ""
emptyIDs := ownerID == "" && clusterID == ""
@ -168,6 +174,7 @@ func (v *verifyCmd) parseVerifyFlags(cmd *cobra.Command, fileHandler file.Handle
configPath: configPath,
ownerID: ownerID,
clusterID: clusterID,
force: force,
}, nil
}
@ -176,6 +183,7 @@ type verifyFlags struct {
ownerID string
clusterID string
configPath string
force bool
}
func addPortIfMissing(endpoint string, defaultPort int) (string, error) {

View File

@ -141,6 +141,7 @@ func TestVerify(t *testing.T) {
cmd := NewVerifyCmd()
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
cmd.Flags().Bool("force", true, "") // register persistent flag manually
out := &bytes.Buffer{}
cmd.SetOut(out)
cmd.SetErr(&bytes.Buffer{})

View File

@ -58,13 +58,18 @@ func runDeploy(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("parsing config path argument: %w", err)
}
force, err := cmd.Flags().GetBool("force")
if err != nil {
return fmt.Errorf("getting force flag: %w", err)
}
fs := afero.NewOsFs()
fileHandler := file.NewHandler(fs)
streamer := streamer.New(fs)
transfer := filetransfer.New(log, streamer, filetransfer.ShowProgress)
constellationConfig, err := config.FromFile(fileHandler, configName)
constellationConfig, err := config.New(fileHandler, configName, force)
if err != nil {
return err
return config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
}
return deploy(cmd, fileHandler, constellationConfig, transfer, log)

View File

@ -22,6 +22,8 @@ func newRootCmd() *cobra.Command {
It connects to Constellation instances running debugd and deploys a self-compiled version of the bootstrapper.`,
}
cmd.PersistentFlags().String("config", constants.ConfigFilename, "Constellation config file")
cmd.PersistentFlags().Bool("force", false, "disables version validation errors - might result in corrupted clusters")
cmd.AddCommand(newDeployCmd())
return cmd
}

1
go.mod
View File

@ -86,6 +86,7 @@ require (
github.com/spf13/afero v1.9.3
github.com/spf13/cobra v1.6.1
github.com/stretchr/testify v1.8.1
github.com/tj/assert v0.0.0-20171129193455-018094318fb0
go.uber.org/goleak v1.2.0
go.uber.org/multierr v1.9.0
go.uber.org/zap v1.24.0

1
go.sum
View File

@ -1310,6 +1310,7 @@ github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhV
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0 h1:Rw8kxzWo1mr6FSaYXjQELRe88y2KdfynXdnK72rdjtA=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=

View File

@ -1305,6 +1305,7 @@ github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhV
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0 h1:Rw8kxzWo1mr6FSaYXjQELRe88y2KdfynXdnK72rdjtA=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=

View File

@ -0,0 +1,139 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
/*
Package compatibility offers helper functions for comparing and filtering versions.
*/
package compatibility
import (
"errors"
"fmt"
"strings"
"github.com/edgelesssys/constellation/v2/internal/constants"
"golang.org/x/mod/semver"
)
var (
// ErrMajorMismatch signals that the major version of two compared versions don't match.
ErrMajorMismatch = errors.New("missmatching major version")
// ErrMinorDrift signals that the minor version of two compared versions are further apart than one.
ErrMinorDrift = errors.New("target version needs to be equal or up to one minor version higher")
// ErrSemVer signals that a given version does not adhere to the Semver syntax.
ErrSemVer = errors.New("invalid semver")
)
// ensurePrefixV returns the input string prefixed with the letter "v", if the string doesn't already start with that letter.
func ensurePrefixV(str string) string {
if strings.HasPrefix(str, "v") {
return str
}
return "v" + str
}
// IsValidUpgrade checks that a and b adhere to a version drift of 1 and b is greater than a.
func IsValidUpgrade(a, b string) error {
a = ensurePrefixV(a)
b = ensurePrefixV(b)
if !semver.IsValid(a) || !semver.IsValid(b) {
return ErrSemVer
}
if semver.Compare(a, b) >= 0 {
return errors.New("current version newer than new version")
}
aMajor, aMinor, err := parseCanonicalSemver(a)
if err != nil {
return err
}
bMajor, bMinor, err := parseCanonicalSemver(b)
if err != nil {
return err
}
if aMajor != bMajor {
return ErrMajorMismatch
}
if bMinor-aMinor > 1 {
return ErrMinorDrift
}
return nil
}
// BinaryWith tests that this binarie's version is greater or equal than some target version, but not further away than one minor version.
func BinaryWith(target string) error {
binaryVersion := ensurePrefixV(constants.VersionInfo)
target = ensurePrefixV(target)
if !semver.IsValid(binaryVersion) || !semver.IsValid(target) {
return ErrSemVer
}
cliMajor, cliMinor, err := parseCanonicalSemver(binaryVersion)
if err != nil {
return err
}
targetMajor, targetMinor, err := parseCanonicalSemver(target)
if err != nil {
return err
}
// Major versions always have to match.
if cliMajor != targetMajor {
return ErrMajorMismatch
}
if semver.Compare(binaryVersion, target) == -1 {
return ErrMinorDrift
}
// Abort if minor version drift between CLI and versionA value is greater than 1.
if cliMinor-targetMinor > 1 {
return ErrMinorDrift
}
return nil
}
// FilterNewerVersion filters the list of versions to only include versions newer than currentVersion.
func FilterNewerVersion(currentVersion string, newVersions []string) []string {
currentVersion = ensurePrefixV(currentVersion)
var result []string
for _, image := range newVersions {
image = ensurePrefixV(image)
// check if image is newer than current version
if semver.Compare(image, currentVersion) <= 0 {
continue
}
result = append(result, image)
}
return result
}
// NextMinorVersion returns the next minor version for a given canonical semver.
// The returned format is vMAJOR.MINOR.
func NextMinorVersion(version string) (string, error) {
major, minor, err := parseCanonicalSemver(ensurePrefixV(version))
if err != nil {
return "", err
}
return fmt.Sprintf("v%d.%d", major, minor+1), nil
}
func parseCanonicalSemver(version string) (major int, minor int, err error) {
version = semver.Canonical(version) // ensure version is in canonical form (vX.Y.Z)
num, err := fmt.Sscanf(version, "v%d.%d", &major, &minor)
if err != nil {
return 0, 0, fmt.Errorf("parsing version: %w", err)
}
if num != 2 {
return 0, 0, fmt.Errorf("parsing version: expected 3 numbers, got %d", num)
}
return major, minor, nil
}

View File

@ -0,0 +1,208 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package compatibility
import (
"testing"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/tj/assert"
)
func TestFilterNewerVersion(t *testing.T) {
imageList := []string{
"v0.0.0",
"v1.0.0",
"v1.0.1",
"v1.0.2",
"v1.1.0",
}
testCases := map[string]struct {
images []string
version string
wantImages []string
}{
"filters <= v1.0.0": {
images: imageList,
version: "v1.0.0",
wantImages: []string{
"v1.0.1",
"v1.0.2",
"v1.1.0",
},
},
"no compatible images": {
images: imageList,
version: "v999.999.999",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
compatibleImages := FilterNewerVersion(tc.version, tc.images)
assert.EqualValues(tc.wantImages, compatibleImages)
})
}
}
func TestNextMinorVersion(t *testing.T) {
testCases := map[string]struct {
version string
wantNextMinorVersion string
wantErr bool
}{
"gets next": {
version: "v1.0.0",
wantNextMinorVersion: "v1.1",
},
"gets next from minor version": {
version: "v1.0",
wantNextMinorVersion: "v1.1",
},
"empty version": {
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
gotNext, err := NextMinorVersion(tc.version)
if tc.wantErr {
assert.Error(err)
return
}
assert.NoError(err)
assert.Equal(tc.wantNextMinorVersion, gotNext)
})
}
}
func TestCLIWith(t *testing.T) {
testCases := map[string]struct {
cli string
target string
wantError bool
}{
"success": {
cli: "v0.0.0",
target: "v0.0.0",
},
"different major version": {
cli: "v1",
target: "v2",
wantError: true,
},
"major version diff too large": {
cli: "v1.0",
target: "v1.2",
wantError: true,
},
"a has to be the newer version": {
cli: "v2.4.0",
target: "v2.5.0",
wantError: true,
},
"pre prelease version ordering is correct": {
cli: "v2.5.0-pre",
target: "v2.4.0",
},
"pre prelease version ordering is correct #2": {
cli: "v2.4.0",
target: "v2.5.0-pre",
wantError: true,
},
"pre release versions match": {
cli: "v2.6.0-pre",
target: "v2.6.0-pre",
},
"pseudo version is newer than first pre release": {
cli: "v2.6.0-pre",
target: "v2.6.0-pre.0.20230125085856-aaaaaaaaaaaa",
wantError: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
constants.VersionInfo = tc.cli
err := BinaryWith(tc.target)
if tc.wantError {
assert.Error(err)
return
}
assert.NoError(err)
})
}
}
func TestIsValidUpgrade(t *testing.T) {
testCases := map[string]struct {
a string
b string
wantError bool
}{
"success": {
a: "v0.0.0",
b: "v0.1.0",
},
"different major version": {
a: "v1",
b: "v2",
wantError: true,
},
"minor version diff too large": {
a: "v1.0",
b: "v1.2",
wantError: true,
},
"b has to be the newer version": {
a: "v2.5.0",
b: "v2.4.0",
wantError: true,
},
"pre prelease version ordering is correct": {
a: "v2.4.0",
b: "v2.5.0-pre",
},
"wrong pre release ordering creates error": {
a: "v2.5.0-pre",
b: "v2.4.0",
wantError: true,
},
"pre release versions are equal": {
a: "v2.6.0-pre",
b: "v2.6.0-pre",
wantError: true,
},
"pseudo version is newer than first pre release": {
a: "v2.6.0-pre.0.20230125085856-aaaaaaaaaaaa",
b: "v2.6.0-pre",
wantError: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
err := IsValidUpgrade(tc.a, tc.b)
if tc.wantError {
assert.Error(err)
return
}
assert.NoError(err)
})
}
}

View File

@ -58,7 +58,7 @@ type Config struct {
Version string `yaml:"version" validate:"eq=v2"`
// description: |
// Machine image used to create Constellation nodes.
Image string `yaml:"image" validate:"required"`
Image string `yaml:"image" validate:"required,version_compatibility"`
// description: |
// Size (in GB) of a node's disk to store the non-volatile state.
StateDiskSizeGB int `yaml:"stateDiskSizeGB" validate:"min=0"`
@ -317,7 +317,7 @@ func FromFile(fileHandler file.Handler, name string) (*Config, error) {
// 1. Reading config file via provided fileHandler from file with name.
// 2. Read secrets from environment variables.
// 3. Validate config.
func New(fileHandler file.Handler, name string) (*Config, error) {
func New(fileHandler file.Handler, name string, force bool) (*Config, error) {
// Read config file
c, err := FromFile(fileHandler, name)
if err != nil {
@ -330,7 +330,7 @@ func New(fileHandler file.Handler, name string) (*Config, error) {
c.Provider.Azure.ClientSecretValue = clientSecretValue
}
return c, c.Validate()
return c, c.Validate(force)
}
// HasProvider checks whether the config contains the provider.
@ -456,7 +456,7 @@ func (c *Config) DeployCSIDriver() bool {
}
// Validate checks the config values and returns validation errors.
func (c *Config) Validate() error {
func (c *Config) Validate(force bool) error {
trans := ut.New(en.New()).GetFallback()
validate := validator.New()
if err := en_translations.RegisterDefaultTranslations(validate, trans); err != nil {
@ -493,6 +493,14 @@ func (c *Config) Validate() error {
return err
}
if err := validate.RegisterTranslation("version_compatibility", trans, registerVersionCompatibilityError, translateVersionCompatibilityError); err != nil {
return err
}
if err := validate.RegisterTranslation("supported_k8s_version", trans, registerInvalidK8sVersionError, translateInvalidK8sVersionError); err != nil {
return err
}
if err := validate.RegisterValidation("no_placeholders", validateNoPlaceholder); err != nil {
return err
}
@ -502,6 +510,14 @@ func (c *Config) Validate() error {
return err
}
versionCompatibilityValidator := validateVersionCompatibility
if force {
versionCompatibilityValidator = returnsTrue
}
if err := validate.RegisterValidation("version_compatibility", versionCompatibilityValidator); err != nil {
return err
}
// register custom validator with label aws_instance_type to validate the AWS instance type from config input.
if err := validate.RegisterValidation("aws_instance_type", validateAWSInstanceType); err != nil {
return err

View File

@ -173,7 +173,7 @@ func TestNewWithDefaultOptions(t *testing.T) {
}
// Test
c, err := New(fileHandler, constants.ConfigFilename)
c, err := New(fileHandler, constants.ConfigFilename, false)
if tc.wantErr {
assert.Error(err)
return
@ -279,7 +279,7 @@ func TestValidate(t *testing.T) {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
err := tc.cnf.Validate()
err := tc.cnf.Validate(false)
if tc.wantErr {
assert.Error(err)
assert.Len(multierr.Errors(err), tc.wantErrCount)

View File

@ -8,25 +8,41 @@ package config
import (
"bytes"
"errors"
"fmt"
"io"
"sort"
"strings"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/compatibility"
"github.com/edgelesssys/constellation/v2/internal/config/instancetypes"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/versions"
"github.com/edgelesssys/constellation/v2/internal/versionsapi"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
"go.uber.org/multierr"
"golang.org/x/mod/semver"
)
func validateK8sVersion(fl validator.FieldLevel) bool {
return versions.IsSupportedK8sVersion(fl.Field().String())
// DisplayValidationErrors shows all validation errors inside configError as one formatted string.
func DisplayValidationErrors(errWriter io.Writer, configError error) error {
errs := multierr.Errors(configError)
if errs != nil {
fmt.Fprintln(errWriter, "Problems validating config file:")
for _, err := range errs {
fmt.Fprintln(errWriter, "\t"+err.Error())
}
fmt.Fprintln(errWriter, "Fix the invalid entries or generate a new configuration using `constellation config generate`")
return errors.New("invalid configuration")
}
return nil
}
func registerInvalidK8sVersionError(ut ut.Translator) error {
return ut.Add("invalid_k8s_version", "{0} specifies an unsupported Kubernetes version. {1}", true)
return ut.Add("supported_k8s_version", "{0} specifies an unsupported Kubernetes version. {1}", true)
}
func translateInvalidK8sVersionError(ut ut.Translator, fe validator.FieldError) string {
@ -55,7 +71,7 @@ func translateInvalidK8sVersionError(ut ut.Translator, fe validator.FieldError)
errorMsg = fmt.Sprintf("The configured version %s is newer than the newest version supported by this CLI: %s.", configured, maxVersion)
}
t, _ := ut.T("invalid_k8s_version", fe.Field(), errorMsg)
t, _ := ut.T("supported_k8s_version", fe.Field(), errorMsg)
return t
}
@ -281,3 +297,56 @@ func getPlaceholderEntries(m Measurements) []uint32 {
return placeholders
}
func validateK8sVersion(fl validator.FieldLevel) bool {
return versions.IsSupportedK8sVersion(fl.Field().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 {
err := validateVersionCompatibilityHelper(fe.Field(), fe.Value().(string))
var msg string
switch err {
case compatibility.ErrSemVer:
msg = fmt.Sprintf("configured version (%s) does not adhere to SemVer syntax", fe.Value().(string))
case compatibility.ErrMajorMismatch:
msg = fmt.Sprintf("the CLI's major version (%s) has to match your configured major version (%s)", constants.VersionInfo, fe.Value().(string))
case compatibility.ErrMinorDrift:
msg = fmt.Sprintf("only the CLI (%s) can be up to one minor version newer than the configured version (%s)", constants.VersionInfo, fe.Value().(string))
default:
msg = err.Error()
}
t, _ := ut.T("version_compatibility", fe.Field(), msg)
return t
}
// Check that the validated field and the CLI version are not more than one minor version apart.
func validateVersionCompatibility(fl validator.FieldLevel) bool {
if err := validateVersionCompatibilityHelper(fl.FieldName(), fl.Field().String()); err != nil {
return false
}
return true
}
func validateVersionCompatibilityHelper(fieldName string, configuredVersion string) error {
if fieldName == "Image" {
imageVersion, err := versionsapi.NewVersionFromShortPath(configuredVersion, versionsapi.VersionKindImage)
if err != nil {
return err
}
configuredVersion = imageVersion.Version
}
return compatibility.BinaryWith(configuredVersion)
}
func returnsTrue(fl validator.FieldLevel) bool {
return true
}

View File

@ -0,0 +1,51 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package config
import (
"testing"
"github.com/edgelesssys/constellation/v2/internal/constants"
"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
target string
wantError bool
}{
"full version works": {
cli: "v0.1.0",
target: "v0.0.0",
},
"short path works": {
cli: "v0.1.0",
target: "ref/main/stream/debug/v0.0.0-pre.0.20230109121528-d24fac00f018",
},
"minor version difference > 1": {
cli: "0.0.0",
target: "ref/main/stream/debug/v0.2.0-pre.0.20230109121528-d24fac00f018",
wantError: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
constants.VersionInfo = tc.cli
err := validateVersionCompatibilityHelper("Image", tc.target)
if tc.wantError {
assert.Error(err)
return
}
assert.NoError(err)
})
}
}