cli: add verbose debug logging (#809)

* feat: add debug logging for init command
* feat: add debug logging to recover command
* feat: add debug logging for configfetchmeasurements
* feat: add debug logging for config generate
* feat: added debug logging for miniup command
* feat: add debug logging for upgrade command
* feat: add debug logging for create command
This commit is contained in:
Alex Darby 2023-01-04 09:46:29 +00:00 committed by GitHub
parent baa1b37681
commit 97c72f5f32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 285 additions and 100 deletions

View File

@ -46,24 +46,37 @@ type fetchMeasurementsFlags struct {
configPath string
}
type configFetchMeasurementsCmd struct {
log debugLog
}
func runConfigFetchMeasurements(cmd *cobra.Command, args []string) error {
log, err := newCLILogger(cmd)
if err != nil {
return fmt.Errorf("creating logger: %w", err)
}
defer log.Sync()
fileHandler := file.NewHandler(afero.NewOsFs())
rekor, err := sigstore.NewRekor()
if err != nil {
return fmt.Errorf("constructing Rekor client: %w", err)
}
return configFetchMeasurements(cmd, rekor, []byte(constants.CosignPublicKey), fileHandler, http.DefaultClient)
cfm := &configFetchMeasurementsCmd{log: log}
return cfm.configFetchMeasurements(cmd, rekor, []byte(constants.CosignPublicKey), fileHandler, http.DefaultClient)
}
func configFetchMeasurements(
func (cfm *configFetchMeasurementsCmd) configFetchMeasurements(
cmd *cobra.Command, verifier rekorVerifier, cosignPublicKey []byte,
fileHandler file.Handler, client *http.Client,
) error {
flags, err := parseFetchMeasurementsFlags(cmd)
flags, err := cfm.parseFetchMeasurementsFlags(cmd)
cfm.log.Debugf("Using flags %v", flags)
if err != nil {
return err
}
cfm.log.Debugf("Loading config file from %s", flags.configPath)
conf, err := config.New(fileHandler, flags.configPath)
if err != nil {
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
@ -73,13 +86,16 @@ func configFetchMeasurements(
cmd.PrintErrln("Configured image doesn't look like a released production image. Double check image before deploying to production.")
}
cfm.log.Debugf("Creating context")
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
cfm.log.Debugf("Updating URLs")
if err := flags.updateURLs(conf); err != nil {
return err
}
cfm.log.Debugf("Fetching and verifying measurements")
var fetchedMeasurements measurements.M
hash, err := fetchedMeasurements.FetchAndVerify(
ctx, client,
@ -95,47 +111,54 @@ func configFetchMeasurements(
return err
}
cfm.log.Debugf("Fetched and verified measurements, hash is %s", hash)
if err := verifyWithRekor(cmd.Context(), verifier, hash); err != nil {
cmd.PrintErrf("Ignoring Rekor related error: %v\n", err)
cmd.PrintErrln("Make sure the downloaded measurements are trustworthy!")
}
cfm.log.Debugf("Verified measurements with Rekor, updating measurements in config")
conf.UpdateMeasurements(fetchedMeasurements)
if err := fileHandler.WriteYAML(flags.configPath, conf, file.OptOverwrite); err != nil {
return err
}
cfm.log.Debugf("Wrote configuration to YAML")
return nil
}
// parseURLFlag checks that flag can be parsed as URL.
// If no value was provided for flag, nil is returned.
func parseURLFlag(cmd *cobra.Command, flag string) (*url.URL, error) {
func (cfm *configFetchMeasurementsCmd) parseURLFlag(cmd *cobra.Command, flag string) (*url.URL, error) {
rawURL, err := cmd.Flags().GetString(flag)
if err != nil {
return nil, fmt.Errorf("parsing config generate flags '%s': %w", flag, err)
}
cfm.log.Debugf("Flag %s has raw URL %s", flag, rawURL)
if rawURL != "" {
cfm.log.Debugf("Parsing raw URL %s", rawURL)
return url.Parse(rawURL)
}
return nil, nil
}
func parseFetchMeasurementsFlags(cmd *cobra.Command) (*fetchMeasurementsFlags, error) {
measurementsURL, err := parseURLFlag(cmd, "url")
func (cfm *configFetchMeasurementsCmd) parseFetchMeasurementsFlags(cmd *cobra.Command) (*fetchMeasurementsFlags, error) {
measurementsURL, err := cfm.parseURLFlag(cmd, "url")
if err != nil {
return &fetchMeasurementsFlags{}, err
}
cfm.log.Debugf("Parsed measurements URL")
measurementsSignatureURL, err := parseURLFlag(cmd, "signature-url")
measurementsSignatureURL, err := cfm.parseURLFlag(cmd, "signature-url")
if err != nil {
return &fetchMeasurementsFlags{}, err
}
cfm.log.Debugf("Parsed measurements signature URL")
config, err := cmd.Flags().GetString("config")
if err != nil {
return &fetchMeasurementsFlags{}, fmt.Errorf("parsing config path argument: %w", err)
}
cfm.log.Debugf("Config path is %s", config)
return &fetchMeasurementsFlags{
measurementsURL: measurementsURL,

View File

@ -19,6 +19,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -82,8 +83,8 @@ func TestParseFetchMeasurementsFlags(t *testing.T) {
if tc.configFlag != "" {
require.NoError(cmd.Flags().Set("config", tc.configFlag))
}
flags, err := parseFetchMeasurementsFlags(cmd)
cfm := &configFetchMeasurementsCmd{log: logger.NewTest(t)}
flags, err := cfm.parseFetchMeasurementsFlags(cmd)
if tc.wantErr {
assert.Error(err)
return
@ -242,8 +243,9 @@ func TestConfigFetchMeasurements(t *testing.T) {
err := fileHandler.WriteYAML(constants.ConfigFilename, gcpConfig, file.OptMkdirAll)
require.NoError(err)
cfm := &configFetchMeasurementsCmd{log: logger.NewTest(t)}
assert.NoError(configFetchMeasurements(cmd, tc.verifier, cosignPublicKey, fileHandler, client))
assert.NoError(cfm.configFetchMeasurements(cmd, tc.verifier, cosignPublicKey, fileHandler, client))
})
}
}

View File

@ -39,21 +39,31 @@ type generateFlags struct {
file string
}
func runConfigGenerate(cmd *cobra.Command, args []string) error {
fileHandler := file.NewHandler(afero.NewOsFs())
provider := cloudprovider.FromString(args[0])
return configGenerate(cmd, fileHandler, provider)
type configGenerateCmd struct {
log debugLog
}
func configGenerate(cmd *cobra.Command, fileHandler file.Handler, provider cloudprovider.Provider) error {
func runConfigGenerate(cmd *cobra.Command, args []string) error {
log, err := newCLILogger(cmd)
if err != nil {
return fmt.Errorf("creating logger: %w", err)
}
defer log.Sync()
fileHandler := file.NewHandler(afero.NewOsFs())
provider := cloudprovider.FromString(args[0])
cg := &configGenerateCmd{log: log}
return cg.configGenerate(cmd, fileHandler, provider)
}
func (cg *configGenerateCmd) configGenerate(cmd *cobra.Command, fileHandler file.Handler, provider cloudprovider.Provider) error {
flags, err := parseGenerateFlags(cmd)
if err != nil {
return err
}
cg.log.Debugf("Parsed flags as %v", flags)
conf := config.Default()
conf.RemoveProviderExcept(provider)
cg.log.Debugf("Using cloud provider %s", provider.String())
// set a lower default for QEMU's state disk
if provider == cloudprovider.QEMU {
conf.StateDiskSizeGB = 10
@ -64,10 +74,13 @@ func configGenerate(cmd *cobra.Command, fileHandler file.Handler, provider cloud
if err != nil {
return fmt.Errorf("encoding config content: %w", err)
}
cg.log.Debugf("Writing YAML data to stdout")
_, err = cmd.OutOrStdout().Write(content)
return err
}
cg.log.Debugf("Writing YAML data to configuration file")
if err := fileHandler.WriteYAML(flags.file, conf, file.OptMkdirAll); err != nil {
return err
}

View File

@ -14,6 +14,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -27,7 +28,8 @@ func TestConfigGenerateDefault(t *testing.T) {
fileHandler := file.NewHandler(afero.NewMemMapFs())
cmd := newConfigGenerateCmd()
require.NoError(configGenerate(cmd, fileHandler, cloudprovider.Unknown))
cg := &configGenerateCmd{log: logger.NewTest(t)}
require.NoError(cg.configGenerate(cmd, fileHandler, cloudprovider.Unknown))
var readConfig config.Config
err := fileHandler.ReadYAML(constants.ConfigFilename, &readConfig)
@ -45,7 +47,8 @@ func TestConfigGenerateDefaultGCPSpecific(t *testing.T) {
fileHandler := file.NewHandler(afero.NewMemMapFs())
cmd := newConfigGenerateCmd()
require.NoError(configGenerate(cmd, fileHandler, cloudprovider.GCP))
cg := &configGenerateCmd{log: logger.NewTest(t)}
require.NoError(cg.configGenerate(cmd, fileHandler, cloudprovider.GCP))
var readConfig config.Config
err := fileHandler.ReadYAML(constants.ConfigFilename, &readConfig)
@ -60,7 +63,8 @@ func TestConfigGenerateDefaultExists(t *testing.T) {
require.NoError(fileHandler.Write(constants.ConfigFilename, []byte("foobar"), file.OptNone))
cmd := newConfigGenerateCmd()
require.Error(configGenerate(cmd, fileHandler, cloudprovider.Unknown))
cg := &configGenerateCmd{log: logger.NewTest(t)}
require.Error(cg.configGenerate(cmd, fileHandler, cloudprovider.Unknown))
}
func TestConfigGenerateFileFlagRemoved(t *testing.T) {
@ -70,7 +74,8 @@ func TestConfigGenerateFileFlagRemoved(t *testing.T) {
cmd := newConfigGenerateCmd()
cmd.ResetFlags()
require.Error(configGenerate(cmd, fileHandler, cloudprovider.Unknown))
cg := &configGenerateCmd{log: logger.NewTest(t)}
require.Error(cg.configGenerate(cmd, fileHandler, cloudprovider.Unknown))
}
func TestConfigGenerateStdOut(t *testing.T) {
@ -84,7 +89,8 @@ func TestConfigGenerateStdOut(t *testing.T) {
cmd.SetOut(&outBuffer)
require.NoError(cmd.Flags().Set("file", "-"))
require.NoError(configGenerate(cmd, fileHandler, cloudprovider.Unknown))
cg := &configGenerateCmd{log: logger.NewTest(t)}
require.NoError(cg.configGenerate(cmd, fileHandler, cloudprovider.Unknown))
var readConfig config.Config
require.NoError(yaml.NewDecoder(&outBuffer).Decode(&readConfig))

View File

@ -39,31 +39,42 @@ func NewCreateCmd() *cobra.Command {
return cmd
}
type createCmd struct {
log debugLog
}
func runCreate(cmd *cobra.Command, args []string) error {
log, err := newCLILogger(cmd)
if err != nil {
return fmt.Errorf("creating logger: %w", err)
}
defer log.Sync()
fileHandler := file.NewHandler(afero.NewOsFs())
spinner := newSpinner(cmd.ErrOrStderr())
defer spinner.Stop()
creator := cloudcmd.NewCreator(spinner)
return create(cmd, creator, fileHandler, spinner)
c := &createCmd{log: log}
return c.create(cmd, creator, fileHandler, spinner)
}
func create(cmd *cobra.Command, creator cloudCreator, fileHandler file.Handler, spinner spinnerInterf,
func (c *createCmd) create(cmd *cobra.Command, creator cloudCreator, fileHandler file.Handler, spinner spinnerInterf,
) (retErr error) {
flags, err := parseCreateFlags(cmd)
flags, err := c.parseCreateFlags(cmd)
if err != nil {
return err
}
if err := checkDirClean(fileHandler); err != nil {
c.log.Debugf("Using flags: %+v", flags)
if err := c.checkDirClean(fileHandler); err != nil {
return err
}
c.log.Debugf("Loading config file from %s", flags.configPath)
conf, err := config.New(fileHandler, flags.configPath)
if err != nil {
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
}
c.log.Debugf("Checking configuration for warnings")
var printedAWarning bool
if !conf.IsReleaseImage() {
cmd.PrintErrln("Configured image doesn't look like a released production image. Double check image before deploying to production.")
@ -93,18 +104,23 @@ func create(cmd *cobra.Command, creator cloudCreator, fileHandler file.Handler,
var instanceType string
switch provider {
case cloudprovider.AWS:
c.log.Debugf("Configuring instance type for AWS")
instanceType = conf.Provider.AWS.InstanceType
if len(flags.name) > 10 {
return fmt.Errorf("cluster name on AWS must not be longer than 10 characters")
}
case cloudprovider.Azure:
c.log.Debugf("Configuring instance type for Azure")
instanceType = conf.Provider.Azure.InstanceType
case cloudprovider.GCP:
c.log.Debugf("Configuring instance type for GCP")
instanceType = conf.Provider.GCP.InstanceType
case cloudprovider.QEMU:
c.log.Debugf("Configuring instance type for QEMU")
cpus := conf.Provider.QEMU.VCPUs
instanceType = fmt.Sprintf("%d-vCPU", cpus)
}
c.log.Debugf("Configured with instance type %s", instanceType)
if !flags.yes {
// Ask user to confirm action.
@ -123,6 +139,7 @@ func create(cmd *cobra.Command, creator cloudCreator, fileHandler file.Handler,
spinner.Start("Creating", false)
idFile, err := creator.Create(cmd.Context(), provider, conf, flags.name, instanceType, flags.controllerCount, flags.workerCount)
c.log.Debugf("Create command generated idFile")
spinner.Stop()
if err != nil {
return translateCreateErrors(cmd, err)
@ -137,8 +154,9 @@ func create(cmd *cobra.Command, creator cloudCreator, fileHandler file.Handler,
}
// parseCreateFlags parses the flags of the create command.
func parseCreateFlags(cmd *cobra.Command) (createFlags, error) {
func (c *createCmd) parseCreateFlags(cmd *cobra.Command) (createFlags, error) {
controllerCount, err := cmd.Flags().GetInt("control-plane-nodes")
c.log.Debugf("Control-plane nodes flag is %d", controllerCount)
if err != nil {
return createFlags{}, fmt.Errorf("parsing number of control-plane nodes: %w", err)
}
@ -147,6 +165,7 @@ func parseCreateFlags(cmd *cobra.Command) (createFlags, error) {
}
workerCount, err := cmd.Flags().GetInt("worker-nodes")
c.log.Debugf("Worker nodes falg is %d", workerCount)
if err != nil {
return createFlags{}, fmt.Errorf("parsing number of worker nodes: %w", err)
}
@ -155,6 +174,7 @@ func parseCreateFlags(cmd *cobra.Command) (createFlags, error) {
}
name, err := cmd.Flags().GetString("name")
c.log.Debugf("Name flag is %s", name)
if err != nil {
return createFlags{}, fmt.Errorf("parsing name argument: %w", err)
}
@ -166,11 +186,13 @@ func parseCreateFlags(cmd *cobra.Command) (createFlags, error) {
}
yes, err := cmd.Flags().GetBool("yes")
c.log.Debugf("Yes flag is %t", yes)
if err != nil {
return createFlags{}, fmt.Errorf("%w; Set '-yes' without a value to automatically confirm", err)
}
configPath, err := cmd.Flags().GetString("config")
c.log.Debugf("Config path flag is %s", configPath)
if err != nil {
return createFlags{}, fmt.Errorf("parsing config path argument: %w", err)
}
@ -194,13 +216,16 @@ type createFlags struct {
}
// checkDirClean checks if files of a previous Constellation are left in the current working dir.
func checkDirClean(fileHandler file.Handler) error {
func (c *createCmd) checkDirClean(fileHandler file.Handler) error {
c.log.Debugf("Checking admin configuration file")
if _, err := fileHandler.Stat(constants.AdminConfFilename); !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("file '%s' already exists in working directory, run 'constellation terminate' before creating a new one", constants.AdminConfFilename)
}
c.log.Debugf("Checking master secrets file")
if _, err := fileHandler.Stat(constants.MasterSecretFilename); !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("file '%s' already exists in working directory. Constellation won't overwrite previous master secrets. Move it somewhere or delete it before creating a new cluster", constants.MasterSecretFilename)
}
c.log.Debugf("Checking cluster IDs file")
if _, err := fileHandler.Stat(constants.ClusterIDsFileName); !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("file '%s' already exists in working directory. Constellation won't overwrite previous cluster IDs. Move it somewhere or delete it before creating a new cluster", constants.ClusterIDsFileName)
}

View File

@ -18,6 +18,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -211,8 +212,8 @@ func TestCreate(t *testing.T) {
}
fileHandler := file.NewHandler(tc.setupFs(require, tc.provider))
err := create(cmd, tc.creator, fileHandler, nopSpinner{})
c := &createCmd{log: logger.NewTest(t)}
err := c.create(cmd, tc.creator, fileHandler, nopSpinner{})
if tc.wantErr {
assert.Error(err)
@ -268,8 +269,8 @@ func TestCheckDirClean(t *testing.T) {
for _, f := range tc.existingFiles {
require.NoError(tc.fileHandler.Write(f, []byte{1, 2, 3}, file.OptNone))
}
err := checkDirClean(tc.fileHandler)
c := &createCmd{log: logger.NewTest(t)}
err := c.checkDirClean(tc.fileHandler)
if tc.wantErr {
assert.Error(err)

View File

@ -53,8 +53,17 @@ func NewInitCmd() *cobra.Command {
return cmd
}
type initCmd struct {
log debugLog
}
// runInitialize runs the initialize command.
func runInitialize(cmd *cobra.Command, args []string) error {
log, err := newCLILogger(cmd)
if err != nil {
return fmt.Errorf("creating logger: %w", err)
}
defer log.Sync()
fileHandler := file.NewHandler(afero.NewOsFs())
newDialer := func(validator *cloudcmd.Validator) *dialer.Dialer {
return dialer.New(nil, validator.V(cmd), &net.Dialer{})
@ -66,24 +75,26 @@ func runInitialize(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithTimeout(cmd.Context(), time.Hour)
defer cancel()
cmd.SetContext(ctx)
return initialize(cmd, newDialer, fileHandler, license.NewClient(), spinner)
i := &initCmd{log: log}
return i.initialize(cmd, newDialer, fileHandler, license.NewClient(), spinner)
}
// initialize initializes a Constellation.
func initialize(cmd *cobra.Command, newDialer func(validator *cloudcmd.Validator) *dialer.Dialer,
func (i *initCmd) initialize(cmd *cobra.Command, newDialer func(validator *cloudcmd.Validator) *dialer.Dialer,
fileHandler file.Handler, quotaChecker license.QuotaChecker, spinner spinnerInterf,
) error {
flags, err := evalFlagArgs(cmd)
flags, err := i.evalFlagArgs(cmd)
if err != nil {
return err
}
i.log.Debugf("Using flags: %+v", flags)
i.log.Debugf("Loading config file from %s", flags.configPath)
conf, err := config.New(fileHandler, flags.configPath)
if err != nil {
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
}
i.log.Debugf("Checking cluster ID file")
var idFile clusterid.File
if err := fileHandler.ReadJSON(constants.ClusterIDsFileName, &idFile); err != nil {
return fmt.Errorf("reading cluster ID file: %w", err)
@ -93,32 +104,37 @@ func initialize(cmd *cobra.Command, newDialer func(validator *cloudcmd.Validator
if err != nil {
return fmt.Errorf("validating kubernetes version: %w", err)
}
i.log.Debugf("Validated k8s version as %s", k8sVersion)
if versions.IsPreviewK8sVersion(k8sVersion) {
cmd.PrintErrf("Warning: Constellation with Kubernetes %v is still in preview. Use only for evaluation purposes.\n", k8sVersion)
}
provider := conf.GetProvider()
i.log.Debugf("Got provider %s", provider.String())
checker := license.NewChecker(quotaChecker, fileHandler)
if err := checker.CheckLicense(cmd.Context(), provider, conf.Provider, cmd.Printf); err != nil {
cmd.PrintErrf("License check failed: %v", err)
}
i.log.Debugf("Checked license")
validator, err := cloudcmd.NewValidator(provider, conf)
if err != nil {
return err
}
serviceAccURI, err := getMarshaledServiceAccountURI(provider, conf, fileHandler)
i.log.Debugf("Created a new validator")
serviceAccURI, err := i.getMarshaledServiceAccountURI(provider, conf, fileHandler)
if err != nil {
return err
}
masterSecret, err := readOrGenerateMasterSecret(cmd.OutOrStdout(), fileHandler, flags.masterSecretPath)
i.log.Debugf("Got service account uri %s", serviceAccURI)
i.log.Debugf("Loading master secret file from %s", flags.masterSecretPath)
masterSecret, err := i.readOrGenerateMasterSecret(cmd.OutOrStdout(), fileHandler, flags.masterSecretPath)
if err != nil {
return fmt.Errorf("parsing or generating master secret from file %s: %w", flags.masterSecretPath, err)
}
helmLoader := helm.NewLoader(provider, k8sVersion)
i.log.Debugf("Created new helm loader")
helmDeployments, err := helmLoader.Load(conf, flags.conformance, masterSecret.Key, masterSecret.Salt)
i.log.Debugf("Loaded helm heployments")
if err != nil {
return fmt.Errorf("loading Helm charts: %w", err)
}
@ -140,7 +156,9 @@ func initialize(cmd *cobra.Command, newDialer func(validator *cloudcmd.Validator
ConformanceMode: flags.conformance,
InitSecret: idFile.InitSecret,
}
resp, err := initCall(cmd.Context(), newDialer(validator), idFile.IP, req)
i.log.Debugf("Sending initialization request")
resp, err := i.initCall(cmd.Context(), newDialer(validator), idFile.IP, req)
i.log.Debugf("Got initialization response")
spinner.Stop()
if err != nil {
var nonRetriable *nonRetriableError
@ -150,21 +168,23 @@ func initialize(cmd *cobra.Command, newDialer func(validator *cloudcmd.Validator
}
return err
}
i.log.Debugf("Writing Constellation id file")
idFile.CloudProvider = provider
if err := writeOutput(idFile, resp, cmd.OutOrStdout(), fileHandler); err != nil {
if err := i.writeOutput(idFile, resp, cmd.OutOrStdout(), fileHandler); err != nil {
return err
}
return nil
}
func initCall(ctx context.Context, dialer grpcDialer, ip string, req *initproto.InitRequest) (*initproto.InitResponse, error) {
func (i *initCmd) initCall(ctx context.Context, dialer grpcDialer, ip string, req *initproto.InitRequest) (*initproto.InitResponse, error) {
doer := &initDoer{
dialer: dialer,
endpoint: net.JoinHostPort(ip, strconv.Itoa(constants.BootstrapperPort)),
req: req,
log: i.log,
}
i.log.Debugf("Making initialization call, doer is %+v", doer)
retrier := retry.NewIntervalRetrier(doer, 30*time.Second, grpcRetry.ServiceIsUnavailable)
if err := retrier.Do(ctx); err != nil {
return nil, err
@ -177,15 +197,18 @@ type initDoer struct {
endpoint string
req *initproto.InitRequest
resp *initproto.InitResponse
log debugLog
}
func (d *initDoer) Do(ctx context.Context) error {
conn, err := d.dialer.Dial(ctx, d.endpoint)
if err != nil {
d.log.Debugf("Dialing init server failed: %w. Retrying...", err)
return fmt.Errorf("dialing init server: %w", err)
}
defer conn.Close()
protoClient := initproto.NewAPIClient(conn)
d.log.Debugf("Created protoClient")
resp, err := protoClient.Init(ctx, d.req)
if err != nil {
return &nonRetriableError{fmt.Errorf("init call: %w", err)}
@ -194,10 +217,11 @@ func (d *initDoer) Do(ctx context.Context) error {
return nil
}
func writeOutput(idFile clusterid.File, resp *initproto.InitResponse, wr io.Writer, fileHandler file.Handler) error {
func (i *initCmd) writeOutput(idFile clusterid.File, resp *initproto.InitResponse, wr io.Writer, fileHandler file.Handler) error {
fmt.Fprint(wr, "Your Constellation cluster was successfully initialized.\n\n")
ownerID := hex.EncodeToString(resp.OwnerId)
i.log.Debugf("Owner id is %s", ownerID)
clusterID := hex.EncodeToString(resp.ClusterId)
tw := tabwriter.NewWriter(wr, 0, 0, 2, ' ', 0)
@ -210,13 +234,14 @@ func writeOutput(idFile clusterid.File, resp *initproto.InitResponse, wr io.Writ
if err := fileHandler.Write(constants.AdminConfFilename, resp.Kubeconfig, file.OptNone); err != nil {
return fmt.Errorf("writing kubeconfig: %w", err)
}
i.log.Debugf("Wrote out kubeconfig")
idFile.OwnerID = ownerID
idFile.ClusterID = clusterID
if err := fileHandler.WriteJSON(constants.ClusterIDsFileName, idFile, file.OptOverwrite); err != nil {
return fmt.Errorf("writing Constellation id file: %w", err)
}
i.log.Debugf("Wrote out Constellation id file")
fmt.Fprintln(wr, "You can now connect to your cluster by executing:")
fmt.Fprintf(wr, "\texport KUBECONFIG=\"$PWD/%s\"\n", constants.AdminConfFilename)
@ -229,16 +254,19 @@ func writeRow(wr io.Writer, col1 string, col2 string) {
// evalFlagArgs gets the flag values and does preprocessing of these values like
// reading the content from file path flags and deriving other values from flag combinations.
func evalFlagArgs(cmd *cobra.Command) (initFlags, error) {
func (i *initCmd) evalFlagArgs(cmd *cobra.Command) (initFlags, error) {
masterSecretPath, err := cmd.Flags().GetString("master-secret")
i.log.Debugf("Master secret path flag value is %s", masterSecretPath)
if err != nil {
return initFlags{}, fmt.Errorf("parsing master-secret path flag: %w", err)
}
conformance, err := cmd.Flags().GetBool("conformance")
i.log.Debugf("Conformance flag is %t", conformance)
if err != nil {
return initFlags{}, fmt.Errorf("parsing autoscale flag: %w", err)
}
configPath, err := cmd.Flags().GetString("config")
i.log.Debugf("Config path flag is %s", conformance)
if err != nil {
return initFlags{}, fmt.Errorf("parsing config path flag: %w", err)
}
@ -264,8 +292,9 @@ type masterSecret struct {
}
// readOrGenerateMasterSecret reads a base64 encoded master secret from file or generates a new 32 byte secret.
func readOrGenerateMasterSecret(outWriter io.Writer, fileHandler file.Handler, filename string) (masterSecret, error) {
func (i *initCmd) readOrGenerateMasterSecret(outWriter io.Writer, fileHandler file.Handler, filename string) (masterSecret, error) {
if filename != "" {
i.log.Debugf("Reading master secret from file")
var secret masterSecret
if err := fileHandler.ReadJSON(filename, &secret); err != nil {
return masterSecret{}, err
@ -281,6 +310,7 @@ func readOrGenerateMasterSecret(outWriter io.Writer, fileHandler file.Handler, f
}
// No file given, generate a new secret, and save it to disk
i.log.Debugf("Generating new master secret")
key, err := crypto.GenerateRandomBytes(crypto.MasterSecretLengthDefault)
if err != nil {
return masterSecret{}, err
@ -293,7 +323,7 @@ func readOrGenerateMasterSecret(outWriter io.Writer, fileHandler file.Handler, f
Key: key,
Salt: salt,
}
i.log.Debugf("Generated master secret key and salt values")
if err := fileHandler.WriteJSON(constants.MasterSecretFilename, secret, file.OptNone); err != nil {
return masterSecret{}, err
}
@ -312,21 +342,26 @@ func readIPFromIDFile(fileHandler file.Handler) (string, error) {
return idFile.IP, nil
}
func getMarshaledServiceAccountURI(provider cloudprovider.Provider, config *config.Config, fileHandler file.Handler) (string, error) {
func (i *initCmd) getMarshaledServiceAccountURI(provider cloudprovider.Provider, config *config.Config, fileHandler file.Handler) (string, error) {
i.log.Debugf("Getting service account URI")
switch provider {
case cloudprovider.GCP:
i.log.Debugf("Handling case for GCP")
path := config.Provider.GCP.ServiceAccountKeyPath
i.log.Debugf("GCP service account key path %s", path)
var key gcpshared.ServiceAccountKey
if err := fileHandler.ReadJSON(path, &key); err != nil {
return "", fmt.Errorf("reading service account key from path %q: %w", path, err)
}
i.log.Debugf("Read GCP service account key from path")
return key.ToCloudServiceAccountURI(), nil
case cloudprovider.AWS:
i.log.Debugf("Handling case for AWS")
return "", nil // AWS does not need a service account URI
case cloudprovider.Azure:
i.log.Debugf("Handling case for Azure")
creds := azureshared.ApplicationCredentials{
TenantID: config.Provider.Azure.TenantID,
AppClientID: config.Provider.Azure.AppClientID,
@ -336,6 +371,7 @@ func getMarshaledServiceAccountURI(provider cloudprovider.Provider, config *conf
return creds.ToCloudServiceAccountURI(), nil
case cloudprovider.QEMU:
i.log.Debugf("Handling case for QEMU")
return "", nil // QEMU does not use service account keys
default:

View File

@ -31,6 +31,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/grpc/dialer"
"github.com/edgelesssys/constellation/v2/internal/grpc/testdialer"
"github.com/edgelesssys/constellation/v2/internal/license"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/oid"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
@ -163,8 +164,8 @@ func TestInitialize(t *testing.T) {
ctx, cancel := context.WithTimeout(ctx, 4*time.Second)
defer cancel()
cmd.SetContext(ctx)
err := initialize(cmd, newDialer, fileHandler, &stubLicenseClient{}, nopSpinner{})
i := &initCmd{log: logger.NewTest(t)}
err := i.initialize(cmd, newDialer, fileHandler, &stubLicenseClient{}, nopSpinner{})
if tc.wantErr {
assert.Error(err)
@ -217,7 +218,8 @@ func TestWriteOutput(t *testing.T) {
UID: "test-uid",
IP: "cluster-ip",
}
err := writeOutput(idFile, resp, &out, fileHandler)
i := &initCmd{log: logger.NewTest(t)}
err := i.writeOutput(idFile, resp, &out, fileHandler)
assert.NoError(err)
// assert.Contains(out.String(), ownerID)
assert.Contains(out.String(), clusterID)
@ -324,7 +326,8 @@ func TestReadOrGenerateMasterSecret(t *testing.T) {
require.NoError(tc.createFileFunc(fileHandler))
var out bytes.Buffer
secret, err := readOrGenerateMasterSecret(&out, fileHandler, tc.filename)
i := &initCmd{log: logger.NewTest(t)}
secret, err := i.readOrGenerateMasterSecret(&out, fileHandler, tc.filename)
if tc.wantErr {
assert.Error(err)
@ -411,7 +414,8 @@ func TestAttestation(t *testing.T) {
defer cancel()
cmd.SetContext(ctx)
err := initialize(cmd, newDialer, fileHandler, &stubLicenseClient{}, nopSpinner{})
i := &initCmd{log: logger.NewTest(t)}
err := i.initialize(cmd, newDialer, fileHandler, &stubLicenseClient{}, nopSpinner{})
assert.Error(err)
// make sure the error is actually a TLS handshake error
assert.Contains(err.Error(), "transport: authentication handshake failed")

View File

@ -46,36 +46,48 @@ func newMiniUpCmd() *cobra.Command {
return cmd
}
type miniUpCmd struct {
log debugLog
}
func runUp(cmd *cobra.Command, args []string) error {
log, err := newCLILogger(cmd)
if err != nil {
return fmt.Errorf("creating logger: %w", err)
}
defer log.Sync()
spinner := newSpinner(cmd.ErrOrStderr())
defer spinner.Stop()
creator := cloudcmd.NewCreator(spinner)
return up(cmd, creator, spinner)
m := &miniUpCmd{log: log}
return m.up(cmd, creator, spinner)
}
func up(cmd *cobra.Command, creator cloudCreator, spinner spinnerInterf) error {
if err := checkSystemRequirements(cmd.ErrOrStderr()); err != nil {
func (m *miniUpCmd) up(cmd *cobra.Command, creator cloudCreator, spinner spinnerInterf) error {
if err := m.checkSystemRequirements(cmd.ErrOrStderr()); err != nil {
return fmt.Errorf("system requirements not met: %w", err)
}
fileHandler := file.NewHandler(afero.NewOsFs())
// create config if not passed as flag and set default values
config, err := prepareConfig(cmd, fileHandler)
config, err := m.prepareConfig(cmd, fileHandler)
if err != nil {
return fmt.Errorf("preparing config: %w", err)
}
m.log.Debugf("Prepared config")
// create cluster
spinner.Start("Creating cluster in QEMU ", false)
err = createMiniCluster(cmd.Context(), fileHandler, creator, config)
err = m.createMiniCluster(cmd.Context(), fileHandler, creator, config)
spinner.Stop()
if err != nil {
return fmt.Errorf("creating cluster: %w", err)
}
cmd.Println("Cluster successfully created.")
connectURI := config.Provider.QEMU.LibvirtURI
m.log.Debugf("Using connect URI %s", connectURI)
if connectURI == "" {
connectURI = libvirt.LibvirtTCPConnectURI
}
@ -83,9 +95,10 @@ func up(cmd *cobra.Command, creator cloudCreator, spinner spinnerInterf) error {
cmd.Printf("\tvirsh -c %s\n\n", connectURI)
// initialize cluster
if err := initializeMiniCluster(cmd, fileHandler, spinner); err != nil {
if err := m.initializeMiniCluster(cmd, fileHandler, spinner); err != nil {
return fmt.Errorf("initializing cluster: %w", err)
}
m.log.Debugf("Initialized cluster")
return nil
}
@ -96,17 +109,18 @@ func up(cmd *cobra.Command, creator cloudCreator, spinner spinnerInterf) error {
// - has at least 4 CPU cores.
// - has at least 4GB of memory.
// - has at least 20GB of free disk space.
func checkSystemRequirements(out io.Writer) error {
func (m *miniUpCmd) checkSystemRequirements(out io.Writer) error {
// check arch/os
if runtime.GOARCH != "amd64" || runtime.GOOS != "linux" {
return fmt.Errorf("creation of a QEMU based Constellation is not supported for %s/%s, a linux/amd64 platform is required", runtime.GOOS, runtime.GOARCH)
}
m.log.Debugf("Checked arch and os")
// check if /dev/kvm exists
if _, err := os.Stat("/dev/kvm"); err != nil {
return fmt.Errorf("unable to access KVM device: %w", err)
}
m.log.Debugf("Checked that /dev/kvm exists")
// check CPU cores
if runtime.NumCPU() < 4 {
return fmt.Errorf("insufficient CPU cores: %d, at least 4 cores are required by MiniConstellation", runtime.NumCPU())
@ -114,6 +128,7 @@ func checkSystemRequirements(out io.Writer) error {
if runtime.NumCPU() < 6 {
fmt.Fprintf(out, "WARNING: Only %d CPU cores available. This may cause performance issues.\n", runtime.NumCPU())
}
m.log.Debugf("Checked CPU cores - there are %d", runtime.NumCPU())
// check memory
f, err := os.Open("/proc/meminfo")
@ -131,6 +146,7 @@ func checkSystemRequirements(out io.Writer) error {
}
}
}
m.log.Debugf("Scanned for available memory")
memGB := memKB / 1024 / 1024
if memGB < 4 {
return fmt.Errorf("insufficient memory: %dGB, at least 4GB of memory are required by MiniConstellation", memGB)
@ -138,6 +154,7 @@ func checkSystemRequirements(out io.Writer) error {
if memGB < 6 {
fmt.Fprintln(out, "WARNING: Less than 6GB of memory available. This may cause performance issues.")
}
m.log.Debugf("Checked available memory, you have %dGB available", memGB)
var stat unix.Statfs_t
if err := unix.Statfs(".", &stat); err != nil {
@ -147,17 +164,18 @@ func checkSystemRequirements(out io.Writer) error {
if freeSpaceGB < 20 {
return fmt.Errorf("insufficient disk space: %dGB, at least 20GB of disk space are required by MiniConstellation", freeSpaceGB)
}
m.log.Debugf("Checked for free space available, you have %dGB available", freeSpaceGB)
return nil
}
// prepareConfig reads a given config, or creates a new minimal QEMU config.
func prepareConfig(cmd *cobra.Command, fileHandler file.Handler) (*config.Config, error) {
func (m *miniUpCmd) prepareConfig(cmd *cobra.Command, fileHandler file.Handler) (*config.Config, error) {
m.log.Debugf("Preparing config")
configPath, err := cmd.Flags().GetString("config")
if err != nil {
return nil, err
}
// check for existing config
if configPath != "" {
conf, err := config.New(fileHandler, configPath)
@ -169,6 +187,7 @@ func prepareConfig(cmd *cobra.Command, fileHandler file.Handler) (*config.Config
}
return conf, nil
}
m.log.Debugf("Config path is %s", configPath)
if err := cmd.Flags().Set("config", constants.ConfigFilename); err != nil {
return nil, err
}
@ -188,24 +207,27 @@ func prepareConfig(cmd *cobra.Command, fileHandler file.Handler) (*config.Config
config := config.Default()
config.RemoveProviderExcept(cloudprovider.QEMU)
config.StateDiskSizeGB = 8
m.log.Debugf("Prepared config")
return config, fileHandler.WriteYAML(constants.ConfigFilename, config, file.OptOverwrite)
}
// createMiniCluster creates a new cluster using the given config.
func createMiniCluster(ctx context.Context, fileHandler file.Handler, creator cloudCreator, config *config.Config) error {
func (m *miniUpCmd) createMiniCluster(ctx context.Context, fileHandler file.Handler, creator cloudCreator, config *config.Config) error {
m.log.Debugf("Creating mini cluster")
idFile, err := creator.Create(ctx, cloudprovider.QEMU, config, "mini", "", 1, 1)
if err != nil {
return err
}
idFile.UID = "mini" // use UID "mini" to identify MiniConstellation clusters.
m.log.Debugf("Cluster id file contains %v", idFile)
return fileHandler.WriteJSON(constants.ClusterIDsFileName, idFile, file.OptNone)
}
// initializeMiniCluster initializes a QEMU cluster.
func initializeMiniCluster(cmd *cobra.Command, fileHandler file.Handler, spinner spinnerInterf) (retErr error) {
func (m *miniUpCmd) initializeMiniCluster(cmd *cobra.Command, fileHandler file.Handler, spinner spinnerInterf) (retErr error) {
m.log.Debugf("Initializing mini cluster")
// clean up cluster resources if initialization fails
defer func() {
if retErr != nil {
@ -218,13 +240,20 @@ func initializeMiniCluster(cmd *cobra.Command, fileHandler file.Handler, spinner
newDialer := func(validator *cloudcmd.Validator) *dialer.Dialer {
return dialer.New(nil, validator.V(cmd), &net.Dialer{})
}
m.log.Debugf("Created new dialer")
cmd.Flags().String("master-secret", "", "")
cmd.Flags().String("endpoint", "", "")
cmd.Flags().Bool("conformance", false, "")
if err := initialize(cmd, newDialer, fileHandler, license.NewClient(), spinner); err != nil {
log, err := newCLILogger(cmd)
if err != nil {
return fmt.Errorf("creating logger: %w", err)
}
m.log.Debugf("Created new logger")
defer log.Sync()
i := &initCmd{log: log}
if err := i.initialize(cmd, newDialer, fileHandler, license.NewClient(), spinner); err != nil {
return err
}
m.log.Debugf("Initialized mini cluster")
return nil
}

View File

@ -45,33 +45,47 @@ func NewRecoverCmd() *cobra.Command {
return cmd
}
type recoverCmd struct {
log debugLog
}
func runRecover(cmd *cobra.Command, _ []string) error {
log, err := newCLILogger(cmd)
if err != nil {
return fmt.Errorf("creating logger: %w", err)
}
defer log.Sync()
fileHandler := file.NewHandler(afero.NewOsFs())
newDialer := func(validator *cloudcmd.Validator) *dialer.Dialer {
return dialer.New(nil, validator.V(cmd), &net.Dialer{})
}
return recover(cmd, fileHandler, 5*time.Second, &recoverDoer{}, newDialer)
r := &recoverCmd{log: log}
return r.recover(cmd, fileHandler, 5*time.Second, &recoverDoer{log: r.log}, newDialer)
}
func recover(
func (r *recoverCmd) recover(
cmd *cobra.Command, fileHandler file.Handler, interval time.Duration,
doer recoverDoerInterface, newDialer func(validator *cloudcmd.Validator) *dialer.Dialer,
) error {
flags, err := parseRecoverFlags(cmd, fileHandler)
flags, err := r.parseRecoverFlags(cmd, fileHandler)
if err != nil {
return err
}
r.log.Debugf("Using flags: %+v", flags)
var masterSecret masterSecret
r.log.Debugf("Loading master secret file from %s", flags.secretPath)
if err := fileHandler.ReadJSON(flags.secretPath, &masterSecret); err != nil {
return err
}
r.log.Debugf("Loading configuration file from %s", flags.configPath)
conf, err := config.New(fileHandler, flags.configPath)
if err != nil {
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
}
provider := conf.GetProvider()
r.log.Debugf("Got provider %s", provider.String())
if provider == cloudprovider.Azure {
interval = 20 * time.Second // Azure LB takes a while to remove unhealthy instances
}
@ -80,15 +94,17 @@ func recover(
if err != nil {
return err
}
r.log.Debugf("Created a new validator")
doer.setDialer(newDialer(validator), flags.endpoint)
r.log.Debugf("Set dialer for endpoint %s", flags.endpoint)
measurementSecret, err := attestation.DeriveMeasurementSecret(masterSecret.Key, masterSecret.Salt)
r.log.Debugf("Derived measurementSecret")
if err != nil {
return err
}
doer.setSecrets(getStateDiskKeyFunc(masterSecret.Key, masterSecret.Salt), measurementSecret)
if err := recoverCall(cmd.Context(), cmd.OutOrStdout(), interval, doer); err != nil {
r.log.Debugf("Set secrets")
if err := r.recoverCall(cmd.Context(), cmd.OutOrStdout(), interval, doer); err != nil {
if grpcRetry.ServiceIsUnavailable(err) {
return nil
}
@ -97,7 +113,7 @@ func recover(
return nil
}
func recoverCall(ctx context.Context, out io.Writer, interval time.Duration, doer recoverDoerInterface) error {
func (r *recoverCmd) recoverCall(ctx context.Context, out io.Writer, interval time.Duration, doer recoverDoerInterface) error {
var err error
ctr := 0
for {
@ -118,6 +134,7 @@ func recoverCall(ctx context.Context, out io.Writer, interval time.Duration, doe
}
retrier := retry.NewIntervalRetrier(doer, interval, retryOnceOnFailure)
r.log.Debugf("Created new interval retrier")
err = retrier.Do(ctx)
if err != nil {
break
@ -125,14 +142,13 @@ func recoverCall(ctx context.Context, out io.Writer, interval time.Duration, doe
fmt.Fprintln(out, "Pushed recovery key.")
ctr++
}
r.log.Debugf("Retry counter is %d", ctr)
if ctr > 0 {
fmt.Fprintf(out, "Recovered %d control-plane nodes.\n", ctr)
} else if grpcRetry.ServiceIsUnavailable(err) {
fmt.Fprintln(out, "No control-plane nodes in need of recovery found. Exiting.")
return nil
}
return err
}
@ -147,6 +163,7 @@ type recoverDoer struct {
endpoint string
measurementSecret []byte
getDiskKey func(uuid string) (key []byte, err error)
log debugLog
}
// Do performs the recover streaming rpc.
@ -155,11 +172,14 @@ func (d *recoverDoer) Do(ctx context.Context) (retErr error) {
if err != nil {
return fmt.Errorf("dialing recovery server: %w", err)
}
d.log.Debugf("Dialed recovery server")
defer conn.Close()
// set up streaming client
protoClient := recoverproto.NewAPIClient(conn)
d.log.Debugf("Created protoClient")
recoverclient, err := protoClient.Recover(ctx)
d.log.Debugf("Created recoverclient")
if err != nil {
return fmt.Errorf("creating client: %w", err)
}
@ -175,16 +195,19 @@ func (d *recoverDoer) Do(ctx context.Context) (retErr error) {
}); err != nil {
return fmt.Errorf("sending measurement secret: %w", err)
}
d.log.Debugf("Sent measurement secret")
// receive disk uuid
res, err := recoverclient.Recv()
if err != nil {
return fmt.Errorf("receiving disk uuid: %w", err)
}
d.log.Debugf("Received disk uuid")
stateDiskKey, err := d.getDiskKey(res.DiskUuid)
if err != nil {
return fmt.Errorf("getting state disk key: %w", err)
}
d.log.Debugf("Got state disk key")
// send disk key
if err := recoverclient.Send(&recoverproto.RecoverMessage{
@ -194,10 +217,12 @@ func (d *recoverDoer) Do(ctx context.Context) (retErr error) {
}); err != nil {
return fmt.Errorf("sending state disk key: %w", err)
}
d.log.Debugf("Sent state disk key")
if _, err := recoverclient.Recv(); err != nil && !errors.Is(err, io.EOF) {
return fmt.Errorf("receiving confirmation: %w", err)
}
d.log.Debugf("Received confirmation")
return nil
}
@ -217,8 +242,9 @@ type recoverFlags struct {
configPath string
}
func parseRecoverFlags(cmd *cobra.Command, fileHandler file.Handler) (recoverFlags, error) {
func (r *recoverCmd) parseRecoverFlags(cmd *cobra.Command, fileHandler file.Handler) (recoverFlags, error) {
endpoint, err := cmd.Flags().GetString("endpoint")
r.log.Debugf("Endpoint flag is %s", endpoint)
if err != nil {
return recoverFlags{}, fmt.Errorf("parsing endpoint argument: %w", err)
}
@ -232,16 +258,17 @@ func parseRecoverFlags(cmd *cobra.Command, fileHandler file.Handler) (recoverFla
if err != nil {
return recoverFlags{}, fmt.Errorf("validating endpoint argument: %w", err)
}
r.log.Debugf("Endpoint value after parsing is %s", endpoint)
masterSecretPath, err := cmd.Flags().GetString("master-secret")
if err != nil {
return recoverFlags{}, fmt.Errorf("parsing master-secret path argument: %w", err)
}
r.log.Debugf("Master secret flag is %s", masterSecretPath)
configPath, err := cmd.Flags().GetString("config")
if err != nil {
return recoverFlags{}, fmt.Errorf("parsing config path argument: %w", err)
}
r.log.Debugf("Config path flag is %s", configPath)
return recoverFlags{
endpoint: endpoint,

View File

@ -26,6 +26,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/grpc/atlscredentials"
"github.com/edgelesssys/constellation/v2/internal/grpc/dialer"
"github.com/edgelesssys/constellation/v2/internal/grpc/testdialer"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -160,8 +161,8 @@ func TestRecover(t *testing.T) {
))
newDialer := func(*cloudcmd.Validator) *dialer.Dialer { return nil }
err := recover(cmd, fileHandler, time.Millisecond, tc.doer, newDialer)
r := &recoverCmd{log: logger.NewTest(t)}
err := r.recover(cmd, fileHandler, time.Millisecond, tc.doer, newDialer)
if tc.wantErr {
assert.Error(err)
if tc.successfulCalls > 0 {
@ -229,8 +230,8 @@ func TestParseRecoverFlags(t *testing.T) {
if tc.writeIDFile {
require.NoError(fileHandler.WriteJSON(constants.ClusterIDsFileName, &clusterid.File{IP: "192.0.2.42"}))
}
flags, err := parseRecoverFlags(cmd, fileHandler)
r := &recoverCmd{log: logger.NewTest(t)}
flags, err := r.parseRecoverFlags(cmd, fileHandler)
if tc.wantErr {
assert.Error(err)
@ -354,6 +355,7 @@ func TestDoRecovery(t *testing.T) {
go recoverServer.Serve(listener)
defer recoverServer.GracefulStop()
r := &recoverCmd{log: logger.NewTest(t)}
recoverDoer := &recoverDoer{
dialer: dialer.New(nil, nil, netDialer),
endpoint: addr,
@ -361,6 +363,7 @@ func TestDoRecovery(t *testing.T) {
getDiskKey: func(string) ([]byte, error) {
return []byte("disk-key"), nil
},
log: r.log,
}
err := recoverDoer.Do(context.Background())

View File

@ -43,13 +43,16 @@ func newUpgradePlanCmd() *cobra.Command {
return cmd
}
type upgradePlanCmd struct {
log debugLog
}
func runUpgradePlan(cmd *cobra.Command, args []string) error {
log, err := newCLILogger(cmd)
if err != nil {
return fmt.Errorf("creating logger: %w", err)
}
defer log.Sync()
fileHandler := file.NewHandler(afero.NewOsFs())
flags, err := parseUpgradePlanFlags(cmd)
if err != nil {
@ -65,12 +68,13 @@ func runUpgradePlan(cmd *cobra.Command, args []string) error {
return fmt.Errorf("constructing Rekor client: %w", err)
}
cliVersion := getCurrentCLIVersion()
up := &upgradePlanCmd{log: log}
return upgradePlan(cmd, planner, patchLister, fileHandler, http.DefaultClient, rekor, flags, cliVersion)
return up.upgradePlan(cmd, planner, patchLister, fileHandler, http.DefaultClient, rekor, flags, cliVersion)
}
// upgradePlan plans an upgrade of a Constellation cluster.
func upgradePlan(cmd *cobra.Command, planner upgradePlanner, patchLister patchLister,
func (up *upgradePlanCmd) upgradePlan(cmd *cobra.Command, planner upgradePlanner, patchLister patchLister,
fileHandler file.Handler, client *http.Client, rekor rekorVerifier, flags upgradePlanFlags,
cliVersion string,
) error {
@ -78,14 +82,16 @@ func upgradePlan(cmd *cobra.Command, planner upgradePlanner, patchLister patchLi
if err != nil {
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
}
up.log.Debugf("Read config from %s", flags.configPath)
// get current image version of the cluster
csp := conf.GetProvider()
up.log.Debugf("Using provider %s", csp.String())
version, err := getCurrentImageVersion(cmd.Context(), planner)
if err != nil {
return fmt.Errorf("checking current image version: %w", err)
}
up.log.Debugf("Using image version %s", version)
// find compatible images
// image updates should always be possible for the current minor version of the cluster
@ -98,7 +104,9 @@ func upgradePlan(cmd *cobra.Command, planner upgradePlanner, patchLister patchLi
if err != nil {
return fmt.Errorf("calculating next image minor version: %w", err)
}
up.log.Debugf("Current image minor version is %s", currentImageMinorVer)
up.log.Debugf("Current CLI minor version is %s", currentCLIMinorVer)
up.log.Debugf("Next image minor version is %s", nextImageMinorVer)
var allowedMinorVersions []string
cliImageCompare := semver.Compare(currentCLIMinorVer, currentImageMinorVer)
@ -111,6 +119,7 @@ func upgradePlan(cmd *cobra.Command, planner upgradePlanner, patchLister patchLi
case cliImageCompare > 0:
allowedMinorVersions = []string{currentImageMinorVer, nextImageMinorVer}
}
up.log.Debugf("Allowed minor versions are %#v", allowedMinorVersions)
var updateCandidates []string
for _, minorVer := range allowedMinorVersions {
@ -119,15 +128,18 @@ func upgradePlan(cmd *cobra.Command, planner upgradePlanner, patchLister patchLi
updateCandidates = append(updateCandidates, versionList.Versions...)
}
}
up.log.Debugf("Update candidates are %v", updateCandidates)
// filter out versions that are not compatible with the current cluster
compatibleImages := getCompatibleImages(version, updateCandidates)
up.log.Debugf("Of those images, these ones are compaitble %v", compatibleImages)
// get expected measurements for each image
upgrades, err := getCompatibleImageMeasurements(cmd.Context(), cmd, client, rekor, []byte(flags.cosignPubKey), csp, compatibleImages)
if err != nil {
return fmt.Errorf("fetching measurements for compatible images: %w", err)
}
up.log.Debugf("Compatible image measurements are %v", upgrades)
if len(upgrades) == 0 {
cmd.PrintErrln("No compatible images found to upgrade to.")
@ -136,6 +148,7 @@ func upgradePlan(cmd *cobra.Command, planner upgradePlanner, patchLister patchLi
// interactive mode
if flags.filePath == "" {
up.log.Debugf("Writing upgrade plan in interactive mode")
cmd.Printf("Current version: %s\n", version)
return upgradePlanInteractive(
&nopWriteCloser{cmd.OutOrStdout()},
@ -147,6 +160,7 @@ func upgradePlan(cmd *cobra.Command, planner upgradePlanner, patchLister patchLi
// write upgrade plan to stdout
if flags.filePath == "-" {
up.log.Debugf("Writing upgrade plan to stdout")
content, err := encoder.NewEncoder(upgrades).Encode()
if err != nil {
return fmt.Errorf("encoding compatible images: %w", err)
@ -156,6 +170,7 @@ func upgradePlan(cmd *cobra.Command, planner upgradePlanner, patchLister patchLi
}
// write upgrade plan to file
up.log.Debugf("Writing upgrade plan to file")
return fileHandler.WriteYAML(flags.filePath, upgrades)
}

View File

@ -19,6 +19,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/versionsapi"
"github.com/spf13/afero"
"github.com/spf13/cobra"
@ -414,8 +415,8 @@ func TestUpgradePlan(t *testing.T) {
Header: make(http.Header),
}
})
err := upgradePlan(cmd, tc.planner, tc.patchLister, fileHandler, client, tc.verifier, tc.flags, tc.cliVersion)
up := &upgradePlanCmd{log: logger.NewTest(t)}
err := up.upgradePlan(cmd, tc.planner, tc.patchLister, fileHandler, client, tc.verifier, tc.flags, tc.cliVersion)
if tc.wantErr {
assert.Error(err)
return