mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-05-02 22:34:56 -04:00
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:
parent
3a7b829107
commit
f204c24174
29 changed files with 590 additions and 61 deletions
|
@ -155,7 +155,7 @@ runs:
|
||||||
echo "Creating cluster using config:"
|
echo "Creating cluster using config:"
|
||||||
cat constellation-conf.yaml
|
cat constellation-conf.yaml
|
||||||
sudo sh -c 'echo "127.0.0.1 license.confidential.cloud" >> /etc/hosts' || true
|
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
|
- name: Cdbg deploy
|
||||||
if: inputs.isDebugImage == 'true'
|
if: inputs.isDebugImage == 'true'
|
||||||
|
@ -173,14 +173,15 @@ runs:
|
||||||
--info logcollect.github.run-attempt="${{ github.run_attempt }}" \
|
--info logcollect.github.run-attempt="${{ github.run_attempt }}" \
|
||||||
--info logcollect.github.ref-name="${{ github.ref_name }}" \
|
--info logcollect.github.ref-name="${{ github.ref_name }}" \
|
||||||
--info logcollect.github.sha="${{ github.sha }}" \
|
--info logcollect.github.sha="${{ github.sha }}" \
|
||||||
--info logcollect.github.runner-os="${{ runner.os }}"
|
--info logcollect.github.runner-os="${{ runner.os }}" \
|
||||||
|
--force
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
|
|
||||||
- name: Constellation init
|
- name: Constellation init
|
||||||
id: constellation-init
|
id: constellation-init
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
constellation init
|
constellation init --force
|
||||||
echo "KUBECONFIG=$(pwd)/constellation-admin.conf" >> $GITHUB_OUTPUT
|
echo "KUBECONFIG=$(pwd)/constellation-admin.conf" >> $GITHUB_OUTPUT
|
||||||
echo "MASTERSECRET=$(pwd)/constellation-mastersecret.json" >> $GITHUB_OUTPUT
|
echo "MASTERSECRET=$(pwd)/constellation-mastersecret.json" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,7 @@ add_custom_target(debugd ALL
|
||||||
# cdbg
|
# cdbg
|
||||||
#
|
#
|
||||||
add_custom_target(cdbg ALL
|
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
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/debugd/cmd/cdbg
|
||||||
BYPRODUCTS cdbg
|
BYPRODUCTS cdbg
|
||||||
)
|
)
|
||||||
|
|
|
@ -47,6 +47,7 @@ func NewRootCmd() *cobra.Command {
|
||||||
must(rootCmd.MarkPersistentFlagFilename("config", "yaml"))
|
must(rootCmd.MarkPersistentFlagFilename("config", "yaml"))
|
||||||
|
|
||||||
rootCmd.PersistentFlags().Bool("debug", false, "enable debug logging")
|
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.NewConfigCmd())
|
||||||
rootCmd.AddCommand(cmd.NewCreateCmd())
|
rootCmd.AddCommand(cmd.NewCreateCmd())
|
||||||
|
|
|
@ -45,6 +45,7 @@ type fetchMeasurementsFlags struct {
|
||||||
measurementsURL *url.URL
|
measurementsURL *url.URL
|
||||||
signatureURL *url.URL
|
signatureURL *url.URL
|
||||||
configPath string
|
configPath string
|
||||||
|
force bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type configFetchMeasurementsCmd struct {
|
type configFetchMeasurementsCmd struct {
|
||||||
|
@ -78,9 +79,9 @@ func (cfm *configFetchMeasurementsCmd) configFetchMeasurements(
|
||||||
cfm.log.Debugf("Using flags %v", flags)
|
cfm.log.Debugf("Using flags %v", flags)
|
||||||
|
|
||||||
cfm.log.Debugf("Loading configuration file from %q", flags.configPath)
|
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 {
|
if err != nil {
|
||||||
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
|
return config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !conf.IsReleaseImage() {
|
if !conf.IsReleaseImage() {
|
||||||
|
@ -161,10 +162,16 @@ func (cfm *configFetchMeasurementsCmd) parseFetchMeasurementsFlags(cmd *cobra.Co
|
||||||
}
|
}
|
||||||
cfm.log.Debugf("Configuration path is %q", config)
|
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{
|
return &fetchMeasurementsFlags{
|
||||||
measurementsURL: measurementsURL,
|
measurementsURL: measurementsURL,
|
||||||
signatureURL: measurementsSignatureURL,
|
signatureURL: measurementsSignatureURL,
|
||||||
configPath: config,
|
configPath: config,
|
||||||
|
force: force,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ func TestParseFetchMeasurementsFlags(t *testing.T) {
|
||||||
urlFlag string
|
urlFlag string
|
||||||
signatureURLFlag string
|
signatureURLFlag string
|
||||||
configFlag string
|
configFlag string
|
||||||
|
forceFlag bool
|
||||||
wantFlags *fetchMeasurementsFlags
|
wantFlags *fetchMeasurementsFlags
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
|
@ -74,6 +75,7 @@ func TestParseFetchMeasurementsFlags(t *testing.T) {
|
||||||
|
|
||||||
cmd := newConfigFetchMeasurementsCmd()
|
cmd := newConfigFetchMeasurementsCmd()
|
||||||
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
|
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
|
||||||
|
cmd.Flags().Bool("force", false, "") // register persistent flag manually
|
||||||
|
|
||||||
if tc.urlFlag != "" {
|
if tc.urlFlag != "" {
|
||||||
require.NoError(cmd.Flags().Set("url", tc.urlFlag))
|
require.NoError(cmd.Flags().Set("url", tc.urlFlag))
|
||||||
|
@ -243,10 +245,12 @@ func TestConfigFetchMeasurements(t *testing.T) {
|
||||||
|
|
||||||
cmd := newConfigFetchMeasurementsCmd()
|
cmd := newConfigFetchMeasurementsCmd()
|
||||||
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
|
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
|
||||||
|
cmd.Flags().Bool("force", true, "") // register persistent flag manually
|
||||||
fileHandler := file.NewHandler(afero.NewMemMapFs())
|
fileHandler := file.NewHandler(afero.NewMemMapFs())
|
||||||
|
|
||||||
gcpConfig := defaultConfigWithExpectedMeasurements(t, config.Default(), cloudprovider.GCP)
|
gcpConfig := defaultConfigWithExpectedMeasurements(t, config.Default(), cloudprovider.GCP)
|
||||||
gcpConfig.Image = "v999.999.999"
|
gcpConfig.Image = "v999.999.999"
|
||||||
|
constants.VersionInfo = "v999.999.999"
|
||||||
|
|
||||||
err := fileHandler.WriteYAML(constants.ConfigFilename, gcpConfig, file.OptMkdirAll)
|
err := fileHandler.WriteYAML(constants.ConfigFilename, gcpConfig, file.OptMkdirAll)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -73,9 +73,9 @@ func (c *createCmd) create(cmd *cobra.Command, creator cloudCreator, fileHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
c.log.Debugf("Loading configuration file from %q", flags.configPath)
|
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 {
|
if err != nil {
|
||||||
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
|
return config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.log.Debugf("Checking configuration for warnings")
|
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)
|
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{
|
return createFlags{
|
||||||
controllerCount: controllerCount,
|
controllerCount: controllerCount,
|
||||||
workerCount: workerCount,
|
workerCount: workerCount,
|
||||||
name: name,
|
name: name,
|
||||||
configPath: configPath,
|
configPath: configPath,
|
||||||
|
force: force,
|
||||||
yes: yes,
|
yes: yes,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -216,6 +223,7 @@ type createFlags struct {
|
||||||
workerCount int
|
workerCount int
|
||||||
name string
|
name string
|
||||||
configPath string
|
configPath string
|
||||||
|
force bool
|
||||||
yes bool
|
yes bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -195,6 +195,8 @@ func TestCreate(t *testing.T) {
|
||||||
cmd.SetErr(&bytes.Buffer{})
|
cmd.SetErr(&bytes.Buffer{})
|
||||||
cmd.SetIn(bytes.NewBufferString(tc.stdin))
|
cmd.SetIn(bytes.NewBufferString(tc.stdin))
|
||||||
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
|
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
|
||||||
|
cmd.Flags().Bool("force", true, "") // register persistent flag manually
|
||||||
|
|
||||||
if tc.yesFlag {
|
if tc.yesFlag {
|
||||||
require.NoError(cmd.Flags().Set("yes", "true"))
|
require.NoError(cmd.Flags().Set("yes", "true"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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("Using flags: %+v", flags)
|
||||||
i.log.Debugf("Loading configuration file from %q", flags.configPath)
|
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 {
|
if err != nil {
|
||||||
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
|
return config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
i.log.Debugf("Checking cluster ID file")
|
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)
|
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{
|
return initFlags{
|
||||||
configPath: configPath,
|
configPath: configPath,
|
||||||
conformance: conformance,
|
conformance: conformance,
|
||||||
masterSecretPath: masterSecretPath,
|
masterSecretPath: masterSecretPath,
|
||||||
|
force: force,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -294,6 +301,7 @@ type initFlags struct {
|
||||||
configPath string
|
configPath string
|
||||||
masterSecretPath string
|
masterSecretPath string
|
||||||
conformance bool
|
conformance bool
|
||||||
|
force bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// readOrGenerateMasterSecret reads a base64 encoded master secret from file or generates a new 32 byte secret.
|
// readOrGenerateMasterSecret reads a base64 encoded master secret from file or generates a new 32 byte secret.
|
||||||
|
|
|
@ -145,6 +145,7 @@ func TestInitialize(t *testing.T) {
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
|
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
|
||||||
|
cmd.Flags().Bool("force", true, "") // register persistent flag manually
|
||||||
|
|
||||||
// File system preparation
|
// File system preparation
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
|
@ -390,6 +391,7 @@ func TestAttestation(t *testing.T) {
|
||||||
|
|
||||||
cmd := NewInitCmd()
|
cmd := NewInitCmd()
|
||||||
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
|
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
|
||||||
|
cmd.Flags().Bool("force", true, "") // register persistent flag manually
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
cmd.SetOut(&out)
|
cmd.SetOut(&out)
|
||||||
var errOut bytes.Buffer
|
var errOut bytes.Buffer
|
||||||
|
|
|
@ -179,11 +179,16 @@ func (m *miniUpCmd) prepareConfig(cmd *cobra.Command, fileHandler file.Handler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// check for existing config
|
||||||
if configPath != "" {
|
if configPath != "" {
|
||||||
conf, err := config.New(fileHandler, configPath)
|
conf, err := config.New(fileHandler, configPath, force)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, displayConfigValidationErrors(cmd.ErrOrStderr(), err)
|
return nil, config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
|
||||||
}
|
}
|
||||||
if conf.GetProvider() != cloudprovider.QEMU {
|
if conf.GetProvider() != cloudprovider.QEMU {
|
||||||
return nil, errors.New("invalid provider for MiniConstellation cluster")
|
return nil, errors.New("invalid provider for MiniConstellation cluster")
|
||||||
|
|
|
@ -79,9 +79,9 @@ func (r *recoverCmd) recover(
|
||||||
}
|
}
|
||||||
|
|
||||||
r.log.Debugf("Loading configuration file from %q", flags.configPath)
|
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 {
|
if err != nil {
|
||||||
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
|
return config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
|
||||||
}
|
}
|
||||||
provider := conf.GetProvider()
|
provider := conf.GetProvider()
|
||||||
r.log.Debugf("Got provider %s", provider.String())
|
r.log.Debugf("Got provider %s", provider.String())
|
||||||
|
@ -202,6 +202,7 @@ type recoverFlags struct {
|
||||||
endpoint string
|
endpoint string
|
||||||
secretPath string
|
secretPath string
|
||||||
configPath string
|
configPath string
|
||||||
|
force bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *recoverCmd) parseRecoverFlags(cmd *cobra.Command, fileHandler file.Handler) (recoverFlags, error) {
|
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)
|
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{
|
return recoverFlags{
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
secretPath: masterSecretPath,
|
secretPath: masterSecretPath,
|
||||||
configPath: configPath,
|
configPath: configPath,
|
||||||
|
force: force,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -140,6 +140,7 @@ func TestRecover(t *testing.T) {
|
||||||
cmd := NewRecoverCmd()
|
cmd := NewRecoverCmd()
|
||||||
cmd.SetContext(context.Background())
|
cmd.SetContext(context.Background())
|
||||||
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
|
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
|
||||||
|
cmd.Flags().Bool("force", true, "") // register persistent flag manually
|
||||||
out := &bytes.Buffer{}
|
out := &bytes.Buffer{}
|
||||||
cmd.SetOut(out)
|
cmd.SetOut(out)
|
||||||
cmd.SetErr(out)
|
cmd.SetErr(out)
|
||||||
|
@ -225,6 +226,7 @@ func TestParseRecoverFlags(t *testing.T) {
|
||||||
|
|
||||||
cmd := NewRecoverCmd()
|
cmd := NewRecoverCmd()
|
||||||
cmd.Flags().String("config", "", "") // register persistent flag manually
|
cmd.Flags().String("config", "", "") // register persistent flag manually
|
||||||
|
cmd.Flags().Bool("force", false, "") // register persistent flag manually
|
||||||
require.NoError(cmd.ParseFlags(tc.args))
|
require.NoError(cmd.ParseFlags(tc.args))
|
||||||
|
|
||||||
fileHandler := file.NewHandler(afero.NewMemMapFs())
|
fileHandler := file.NewHandler(afero.NewMemMapFs())
|
||||||
|
|
|
@ -69,9 +69,9 @@ func upgradeExecute(cmd *cobra.Command, imageFetcher imageFetcher, upgrader clou
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parsing flags: %w", err)
|
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 {
|
if err != nil {
|
||||||
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
|
return config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if flags.helmFlag {
|
if flags.helmFlag {
|
||||||
|
@ -130,7 +130,13 @@ func parseUpgradeExecuteFlags(cmd *cobra.Command) (upgradeExecuteFlags, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return upgradeExecuteFlags{}, err
|
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 {
|
type upgradeExecuteFlags struct {
|
||||||
|
@ -138,6 +144,7 @@ type upgradeExecuteFlags struct {
|
||||||
helmFlag bool
|
helmFlag bool
|
||||||
yes bool
|
yes bool
|
||||||
upgradeTimeout time.Duration
|
upgradeTimeout time.Duration
|
||||||
|
force bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type cloudUpgrader interface {
|
type cloudUpgrader interface {
|
||||||
|
|
|
@ -51,6 +51,7 @@ func TestUpgradeExecute(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
cmd := newUpgradeExecuteCmd()
|
cmd := newUpgradeExecuteCmd()
|
||||||
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
|
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
|
||||||
|
cmd.Flags().Bool("force", true, "") // register persistent flag manually
|
||||||
|
|
||||||
handler := file.NewHandler(afero.NewMemMapFs())
|
handler := file.NewHandler(afero.NewMemMapFs())
|
||||||
cfg := defaultConfigWithExpectedMeasurements(t, config.Default(), cloudprovider.Azure)
|
cfg := defaultConfigWithExpectedMeasurements(t, config.Default(), cloudprovider.Azure)
|
||||||
|
|
|
@ -79,9 +79,9 @@ func (up *upgradePlanCmd) upgradePlan(cmd *cobra.Command, planner upgradePlanner
|
||||||
fileHandler file.Handler, client *http.Client, rekor rekorVerifier, flags upgradePlanFlags,
|
fileHandler file.Handler, client *http.Client, rekor rekorVerifier, flags upgradePlanFlags,
|
||||||
cliVersion string,
|
cliVersion string,
|
||||||
) error {
|
) error {
|
||||||
conf, err := config.New(fileHandler, flags.configPath)
|
conf, err := config.New(fileHandler, flags.configPath, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
|
return config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
|
||||||
}
|
}
|
||||||
up.log.Debugf("Read configuration from %q", flags.configPath)
|
up.log.Debugf("Read configuration from %q", flags.configPath)
|
||||||
// get current image version of the cluster
|
// get current image version of the cluster
|
||||||
|
|
|
@ -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("Using flags: %+v", flags)
|
||||||
|
|
||||||
v.log.Debugf("Loading configuration file from %q", flags.configPath)
|
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 {
|
if err != nil {
|
||||||
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
|
return config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
provider := conf.GetProvider()
|
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)
|
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
|
// Get empty values from ID file
|
||||||
emptyEndpoint := endpoint == ""
|
emptyEndpoint := endpoint == ""
|
||||||
emptyIDs := ownerID == "" && clusterID == ""
|
emptyIDs := ownerID == "" && clusterID == ""
|
||||||
|
@ -168,6 +174,7 @@ func (v *verifyCmd) parseVerifyFlags(cmd *cobra.Command, fileHandler file.Handle
|
||||||
configPath: configPath,
|
configPath: configPath,
|
||||||
ownerID: ownerID,
|
ownerID: ownerID,
|
||||||
clusterID: clusterID,
|
clusterID: clusterID,
|
||||||
|
force: force,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,6 +183,7 @@ type verifyFlags struct {
|
||||||
ownerID string
|
ownerID string
|
||||||
clusterID string
|
clusterID string
|
||||||
configPath string
|
configPath string
|
||||||
|
force bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPortIfMissing(endpoint string, defaultPort int) (string, error) {
|
func addPortIfMissing(endpoint string, defaultPort int) (string, error) {
|
||||||
|
|
|
@ -141,6 +141,7 @@ func TestVerify(t *testing.T) {
|
||||||
|
|
||||||
cmd := NewVerifyCmd()
|
cmd := NewVerifyCmd()
|
||||||
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
|
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
|
||||||
|
cmd.Flags().Bool("force", true, "") // register persistent flag manually
|
||||||
out := &bytes.Buffer{}
|
out := &bytes.Buffer{}
|
||||||
cmd.SetOut(out)
|
cmd.SetOut(out)
|
||||||
cmd.SetErr(&bytes.Buffer{})
|
cmd.SetErr(&bytes.Buffer{})
|
||||||
|
|
|
@ -58,13 +58,18 @@ func runDeploy(cmd *cobra.Command, args []string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parsing config path argument: %w", err)
|
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()
|
fs := afero.NewOsFs()
|
||||||
fileHandler := file.NewHandler(fs)
|
fileHandler := file.NewHandler(fs)
|
||||||
streamer := streamer.New(fs)
|
streamer := streamer.New(fs)
|
||||||
transfer := filetransfer.New(log, streamer, filetransfer.ShowProgress)
|
transfer := filetransfer.New(log, streamer, filetransfer.ShowProgress)
|
||||||
constellationConfig, err := config.FromFile(fileHandler, configName)
|
constellationConfig, err := config.New(fileHandler, configName, force)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return deploy(cmd, fileHandler, constellationConfig, transfer, log)
|
return deploy(cmd, fileHandler, constellationConfig, transfer, log)
|
||||||
|
|
|
@ -22,6 +22,8 @@ func newRootCmd() *cobra.Command {
|
||||||
It connects to Constellation instances running debugd and deploys a self-compiled version of the bootstrapper.`,
|
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().String("config", constants.ConfigFilename, "Constellation config file")
|
||||||
|
cmd.PersistentFlags().Bool("force", false, "disables version validation errors - might result in corrupted clusters")
|
||||||
|
|
||||||
cmd.AddCommand(newDeployCmd())
|
cmd.AddCommand(newDeployCmd())
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -86,6 +86,7 @@ require (
|
||||||
github.com/spf13/afero v1.9.3
|
github.com/spf13/afero v1.9.3
|
||||||
github.com/spf13/cobra v1.6.1
|
github.com/spf13/cobra v1.6.1
|
||||||
github.com/stretchr/testify v1.8.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/goleak v1.2.0
|
||||||
go.uber.org/multierr v1.9.0
|
go.uber.org/multierr v1.9.0
|
||||||
go.uber.org/zap v1.24.0
|
go.uber.org/zap v1.24.0
|
||||||
|
|
1
go.sum
1
go.sum
|
@ -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/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 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
|
||||||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
|
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/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-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=
|
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
|
||||||
|
|
|
@ -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/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 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
|
||||||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
|
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/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-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=
|
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
|
||||||
|
|
139
internal/compatibility/compatibility.go
Normal file
139
internal/compatibility/compatibility.go
Normal 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
|
||||||
|
}
|
208
internal/compatibility/compatibility_test.go
Normal file
208
internal/compatibility/compatibility_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -58,7 +58,7 @@ type Config struct {
|
||||||
Version string `yaml:"version" validate:"eq=v2"`
|
Version string `yaml:"version" validate:"eq=v2"`
|
||||||
// description: |
|
// description: |
|
||||||
// Machine image used to create Constellation nodes.
|
// Machine image used to create Constellation nodes.
|
||||||
Image string `yaml:"image" validate:"required"`
|
Image string `yaml:"image" validate:"required,version_compatibility"`
|
||||||
// description: |
|
// description: |
|
||||||
// Size (in GB) of a node's disk to store the non-volatile state.
|
// Size (in GB) of a node's disk to store the non-volatile state.
|
||||||
StateDiskSizeGB int `yaml:"stateDiskSizeGB" validate:"min=0"`
|
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.
|
// 1. Reading config file via provided fileHandler from file with name.
|
||||||
// 2. Read secrets from environment variables.
|
// 2. Read secrets from environment variables.
|
||||||
// 3. Validate config.
|
// 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
|
// Read config file
|
||||||
c, err := FromFile(fileHandler, name)
|
c, err := FromFile(fileHandler, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -330,7 +330,7 @@ func New(fileHandler file.Handler, name string) (*Config, error) {
|
||||||
c.Provider.Azure.ClientSecretValue = clientSecretValue
|
c.Provider.Azure.ClientSecretValue = clientSecretValue
|
||||||
}
|
}
|
||||||
|
|
||||||
return c, c.Validate()
|
return c, c.Validate(force)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasProvider checks whether the config contains the provider.
|
// 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.
|
// 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()
|
trans := ut.New(en.New()).GetFallback()
|
||||||
validate := validator.New()
|
validate := validator.New()
|
||||||
if err := en_translations.RegisterDefaultTranslations(validate, trans); err != nil {
|
if err := en_translations.RegisterDefaultTranslations(validate, trans); err != nil {
|
||||||
|
@ -493,6 +493,14 @@ func (c *Config) Validate() error {
|
||||||
return err
|
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 {
|
if err := validate.RegisterValidation("no_placeholders", validateNoPlaceholder); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -502,6 +510,14 @@ func (c *Config) Validate() error {
|
||||||
return err
|
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.
|
// 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 {
|
if err := validate.RegisterValidation("aws_instance_type", validateAWSInstanceType); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -173,7 +173,7 @@ func TestNewWithDefaultOptions(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
c, err := New(fileHandler, constants.ConfigFilename)
|
c, err := New(fileHandler, constants.ConfigFilename, false)
|
||||||
if tc.wantErr {
|
if tc.wantErr {
|
||||||
assert.Error(err)
|
assert.Error(err)
|
||||||
return
|
return
|
||||||
|
@ -279,7 +279,7 @@ func TestValidate(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
err := tc.cnf.Validate()
|
err := tc.cnf.Validate(false)
|
||||||
if tc.wantErr {
|
if tc.wantErr {
|
||||||
assert.Error(err)
|
assert.Error(err)
|
||||||
assert.Len(multierr.Errors(err), tc.wantErrCount)
|
assert.Len(multierr.Errors(err), tc.wantErrCount)
|
||||||
|
|
|
@ -8,25 +8,41 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
|
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
"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/config/instancetypes"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/versions"
|
"github.com/edgelesssys/constellation/v2/internal/versions"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/versionsapi"
|
||||||
ut "github.com/go-playground/universal-translator"
|
ut "github.com/go-playground/universal-translator"
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
|
"go.uber.org/multierr"
|
||||||
"golang.org/x/mod/semver"
|
"golang.org/x/mod/semver"
|
||||||
)
|
)
|
||||||
|
|
||||||
func validateK8sVersion(fl validator.FieldLevel) bool {
|
// DisplayValidationErrors shows all validation errors inside configError as one formatted string.
|
||||||
return versions.IsSupportedK8sVersion(fl.Field().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 {
|
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 {
|
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)
|
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
|
return t
|
||||||
}
|
}
|
||||||
|
@ -281,3 +297,56 @@ func getPlaceholderEntries(m Measurements) []uint32 {
|
||||||
|
|
||||||
return placeholders
|
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
|
||||||
|
}
|
||||||
|
|
51
internal/config/validation_test.go
Normal file
51
internal/config/validation_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue