mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-10-01 01:36:09 -04:00
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:
parent
baa1b37681
commit
97c72f5f32
@ -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,
|
||||
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user