From c52086c5ffec93133166398262faaf9008fe410d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Wei=C3=9Fe?= <66256922+daniel-weisse@users.noreply.github.com> Date: Mon, 16 Oct 2023 15:05:29 +0200 Subject: [PATCH 01/17] cli: refactor flag parsing code (#2425) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Weiße --- cli/internal/cmd/BUILD.bazel | 5 + cli/internal/cmd/cmd.go | 55 ++ cli/internal/cmd/configfetchmeasurements.go | 118 ++-- .../cmd/configfetchmeasurements_test.go | 20 +- cli/internal/cmd/configgenerate.go | 122 ++-- cli/internal/cmd/configgenerate_test.go | 191 +++---- cli/internal/cmd/create.go | 113 ++-- cli/internal/cmd/create_test.go | 9 +- cli/internal/cmd/iam.go | 23 + cli/internal/cmd/iamcreate.go | 524 +++--------------- cli/internal/cmd/iamcreate_test.go | 237 +++----- cli/internal/cmd/iamcreateaws.go | 121 ++++ cli/internal/cmd/iamcreateazure.go | 113 ++++ cli/internal/cmd/iamcreategcp.go | 153 +++++ cli/internal/cmd/iamdestroy.go | 110 ++-- cli/internal/cmd/iamdestroy_test.go | 26 +- cli/internal/cmd/iamupgradeapply.go | 42 +- cli/internal/cmd/iamupgradeapply_test.go | 5 +- cli/internal/cmd/init.go | 126 ++--- cli/internal/cmd/init_test.go | 12 +- cli/internal/cmd/miniup.go | 89 +-- cli/internal/cmd/recover.go | 119 ++-- cli/internal/cmd/recover_test.go | 81 +-- cli/internal/cmd/status.go | 77 +-- cli/internal/cmd/status_test.go | 33 +- cli/internal/cmd/terminate.go | 90 ++- cli/internal/cmd/terminate_test.go | 16 +- cli/internal/cmd/upgradeapply.go | 199 +++---- cli/internal/cmd/upgradeapply_test.go | 25 +- cli/internal/cmd/upgradecheck.go | 91 ++- cli/internal/cmd/upgradecheck_test.go | 2 +- cli/internal/cmd/verify.go | 219 ++++---- cli/internal/cmd/verify_test.go | 33 +- go.mod | 5 +- 34 files changed, 1484 insertions(+), 1720 deletions(-) create mode 100644 cli/internal/cmd/iam.go create mode 100644 cli/internal/cmd/iamcreateaws.go create mode 100644 cli/internal/cmd/iamcreateazure.go create mode 100644 cli/internal/cmd/iamcreategcp.go diff --git a/cli/internal/cmd/BUILD.bazel b/cli/internal/cmd/BUILD.bazel index e3dcf2dc0f..8e8963da62 100644 --- a/cli/internal/cmd/BUILD.bazel +++ b/cli/internal/cmd/BUILD.bazel @@ -13,7 +13,11 @@ go_library( "configkubernetesversions.go", "configmigrate.go", "create.go", + "iam.go", "iamcreate.go", + "iamcreateaws.go", + "iamcreateazure.go", + "iamcreategcp.go", "iamdestroy.go", "iamupgradeapply.go", "init.go", @@ -87,6 +91,7 @@ go_library( "@com_github_siderolabs_talos_pkg_machinery//config/encoder", "@com_github_spf13_afero//:afero", "@com_github_spf13_cobra//:cobra", + "@com_github_spf13_pflag//:pflag", "@in_gopkg_yaml_v3//:yaml_v3", "@io_k8s_apiextensions_apiserver//pkg/apis/apiextensions/v1:apiextensions", "@io_k8s_apimachinery//pkg/runtime", diff --git a/cli/internal/cmd/cmd.go b/cli/internal/cmd/cmd.go index ad0e9c6e61..a5997b9417 100644 --- a/cli/internal/cmd/cmd.go +++ b/cli/internal/cmd/cmd.go @@ -20,3 +20,58 @@ Common filepaths are defined as constants in the global "/internal/constants" pa To generate workspace correct filepaths for printing, use the functions from the "workspace" package. */ package cmd + +import ( + "errors" + "fmt" + + "github.com/edgelesssys/constellation/v2/cli/internal/cmd/pathprefix" + "github.com/edgelesssys/constellation/v2/cli/internal/terraform" + "github.com/spf13/pflag" +) + +// rootFlags are flags defined on the root command. +// They are available to all subcommands. +type rootFlags struct { + pathPrefixer pathprefix.PathPrefixer + tfLogLevel terraform.LogLevel + debug bool + force bool +} + +// parse flags into the rootFlags struct. +func (f *rootFlags) parse(flags *pflag.FlagSet) error { + var errs error + + workspace, err := flags.GetString("workspace") + if err != nil { + errs = errors.Join(err, fmt.Errorf("getting 'workspace' flag: %w", err)) + } + f.pathPrefixer = pathprefix.New(workspace) + + tfLogString, err := flags.GetString("tf-log") + if err != nil { + errs = errors.Join(err, fmt.Errorf("getting 'tf-log' flag: %w", err)) + } + f.tfLogLevel, err = terraform.ParseLogLevel(tfLogString) + if err != nil { + errs = errors.Join(err, fmt.Errorf("parsing 'tf-log' flag: %w", err)) + } + + f.debug, err = flags.GetBool("debug") + if err != nil { + errs = errors.Join(err, fmt.Errorf("getting 'debug' flag: %w", err)) + } + + f.force, err = flags.GetBool("force") + if err != nil { + errs = errors.Join(err, fmt.Errorf("getting 'force' flag: %w", err)) + } + return errs +} + +func must(err error) { + if err != nil { + panic(err) + } +} diff --git a/cli/internal/cmd/configfetchmeasurements.go b/cli/internal/cmd/configfetchmeasurements.go index 7a3ed1a480..6630c8ec46 100644 --- a/cli/internal/cmd/configfetchmeasurements.go +++ b/cli/internal/cmd/configfetchmeasurements.go @@ -14,7 +14,6 @@ import ( "net/url" "time" - "github.com/edgelesssys/constellation/v2/cli/internal/cmd/pathprefix" "github.com/edgelesssys/constellation/v2/cli/internal/featureset" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/api/versionsapi" @@ -26,6 +25,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/sigstore/keyselect" "github.com/spf13/afero" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) func newConfigFetchMeasurementsCmd() *cobra.Command { @@ -46,14 +46,35 @@ func newConfigFetchMeasurementsCmd() *cobra.Command { } type fetchMeasurementsFlags struct { + rootFlags measurementsURL *url.URL signatureURL *url.URL insecure bool - force bool - pf pathprefix.PathPrefixer +} + +func (f *fetchMeasurementsFlags) parse(flags *pflag.FlagSet) error { + var err error + if err := f.rootFlags.parse(flags); err != nil { + return err + } + + f.measurementsURL, err = parseURLFlag(flags, "url") + if err != nil { + return err + } + f.signatureURL, err = parseURLFlag(flags, "signature-url") + if err != nil { + return err + } + f.insecure, err = flags.GetBool("insecure") + if err != nil { + return fmt.Errorf("getting 'insecure' flag: %w", err) + } + return nil } type configFetchMeasurementsCmd struct { + flags fetchMeasurementsFlags canFetchMeasurements bool log debugLog } @@ -70,6 +91,10 @@ func runConfigFetchMeasurements(cmd *cobra.Command, _ []string) error { return fmt.Errorf("constructing Rekor client: %w", err) } cfm := &configFetchMeasurementsCmd{log: log, canFetchMeasurements: featureset.CanFetchMeasurements} + if err := cfm.flags.parse(cmd.Flags()); err != nil { + return fmt.Errorf("parsing flags: %w", err) + } + cfm.log.Debugf("Using flags %+v", cfm.flags) fetcher := attestationconfigapi.NewFetcherWithClient(http.DefaultClient, constants.CDNRepositoryURL) return cfm.configFetchMeasurements(cmd, sigstore.NewCosignVerifier, rekor, fileHandler, fetcher, http.DefaultClient) @@ -79,20 +104,14 @@ func (cfm *configFetchMeasurementsCmd) configFetchMeasurements( cmd *cobra.Command, newCosignVerifier cosignVerifierConstructor, rekor rekorVerifier, fileHandler file.Handler, fetcher attestationconfigapi.Fetcher, client *http.Client, ) error { - flags, err := cfm.parseFetchMeasurementsFlags(cmd) - if err != nil { - return err - } - cfm.log.Debugf("Using flags %v", flags) - if !cfm.canFetchMeasurements { cmd.PrintErrln("Fetching measurements is not supported in the OSS build of the Constellation CLI. Consult the documentation for instructions on where to download the enterprise version.") return errors.New("fetching measurements is not supported") } - cfm.log.Debugf("Loading configuration file from %q", flags.pf.PrefixPrintablePath(constants.ConfigFilename)) + cfm.log.Debugf("Loading configuration file from %q", cfm.flags.pathPrefixer.PrefixPrintablePath(constants.ConfigFilename)) - conf, err := config.New(fileHandler, constants.ConfigFilename, fetcher, flags.force) + conf, err := config.New(fileHandler, constants.ConfigFilename, fetcher, cfm.flags.force) var configValidationErr *config.ValidationError if errors.As(err, &configValidationErr) { cmd.PrintErrln(configValidationErr.LongMessage()) @@ -110,7 +129,7 @@ func (cfm *configFetchMeasurementsCmd) configFetchMeasurements( defer cancel() cfm.log.Debugf("Updating URLs") - if err := flags.updateURLs(conf); err != nil { + if err := cfm.flags.updateURLs(conf); err != nil { return err } @@ -131,11 +150,11 @@ func (cfm *configFetchMeasurementsCmd) configFetchMeasurements( var fetchedMeasurements measurements.M var hash string - if flags.insecure { + if cfm.flags.insecure { if err := fetchedMeasurements.FetchNoVerify( ctx, client, - flags.measurementsURL, + cfm.flags.measurementsURL, imageVersion, conf.GetProvider(), conf.GetAttestationConfig().GetVariant(), @@ -149,8 +168,8 @@ func (cfm *configFetchMeasurementsCmd) configFetchMeasurements( ctx, client, cosign, - flags.measurementsURL, - flags.signatureURL, + cfm.flags.measurementsURL, + cfm.flags.signatureURL, imageVersion, conf.GetProvider(), conf.GetAttestationConfig().GetVariant(), @@ -173,63 +192,11 @@ func (cfm *configFetchMeasurementsCmd) configFetchMeasurements( if err := fileHandler.WriteYAML(constants.ConfigFilename, conf, file.OptOverwrite); err != nil { return err } - cfm.log.Debugf("Configuration written to %s", flags.pf.PrefixPrintablePath(constants.ConfigFilename)) + cfm.log.Debugf("Configuration written to %s", cfm.flags.pathPrefixer.PrefixPrintablePath(constants.ConfigFilename)) cmd.Print("Successfully fetched measurements and updated Configuration\n") return nil } -// parseURLFlag checks that flag can be parsed as URL. -// If no value was provided for flag, nil is returned. -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 %q", flag, rawURL) - if rawURL != "" { - cfm.log.Debugf("Parsing raw URL") - return url.Parse(rawURL) - } - return nil, nil -} - -func (cfm *configFetchMeasurementsCmd) parseFetchMeasurementsFlags(cmd *cobra.Command) (*fetchMeasurementsFlags, error) { - workDir, err := cmd.Flags().GetString("workspace") - if err != nil { - return nil, fmt.Errorf("parsing workspace argument: %w", err) - } - measurementsURL, err := cfm.parseURLFlag(cmd, "url") - if err != nil { - return nil, err - } - cfm.log.Debugf("Parsed measurements URL as %v", measurementsURL) - - measurementsSignatureURL, err := cfm.parseURLFlag(cmd, "signature-url") - if err != nil { - return nil, err - } - cfm.log.Debugf("Parsed measurements signature URL as %v", measurementsSignatureURL) - - insecure, err := cmd.Flags().GetBool("insecure") - if err != nil { - return nil, fmt.Errorf("parsing insecure argument: %w", err) - } - cfm.log.Debugf("Insecure flag is %v", insecure) - - force, err := cmd.Flags().GetBool("force") - if err != nil { - return nil, fmt.Errorf("parsing force argument: %w", err) - } - - return &fetchMeasurementsFlags{ - measurementsURL: measurementsURL, - signatureURL: measurementsSignatureURL, - insecure: insecure, - force: force, - pf: pathprefix.New(workDir), - }, nil -} - func (f *fetchMeasurementsFlags) updateURLs(conf *config.Config) error { ver, err := versionsapi.NewVersionFromShortPath(conf.Image, versionsapi.VersionKindImage) if err != nil { @@ -250,6 +217,19 @@ func (f *fetchMeasurementsFlags) updateURLs(conf *config.Config) error { return nil } +// parseURLFlag checks that flag can be parsed as URL. +// If no value was provided for flag, nil is returned. +func parseURLFlag(flags *pflag.FlagSet, flag string) (*url.URL, error) { + rawURL, err := flags.GetString(flag) + if err != nil { + return nil, fmt.Errorf("getting '%s' flag: %w", flag, err) + } + if rawURL != "" { + return url.Parse(rawURL) + } + return nil, nil +} + type rekorVerifier interface { SearchByHash(context.Context, string) ([]string, error) VerifyEntry(context.Context, string, string) error diff --git a/cli/internal/cmd/configfetchmeasurements_test.go b/cli/internal/cmd/configfetchmeasurements_test.go index 0b5b549804..6dd230ef1b 100644 --- a/cli/internal/cmd/configfetchmeasurements_test.go +++ b/cli/internal/cmd/configfetchmeasurements_test.go @@ -13,7 +13,6 @@ import ( "io" "net/http" "net/url" - "strconv" "testing" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" @@ -39,11 +38,11 @@ func TestParseFetchMeasurementsFlags(t *testing.T) { urlFlag string signatureURLFlag string forceFlag bool - wantFlags *fetchMeasurementsFlags + wantFlags fetchMeasurementsFlags wantErr bool }{ "default": { - wantFlags: &fetchMeasurementsFlags{ + wantFlags: fetchMeasurementsFlags{ measurementsURL: nil, signatureURL: nil, }, @@ -51,7 +50,7 @@ func TestParseFetchMeasurementsFlags(t *testing.T) { "url": { urlFlag: "https://some.other.url/with/path", signatureURLFlag: "https://some.other.url/with/path.sig", - wantFlags: &fetchMeasurementsFlags{ + wantFlags: fetchMeasurementsFlags{ measurementsURL: urlMustParse("https://some.other.url/with/path"), signatureURL: urlMustParse("https://some.other.url/with/path.sig"), }, @@ -69,7 +68,9 @@ func TestParseFetchMeasurementsFlags(t *testing.T) { cmd := newConfigFetchMeasurementsCmd() cmd.Flags().String("workspace", "", "") // register persistent flag manually - cmd.Flags().Bool("force", false, "") // register persistent flag manually + cmd.Flags().Bool("force", false, "") + cmd.Flags().Bool("debug", false, "") + cmd.Flags().String("tf-log", "NONE", "") if tc.urlFlag != "" { require.NoError(cmd.Flags().Set("url", tc.urlFlag)) @@ -77,8 +78,8 @@ func TestParseFetchMeasurementsFlags(t *testing.T) { if tc.signatureURLFlag != "" { require.NoError(cmd.Flags().Set("signature-url", tc.signatureURLFlag)) } - cfm := &configFetchMeasurementsCmd{log: logger.NewTest(t)} - flags, err := cfm.parseFetchMeasurementsFlags(cmd) + var flags fetchMeasurementsFlags + err := flags.parse(cmd.Flags()) if tc.wantErr { assert.Error(err) return @@ -270,9 +271,6 @@ func TestConfigFetchMeasurements(t *testing.T) { require := require.New(t) cmd := newConfigFetchMeasurementsCmd() - cmd.Flags().String("workspace", "", "") // register persistent flag manually - cmd.Flags().Bool("force", true, "") // register persistent flag manually - require.NoError(cmd.Flags().Set("insecure", strconv.FormatBool(tc.insecureFlag))) fileHandler := file.NewHandler(afero.NewMemMapFs()) gcpConfig := defaultConfigWithExpectedMeasurements(t, config.Default(), cloudprovider.GCP) @@ -281,6 +279,8 @@ func TestConfigFetchMeasurements(t *testing.T) { err := fileHandler.WriteYAML(constants.ConfigFilename, gcpConfig, file.OptMkdirAll) require.NoError(err) cfm := &configFetchMeasurementsCmd{canFetchMeasurements: true, log: logger.NewTest(t)} + cfm.flags.insecure = tc.insecureFlag + cfm.flags.force = true err = cfm.configFetchMeasurements(cmd, tc.cosign, tc.rekor, fileHandler, stubAttestationFetcher{}, client) if tc.wantErr { diff --git a/cli/internal/cmd/configgenerate.go b/cli/internal/cmd/configgenerate.go index 316157adb8..7a8f05ec71 100644 --- a/cli/internal/cmd/configgenerate.go +++ b/cli/internal/cmd/configgenerate.go @@ -10,7 +10,6 @@ import ( "fmt" "strings" - "github.com/edgelesssys/constellation/v2/cli/internal/cmd/pathprefix" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/config" @@ -19,6 +18,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/versions" "github.com/spf13/afero" "github.com/spf13/cobra" + "github.com/spf13/pflag" "golang.org/x/mod/semver" ) @@ -41,13 +41,34 @@ func newConfigGenerateCmd() *cobra.Command { } type generateFlags struct { - pf pathprefix.PathPrefixer + rootFlags k8sVersion versions.ValidK8sVersion attestationVariant variant.Variant } +func (f *generateFlags) parse(flags *pflag.FlagSet) error { + if err := f.rootFlags.parse(flags); err != nil { + return err + } + + k8sVersion, err := parseK8sFlag(flags) + if err != nil { + return err + } + f.k8sVersion = k8sVersion + + variant, err := parseAttestationFlag(flags) + if err != nil { + return err + } + f.attestationVariant = variant + + return nil +} + type configGenerateCmd struct { - log debugLog + flags generateFlags + log debugLog } func runConfigGenerate(cmd *cobra.Command, args []string) error { @@ -56,31 +77,32 @@ func runConfigGenerate(cmd *cobra.Command, args []string) error { return fmt.Errorf("creating logger: %w", err) } defer log.Sync() + fileHandler := file.NewHandler(afero.NewOsFs()) provider := cloudprovider.FromString(args[0]) + cg := &configGenerateCmd{log: log} + if err := cg.flags.parse(cmd.Flags()); err != nil { + return fmt.Errorf("parsing flags: %w", err) + } + log.Debugf("Parsed flags as %+v", cg.flags) + return cg.configGenerate(cmd, fileHandler, provider, args[0]) } func (cg *configGenerateCmd) configGenerate(cmd *cobra.Command, fileHandler file.Handler, provider cloudprovider.Provider, rawProvider string) error { - flags, err := parseGenerateFlags(cmd) - if err != nil { - return err - } - - cg.log.Debugf("Parsed flags as %v", flags) cg.log.Debugf("Using cloud provider %s", provider.String()) - conf, err := createConfigWithAttestationVariant(provider, rawProvider, flags.attestationVariant) + conf, err := createConfigWithAttestationVariant(provider, rawProvider, cg.flags.attestationVariant) if err != nil { return fmt.Errorf("creating config: %w", err) } - conf.KubernetesVersion = flags.k8sVersion + conf.KubernetesVersion = cg.flags.k8sVersion cg.log.Debugf("Writing YAML data to configuration file") if err := fileHandler.WriteYAML(constants.ConfigFilename, conf, file.OptMkdirAll); err != nil { return err } - cmd.Println("Config file written to", flags.pf.PrefixPrintablePath(constants.ConfigFilename)) + cmd.Println("Config file written to", cg.flags.pathPrefixer.PrefixPrintablePath(constants.ConfigFilename)) cmd.Println("Please fill in your CSP-specific configuration before proceeding.") cmd.Println("For more information refer to the documentation:") cmd.Println("\thttps://docs.edgeless.systems/constellation/getting-started/first-steps") @@ -123,46 +145,6 @@ func createConfig(provider cloudprovider.Provider) *config.Config { return res } -func parseGenerateFlags(cmd *cobra.Command) (generateFlags, error) { - workDir, err := cmd.Flags().GetString("workspace") - if err != nil { - return generateFlags{}, fmt.Errorf("parsing workspace flag: %w", err) - } - k8sVersion, err := cmd.Flags().GetString("kubernetes") - if err != nil { - return generateFlags{}, fmt.Errorf("parsing Kubernetes flag: %w", err) - } - resolvedVersion, err := versions.ResolveK8sPatchVersion(k8sVersion) - if err != nil { - return generateFlags{}, fmt.Errorf("resolving kubernetes patch version from flag: %w", err) - } - validK8sVersion, err := versions.NewValidK8sVersion(resolvedVersion, true) - if err != nil { - return generateFlags{}, fmt.Errorf("resolving Kubernetes version from flag: %w", err) - } - - attestationString, err := cmd.Flags().GetString("attestation") - if err != nil { - return generateFlags{}, fmt.Errorf("parsing attestation flag: %w", err) - } - - var attestationVariant variant.Variant - // if no attestation variant is specified, use the default for the cloud provider - if attestationString == "" { - attestationVariant = variant.Dummy{} - } else { - attestationVariant, err = variant.FromString(attestationString) - if err != nil { - return generateFlags{}, fmt.Errorf("invalid attestation variant: %s", attestationString) - } - } - return generateFlags{ - pf: pathprefix.New(workDir), - k8sVersion: validK8sVersion, - attestationVariant: attestationVariant, - }, nil -} - // generateCompletion handles the completion of the create command. It is frequently called // while the user types arguments of the command to suggest completion. func generateCompletion(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) { @@ -185,3 +167,39 @@ func toString[T any](t []T) []string { } return res } + +func parseK8sFlag(flags *pflag.FlagSet) (versions.ValidK8sVersion, error) { + versionString, err := flags.GetString("kubernetes") + if err != nil { + return "", fmt.Errorf("getting kubernetes flag: %w", err) + } + resolvedVersion, err := versions.ResolveK8sPatchVersion(versionString) + if err != nil { + return "", fmt.Errorf("resolving kubernetes patch version from flag: %w", err) + } + k8sVersion, err := versions.NewValidK8sVersion(resolvedVersion, true) + if err != nil { + return "", fmt.Errorf("resolving Kubernetes version from flag: %w", err) + } + return k8sVersion, nil +} + +func parseAttestationFlag(flags *pflag.FlagSet) (variant.Variant, error) { + attestationString, err := flags.GetString("attestation") + if err != nil { + return nil, fmt.Errorf("getting attestation flag: %w", err) + } + + var attestationVariant variant.Variant + // if no attestation variant is specified, use the default for the cloud provider + if attestationString == "" { + attestationVariant = variant.Dummy{} + } else { + attestationVariant, err = variant.FromString(attestationString) + if err != nil { + return nil, fmt.Errorf("invalid attestation variant: %s", attestationString) + } + } + + return attestationVariant, nil +} diff --git a/cli/internal/cmd/configgenerate_test.go b/cli/internal/cmd/configgenerate_test.go index 9a07c7ad04..c0be7160fc 100644 --- a/cli/internal/cmd/configgenerate_test.go +++ b/cli/internal/cmd/configgenerate_test.go @@ -19,13 +19,12 @@ import ( "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/versions" "github.com/spf13/afero" - "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/mod/semver" ) -func TestConfigGenerateKubernetesVersion(t *testing.T) { +func TestParseKubernetesVersion(t *testing.T) { testCases := map[string]struct { version string wantErr bool @@ -68,22 +67,18 @@ func TestConfigGenerateKubernetesVersion(t *testing.T) { assert := assert.New(t) require := require.New(t) - fileHandler := file.NewHandler(afero.NewMemMapFs()) - cmd := newConfigGenerateCmd() - cmd.Flags().String("workspace", "", "") // register persistent flag manually + flags := newConfigGenerateCmd().Flags() if tc.version != "" { - err := cmd.Flags().Set("kubernetes", tc.version) - require.NoError(err) + require.NoError(flags.Set("kubernetes", tc.version)) } - cg := &configGenerateCmd{log: logger.NewTest(t)} - err := cg.configGenerate(cmd, fileHandler, cloudprovider.Unknown, "") - + version, err := parseK8sFlag(flags) if tc.wantErr { assert.Error(err) return } assert.NoError(err) + assert.Equal(versions.Default, version) }) } } @@ -94,9 +89,14 @@ func TestConfigGenerateDefault(t *testing.T) { fileHandler := file.NewHandler(afero.NewMemMapFs()) cmd := newConfigGenerateCmd() - cmd.Flags().String("workspace", "", "") // register persistent flag manually - cg := &configGenerateCmd{log: logger.NewTest(t)} + cg := &configGenerateCmd{ + log: logger.NewTest(t), + flags: generateFlags{ + attestationVariant: variant.Dummy{}, + k8sVersion: versions.Default, + }, + } require.NoError(cg.configGenerate(cmd, fileHandler, cloudprovider.Unknown, "")) var readConfig config.Config @@ -106,53 +106,47 @@ func TestConfigGenerateDefault(t *testing.T) { } func TestConfigGenerateDefaultProviderSpecific(t *testing.T) { - providers := []cloudprovider.Provider{ - cloudprovider.AWS, - cloudprovider.Azure, - cloudprovider.GCP, - cloudprovider.OpenStack, - } - - for _, provider := range providers { - t.Run(provider.String(), func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - fileHandler := file.NewHandler(afero.NewMemMapFs()) - cmd := newConfigGenerateCmd() - cmd.Flags().String("workspace", "", "") // register persistent flag manually - - wantConf := config.Default() - wantConf.RemoveProviderAndAttestationExcept(provider) - - cg := &configGenerateCmd{log: logger.NewTest(t)} - require.NoError(cg.configGenerate(cmd, fileHandler, provider, "")) - - var readConfig config.Config - err := fileHandler.ReadYAML(constants.ConfigFilename, &readConfig) - assert.NoError(err) - assert.Equal(*wantConf, readConfig) - }) + testCases := map[string]struct { + provider cloudprovider.Provider + rawProvider string + }{ + "aws": { + provider: cloudprovider.AWS, + }, + "azure": { + provider: cloudprovider.Azure, + }, + "gcp": { + provider: cloudprovider.GCP, + }, + "openstack": { + provider: cloudprovider.OpenStack, + }, + "stackit": { + provider: cloudprovider.OpenStack, + rawProvider: "stackit", + }, } -} - -func TestConfigGenerateWithStackIt(t *testing.T) { - openStackProviders := []string{"stackit"} - for _, openStackProvider := range openStackProviders { - t.Run(openStackProvider, func(t *testing.T) { + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { assert := assert.New(t) require := require.New(t) fileHandler := file.NewHandler(afero.NewMemMapFs()) cmd := newConfigGenerateCmd() - cmd.Flags().String("workspace", "", "") // register persistent flag manually - wantConf := config.Default().WithOpenStackProviderDefaults(openStackProvider) - wantConf.RemoveProviderAndAttestationExcept(cloudprovider.OpenStack) + wantConf := config.Default().WithOpenStackProviderDefaults(tc.rawProvider) + wantConf.RemoveProviderAndAttestationExcept(tc.provider) - cg := &configGenerateCmd{log: logger.NewTest(t)} - require.NoError(cg.configGenerate(cmd, fileHandler, cloudprovider.OpenStack, openStackProvider)) + cg := &configGenerateCmd{ + log: logger.NewTest(t), + flags: generateFlags{ + attestationVariant: variant.Dummy{}, + k8sVersion: versions.Default, + }, + } + require.NoError(cg.configGenerate(cmd, fileHandler, tc.provider, tc.rawProvider)) var readConfig config.Config err := fileHandler.ReadYAML(constants.ConfigFilename, &readConfig) @@ -168,9 +162,11 @@ func TestConfigGenerateDefaultExists(t *testing.T) { fileHandler := file.NewHandler(afero.NewMemMapFs()) require.NoError(fileHandler.Write(constants.ConfigFilename, []byte("foobar"), file.OptNone)) cmd := newConfigGenerateCmd() - cmd.Flags().String("workspace", "", "") // register persistent flag manually - cg := &configGenerateCmd{log: logger.NewTest(t)} + cg := &configGenerateCmd{ + log: logger.NewTest(t), + flags: generateFlags{attestationVariant: variant.Dummy{}}, + } require.Error(cg.configGenerate(cmd, fileHandler, cloudprovider.Unknown, "")) } @@ -247,64 +243,61 @@ func TestValidProviderAttestationCombination(t *testing.T) { } } -func TestAttestationArgument(t *testing.T) { - defaultAttestation := config.Default().Attestation - tests := []struct { - name string - provider cloudprovider.Provider - expectErr bool - expectedCfg config.AttestationConfig - setFlag func(*cobra.Command) error +func TestParseAttestationFlag(t *testing.T) { + testCases := map[string]struct { + wantErr bool + attestationFlag string + wantVariant variant.Variant }{ - { - name: "InvalidAttestationArgument", - provider: cloudprovider.Unknown, - expectErr: true, - setFlag: func(cmd *cobra.Command) error { - return cmd.Flags().Set("attestation", "unknown") - }, + "invalid": { + wantErr: true, + attestationFlag: "unknown", }, - { - name: "ValidAttestationArgument", - provider: cloudprovider.Azure, - expectErr: false, - setFlag: func(cmd *cobra.Command) error { - return cmd.Flags().Set("attestation", "azure-trustedlaunch") - }, - expectedCfg: config.AttestationConfig{AzureTrustedLaunch: defaultAttestation.AzureTrustedLaunch}, + "AzureTrustedLaunch": { + attestationFlag: "azure-trustedlaunch", + wantVariant: variant.AzureTrustedLaunch{}, }, - { - name: "WithoutAttestationArgument", - provider: cloudprovider.Azure, - expectErr: false, - setFlag: func(cmd *cobra.Command) error { - return nil - }, - expectedCfg: config.AttestationConfig{AzureSEVSNP: defaultAttestation.AzureSEVSNP}, + "AzureSEVSNP": { + attestationFlag: "azure-sev-snp", + wantVariant: variant.AzureSEVSNP{}, + }, + "AWSSEVSNP": { + attestationFlag: "aws-sev-snp", + wantVariant: variant.AWSSEVSNP{}, + }, + "AWSNitroTPM": { + attestationFlag: "aws-nitro-tpm", + wantVariant: variant.AWSNitroTPM{}, + }, + "GCPSEVES": { + attestationFlag: "gcp-sev-es", + wantVariant: variant.GCPSEVES{}, + }, + "QEMUVTPM": { + attestationFlag: "qemu-vtpm", + wantVariant: variant.QEMUVTPM{}, + }, + "no flag": { + wantVariant: variant.Dummy{}, }, } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - require := assert.New(t) + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + require := require.New(t) assert := assert.New(t) cmd := newConfigGenerateCmd() - cmd.Flags().String("workspace", "", "") // register persistent flag manually - require.NoError(test.setFlag(cmd)) - - fileHandler := file.NewHandler(afero.NewMemMapFs()) + if tc.attestationFlag != "" { + require.NoError(cmd.Flags().Set("attestation", tc.attestationFlag)) + } - cg := &configGenerateCmd{log: logger.NewTest(t)} - err := cg.configGenerate(cmd, fileHandler, test.provider, "") - if test.expectErr { + attestation, err := parseAttestationFlag(cmd.Flags()) + if tc.wantErr { assert.Error(err) - } else { - assert.NoError(err) - var readConfig config.Config - require.NoError(fileHandler.ReadYAML(constants.ConfigFilename, &readConfig)) - - assert.Equal(test.expectedCfg, readConfig.Attestation) + return } + require.NoError(err) + assert.True(tc.wantVariant.Equal(attestation)) }) } } diff --git a/cli/internal/cmd/create.go b/cli/internal/cmd/create.go index 43fd9b93e2..4c3140227a 100644 --- a/cli/internal/cmd/create.go +++ b/cli/internal/cmd/create.go @@ -24,6 +24,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/semver" "github.com/spf13/afero" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) // NewCreateCmd returns a new cobra.Command for the create command. @@ -39,9 +40,29 @@ func NewCreateCmd() *cobra.Command { return cmd } +// createFlags contains the parsed flags of the create command. +type createFlags struct { + rootFlags + yes bool +} + +// parse parses the flags of the create command. +func (f *createFlags) parse(flags *pflag.FlagSet) error { + if err := f.rootFlags.parse(flags); err != nil { + return err + } + + yes, err := flags.GetBool("yes") + if err != nil { + return fmt.Errorf("getting 'yes' flag: %w", err) + } + f.yes = yes + return nil +} + type createCmd struct { - log debugLog - pf pathprefix.PathPrefixer + log debugLog + flags createFlags } func runCreate(cmd *cobra.Command, _ []string) error { @@ -59,22 +80,22 @@ func runCreate(cmd *cobra.Command, _ []string) error { fileHandler := file.NewHandler(afero.NewOsFs()) creator := cloudcmd.NewCreator(spinner) c := &createCmd{log: log} + if err := c.flags.parse(cmd.Flags()); err != nil { + return err + } + c.log.Debugf("Using flags: %+v", c.flags) + fetcher := attestationconfigapi.NewFetcher() return c.create(cmd, creator, fileHandler, spinner, fetcher) } func (c *createCmd) create(cmd *cobra.Command, creator cloudCreator, fileHandler file.Handler, spinner spinnerInterf, fetcher attestationconfigapi.Fetcher) (retErr error) { - flags, err := c.parseCreateFlags(cmd) - if err != nil { - return err - } - c.log.Debugf("Using flags: %+v", flags) if err := c.checkDirClean(fileHandler); err != nil { return err } - c.log.Debugf("Loading configuration file from %q", c.pf.PrefixPrintablePath(constants.ConfigFilename)) - conf, err := config.New(fileHandler, constants.ConfigFilename, fetcher, flags.force) + c.log.Debugf("Loading configuration file from %q", c.flags.pathPrefixer.PrefixPrintablePath(constants.ConfigFilename)) + conf, err := config.New(fileHandler, constants.ConfigFilename, fetcher, c.flags.force) c.log.Debugf("Configuration file loaded: %+v", conf) var configValidationErr *config.ValidationError if errors.As(err, &configValidationErr) { @@ -83,7 +104,7 @@ func (c *createCmd) create(cmd *cobra.Command, creator cloudCreator, fileHandler if err != nil { return err } - if !flags.force { + if !c.flags.force { if err := validateCLIandConstellationVersionAreEqual(constants.BinaryVersion(), conf.Image, conf.MicroserviceVersion); err != nil { return err } @@ -137,7 +158,7 @@ func (c *createCmd) create(cmd *cobra.Command, creator cloudCreator, fileHandler c.log.Debugf("Creating %d additional node groups: %v", len(otherGroupNames), otherGroupNames) } - if !flags.yes { + if !c.flags.yes { // Ask user to confirm action. cmd.Printf("The following Constellation cluster will be created:\n") cmd.Printf(" %d control-plane node%s of type %s will be created.\n", controlPlaneGroup.InitialCount, isPlural(controlPlaneGroup.InitialCount), controlPlaneGroup.InstanceType) @@ -160,13 +181,13 @@ func (c *createCmd) create(cmd *cobra.Command, creator cloudCreator, fileHandler opts := cloudcmd.CreateOptions{ Provider: provider, Config: conf, - TFLogLevel: flags.tfLogLevel, + TFLogLevel: c.flags.tfLogLevel, TFWorkspace: constants.TerraformWorkingDir, } infraState, err := creator.Create(cmd.Context(), opts) spinner.Stop() if err != nil { - return translateCreateErrors(cmd, c.pf, err) + return translateCreateErrors(cmd, c.flags.pathPrefixer, err) } c.log.Debugf("Successfully created the cloud resources for the cluster") @@ -179,64 +200,28 @@ func (c *createCmd) create(cmd *cobra.Command, creator cloudCreator, fileHandler return nil } -// parseCreateFlags parses the flags of the create command. -func (c *createCmd) parseCreateFlags(cmd *cobra.Command) (createFlags, error) { - yes, err := cmd.Flags().GetBool("yes") - if err != nil { - return createFlags{}, fmt.Errorf("parsing yes bool: %w", err) - } - c.log.Debugf("Yes flag is %t", yes) - - workDir, err := cmd.Flags().GetString("workspace") - if err != nil { - return createFlags{}, fmt.Errorf("parsing config path argument: %w", err) - } - c.log.Debugf("Workspace set to %q", workDir) - c.pf = pathprefix.New(workDir) - - force, err := cmd.Flags().GetBool("force") - if err != nil { - return createFlags{}, fmt.Errorf("parsing force argument: %w", err) - } - c.log.Debugf("force flag is %t", force) - - logLevelString, err := cmd.Flags().GetString("tf-log") - if err != nil { - return createFlags{}, fmt.Errorf("parsing tf-log string: %w", err) - } - logLevel, err := terraform.ParseLogLevel(logLevelString) - if err != nil { - return createFlags{}, fmt.Errorf("parsing Terraform log level %s: %w", logLevelString, err) - } - c.log.Debugf("Terraform logs will be written into %s at level %s", c.pf.PrefixPrintablePath(constants.TerraformLogFile), logLevel.String()) - - return createFlags{ - tfLogLevel: logLevel, - force: force, - yes: yes, - }, nil -} - -// createFlags contains the parsed flags of the create command. -type createFlags struct { - tfLogLevel terraform.LogLevel - force bool - yes bool -} - // checkDirClean checks if files of a previous Constellation are left in the current working dir. 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", c.pf.PrefixPrintablePath(constants.AdminConfFilename)) + return fmt.Errorf( + "file '%s' already exists in working directory, run 'constellation terminate' before creating a new one", + c.flags.pathPrefixer.PrefixPrintablePath(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", c.pf.PrefixPrintablePath(constants.MasterSecretFilename)) + 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", + c.flags.pathPrefixer.PrefixPrintablePath(constants.MasterSecretFilename), + ) } c.log.Debugf("Checking state file") if _, err := fileHandler.Stat(constants.StateFilename); !errors.Is(err, fs.ErrNotExist) { - return fmt.Errorf("file '%s' already exists in working directory. Constellation won't overwrite previous cluster state. Move it somewhere or delete it before creating a new cluster", c.pf.PrefixPrintablePath(constants.StateFilename)) + return fmt.Errorf( + "file '%s' already exists in working directory. Constellation won't overwrite previous cluster state. Move it somewhere or delete it before creating a new cluster", + c.flags.pathPrefixer.PrefixPrintablePath(constants.StateFilename), + ) } return nil @@ -270,12 +255,6 @@ func isPlural(count int) string { return "s" } -func must(err error) { - if err != nil { - panic(err) - } -} - // validateCLIandConstellationVersionAreEqual checks if the image and microservice version are equal (down to patch level) to the CLI version. func validateCLIandConstellationVersionAreEqual(cliVersion semver.Semver, imageVersion string, microserviceVersion semver.Semver) error { parsedImageVersion, err := versionsapi.NewVersionFromShortPath(imageVersion, versionsapi.VersionKindImage) diff --git a/cli/internal/cmd/create_test.go b/cli/internal/cmd/create_test.go index 0fc3828585..8767662137 100644 --- a/cli/internal/cmd/create_test.go +++ b/cli/internal/cmd/create_test.go @@ -133,16 +133,9 @@ func TestCreate(t *testing.T) { cmd.SetOut(&bytes.Buffer{}) cmd.SetErr(&bytes.Buffer{}) cmd.SetIn(bytes.NewBufferString(tc.stdin)) - cmd.Flags().String("workspace", "", "") // register persistent flag manually - cmd.Flags().Bool("force", true, "") // register persistent flag manually - cmd.Flags().String("tf-log", "NONE", "") // register persistent flag manually - - if tc.yesFlag { - require.NoError(cmd.Flags().Set("yes", "true")) - } fileHandler := file.NewHandler(tc.setupFs(require, tc.provider)) - c := &createCmd{log: logger.NewTest(t)} + c := &createCmd{log: logger.NewTest(t), flags: createFlags{yes: tc.yesFlag}} err := c.create(cmd, tc.creator, fileHandler, &nopSpinner{}, stubAttestationFetcher{}) if tc.wantErr { diff --git a/cli/internal/cmd/iam.go b/cli/internal/cmd/iam.go new file mode 100644 index 0000000000..229a0b2ee0 --- /dev/null +++ b/cli/internal/cmd/iam.go @@ -0,0 +1,23 @@ +/* +Copyright (c) Edgeless Systems GmbH +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package cmd + +import "github.com/spf13/cobra" + +// NewIAMCmd returns a new cobra.Command for the iam parent command. It needs another verb and does nothing on its own. +func NewIAMCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "iam", + Short: "Work with the IAM configuration on your cloud provider", + Long: "Work with the IAM configuration on your cloud provider.", + Args: cobra.ExactArgs(0), + } + + cmd.AddCommand(newIAMCreateCmd()) + cmd.AddCommand(newIAMDestroyCmd()) + cmd.AddCommand(newIAMUpgradeCmd()) + return cmd +} diff --git a/cli/internal/cmd/iamcreate.go b/cli/internal/cmd/iamcreate.go index 9d3051946c..337bf5f863 100644 --- a/cli/internal/cmd/iamcreate.go +++ b/cli/internal/cmd/iamcreate.go @@ -11,17 +11,15 @@ import ( "encoding/json" "fmt" "regexp" - "strings" "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" - "github.com/edgelesssys/constellation/v2/cli/internal/cmd/pathprefix" - "github.com/edgelesssys/constellation/v2/cli/internal/terraform" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" "github.com/spf13/afero" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) var ( @@ -33,22 +31,7 @@ var ( gcpIDRegex = regexp.MustCompile(`^[a-z][-a-z0-9]{4,28}[a-z0-9]$`) ) -// NewIAMCmd returns a new cobra.Command for the iam parent command. It needs another verb and does nothing on its own. -func NewIAMCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "iam", - Short: "Work with the IAM configuration on your cloud provider", - Long: "Work with the IAM configuration on your cloud provider.", - Args: cobra.ExactArgs(0), - } - - cmd.AddCommand(newIAMCreateCmd()) - cmd.AddCommand(newIAMDestroyCmd()) - cmd.AddCommand(newIAMUpgradeCmd()) - return cmd -} - -// NewIAMCreateCmd returns a new cobra.Command for the iam create parent command. It needs another verb, and does nothing on its own. +// newIAMCreateCmd returns a new cobra.Command for the iam create parent command. It needs another verb, and does nothing on its own. func newIAMCreateCmd() *cobra.Command { cmd := &cobra.Command{ Use: "create", @@ -67,135 +50,54 @@ func newIAMCreateCmd() *cobra.Command { return cmd } -// newIAMCreateAWSCmd returns a new cobra.Command for the iam create aws command. -func newIAMCreateAWSCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "aws", - Short: "Create IAM configuration on AWS for your Constellation cluster", - Long: "Create IAM configuration on AWS for your Constellation cluster.", - Args: cobra.ExactArgs(0), - RunE: createRunIAMFunc(cloudprovider.AWS), - } - - cmd.Flags().String("prefix", "", "name prefix for all resources (required)") - must(cobra.MarkFlagRequired(cmd.Flags(), "prefix")) - cmd.Flags().String("zone", "", "AWS availability zone the resources will be created in, e.g., us-east-2a (required)\n"+ - "See the Constellation docs for a list of currently supported regions.") - must(cobra.MarkFlagRequired(cmd.Flags(), "zone")) - return cmd -} - -// newIAMCreateAzureCmd returns a new cobra.Command for the iam create azure command. -func newIAMCreateAzureCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "azure", - Short: "Create IAM configuration on Microsoft Azure for your Constellation cluster", - Long: "Create IAM configuration on Microsoft Azure for your Constellation cluster.", - Args: cobra.ExactArgs(0), - RunE: createRunIAMFunc(cloudprovider.Azure), - } - - cmd.Flags().String("resourceGroup", "", "name prefix of the two resource groups your cluster / IAM resources will be created in (required)") - must(cobra.MarkFlagRequired(cmd.Flags(), "resourceGroup")) - cmd.Flags().String("region", "", "region the resources will be created in, e.g., westus (required)") - must(cobra.MarkFlagRequired(cmd.Flags(), "region")) - cmd.Flags().String("servicePrincipal", "", "name of the service principal that will be created (required)") - must(cobra.MarkFlagRequired(cmd.Flags(), "servicePrincipal")) - return cmd +type iamCreateFlags struct { + rootFlags + yes bool + updateConfig bool } -// NewIAMCreateGCPCmd returns a new cobra.Command for the iam create gcp command. -func newIAMCreateGCPCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "gcp", - Short: "Create IAM configuration on GCP for your Constellation cluster", - Long: "Create IAM configuration on GCP for your Constellation cluster.", - Args: cobra.ExactArgs(0), - RunE: createRunIAMFunc(cloudprovider.GCP), +func (f *iamCreateFlags) parse(flags *pflag.FlagSet) error { + var err error + if err = f.rootFlags.parse(flags); err != nil { + return err } - - cmd.Flags().String("zone", "", "GCP zone the cluster will be deployed in (required)\n"+ - "Find a list of available zones here: https://cloud.google.com/compute/docs/regions-zones#available") - must(cobra.MarkFlagRequired(cmd.Flags(), "zone")) - cmd.Flags().String("serviceAccountID", "", "ID for the service account that will be created (required)\n"+ - "Must be 6 to 30 lowercase letters, digits, or hyphens.") - must(cobra.MarkFlagRequired(cmd.Flags(), "serviceAccountID")) - cmd.Flags().String("projectID", "", "ID of the GCP project the configuration will be created in (required)\n"+ - "Find it on the welcome screen of your project: https://console.cloud.google.com/welcome") - must(cobra.MarkFlagRequired(cmd.Flags(), "projectID")) - - return cmd -} - -// createRunIAMFunc is the entrypoint for the iam create command. It sets up the iamCreator -// and starts IAM creation for the specific cloud provider. -func createRunIAMFunc(provider cloudprovider.Provider) func(cmd *cobra.Command, args []string) error { - var providerCreator func(pf pathprefix.PathPrefixer) providerIAMCreator - switch provider { - case cloudprovider.AWS: - providerCreator = func(pathprefix.PathPrefixer) providerIAMCreator { return &awsIAMCreator{} } - case cloudprovider.Azure: - providerCreator = func(pathprefix.PathPrefixer) providerIAMCreator { return &azureIAMCreator{} } - case cloudprovider.GCP: - providerCreator = func(pf pathprefix.PathPrefixer) providerIAMCreator { - return &gcpIAMCreator{pf} - } - default: - return func(cmd *cobra.Command, args []string) error { - return fmt.Errorf("unknown provider %s", provider) - } + f.yes, err = flags.GetBool("yes") + if err != nil { + return fmt.Errorf("getting 'yes' flag: %w", err) } - - return func(cmd *cobra.Command, args []string) error { - logLevelString, err := cmd.Flags().GetString("tf-log") - if err != nil { - return fmt.Errorf("parsing tf-log string: %w", err) - } - logLevel, err := terraform.ParseLogLevel(logLevelString) - if err != nil { - return fmt.Errorf("parsing Terraform log level %s: %w", logLevelString, err) - } - workDir, err := cmd.Flags().GetString("workspace") - if err != nil { - return fmt.Errorf("parsing workspace string: %w", err) - } - pf := pathprefix.New(workDir) - - iamCreator, err := newIAMCreator(cmd, pf, logLevel) - if err != nil { - return fmt.Errorf("creating iamCreator: %w", err) - } - defer iamCreator.spinner.Stop() - defer iamCreator.log.Sync() - iamCreator.provider = provider - iamCreator.providerCreator = providerCreator(pf) - return iamCreator.create(cmd.Context()) + f.updateConfig, err = flags.GetBool("update-config") + if err != nil { + return fmt.Errorf("getting 'update-config' flag: %w", err) } + return nil } -// newIAMCreator creates a new iamiamCreator. -func newIAMCreator(cmd *cobra.Command, pf pathprefix.PathPrefixer, logLevel terraform.LogLevel) (*iamCreator, error) { +func runIAMCreate(cmd *cobra.Command, providerCreator providerIAMCreator, provider cloudprovider.Provider) error { spinner, err := newSpinnerOrStderr(cmd) if err != nil { - return nil, fmt.Errorf("creating spinner: %w", err) + return fmt.Errorf("creating spinner: %w", err) } + defer spinner.Stop() log, err := newCLILogger(cmd) if err != nil { - return nil, fmt.Errorf("creating logger: %w", err) + return fmt.Errorf("creating logger: %w", err) + } + defer log.Sync() + + iamCreator := &iamCreator{ + cmd: cmd, + spinner: spinner, + log: log, + creator: cloudcmd.NewIAMCreator(spinner), + fileHandler: file.NewHandler(afero.NewOsFs()), + providerCreator: providerCreator, + provider: provider, + } + if err := iamCreator.flags.parse(cmd.Flags()); err != nil { + return err } - log.Debugf("Terraform logs will be written into %s at level %s", pf.PrefixPrintablePath(constants.TerraformLogFile), logLevel.String()) - return &iamCreator{ - cmd: cmd, - spinner: spinner, - log: log, - creator: cloudcmd.NewIAMCreator(spinner), - fileHandler: file.NewHandler(afero.NewOsFs()), - iamConfig: &cloudcmd.IAMConfigOptions{ - TFWorkspace: constants.TerraformIAMWorkingDir, - TFLogLevel: logLevel, - }, - }, nil + return iamCreator.create(cmd.Context()) } // iamCreator is the iamCreator for the iam create command. @@ -208,24 +110,18 @@ type iamCreator struct { providerCreator providerIAMCreator iamConfig *cloudcmd.IAMConfigOptions log debugLog - pf pathprefix.PathPrefixer + flags iamCreateFlags } // create IAM configuration on the iamCreator's cloud provider. func (c *iamCreator) create(ctx context.Context) error { - flags, err := c.parseFlagsAndSetupConfig() - if err != nil { - return err - } - c.log.Debugf("Using flags: %+v", flags) - if err := c.checkWorkingDir(); err != nil { return err } - if !flags.yesFlag { + if !c.flags.yes { c.cmd.Printf("The following IAM configuration will be created:\n\n") - c.providerCreator.printConfirmValues(c.cmd, flags) + c.providerCreator.printConfirmValues(c.cmd) ok, err := askToConfirm(c.cmd, "Do you want to create the configuration?") if err != nil { return err @@ -237,19 +133,22 @@ func (c *iamCreator) create(ctx context.Context) error { } var conf config.Config - if flags.updateConfig { - c.log.Debugf("Parsing config %s", c.pf.PrefixPrintablePath(constants.ConfigFilename)) - if err = c.fileHandler.ReadYAML(constants.ConfigFilename, &conf); err != nil { + if c.flags.updateConfig { + c.log.Debugf("Parsing config %s", c.flags.pathPrefixer.PrefixPrintablePath(constants.ConfigFilename)) + if err := c.fileHandler.ReadYAML(constants.ConfigFilename, &conf); err != nil { return fmt.Errorf("error reading the configuration file: %w", err) } - if err := validateConfigWithFlagCompatibility(c.provider, conf, flags); err != nil { + if err := c.providerCreator.validateConfigWithFlagCompatibility(conf); err != nil { return err } - c.cmd.Printf("The configuration file %q will be automatically updated with the IAM values and zone/region information.\n", c.pf.PrefixPrintablePath(constants.ConfigFilename)) + c.cmd.Printf("The configuration file %q will be automatically updated with the IAM values and zone/region information.\n", c.flags.pathPrefixer.PrefixPrintablePath(constants.ConfigFilename)) } + iamConfig := c.providerCreator.getIAMConfigOptions() + iamConfig.TFWorkspace = constants.TerraformIAMWorkingDir + iamConfig.TFLogLevel = c.flags.tfLogLevel c.spinner.Start("Creating", false) - iamFile, err := c.creator.Create(ctx, c.provider, c.iamConfig) + iamFile, err := c.creator.Create(ctx, c.provider, iamConfig) c.spinner.Stop() if err != nil { return err @@ -262,321 +161,47 @@ func (c *iamCreator) create(ctx context.Context) error { return err } - if flags.updateConfig { - c.log.Debugf("Writing IAM configuration to %s", c.pf.PrefixPrintablePath(constants.ConfigFilename)) - c.providerCreator.writeOutputValuesToConfig(&conf, flags, iamFile) + if c.flags.updateConfig { + c.log.Debugf("Writing IAM configuration to %s", c.flags.pathPrefixer.PrefixPrintablePath(constants.ConfigFilename)) + c.providerCreator.writeOutputValuesToConfig(&conf, iamFile) if err := c.fileHandler.WriteYAML(constants.ConfigFilename, conf, file.OptOverwrite); err != nil { return err } - c.cmd.Printf("Your IAM configuration was created and filled into %s successfully.\n", c.pf.PrefixPrintablePath(constants.ConfigFilename)) + c.cmd.Printf("Your IAM configuration was created and filled into %s successfully.\n", c.flags.pathPrefixer.PrefixPrintablePath(constants.ConfigFilename)) return nil } - c.providerCreator.printOutputValues(c.cmd, flags, iamFile) + c.providerCreator.printOutputValues(c.cmd, iamFile) c.cmd.Println("Your IAM configuration was created successfully. Please fill the above values into your configuration file.") return nil } -// parseFlagsAndSetupConfig parses the flags of the iam create command and fills the values into the IAM config (output values of the command). -func (c *iamCreator) parseFlagsAndSetupConfig() (iamFlags, error) { - workDir, err := c.cmd.Flags().GetString("workspace") - if err != nil { - return iamFlags{}, fmt.Errorf("parsing config string: %w", err) - } - c.pf = pathprefix.New(workDir) - - yesFlag, err := c.cmd.Flags().GetBool("yes") - if err != nil { - return iamFlags{}, fmt.Errorf("parsing yes bool: %w", err) - } - updateConfig, err := c.cmd.Flags().GetBool("update-config") - if err != nil { - return iamFlags{}, fmt.Errorf("parsing update-config bool: %w", err) - } - - flags := iamFlags{ - yesFlag: yesFlag, - updateConfig: updateConfig, - } - - flags, err = c.providerCreator.parseFlagsAndSetupConfig(c.cmd, flags, c.iamConfig) - if err != nil { - return iamFlags{}, fmt.Errorf("parsing provider-specific value: %w", err) - } - - return flags, nil -} - // checkWorkingDir checks if the current working directory already contains a Terraform dir. func (c *iamCreator) checkWorkingDir() error { if _, err := c.fileHandler.Stat(constants.TerraformIAMWorkingDir); err == nil { - return fmt.Errorf("the current working directory already contains the Terraform workspace directory %q. Please run the command in a different directory or destroy the existing workspace", c.pf.PrefixPrintablePath(constants.TerraformIAMWorkingDir)) + return fmt.Errorf( + "the current working directory already contains the Terraform workspace directory %q. Please run the command in a different directory or destroy the existing workspace", + c.flags.pathPrefixer.PrefixPrintablePath(constants.TerraformIAMWorkingDir), + ) } return nil } -// iamFlags contains the parsed flags of the iam create command, including the parsed flags of the selected cloud provider. -type iamFlags struct { - aws awsFlags - azure azureFlags - gcp gcpFlags - yesFlag bool - updateConfig bool -} - -// awsFlags contains the parsed flags of the iam create aws command. -type awsFlags struct { - prefix string - region string - zone string -} - -// azureFlags contains the parsed flags of the iam create azure command. -type azureFlags struct { - region string - resourceGroup string - servicePrincipal string -} - -// gcpFlags contains the parsed flags of the iam create gcp command. -type gcpFlags struct { - serviceAccountID string - zone string - region string - projectID string -} - // providerIAMCreator is an interface for the IAM actions of different cloud providers. type providerIAMCreator interface { // printConfirmValues prints the values that will be created on the cloud provider and need to be confirmed by the user. - printConfirmValues(cmd *cobra.Command, flags iamFlags) + printConfirmValues(cmd *cobra.Command) // printOutputValues prints the values that were created on the cloud provider. - printOutputValues(cmd *cobra.Command, flags iamFlags, iamFile cloudcmd.IAMOutput) + printOutputValues(cmd *cobra.Command, iamFile cloudcmd.IAMOutput) // writeOutputValuesToConfig writes the output values of the IAM creation to the constellation config file. - writeOutputValuesToConfig(conf *config.Config, flags iamFlags, iamFile cloudcmd.IAMOutput) - // parseFlagsAndSetupConfig parses the provider-specific flags and fills the values into the IAM config (output values of the command). - parseFlagsAndSetupConfig(cmd *cobra.Command, flags iamFlags, iamConfig *cloudcmd.IAMConfigOptions) (iamFlags, error) + writeOutputValuesToConfig(conf *config.Config, iamFile cloudcmd.IAMOutput) + // getIAMConfigOptions sets up the IAM values required to create the IAM configuration. + getIAMConfigOptions() *cloudcmd.IAMConfigOptions // parseAndWriteIDFile parses the GCP service account key and writes it to a keyfile. It is only implemented for GCP. parseAndWriteIDFile(iamFile cloudcmd.IAMOutput, fileHandler file.Handler) error -} - -// awsIAMCreator implements the providerIAMCreator interface for AWS. -type awsIAMCreator struct{} -func (c *awsIAMCreator) parseFlagsAndSetupConfig(cmd *cobra.Command, flags iamFlags, iamConfig *cloudcmd.IAMConfigOptions) (iamFlags, error) { - prefix, err := cmd.Flags().GetString("prefix") - if err != nil { - return iamFlags{}, fmt.Errorf("parsing prefix string: %w", err) - } - if len(prefix) > 36 { - return iamFlags{}, fmt.Errorf("prefix must be 36 characters or less") - } - zone, err := cmd.Flags().GetString("zone") - if err != nil { - return iamFlags{}, fmt.Errorf("parsing zone string: %w", err) - } - - if !config.ValidateAWSZone(zone) { - return iamFlags{}, fmt.Errorf("invalid AWS zone. To find a valid zone, please refer to our docs and https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-availability-zones") - } - // Infer region from zone. - region := zone[:len(zone)-1] - if !config.ValidateAWSRegion(region) { - return iamFlags{}, fmt.Errorf("invalid AWS region: %s", region) - } - - flags.aws = awsFlags{ - prefix: prefix, - zone: zone, - region: region, - } - - // Setup IAM config. - iamConfig.AWS = cloudcmd.AWSIAMConfig{ - Region: flags.aws.region, - Prefix: flags.aws.prefix, - } - - return flags, nil -} - -func (c *awsIAMCreator) printConfirmValues(cmd *cobra.Command, flags iamFlags) { - cmd.Printf("Region:\t\t%s\n", flags.aws.region) - cmd.Printf("Name Prefix:\t%s\n\n", flags.aws.prefix) -} - -func (c *awsIAMCreator) printOutputValues(cmd *cobra.Command, flags iamFlags, iamFile cloudcmd.IAMOutput) { - cmd.Printf("region:\t\t\t%s\n", flags.aws.region) - cmd.Printf("zone:\t\t\t%s\n", flags.aws.zone) - cmd.Printf("iamProfileControlPlane:\t%s\n", iamFile.AWSOutput.ControlPlaneInstanceProfile) - cmd.Printf("iamProfileWorkerNodes:\t%s\n\n", iamFile.AWSOutput.WorkerNodeInstanceProfile) -} - -func (c *awsIAMCreator) writeOutputValuesToConfig(conf *config.Config, flags iamFlags, iamFile cloudcmd.IAMOutput) { - conf.Provider.AWS.Region = flags.aws.region - conf.Provider.AWS.Zone = flags.aws.zone - conf.Provider.AWS.IAMProfileControlPlane = iamFile.AWSOutput.ControlPlaneInstanceProfile - conf.Provider.AWS.IAMProfileWorkerNodes = iamFile.AWSOutput.WorkerNodeInstanceProfile - for groupName, group := range conf.NodeGroups { - group.Zone = flags.aws.zone - conf.NodeGroups[groupName] = group - } -} - -func (c *awsIAMCreator) parseAndWriteIDFile(_ cloudcmd.IAMOutput, _ file.Handler) error { - return nil -} - -// azureIAMCreator implements the providerIAMCreator interface for Azure. -type azureIAMCreator struct{} - -func (c *azureIAMCreator) parseFlagsAndSetupConfig(cmd *cobra.Command, flags iamFlags, iamConfig *cloudcmd.IAMConfigOptions) (iamFlags, error) { - region, err := cmd.Flags().GetString("region") - if err != nil { - return iamFlags{}, fmt.Errorf("parsing region string: %w", err) - } - - resourceGroup, err := cmd.Flags().GetString("resourceGroup") - if err != nil { - return iamFlags{}, fmt.Errorf("parsing resourceGroup string: %w", err) - } - - servicePrincipal, err := cmd.Flags().GetString("servicePrincipal") - if err != nil { - return iamFlags{}, fmt.Errorf("parsing servicePrincipal string: %w", err) - } - - flags.azure = azureFlags{ - region: region, - resourceGroup: resourceGroup, - servicePrincipal: servicePrincipal, - } - - // Setup IAM config. - iamConfig.Azure = cloudcmd.AzureIAMConfig{ - Region: flags.azure.region, - ResourceGroup: flags.azure.resourceGroup, - ServicePrincipal: flags.azure.servicePrincipal, - } - - return flags, nil -} - -func (c *azureIAMCreator) printConfirmValues(cmd *cobra.Command, flags iamFlags) { - cmd.Printf("Region:\t\t\t%s\n", flags.azure.region) - cmd.Printf("Resource Group:\t\t%s\n", flags.azure.resourceGroup) - cmd.Printf("Service Principal:\t%s\n\n", flags.azure.servicePrincipal) -} - -func (c *azureIAMCreator) printOutputValues(cmd *cobra.Command, flags iamFlags, iamFile cloudcmd.IAMOutput) { - cmd.Printf("subscription:\t\t%s\n", iamFile.AzureOutput.SubscriptionID) - cmd.Printf("tenant:\t\t\t%s\n", iamFile.AzureOutput.TenantID) - cmd.Printf("location:\t\t%s\n", flags.azure.region) - cmd.Printf("resourceGroup:\t\t%s\n", flags.azure.resourceGroup) - cmd.Printf("userAssignedIdentity:\t%s\n", iamFile.AzureOutput.UAMIID) -} - -func (c *azureIAMCreator) writeOutputValuesToConfig(conf *config.Config, flags iamFlags, iamFile cloudcmd.IAMOutput) { - conf.Provider.Azure.SubscriptionID = iamFile.AzureOutput.SubscriptionID - conf.Provider.Azure.TenantID = iamFile.AzureOutput.TenantID - conf.Provider.Azure.Location = flags.azure.region - conf.Provider.Azure.ResourceGroup = flags.azure.resourceGroup - conf.Provider.Azure.UserAssignedIdentity = iamFile.AzureOutput.UAMIID -} - -func (c *azureIAMCreator) parseAndWriteIDFile(_ cloudcmd.IAMOutput, _ file.Handler) error { - return nil -} - -// gcpIAMCreator implements the providerIAMCreator interface for GCP. -type gcpIAMCreator struct { - pf pathprefix.PathPrefixer -} - -func (c *gcpIAMCreator) parseFlagsAndSetupConfig(cmd *cobra.Command, flags iamFlags, iamConfig *cloudcmd.IAMConfigOptions) (iamFlags, error) { - zone, err := cmd.Flags().GetString("zone") - if err != nil { - return iamFlags{}, fmt.Errorf("parsing zone string: %w", err) - } - if !zoneRegex.MatchString(zone) { - return iamFlags{}, fmt.Errorf("invalid zone string: %s", zone) - } - - // Infer region from zone. - zoneParts := strings.Split(zone, "-") - region := fmt.Sprintf("%s-%s", zoneParts[0], zoneParts[1]) - if !regionRegex.MatchString(region) { - return iamFlags{}, fmt.Errorf("invalid region string: %s", region) - } - - projectID, err := cmd.Flags().GetString("projectID") - if err != nil { - return iamFlags{}, fmt.Errorf("parsing projectID string: %w", err) - } - if !gcpIDRegex.MatchString(projectID) { - return iamFlags{}, fmt.Errorf("projectID %q doesn't match %s", projectID, gcpIDRegex) - } - - serviceAccID, err := cmd.Flags().GetString("serviceAccountID") - if err != nil { - return iamFlags{}, fmt.Errorf("parsing serviceAccountID string: %w", err) - } - if !gcpIDRegex.MatchString(serviceAccID) { - return iamFlags{}, fmt.Errorf("serviceAccountID %q doesn't match %s", serviceAccID, gcpIDRegex) - } - - flags.gcp = gcpFlags{ - zone: zone, - region: region, - projectID: projectID, - serviceAccountID: serviceAccID, - } - - // Setup IAM config. - iamConfig.GCP = cloudcmd.GCPIAMConfig{ - Zone: flags.gcp.zone, - Region: flags.gcp.region, - ProjectID: flags.gcp.projectID, - ServiceAccountID: flags.gcp.serviceAccountID, - } - - return flags, nil -} - -func (c *gcpIAMCreator) printConfirmValues(cmd *cobra.Command, flags iamFlags) { - cmd.Printf("Project ID:\t\t%s\n", flags.gcp.projectID) - cmd.Printf("Service Account ID:\t%s\n", flags.gcp.serviceAccountID) - cmd.Printf("Region:\t\t\t%s\n", flags.gcp.region) - cmd.Printf("Zone:\t\t\t%s\n\n", flags.gcp.zone) -} - -func (c *gcpIAMCreator) printOutputValues(cmd *cobra.Command, flags iamFlags, _ cloudcmd.IAMOutput) { - cmd.Printf("projectID:\t\t%s\n", flags.gcp.projectID) - cmd.Printf("region:\t\t\t%s\n", flags.gcp.region) - cmd.Printf("zone:\t\t\t%s\n", flags.gcp.zone) - cmd.Printf("serviceAccountKeyPath:\t%s\n\n", c.pf.PrefixPrintablePath(constants.GCPServiceAccountKeyFilename)) -} - -func (c *gcpIAMCreator) writeOutputValuesToConfig(conf *config.Config, flags iamFlags, _ cloudcmd.IAMOutput) { - conf.Provider.GCP.Project = flags.gcp.projectID - conf.Provider.GCP.ServiceAccountKeyPath = constants.GCPServiceAccountKeyFilename // File was created in workspace, so only the filename is needed. - conf.Provider.GCP.Region = flags.gcp.region - conf.Provider.GCP.Zone = flags.gcp.zone - for groupName, group := range conf.NodeGroups { - group.Zone = flags.gcp.zone - conf.NodeGroups[groupName] = group - } -} - -func (c *gcpIAMCreator) parseAndWriteIDFile(iamFile cloudcmd.IAMOutput, fileHandler file.Handler) error { - // GCP needs to write the service account key to a file. - tmpOut, err := parseIDFile(iamFile.GCPOutput.ServiceAccountKey) - if err != nil { - return err - } - - return fileHandler.WriteJSON(constants.GCPServiceAccountKeyFilename, tmpOut, file.OptNone) + validateConfigWithFlagCompatibility(config.Config) error } // parseIDFile parses the given base64 encoded JSON string of the GCP service account key and returns a map. @@ -594,30 +219,17 @@ func parseIDFile(serviceAccountKeyBase64 string) (map[string]string, error) { } // validateConfigWithFlagCompatibility checks if the config is compatible with the flags. -func validateConfigWithFlagCompatibility(iamProvider cloudprovider.Provider, cfg config.Config, flags iamFlags) error { +func validateConfigWithFlagCompatibility(iamProvider cloudprovider.Provider, cfg config.Config, zone string) error { if !cfg.HasProvider(iamProvider) { return fmt.Errorf("cloud provider from the the configuration file differs from the one provided via the command %q", iamProvider) } - return checkIfCfgZoneAndFlagZoneDiffer(iamProvider, flags, cfg) + return checkIfCfgZoneAndFlagZoneDiffer(zone, cfg) } -func checkIfCfgZoneAndFlagZoneDiffer(iamProvider cloudprovider.Provider, flags iamFlags, cfg config.Config) error { - flagZone := flagZoneOrAzRegion(iamProvider, flags) +func checkIfCfgZoneAndFlagZoneDiffer(zone string, cfg config.Config) error { configZone := cfg.GetZone() - if configZone != "" && flagZone != configZone { - return fmt.Errorf("zone/region from the configuration file %q differs from the one provided via flags %q", configZone, flagZone) + if configZone != "" && zone != configZone { + return fmt.Errorf("zone/region from the configuration file %q differs from the one provided via flags %q", configZone, zone) } return nil } - -func flagZoneOrAzRegion(provider cloudprovider.Provider, flags iamFlags) string { - switch provider { - case cloudprovider.AWS: - return flags.aws.zone - case cloudprovider.Azure: - return flags.azure.region - case cloudprovider.GCP: - return flags.gcp.zone - } - return "" -} diff --git a/cli/internal/cmd/iamcreate_test.go b/cli/internal/cmd/iamcreate_test.go index fee84186d5..f4ab13627b 100644 --- a/cli/internal/cmd/iamcreate_test.go +++ b/cli/internal/cmd/iamcreate_test.go @@ -82,7 +82,6 @@ func TestIAMCreateAWS(t *testing.T) { testCases := map[string]struct { setupFs func(require *require.Assertions, provider cloudprovider.Provider, existingConfigFiles []string, existingDirs []string) afero.Fs creator *stubIAMCreator - provider cloudprovider.Provider zoneFlag string prefixFlag string yesFlag bool @@ -96,26 +95,14 @@ func TestIAMCreateAWS(t *testing.T) { "iam create aws": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.AWS, zoneFlag: "us-east-2a", prefixFlag: "test", yesFlag: true, existingConfigFiles: []string{constants.ConfigFilename}, }, - "iam create aws fails when --zone has no availability zone": { - setupFs: defaultFs, - creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.AWS, - zoneFlag: "us-east-1", - prefixFlag: "test", - yesFlag: true, - existingConfigFiles: []string{constants.ConfigFilename}, - wantErr: true, - }, "iam create aws --update-config": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.AWS, zoneFlag: "us-east-2a", prefixFlag: "test", yesFlag: true, @@ -130,7 +117,6 @@ func TestIAMCreateAWS(t *testing.T) { return *cfg }()), creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.AWS, zoneFlag: "us-east-1a", prefixFlag: "test", yesFlag: true, @@ -141,7 +127,6 @@ func TestIAMCreateAWS(t *testing.T) { "iam create aws --update-config fails when config has different provider": { setupFs: createFSWithConfig(*createConfig(cloudprovider.GCP)), creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.AWS, zoneFlag: "us-east-1a", prefixFlag: "test", yesFlag: true, @@ -152,7 +137,6 @@ func TestIAMCreateAWS(t *testing.T) { "iam create aws no config": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.AWS, zoneFlag: "us-east-2a", prefixFlag: "test", yesFlag: true, @@ -160,7 +144,6 @@ func TestIAMCreateAWS(t *testing.T) { "iam create aws existing terraform dir": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.AWS, zoneFlag: "us-east-2a", prefixFlag: "test", yesFlag: true, @@ -170,7 +153,6 @@ func TestIAMCreateAWS(t *testing.T) { "interactive": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.AWS, zoneFlag: "us-east-2a", prefixFlag: "test", stdin: "yes\n", @@ -178,7 +160,6 @@ func TestIAMCreateAWS(t *testing.T) { "interactive update config": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.AWS, zoneFlag: "us-east-2a", prefixFlag: "test", stdin: "yes\n", @@ -188,7 +169,6 @@ func TestIAMCreateAWS(t *testing.T) { "interactive abort": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.AWS, zoneFlag: "us-east-2a", prefixFlag: "test", stdin: "no\n", @@ -197,7 +177,6 @@ func TestIAMCreateAWS(t *testing.T) { "interactive update config abort": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.AWS, zoneFlag: "us-east-2a", prefixFlag: "test", stdin: "no\n", @@ -205,19 +184,9 @@ func TestIAMCreateAWS(t *testing.T) { wantAbort: true, existingConfigFiles: []string{constants.ConfigFilename}, }, - "invalid zone": { - setupFs: defaultFs, - creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.AWS, - zoneFlag: "us-west", - prefixFlag: "test", - yesFlag: true, - wantErr: true, - }, "unwritable fs": { setupFs: readOnlyFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.AWS, zoneFlag: "us-east-2a", prefixFlag: "test", yesFlag: true, @@ -236,37 +205,26 @@ func TestIAMCreateAWS(t *testing.T) { cmd.SetErr(&bytes.Buffer{}) cmd.SetIn(bytes.NewBufferString(tc.stdin)) - // register persistent flags manually - cmd.Flags().String("workspace", "", "") - cmd.Flags().Bool("update-config", false, "") - cmd.Flags().Bool("yes", false, "") - cmd.Flags().String("name", "constell", "") - cmd.Flags().String("tf-log", "NONE", "") - - if tc.zoneFlag != "" { - require.NoError(cmd.Flags().Set("zone", tc.zoneFlag)) - } - if tc.prefixFlag != "" { - require.NoError(cmd.Flags().Set("prefix", tc.prefixFlag)) - } - if tc.yesFlag { - require.NoError(cmd.Flags().Set("yes", "true")) - } - if tc.updateConfigFlag { - require.NoError(cmd.Flags().Set("update-config", "true")) - } - - fileHandler := file.NewHandler(tc.setupFs(require, tc.provider, tc.existingConfigFiles, tc.existingDirs)) + fileHandler := file.NewHandler(tc.setupFs(require, cloudprovider.AWS, tc.existingConfigFiles, tc.existingDirs)) iamCreator := &iamCreator{ - cmd: cmd, - log: logger.NewTest(t), - spinner: &nopSpinner{}, - creator: tc.creator, - fileHandler: fileHandler, - iamConfig: &cloudcmd.IAMConfigOptions{}, - provider: tc.provider, - providerCreator: &awsIAMCreator{}, + cmd: cmd, + log: logger.NewTest(t), + spinner: &nopSpinner{}, + creator: tc.creator, + fileHandler: fileHandler, + iamConfig: &cloudcmd.IAMConfigOptions{}, + provider: cloudprovider.AWS, + flags: iamCreateFlags{ + yes: tc.yesFlag, + updateConfig: tc.updateConfigFlag, + }, + providerCreator: &awsIAMCreator{ + flags: awsIAMCreateFlags{ + zone: tc.zoneFlag, + prefix: tc.prefixFlag, + }, + }, } err := iamCreator.create(cmd.Context()) @@ -315,7 +273,6 @@ func TestIAMCreateAzure(t *testing.T) { testCases := map[string]struct { setupFs func(require *require.Assertions, provider cloudprovider.Provider, existingConfigFiles []string, existingDirs []string) afero.Fs creator *stubIAMCreator - provider cloudprovider.Provider regionFlag string servicePrincipalFlag string resourceGroupFlag string @@ -330,7 +287,6 @@ func TestIAMCreateAzure(t *testing.T) { "iam create azure": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.Azure, regionFlag: "westus", servicePrincipalFlag: "constell-test-sp", resourceGroupFlag: "constell-test-rg", @@ -339,7 +295,6 @@ func TestIAMCreateAzure(t *testing.T) { "iam create azure with existing config": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.Azure, regionFlag: "westus", servicePrincipalFlag: "constell-test-sp", resourceGroupFlag: "constell-test-rg", @@ -349,7 +304,6 @@ func TestIAMCreateAzure(t *testing.T) { "iam create azure --update-config": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.Azure, regionFlag: "westus", servicePrincipalFlag: "constell-test-sp", resourceGroupFlag: "constell-test-rg", @@ -360,7 +314,6 @@ func TestIAMCreateAzure(t *testing.T) { "iam create azure existing terraform dir": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.Azure, regionFlag: "westus", servicePrincipalFlag: "constell-test-sp", resourceGroupFlag: "constell-test-rg", @@ -371,7 +324,6 @@ func TestIAMCreateAzure(t *testing.T) { "interactive": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.Azure, regionFlag: "westus", servicePrincipalFlag: "constell-test-sp", resourceGroupFlag: "constell-test-rg", @@ -380,7 +332,6 @@ func TestIAMCreateAzure(t *testing.T) { "interactive update config": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.Azure, regionFlag: "westus", servicePrincipalFlag: "constell-test-sp", resourceGroupFlag: "constell-test-rg", @@ -391,7 +342,6 @@ func TestIAMCreateAzure(t *testing.T) { "interactive abort": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.Azure, regionFlag: "westus", servicePrincipalFlag: "constell-test-sp", resourceGroupFlag: "constell-test-rg", @@ -401,7 +351,6 @@ func TestIAMCreateAzure(t *testing.T) { "interactive update config abort": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.Azure, regionFlag: "westus", servicePrincipalFlag: "constell-test-sp", resourceGroupFlag: "constell-test-rg", @@ -413,7 +362,6 @@ func TestIAMCreateAzure(t *testing.T) { "unwritable fs": { setupFs: readOnlyFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.Azure, regionFlag: "westus", servicePrincipalFlag: "constell-test-sp", resourceGroupFlag: "constell-test-rg", @@ -433,40 +381,27 @@ func TestIAMCreateAzure(t *testing.T) { cmd.SetErr(&bytes.Buffer{}) cmd.SetIn(bytes.NewBufferString(tc.stdin)) - // register persistent flags manually - cmd.Flags().String("workspace", "", "") - cmd.Flags().Bool("update-config", false, "") - cmd.Flags().Bool("yes", false, "") - cmd.Flags().String("name", "constell", "") - cmd.Flags().String("tf-log", "NONE", "") - - if tc.regionFlag != "" { - require.NoError(cmd.Flags().Set("region", tc.regionFlag)) - } - if tc.resourceGroupFlag != "" { - require.NoError(cmd.Flags().Set("resourceGroup", tc.resourceGroupFlag)) - } - if tc.servicePrincipalFlag != "" { - require.NoError(cmd.Flags().Set("servicePrincipal", tc.servicePrincipalFlag)) - } - if tc.yesFlag { - require.NoError(cmd.Flags().Set("yes", "true")) - } - if tc.updateConfigFlag { - require.NoError(cmd.Flags().Set("update-config", "true")) - } - - fileHandler := file.NewHandler(tc.setupFs(require, tc.provider, tc.existingConfigFiles, tc.existingDirs)) + fileHandler := file.NewHandler(tc.setupFs(require, cloudprovider.Azure, tc.existingConfigFiles, tc.existingDirs)) iamCreator := &iamCreator{ - cmd: cmd, - log: logger.NewTest(t), - spinner: &nopSpinner{}, - creator: tc.creator, - fileHandler: fileHandler, - iamConfig: &cloudcmd.IAMConfigOptions{}, - provider: tc.provider, - providerCreator: &azureIAMCreator{}, + cmd: cmd, + log: logger.NewTest(t), + spinner: &nopSpinner{}, + creator: tc.creator, + fileHandler: fileHandler, + iamConfig: &cloudcmd.IAMConfigOptions{}, + provider: cloudprovider.Azure, + flags: iamCreateFlags{ + yes: tc.yesFlag, + updateConfig: tc.updateConfigFlag, + }, + providerCreator: &azureIAMCreator{ + flags: azureIAMCreateFlags{ + region: tc.regionFlag, + resourceGroup: tc.resourceGroupFlag, + servicePrincipal: tc.servicePrincipalFlag, + }, + }, } err := iamCreator.create(cmd.Context()) @@ -519,7 +454,6 @@ func TestIAMCreateGCP(t *testing.T) { testCases := map[string]struct { setupFs func(require *require.Assertions, provider cloudprovider.Provider, existingConfigFiles []string, existingDirs []string) afero.Fs creator *stubIAMCreator - provider cloudprovider.Provider zoneFlag string serviceAccountIDFlag string projectIDFlag string @@ -534,7 +468,6 @@ func TestIAMCreateGCP(t *testing.T) { "iam create gcp": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.GCP, zoneFlag: "europe-west1-a", serviceAccountIDFlag: "constell-test", projectIDFlag: "constell-1234", @@ -543,7 +476,6 @@ func TestIAMCreateGCP(t *testing.T) { "iam create gcp with existing config": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.GCP, zoneFlag: "europe-west1-a", serviceAccountIDFlag: "constell-test", projectIDFlag: "constell-1234", @@ -553,7 +485,6 @@ func TestIAMCreateGCP(t *testing.T) { "iam create gcp --update-config": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.GCP, zoneFlag: "europe-west1-a", serviceAccountIDFlag: "constell-test", projectIDFlag: "constell-1234", @@ -564,7 +495,6 @@ func TestIAMCreateGCP(t *testing.T) { "iam create gcp existing terraform dir": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.GCP, zoneFlag: "europe-west1-a", serviceAccountIDFlag: "constell-test", projectIDFlag: "constell-1234", @@ -573,18 +503,9 @@ func TestIAMCreateGCP(t *testing.T) { yesFlag: true, wantErr: true, }, - "iam create gcp invalid flags": { - setupFs: defaultFs, - creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.GCP, - zoneFlag: "-a", - yesFlag: true, - wantErr: true, - }, "iam create gcp invalid b64": { setupFs: defaultFs, creator: &stubIAMCreator{id: invalidIAMIDFile}, - provider: cloudprovider.GCP, zoneFlag: "europe-west1-a", serviceAccountIDFlag: "constell-test", projectIDFlag: "constell-1234", @@ -594,7 +515,6 @@ func TestIAMCreateGCP(t *testing.T) { "interactive": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.GCP, zoneFlag: "europe-west1-a", serviceAccountIDFlag: "constell-test", projectIDFlag: "constell-1234", @@ -603,7 +523,6 @@ func TestIAMCreateGCP(t *testing.T) { "interactive update config": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.GCP, zoneFlag: "europe-west1-a", serviceAccountIDFlag: "constell-test", projectIDFlag: "constell-1234", @@ -614,7 +533,6 @@ func TestIAMCreateGCP(t *testing.T) { "interactive abort": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.GCP, zoneFlag: "europe-west1-a", serviceAccountIDFlag: "constell-test", projectIDFlag: "constell-1234", @@ -624,7 +542,6 @@ func TestIAMCreateGCP(t *testing.T) { "interactive abort update config": { setupFs: defaultFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.GCP, zoneFlag: "europe-west1-a", serviceAccountIDFlag: "constell-test", projectIDFlag: "constell-1234", @@ -636,7 +553,6 @@ func TestIAMCreateGCP(t *testing.T) { "unwritable fs": { setupFs: readOnlyFs, creator: &stubIAMCreator{id: validIAMIDFile}, - provider: cloudprovider.GCP, zoneFlag: "europe-west1-a", serviceAccountIDFlag: "constell-test", projectIDFlag: "constell-1234", @@ -656,40 +572,27 @@ func TestIAMCreateGCP(t *testing.T) { cmd.SetErr(&bytes.Buffer{}) cmd.SetIn(bytes.NewBufferString(tc.stdin)) - // register persistent flags manually - cmd.Flags().String("workspace", "", "") - cmd.Flags().Bool("update-config", false, "") - cmd.Flags().Bool("yes", false, "") - cmd.Flags().String("name", "constell", "") - cmd.Flags().String("tf-log", "NONE", "") - - if tc.zoneFlag != "" { - require.NoError(cmd.Flags().Set("zone", tc.zoneFlag)) - } - if tc.serviceAccountIDFlag != "" { - require.NoError(cmd.Flags().Set("serviceAccountID", tc.serviceAccountIDFlag)) - } - if tc.projectIDFlag != "" { - require.NoError(cmd.Flags().Set("projectID", tc.projectIDFlag)) - } - if tc.yesFlag { - require.NoError(cmd.Flags().Set("yes", "true")) - } - if tc.updateConfigFlag { - require.NoError(cmd.Flags().Set("update-config", "true")) - } - - fileHandler := file.NewHandler(tc.setupFs(require, tc.provider, tc.existingConfigFiles, tc.existingDirs)) + fileHandler := file.NewHandler(tc.setupFs(require, cloudprovider.GCP, tc.existingConfigFiles, tc.existingDirs)) iamCreator := &iamCreator{ - cmd: cmd, - log: logger.NewTest(t), - spinner: &nopSpinner{}, - creator: tc.creator, - fileHandler: fileHandler, - iamConfig: &cloudcmd.IAMConfigOptions{}, - provider: tc.provider, - providerCreator: &gcpIAMCreator{}, + cmd: cmd, + log: logger.NewTest(t), + spinner: &nopSpinner{}, + creator: tc.creator, + fileHandler: fileHandler, + iamConfig: &cloudcmd.IAMConfigOptions{}, + provider: cloudprovider.GCP, + flags: iamCreateFlags{ + yes: tc.yesFlag, + updateConfig: tc.updateConfigFlag, + }, + providerCreator: &gcpIAMCreator{ + flags: gcpIAMCreateFlags{ + zone: tc.zoneFlag, + serviceAccountID: tc.serviceAccountIDFlag, + projectID: tc.projectIDFlag, + }, + }, } err := iamCreator.create(cmd.Context()) @@ -724,7 +627,7 @@ func TestValidateConfigWithFlagCompatibility(t *testing.T) { testCases := map[string]struct { iamProvider cloudprovider.Provider cfg config.Config - flags iamFlags + zone string wantErr bool }{ "AWS valid when cfg.zone == flag.zone": { @@ -734,20 +637,12 @@ func TestValidateConfigWithFlagCompatibility(t *testing.T) { cfg.Provider.AWS.Zone = "europe-west-1a" return *cfg }(), - flags: iamFlags{ - aws: awsFlags{ - zone: "europe-west-1a", - }, - }, + zone: "europe-west-1a", }, "AWS valid when cfg.zone not set": { iamProvider: cloudprovider.AWS, cfg: *createConfig(cloudprovider.AWS), - flags: iamFlags{ - aws: awsFlags{ - zone: "europe-west-1a", - }, - }, + zone: "europe-west-1a", }, "GCP invalid when cfg.zone != flag.zone": { iamProvider: cloudprovider.GCP, @@ -756,11 +651,7 @@ func TestValidateConfigWithFlagCompatibility(t *testing.T) { cfg.Provider.GCP.Zone = "europe-west-1a" return *cfg }(), - flags: iamFlags{ - aws: awsFlags{ - zone: "us-west-1a", - }, - }, + zone: "us-west-1a", wantErr: true, }, "Azure invalid when cfg.zone != flag.zone": { @@ -770,11 +661,7 @@ func TestValidateConfigWithFlagCompatibility(t *testing.T) { cfg.Provider.Azure.Location = "europe-west-1a" return *cfg }(), - flags: iamFlags{ - aws: awsFlags{ - zone: "us-west-1a", - }, - }, + zone: "us-west-1a", wantErr: true, }, "GCP invalid when cfg.provider different from iam provider": { @@ -786,7 +673,7 @@ func TestValidateConfigWithFlagCompatibility(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { assert := assert.New(t) - err := validateConfigWithFlagCompatibility(tc.iamProvider, tc.cfg, tc.flags) + err := validateConfigWithFlagCompatibility(tc.iamProvider, tc.cfg, tc.zone) if tc.wantErr { assert.Error(err) return diff --git a/cli/internal/cmd/iamcreateaws.go b/cli/internal/cmd/iamcreateaws.go new file mode 100644 index 0000000000..b648b87fc1 --- /dev/null +++ b/cli/internal/cmd/iamcreateaws.go @@ -0,0 +1,121 @@ +/* +Copyright (c) Edgeless Systems GmbH +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package cmd + +import ( + "errors" + "fmt" + + "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" + "github.com/edgelesssys/constellation/v2/internal/config" + "github.com/edgelesssys/constellation/v2/internal/file" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// newIAMCreateAWSCmd returns a new cobra.Command for the iam create aws command. +func newIAMCreateAWSCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "aws", + Short: "Create IAM configuration on AWS for your Constellation cluster", + Long: "Create IAM configuration on AWS for your Constellation cluster.", + Args: cobra.ExactArgs(0), + RunE: runIAMCreateAWS, + } + + cmd.Flags().String("prefix", "", "name prefix for all resources (required)") + must(cobra.MarkFlagRequired(cmd.Flags(), "prefix")) + cmd.Flags().String("zone", "", "AWS availability zone the resources will be created in, e.g., us-east-2a (required)\n"+ + "See the Constellation docs for a list of currently supported regions.") + must(cobra.MarkFlagRequired(cmd.Flags(), "zone")) + return cmd +} + +func runIAMCreateAWS(cmd *cobra.Command, _ []string) error { + creator := &awsIAMCreator{} + if err := creator.flags.parse(cmd.Flags()); err != nil { + return err + } + return runIAMCreate(cmd, creator, cloudprovider.AWS) +} + +// awsIAMCreateFlags contains the parsed flags of the iam create aws command. +type awsIAMCreateFlags struct { + prefix string + region string + zone string +} + +func (f *awsIAMCreateFlags) parse(flags *pflag.FlagSet) error { + var err error + f.prefix, err = flags.GetString("prefix") + if err != nil { + return fmt.Errorf("getting 'prefix' flag: %w", err) + } + if len(f.prefix) > 36 { + return errors.New("prefix must be 36 characters or less") + } + f.zone, err = flags.GetString("zone") + if err != nil { + return fmt.Errorf("getting 'zone' flag: %w", err) + } + if !config.ValidateAWSZone(f.zone) { + return errors.New("invalid AWS zone. To find a valid zone, please refer to our docs and https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-availability-zones") + } + // Infer region from zone. + f.region = f.zone[:len(f.zone)-1] + if !config.ValidateAWSRegion(f.region) { + return fmt.Errorf("invalid AWS region: %s", f.region) + } + + return nil +} + +// awsIAMCreator implements the providerIAMCreator interface for AWS. +type awsIAMCreator struct { + flags awsIAMCreateFlags +} + +func (c *awsIAMCreator) getIAMConfigOptions() *cloudcmd.IAMConfigOptions { + return &cloudcmd.IAMConfigOptions{ + AWS: cloudcmd.AWSIAMConfig{ + Region: c.flags.region, + Prefix: c.flags.prefix, + }, + } +} + +func (c *awsIAMCreator) printConfirmValues(cmd *cobra.Command) { + cmd.Printf("Region:\t\t%s\n", c.flags.region) + cmd.Printf("Name Prefix:\t%s\n\n", c.flags.prefix) +} + +func (c *awsIAMCreator) printOutputValues(cmd *cobra.Command, iamFile cloudcmd.IAMOutput) { + cmd.Printf("region:\t\t\t%s\n", c.flags.region) + cmd.Printf("zone:\t\t\t%s\n", c.flags.zone) + cmd.Printf("iamProfileControlPlane:\t%s\n", iamFile.AWSOutput.ControlPlaneInstanceProfile) + cmd.Printf("iamProfileWorkerNodes:\t%s\n\n", iamFile.AWSOutput.WorkerNodeInstanceProfile) +} + +func (c *awsIAMCreator) writeOutputValuesToConfig(conf *config.Config, iamFile cloudcmd.IAMOutput) { + conf.Provider.AWS.Region = c.flags.region + conf.Provider.AWS.Zone = c.flags.zone + conf.Provider.AWS.IAMProfileControlPlane = iamFile.AWSOutput.ControlPlaneInstanceProfile + conf.Provider.AWS.IAMProfileWorkerNodes = iamFile.AWSOutput.WorkerNodeInstanceProfile + for groupName, group := range conf.NodeGroups { + group.Zone = c.flags.zone + conf.NodeGroups[groupName] = group + } +} + +func (c *awsIAMCreator) parseAndWriteIDFile(_ cloudcmd.IAMOutput, _ file.Handler) error { + return nil +} + +func (c *awsIAMCreator) validateConfigWithFlagCompatibility(conf config.Config) error { + return validateConfigWithFlagCompatibility(cloudprovider.AWS, conf, c.flags.zone) +} diff --git a/cli/internal/cmd/iamcreateazure.go b/cli/internal/cmd/iamcreateazure.go new file mode 100644 index 0000000000..6c82c10f63 --- /dev/null +++ b/cli/internal/cmd/iamcreateazure.go @@ -0,0 +1,113 @@ +/* +Copyright (c) Edgeless Systems GmbH +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package cmd + +import ( + "fmt" + + "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" + "github.com/edgelesssys/constellation/v2/internal/config" + "github.com/edgelesssys/constellation/v2/internal/file" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// newIAMCreateAzureCmd returns a new cobra.Command for the iam create azure command. +func newIAMCreateAzureCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "azure", + Short: "Create IAM configuration on Microsoft Azure for your Constellation cluster", + Long: "Create IAM configuration on Microsoft Azure for your Constellation cluster.", + Args: cobra.ExactArgs(0), + RunE: runIAMCreateAzure, + } + + cmd.Flags().String("resourceGroup", "", "name prefix of the two resource groups your cluster / IAM resources will be created in (required)") + must(cobra.MarkFlagRequired(cmd.Flags(), "resourceGroup")) + cmd.Flags().String("region", "", "region the resources will be created in, e.g., westus (required)") + must(cobra.MarkFlagRequired(cmd.Flags(), "region")) + cmd.Flags().String("servicePrincipal", "", "name of the service principal that will be created (required)") + must(cobra.MarkFlagRequired(cmd.Flags(), "servicePrincipal")) + return cmd +} + +func runIAMCreateAzure(cmd *cobra.Command, _ []string) error { + creator := &azureIAMCreator{} + if err := creator.flags.parse(cmd.Flags()); err != nil { + return err + } + return runIAMCreate(cmd, creator, cloudprovider.Azure) +} + +// azureIAMCreateFlags contains the parsed flags of the iam create azure command. +type azureIAMCreateFlags struct { + region string + resourceGroup string + servicePrincipal string +} + +func (f *azureIAMCreateFlags) parse(flags *pflag.FlagSet) error { + var err error + f.region, err = flags.GetString("region") + if err != nil { + return fmt.Errorf("getting 'region' flag: %w", err) + } + f.resourceGroup, err = flags.GetString("resourceGroup") + if err != nil { + return fmt.Errorf("getting 'resourceGroup' flag: %w", err) + } + f.servicePrincipal, err = flags.GetString("servicePrincipal") + if err != nil { + return fmt.Errorf("getting 'servicePrincipal' flag: %w", err) + } + return nil +} + +// azureIAMCreator implements the providerIAMCreator interface for Azure. +type azureIAMCreator struct { + flags azureIAMCreateFlags +} + +func (c *azureIAMCreator) getIAMConfigOptions() *cloudcmd.IAMConfigOptions { + return &cloudcmd.IAMConfigOptions{ + Azure: cloudcmd.AzureIAMConfig{ + Region: c.flags.region, + ResourceGroup: c.flags.resourceGroup, + ServicePrincipal: c.flags.servicePrincipal, + }, + } +} + +func (c *azureIAMCreator) printConfirmValues(cmd *cobra.Command) { + cmd.Printf("Region:\t\t\t%s\n", c.flags.region) + cmd.Printf("Resource Group:\t\t%s\n", c.flags.resourceGroup) + cmd.Printf("Service Principal:\t%s\n\n", c.flags.servicePrincipal) +} + +func (c *azureIAMCreator) printOutputValues(cmd *cobra.Command, iamFile cloudcmd.IAMOutput) { + cmd.Printf("subscription:\t\t%s\n", iamFile.AzureOutput.SubscriptionID) + cmd.Printf("tenant:\t\t\t%s\n", iamFile.AzureOutput.TenantID) + cmd.Printf("location:\t\t%s\n", c.flags.region) + cmd.Printf("resourceGroup:\t\t%s\n", c.flags.resourceGroup) + cmd.Printf("userAssignedIdentity:\t%s\n", iamFile.AzureOutput.UAMIID) +} + +func (c *azureIAMCreator) writeOutputValuesToConfig(conf *config.Config, iamFile cloudcmd.IAMOutput) { + conf.Provider.Azure.SubscriptionID = iamFile.AzureOutput.SubscriptionID + conf.Provider.Azure.TenantID = iamFile.AzureOutput.TenantID + conf.Provider.Azure.Location = c.flags.region + conf.Provider.Azure.ResourceGroup = c.flags.resourceGroup + conf.Provider.Azure.UserAssignedIdentity = iamFile.AzureOutput.UAMIID +} + +func (c *azureIAMCreator) parseAndWriteIDFile(_ cloudcmd.IAMOutput, _ file.Handler) error { + return nil +} + +func (c *azureIAMCreator) validateConfigWithFlagCompatibility(conf config.Config) error { + return validateConfigWithFlagCompatibility(cloudprovider.Azure, conf, c.flags.region) +} diff --git a/cli/internal/cmd/iamcreategcp.go b/cli/internal/cmd/iamcreategcp.go new file mode 100644 index 0000000000..b6c55e5d1f --- /dev/null +++ b/cli/internal/cmd/iamcreategcp.go @@ -0,0 +1,153 @@ +/* +Copyright (c) Edgeless Systems GmbH +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package cmd + +import ( + "fmt" + "strings" + + "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" + "github.com/edgelesssys/constellation/v2/internal/config" + "github.com/edgelesssys/constellation/v2/internal/constants" + "github.com/edgelesssys/constellation/v2/internal/file" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// NewIAMCreateGCPCmd returns a new cobra.Command for the iam create gcp command. +func newIAMCreateGCPCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "gcp", + Short: "Create IAM configuration on GCP for your Constellation cluster", + Long: "Create IAM configuration on GCP for your Constellation cluster.", + Args: cobra.ExactArgs(0), + RunE: runIAMCreateGCP, + } + + cmd.Flags().String("zone", "", "GCP zone the cluster will be deployed in (required)\n"+ + "Find a list of available zones here: https://cloud.google.com/compute/docs/regions-zones#available") + must(cobra.MarkFlagRequired(cmd.Flags(), "zone")) + cmd.Flags().String("serviceAccountID", "", "ID for the service account that will be created (required)\n"+ + "Must be 6 to 30 lowercase letters, digits, or hyphens.") + must(cobra.MarkFlagRequired(cmd.Flags(), "serviceAccountID")) + cmd.Flags().String("projectID", "", "ID of the GCP project the configuration will be created in (required)\n"+ + "Find it on the welcome screen of your project: https://console.cloud.google.com/welcome") + must(cobra.MarkFlagRequired(cmd.Flags(), "projectID")) + + return cmd +} + +func runIAMCreateGCP(cmd *cobra.Command, _ []string) error { + creator := &gcpIAMCreator{} + if err := creator.flags.parse(cmd.Flags()); err != nil { + return err + } + return runIAMCreate(cmd, creator, cloudprovider.GCP) +} + +// gcpIAMCreateFlags contains the parsed flags of the iam create gcp command. +type gcpIAMCreateFlags struct { + rootFlags + serviceAccountID string + zone string + region string + projectID string +} + +func (f *gcpIAMCreateFlags) parse(flags *pflag.FlagSet) error { + var err error + if err = f.rootFlags.parse(flags); err != nil { + return err + } + + f.zone, err = flags.GetString("zone") + if err != nil { + return fmt.Errorf("getting 'zone' flag: %w", err) + } + if !zoneRegex.MatchString(f.zone) { + return fmt.Errorf("invalid zone string: %s", f.zone) + } + + // Infer region from zone. + zoneParts := strings.Split(f.zone, "-") + f.region = fmt.Sprintf("%s-%s", zoneParts[0], zoneParts[1]) + if !regionRegex.MatchString(f.region) { + return fmt.Errorf("invalid region string: %s", f.region) + } + + f.projectID, err = flags.GetString("projectID") + if err != nil { + return fmt.Errorf("getting 'projectID' flag: %w", err) + } + if !gcpIDRegex.MatchString(f.projectID) { + return fmt.Errorf("projectID %q doesn't match %s", f.projectID, gcpIDRegex) + } + + f.serviceAccountID, err = flags.GetString("serviceAccountID") + if err != nil { + return fmt.Errorf("getting 'serviceAccountID' flag: %w", err) + } + if !gcpIDRegex.MatchString(f.serviceAccountID) { + return fmt.Errorf("serviceAccountID %q doesn't match %s", f.serviceAccountID, gcpIDRegex) + } + return nil +} + +// gcpIAMCreator implements the providerIAMCreator interface for GCP. +type gcpIAMCreator struct { + flags gcpIAMCreateFlags +} + +func (c *gcpIAMCreator) getIAMConfigOptions() *cloudcmd.IAMConfigOptions { + return &cloudcmd.IAMConfigOptions{ + GCP: cloudcmd.GCPIAMConfig{ + Zone: c.flags.zone, + Region: c.flags.region, + ProjectID: c.flags.projectID, + ServiceAccountID: c.flags.serviceAccountID, + }, + } +} + +func (c *gcpIAMCreator) printConfirmValues(cmd *cobra.Command) { + cmd.Printf("Project ID:\t\t%s\n", c.flags.projectID) + cmd.Printf("Service Account ID:\t%s\n", c.flags.serviceAccountID) + cmd.Printf("Region:\t\t\t%s\n", c.flags.region) + cmd.Printf("Zone:\t\t\t%s\n\n", c.flags.zone) +} + +func (c *gcpIAMCreator) printOutputValues(cmd *cobra.Command, _ cloudcmd.IAMOutput) { + cmd.Printf("projectID:\t\t%s\n", c.flags.projectID) + cmd.Printf("region:\t\t\t%s\n", c.flags.region) + cmd.Printf("zone:\t\t\t%s\n", c.flags.zone) + cmd.Printf("serviceAccountKeyPath:\t%s\n\n", c.flags.pathPrefixer.PrefixPrintablePath(constants.GCPServiceAccountKeyFilename)) +} + +func (c *gcpIAMCreator) writeOutputValuesToConfig(conf *config.Config, _ cloudcmd.IAMOutput) { + conf.Provider.GCP.Project = c.flags.projectID + conf.Provider.GCP.ServiceAccountKeyPath = constants.GCPServiceAccountKeyFilename // File was created in workspace, so only the filename is needed. + conf.Provider.GCP.Region = c.flags.region + conf.Provider.GCP.Zone = c.flags.zone + for groupName, group := range conf.NodeGroups { + group.Zone = c.flags.zone + conf.NodeGroups[groupName] = group + } +} + +func (c *gcpIAMCreator) parseAndWriteIDFile(iamFile cloudcmd.IAMOutput, fileHandler file.Handler) error { + // GCP needs to write the service account key to a file. + tmpOut, err := parseIDFile(iamFile.GCPOutput.ServiceAccountKey) + if err != nil { + return err + } + + return fileHandler.WriteJSON(constants.GCPServiceAccountKeyFilename, tmpOut, file.OptNone) +} + +func (c *gcpIAMCreator) validateConfigWithFlagCompatibility(conf config.Config) error { + return validateConfigWithFlagCompatibility(cloudprovider.GCP, conf, c.flags.zone) +} diff --git a/cli/internal/cmd/iamdestroy.go b/cli/internal/cmd/iamdestroy.go index 72bf315fad..667218b810 100644 --- a/cli/internal/cmd/iamdestroy.go +++ b/cli/internal/cmd/iamdestroy.go @@ -11,13 +11,12 @@ import ( "os" "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" - "github.com/edgelesssys/constellation/v2/cli/internal/cmd/pathprefix" - "github.com/edgelesssys/constellation/v2/cli/internal/terraform" "github.com/edgelesssys/constellation/v2/internal/cloud/gcpshared" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" "github.com/spf13/afero" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) // NewIAMDestroyCmd returns a new cobra.Command for the iam destroy subcommand. @@ -35,6 +34,25 @@ func newIAMDestroyCmd() *cobra.Command { return cmd } +type iamDestroyFlags struct { + rootFlags + yes bool +} + +func (f *iamDestroyFlags) parse(flags *pflag.FlagSet) error { + if err := f.rootFlags.parse(flags); err != nil { + return err + } + + yes, err := flags.GetBool("yes") + if err != nil { + return fmt.Errorf("getting 'yes' flag: %w", err) + } + f.yes = yes + + return nil +} + func runIAMDestroy(cmd *cobra.Command, _ []string) error { log, err := newCLILogger(cmd) if err != nil { @@ -46,51 +64,47 @@ func runIAMDestroy(cmd *cobra.Command, _ []string) error { fsHandler := file.NewHandler(afero.NewOsFs()) c := &destroyCmd{log: log} + if err := c.flags.parse(cmd.Flags()); err != nil { + return err + } return c.iamDestroy(cmd, spinner, destroyer, fsHandler) } type destroyCmd struct { - log debugLog - pf pathprefix.PathPrefixer + log debugLog + flags iamDestroyFlags } func (c *destroyCmd) iamDestroy(cmd *cobra.Command, spinner spinnerInterf, destroyer iamDestroyer, fsHandler file.Handler) error { - flags, err := c.parseDestroyFlags(cmd) - if err != nil { - return fmt.Errorf("parsing flags: %w", err) - } - // check if there is a possibility that the cluster is still running by looking out for specific files - c.log.Debugf("Checking if %q exists", c.pf.PrefixPrintablePath(constants.AdminConfFilename)) - _, err = fsHandler.Stat(constants.AdminConfFilename) - if !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("file %q still exists, please make sure to terminate your cluster before destroying your IAM configuration", c.pf.PrefixPrintablePath(constants.AdminConfFilename)) + c.log.Debugf("Checking if %q exists", c.flags.pathPrefixer.PrefixPrintablePath(constants.AdminConfFilename)) + if _, err := fsHandler.Stat(constants.AdminConfFilename); !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("file %q still exists, please make sure to terminate your cluster before destroying your IAM configuration", c.flags.pathPrefixer.PrefixPrintablePath(constants.AdminConfFilename)) } - c.log.Debugf("Checking if %q exists", c.pf.PrefixPrintablePath(constants.StateFilename)) - _, err = fsHandler.Stat(constants.StateFilename) - if !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("file %q still exists, please make sure to terminate your cluster before destroying your IAM configuration", c.pf.PrefixPrintablePath(constants.StateFilename)) + + c.log.Debugf("Checking if %q exists", c.flags.pathPrefixer.PrefixPrintablePath(constants.StateFilename)) + if _, err := fsHandler.Stat(constants.StateFilename); !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("file %q still exists, please make sure to terminate your cluster before destroying your IAM configuration", c.flags.pathPrefixer.PrefixPrintablePath(constants.StateFilename)) } gcpFileExists := false - c.log.Debugf("Checking if %q exists", c.pf.PrefixPrintablePath(constants.GCPServiceAccountKeyFilename)) - _, err = fsHandler.Stat(constants.GCPServiceAccountKeyFilename) - if err != nil { + c.log.Debugf("Checking if %q exists", c.flags.pathPrefixer.PrefixPrintablePath(constants.GCPServiceAccountKeyFilename)) + if _, err := fsHandler.Stat(constants.GCPServiceAccountKeyFilename); err != nil { if !errors.Is(err, os.ErrNotExist) { return err } } else { - c.log.Debugf("%q exists", c.pf.PrefixPrintablePath(constants.GCPServiceAccountKeyFilename)) + c.log.Debugf("%q exists", c.flags.pathPrefixer.PrefixPrintablePath(constants.GCPServiceAccountKeyFilename)) gcpFileExists = true } - if !flags.yes { + if !c.flags.yes { // Confirmation confirmString := "Do you really want to destroy your IAM configuration? Note that this will remove all resources in the resource group." if gcpFileExists { - confirmString += fmt.Sprintf("\nThis will also delete %q", c.pf.PrefixPrintablePath(constants.GCPServiceAccountKeyFilename)) + confirmString += fmt.Sprintf("\nThis will also delete %q", c.flags.pathPrefixer.PrefixPrintablePath(constants.GCPServiceAccountKeyFilename)) } ok, err := askToConfirm(cmd, confirmString) if err != nil { @@ -103,7 +117,7 @@ func (c *destroyCmd) iamDestroy(cmd *cobra.Command, spinner spinnerInterf, destr } if gcpFileExists { - c.log.Debugf("Starting to delete %q", c.pf.PrefixPrintablePath(constants.GCPServiceAccountKeyFilename)) + c.log.Debugf("Starting to delete %q", c.flags.pathPrefixer.PrefixPrintablePath(constants.GCPServiceAccountKeyFilename)) proceed, err := c.deleteGCPServiceAccountKeyFile(cmd, destroyer, fsHandler) if err != nil { return err @@ -118,7 +132,7 @@ func (c *destroyCmd) iamDestroy(cmd *cobra.Command, spinner spinnerInterf, destr spinner.Start("Destroying IAM configuration", false) defer spinner.Stop() - if err := destroyer.DestroyIAMConfiguration(cmd.Context(), constants.TerraformIAMWorkingDir, flags.tfLogLevel); err != nil { + if err := destroyer.DestroyIAMConfiguration(cmd.Context(), constants.TerraformIAMWorkingDir, c.flags.tfLogLevel); err != nil { return fmt.Errorf("destroying IAM configuration: %w", err) } @@ -130,7 +144,7 @@ func (c *destroyCmd) iamDestroy(cmd *cobra.Command, spinner spinnerInterf, destr func (c *destroyCmd) deleteGCPServiceAccountKeyFile(cmd *cobra.Command, destroyer iamDestroyer, fsHandler file.Handler) (bool, error) { var fileSaKey gcpshared.ServiceAccountKey - c.log.Debugf("Parsing %q", c.pf.PrefixPrintablePath(constants.GCPServiceAccountKeyFilename)) + c.log.Debugf("Parsing %q", c.flags.pathPrefixer.PrefixPrintablePath(constants.GCPServiceAccountKeyFilename)) if err := fsHandler.ReadJSON(constants.GCPServiceAccountKeyFilename, &fileSaKey); err != nil { return false, err } @@ -143,7 +157,11 @@ func (c *destroyCmd) deleteGCPServiceAccountKeyFile(cmd *cobra.Command, destroye c.log.Debugf("Checking if keys are the same") if tfSaKey != fileSaKey { - cmd.Printf("The key in %q don't match up with your Terraform state. %q will not be deleted.\n", c.pf.PrefixPrintablePath(constants.GCPServiceAccountKeyFilename), c.pf.PrefixPrintablePath(constants.GCPServiceAccountKeyFilename)) + cmd.Printf( + "The key in %q don't match up with your Terraform state. %q will not be deleted.\n", + c.flags.pathPrefixer.PrefixPrintablePath(constants.GCPServiceAccountKeyFilename), + c.flags.pathPrefixer.PrefixPrintablePath(constants.GCPServiceAccountKeyFilename), + ) return true, nil } @@ -151,42 +169,6 @@ func (c *destroyCmd) deleteGCPServiceAccountKeyFile(cmd *cobra.Command, destroye return false, err } - c.log.Debugf("Successfully deleted %q", c.pf.PrefixPrintablePath(constants.GCPServiceAccountKeyFilename)) + c.log.Debugf("Successfully deleted %q", c.flags.pathPrefixer.PrefixPrintablePath(constants.GCPServiceAccountKeyFilename)) return true, nil } - -type destroyFlags struct { - yes bool - tfLogLevel terraform.LogLevel -} - -// parseDestroyFlags parses the flags of the create command. -func (c *destroyCmd) parseDestroyFlags(cmd *cobra.Command) (destroyFlags, error) { - yes, err := cmd.Flags().GetBool("yes") - if err != nil { - return destroyFlags{}, fmt.Errorf("parsing yes bool: %w", err) - } - c.log.Debugf("Yes flag is %t", yes) - - workDir, err := cmd.Flags().GetString("workspace") - if err != nil { - return destroyFlags{}, fmt.Errorf("parsing workspace string: %w", err) - } - c.log.Debugf("Workspace set to %q", workDir) - c.pf = pathprefix.New(workDir) - - logLevelString, err := cmd.Flags().GetString("tf-log") - if err != nil { - return destroyFlags{}, fmt.Errorf("parsing tf-log string: %w", err) - } - logLevel, err := terraform.ParseLogLevel(logLevelString) - if err != nil { - return destroyFlags{}, fmt.Errorf("parsing Terraform log level %s: %w", logLevelString, err) - } - c.log.Debugf("Terraform logs will be written into %s at level %s", c.pf.PrefixPrintablePath(constants.TerraformWorkingDir), logLevel.String()) - - return destroyFlags{ - tfLogLevel: logLevel, - yes: yes, - }, nil -} diff --git a/cli/internal/cmd/iamdestroy_test.go b/cli/internal/cmd/iamdestroy_test.go index bbcb26c25f..e6dd4feb2e 100644 --- a/cli/internal/cmd/iamdestroy_test.go +++ b/cli/internal/cmd/iamdestroy_test.go @@ -46,52 +46,52 @@ func TestIAMDestroy(t *testing.T) { iamDestroyer *stubIAMDestroyer fh file.Handler stdin string - yesFlag string + yesFlag bool wantErr bool wantDestroyCalled bool }{ "cluster running admin conf": { fh: newFsWithAdminConf(), iamDestroyer: &stubIAMDestroyer{}, - yesFlag: "false", + yesFlag: false, wantErr: true, }, "cluster running cluster state": { fh: newFsWithStateFile(), iamDestroyer: &stubIAMDestroyer{}, - yesFlag: "false", + yesFlag: false, wantErr: true, }, "file missing abort": { fh: newFsMissing(), stdin: "n\n", - yesFlag: "false", + yesFlag: false, iamDestroyer: &stubIAMDestroyer{}, }, "file missing": { fh: newFsMissing(), stdin: "y\n", - yesFlag: "false", + yesFlag: false, iamDestroyer: &stubIAMDestroyer{}, wantDestroyCalled: true, }, "file exists abort": { fh: newFsExists(), stdin: "n\n", - yesFlag: "false", + yesFlag: false, iamDestroyer: &stubIAMDestroyer{}, }, "error destroying user": { fh: newFsMissing(), stdin: "y\n", - yesFlag: "false", + yesFlag: false, iamDestroyer: &stubIAMDestroyer{destroyErr: someError}, wantErr: true, wantDestroyCalled: true, }, "gcp delete error": { fh: newFsExists(), - yesFlag: "true", + yesFlag: true, iamDestroyer: &stubIAMDestroyer{getTfStateKeyErr: someError}, wantErr: true, }, @@ -106,13 +106,9 @@ func TestIAMDestroy(t *testing.T) { cmd.SetErr(&bytes.Buffer{}) cmd.SetIn(bytes.NewBufferString(tc.stdin)) - // register persistent flags manually - cmd.Flags().String("tf-log", "NONE", "") - cmd.Flags().String("workspace", "", "") - - assert.NoError(cmd.Flags().Set("yes", tc.yesFlag)) - - c := &destroyCmd{log: logger.NewTest(t)} + c := &destroyCmd{log: logger.NewTest(t), flags: iamDestroyFlags{ + yes: tc.yesFlag, + }} err := c.iamDestroy(cmd, &nopSpinner{}, tc.iamDestroyer, tc.fh) diff --git a/cli/internal/cmd/iamupgradeapply.go b/cli/internal/cmd/iamupgradeapply.go index 08c6325114..7f1e98544d 100644 --- a/cli/internal/cmd/iamupgradeapply.go +++ b/cli/internal/cmd/iamupgradeapply.go @@ -21,6 +21,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/file" "github.com/spf13/afero" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) func newIAMUpgradeCmd() *cobra.Command { @@ -46,17 +47,32 @@ func newIAMUpgradeApplyCmd() *cobra.Command { return cmd } +type iamUpgradeApplyFlags struct { + rootFlags + yes bool +} + +func (f *iamUpgradeApplyFlags) parse(flags *pflag.FlagSet) error { + if err := f.rootFlags.parse(flags); err != nil { + return err + } + + yes, err := flags.GetBool("yes") + if err != nil { + return fmt.Errorf("getting 'yes' flag: %w", err) + } + f.yes = yes + return nil +} + type iamUpgradeApplyCmd struct { fileHandler file.Handler log debugLog configFetcher attestationconfigapi.Fetcher + flags iamUpgradeApplyFlags } func runIAMUpgradeApply(cmd *cobra.Command, _ []string) error { - force, err := cmd.Flags().GetBool("force") - if err != nil { - return fmt.Errorf("parsing force argument: %w", err) - } fileHandler := file.NewHandler(afero.NewOsFs()) upgradeID := generateUpgradeID(upgradeCmdKindIAM) upgradeDir := filepath.Join(constants.UpgradeDir, upgradeID) @@ -77,22 +93,20 @@ func runIAMUpgradeApply(cmd *cobra.Command, _ []string) error { return fmt.Errorf("setting up logger: %w", err) } - yes, err := cmd.Flags().GetBool("yes") - if err != nil { - return err - } - i := iamUpgradeApplyCmd{ fileHandler: fileHandler, log: log, configFetcher: configFetcher, } + if err := i.flags.parse(cmd.Flags()); err != nil { + return err + } - return i.iamUpgradeApply(cmd, iamMigrateCmd, upgradeDir, force, yes) + return i.iamUpgradeApply(cmd, iamMigrateCmd, upgradeDir) } -func (i iamUpgradeApplyCmd) iamUpgradeApply(cmd *cobra.Command, iamUpgrader iamUpgrader, upgradeDir string, force, yes bool) error { - conf, err := config.New(i.fileHandler, constants.ConfigFilename, i.configFetcher, force) +func (i iamUpgradeApplyCmd) iamUpgradeApply(cmd *cobra.Command, iamUpgrader iamUpgrader, upgradeDir string) error { + conf, err := config.New(i.fileHandler, constants.ConfigFilename, i.configFetcher, i.flags.force) var configValidationErr *config.ValidationError if errors.As(err, &configValidationErr) { cmd.PrintErrln(configValidationErr.LongMessage()) @@ -109,14 +123,14 @@ func (i iamUpgradeApplyCmd) iamUpgradeApply(cmd *cobra.Command, iamUpgrader iamU if err != nil { return fmt.Errorf("planning terraform migrations: %w", err) } - if !hasDiff && !force { + if !hasDiff && !i.flags.force { cmd.Println("No IAM migrations necessary.") return nil } // If there are any Terraform migrations to apply, ask for confirmation cmd.Println("The IAM upgrade requires a migration by applying an updated Terraform template. Please manually review the suggested changes.") - if !yes { + if !i.flags.yes { ok, err := askToConfirm(cmd, "Do you want to apply the IAM upgrade?") if err != nil { return fmt.Errorf("asking for confirmation: %w", err) diff --git a/cli/internal/cmd/iamupgradeapply_test.go b/cli/internal/cmd/iamupgradeapply_test.go index f3b451ae5d..4a6a580969 100644 --- a/cli/internal/cmd/iamupgradeapply_test.go +++ b/cli/internal/cmd/iamupgradeapply_test.go @@ -132,9 +132,12 @@ func TestIamUpgradeApply(t *testing.T) { fileHandler: tc.fh, log: logger.NewTest(t), configFetcher: tc.configFetcher, + flags: iamUpgradeApplyFlags{ + yes: tc.yesFlag, + }, } - err := iamUpgradeApplyCmd.iamUpgradeApply(cmd, tc.iamUpgrader, "", false, tc.yesFlag) + err := iamUpgradeApplyCmd.iamUpgradeApply(cmd, tc.iamUpgrader, "") if tc.wantErr { assert.Error(err) } else { diff --git a/cli/internal/cmd/init.go b/cli/internal/cmd/init.go index 1826a4569e..fae66a0080 100644 --- a/cli/internal/cmd/init.go +++ b/cli/internal/cmd/init.go @@ -28,6 +28,7 @@ import ( "github.com/spf13/afero" "github.com/spf13/cobra" + "github.com/spf13/pflag" "google.golang.org/grpc" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/clientcmd" @@ -36,7 +37,6 @@ import ( "github.com/edgelesssys/constellation/v2/bootstrapper/initproto" "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" - "github.com/edgelesssys/constellation/v2/cli/internal/cmd/pathprefix" "github.com/edgelesssys/constellation/v2/cli/internal/helm" "github.com/edgelesssys/constellation/v2/cli/internal/kubecmd" "github.com/edgelesssys/constellation/v2/cli/internal/state" @@ -69,12 +69,45 @@ func NewInitCmd() *cobra.Command { return cmd } +// initFlags are flags used by the init command. +type initFlags struct { + rootFlags + conformance bool + helmWaitMode helm.WaitMode + mergeConfigs bool +} + +func (f *initFlags) parse(flags *pflag.FlagSet) error { + if err := f.rootFlags.parse(flags); err != nil { + return err + } + + skipHelmWait, err := flags.GetBool("skip-helm-wait") + if err != nil { + return fmt.Errorf("getting 'skip-helm-wait' flag: %w", err) + } + f.helmWaitMode = helm.WaitModeAtomic + if skipHelmWait { + f.helmWaitMode = helm.WaitModeNone + } + + f.conformance, err = flags.GetBool("conformance") + if err != nil { + return fmt.Errorf("getting 'conformance' flag: %w", err) + } + f.mergeConfigs, err = flags.GetBool("merge-kubeconfig") + if err != nil { + return fmt.Errorf("getting 'merge-kubeconfig' flag: %w", err) + } + return nil +} + type initCmd struct { log debugLog merger configMerger spinner spinnerInterf fileHandler file.Handler - pf pathprefix.PathPrefixer + flags initFlags } func newInitCmd(fileHandler file.Handler, spinner spinnerInterf, merger configMerger, log debugLog) *initCmd { @@ -109,6 +142,11 @@ func runInitialize(cmd *cobra.Command, _ []string) error { cmd.SetContext(ctx) i := newInitCmd(fileHandler, spinner, &kubeconfigMerger{log: log}, log) + if err := i.flags.parse(cmd.Flags()); err != nil { + return err + } + i.log.Debugf("Using flags: %+v", i.flags) + fetcher := attestationconfigapi.NewFetcher() newAttestationApplier := func(w io.Writer, kubeConfig string, log debugLog) (attestationConfigApplier, error) { return kubecmd.New(w, kubeConfig, fileHandler, log) @@ -127,13 +165,8 @@ func (i *initCmd) initialize( newAttestationApplier func(io.Writer, string, debugLog) (attestationConfigApplier, error), newHelmClient func(kubeConfigPath string, log debugLog) (helmApplier, error), ) error { - flags, err := i.evalFlagArgs(cmd) - if err != nil { - return err - } - i.log.Debugf("Using flags: %+v", flags) - i.log.Debugf("Loading configuration file from %q", i.pf.PrefixPrintablePath(constants.ConfigFilename)) - conf, err := config.New(i.fileHandler, constants.ConfigFilename, configFetcher, flags.force) + i.log.Debugf("Loading configuration file from %q", i.flags.pathPrefixer.PrefixPrintablePath(constants.ConfigFilename)) + conf, err := config.New(i.fileHandler, constants.ConfigFilename, configFetcher, i.flags.force) var configValidationErr *config.ValidationError if errors.As(err, &configValidationErr) { cmd.PrintErrln(configValidationErr.LongMessage()) @@ -146,7 +179,7 @@ func (i *initCmd) initialize( if err != nil { return err } - if !flags.force { + if !i.flags.force { if err := validateCLIandConstellationVersionAreEqual(constants.BinaryVersion(), conf.Image, conf.MicroserviceVersion); err != nil { return err } @@ -183,7 +216,7 @@ func (i *initCmd) initialize( return fmt.Errorf("creating new validator: %w", err) } i.log.Debugf("Created a new validator") - serviceAccURI, err := cloudcmd.GetMarshaledServiceAccountURI(provider, conf, i.pf, i.log, i.fileHandler) + serviceAccURI, err := cloudcmd.GetMarshaledServiceAccountURI(provider, conf, i.flags.pathPrefixer, i.log, i.fileHandler) if err != nil { return err } @@ -211,7 +244,7 @@ func (i *initCmd) initialize( MeasurementSalt: measurementSalt, KubernetesVersion: versions.VersionConfigs[k8sVersion].ClusterVersion, KubernetesComponents: versions.VersionConfigs[k8sVersion].KubernetesComponents.ToInitProto(), - ConformanceMode: flags.conformance, + ConformanceMode: i.flags.conformance, InitSecret: stateFile.Infrastructure.InitSecret, ClusterName: stateFile.Infrastructure.Name, ApiserverCertSans: stateFile.Infrastructure.APIServerCertSANs, @@ -228,7 +261,7 @@ func (i *initCmd) initialize( if nonRetriable.logCollectionErr != nil { cmd.PrintErrf("Failed to collect logs from bootstrapper: %s\n", nonRetriable.logCollectionErr) } else { - cmd.PrintErrf("Fetched bootstrapper logs are stored in %q\n", i.pf.PrefixPrintablePath(constants.ErrorLog)) + cmd.PrintErrf("Fetched bootstrapper logs are stored in %q\n", i.flags.pathPrefixer.PrefixPrintablePath(constants.ErrorLog)) } } return err @@ -236,7 +269,7 @@ func (i *initCmd) initialize( i.log.Debugf("Initialization request succeeded") bufferedOutput := &bytes.Buffer{} - if err := i.writeOutput(stateFile, resp, flags.mergeConfigs, bufferedOutput, measurementSalt); err != nil { + if err := i.writeOutput(stateFile, resp, i.flags.mergeConfigs, bufferedOutput, measurementSalt); err != nil { return err } @@ -250,9 +283,9 @@ func (i *initCmd) initialize( i.spinner.Start("Installing Kubernetes components ", false) options := helm.Options{ - Force: flags.force, - Conformance: flags.conformance, - HelmWaitMode: flags.helmWaitMode, + Force: i.flags.force, + Conformance: i.flags.conformance, + HelmWaitMode: i.flags.helmWaitMode, AllowDestructive: helm.DenyDestructive, } helmApplier, err := newHelmClient(constants.AdminConfFilename, i.log) @@ -457,7 +490,7 @@ func (i *initCmd) writeOutput( tw := tabwriter.NewWriter(wr, 0, 0, 2, ' ', 0) writeRow(tw, "Constellation cluster identifier", clusterID) - writeRow(tw, "Kubernetes configuration", i.pf.PrefixPrintablePath(constants.AdminConfFilename)) + writeRow(tw, "Kubernetes configuration", i.flags.pathPrefixer.PrefixPrintablePath(constants.AdminConfFilename)) tw.Flush() fmt.Fprintln(wr) @@ -485,7 +518,7 @@ func (i *initCmd) writeOutput( if err := i.fileHandler.Write(constants.AdminConfFilename, kubeconfigBytes, file.OptNone); err != nil { return fmt.Errorf("writing kubeconfig: %w", err) } - i.log.Debugf("Kubeconfig written to %s", i.pf.PrefixPrintablePath(constants.AdminConfFilename)) + i.log.Debugf("Kubeconfig written to %s", i.flags.pathPrefixer.PrefixPrintablePath(constants.AdminConfFilename)) if mergeConfig { if err := i.merger.mergeConfigs(constants.AdminConfFilename, i.fileHandler); err != nil { @@ -500,7 +533,7 @@ func (i *initCmd) writeOutput( return fmt.Errorf("writing Constellation state file: %w", err) } - i.log.Debugf("Constellation state file written to %s", i.pf.PrefixPrintablePath(constants.StateFilename)) + i.log.Debugf("Constellation state file written to %s", i.flags.pathPrefixer.PrefixPrintablePath(constants.StateFilename)) if !mergeConfig { fmt.Fprintln(wr, "You can now connect to your cluster by executing:") @@ -528,57 +561,6 @@ func writeRow(wr io.Writer, col1 string, col2 string) { fmt.Fprint(wr, col1, "\t", col2, "\n") } -// 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 (i *initCmd) evalFlagArgs(cmd *cobra.Command) (initFlags, error) { - conformance, err := cmd.Flags().GetBool("conformance") - if err != nil { - return initFlags{}, fmt.Errorf("parsing conformance flag: %w", err) - } - i.log.Debugf("Conformance flag is %t", conformance) - skipHelmWait, err := cmd.Flags().GetBool("skip-helm-wait") - if err != nil { - return initFlags{}, fmt.Errorf("parsing skip-helm-wait flag: %w", err) - } - helmWaitMode := helm.WaitModeAtomic - if skipHelmWait { - helmWaitMode = helm.WaitModeNone - } - i.log.Debugf("Helm wait flag is %t", skipHelmWait) - workDir, err := cmd.Flags().GetString("workspace") - if err != nil { - return initFlags{}, fmt.Errorf("parsing config path flag: %w", err) - } - i.pf = pathprefix.New(workDir) - - mergeConfigs, err := cmd.Flags().GetBool("merge-kubeconfig") - if err != nil { - return initFlags{}, fmt.Errorf("parsing merge-kubeconfig flag: %w", err) - } - i.log.Debugf("Merge kubeconfig flag is %t", mergeConfigs) - - force, err := cmd.Flags().GetBool("force") - if err != nil { - return initFlags{}, fmt.Errorf("parsing force argument: %w", err) - } - i.log.Debugf("force flag is %t", force) - - return initFlags{ - conformance: conformance, - helmWaitMode: helmWaitMode, - force: force, - mergeConfigs: mergeConfigs, - }, nil -} - -// initFlags are the resulting values of flag preprocessing. -type initFlags struct { - conformance bool - helmWaitMode helm.WaitMode - force bool - mergeConfigs bool -} - // generateMasterSecret reads a base64 encoded master secret from file or generates a new 32 byte secret. func (i *initCmd) generateMasterSecret(outWriter io.Writer) (uri.MasterSecret, error) { // No file given, generate a new secret, and save it to disk @@ -599,7 +581,7 @@ func (i *initCmd) generateMasterSecret(outWriter io.Writer) (uri.MasterSecret, e if err := i.fileHandler.WriteJSON(constants.MasterSecretFilename, secret, file.OptNone); err != nil { return uri.MasterSecret{}, err } - fmt.Fprintf(outWriter, "Your Constellation master secret was successfully written to %q\n", i.pf.PrefixPrintablePath(constants.MasterSecretFilename)) + fmt.Fprintf(outWriter, "Your Constellation master secret was successfully written to %q\n", i.flags.pathPrefixer.PrefixPrintablePath(constants.MasterSecretFilename)) return secret, nil } diff --git a/cli/internal/cmd/init_test.go b/cli/internal/cmd/init_test.go index 59139256b0..7e8603894d 100644 --- a/cli/internal/cmd/init_test.go +++ b/cli/internal/cmd/init_test.go @@ -223,10 +223,6 @@ func TestInitialize(t *testing.T) { var errOut bytes.Buffer cmd.SetErr(&errOut) - // Flags - cmd.Flags().String("workspace", "", "") // register persistent flag manually - cmd.Flags().Bool("force", true, "") // register persistent flag manually - // File system preparation fs := afero.NewMemMapFs() fileHandler := file.NewHandler(fs) @@ -249,6 +245,8 @@ func TestInitialize(t *testing.T) { defer cancel() cmd.SetContext(ctx) i := newInitCmd(fileHandler, &nopSpinner{}, nil, logger.NewTest(t)) + i.flags.force = true + err := i.initialize( cmd, newDialer, @@ -442,15 +440,15 @@ func TestWriteOutput(t *testing.T) { require.NoError(afs.Remove(constants.AdminConfFilename)) // test custom workspace - i.pf = pathprefix.New("/some/path") + i.flags.pathPrefixer = pathprefix.New("/some/path") err = i.writeOutput(stateFile, resp.GetInitSuccess(), true, &out, measurementSalt) require.NoError(err) assert.Contains(out.String(), clusterID) - assert.Contains(out.String(), i.pf.PrefixPrintablePath(constants.AdminConfFilename)) + assert.Contains(out.String(), i.flags.pathPrefixer.PrefixPrintablePath(constants.AdminConfFilename)) out.Reset() // File is written to current working dir, we simply pass the workspace for generating readable user output require.NoError(afs.Remove(constants.AdminConfFilename)) - i.pf = pathprefix.PathPrefixer{} + i.flags.pathPrefixer = pathprefix.PathPrefixer{} // test config merging err = i.writeOutput(stateFile, resp.GetInitSuccess(), true, &out, measurementSalt) diff --git a/cli/internal/cmd/miniup.go b/cli/internal/cmd/miniup.go index 4e4faad55c..f2612a5c6d 100644 --- a/cli/internal/cmd/miniup.go +++ b/cli/internal/cmd/miniup.go @@ -14,13 +14,11 @@ import ( "net" "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" - "github.com/edgelesssys/constellation/v2/cli/internal/cmd/pathprefix" "github.com/edgelesssys/constellation/v2/cli/internal/featureset" "github.com/edgelesssys/constellation/v2/cli/internal/helm" "github.com/edgelesssys/constellation/v2/cli/internal/kubecmd" "github.com/edgelesssys/constellation/v2/cli/internal/libvirt" "github.com/edgelesssys/constellation/v2/cli/internal/state" - "github.com/edgelesssys/constellation/v2/cli/internal/terraform" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/atls" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" @@ -51,6 +49,8 @@ func newMiniUpCmd() *cobra.Command { type miniUpCmd struct { log debugLog configFetcher attestationconfigapi.Fetcher + fileHandler file.Handler + flags rootFlags } func runUp(cmd *cobra.Command, _ []string) error { @@ -66,7 +66,14 @@ func runUp(cmd *cobra.Command, _ []string) error { defer spinner.Stop() creator := cloudcmd.NewCreator(spinner) - m := &miniUpCmd{log: log, configFetcher: attestationconfigapi.NewFetcher()} + m := &miniUpCmd{ + log: log, + configFetcher: attestationconfigapi.NewFetcher(), + fileHandler: file.NewHandler(afero.NewOsFs()), + } + if err := m.flags.parse(cmd.Flags()); err != nil { + return err + } return m.up(cmd, creator, spinner) } @@ -75,22 +82,15 @@ func (m *miniUpCmd) up(cmd *cobra.Command, creator cloudCreator, spinner spinner return fmt.Errorf("system requirements not met: %w", err) } - flags, err := m.parseUpFlags(cmd) - if err != nil { - return fmt.Errorf("parsing flags: %w", err) - } - - fileHandler := file.NewHandler(afero.NewOsFs()) - // create config if not passed as flag and set default values - config, err := m.prepareConfig(cmd, fileHandler, flags) + config, err := m.prepareConfig(cmd) if err != nil { return fmt.Errorf("preparing config: %w", err) } // create cluster spinner.Start("Creating cluster in QEMU ", false) - err = m.createMiniCluster(cmd.Context(), fileHandler, creator, config, flags) + err = m.createMiniCluster(cmd.Context(), creator, config) spinner.Stop() if err != nil { return fmt.Errorf("creating cluster: %w", err) @@ -105,7 +105,7 @@ func (m *miniUpCmd) up(cmd *cobra.Command, creator cloudCreator, spinner spinner cmd.Printf("\tvirsh -c %s\n\n", connectURI) // initialize cluster - if err := m.initializeMiniCluster(cmd, fileHandler, spinner); err != nil { + if err := m.initializeMiniCluster(cmd, spinner); err != nil { return fmt.Errorf("initializing cluster: %w", err) } m.log.Debugf("Initialized cluster") @@ -113,8 +113,8 @@ func (m *miniUpCmd) up(cmd *cobra.Command, creator cloudCreator, spinner spinner } // prepareConfig reads a given config, or creates a new minimal QEMU config. -func (m *miniUpCmd) prepareConfig(cmd *cobra.Command, fileHandler file.Handler, flags upFlags) (*config.Config, error) { - _, err := fileHandler.Stat(constants.ConfigFilename) +func (m *miniUpCmd) prepareConfig(cmd *cobra.Command) (*config.Config, error) { + _, err := m.fileHandler.Stat(constants.ConfigFilename) if err == nil { // config already exists, prompt user if they want to use this file cmd.PrintErrln("A config file already exists in the configured workspace.") @@ -123,7 +123,7 @@ func (m *miniUpCmd) prepareConfig(cmd *cobra.Command, fileHandler file.Handler, return nil, err } if ok { - return m.prepareExistingConfig(cmd, fileHandler, flags) + return m.prepareExistingConfig(cmd) } // user declined to reuse config file, prompt if they want to overwrite it @@ -146,11 +146,11 @@ func (m *miniUpCmd) prepareConfig(cmd *cobra.Command, fileHandler file.Handler, } m.log.Debugf("Prepared configuration") - return config, fileHandler.WriteYAML(constants.ConfigFilename, config, file.OptOverwrite) + return config, m.fileHandler.WriteYAML(constants.ConfigFilename, config, file.OptOverwrite) } -func (m *miniUpCmd) prepareExistingConfig(cmd *cobra.Command, fileHandler file.Handler, flags upFlags) (*config.Config, error) { - conf, err := config.New(fileHandler, constants.ConfigFilename, m.configFetcher, flags.force) +func (m *miniUpCmd) prepareExistingConfig(cmd *cobra.Command) (*config.Config, error) { + conf, err := config.New(m.fileHandler, constants.ConfigFilename, m.configFetcher, m.flags.force) var configValidationErr *config.ValidationError if errors.As(err, &configValidationErr) { cmd.PrintErrln(configValidationErr.LongMessage()) @@ -165,13 +165,13 @@ func (m *miniUpCmd) prepareExistingConfig(cmd *cobra.Command, fileHandler file.H } // createMiniCluster creates a new cluster using the given config. -func (m *miniUpCmd) createMiniCluster(ctx context.Context, fileHandler file.Handler, creator cloudCreator, config *config.Config, flags upFlags) error { +func (m *miniUpCmd) createMiniCluster(ctx context.Context, creator cloudCreator, config *config.Config) error { m.log.Debugf("Creating mini cluster") opts := cloudcmd.CreateOptions{ Provider: cloudprovider.QEMU, Config: config, TFWorkspace: constants.TerraformWorkingDir, - TFLogLevel: flags.tfLogLevel, + TFLogLevel: m.flags.tfLogLevel, } infraState, err := creator.Create(ctx, opts) if err != nil { @@ -184,11 +184,11 @@ func (m *miniUpCmd) createMiniCluster(ctx context.Context, fileHandler file.Hand SetInfrastructure(infraState) m.log.Debugf("Cluster state file contains %v", stateFile) - return stateFile.WriteToFile(fileHandler, constants.StateFilename) + return stateFile.WriteToFile(m.fileHandler, constants.StateFilename) } // initializeMiniCluster initializes a QEMU cluster. -func (m *miniUpCmd) initializeMiniCluster(cmd *cobra.Command, fileHandler file.Handler, spinner spinnerInterf) (retErr error) { +func (m *miniUpCmd) initializeMiniCluster(cmd *cobra.Command, spinner spinnerInterf) (retErr error) { m.log.Debugf("Initializing mini cluster") // clean up cluster resources if initialization fails defer func() { @@ -214,12 +214,17 @@ func (m *miniUpCmd) initializeMiniCluster(cmd *cobra.Command, fileHandler file.H defer log.Sync() newAttestationApplier := func(w io.Writer, kubeConfig string, log debugLog) (attestationConfigApplier, error) { - return kubecmd.New(w, kubeConfig, fileHandler, log) + return kubecmd.New(w, kubeConfig, m.fileHandler, log) } newHelmClient := func(kubeConfigPath string, log debugLog) (helmApplier, error) { return helm.NewClient(kubeConfigPath, log) } // need to defer helm client instantiation until kubeconfig is available - i := newInitCmd(fileHandler, spinner, &kubeconfigMerger{log: log}, log) + + i := newInitCmd(m.fileHandler, spinner, &kubeconfigMerger{log: log}, log) + if err := i.flags.parse(cmd.Flags()); err != nil { + return err + } + if err := i.initialize(cmd, newDialer, license.NewClient(), m.configFetcher, newAttestationApplier, newHelmClient); err != nil { return err @@ -227,37 +232,3 @@ func (m *miniUpCmd) initializeMiniCluster(cmd *cobra.Command, fileHandler file.H m.log.Debugf("Initialized mini cluster") return nil } - -type upFlags struct { - force bool - tfLogLevel terraform.LogLevel -} - -func (m *miniUpCmd) parseUpFlags(cmd *cobra.Command) (upFlags, error) { - m.log.Debugf("Preparing configuration") - workDir, err := cmd.Flags().GetString("workspace") - if err != nil { - return upFlags{}, fmt.Errorf("parsing config string: %w", err) - } - m.log.Debugf("Workspace set to %q", workDir) - force, err := cmd.Flags().GetBool("force") - if err != nil { - return upFlags{}, fmt.Errorf("parsing force bool: %w", err) - } - m.log.Debugf("force flag is %q", force) - - logLevelString, err := cmd.Flags().GetString("tf-log") - if err != nil { - return upFlags{}, fmt.Errorf("parsing tf-log string: %w", err) - } - logLevel, err := terraform.ParseLogLevel(logLevelString) - if err != nil { - return upFlags{}, fmt.Errorf("parsing Terraform log level %s: %w", logLevelString, err) - } - m.log.Debugf("Terraform logs will be written into %s at level %s", pathprefix.New(workDir).PrefixPrintablePath(constants.TerraformLogFile), logLevel.String()) - - return upFlags{ - force: force, - tfLogLevel: logLevel, - }, nil -} diff --git a/cli/internal/cmd/recover.go b/cli/internal/cmd/recover.go index 0f7875edf8..951fc13827 100644 --- a/cli/internal/cmd/recover.go +++ b/cli/internal/cmd/recover.go @@ -16,7 +16,6 @@ import ( "time" "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" - "github.com/edgelesssys/constellation/v2/cli/internal/cmd/pathprefix" "github.com/edgelesssys/constellation/v2/cli/internal/state" "github.com/edgelesssys/constellation/v2/disk-mapper/recoverproto" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" @@ -31,6 +30,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/retry" "github.com/spf13/afero" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) // NewRecoverCmd returns a new cobra.Command for the recover command. @@ -47,10 +47,28 @@ func NewRecoverCmd() *cobra.Command { return cmd } +type recoverFlags struct { + rootFlags + endpoint string +} + +func (f *recoverFlags) parse(flags *pflag.FlagSet) error { + if err := f.rootFlags.parse(flags); err != nil { + return err + } + + endpoint, err := flags.GetString("endpoint") + if err != nil { + return fmt.Errorf("getting 'endpoint' flag: %w", err) + } + f.endpoint = endpoint + return nil +} + type recoverCmd struct { log debugLog configFetcher attestationconfigapi.Fetcher - pf pathprefix.PathPrefixer + flags recoverFlags } func runRecover(cmd *cobra.Command, _ []string) error { @@ -64,6 +82,10 @@ func runRecover(cmd *cobra.Command, _ []string) error { return dialer.New(nil, validator, &net.Dialer{}) } r := &recoverCmd{log: log, configFetcher: attestationconfigapi.NewFetcher()} + if err := r.flags.parse(cmd.Flags()); err != nil { + return err + } + r.log.Debugf("Using flags: %+v", r.flags) return r.recover(cmd, fileHandler, 5*time.Second, &recoverDoer{log: r.log}, newDialer) } @@ -71,20 +93,14 @@ func (r *recoverCmd) recover( cmd *cobra.Command, fileHandler file.Handler, interval time.Duration, doer recoverDoerInterface, newDialer func(validator atls.Validator) *dialer.Dialer, ) error { - flags, err := r.parseRecoverFlags(cmd, fileHandler) - if err != nil { - return err - } - r.log.Debugf("Using flags: %+v", flags) - var masterSecret uri.MasterSecret - r.log.Debugf("Loading master secret file from %s", r.pf.PrefixPrintablePath(constants.MasterSecretFilename)) + r.log.Debugf("Loading master secret file from %s", r.flags.pathPrefixer.PrefixPrintablePath(constants.MasterSecretFilename)) if err := fileHandler.ReadJSON(constants.MasterSecretFilename, &masterSecret); err != nil { return err } - r.log.Debugf("Loading configuration file from %q", r.pf.PrefixPrintablePath(constants.ConfigFilename)) - conf, err := config.New(fileHandler, constants.ConfigFilename, r.configFetcher, flags.force) + r.log.Debugf("Loading configuration file from %q", r.flags.pathPrefixer.PrefixPrintablePath(constants.ConfigFilename)) + conf, err := config.New(fileHandler, constants.ConfigFilename, r.configFetcher, r.flags.force) var configValidationErr *config.ValidationError if errors.As(err, &configValidationErr) { cmd.PrintErrln(configValidationErr.LongMessage()) @@ -99,15 +115,26 @@ func (r *recoverCmd) recover( interval = 20 * time.Second // Azure LB takes a while to remove unhealthy instances } - conf.UpdateMAAURL(flags.maaURL) + stateFile, err := state.ReadFromFile(fileHandler, constants.StateFilename) + if err != nil { + return fmt.Errorf("reading state file: %w", err) + } + endpoint, err := r.parseEndpoint(stateFile) + if err != nil { + return err + } + if stateFile.Infrastructure.Azure != nil { + conf.UpdateMAAURL(stateFile.Infrastructure.Azure.AttestationURL) + } + r.log.Debugf("Creating aTLS Validator for %s", conf.GetAttestationConfig().GetVariant()) validator, err := cloudcmd.NewValidator(cmd, conf.GetAttestationConfig(), r.log) if err != nil { return fmt.Errorf("creating new validator: %w", err) } r.log.Debugf("Created a new validator") - doer.setDialer(newDialer(validator), flags.endpoint) - r.log.Debugf("Set dialer for endpoint %s", flags.endpoint) + doer.setDialer(newDialer(validator), endpoint) + r.log.Debugf("Set dialer for endpoint %s", endpoint) doer.setURIs(masterSecret.EncodeToURI(), uri.NoStoreURI) r.log.Debugf("Set secrets") if err := r.recoverCall(cmd.Context(), cmd.OutOrStdout(), interval, doer); err != nil { @@ -160,6 +187,18 @@ func (r *recoverCmd) recoverCall(ctx context.Context, out io.Writer, interval ti return err } +func (r *recoverCmd) parseEndpoint(state *state.State) (string, error) { + endpoint := r.flags.endpoint + if endpoint == "" { + endpoint = state.Infrastructure.ClusterEndpoint + } + endpoint, err := addPortIfMissing(endpoint, constants.RecoveryPort) + if err != nil { + return "", fmt.Errorf("validating cluster endpoint: %w", err) + } + return endpoint, nil +} + type recoverDoerInterface interface { Do(ctx context.Context) error setDialer(dialer grpcDialer, endpoint string) @@ -209,55 +248,3 @@ func (d *recoverDoer) setURIs(kmsURI, storageURI string) { d.kmsURI = kmsURI d.storageURI = storageURI } - -type recoverFlags struct { - endpoint string - maaURL string - force bool -} - -func (r *recoverCmd) parseRecoverFlags(cmd *cobra.Command, fileHandler file.Handler) (recoverFlags, error) { - workDir, err := cmd.Flags().GetString("workspace") - if err != nil { - return recoverFlags{}, fmt.Errorf("parsing config path argument: %w", err) - } - r.log.Debugf("Workspace set to %q", workDir) - r.pf = pathprefix.New(workDir) - - 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) - } - - force, err := cmd.Flags().GetBool("force") - if err != nil { - return recoverFlags{}, fmt.Errorf("parsing force argument: %w", err) - } - - var attestationURL string - stateFile := state.New() - if endpoint == "" { - stateFile, err = state.ReadFromFile(fileHandler, constants.StateFilename) - if err != nil { - return recoverFlags{}, fmt.Errorf("reading state file: %w", err) - } - endpoint = stateFile.Infrastructure.ClusterEndpoint - } - - endpoint, err = addPortIfMissing(endpoint, constants.RecoveryPort) - if err != nil { - return recoverFlags{}, fmt.Errorf("validating endpoint argument: %w", err) - } - r.log.Debugf("Endpoint value after parsing is %s", endpoint) - - if stateFile.Infrastructure.Azure != nil { - attestationURL = stateFile.Infrastructure.Azure.AttestationURL - } - - return recoverFlags{ - endpoint: endpoint, - maaURL: attestationURL, - force: force, - }, nil -} diff --git a/cli/internal/cmd/recover_test.go b/cli/internal/cmd/recover_test.go index 5dab2807d2..c5ac9a38f2 100644 --- a/cli/internal/cmd/recover_test.go +++ b/cli/internal/cmd/recover_test.go @@ -140,12 +140,9 @@ func TestRecover(t *testing.T) { cmd := NewRecoverCmd() cmd.SetContext(context.Background()) - cmd.Flags().String("workspace", "", "") // register persistent flag manually - cmd.Flags().Bool("force", true, "") // register persistent flag manually out := &bytes.Buffer{} cmd.SetOut(out) cmd.SetErr(out) - require.NoError(cmd.Flags().Set("endpoint", tc.endpoint)) fs := afero.NewMemMapFs() fileHandler := file.NewHandler(fs) @@ -156,13 +153,25 @@ func TestRecover(t *testing.T) { } require.NoError(fileHandler.WriteJSON( - "constellation-mastersecret.json", + constants.MasterSecretFilename, uri.MasterSecret{Key: tc.masterSecret.Secret, Salt: tc.masterSecret.Salt}, file.OptNone, )) + require.NoError(fileHandler.WriteYAML( + constants.StateFilename, + state.New(), + file.OptNone, + )) newDialer := func(atls.Validator) *dialer.Dialer { return nil } - r := &recoverCmd{log: logger.NewTest(t), configFetcher: stubAttestationFetcher{}} + r := &recoverCmd{ + log: logger.NewTest(t), + configFetcher: stubAttestationFetcher{}, + flags: recoverFlags{ + rootFlags: rootFlags{force: true}, + endpoint: tc.endpoint, + }, + } err := r.recover(cmd, fileHandler, time.Millisecond, tc.doer, newDialer) if tc.wantErr { assert.Error(err) @@ -183,68 +192,6 @@ func TestRecover(t *testing.T) { } } -func TestParseRecoverFlags(t *testing.T) { - testCases := map[string]struct { - args []string - wantFlags recoverFlags - writeStateFile bool - wantErr bool - }{ - "no flags": { - wantFlags: recoverFlags{ - endpoint: "192.0.2.42:9999", - }, - writeStateFile: true, - }, - "no flags, no ID file": { - wantFlags: recoverFlags{ - endpoint: "192.0.2.42:9999", - }, - wantErr: true, - }, - "invalid endpoint": { - args: []string{"-e", "192.0.2.42:2:2"}, - wantErr: true, - }, - "all args set": { - args: []string{"-e", "192.0.2.42:2", "--workspace", "./constellation-workspace"}, - wantFlags: recoverFlags{ - endpoint: "192.0.2.42:2", - }, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - cmd := NewRecoverCmd() - cmd.Flags().String("workspace", "", "") // register persistent flag manually - cmd.Flags().Bool("force", false, "") // register persistent flag manually - require.NoError(cmd.ParseFlags(tc.args)) - - fileHandler := file.NewHandler(afero.NewMemMapFs()) - if tc.writeStateFile { - require.NoError( - state.New(). - SetInfrastructure(state.Infrastructure{ClusterEndpoint: "192.0.2.42"}). - WriteToFile(fileHandler, constants.StateFilename), - ) - } - r := &recoverCmd{log: logger.NewTest(t)} - flags, err := r.parseRecoverFlags(cmd, fileHandler) - - if tc.wantErr { - assert.Error(err) - return - } - assert.NoError(err) - assert.Equal(tc.wantFlags, flags) - }) - } -} - func TestDoRecovery(t *testing.T) { testCases := map[string]struct { recoveryServer *stubRecoveryServer diff --git a/cli/internal/cmd/status.go b/cli/internal/cmd/status.go index 06df4b9907..41467e8884 100644 --- a/cli/internal/cmd/status.go +++ b/cli/internal/cmd/status.go @@ -45,11 +45,6 @@ func runStatus(cmd *cobra.Command, _ []string) error { } defer log.Sync() - flags, err := parseStatusFlags(cmd) - if err != nil { - return fmt.Errorf("parsing flags: %w", err) - } - fileHandler := file.NewHandler(afero.NewOsFs()) helmClient, err := helm.NewReleaseVersionClient(constants.AdminConfFilename, log) @@ -61,55 +56,61 @@ func runStatus(cmd *cobra.Command, _ []string) error { } fetcher := attestationconfigapi.NewFetcher() - conf, err := config.New(fileHandler, constants.ConfigFilename, fetcher, flags.force) - var configValidationErr *config.ValidationError - if errors.As(err, &configValidationErr) { - cmd.PrintErrln(configValidationErr.LongMessage()) - } - variant := conf.GetAttestationConfig().GetVariant() - kubeClient, err := kubecmd.New(cmd.OutOrStdout(), constants.AdminConfFilename, fileHandler, log) if err != nil { return fmt.Errorf("setting up kubernetes client: %w", err) } - output, err := status(cmd.Context(), helmVersionGetter, kubeClient, variant) - if err != nil { - return fmt.Errorf("getting status: %w", err) + s := statusCmd{log: log, fileHandler: fileHandler} + if err := s.flags.parse(cmd.Flags()); err != nil { + return err } + return s.status(cmd, helmVersionGetter, kubeClient, fetcher) +} - cmd.Print(output) - return nil +type statusCmd struct { + log debugLog + fileHandler file.Handler + flags rootFlags } // status queries the cluster for the relevant status information and returns the output string. -func status(ctx context.Context, getHelmVersions func() (fmt.Stringer, error), kubeClient kubeCmd, attestVariant variant.Variant, -) (string, error) { - nodeVersion, err := kubeClient.GetConstellationVersion(ctx) +func (s *statusCmd) status( + cmd *cobra.Command, getHelmVersions func() (fmt.Stringer, error), + kubeClient kubeCmd, fetcher attestationconfigapi.Fetcher, +) error { + conf, err := config.New(s.fileHandler, constants.ConfigFilename, fetcher, s.flags.force) + var configValidationErr *config.ValidationError + if errors.As(err, &configValidationErr) { + cmd.PrintErrln(configValidationErr.LongMessage()) + } + + nodeVersion, err := kubeClient.GetConstellationVersion(cmd.Context()) if err != nil { - return "", fmt.Errorf("getting constellation version: %w", err) + return fmt.Errorf("getting constellation version: %w", err) } - attestationConfig, err := kubeClient.GetClusterAttestationConfig(ctx, attestVariant) + attestationConfig, err := kubeClient.GetClusterAttestationConfig(cmd.Context(), conf.GetAttestationConfig().GetVariant()) if err != nil { - return "", fmt.Errorf("getting attestation config: %w", err) + return fmt.Errorf("getting attestation config: %w", err) } prettyYAML, err := yaml.Marshal(attestationConfig) if err != nil { - return "", fmt.Errorf("marshalling attestation config: %w", err) + return fmt.Errorf("marshalling attestation config: %w", err) } serviceVersions, err := getHelmVersions() if err != nil { - return "", fmt.Errorf("getting service versions: %w", err) + return fmt.Errorf("getting service versions: %w", err) } - status, err := kubeClient.ClusterStatus(ctx) + status, err := kubeClient.ClusterStatus(cmd.Context()) if err != nil { - return "", fmt.Errorf("getting cluster status: %w", err) + return fmt.Errorf("getting cluster status: %w", err) } - return statusOutput(nodeVersion, serviceVersions, status, string(prettyYAML)), nil + cmd.Print(statusOutput(nodeVersion, serviceVersions, status, string(prettyYAML))) + return nil } // statusOutput creates the status cmd output string by formatting the received information. @@ -167,26 +168,6 @@ func targetVersionsString(target kubecmd.NodeVersion) string { return builder.String() } -type statusFlags struct { - workspace string - force bool -} - -func parseStatusFlags(cmd *cobra.Command) (statusFlags, error) { - workspace, err := cmd.Flags().GetString("workspace") - if err != nil { - return statusFlags{}, fmt.Errorf("getting config flag: %w", err) - } - force, err := cmd.Flags().GetBool("force") - if err != nil { - return statusFlags{}, fmt.Errorf("getting config flag: %w", err) - } - return statusFlags{ - workspace: workspace, - force: force, - }, nil -} - type kubeCmd interface { ClusterStatus(ctx context.Context) (map[string]kubecmd.NodeStatus, error) GetConstellationVersion(ctx context.Context) (kubecmd.NodeVersion, error) diff --git a/cli/internal/cmd/status_test.go b/cli/internal/cmd/status_test.go index 358f36facd..6d94b6a415 100644 --- a/cli/internal/cmd/status_test.go +++ b/cli/internal/cmd/status_test.go @@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only package cmd import ( + "bytes" "context" "fmt" "testing" @@ -14,8 +15,12 @@ import ( "github.com/edgelesssys/constellation/v2/cli/internal/kubecmd" "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/config" + "github.com/edgelesssys/constellation/v2/internal/constants" + "github.com/edgelesssys/constellation/v2/internal/file" updatev1alpha1 "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/api/v1alpha1" + "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -63,7 +68,6 @@ func TestStatus(t *testing.T) { testCases := map[string]struct { kubeClient stubKubeClient - attestVariant variant.Variant expectedOutput string wantErr bool }{ @@ -104,7 +108,6 @@ func TestStatus(t *testing.T) { }, }, }, - attestVariant: variant.QEMUVTPM{}, expectedOutput: successOutput, }, "one of two nodes not upgraded": { @@ -157,7 +160,6 @@ func TestStatus(t *testing.T) { }, }, }, - attestVariant: variant.QEMUVTPM{}, expectedOutput: inProgressOutput, }, "error getting node status": { @@ -183,7 +185,6 @@ func TestStatus(t *testing.T) { }, }, }, - attestVariant: variant.QEMUVTPM{}, expectedOutput: successOutput, wantErr: true, }, @@ -211,7 +212,6 @@ func TestStatus(t *testing.T) { }, }, }, - attestVariant: variant.QEMUVTPM{}, expectedOutput: successOutput, wantErr: true, }, @@ -248,7 +248,6 @@ func TestStatus(t *testing.T) { }), attestationErr: assert.AnError, }, - attestVariant: variant.QEMUVTPM{}, expectedOutput: successOutput, wantErr: true, }, @@ -259,19 +258,31 @@ func TestStatus(t *testing.T) { require := require.New(t) assert := assert.New(t) - variant := variant.AWSNitroTPM{} - output, err := status( - context.Background(), + cmd := NewStatusCmd() + var out bytes.Buffer + cmd.SetOut(&out) + var errOut bytes.Buffer + cmd.SetErr(&errOut) + + fileHandler := file.NewHandler(afero.NewMemMapFs()) + cfg, err := createConfigWithAttestationVariant(cloudprovider.QEMU, "", variant.QEMUVTPM{}) + require.NoError(err) + require.NoError(fileHandler.WriteYAML(constants.ConfigFilename, cfg)) + + s := statusCmd{fileHandler: fileHandler} + + err = s.status( + cmd, stubGetVersions(versionsOutput), tc.kubeClient, - variant, + stubAttestationFetcher{}, ) if tc.wantErr { assert.Error(err) return } require.NoError(err) - assert.Equal(tc.expectedOutput, output) + assert.Equal(tc.expectedOutput, out.String()) }) } } diff --git a/cli/internal/cmd/terminate.go b/cli/internal/cmd/terminate.go index 141b755d46..c20c3fe2ca 100644 --- a/cli/internal/cmd/terminate.go +++ b/cli/internal/cmd/terminate.go @@ -13,10 +13,9 @@ import ( "github.com/spf13/afero" "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" - "github.com/edgelesssys/constellation/v2/cli/internal/cmd/pathprefix" - "github.com/edgelesssys/constellation/v2/cli/internal/terraform" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" ) @@ -35,9 +34,26 @@ func NewTerminateCmd() *cobra.Command { return cmd } +type terminateFlags struct { + rootFlags + yes bool +} + +func (f *terminateFlags) parse(flags *pflag.FlagSet) error { + if err := f.rootFlags.parse(flags); err != nil { + return err + } + + yes, err := flags.GetBool("yes") + if err != nil { + return fmt.Errorf("getting 'yes' flag: %w", err) + } + f.yes = yes + return nil +} + // runTerminate runs the terminate command. func runTerminate(cmd *cobra.Command, _ []string) error { - fileHandler := file.NewHandler(afero.NewOsFs()) spinner, err := newSpinnerOrStderr(cmd) if err != nil { return fmt.Errorf("creating spinner: %w", err) @@ -45,18 +61,27 @@ func runTerminate(cmd *cobra.Command, _ []string) error { defer spinner.Stop() terminator := cloudcmd.NewTerminator() - return terminate(cmd, terminator, fileHandler, spinner) -} - -func terminate(cmd *cobra.Command, terminator cloudTerminator, fileHandler file.Handler, spinner spinnerInterf, -) error { - flags, err := parseTerminateFlags(cmd) + logger, err := newCLILogger(cmd) if err != nil { - return fmt.Errorf("parsing flags: %w", err) + return fmt.Errorf("creating logger: %w", err) + } + + t := &terminateCmd{log: logger, fileHandler: file.NewHandler(afero.NewOsFs())} + if err := t.flags.parse(cmd.Flags()); err != nil { + return err } - pf := pathprefix.New(flags.workspace) - if !flags.yes { + return t.terminate(cmd, terminator, spinner) +} + +type terminateCmd struct { + log debugLog + fileHandler file.Handler + flags terminateFlags +} + +func (t *terminateCmd) terminate(cmd *cobra.Command, terminator cloudTerminator, spinner spinnerInterf) error { + if !t.flags.yes { cmd.Println("You are about to terminate a Constellation cluster.") cmd.Println("All of its associated resources will be DESTROYED.") cmd.Println("This action is irreversible and ALL DATA WILL BE LOST.") @@ -71,7 +96,7 @@ func terminate(cmd *cobra.Command, terminator cloudTerminator, fileHandler file. } spinner.Start("Terminating", false) - err = terminator.Terminate(cmd.Context(), constants.TerraformWorkingDir, flags.logLevel) + err := terminator.Terminate(cmd.Context(), constants.TerraformWorkingDir, t.flags.tfLogLevel) spinner.Stop() if err != nil { return fmt.Errorf("terminating Constellation cluster: %w", err) @@ -80,44 +105,13 @@ func terminate(cmd *cobra.Command, terminator cloudTerminator, fileHandler file. cmd.Println("Your Constellation cluster was terminated successfully.") var removeErr error - if err := fileHandler.Remove(constants.AdminConfFilename); err != nil && !errors.Is(err, fs.ErrNotExist) { - removeErr = errors.Join(err, fmt.Errorf("failed to remove file: '%s', please remove it manually", pf.PrefixPrintablePath(constants.AdminConfFilename))) + if err := t.fileHandler.Remove(constants.AdminConfFilename); err != nil && !errors.Is(err, fs.ErrNotExist) { + removeErr = errors.Join(err, fmt.Errorf("failed to remove file: '%s', please remove it manually", t.flags.pathPrefixer.PrefixPrintablePath(constants.AdminConfFilename))) } - if err := fileHandler.Remove(constants.StateFilename); err != nil && !errors.Is(err, fs.ErrNotExist) { - removeErr = errors.Join(err, fmt.Errorf("failed to remove file: '%s', please remove it manually", pf.PrefixPrintablePath(constants.StateFilename))) + if err := t.fileHandler.Remove(constants.StateFilename); err != nil && !errors.Is(err, fs.ErrNotExist) { + removeErr = errors.Join(err, fmt.Errorf("failed to remove file: '%s', please remove it manually", t.flags.pathPrefixer.PrefixPrintablePath(constants.StateFilename))) } return removeErr } - -type terminateFlags struct { - yes bool - workspace string - logLevel terraform.LogLevel -} - -func parseTerminateFlags(cmd *cobra.Command) (terminateFlags, error) { - yes, err := cmd.Flags().GetBool("yes") - if err != nil { - return terminateFlags{}, fmt.Errorf("parsing yes bool: %w", err) - } - logLevelString, err := cmd.Flags().GetString("tf-log") - if err != nil { - return terminateFlags{}, fmt.Errorf("parsing tf-log string: %w", err) - } - logLevel, err := terraform.ParseLogLevel(logLevelString) - if err != nil { - return terminateFlags{}, fmt.Errorf("parsing Terraform log level %s: %w", logLevelString, err) - } - workspace, err := cmd.Flags().GetString("workspace") - if err != nil { - return terminateFlags{}, fmt.Errorf("parsing workspace string: %w", err) - } - - return terminateFlags{ - yes: yes, - workspace: workspace, - logLevel: logLevel, - }, nil -} diff --git a/cli/internal/cmd/terminate_test.go b/cli/internal/cmd/terminate_test.go index 3ddf1487b1..6780f240eb 100644 --- a/cli/internal/cmd/terminate_test.go +++ b/cli/internal/cmd/terminate_test.go @@ -14,6 +14,7 @@ import ( "github.com/edgelesssys/constellation/v2/cli/internal/state" "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" @@ -134,18 +135,17 @@ func TestTerminate(t *testing.T) { cmd.SetErr(&bytes.Buffer{}) cmd.SetIn(bytes.NewBufferString(tc.stdin)) - // register persistent flags manually - cmd.Flags().String("tf-log", "NONE", "") - cmd.Flags().String("workspace", "", "") - require.NotNil(tc.setupFs) fileHandler := file.NewHandler(tc.setupFs(require, tc.stateFile)) - if tc.yesFlag { - require.NoError(cmd.Flags().Set("yes", "true")) + tCmd := &terminateCmd{ + log: logger.NewTest(t), + fileHandler: fileHandler, + flags: terminateFlags{ + yes: tc.yesFlag, + }, } - - err := terminate(cmd, tc.terminator, fileHandler, &nopSpinner{}) + err := tCmd.terminate(cmd, tc.terminator, &nopSpinner{}) if tc.wantErr { assert.Error(err) diff --git a/cli/internal/cmd/upgradeapply.go b/cli/internal/cmd/upgradeapply.go index 9e564e4eb4..0b38947776 100644 --- a/cli/internal/cmd/upgradeapply.go +++ b/cli/internal/cmd/upgradeapply.go @@ -16,7 +16,6 @@ import ( "time" "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" - "github.com/edgelesssys/constellation/v2/cli/internal/cmd/pathprefix" "github.com/edgelesssys/constellation/v2/cli/internal/helm" "github.com/edgelesssys/constellation/v2/cli/internal/kubecmd" "github.com/edgelesssys/constellation/v2/cli/internal/state" @@ -33,6 +32,7 @@ import ( "github.com/rogpeppe/go-internal/diff" "github.com/spf13/afero" "github.com/spf13/cobra" + "github.com/spf13/pflag" "gopkg.in/yaml.v3" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" ) @@ -76,12 +76,62 @@ func newUpgradeApplyCmd() *cobra.Command { return cmd } -func runUpgradeApply(cmd *cobra.Command, _ []string) error { - flags, err := parseUpgradeApplyFlags(cmd) +type upgradeApplyFlags struct { + rootFlags + yes bool + upgradeTimeout time.Duration + conformance bool + helmWaitMode helm.WaitMode + skipPhases skipPhases +} + +func (f *upgradeApplyFlags) parse(flags *pflag.FlagSet) error { + if err := f.rootFlags.parse(flags); err != nil { + return err + } + + rawSkipPhases, err := flags.GetStringSlice("skip-phases") + if err != nil { + return fmt.Errorf("parsing skip-phases flag: %w", err) + } + var skipPhases []skipPhase + for _, phase := range rawSkipPhases { + switch skipPhase(phase) { + case skipInfrastructurePhase, skipHelmPhase, skipImagePhase, skipK8sPhase: + skipPhases = append(skipPhases, skipPhase(phase)) + default: + return fmt.Errorf("invalid phase %s", phase) + } + } + f.skipPhases = skipPhases + + f.yes, err = flags.GetBool("yes") + if err != nil { + return fmt.Errorf("getting 'yes' flag: %w", err) + } + + f.upgradeTimeout, err = flags.GetDuration("timeout") + if err != nil { + return fmt.Errorf("getting 'timeout' flag: %w", err) + } + + f.conformance, err = flags.GetBool("conformance") if err != nil { - return fmt.Errorf("parsing flags: %w", err) + return fmt.Errorf("getting 'conformance' flag: %w", err) + } + skipHelmWait, err := flags.GetBool("skip-helm-wait") + if err != nil { + return fmt.Errorf("getting 'skip-helm-wait' flag: %w", err) + } + f.helmWaitMode = helm.WaitModeAtomic + if skipHelmWait { + f.helmWaitMode = helm.WaitModeNone } + return nil +} + +func runUpgradeApply(cmd *cobra.Command, _ []string) error { log, err := newCLILogger(cmd) if err != nil { return fmt.Errorf("creating logger: %w", err) @@ -98,13 +148,18 @@ func runUpgradeApply(cmd *cobra.Command, _ []string) error { configFetcher := attestationconfigapi.NewFetcher() + var flags upgradeApplyFlags + if err := flags.parse(cmd.Flags()); err != nil { + return err + } + // Set up terraform upgrader upgradeDir := filepath.Join(constants.UpgradeDir, upgradeID) clusterUpgrader, err := cloudcmd.NewClusterUpgrader( cmd.Context(), constants.TerraformWorkingDir, upgradeDir, - flags.terraformLogLevel, + flags.tfLogLevel, fileHandler, ) if err != nil { @@ -122,9 +177,10 @@ func runUpgradeApply(cmd *cobra.Command, _ []string) error { clusterUpgrader: clusterUpgrader, configFetcher: configFetcher, fileHandler: fileHandler, + flags: flags, log: log, } - return applyCmd.upgradeApply(cmd, upgradeDir, flags) + return applyCmd.upgradeApply(cmd, upgradeDir) } type upgradeApplyCmd struct { @@ -133,11 +189,12 @@ type upgradeApplyCmd struct { clusterUpgrader clusterUpgrader configFetcher attestationconfigapi.Fetcher fileHandler file.Handler + flags upgradeApplyFlags log debugLog } -func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command, upgradeDir string, flags upgradeApplyFlags) error { - conf, err := config.New(u.fileHandler, constants.ConfigFilename, u.configFetcher, flags.force) +func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command, upgradeDir string) error { + conf, err := config.New(u.fileHandler, constants.ConfigFilename, u.configFetcher, u.flags.force) var configValidationErr *config.ValidationError if errors.As(err, &configValidationErr) { cmd.PrintErrln(configValidationErr.LongMessage()) @@ -147,7 +204,7 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command, upgradeDir string, fl } if cloudcmd.UpgradeRequiresIAMMigration(conf.GetProvider()) { cmd.Println("WARNING: This upgrade requires an IAM migration. Please make sure you have applied the IAM migration using `iam upgrade apply` before continuing.") - if !flags.yes { + if !u.flags.yes { yes, err := askToConfirm(cmd, "Did you upgrade the IAM resources?") if err != nil { return fmt.Errorf("asking for confirmation: %w", err) @@ -158,7 +215,7 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command, upgradeDir string, fl } } } - conf.KubernetesVersion, err = validK8sVersion(cmd, string(conf.KubernetesVersion), flags.yes) + conf.KubernetesVersion, err = validK8sVersion(cmd, string(conf.KubernetesVersion), u.flags.yes) if err != nil { return err } @@ -168,21 +225,21 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command, upgradeDir string, fl return fmt.Errorf("reading state file: %w", err) } - if err := u.confirmAndUpgradeAttestationConfig(cmd, conf.GetAttestationConfig(), stateFile.ClusterValues.MeasurementSalt, flags); err != nil { + if err := u.confirmAndUpgradeAttestationConfig(cmd, conf.GetAttestationConfig(), stateFile.ClusterValues.MeasurementSalt); err != nil { return fmt.Errorf("upgrading measurements: %w", err) } // If infrastructure phase is skipped, we expect the new infrastructure // to be in the Terraform configuration already. Otherwise, perform // the Terraform migrations. - if !flags.skipPhases.contains(skipInfrastructurePhase) { + if !u.flags.skipPhases.contains(skipInfrastructurePhase) { migrationRequired, err := u.planTerraformMigration(cmd, conf) if err != nil { return fmt.Errorf("planning Terraform migrations: %w", err) } if migrationRequired { - postMigrationInfraState, err := u.migrateTerraform(cmd, conf, upgradeDir, flags) + postMigrationInfraState, err := u.migrateTerraform(cmd, conf, upgradeDir) if err != nil { return fmt.Errorf("performing Terraform migrations: %w", err) } @@ -217,8 +274,8 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command, upgradeDir string, fl } var upgradeErr *compatibility.InvalidUpgradeError - if !flags.skipPhases.contains(skipHelmPhase) { - err = u.handleServiceUpgrade(cmd, conf, stateFile, upgradeDir, flags) + if !u.flags.skipPhases.contains(skipHelmPhase) { + err = u.handleServiceUpgrade(cmd, conf, stateFile, upgradeDir) switch { case errors.As(err, &upgradeErr): cmd.PrintErrln(err) @@ -228,10 +285,10 @@ func (u *upgradeApplyCmd) upgradeApply(cmd *cobra.Command, upgradeDir string, fl return fmt.Errorf("upgrading services: %w", err) } } - skipImageUpgrade := flags.skipPhases.contains(skipImagePhase) - skipK8sUpgrade := flags.skipPhases.contains(skipK8sPhase) + skipImageUpgrade := u.flags.skipPhases.contains(skipImagePhase) + skipK8sUpgrade := u.flags.skipPhases.contains(skipK8sPhase) if !(skipImageUpgrade && skipK8sUpgrade) { - err = u.kubeUpgrader.UpgradeNodeVersion(cmd.Context(), conf, flags.force, skipImageUpgrade, skipK8sUpgrade) + err = u.kubeUpgrader.UpgradeNodeVersion(cmd.Context(), conf, u.flags.force, skipImageUpgrade, skipK8sUpgrade) switch { case errors.Is(err, kubecmd.ErrInProgress): cmd.PrintErrln("Skipping image and Kubernetes upgrades. Another upgrade is in progress.") @@ -284,12 +341,11 @@ func (u *upgradeApplyCmd) planTerraformMigration(cmd *cobra.Command, conf *confi // migrateTerraform checks if the Constellation version the cluster is being upgraded to requires a migration // of cloud resources with Terraform. If so, the migration is performed and the post-migration infrastructure state is returned. // If no migration is required, the current (pre-upgrade) infrastructure state is returned. -func (u *upgradeApplyCmd) migrateTerraform( - cmd *cobra.Command, conf *config.Config, upgradeDir string, flags upgradeApplyFlags, +func (u *upgradeApplyCmd) migrateTerraform(cmd *cobra.Command, conf *config.Config, upgradeDir string, ) (state.Infrastructure, error) { // If there are any Terraform migrations to apply, ask for confirmation fmt.Fprintln(cmd.OutOrStdout(), "The upgrade requires a migration of Constellation cloud resources by applying an updated Terraform template. Please manually review the suggested changes below.") - if !flags.yes { + if !u.flags.yes { ok, err := askToConfirm(cmd, "Do you want to apply the Terraform migrations?") if err != nil { return state.Infrastructure{}, fmt.Errorf("asking for confirmation: %w", err) @@ -317,8 +373,8 @@ func (u *upgradeApplyCmd) migrateTerraform( cmd.Printf("Infrastructure migrations applied successfully and output written to: %s\n"+ "A backup of the pre-upgrade state has been written to: %s\n", - flags.pf.PrefixPrintablePath(constants.StateFilename), - flags.pf.PrefixPrintablePath(filepath.Join(upgradeDir, constants.TerraformUpgradeBackupDir)), + u.flags.pathPrefixer.PrefixPrintablePath(constants.StateFilename), + u.flags.pathPrefixer.PrefixPrintablePath(filepath.Join(upgradeDir, constants.TerraformUpgradeBackupDir)), ) return infraState, nil } @@ -347,7 +403,7 @@ func validK8sVersion(cmd *cobra.Command, version string, yes bool) (validVersion // confirmAndUpgradeAttestationConfig checks if the locally configured measurements are different from the cluster's measurements. // If so the function will ask the user to confirm (if --yes is not set) and upgrade the cluster's config. func (u *upgradeApplyCmd) confirmAndUpgradeAttestationConfig( - cmd *cobra.Command, newConfig config.AttestationCfg, measurementSalt []byte, flags upgradeApplyFlags, + cmd *cobra.Command, newConfig config.AttestationCfg, measurementSalt []byte, ) error { clusterAttestationConfig, err := u.kubeUpgrader.GetClusterAttestationConfig(cmd.Context(), newConfig.GetVariant()) if err != nil { @@ -369,7 +425,7 @@ func (u *upgradeApplyCmd) confirmAndUpgradeAttestationConfig( } cmd.Println("The following changes will be applied to the attestation config:") cmd.Println(diffStr) - if !flags.yes { + if !u.flags.yes { ok, err := askToConfirm(cmd, "Are you sure you want to change your cluster's attestation config?") if err != nil { return fmt.Errorf("asking for confirmation: %w", err) @@ -387,21 +443,20 @@ func (u *upgradeApplyCmd) confirmAndUpgradeAttestationConfig( } func (u *upgradeApplyCmd) handleServiceUpgrade( - cmd *cobra.Command, conf *config.Config, stateFile *state.State, - upgradeDir string, flags upgradeApplyFlags, + cmd *cobra.Command, conf *config.Config, stateFile *state.State, upgradeDir string, ) error { var secret uri.MasterSecret if err := u.fileHandler.ReadJSON(constants.MasterSecretFilename, &secret); err != nil { return fmt.Errorf("reading master secret: %w", err) } - serviceAccURI, err := cloudcmd.GetMarshaledServiceAccountURI(conf.GetProvider(), conf, flags.pf, u.log, u.fileHandler) + serviceAccURI, err := cloudcmd.GetMarshaledServiceAccountURI(conf.GetProvider(), conf, u.flags.pathPrefixer, u.log, u.fileHandler) if err != nil { return fmt.Errorf("getting service account URI: %w", err) } options := helm.Options{ - Force: flags.force, - Conformance: flags.conformance, - HelmWaitMode: flags.helmWaitMode, + Force: u.flags.force, + Conformance: u.flags.conformance, + HelmWaitMode: u.flags.helmWaitMode, } prepareApply := func(allowDestructive bool) (helm.Applier, bool, error) { @@ -422,7 +477,7 @@ func (u *upgradeApplyCmd) handleServiceUpgrade( if !errors.Is(err, helm.ErrConfirmationMissing) { return fmt.Errorf("upgrading charts with deny destructive mode: %w", err) } - if !flags.yes { + if !u.flags.yes { cmd.PrintErrln("WARNING: Upgrading cert-manager will destroy all custom resources you have manually created that are based on the current version of cert-manager.") ok, askErr := askToConfirm(cmd, "Do you want to upgrade cert-manager anyway?") if askErr != nil { @@ -463,86 +518,6 @@ func (u *upgradeApplyCmd) handleServiceUpgrade( return nil } -func parseUpgradeApplyFlags(cmd *cobra.Command) (upgradeApplyFlags, error) { - workDir, err := cmd.Flags().GetString("workspace") - if err != nil { - return upgradeApplyFlags{}, err - } - - yes, err := cmd.Flags().GetBool("yes") - if err != nil { - return upgradeApplyFlags{}, err - } - - timeout, err := cmd.Flags().GetDuration("timeout") - if err != nil { - return upgradeApplyFlags{}, err - } - - force, err := cmd.Flags().GetBool("force") - if err != nil { - return upgradeApplyFlags{}, fmt.Errorf("parsing force argument: %w", err) - } - - logLevelString, err := cmd.Flags().GetString("tf-log") - if err != nil { - return upgradeApplyFlags{}, fmt.Errorf("parsing tf-log string: %w", err) - } - logLevel, err := terraform.ParseLogLevel(logLevelString) - if err != nil { - return upgradeApplyFlags{}, fmt.Errorf("parsing Terraform log level %s: %w", logLevelString, err) - } - - conformance, err := cmd.Flags().GetBool("conformance") - if err != nil { - return upgradeApplyFlags{}, fmt.Errorf("parsing conformance flag: %w", err) - } - skipHelmWait, err := cmd.Flags().GetBool("skip-helm-wait") - if err != nil { - return upgradeApplyFlags{}, fmt.Errorf("parsing skip-helm-wait flag: %w", err) - } - helmWaitMode := helm.WaitModeAtomic - if skipHelmWait { - helmWaitMode = helm.WaitModeNone - } - - rawSkipPhases, err := cmd.Flags().GetStringSlice("skip-phases") - if err != nil { - return upgradeApplyFlags{}, fmt.Errorf("parsing skip-phases flag: %w", err) - } - var skipPhases []skipPhase - for _, phase := range rawSkipPhases { - switch skipPhase(phase) { - case skipInfrastructurePhase, skipHelmPhase, skipImagePhase, skipK8sPhase: - skipPhases = append(skipPhases, skipPhase(phase)) - default: - return upgradeApplyFlags{}, fmt.Errorf("invalid phase %s", phase) - } - } - - return upgradeApplyFlags{ - pf: pathprefix.New(workDir), - yes: yes, - upgradeTimeout: timeout, - force: force, - terraformLogLevel: logLevel, - conformance: conformance, - helmWaitMode: helmWaitMode, - skipPhases: skipPhases, - }, nil -} - -type upgradeApplyFlags struct { - pf pathprefix.PathPrefixer - yes bool - upgradeTimeout time.Duration - force bool - terraformLogLevel terraform.LogLevel - conformance bool - helmWaitMode helm.WaitMode - skipPhases skipPhases -} - // skipPhases is a list of phases that can be skipped during the upgrade process. type skipPhases []skipPhase diff --git a/cli/internal/cmd/upgradeapply_test.go b/cli/internal/cmd/upgradeapply_test.go index 96e4c8688b..6f8662d9a4 100644 --- a/cli/internal/cmd/upgradeapply_test.go +++ b/cli/internal/cmd/upgradeapply_test.go @@ -227,10 +227,11 @@ func TestUpgradeApply(t *testing.T) { clusterUpgrader: tc.terraformUpgrader, log: logger.NewTest(t), configFetcher: stubAttestationFetcher{}, + flags: tc.flags, fileHandler: fh, } - err := upgrader.upgradeApply(cmd, "test", tc.flags) + err := upgrader.upgradeApply(cmd, "test") if tc.wantErr { assert.Error(err) return @@ -247,16 +248,20 @@ func TestUpgradeApply(t *testing.T) { } func TestUpgradeApplyFlagsForSkipPhases(t *testing.T) { + require := require.New(t) cmd := newUpgradeApplyCmd() - cmd.Flags().String("workspace", "", "") // register persistent flag manually - cmd.Flags().Bool("force", true, "") // register persistent flag manually - cmd.Flags().String("tf-log", "NONE", "") // register persistent flag manually - require.NoError(t, cmd.Flags().Set("skip-phases", "infrastructure,helm,k8s,image")) - result, err := parseUpgradeApplyFlags(cmd) - if err != nil { - t.Fatalf("Error while parsing flags: %v", err) - } - assert.ElementsMatch(t, []skipPhase{skipInfrastructurePhase, skipHelmPhase, skipK8sPhase, skipImagePhase}, result.skipPhases) + // register persistent flags manually + cmd.Flags().String("workspace", "", "") + cmd.Flags().Bool("force", true, "") + cmd.Flags().String("tf-log", "NONE", "") + cmd.Flags().Bool("debug", false, "") + + require.NoError(cmd.Flags().Set("skip-phases", "infrastructure,helm,k8s,image")) + + var flags upgradeApplyFlags + err := flags.parse(cmd.Flags()) + require.NoError(err) + assert.ElementsMatch(t, []skipPhase{skipInfrastructurePhase, skipHelmPhase, skipK8sPhase, skipImagePhase}, flags.skipPhases) } type stubKubernetesUpgrader struct { diff --git a/cli/internal/cmd/upgradecheck.go b/cli/internal/cmd/upgradecheck.go index b81ff8a34a..ec6f6c2034 100644 --- a/cli/internal/cmd/upgradecheck.go +++ b/cli/internal/cmd/upgradecheck.go @@ -38,6 +38,7 @@ import ( "github.com/siderolabs/talos/pkg/machinery/config/encoder" "github.com/spf13/afero" "github.com/spf13/cobra" + "github.com/spf13/pflag" "golang.org/x/mod/semver" ) @@ -57,6 +58,36 @@ func newUpgradeCheckCmd() *cobra.Command { return cmd } +type upgradeCheckFlags struct { + rootFlags + updateConfig bool + ref string + stream string +} + +func (f *upgradeCheckFlags) parse(flags *pflag.FlagSet) error { + if err := f.rootFlags.parse(flags); err != nil { + return err + } + + updateConfig, err := flags.GetBool("update-config") + if err != nil { + return fmt.Errorf("getting 'update-config' flag: %w", err) + } + f.updateConfig = updateConfig + + f.ref, err = flags.GetString("ref") + if err != nil { + return fmt.Errorf("getting 'ref' flag: %w", err) + } + f.stream, err = flags.GetString("stream") + if err != nil { + return fmt.Errorf("getting 'stream' flag: %w", err) + } + + return nil +} + func runUpgradeCheck(cmd *cobra.Command, _ []string) error { log, err := newCLILogger(cmd) if err != nil { @@ -64,8 +95,8 @@ func runUpgradeCheck(cmd *cobra.Command, _ []string) error { } defer log.Sync() - flags, err := parseUpgradeCheckFlags(cmd) - if err != nil { + var flags upgradeCheckFlags + if err := flags.parse(cmd.Flags()); err != nil { return err } @@ -77,7 +108,7 @@ func runUpgradeCheck(cmd *cobra.Command, _ []string) error { cmd.Context(), constants.TerraformWorkingDir, upgradeDir, - flags.terraformLogLevel, + flags.tfLogLevel, fileHandler, ) if err != nil { @@ -111,46 +142,11 @@ func runUpgradeCheck(cmd *cobra.Command, _ []string) error { upgradeDir: upgradeDir, terraformChecker: tfClient, fileHandler: fileHandler, + flags: flags, log: log, } - return up.upgradeCheck(cmd, attestationconfigapi.NewFetcher(), flags) -} - -func parseUpgradeCheckFlags(cmd *cobra.Command) (upgradeCheckFlags, error) { - force, err := cmd.Flags().GetBool("force") - if err != nil { - return upgradeCheckFlags{}, fmt.Errorf("parsing force bool: %w", err) - } - updateConfig, err := cmd.Flags().GetBool("update-config") - if err != nil { - return upgradeCheckFlags{}, fmt.Errorf("parsing update-config bool: %w", err) - } - ref, err := cmd.Flags().GetString("ref") - if err != nil { - return upgradeCheckFlags{}, fmt.Errorf("parsing ref string: %w", err) - } - stream, err := cmd.Flags().GetString("stream") - if err != nil { - return upgradeCheckFlags{}, fmt.Errorf("parsing stream string: %w", err) - } - - logLevelString, err := cmd.Flags().GetString("tf-log") - if err != nil { - return upgradeCheckFlags{}, fmt.Errorf("parsing tf-log string: %w", err) - } - logLevel, err := terraform.ParseLogLevel(logLevelString) - if err != nil { - return upgradeCheckFlags{}, fmt.Errorf("parsing Terraform log level %s: %w", logLevelString, err) - } - - return upgradeCheckFlags{ - force: force, - updateConfig: updateConfig, - ref: ref, - stream: stream, - terraformLogLevel: logLevel, - }, nil + return up.upgradeCheck(cmd, attestationconfigapi.NewFetcher()) } type upgradeCheckCmd struct { @@ -159,12 +155,13 @@ type upgradeCheckCmd struct { collect collector terraformChecker terraformChecker fileHandler file.Handler + flags upgradeCheckFlags log debugLog } // upgradePlan plans an upgrade of a Constellation cluster. -func (u *upgradeCheckCmd) upgradeCheck(cmd *cobra.Command, fetcher attestationconfigapi.Fetcher, flags upgradeCheckFlags) error { - conf, err := config.New(u.fileHandler, constants.ConfigFilename, fetcher, flags.force) +func (u *upgradeCheckCmd) upgradeCheck(cmd *cobra.Command, fetcher attestationconfigapi.Fetcher) error { + conf, err := config.New(u.fileHandler, constants.ConfigFilename, fetcher, u.flags.force) var configValidationErr *config.ValidationError if errors.As(err, &configValidationErr) { cmd.PrintErrln(configValidationErr.LongMessage()) @@ -271,7 +268,7 @@ func (u *upgradeCheckCmd) upgradeCheck(cmd *cobra.Command, fetcher attestationco // Using Print over Println as buildString already includes a trailing newline where necessary. cmd.Print(updateMsg) - if flags.updateConfig { + if u.flags.updateConfig { if err := upgrade.writeConfig(conf, u.fileHandler, constants.ConfigFilename); err != nil { return fmt.Errorf("writing config: %w", err) } @@ -725,14 +722,6 @@ func (v *versionCollector) filterCompatibleCLIVersions(ctx context.Context, cliP return compatibleVersions, nil } -type upgradeCheckFlags struct { - force bool - updateConfig bool - ref string - stream string - terraformLogLevel terraform.LogLevel -} - type kubernetesChecker interface { GetConstellationVersion(ctx context.Context) (kubecmd.NodeVersion, error) } diff --git a/cli/internal/cmd/upgradecheck_test.go b/cli/internal/cmd/upgradecheck_test.go index fd99475366..0037969131 100644 --- a/cli/internal/cmd/upgradecheck_test.go +++ b/cli/internal/cmd/upgradecheck_test.go @@ -221,7 +221,7 @@ func TestUpgradeCheck(t *testing.T) { cmd := newUpgradeCheckCmd() - err := checkCmd.upgradeCheck(cmd, stubAttestationFetcher{}, upgradeCheckFlags{}) + err := checkCmd.upgradeCheck(cmd, stubAttestationFetcher{}) if tc.wantError { assert.Error(err) return diff --git a/cli/internal/cmd/verify.go b/cli/internal/cmd/verify.go index 68f787641e..856de315e2 100644 --- a/cli/internal/cmd/verify.go +++ b/cli/internal/cmd/verify.go @@ -26,7 +26,6 @@ import ( tpmProto "github.com/google/go-tpm-tools/proto/tpm" "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" - "github.com/edgelesssys/constellation/v2/cli/internal/cmd/pathprefix" "github.com/edgelesssys/constellation/v2/cli/internal/state" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/atls" @@ -45,6 +44,7 @@ import ( "github.com/google/go-sev-guest/kds" "github.com/spf13/afero" "github.com/spf13/cobra" + "github.com/spf13/pflag" "google.golang.org/grpc" ) @@ -64,8 +64,39 @@ func NewVerifyCmd() *cobra.Command { return cmd } +type verifyFlags struct { + rootFlags + endpoint string + ownerID string + clusterID string + output string +} + +func (f *verifyFlags) parse(flags *pflag.FlagSet) error { + if err := f.rootFlags.parse(flags); err != nil { + return err + } + + var err error + f.output, err = flags.GetString("output") + if err != nil { + return fmt.Errorf("getting 'output' flag: %w", err) + } + f.endpoint, err = flags.GetString("node-endpoint") + if err != nil { + return fmt.Errorf("getting 'node-endpoint' flag: %w", err) + } + f.clusterID, err = flags.GetString("cluster-id") + if err != nil { + return fmt.Errorf("getting 'cluster-id' flag: %w", err) + } + return nil +} + type verifyCmd struct { - log debugLog + fileHandler file.Handler + flags verifyFlags + log debugLog } func runVerify(cmd *cobra.Command, _ []string) error { @@ -95,22 +126,23 @@ func runVerify(cmd *cobra.Command, _ []string) error { return nil, fmt.Errorf("invalid output value for formatter: %s", output) } } - v := &verifyCmd{log: log} + v := &verifyCmd{ + fileHandler: fileHandler, + log: log, + } + if err := v.flags.parse(cmd.Flags()); err != nil { + return err + } + v.log.Debugf("Using flags: %+v", v.flags) fetcher := attestationconfigapi.NewFetcher() - return v.verify(cmd, fileHandler, verifyClient, formatterFactory, fetcher) + return v.verify(cmd, verifyClient, formatterFactory, fetcher) } type formatterFactory func(output string, provider cloudprovider.Provider, log debugLog) (attestationDocFormatter, error) -func (c *verifyCmd) verify(cmd *cobra.Command, fileHandler file.Handler, verifyClient verifyClient, factory formatterFactory, configFetcher attestationconfigapi.Fetcher) error { - flags, err := c.parseVerifyFlags(cmd, fileHandler) - if err != nil { - return fmt.Errorf("parsing flags: %w", err) - } - c.log.Debugf("Using flags: %+v", flags) - - c.log.Debugf("Loading configuration file from %q", flags.pf.PrefixPrintablePath(constants.ConfigFilename)) - conf, err := config.New(fileHandler, constants.ConfigFilename, configFetcher, flags.force) +func (c *verifyCmd) verify(cmd *cobra.Command, verifyClient verifyClient, factory formatterFactory, configFetcher attestationconfigapi.Fetcher) error { + c.log.Debugf("Loading configuration file from %q", c.flags.pathPrefixer.PrefixPrintablePath(constants.ConfigFilename)) + conf, err := config.New(c.fileHandler, constants.ConfigFilename, configFetcher, c.flags.force) var configValidationErr *config.ValidationError if errors.As(err, &configValidationErr) { cmd.PrintErrln(configValidationErr.LongMessage()) @@ -119,10 +151,29 @@ func (c *verifyCmd) verify(cmd *cobra.Command, fileHandler file.Handler, verifyC return fmt.Errorf("loading config file: %w", err) } - conf.UpdateMAAURL(flags.maaURL) + stateFile, err := state.ReadFromFile(c.fileHandler, constants.StateFilename) + if err != nil { + return fmt.Errorf("reading state file: %w", err) + } + + ownerID, clusterID, err := c.validateIDFlags(cmd, stateFile) + if err != nil { + return err + } + endpoint, err := c.validateEndpointFlag(cmd, stateFile) + if err != nil { + return err + } + + var maaURL string + if stateFile.Infrastructure.Azure != nil { + maaURL = stateFile.Infrastructure.Azure.AttestationURL + } + conf.UpdateMAAURL(maaURL) + c.log.Debugf("Updating expected PCRs") attConfig := conf.GetAttestationConfig() - if err := cloudcmd.UpdateInitMeasurements(attConfig, flags.ownerID, flags.clusterID); err != nil { + if err := cloudcmd.UpdateInitMeasurements(attConfig, ownerID, clusterID); err != nil { return fmt.Errorf("updating expected PCRs: %w", err) } @@ -140,7 +191,7 @@ func (c *verifyCmd) verify(cmd *cobra.Command, fileHandler file.Handler, verifyC rawAttestationDoc, err := verifyClient.Verify( cmd.Context(), - flags.endpoint, + endpoint, &verifyproto.GetAttestationRequest{ Nonce: nonce, }, @@ -151,7 +202,7 @@ func (c *verifyCmd) verify(cmd *cobra.Command, fileHandler file.Handler, verifyC } // certificates are only available for Azure - formatter, err := factory(flags.output, conf.GetProvider(), c.log) + formatter, err := factory(c.flags.output, conf.GetProvider(), c.log) if err != nil { return fmt.Errorf("creating formatter: %w", err) } @@ -160,7 +211,7 @@ func (c *verifyCmd) verify(cmd *cobra.Command, fileHandler file.Handler, verifyC rawAttestationDoc, conf.Provider.Azure == nil, attConfig.GetMeasurements(), - flags.maaURL, + maaURL, ) if err != nil { return fmt.Errorf("printing attestation document: %w", err) @@ -171,114 +222,37 @@ func (c *verifyCmd) verify(cmd *cobra.Command, fileHandler file.Handler, verifyC return nil } -func (c *verifyCmd) parseVerifyFlags(cmd *cobra.Command, fileHandler file.Handler) (verifyFlags, error) { - workDir, err := cmd.Flags().GetString("workspace") - if err != nil { - return verifyFlags{}, fmt.Errorf("parsing config path argument: %w", err) - } - c.log.Debugf("Flag 'workspace' set to %q", workDir) - pf := pathprefix.New(workDir) - - ownerID := "" - clusterID, err := cmd.Flags().GetString("cluster-id") - if err != nil { - return verifyFlags{}, fmt.Errorf("parsing cluster-id argument: %w", err) - } - c.log.Debugf("Flag 'cluster-id' set to %q", clusterID) - - endpoint, err := cmd.Flags().GetString("node-endpoint") - if err != nil { - return verifyFlags{}, fmt.Errorf("parsing node-endpoint argument: %w", err) - } - c.log.Debugf("Flag 'node-endpoint' set to %q", endpoint) - - force, err := cmd.Flags().GetBool("force") - if err != nil { - return verifyFlags{}, fmt.Errorf("parsing force argument: %w", err) - } - c.log.Debugf("Flag 'force' set to %t", force) - - output, err := cmd.Flags().GetString("output") - if err != nil { - return verifyFlags{}, fmt.Errorf("parsing raw argument: %w", err) - } - c.log.Debugf("Flag 'output' set to %t", output) - - // Get empty values from state file - stateFile, err := state.ReadFromFile(fileHandler, constants.StateFilename) - isFileNotFound := errors.Is(err, afero.ErrFileNotFound) - if isFileNotFound { - c.log.Debugf("State file %q not found, using empty state", pf.PrefixPrintablePath(constants.StateFilename)) - stateFile = state.New() // error compat - } else if err != nil { - return verifyFlags{}, fmt.Errorf("reading state file: %w", err) - } - - emptyEndpoint := endpoint == "" - emptyIDs := ownerID == "" && clusterID == "" - if emptyEndpoint || emptyIDs { - c.log.Debugf("Trying to supplement empty flag values from %q", pf.PrefixPrintablePath(constants.StateFilename)) - if emptyEndpoint { - cmd.PrintErrf("Using endpoint from %q. Specify --node-endpoint to override this.\n", pf.PrefixPrintablePath(constants.StateFilename)) - endpoint = stateFile.Infrastructure.ClusterEndpoint - } - if emptyIDs { - cmd.PrintErrf("Using ID from %q. Specify --cluster-id to override this.\n", pf.PrefixPrintablePath(constants.StateFilename)) - ownerID = stateFile.ClusterValues.OwnerID - clusterID = stateFile.ClusterValues.ClusterID - } +func (c *verifyCmd) validateIDFlags(cmd *cobra.Command, stateFile *state.State) (ownerID, clusterID string, err error) { + ownerID, clusterID = c.flags.ownerID, c.flags.clusterID + if c.flags.clusterID == "" { + cmd.PrintErrf("Using ID from %q. Specify --cluster-id to override this.\n", c.flags.pathPrefixer.PrefixPrintablePath(constants.StateFilename)) + clusterID = stateFile.ClusterValues.ClusterID } - - var attestationURL string - if stateFile.Infrastructure.Azure != nil { - attestationURL = stateFile.Infrastructure.Azure.AttestationURL + if ownerID == "" { + // We don't want to print warnings until this is implemented again + // cmd.PrintErrf("Using ID from %q. Specify --owner-id to override this.\n", c.flags.pathPrefixer.PrefixPrintablePath(constants.StateFilename)) + ownerID = stateFile.ClusterValues.OwnerID } // Validate if ownerID == "" && clusterID == "" { - return verifyFlags{}, errors.New("cluster-id not provided to verify the cluster") - } - endpoint, err = addPortIfMissing(endpoint, constants.VerifyServiceNodePortGRPC) - if err != nil { - return verifyFlags{}, fmt.Errorf("validating endpoint argument: %w", err) + return "", "", errors.New("cluster-id not provided to verify the cluster") } - return verifyFlags{ - endpoint: endpoint, - pf: pf, - ownerID: ownerID, - clusterID: clusterID, - output: output, - maaURL: attestationURL, - force: force, - }, nil -} - -type verifyFlags struct { - endpoint string - ownerID string - clusterID string - maaURL string - output string - force bool - pf pathprefix.PathPrefixer + return ownerID, clusterID, nil } -func addPortIfMissing(endpoint string, defaultPort int) (string, error) { +func (c *verifyCmd) validateEndpointFlag(cmd *cobra.Command, stateFile *state.State) (string, error) { + endpoint := c.flags.endpoint if endpoint == "" { - return "", errors.New("endpoint is empty") - } - - _, _, err := net.SplitHostPort(endpoint) - if err == nil { - return endpoint, nil + cmd.PrintErrf("Using endpoint from %q. Specify --node-endpoint to override this.\n", c.flags.pathPrefixer.PrefixPrintablePath(constants.StateFilename)) + endpoint = stateFile.Infrastructure.ClusterEndpoint } - - if strings.Contains(err.Error(), "missing port in address") { - return net.JoinHostPort(endpoint, strconv.Itoa(defaultPort)), nil + endpoint, err := addPortIfMissing(endpoint, constants.VerifyServiceNodePortGRPC) + if err != nil { + return "", fmt.Errorf("validating endpoint argument: %w", err) } - - return "", err + return endpoint, nil } // an attestationDocFormatter formats the attestation document. @@ -869,3 +843,20 @@ func extractAzureInstanceInfo(docString string) (azureInstanceInfo, error) { } return instanceInfo, nil } + +func addPortIfMissing(endpoint string, defaultPort int) (string, error) { + if endpoint == "" { + return "", errors.New("endpoint is empty") + } + + _, _, err := net.SplitHostPort(endpoint) + if err == nil { + return endpoint, nil + } + + if strings.Contains(err.Error(), "missing port in address") { + return net.JoinHostPort(endpoint, strconv.Itoa(defaultPort)), nil + } + + return "", err +} diff --git a/cli/internal/cmd/verify_test.go b/cli/internal/cmd/verify_test.go index f2da96a257..a9a44c4515 100644 --- a/cli/internal/cmd/verify_test.go +++ b/cli/internal/cmd/verify_test.go @@ -58,6 +58,7 @@ func TestVerify(t *testing.T) { nodeEndpointFlag: "192.0.2.1:1234", clusterIDFlag: zeroBase64, protoClient: &stubVerifyClient{}, + stateFile: state.New(), wantEndpoint: "192.0.2.1:1234", formatter: &stubAttDocFormatter{}, }, @@ -66,6 +67,7 @@ func TestVerify(t *testing.T) { nodeEndpointFlag: "192.0.2.1:1234", clusterIDFlag: zeroBase64, protoClient: &stubVerifyClient{}, + stateFile: state.New(), wantEndpoint: "192.0.2.1:1234", formatter: &stubAttDocFormatter{}, }, @@ -74,6 +76,7 @@ func TestVerify(t *testing.T) { nodeEndpointFlag: "192.0.2.1", clusterIDFlag: zeroBase64, protoClient: &stubVerifyClient{}, + stateFile: state.New(), wantEndpoint: "192.0.2.1:" + strconv.Itoa(constants.VerifyServiceNodePortGRPC), formatter: &stubAttDocFormatter{}, }, @@ -81,6 +84,7 @@ func TestVerify(t *testing.T) { provider: cloudprovider.GCP, clusterIDFlag: zeroBase64, protoClient: &stubVerifyClient{}, + stateFile: state.New(), formatter: &stubAttDocFormatter{}, wantErr: true, }, @@ -106,12 +110,14 @@ func TestVerify(t *testing.T) { nodeEndpointFlag: ":::::", clusterIDFlag: zeroBase64, protoClient: &stubVerifyClient{}, + stateFile: state.New(), formatter: &stubAttDocFormatter{}, wantErr: true, }, "neither owner id nor cluster id set": { provider: cloudprovider.GCP, nodeEndpointFlag: "192.0.2.1:1234", + stateFile: state.New(), formatter: &stubAttDocFormatter{}, wantErr: true, }, @@ -127,6 +133,7 @@ func TestVerify(t *testing.T) { provider: cloudprovider.GCP, clusterIDFlag: zeroBase64, nodeEndpointFlag: "192.0.2.1:1234", + stateFile: state.New(), formatter: &stubAttDocFormatter{}, skipConfigCreation: true, wantErr: true, @@ -136,6 +143,7 @@ func TestVerify(t *testing.T) { nodeEndpointFlag: "192.0.2.1:1234", clusterIDFlag: zeroBase64, protoClient: &stubVerifyClient{verifyErr: rpcStatus.Error(codes.Internal, "failed")}, + stateFile: state.New(), formatter: &stubAttDocFormatter{}, wantErr: true, }, @@ -144,6 +152,7 @@ func TestVerify(t *testing.T) { nodeEndpointFlag: "192.0.2.1:1234", clusterIDFlag: zeroBase64, protoClient: &stubVerifyClient{verifyErr: someErr}, + stateFile: state.New(), formatter: &stubAttDocFormatter{}, wantErr: true, }, @@ -152,6 +161,7 @@ func TestVerify(t *testing.T) { nodeEndpointFlag: "192.0.2.1:1234", clusterIDFlag: zeroBase64, protoClient: &stubVerifyClient{}, + stateFile: state.New(), wantEndpoint: "192.0.2.1:1234", formatter: &stubAttDocFormatter{formatErr: someErr}, wantErr: true, @@ -164,31 +174,28 @@ func TestVerify(t *testing.T) { require := require.New(t) cmd := NewVerifyCmd() - cmd.Flags().String("workspace", "", "") // register persistent flag manually - cmd.Flags().Bool("force", true, "") // register persistent flag manually out := &bytes.Buffer{} cmd.SetErr(out) - if tc.clusterIDFlag != "" { - require.NoError(cmd.Flags().Set("cluster-id", tc.clusterIDFlag)) - } - if tc.nodeEndpointFlag != "" { - require.NoError(cmd.Flags().Set("node-endpoint", tc.nodeEndpointFlag)) - } fileHandler := file.NewHandler(afero.NewMemMapFs()) if !tc.skipConfigCreation { cfg := defaultConfigWithExpectedMeasurements(t, config.Default(), tc.provider) require.NoError(fileHandler.WriteYAML(constants.ConfigFilename, cfg)) } - if tc.stateFile != nil { - require.NoError(tc.stateFile.WriteToFile(fileHandler, constants.StateFilename)) + require.NoError(tc.stateFile.WriteToFile(fileHandler, constants.StateFilename)) + + v := &verifyCmd{ + fileHandler: fileHandler, + log: logger.NewTest(t), + flags: verifyFlags{ + clusterID: tc.clusterIDFlag, + endpoint: tc.nodeEndpointFlag, + }, } - - v := &verifyCmd{log: logger.NewTest(t)} formatterFac := func(_ string, _ cloudprovider.Provider, _ debugLog) (attestationDocFormatter, error) { return tc.formatter, nil } - err := v.verify(cmd, fileHandler, tc.protoClient, formatterFac, stubAttestationFetcher{}) + err := v.verify(cmd, tc.protoClient, formatterFac, stubAttestationFetcher{}) if tc.wantErr { assert.Error(err) } else { diff --git a/go.mod b/go.mod index 63f080559f..d781a3ffc1 100644 --- a/go.mod +++ b/go.mod @@ -109,6 +109,7 @@ require ( github.com/sigstore/sigstore v1.7.1 github.com/spf13/afero v1.10.0 github.com/spf13/cobra v1.7.0 + github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 github.com/theupdateframework/go-tuf v0.5.2 github.com/tink-crypto/tink-go/v2 v2.0.0 @@ -137,8 +138,6 @@ require ( sigs.k8s.io/yaml v1.3.0 ) -require github.com/google/go-tdx-guest v0.2.2 // indirect - require ( cloud.google.com/go v0.110.2 // indirect cloud.google.com/go/iam v1.1.0 // indirect @@ -234,6 +233,7 @@ require ( github.com/google/go-attestation v0.5.0 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-containerregistry v0.15.2 // indirect + github.com/google/go-tdx-guest v0.2.2 // indirect github.com/google/go-tspi v0.3.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/logger v1.1.1 // indirect @@ -306,7 +306,6 @@ require ( github.com/shopspring/decimal v1.3.1 // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/cast v1.5.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect github.com/transparency-dev/merkle v0.0.2 // indirect From afb154ceb70a73161724fba0a5e3a43391aee896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Wei=C3=9Fe?= <66256922+daniel-weisse@users.noreply.github.com> Date: Mon, 16 Oct 2023 16:20:32 +0200 Subject: [PATCH 02/17] ci: add missing quotation marks for region flag + revert to northeurope (#2459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add missing quotation marks for region flag * Revert default Azure region to northeurope --------- Signed-off-by: Daniel Weiße --- .../constellation_iam_create/action.yml | 23 +++++++------------ .github/actions/e2e_test/action.yml | 2 +- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/.github/actions/constellation_iam_create/action.yml b/.github/actions/constellation_iam_create/action.yml index eac415a8b6..f5f307e88d 100644 --- a/.github/actions/constellation_iam_create/action.yml +++ b/.github/actions/constellation_iam_create/action.yml @@ -39,14 +39,7 @@ runs: - name: Generate config id: generate-config shell: bash - # TODO(katexochen): Remove the generate-config flag once v2.10 is released. run: | - output=$(constellation iam create --help) - if [[ $output == *"generate-config"* ]]; then - echo "flag=--generate-config" | tee -a "$GITHUB_OUTPUT" - exit 0 - fi - kubernetesFlag="" if [[ ! -z "${{ inputs.kubernetesVersion }}" ]]; then kubernetesFlag="--kubernetes=${{ inputs.kubernetesVersion }}" @@ -60,9 +53,9 @@ runs: if: inputs.cloudProvider == 'aws' run: | constellation iam create aws \ - --zone=${{ inputs.awsZone }} \ - --prefix=${{ inputs.namePrefix }} \ - ${{ steps.generate-config.outputs.flag }} \ + --zone="${{ inputs.awsZone }}" \ + --prefix="${{ inputs.namePrefix }}" \ + --update-config \ --tf-log=DEBUG \ --yes @@ -71,10 +64,10 @@ runs: if: inputs.cloudProvider == 'azure' run: | constellation iam create azure \ - --region=${{ inputs.azureRegion }} \ + --region="${{ inputs.azureRegion }}" \ --resourceGroup="${{ inputs.namePrefix }}-rg" \ --servicePrincipal="${{ inputs.namePrefix }}-sp" \ - ${{ steps.generate-config.outputs.flag }} \ + --update-config \ --tf-log=DEBUG \ --yes @@ -83,9 +76,9 @@ runs: if: inputs.cloudProvider == 'gcp' run: | constellation iam create gcp \ - --projectID=${{ inputs.gcpProjectID }} \ - --zone=${{ inputs.gcpZone }} \ + --projectID="${{ inputs.gcpProjectID }}" \ + --zone="${{ inputs.gcpZone }}" \ --serviceAccountID="${{ inputs.namePrefix }}-sa" \ - ${{ steps.generate-config.outputs.flag }} \ + --update-config \ --tf-log=DEBUG \ --yes diff --git a/.github/actions/e2e_test/action.yml b/.github/actions/e2e_test/action.yml index ce6f3aab91..9e8f5f87c1 100644 --- a/.github/actions/e2e_test/action.yml +++ b/.github/actions/e2e_test/action.yml @@ -209,7 +209,7 @@ runs: cloudProvider: ${{ inputs.cloudProvider }} namePrefix: ${{ steps.create-prefix.outputs.prefix }} awsZone: ${{ inputs.regionZone || 'us-east-2c' }} - azureRegion: ${{ inputs.regionZone || 'westus' }} + azureRegion: ${{ inputs.regionZone || 'northeurope' }} gcpProjectID: ${{ inputs.gcpProject }} gcpZone: ${{ inputs.regionZone || 'europe-west3-b' }} kubernetesVersion: ${{ inputs.kubernetesVersion }} From e5513f14e6bdaab0ac270e2a57b10a91c5ae173a Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Mon, 16 Oct 2023 16:49:07 +0200 Subject: [PATCH 03/17] cli: add field docs to the state file (#2453) * add field docs to the state file * mark only optional fields * tidy * use talos encoder --- cli/internal/state/BUILD.bazel | 8 +- cli/internal/state/state.go | 137 ++++++++++++++------ cli/internal/state/state_doc.go | 215 +++++++++++++++++++++++++++++++ cli/internal/state/state_test.go | 4 +- 4 files changed, 319 insertions(+), 45 deletions(-) create mode 100644 cli/internal/state/state_doc.go diff --git a/cli/internal/state/BUILD.bazel b/cli/internal/state/BUILD.bazel index 914e81bd29..5d5e626070 100644 --- a/cli/internal/state/BUILD.bazel +++ b/cli/internal/state/BUILD.bazel @@ -3,12 +3,16 @@ load("//bazel/go:go_test.bzl", "go_test") go_library( name = "state", - srcs = ["state.go"], + srcs = [ + "state.go", + "state_doc.go", + ], importpath = "github.com/edgelesssys/constellation/v2/cli/internal/state", visibility = ["//cli:__subpackages__"], deps = [ "//internal/file", "@cat_dario_mergo//:mergo", + "@com_github_siderolabs_talos_pkg_machinery//config/encoder", ], ) @@ -19,8 +23,8 @@ go_test( deps = [ "//internal/constants", "//internal/file", + "@com_github_siderolabs_talos_pkg_machinery//config/encoder", "@com_github_spf13_afero//:afero", "@com_github_stretchr_testify//assert", - "@in_gopkg_yaml_v3//:yaml_v3", ], ) diff --git a/cli/internal/state/state.go b/cli/internal/state/state.go index 4ea873fc3f..e1283d4a21 100644 --- a/cli/internal/state/state.go +++ b/cli/internal/state/state.go @@ -4,6 +4,11 @@ Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ +// This binary can be build from siderolabs/talos projects. Located at: +// https://github.com/siderolabs/talos/tree/master/hack/docgen +// +//go:generate docgen ./state.go ./state_doc.go Configuration + // package state defines the structure of the Constellation state file. package state @@ -30,9 +35,98 @@ func ReadFromFile(fileHandler file.Handler, path string) (*State, error) { // State describe the entire state to describe a Constellation cluster. type State struct { - Version string `yaml:"version"` + // description: | + // Schema version of this state file. + Version string `yaml:"version"` + + // TODO(msanft): Add link to self-managed infrastructure docs once existing. + + // description: | + // State of the cluster's cloud resources. These values are retrieved during + // cluster creation. In the case of self-managed infrastructure, the marked + // fields in this struct should be filled by the user as per + // https://docs.edgeless.systems/constellation/workflows/create. Infrastructure Infrastructure `yaml:"infrastructure"` - ClusterValues ClusterValues `yaml:"clusterValues"` + // description: | + // DO NOT EDIT. State of the Constellation Kubernetes cluster. + // These values are set during cluster initialization and should not be changed. + ClusterValues ClusterValues `yaml:"clusterValues"` +} + +// ClusterValues describe the (Kubernetes) cluster state, set during initialization of the cluster. +type ClusterValues struct { + // description: | + // Unique identifier of the cluster. + ClusterID string `yaml:"clusterID"` + // description: | + // Unique identifier of the owner of the cluster. + OwnerID string `yaml:"ownerID"` + // description: | + // Salt used to generate the ClusterID on the bootstrapping node. + MeasurementSalt []byte `yaml:"measurementSalt"` +} + +// Infrastructure describe the state related to the cloud resources of the cluster. +type Infrastructure struct { + // description: | + // Unique identifier the cluster's cloud resources are tagged with. + UID string `yaml:"uid"` + // description: | + // Endpoint the cluster can be reached at. + ClusterEndpoint string `yaml:"clusterEndpoint"` + // description: | + // Secret used to authenticate the bootstrapping node. + InitSecret []byte `yaml:"initSecret"` + // description: | + // List of Subject Alternative Names (SANs) to add to the Kubernetes API server certificate. + // If no SANs should be added, this field can be left empty. + APIServerCertSANs []string `yaml:"apiServerCertSANs"` + // description: | + // Name used in the cluster's named resources. + Name string `yaml:"name"` + // description: | + // Values specific to a Constellation cluster running on Azure. + Azure *Azure `yaml:"azure,omitempty"` + // description: | + // Values specific to a Constellation cluster running on GCP. + GCP *GCP `yaml:"gcp,omitempty"` +} + +// GCP describes the infra state related to GCP. +type GCP struct { + // description: | + // Project ID of the GCP project the cluster is running in. + ProjectID string `yaml:"projectID"` + // description: | + // CIDR range of the cluster's nodes. + IPCidrNode string `yaml:"ipCidrNode"` + // description: | + // CIDR range of the cluster's pods. + IPCidrPod string `yaml:"ipCidrPod"` +} + +// Azure describes the infra state related to Azure. +type Azure struct { + // description: | + // Resource Group the cluster's resources are placed in. + ResourceGroup string `yaml:"resourceGroup"` + // description: | + // ID of the Azure subscription the cluster is running in. + SubscriptionID string `yaml:"subscriptionID"` + // description: | + // Security group name of the cluster's resource group. + NetworkSecurityGroupName string `yaml:"networkSecurityGroupName"` + // description: | + // Name of the cluster's load balancer. + LoadBalancerName string `yaml:"loadBalancerName"` + // description: | + // ID of the UAMI the cluster's nodes are running with. + UserAssignedIdentity string `yaml:"userAssignedIdentity"` + // description: | + // MAA endpoint that can be used as a fallback for veryifying the ID key digests + // in the cluster's attestation report if the enforcement policy is set accordingly. + // Can be left empty otherwise. + AttestationURL string `yaml:"attestationURL"` } // New creates a new cluster state (file). @@ -70,42 +164,3 @@ func (s *State) Merge(other *State) (*State, error) { } return s, nil } - -// ClusterValues describe the (Kubernetes) cluster state, set during initialization of the cluster. -type ClusterValues struct { - // ClusterID is the unique identifier of the cluster. - ClusterID string `yaml:"clusterID"` - // OwnerID is the unique identifier of the owner of the cluster. - OwnerID string `yaml:"ownerID"` - // MeasurementSalt is the salt generated during cluster init. - MeasurementSalt []byte `yaml:"measurementSalt"` -} - -// Infrastructure describe the state related to the cloud resources of the cluster. -type Infrastructure struct { - UID string `yaml:"uid"` - ClusterEndpoint string `yaml:"clusterEndpoint"` - InitSecret []byte `yaml:"initSecret"` - APIServerCertSANs []string `yaml:"apiServerCertSANs"` - // Name is the name of the cluster. - Name string `yaml:"name"` - Azure *Azure `yaml:"azure,omitempty"` - GCP *GCP `yaml:"gcp,omitempty"` -} - -// GCP describes the infra state related to GCP. -type GCP struct { - ProjectID string `yaml:"projectID"` - IPCidrNode string `yaml:"ipCidrNode"` - IPCidrPod string `yaml:"ipCidrPod"` -} - -// Azure describes the infra state related to Azure. -type Azure struct { - ResourceGroup string `yaml:"resourceGroup"` - SubscriptionID string `yaml:"subscriptionID"` - NetworkSecurityGroupName string `yaml:"networkSecurityGroupName"` - LoadBalancerName string `yaml:"loadBalancerName"` - UserAssignedIdentity string `yaml:"userAssignedIdentity"` - AttestationURL string `yaml:"attestationURL"` -} diff --git a/cli/internal/state/state_doc.go b/cli/internal/state/state_doc.go new file mode 100644 index 0000000000..c57088ac3b --- /dev/null +++ b/cli/internal/state/state_doc.go @@ -0,0 +1,215 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Code generated by hack/docgen tool. DO NOT EDIT. + +package state + +import ( + "github.com/siderolabs/talos/pkg/machinery/config/encoder" +) + +var ( + StateDoc encoder.Doc + ClusterValuesDoc encoder.Doc + InfrastructureDoc encoder.Doc + GCPDoc encoder.Doc + AzureDoc encoder.Doc +) + +func init() { + StateDoc.Type = "State" + StateDoc.Comments[encoder.LineComment] = "State describe the entire state to describe a Constellation cluster." + StateDoc.Description = "State describe the entire state to describe a Constellation cluster." + StateDoc.Fields = make([]encoder.Doc, 3) + StateDoc.Fields[0].Name = "version" + StateDoc.Fields[0].Type = "string" + StateDoc.Fields[0].Note = "" + StateDoc.Fields[0].Description = "Schema version of this state file." + StateDoc.Fields[0].Comments[encoder.LineComment] = "Schema version of this state file." + StateDoc.Fields[1].Name = "infrastructure" + StateDoc.Fields[1].Type = "Infrastructure" + StateDoc.Fields[1].Note = "" + StateDoc.Fields[1].Description = "State of the cluster's cloud resources. These values are retrieved during\ncluster creation. In the case of self-managed infrastructure, the marked\nfields in this struct should be filled by the user as per\nhttps://docs.edgeless.systems/constellation/workflows/create." + StateDoc.Fields[1].Comments[encoder.LineComment] = "State of the cluster's cloud resources. These values are retrieved during" + StateDoc.Fields[2].Name = "clusterValues" + StateDoc.Fields[2].Type = "ClusterValues" + StateDoc.Fields[2].Note = "" + StateDoc.Fields[2].Description = "DO NOT EDIT. State of the Constellation Kubernetes cluster.\nThese values are set during cluster initialization and should not be changed." + StateDoc.Fields[2].Comments[encoder.LineComment] = "DO NOT EDIT. State of the Constellation Kubernetes cluster." + + ClusterValuesDoc.Type = "ClusterValues" + ClusterValuesDoc.Comments[encoder.LineComment] = "ClusterValues describe the (Kubernetes) cluster state, set during initialization of the cluster." + ClusterValuesDoc.Description = "ClusterValues describe the (Kubernetes) cluster state, set during initialization of the cluster." + ClusterValuesDoc.AppearsIn = []encoder.Appearance{ + { + TypeName: "State", + FieldName: "clusterValues", + }, + } + ClusterValuesDoc.Fields = make([]encoder.Doc, 3) + ClusterValuesDoc.Fields[0].Name = "clusterID" + ClusterValuesDoc.Fields[0].Type = "string" + ClusterValuesDoc.Fields[0].Note = "" + ClusterValuesDoc.Fields[0].Description = "Unique identifier of the cluster." + ClusterValuesDoc.Fields[0].Comments[encoder.LineComment] = "Unique identifier of the cluster." + ClusterValuesDoc.Fields[1].Name = "ownerID" + ClusterValuesDoc.Fields[1].Type = "string" + ClusterValuesDoc.Fields[1].Note = "" + ClusterValuesDoc.Fields[1].Description = "Unique identifier of the owner of the cluster." + ClusterValuesDoc.Fields[1].Comments[encoder.LineComment] = "Unique identifier of the owner of the cluster." + ClusterValuesDoc.Fields[2].Name = "measurementSalt" + ClusterValuesDoc.Fields[2].Type = "[]byte" + ClusterValuesDoc.Fields[2].Note = "" + ClusterValuesDoc.Fields[2].Description = "Salt used to generate the ClusterID on the bootstrapping node." + ClusterValuesDoc.Fields[2].Comments[encoder.LineComment] = "Salt used to generate the ClusterID on the bootstrapping node." + + InfrastructureDoc.Type = "Infrastructure" + InfrastructureDoc.Comments[encoder.LineComment] = "Infrastructure describe the state related to the cloud resources of the cluster." + InfrastructureDoc.Description = "Infrastructure describe the state related to the cloud resources of the cluster." + InfrastructureDoc.AppearsIn = []encoder.Appearance{ + { + TypeName: "State", + FieldName: "infrastructure", + }, + } + InfrastructureDoc.Fields = make([]encoder.Doc, 7) + InfrastructureDoc.Fields[0].Name = "uid" + InfrastructureDoc.Fields[0].Type = "string" + InfrastructureDoc.Fields[0].Note = "" + InfrastructureDoc.Fields[0].Description = "Unique identifier the cluster's cloud resources are tagged with." + InfrastructureDoc.Fields[0].Comments[encoder.LineComment] = "Unique identifier the cluster's cloud resources are tagged with." + InfrastructureDoc.Fields[1].Name = "clusterEndpoint" + InfrastructureDoc.Fields[1].Type = "string" + InfrastructureDoc.Fields[1].Note = "" + InfrastructureDoc.Fields[1].Description = "Endpoint the cluster can be reached at." + InfrastructureDoc.Fields[1].Comments[encoder.LineComment] = "Endpoint the cluster can be reached at." + InfrastructureDoc.Fields[2].Name = "initSecret" + InfrastructureDoc.Fields[2].Type = "[]byte" + InfrastructureDoc.Fields[2].Note = "" + InfrastructureDoc.Fields[2].Description = "Secret used to authenticate the bootstrapping node." + InfrastructureDoc.Fields[2].Comments[encoder.LineComment] = "Secret used to authenticate the bootstrapping node." + InfrastructureDoc.Fields[3].Name = "apiServerCertSANs" + InfrastructureDoc.Fields[3].Type = "[]string" + InfrastructureDoc.Fields[3].Note = "" + InfrastructureDoc.Fields[3].Description = "description: |\n List of Subject Alternative Names (SANs) to add to the Kubernetes API server certificate.\n If no SANs should be added, this field can be left empty.\n" + InfrastructureDoc.Fields[3].Comments[encoder.LineComment] = "description: |" + InfrastructureDoc.Fields[4].Name = "name" + InfrastructureDoc.Fields[4].Type = "string" + InfrastructureDoc.Fields[4].Note = "" + InfrastructureDoc.Fields[4].Description = "Name used in the cluster's named resources." + InfrastructureDoc.Fields[4].Comments[encoder.LineComment] = "Name used in the cluster's named resources." + InfrastructureDoc.Fields[5].Name = "azure" + InfrastructureDoc.Fields[5].Type = "Azure" + InfrastructureDoc.Fields[5].Note = "" + InfrastructureDoc.Fields[5].Description = "Values specific to a Constellation cluster running on Azure." + InfrastructureDoc.Fields[5].Comments[encoder.LineComment] = "Values specific to a Constellation cluster running on Azure." + InfrastructureDoc.Fields[6].Name = "gcp" + InfrastructureDoc.Fields[6].Type = "GCP" + InfrastructureDoc.Fields[6].Note = "" + InfrastructureDoc.Fields[6].Description = "Values specific to a Constellation cluster running on GCP." + InfrastructureDoc.Fields[6].Comments[encoder.LineComment] = "Values specific to a Constellation cluster running on GCP." + + GCPDoc.Type = "GCP" + GCPDoc.Comments[encoder.LineComment] = "GCP describes the infra state related to GCP." + GCPDoc.Description = "GCP describes the infra state related to GCP." + GCPDoc.AppearsIn = []encoder.Appearance{ + { + TypeName: "Infrastructure", + FieldName: "gcp", + }, + } + GCPDoc.Fields = make([]encoder.Doc, 3) + GCPDoc.Fields[0].Name = "projectID" + GCPDoc.Fields[0].Type = "string" + GCPDoc.Fields[0].Note = "" + GCPDoc.Fields[0].Description = "Project ID of the GCP project the cluster is running in." + GCPDoc.Fields[0].Comments[encoder.LineComment] = "Project ID of the GCP project the cluster is running in." + GCPDoc.Fields[1].Name = "ipCidrNode" + GCPDoc.Fields[1].Type = "string" + GCPDoc.Fields[1].Note = "" + GCPDoc.Fields[1].Description = "CIDR range of the cluster's nodes." + GCPDoc.Fields[1].Comments[encoder.LineComment] = "CIDR range of the cluster's nodes." + GCPDoc.Fields[2].Name = "ipCidrPod" + GCPDoc.Fields[2].Type = "string" + GCPDoc.Fields[2].Note = "" + GCPDoc.Fields[2].Description = "CIDR range of the cluster's pods." + GCPDoc.Fields[2].Comments[encoder.LineComment] = "CIDR range of the cluster's pods." + + AzureDoc.Type = "Azure" + AzureDoc.Comments[encoder.LineComment] = "Azure describes the infra state related to Azure." + AzureDoc.Description = "Azure describes the infra state related to Azure." + AzureDoc.AppearsIn = []encoder.Appearance{ + { + TypeName: "Infrastructure", + FieldName: "azure", + }, + } + AzureDoc.Fields = make([]encoder.Doc, 6) + AzureDoc.Fields[0].Name = "resourceGroup" + AzureDoc.Fields[0].Type = "string" + AzureDoc.Fields[0].Note = "" + AzureDoc.Fields[0].Description = "Resource Group the cluster's resources are placed in." + AzureDoc.Fields[0].Comments[encoder.LineComment] = "Resource Group the cluster's resources are placed in." + AzureDoc.Fields[1].Name = "subscriptionID" + AzureDoc.Fields[1].Type = "string" + AzureDoc.Fields[1].Note = "" + AzureDoc.Fields[1].Description = "ID of the Azure subscription the cluster is running in." + AzureDoc.Fields[1].Comments[encoder.LineComment] = "ID of the Azure subscription the cluster is running in." + AzureDoc.Fields[2].Name = "networkSecurityGroupName" + AzureDoc.Fields[2].Type = "string" + AzureDoc.Fields[2].Note = "" + AzureDoc.Fields[2].Description = "Security group name of the cluster's resource group." + AzureDoc.Fields[2].Comments[encoder.LineComment] = "Security group name of the cluster's resource group." + AzureDoc.Fields[3].Name = "loadBalancerName" + AzureDoc.Fields[3].Type = "string" + AzureDoc.Fields[3].Note = "" + AzureDoc.Fields[3].Description = "Name of the cluster's load balancer." + AzureDoc.Fields[3].Comments[encoder.LineComment] = "Name of the cluster's load balancer." + AzureDoc.Fields[4].Name = "userAssignedIdentity" + AzureDoc.Fields[4].Type = "string" + AzureDoc.Fields[4].Note = "" + AzureDoc.Fields[4].Description = "ID of the UAMI the cluster's nodes are running with." + AzureDoc.Fields[4].Comments[encoder.LineComment] = "ID of the UAMI the cluster's nodes are running with." + AzureDoc.Fields[5].Name = "attestationURL" + AzureDoc.Fields[5].Type = "string" + AzureDoc.Fields[5].Note = "" + AzureDoc.Fields[5].Description = "MAA endpoint that can be used as a fallback for veryifying the ID key digests\nin the cluster's attestation report if the enforcement policy is set accordingly.\nCan be left empty otherwise." + AzureDoc.Fields[5].Comments[encoder.LineComment] = "MAA endpoint that can be used as a fallback for veryifying the ID key digests" +} + +func (_ State) Doc() *encoder.Doc { + return &StateDoc +} + +func (_ ClusterValues) Doc() *encoder.Doc { + return &ClusterValuesDoc +} + +func (_ Infrastructure) Doc() *encoder.Doc { + return &InfrastructureDoc +} + +func (_ GCP) Doc() *encoder.Doc { + return &GCPDoc +} + +func (_ Azure) Doc() *encoder.Doc { + return &AzureDoc +} + +// GetConfigurationDoc returns documentation for the file ./state_doc.go. +func GetConfigurationDoc() *encoder.FileDoc { + return &encoder.FileDoc{ + Name: "Configuration", + Description: "package state defines the structure of the Constellation state file.\n", + Structs: []*encoder.Doc{ + &StateDoc, + &ClusterValuesDoc, + &InfrastructureDoc, + &GCPDoc, + &AzureDoc, + }, + } +} diff --git a/cli/internal/state/state_test.go b/cli/internal/state/state_test.go index dd7c305bbc..96311977e8 100644 --- a/cli/internal/state/state_test.go +++ b/cli/internal/state/state_test.go @@ -11,9 +11,9 @@ import ( "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" + "github.com/siderolabs/talos/pkg/machinery/config/encoder" "github.com/spf13/afero" "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" ) var defaultState = &State{ @@ -145,7 +145,7 @@ func TestReadFromFile(t *testing.T) { func mustMarshalYaml(t *testing.T, v any) []byte { t.Helper() - b, err := yaml.Marshal(v) + b, err := encoder.NewEncoder(v).Encode() if err != nil { t.Fatalf("failed to marshal yaml: %v", err) } From 25b23689add369bf81a64f824744ab6bcd828814 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Mon, 16 Oct 2023 20:18:59 +0200 Subject: [PATCH 04/17] cli: generate state file during `constellation config generate` (#2455) * create state file during config generate * use written file in `constellation create` * document creation of state file * remove accidentally added test * check error when writing state file --- cli/internal/cmd/configgenerate.go | 23 ++++++++++++++--- cli/internal/cmd/configgenerate_test.go | 13 ++++++++++ cli/internal/cmd/create.go | 15 +++++------ cli/internal/cmd/create_test.go | 33 +++++++++++++++++++----- docs/docs/getting-started/first-steps.md | 2 +- docs/docs/reference/cli.md | 6 ++--- 6 files changed, 70 insertions(+), 22 deletions(-) diff --git a/cli/internal/cmd/configgenerate.go b/cli/internal/cmd/configgenerate.go index 7a8f05ec71..36c7448917 100644 --- a/cli/internal/cmd/configgenerate.go +++ b/cli/internal/cmd/configgenerate.go @@ -10,6 +10,7 @@ import ( "fmt" "strings" + "github.com/edgelesssys/constellation/v2/cli/internal/state" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/config" @@ -25,8 +26,8 @@ import ( func newConfigGenerateCmd() *cobra.Command { cmd := &cobra.Command{ Use: "generate {aws|azure|gcp|openstack|qemu|stackit}", - Short: "Generate a default configuration file", - Long: "Generate a default configuration file for your selected cloud provider.", + Short: "Generate a default configuration and state file", + Long: "Generate a default configuration and state file for your selected cloud provider.", Args: cobra.MatchAll( cobra.ExactArgs(1), isCloudProvider(0), @@ -92,6 +93,8 @@ func runConfigGenerate(cmd *cobra.Command, args []string) error { func (cg *configGenerateCmd) configGenerate(cmd *cobra.Command, fileHandler file.Handler, provider cloudprovider.Provider, rawProvider string) error { cg.log.Debugf("Using cloud provider %s", provider.String()) + + // Config creation conf, err := createConfigWithAttestationVariant(provider, rawProvider, cg.flags.attestationVariant) if err != nil { return fmt.Errorf("creating config: %w", err) @@ -99,11 +102,25 @@ func (cg *configGenerateCmd) configGenerate(cmd *cobra.Command, fileHandler file conf.KubernetesVersion = cg.flags.k8sVersion cg.log.Debugf("Writing YAML data to configuration file") if err := fileHandler.WriteYAML(constants.ConfigFilename, conf, file.OptMkdirAll); err != nil { - return err + return fmt.Errorf("writing config file: %w", err) } cmd.Println("Config file written to", cg.flags.pathPrefixer.PrefixPrintablePath(constants.ConfigFilename)) cmd.Println("Please fill in your CSP-specific configuration before proceeding.") + + // State-file creation + stateFile := state.New() + switch provider { + case cloudprovider.GCP: + stateFile.SetInfrastructure(state.Infrastructure{GCP: &state.GCP{}}) + case cloudprovider.Azure: + stateFile.SetInfrastructure(state.Infrastructure{Azure: &state.Azure{}}) + } + if err = stateFile.WriteToFile(fileHandler, constants.StateFilename); err != nil { + return fmt.Errorf("writing state file: %w", err) + } + cmd.Println("State file written to", cg.flags.pathPrefixer.PrefixPrintablePath(constants.StateFilename)) + cmd.Println("For more information refer to the documentation:") cmd.Println("\thttps://docs.edgeless.systems/constellation/getting-started/first-steps") diff --git a/cli/internal/cmd/configgenerate_test.go b/cli/internal/cmd/configgenerate_test.go index c0be7160fc..6c84179889 100644 --- a/cli/internal/cmd/configgenerate_test.go +++ b/cli/internal/cmd/configgenerate_test.go @@ -11,6 +11,7 @@ import ( "strings" "testing" + "github.com/edgelesssys/constellation/v2/cli/internal/state" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/config" @@ -103,6 +104,9 @@ func TestConfigGenerateDefault(t *testing.T) { err := fileHandler.ReadYAML(constants.ConfigFilename, &readConfig) assert.NoError(err) assert.Equal(*config.Default(), readConfig) + + _, err = state.ReadFromFile(fileHandler, constants.StateFilename) + assert.NoError(err) } func TestConfigGenerateDefaultProviderSpecific(t *testing.T) { @@ -152,6 +156,15 @@ func TestConfigGenerateDefaultProviderSpecific(t *testing.T) { err := fileHandler.ReadYAML(constants.ConfigFilename, &readConfig) assert.NoError(err) assert.Equal(*wantConf, readConfig) + + stateFile, err := state.ReadFromFile(fileHandler, constants.StateFilename) + assert.NoError(err) + switch tc.provider { + case cloudprovider.GCP: + assert.NotNil(stateFile.Infrastructure.GCP) + case cloudprovider.Azure: + assert.NotNil(stateFile.Infrastructure.Azure) + } }) } } diff --git a/cli/internal/cmd/create.go b/cli/internal/cmd/create.go index 4c3140227a..c9be99d3c9 100644 --- a/cli/internal/cmd/create.go +++ b/cli/internal/cmd/create.go @@ -191,8 +191,12 @@ func (c *createCmd) create(cmd *cobra.Command, creator cloudCreator, fileHandler } c.log.Debugf("Successfully created the cloud resources for the cluster") - state := state.New().SetInfrastructure(infraState) - if err := state.WriteToFile(fileHandler, constants.StateFilename); err != nil { + stateFile, err := state.ReadFromFile(fileHandler, constants.StateFilename) + if err != nil { + return fmt.Errorf("reading state file: %w", err) + } + stateFile = stateFile.SetInfrastructure(infraState) + if err := stateFile.WriteToFile(fileHandler, constants.StateFilename); err != nil { return fmt.Errorf("writing state file: %w", err) } @@ -216,13 +220,6 @@ func (c *createCmd) checkDirClean(fileHandler file.Handler) error { c.flags.pathPrefixer.PrefixPrintablePath(constants.MasterSecretFilename), ) } - c.log.Debugf("Checking state file") - if _, err := fileHandler.Stat(constants.StateFilename); !errors.Is(err, fs.ErrNotExist) { - return fmt.Errorf( - "file '%s' already exists in working directory. Constellation won't overwrite previous cluster state. Move it somewhere or delete it before creating a new cluster", - c.flags.pathPrefixer.PrefixPrintablePath(constants.StateFilename), - ) - } return nil } diff --git a/cli/internal/cmd/create_test.go b/cli/internal/cmd/create_test.go index 8767662137..5df2f7fa71 100644 --- a/cli/internal/cmd/create_test.go +++ b/cli/internal/cmd/create_test.go @@ -24,7 +24,21 @@ import ( ) func TestCreate(t *testing.T) { - fsWithDefaultConfig := func(require *require.Assertions, provider cloudprovider.Provider) afero.Fs { + fsWithDefaultConfigAndState := func(require *require.Assertions, provider cloudprovider.Provider) afero.Fs { + fs := afero.NewMemMapFs() + file := file.NewHandler(fs) + require.NoError(file.WriteYAML(constants.ConfigFilename, defaultConfigWithExpectedMeasurements(t, config.Default(), provider))) + stateFile := state.New() + switch provider { + case cloudprovider.GCP: + stateFile.SetInfrastructure(state.Infrastructure{GCP: &state.GCP{}}) + case cloudprovider.Azure: + stateFile.SetInfrastructure(state.Infrastructure{Azure: &state.Azure{}}) + } + require.NoError(stateFile.WriteToFile(file, constants.StateFilename)) + return fs + } + fsWithoutState := func(require *require.Assertions, provider cloudprovider.Provider) afero.Fs { fs := afero.NewMemMapFs() file := file.NewHandler(fs) require.NoError(file.WriteYAML(constants.ConfigFilename, defaultConfigWithExpectedMeasurements(t, config.Default(), provider))) @@ -45,26 +59,26 @@ func TestCreate(t *testing.T) { wantAbort bool }{ "create": { - setupFs: fsWithDefaultConfig, + setupFs: fsWithDefaultConfigAndState, creator: &stubCloudCreator{state: infraState}, provider: cloudprovider.GCP, yesFlag: true, }, "interactive": { - setupFs: fsWithDefaultConfig, + setupFs: fsWithDefaultConfigAndState, creator: &stubCloudCreator{state: infraState}, provider: cloudprovider.Azure, stdin: "yes\n", }, "interactive abort": { - setupFs: fsWithDefaultConfig, + setupFs: fsWithDefaultConfigAndState, creator: &stubCloudCreator{}, provider: cloudprovider.GCP, stdin: "no\n", wantAbort: true, }, "interactive error": { - setupFs: fsWithDefaultConfig, + setupFs: fsWithDefaultConfigAndState, creator: &stubCloudCreator{}, provider: cloudprovider.GCP, stdin: "foo\nfoo\nfoo\n", @@ -103,8 +117,15 @@ func TestCreate(t *testing.T) { yesFlag: true, wantErr: true, }, + "state file does not exist": { + setupFs: fsWithoutState, + creator: &stubCloudCreator{}, + provider: cloudprovider.GCP, + yesFlag: true, + wantErr: true, + }, "create error": { - setupFs: fsWithDefaultConfig, + setupFs: fsWithDefaultConfigAndState, creator: &stubCloudCreator{createErr: someErr}, provider: cloudprovider.GCP, yesFlag: true, diff --git a/docs/docs/getting-started/first-steps.md b/docs/docs/getting-started/first-steps.md index 07b7f84106..6435271b88 100644 --- a/docs/docs/getting-started/first-steps.md +++ b/docs/docs/getting-started/first-steps.md @@ -13,7 +13,7 @@ If you encounter any problem with the following steps, make sure to use the [lat ## Create a cluster -1. Create the [configuration file](../workflows/config.md) for your cloud provider. +1. Create the [configuration file](../workflows/config.md) and state file for your cloud provider. diff --git a/docs/docs/reference/cli.md b/docs/docs/reference/cli.md index b5230e279a..a8556fc28b 100644 --- a/docs/docs/reference/cli.md +++ b/docs/docs/reference/cli.md @@ -12,7 +12,7 @@ constellation [command] Commands: * [config](#constellation-config): Work with the Constellation configuration file - * [generate](#constellation-config-generate): Generate a default configuration file + * [generate](#constellation-config-generate): Generate a default configuration and state file * [fetch-measurements](#constellation-config-fetch-measurements): Fetch measurements for configured cloud provider and image * [instance-types](#constellation-config-instance-types): Print the supported instance types for all cloud providers * [kubernetes-versions](#constellation-config-kubernetes-versions): Print the Kubernetes versions supported by this CLI @@ -64,11 +64,11 @@ Work with the Constellation configuration file. ## constellation config generate -Generate a default configuration file +Generate a default configuration and state file ### Synopsis -Generate a default configuration file for your selected cloud provider. +Generate a default configuration and state file for your selected cloud provider. ``` constellation config generate {aws|azure|gcp|openstack|qemu|stackit} [flags] From 8bc1d80d862f0ce6c215b8db0ff9fe3f7aa587d1 Mon Sep 17 00:00:00 2001 From: Malte Poll Date: Mon, 16 Oct 2023 16:45:36 +0200 Subject: [PATCH 05/17] image: install rpms from lockfile --- WORKSPACE.bazel | 29 ++ bazel/mkosi/mkosi_image.bzl | 4 + bazel/mkosi/mkosi_wrapper.sh.in | 13 + bazel/rpm/BUILD.bazel | 0 bazel/rpm/BUILD.bazel.tpl | 10 + bazel/rpm/package_manifest.bzl | 54 +++ flake.nix | 6 + image/base/BUILD.bazel | 2 +- image/base/mkosi.conf | 2 - image/initrd/BUILD.bazel | 1 + image/initrd/mkosi.conf | 2 - image/initrd/reposdir/amzn2-core.repo | 39 -- image/mirror/BUILD.bazel | 19 + image/mirror/SHA256SUMS | 353 ++++++++++++++++++ image/mirror/dnf.conf | 11 + image/mirror/packages.txt | 39 ++ image/mirror/update_packages.sh | 88 +++++ .../upstream-repos}/amzn2-core.repo | 0 .../mirror/upstream-repos/fedora-updates.repo | 36 ++ image/mirror/upstream-repos/fedora.repo | 36 ++ 20 files changed, 700 insertions(+), 44 deletions(-) create mode 100644 bazel/rpm/BUILD.bazel create mode 100644 bazel/rpm/BUILD.bazel.tpl create mode 100644 bazel/rpm/package_manifest.bzl delete mode 100644 image/initrd/reposdir/amzn2-core.repo create mode 100644 image/mirror/BUILD.bazel create mode 100644 image/mirror/SHA256SUMS create mode 100644 image/mirror/dnf.conf create mode 100644 image/mirror/packages.txt create mode 100755 image/mirror/update_packages.sh rename image/{base/reposdir => mirror/upstream-repos}/amzn2-core.repo (100%) create mode 100644 image/mirror/upstream-repos/fedora-updates.repo create mode 100644 image/mirror/upstream-repos/fedora.repo diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index 17e1eac6b9..eb7e40c73a 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -21,6 +21,27 @@ nixpkgs_git_repository( sha256 = "2c8c39259595441e2fe529b75b2e69eba486e0f3457e810bf9bb2b531822743e", ) +nixpkgs_flake_package( + name = "awscli", + nix_flake_file = "//:flake.nix", + nix_flake_lock_file = "//:flake.lock", + package = "awscli2", +) + +nixpkgs_flake_package( + name = "createrepo_c", + nix_flake_file = "//:flake.nix", + nix_flake_lock_file = "//:flake.lock", + package = "createrepo_c", +) + +nixpkgs_flake_package( + name = "dnf5", + nix_flake_file = "//:flake.nix", + nix_flake_lock_file = "//:flake.lock", + package = "dnf5", +) + nixpkgs_flake_package( name = "mkosi", nix_flake_file = "//:flake.nix", @@ -234,3 +255,11 @@ k8s_deps() load("//bazel/toolchains:linux_kernel.bzl", "kernel_rpms") kernel_rpms() + +# mkosi rpms +load("//bazel/rpm:package_manifest.bzl", "rpm_repository") + +rpm_repository( + name = "mkosi_rpms", + lockfile = "//image/mirror:SHA256SUMS", +) diff --git a/bazel/mkosi/mkosi_image.bzl b/bazel/mkosi/mkosi_image.bzl index 45c00df8c3..c917c09ad0 100644 --- a/bazel/mkosi/mkosi_image.bzl +++ b/bazel/mkosi/mkosi_image.bzl @@ -19,6 +19,8 @@ def _mkosi_image_impl(ctx): args.add_all(ctx.attr.packages, before_each = "--package") for package_file in ctx.files.package_files: args.add("--package", config_rel(package_file.path)) + if len(ctx.files.local_mirror) > 0: + env["LOCAL_MIRROR"] = config_rel(ctx.files.local_mirror[0].dirname) for tree in ctx.files.base_trees: args.add("--base-tree", config_rel(tree.path)) for tree in ctx.files.skeleton_trees: @@ -38,6 +40,7 @@ def _mkosi_image_impl(ctx): inputs.extend(ctx.files.package_manager_trees[:]) inputs.extend(ctx.files.extra_trees[:]) inputs.extend(ctx.files.initrds[:]) + inputs.extend(ctx.files.local_mirror[:]) if ctx.attr.source_date_epoch: args.add("--source-date-epoch", ctx.attr.source_date_epoch) if ctx.attr.seed: @@ -112,6 +115,7 @@ mkosi_image = rule( "initrds": attr.label_list(allow_files = True), "kernel_command_line": attr.string(), "kernel_command_line_dict": attr.string_dict(), + "local_mirror": attr.label_list(allow_files = True), "mkosi_conf": attr.label( allow_single_file = True, mandatory = True, diff --git a/bazel/mkosi/mkosi_wrapper.sh.in b/bazel/mkosi/mkosi_wrapper.sh.in index 97078acbcb..c8e541a0b3 100644 --- a/bazel/mkosi/mkosi_wrapper.sh.in +++ b/bazel/mkosi/mkosi_wrapper.sh.in @@ -11,4 +11,17 @@ if [[ -n ${VERSION_FILE+x} ]]; then args+=("$VERSION_ARG") fi +if [[ -n ${LOCAL_MIRROR+x} ]]; then + LOCAL_MIRROR=$(realpath "${LOCAL_MIRROR}") + reposdir=$(mktemp -d) + cat > "${reposdir}/mkosi.repo" << EOF +[local-mirror] +name=local-mirror +baseurl=file://${LOCAL_MIRROR} +enabled=1 +gpgcheck=0 +EOF + args+=("--package-manager-tree=${reposdir}:/etc/yum.repos.d") +fi + exec @@MKOSI@@ "${args[@]}" build diff --git a/bazel/rpm/BUILD.bazel b/bazel/rpm/BUILD.bazel new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bazel/rpm/BUILD.bazel.tpl b/bazel/rpm/BUILD.bazel.tpl new file mode 100644 index 0000000000..b7a05c33d3 --- /dev/null +++ b/bazel/rpm/BUILD.bazel.tpl @@ -0,0 +1,10 @@ +filegroup( + name = "repo", + srcs = glob(["*.rpm", "repodata/*"]), + visibility = ["//visibility:public"], +) + +exports_files(glob( + ["*.rpm"], + ["repodata/*"], +)) diff --git a/bazel/rpm/package_manifest.bzl b/bazel/rpm/package_manifest.bzl new file mode 100644 index 0000000000..28fb399b62 --- /dev/null +++ b/bazel/rpm/package_manifest.bzl @@ -0,0 +1,54 @@ +"""A rule to create an RPM mirror from a lockfile.""" + +def _impl(ctx): + contents = ctx.read(ctx.path(ctx.attr.lockfile)) + lines = contents.split("\n") + package_hashes = {} + for line in lines: + components = line.split(" ") + if len(components) != 2: + continue + package_hashes[components[1]] = components[0] + for package in sorted(package_hashes.keys()): + ctx.download( + "%s%s" % (ctx.attr._cas_base_url, package_hashes[package]), + output = package, + sha256 = package_hashes[package], + ) + ctx.execute([ + ctx.path(ctx.attr._createrepo_c), + "--revision", + "0", + "--set-timestamp-to-revision", + ".", + ]) + ctx.template( + "BUILD.bazel", + ctx.attr._build_tpl, + ) + +rpm_repository = repository_rule( + implementation = _impl, + attrs = { + "lockfile": attr.label( + mandatory = True, + allow_single_file = True, + doc = "The lockfile (SHA256SUMS) to use.", + ), + "_build_tpl": attr.label( + default = Label(":BUILD.bazel.tpl"), + allow_single_file = True, + ), + "_cas_base_url": attr.string( + default = "https://cdn.confidential.cloud/constellation/cas/sha256/", + doc = "The base URL for the CAS.", + ), + "_createrepo_c": attr.label( + default = Label("@createrepo_c//:bin/createrepo_c"), + executable = True, + cfg = "exec", + allow_single_file = True, + doc = "The createrepo_c tool.", + ), + }, +) diff --git a/flake.nix b/flake.nix index ef36e9a6e8..b9bdd7273f 100644 --- a/flake.nix +++ b/flake.nix @@ -46,6 +46,12 @@ paths = [ openssl-static.out openssl-static.dev ]; }; + packages.awscli2 = pkgsUnstable.awscli2; + + packages.createrepo_c = pkgsUnstable.createrepo_c; + + packages.dnf5 = pkgsUnstable.dnf5; + devShells.default = import ./nix/shells/default.nix { pkgs = pkgsUnstable; }; formatter = nixpkgsUnstable.legacyPackages.${system}.nixpkgs-fmt; diff --git a/image/base/BUILD.bazel b/image/base/BUILD.bazel index 7a0e293605..20155ca04e 100644 --- a/image/base/BUILD.bazel +++ b/image/base/BUILD.bazel @@ -36,7 +36,6 @@ mkosi_image( "mkosi.prepare", ] + glob([ "mkosi.skeleton/**", - "reposdir/**", ]), outs = [ "image", @@ -45,6 +44,7 @@ mkosi_image( extra_trees = [ "//image:sysroot_tar", ], + local_mirror = ["@mkosi_rpms//:repo"], mkosi_conf = "mkosi.conf", package_files = [ ":kernel", diff --git a/image/base/mkosi.conf b/image/base/mkosi.conf index 362efdb258..a299547853 100644 --- a/image/base/mkosi.conf +++ b/image/base/mkosi.conf @@ -57,8 +57,6 @@ Packages=passwd curl wget -PackageManagerTrees=reposdir:/etc/yum.repos.d - RemoveFiles=/var/log RemoveFiles=/var/cache RemoveFiles=/etc/pki/ca-trust/extracted/java/cacerts diff --git a/image/initrd/BUILD.bazel b/image/initrd/BUILD.bazel index 3b7b57975c..18372f279a 100644 --- a/image/initrd/BUILD.bazel +++ b/image/initrd/BUILD.bazel @@ -16,6 +16,7 @@ mkosi_image( "//image:sysroot_tar", "//disk-mapper/cmd:disk-mapper-package.tar", ], + local_mirror = ["@mkosi_rpms//:repo"], mkosi_conf = "mkosi.conf", tags = [ "manual", diff --git a/image/initrd/mkosi.conf b/image/initrd/mkosi.conf index 38304bdf31..45c315cfe3 100644 --- a/image/initrd/mkosi.conf +++ b/image/initrd/mkosi.conf @@ -30,8 +30,6 @@ Packages=udev device-mapper cryptsetup -PackageManagerTrees=reposdir:/etc/yum.repos.d - RemoveFiles=/var/log RemoveFiles=/var/cache RemoveFiles=/etc/pki/ca-trust/extracted/java/cacerts diff --git a/image/initrd/reposdir/amzn2-core.repo b/image/initrd/reposdir/amzn2-core.repo deleted file mode 100644 index 32b6472b41..0000000000 --- a/image/initrd/reposdir/amzn2-core.repo +++ /dev/null @@ -1,39 +0,0 @@ -[amzn2-core] -name=Amazon Linux 2 core repository -#mirrorlist=$awsproto://$amazonlinux.$awsregion.$awsdomain/$releasever/$product/$target/$basearch/mirror.list -mirrorlist=https://amazonlinux-2-repos-us-east-2.s3.dualstack.us-east-2.amazonaws.com/2/core/latest/x86_64/mirror.list -priority=10 -gpgcheck=1 -#gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-amazon-linux-2 -gpgkey=https://cdn.amazonlinux.com/_assets/11CF1F95C87F5B1A.asc -enabled=1 -metadata_expire=300 -mirrorlist_expire=300 -report_instanceid=yes -includepkgs=ec2-utils - -# [amzn2-core-source] -# name=Amazon Linux 2 core repository - source packages -# mirrorlist=$awsproto://$amazonlinux.$awsregion.$awsdomain/$releasever/$product/$target/SRPMS/mirror.list -# priority=10 -# gpgcheck=1 -# #gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-amazon-linux-2 -# gpgkey=https://cdn.amazonlinux.com/_assets/11CF1F95C87F5B1A.asc -# enabled=0 -# metadata_expire=300 -# mirrorlist_expire=300 -# report_instanceid=yes -# includepkgs=ec2-utils - -# [amzn2-core-debuginfo] -# name=Amazon Linux 2 core repository - debuginfo packages -# mirrorlist=$awsproto://$amazonlinux.$awsregion.$awsdomain/$releasever/$product/$target/debuginfo/$basearch/mirror.list -# priority=10 -# gpgcheck=1 -# #gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-amazon-linux-2 -# gpgkey=https://cdn.amazonlinux.com/_assets/11CF1F95C87F5B1A.asc -# enabled=0 -# metadata_expire=300 -# mirrorlist_expire=300 -# report_instanceid=yes -# includepkgs=ec2-utils diff --git a/image/mirror/BUILD.bazel b/image/mirror/BUILD.bazel new file mode 100644 index 0000000000..56b425add5 --- /dev/null +++ b/image/mirror/BUILD.bazel @@ -0,0 +1,19 @@ +sh_binary( + name = "update_packages", + srcs = ["update_packages.sh"], + data = [ + "dnf.conf", + "packages.txt", + "@awscli//:bin/aws", + "@dnf5//:bin/dnf5", + ] + glob([ + "upstream-repos/*.repo", + ]), + env = { + "AWS": "$(rootpath @awscli//:bin/aws)", + "DNF5": "$(rootpath @dnf5//:bin/dnf5)", + "DNF_CONF": "$(rootpath dnf.conf)", + "PACKAGES": "$(rootpath packages.txt)", + "REPOSDIR": "$(rootpath upstream-repos/fedora.repo) ", + }, +) diff --git a/image/mirror/SHA256SUMS b/image/mirror/SHA256SUMS new file mode 100644 index 0000000000..479234b31b --- /dev/null +++ b/image/mirror/SHA256SUMS @@ -0,0 +1,353 @@ +bfa3b16b3e9f2d71d47309188a29ec3527f6f0378fc249ab60834da726cd37b9 aardvark-dns-1.8.0-1.fc38.x86_64.rpm +53572033e3dbec252fc928cd04eb93a49509f4a62a31582853248bead759d0a8 alternatives-1.25-1.fc38.x86_64.rpm +779024c2f731c55677d591e626ad7c22aa33ad8388af8b2459091b022f3af962 audit-libs-3.1.2-1.fc38.i686.rpm +ec30d82a4a358339856d94560fdadc6de354c1aa98a22408641b8d49d4730b9d audit-libs-3.1.2-1.fc38.x86_64.rpm +17f200a45179c59193ab7c72a4641b502ab5c524f0e5a0d59fd95aa6f15bffc8 authselect-1.4.3-1.fc38.x86_64.rpm +d2f324c915eb5e14542a55636c8e49c1cd75b17db1a8c7b11693a989629a250b authselect-libs-1.4.3-1.fc38.x86_64.rpm +718d95c40b41c2f0ecc8dc2290ebb91b529ba3be7accbad9c30c88e9ce408349 basesystem-11-15.fc38.noarch.rpm +961883b6ac18ca54b525209adce0c593f81fd8a7e71bb75bc07724e4ef72bc5f bash-5.2.15-3.fc38.x86_64.rpm +0551362b7c7efc44188147b482179f477240cb8146129d68e9f8a584af0cee68 bzip2-libs-1.0.8-13.fc38.i686.rpm +95273426afa05a81e6cf77f941e1156f6a0a3305a7768a02c04a4164280cf876 bzip2-libs-1.0.8-13.fc38.x86_64.rpm +43df24cf3974e8f8b5472a0389d519282ee55c7a720460351d0c75c4866ccf19 ca-certificates-2023.2.60_v7.0.306-1.0.fc38.noarch.rpm +152f433af3f709a3f5060505f5b3b009e3da8ea455d6d1dab5c3ed2173f9e016 catatonit-0.1.7-14.fc38.x86_64.rpm +e88a67a488d2aede3fdc1135e76205169c2ff98216320a89b93614ada73caabf conmon-2.1.7-2.fc38.x86_64.rpm +17dfa6a009e8e80119911c2fbde44b43425f2bfe9abf677f68657f0d96603d2a conntrack-tools-1.4.7-1.fc38.x86_64.rpm +d9fb9be5604e68ea59a87c14c09f89cdbaddba7ec5761ecded9211ad976db910 containerd-1.6.19-1.fc38.x86_64.rpm +db4691e52e7d8b9614445428ef8f43a91b0d7069ffd07000c606e7d562df9804 containernetworking-plugins-1.3.0-2.fc38.x86_64.rpm +cd9583500892b5f50f51167f5c4b16855e15808a79e5137e0c6674e19f305930 containers-common-1-89.fc38.noarch.rpm +66f5951f5ae9c8ec9cddf1ff87b0c8aea29298c763712bfec0ed507f57b6d3e6 containers-common-extra-1-89.fc38.noarch.rpm +6d4e35e88de6637f7f86ddd500aca05531b7076b1dc6d6382456214a92e6b081 container-selinux-2.222.0-1.fc38.noarch.rpm +1100feca307bb7159a00f336fa5303fd52ded761fc25c77fdc5f095e2add29b3 coreutils-single-9.1-12.fc38.x86_64.rpm +75ecf8c60bea53432b973d9391e3bdb1b6cdc1f1cad0a7b56aabfb8b70252832 cpio-2.13-14.fc38.x86_64.rpm +65ed3fa87ea138a51a5280848aac565e996c3217fa9cd9eafa8a4a2c05f7b37d cracklib-2.9.11-1.fc38.i686.rpm +9e13cb10c94f5de3573a2cacd153050d6dad05fe13c50c8fa69406e8f881d3d9 cracklib-2.9.11-1.fc38.x86_64.rpm +c09679c55fdeef598c6cd9118e8a2141da369dcf27ed9e9ede8e42ffc44743f2 cracklib-dicts-2.9.11-1.fc38.x86_64.rpm +3c6985fe1a0388482131411b5a0811658411056ac7b96687225bf77143645ca4 criu-3.18-1.fc38.x86_64.rpm +979323d3e3e891195ebb48edbf1e741f14937ecd3e3639f8c86eabdce0c30b94 criu-libs-3.18-1.fc38.x86_64.rpm +778e7ff9a2cd3ac67b77192e0facb2ae80542ea1e9c3c056396a3ef38afcc0fd crun-1.9.2-1.fc38.x86_64.rpm +6809fe060dc93dfed723bd8faca0f5d65e4f6c62aebd2f9f39f679413b260539 crypto-policies-20230301-1.gita12f7b2.fc38.noarch.rpm +d1880b8f3e8a5fa943ba033c96dc964fac3e07c34cbd0bb8ac2b0fdcb57abcbc crypto-policies-scripts-20230301-1.gita12f7b2.fc38.noarch.rpm +418287cd51f9ae9e5b74a18112a062a49ee51d22656dea86d08daba311c93e44 cryptsetup-2.6.1-1.fc38.x86_64.rpm +364cfba8a49773110ea200a533037afd99307b80a28333fc5f900f528a66333e cryptsetup-libs-2.6.1-1.fc38.i686.rpm +070d86aca4548e9148901881a4ef64d98c5dfd4ea158e9208c650c7f4b225c47 cryptsetup-libs-2.6.1-1.fc38.x86_64.rpm +b730724e9331de5b8a08fd99cf2052632481725a6726dd4ce95ceb3426c21e60 curl-8.0.1-5.fc38.x86_64.rpm +0292fdcde5c5a3eaff1836cfccf5af99f60e6cc5e6e8918dce5f3e7db5b454b2 curl-minimal-8.0.1-5.fc38.x86_64.rpm +b570b4857289cf32ed57d0d84cb861677a649cef7c5cc2498a249d6593eb3614 cyrus-sasl-lib-2.1.28-9.fc38.x86_64.rpm +faa01592782cad3faec0c0a07a3fa17bd0a793dc906fcb7ffedb975f1d077be4 dbus-1.14.10-1.fc38.x86_64.rpm +6652f5d40acfaeda20555bdd63e964f123e8ab4a52f8c8890e749e06b6bae3e0 dbus-broker-33-1.fc38.x86_64.rpm +a97ebbbaf97348a0bcf967eb8c3c822fc30154cf9679f5a1cb21f7c507077bbd dbus-common-1.14.10-1.fc38.noarch.rpm +695aab11fda4b6a396c3ca141fb3ccc48af757a70f36ea3a1be2292e90c40d6f device-mapper-1.02.189-2.fc38.x86_64.rpm +4be737f08ab8e072cad67bcfa0063d87b55aa4ada132f5d442c2c1bc40205599 device-mapper-libs-1.02.189-2.fc38.i686.rpm +42e8600a8d7e7109de32df6cb6a619b9805f9335bf467a085009f6b82dda6e22 device-mapper-libs-1.02.189-2.fc38.x86_64.rpm +f20b884c9452d4cf839707d4ff38191d34dd2035c87f832c697f6f328fa1b36b diffutils-3.10-1.fc38.x86_64.rpm +6c2286c8b90a5002c6c8ffb3dab7f47c521422e05c645e38f574e507f0c7ae3e dracut-059-4.fc38.x86_64.rpm +8bcdb9b5ce9b5e19c4f772f52c2c40712491d2a953496e415c380a21a792b050 duktape-2.7.0-2.fc38.x86_64.rpm +b25a042f52c8cdc1ddab56fc726d98789ff902d5264d9835f5b910db3e565213 e2fsprogs-1.46.5-4.fc38.x86_64.rpm +1ca049bff926a8ec9b6e0e69f23662934eab96c35d8eda1cf429a2a31f186045 e2fsprogs-libs-1.46.5-4.fc38.x86_64.rpm +3e0ec4d7b4b95d10f58c5269688e03da7abb4d73169c76761f4fc7e7f7797a47 ec2-utils-1.2-47.amzn2.noarch.rpm +5b5195bb6dacdd7c78522675ce83784e2befd1bdbadb14c609d9e1488754ca79 efitools-1.9.2-9.fc38.x86_64.rpm +fbeb7a7a95051df0dfeb7c4ab379b77ccfb0bf0174a39dae4a423b9108627771 efivar-libs-38-7.fc38.x86_64.rpm +2583944f1717e8575eefbaa44c33cc245fe2f7210b03072bf24c6b8e9cca42c8 elfutils-debuginfod-client-0.189-3.fc38.i686.rpm +77a4098c35a7366a75cbd0c2cef2885d52d569aa211893fb2f026fdf479f19b0 elfutils-debuginfod-client-0.189-3.fc38.x86_64.rpm +67510dd0b6ea031e4182bd10ab3b4a18fe6b5c48bf9ac9a160006f9e3a041ce7 elfutils-default-yama-scope-0.189-3.fc38.noarch.rpm +5e63ec019af490ebf3c00df2fca9217dfd7b1b169373333a18bdc38b5e2afb94 elfutils-libelf-0.189-3.fc38.i686.rpm +e3570a5f03e9117e730a051d95bb8513c02cf224b160734db534c60a4aa8e67c elfutils-libelf-0.189-3.fc38.x86_64.rpm +10f606abfa518da2bb8854b8b4b4c2b97b9e43ab7b86264b994807e83ddbfd8a elfutils-libs-0.189-3.fc38.i686.rpm +d93615d941d93d565b7c32bd4e64a108ec51d530adf63ab65436643890084e8c elfutils-libs-0.189-3.fc38.x86_64.rpm +436e65799053daabea2eb0fb8050c77f8ad60c7386499baadad261ee3619f46c ethtool-6.5-1.fc38.x86_64.rpm +6c64fea958acfb77da5ee23ec1e8d0916c7809ce39987f219927e8f94e5f2755 expat-2.5.0-2.fc38.x86_64.rpm +7f7c78f598f7ff131bbe77913b9fc6b7b49d1fce30f2d8505b2d8a85458f519a fedora-gpg-keys-38-1.noarch.rpm +40f7d64e38ae31dcb3e7273c7e089705e816dc131ae87c176e2c1aad02d5b510 fedora-release-38-36.noarch.rpm +ac9ede79357b33f0d0c9087333b0dd3e3fd1cf5ccab5c36310b0ec446390e0c7 fedora-release-common-38-36.noarch.rpm +bacf386d747343cb10a2c3847c426d3e044b7de1742b4918e3b1b78cc1b54bc4 fedora-release-identity-basic-38-36.noarch.rpm +916b75b58e9a2afe5d53cb73fdabea4d0cd8b1eba9f1213754384d0ccd531e57 fedora-repos-38-1.noarch.rpm +196b3612e50069c68d67ffaf3081d25923e2b4ca7e1bad8f124515b3fa472d4a file-5.44-3.fc38.x86_64.rpm +83bb8576a018ae028ff1434c32e1f3dfab66a781e1af9057862a2d7f3d725f58 file-libs-5.44-3.fc38.x86_64.rpm +b0fc6c55f5989aebf6e71279541206070b32b3b28b708a249bd3bdeaa6c088a4 filesystem-3.18-3.fc38.x86_64.rpm +79986f917ef1bae7ca2378b16515ba44c19160f5a5eae4f6b697eda160bc26c1 findutils-4.9.0-3.fc38.x86_64.rpm +55ca555fe815bd360b08500889b652f50fe4c56dfafbed0cc459f2362641f1a0 fuse3-3.14.1-1.fc38.x86_64.rpm +f54340fec047cc359a6a164a1ce88d0d7ffcd8f7d6334b50dc5b3d234e3a19ac fuse3-libs-3.14.1-1.fc38.x86_64.rpm +d5ae6a7d99826a17d163d9846c2705442b5792a7ccacc5169e4986cdf4b6bae2 fuse-common-3.14.1-1.fc38.x86_64.rpm +56df47937646df892dad25c6b9ae63d111328febfe86eb93096b8b0a11700b60 fuse-libs-2.9.9-16.fc38.x86_64.rpm +1e9e8b6447c2650a306e8d107dbdcdaa4f81d4175012eea0c87846faecd64c70 fuse-overlayfs-1.12-1.fc38.x86_64.rpm +e607df61803999da46a199d23d4acadb45b290f29b5b644e583c5526d8081178 gawk-5.1.1-5.fc38.x86_64.rpm +254c6789b4a98b96a53172e1b3fb866f71ea29a5b5fa7b39f072f33c27d897bc gawk-all-langpacks-5.1.1-5.fc38.x86_64.rpm +eb376264750aae673aa2a3218c291756023dea980640b30a3efe0f2199ff3889 gdbm-libs-1.23-3.fc38.x86_64.rpm +276c7af4b13262c0307cf2528c6d79c859ed348db68c0d2780869ea4b179dd02 gettext-envsubst-0.21.1-2.fc38.x86_64.rpm +4fb6fcf7eef64a48666ff9fe5a46344087979d9e8fd87be4d58a17cf9c3ef108 gettext-libs-0.21.1-2.fc38.x86_64.rpm +3837cbe450ceb59e1f9e7469aeb6ec98e08150773b83463725acfb2ebb77a98a gettext-runtime-0.21.1-2.fc38.x86_64.rpm +ea69af1f3dc38fad0781ccf05e31fa6e19e0c53fd2a8a3452fb04f11471411aa glib2-2.76.5-2.fc38.x86_64.rpm +5a52415df6adf58a6da022a738f542ae6abc0abf0a9070c7bb131d1c8ed80dac glibc-2.37-10.fc38.i686.rpm +34ab425ff3e0fc7d5fa993644de406dfd4a52540fc65079b3c130c5c9bd3492f glibc-2.37-10.fc38.x86_64.rpm +9ed8e0482c6828fc47066bb131da938933590da538e6645c2adcb9381c32a389 glibc-common-2.37-10.fc38.x86_64.rpm +8aa4d22e420082ad292992e8d092ec71bdb454835eb9593e0ebca7d113b38800 glibc-gconv-extra-2.37-10.fc38.i686.rpm +c31929d7b4899a036d63cbf5d1ac2a9636b2f001efab8196c7483ea86b4162ee glibc-gconv-extra-2.37-10.fc38.x86_64.rpm +218fe30c1e7ad9fca1aaac2422aba3b0f175fe2082f2a6188ae6d5f59feecda3 glibc-minimal-langpack-2.37-10.fc38.x86_64.rpm +69e48c73d962e3798fbc84150dfd79258b32a4f9250ee801d8cdb9540edfc21a gmp-6.2.1-4.fc38.x86_64.rpm +940192d99005896a29de3c682259b3505652778d58ec74a003639f3ba3193a1a gnupg2-2.4.0-3.fc38.x86_64.rpm +cd41c94b8c668602f7fb5eae595e5d5c34bd1b91690b5cc06f4c8c199794dfa8 gnupg2-smime-2.4.0-3.fc38.x86_64.rpm +d592abfff7d8d2c7ec9a0882bfd0903a8dc7ca2bd132f97d8c06cea4509ee1f1 gnutls-3.8.1-1.fc38.x86_64.rpm +86475210e5d0994dbc614bdc2ad1bd85d37f1e6c2aeee08f41d803195691ee89 google-compute-engine-guest-configs-udev-20230929.00-1.fc38.noarch.rpm +e0481a0fd263907193fe9f3f080a17e89de1ef1d8a490078a6225062b4eec761 gpgme-1.17.1-5.fc38.x86_64.rpm +ad16ec814c4423d007d218a3f45d2e39d3dab00fc8c0d75eef176041594e3970 gpm-libs-1.20.7-42.fc38.x86_64.rpm +60ed241ec381a23d03fac733a72132dbdc4ba04c412add78bfc67f1b9f1b4daa grep-3.8-3.fc38.x86_64.rpm +e6bb1d2b197dc2de38752176b7c4ea9da06a4d6599b0cd9a0f64a3828b3d2400 grub2-common-2.06-102.fc38.noarch.rpm +ab393f36cb91dd81926f17df666c4631a9f59b17594bc2a9e4b4ff4ae10c22ee grub2-tools-2.06-102.fc38.x86_64.rpm +3afd4b6265819a0a7e470542e756c945c0705acc6c7e410edc22b5ff1a1b7b9d grub2-tools-minimal-2.06-102.fc38.x86_64.rpm +d48579aa97fba34dc3a4c4dd3fb10e8fd66186d970e37c2f7fc28bebbc2d31eb grubby-8.40-69.fc38.x86_64.rpm +90fb8fe8a8dbe9bad49dbecf3f6b9e2ed7e699a68489a4839a05de1c4678dfdc gvisor-tap-vsock-0.7.1-1.fc38.x86_64.rpm +fcfc9d204d917c9557bc6017879e601995e60d0ff10075c7b8ab88e57f14b2af gvisor-tap-vsock-gvforwarder-0.7.1-1.fc38.x86_64.rpm +166e842798813a72e4d075dcd9b6e814d7ad3fc8d1b5860281cffe7a68784b25 gzip-1.12-3.fc38.x86_64.rpm +95c1a55fd14757d33b9c8e2a85dbcda5601bc20c166f983dacbb410849fad4da iproute-6.4.0-1.fc38.x86_64.rpm +74e4a1b67ba1772df7bedbb2510a45feb110c58902c805cd105e9693c6875ddd iproute-tc-6.4.0-1.fc38.x86_64.rpm +ee2e072b955ef8e80997b53be022bb55abc02006c44f41c38694c62c734dfee2 iptables-legacy-1.8.9-4.fc38.x86_64.rpm +491cbd3615967da69697e4fd666bbbb00cc201ba814c4be4f3f46bb35a47ba0e iptables-legacy-libs-1.8.9-4.fc38.x86_64.rpm +79f0bbdb2dd45dffe1841f82ee86113172b845bb766aa16b1249d04784e6fd87 iptables-libs-1.8.9-4.fc38.x86_64.rpm +1d4d1d67702bc624b6c63088b8fd5653050f13ef193bfeb48549e78e419fb9ec iptables-nft-1.8.9-4.fc38.x86_64.rpm +f74d1e8ba95ff5df02477ba6430d09476e8a1af6e8476453d08d99b276924b25 jansson-2.13.1-6.fc38.x86_64.rpm +7fa521efbcc6a27d679a2ad6ab7d07a8a742d9de49458d4b4352f528c8c78e1d json-c-0.17-1.fc38.i686.rpm +d111f87bed4f918bbd720d509fd97072b345f3a5998910d0a0413104b6fe98c2 json-c-0.17-1.fc38.x86_64.rpm +8a3a4007d921c6a9b372e4573520866b445c18476f907b023ea19b436436ec33 kbd-2.5.1-5.fc38.x86_64.rpm +97fcab8f93c5213714e0a9d15e48440c567d2ac39e4ed12743198268082f0583 kbd-legacy-2.5.1-5.fc38.noarch.rpm +2ea5dfd9d8c8fe7f61022b41fd6f8a2653692acf30253f45e1dc67def1648f32 kbd-misc-2.5.1-5.fc38.noarch.rpm +578e492c3e144f3d0469420823d2d3733b2c0428e1ecc3b542f3d8b465567762 keyutils-libs-1.6.1-6.fc38.i686.rpm +b66376e78fe54024531d94036d10b22b5050d52a9e65682b36dc7fdf24405997 keyutils-libs-1.6.1-6.fc38.x86_64.rpm +7419671c64795b96be18231e2f5d3f95eca8e6a71771863ac035f961041c1d7c kmod-30-4.fc38.x86_64.rpm +b4021011438edc3e8a8ef6535f84e72829f6346716d079bd2cf032f72deff3f7 kmod-libs-30-4.fc38.i686.rpm +19f873b67f23362074c03d5825e709ad521278c02e44bdeb30eba6d3bb3a8e0f kmod-libs-30-4.fc38.x86_64.rpm +ac8a7628a2a4b1f742fc90719145f50cfad9ecf67e3309fcf623fb3d82c2a768 kpartx-0.9.4-2.fc38.x86_64.rpm +96c1846341e14589a7c66f4cf21bcc0788225b44ce652c5729299a701d31a2fa krb5-libs-1.21-3.fc38.i686.rpm +9ea8a9ddec5b16c157a9b3a6661957da3688969fa94c55ae9e0ba4fcb07539b9 krb5-libs-1.21-3.fc38.x86_64.rpm +5d959bcbd96ad757865da02c799babce8a7f7fd71d740b616215ea2d0d00a985 libacl-2.3.1-6.fc38.i686.rpm +9b093be8a99bfbae03c2f3dd5435fc9508003f7ef21e4280ff72fe814c1d794e libacl-2.3.1-6.fc38.x86_64.rpm +0d0890dba8274458d068b981164111f554b47632bf9c82d1ec41c14697f2b4af libarchive-3.6.1-4.fc38.x86_64.rpm +0ffba864821a315749a7ad1773eb43e506d26d4ef633cd0940188e76add8ebd4 libargon2-20190702-2.fc38.i686.rpm +dd044973c572e64f505f3d00482249b2b4d71369babb5395a22861fd55b21d79 libargon2-20190702-2.fc38.x86_64.rpm +03c7ea2565af3b61a6ff0d5d4e0e5c65747053fc43903b913a46337903c6fdb5 libassuan-2.5.6-1.fc38.x86_64.rpm +8b43d1f6b92bf174e95ec60e2d6f0e5b2ed76177515c4e13e2b202d9a594c139 libattr-2.5.1-6.fc38.i686.rpm +d78d7bc485f099bb08c9de55dd12ea6a984b948face1f947de6ec805663a96c5 libattr-2.5.1-6.fc38.x86_64.rpm +9e73a2b591ebf2915bfbe7f9479498a73c982a4c74e96cc555930022e3ef0aba libb2-0.98.1-8.fc38.x86_64.rpm +dca5cafabf192d1f5abe37fa06425877bf74bb6e8c5ce5cad577274b18169b94 libblkid-2.38.1-4.fc38.i686.rpm +21b5a1a024c2d1877d2b7271fd3f82424eb0bd6b95395ad3a3dae5776eec8714 libblkid-2.38.1-4.fc38.x86_64.rpm +8079443881e764cece2f8f6789b39ebbe43226cde61675bdfae5a5a18a439b5f libbpf-1.1.0-2.fc38.x86_64.rpm +faea0f6f20d52f95031352fb3f7a35d5fe2065b736694decee5728fa9a153907 libbsd-0.11.7-4.fc38.x86_64.rpm +b6f8d9fee72db39ab6f5bf63124735063523e7475fe9dab1664dbfc810c2d421 libcap-2.48-6.fc38.i686.rpm +b6a2b3872182fe877fcd1dd85ef66282fdeec79fab87157327c9fc6cbd80ab15 libcap-2.48-6.fc38.x86_64.rpm +5257031cba9a8791a277994e026b0f4c7a1cf2878505f5e1ed463fa670b67f05 libcap-ng-0.8.3-8.fc38.i686.rpm +c0b6770708d273bcbf83d78f87353ca03629150e7a2f0efcadc24c41ca140500 libcap-ng-0.8.3-8.fc38.x86_64.rpm +5922028bb5642faf00d781f34bf105ef30f1988932b4b80f6bb54e9f6eed0fd6 libcbor-0.7.0-9.fc38.x86_64.rpm +ba35e2297e78af6c55bbb2a3e46a1d0819ced548088023d32f8f5dec2a9dfb88 libcom_err-1.46.5-4.fc38.i686.rpm +4ed3e7b6b0727b86ae9af17bd4839c06762a417a263a5d22eb7fcb39714bb480 libcom_err-1.46.5-4.fc38.x86_64.rpm +9cf19d214033bb24b0fbc64b96fff96ca4124be2f3d89efe58587f9f5ef5bf2d libcurl-minimal-8.0.1-5.fc38.i686.rpm +acf910b3cb71cca2e7d0d1952c59d6b8aee3f32ea75add057aa00f9fdc3df525 libcurl-minimal-8.0.1-5.fc38.x86_64.rpm +d7030af9e9e0dd9afc5b9ee02839c94757d6a4f8ebd3b21e6d81ba6141a76d46 libdb-5.3.28-55.fc38.x86_64.rpm +377c739918e9636e36218c7074316b7817e7cba9cff862198c478fb1c028112c libeconf-0.5.2-1.fc38.i686.rpm +56768f18f856353a2344f47f065b5f6dff164a5188d466df4bc64d675f455e42 libeconf-0.5.2-1.fc38.x86_64.rpm +974a64a10a3021de8a440ff4810a720f738951abd5bb944110cb9355d4ae8fa8 libedit-3.1-45.20221030cvs.fc38.x86_64.rpm +e9741c40e94cf45bdc699b950c238646c2d56b3ee7984e748b94d8e6f87ba3cd libevent-2.1.12-8.fc38.x86_64.rpm +768346084add1264110b08521694b0b25506b9c8b4bdbc53dc492b05cf70d052 libfdisk-2.38.1-4.fc38.i686.rpm +2fb7ee2d94f7ee34cff49ab28659c07b075ed67ac147f817e19d8ee8e0adbc9c libfdisk-2.38.1-4.fc38.x86_64.rpm +cd08ba5d43459f6e3bbb3cc86caee43b74b38d551d69eae3e86861c2622da1cd libffi-3.4.4-2.fc38.i686.rpm +098e8ba05482205c70c3510907da71819faf5d40b588f865ad2a77d6eaf4af09 libffi-3.4.4-2.fc38.x86_64.rpm +0a30628b39d9c5ca81c4e073dfbf64d543284f17d4ae1325e23e3eda55f92fd9 libfido2-1.12.0-3.fc38.x86_64.rpm +10bef1a628b5691f7dc0c5e17922e98b914522c6f7c7cc02b496f63c79a57f18 libgcc-13.2.1-4.fc38.i686.rpm +18142b95b05275d198bb77cefbf0be8f2c29c60293e2e99b013f6f7e28595969 libgcc-13.2.1-4.fc38.x86_64.rpm +ef4b2686134e6be036755ee093aad43eb0ce4a4c84c93a2defb755cfeb398754 libgcrypt-1.10.2-1.fc38.x86_64.rpm +fc552c5c10bccff56930ef74422c1774dd6fd4900f61376f7602ae3f05de52d9 libgomp-13.2.1-4.fc38.x86_64.rpm +40b98bdd00b44fbf808df1d653ecf0f0ed8549643d52b68da2455c5a878f0021 libgpg-error-1.47-1.fc38.x86_64.rpm +9f2059c5d699f3dd2337f0872968123a06cf56b9f349d58bd64a5ef22a9815b4 libibverbs-44.0-3.fc38.x86_64.rpm +6890b8a81db38f0d03635c69d1b641e5bba302938f3c0c49d715b90651c5b06f libidn2-2.3.4-2.fc38.i686.rpm +d3416e2b6c7565d7a607225d86b556398827316ae7ce43280b82630f0a022bc0 libidn2-2.3.4-2.fc38.x86_64.rpm +75c0097330fa3c995e80b7791cbe7baf75d86f3523f67b3becaf37360fdb4b16 libkcapi-1.4.0-5.fc38.x86_64.rpm +e552fae193775507d8264f7a856fbdc51f7e594d7d8726f181312aeb9cf8b973 libkcapi-hmaccalc-1.4.0-5.fc38.x86_64.rpm +4c5af4c1d44e1720beb79fd4202d3754e2c47c4faeb4ff5bdbe2ee3d24c9a5e0 libksba-1.6.4-1.fc38.x86_64.rpm +96213462b3d77ef8e73d72510dd70210e9af4ae28f7466b88f2740743e6ca49d libmd-1.1.0-1.fc38.x86_64.rpm +f79da3b0edf002221250bce8352015009f8259793278ae59cb74d6c9e0c8395b libmetalink-0.1.3-30.fc38.x86_64.rpm +729b80bbf6ca427c21dd28d1f7029b736dc42f3f4f0c43d322ddfa168bc2ce9b libmnl-1.0.5-2.fc38.x86_64.rpm +34b0fbfe9493d0e0762bfe42137238f3eb9cee0832e1d816f5c9f0a48ac798e9 libmount-2.38.1-4.fc38.i686.rpm +14541cda2a4516ad6a17f46be6b7ad85ef5f6508d36f209f2ba7bd45bc1504e2 libmount-2.38.1-4.fc38.x86_64.rpm +db30396e0f1eb0ac81d353524bc2e022371a5e5a3bed363e101e461d8d248fca libnet-1.3-1.fc38.x86_64.rpm +cc6bca4b52d6aaca9b1cc1f4f02721301fba447a3d2f009a7b9e9c38da3eb10f libnetfilter_conntrack-1.0.9-1.fc38.x86_64.rpm +0054a179032b916330c202a4a5713d9403fedc0809ffc215e6345705f869633c libnetfilter_cthelper-1.0.0-23.fc38.x86_64.rpm +95a9876660af858339b38a2272c7307917f4cc15a6f4ffac4fbd4d6f6743473f libnetfilter_cttimeout-1.0.0-21.fc38.x86_64.rpm +e86f7341f80e9143a9cc1df8b7fcf864eccdea487fc34500b412678501b37146 libnetfilter_queue-1.0.5-4.fc38.x86_64.rpm +3c981697fe61f23ad41b615b4c3197d023ec70f713395fc3104b837c61b74294 libnfnetlink-1.0.1-23.fc38.x86_64.rpm +06f6aeaded732bcff2d7dd5c8b1c430bebc3834d0968f20e3c2918ae15502ace libnftnl-1.2.4-2.fc38.x86_64.rpm +13161773e6afa5439ff8ef19faaa3baec35930a07df21901b01726eeda4e2644 libnghttp2-1.52.0-2.fc38.i686.rpm +9daf3a24341811a5feded45fe3e111a1d01f6a91854c231ff56fa8dffb8d40f9 libnghttp2-1.52.0-2.fc38.x86_64.rpm +a9d80e55bd59e26338a7778de28caf9eb3874f8d90574c879bae1302beaa862b libnl3-3.7.0-3.fc38.x86_64.rpm +28697cf1b5cb4d62c3bd154fc24a23d91a84a5bda2f974fb64bdd04e91b6cec5 libnsl2-2.0.0-5.fc38.x86_64.rpm +bf1e07244e3c9aacfe96b2b7f21e7bb678d1c52042885d3f0518301de38dd759 libnvme-1.4-2.fc38.x86_64.rpm +f4d87eb23450cd3888af3d984ad623d229e5bea482188d25f996e61a939486bf libpcap-1.10.4-1.fc38.x86_64.rpm +e0bccc94a740acf317caa4fa1fae6b0d57442ef4be36341472b7db93d588ec13 libpsl-0.21.2-2.fc38.x86_64.rpm +4625cab157ff1760c2f5053a3f75cec21b366f80c3b1ff53bf0db1a033a28439 libpwquality-1.4.5-3.fc38.i686.rpm +aefb7d2d96af03f4d7ac5a132138d383faf858011b1740c48fcd152000f3c617 libpwquality-1.4.5-3.fc38.x86_64.rpm +e3ef79196bc8cb77c35bd20f265f5a551ec3e8482a82ad92af3e802f9a302ad7 libseccomp-2.5.3-4.fc38.i686.rpm +dec378b594b79258dd8b44836c5371f316bcf5e4596d53dd84badcb6d00090df libseccomp-2.5.3-4.fc38.x86_64.rpm +46ed6b8fee11c16bb8b58f698dfba9874a8f393c1e72eb7f9a7b6802ac68dd1a libsecret-0.20.5-3.fc38.x86_64.rpm +9c938bd9917a9f977ab0572e1cea573f2a886a0a0a48587463faa5ed1d6b22e0 libselinux-3.5-1.fc38.i686.rpm +790c6d821ff575ad51242ec6832ed61c8a3c4e0ece245c3dee3292d19acb23b7 libselinux-3.5-1.fc38.x86_64.rpm +78a15621e7e3dfb5a65b8b8aa482cf5b07f08bcef217ad29435e299d6c8aec74 libselinux-utils-3.5-1.fc38.x86_64.rpm +1b6b7ad33391919a3315e398d737a764121e2fc9581f75318a999e02bfc0c7c4 libsemanage-3.5-2.fc38.x86_64.rpm +14292c07496f6db6ef27913d6d144e01ce7017c57ef990edff3d04a443e5507d libsepol-3.5-1.fc38.i686.rpm +15ec70665f200a5423589539c3253677eb3c15d7d620fd9bdfe2d1e429735198 libsepol-3.5-1.fc38.x86_64.rpm +ac0a6bf295151973d2e34392a134e246560b19b7351ced244abc1ed81dfe5b8e libsigsegv-2.14-4.fc38.x86_64.rpm +b35a0d6b1ecb151982b6a9342d00e8da9663e8a6da6b21b7c559634f7f29fd2d libslirp-4.7.0-3.fc38.x86_64.rpm +b71b1633a2b514d27dd9332d733f61ddd8c5a501360f9e8bafee313b793d6ad0 libsmartcols-2.38.1-4.fc38.i686.rpm +dbf5c73c71c798533cbecfa54ba28c42878c455df8cb382087d8a758c3ffe290 libsmartcols-2.38.1-4.fc38.x86_64.rpm +17da6760ce632b1726de4291f287f2928a70becc1ed414bb2e8b359dd1b7d815 libsodium-1.0.18-11.fc38.x86_64.rpm +faccff819eecffcee9dad49bda930a007e78b905b775b4ac0103121d7a8100db libss-1.46.5-4.fc38.x86_64.rpm +c70b6fa3ddd6765e2b1c9a8eddb4c0446214c55afa85fed7e79dfc98d91d10ee libstdc++-13.2.1-4.fc38.x86_64.rpm +8b49dd88579f1c37e05780202e81022c9400422b830d9bdd9087161683628b22 libtasn1-4.19.0-2.fc38.x86_64.rpm +6dddc9faad74bfb86029c0676cdd34741077ef118308de1bb8f6067dbf9e90c9 libtirpc-1.3.3-1.rc2.fc38.x86_64.rpm +aa187ea45be32306620ad8ec6318d755075b2cad99fba7c01dc4763228a98190 libtool-ltdl-2.4.7-6.fc38.x86_64.rpm +882075e0d35161170ea243a7d318e51dd45c51b9c7b749ef7d8cbbab4ab75895 libunistring1.0-1.0-1.fc38.i686.rpm +cd0e8eb5d983a985f7df99718fde6245997bdf088fb6086442a883ddb9ed03e3 libunistring1.0-1.0-1.fc38.x86_64.rpm +c4012952872a08b9662963b13e29f89388ce6e695e68fa8c37eb6e62bad62441 libunistring-1.1-3.fc38.x86_64.rpm +805da27b46f0d8cca2cf21a30e52401ae61ca472ae7c2d096de1cfb4b7a0d15c libusb1-1.0.26-2.fc38.x86_64.rpm +1a3ce5232d21b6f41c4bf290768684f2e665ca600d95eddfba4c4ada02854b86 libuser-0.64-2.fc38.x86_64.rpm +8ad1a4a44f1052c66318ca789042bedf43d7eea6282ab7872bfecd693b1393a0 libutempter-1.2.1-8.fc38.i686.rpm +c5c409a2d5f8890eeab48b27b9f4f02925a6bbabeb21ee5e45694c7c9009f037 libutempter-1.2.1-8.fc38.x86_64.rpm +b1a0a577a7d03111397b172c63ccf5f22bff39d1e97487d8f6962afc704020ed libuuid-2.38.1-4.fc38.i686.rpm +876ef0556ddeca2c8f56536c80a2f6e0f64357f40bacb92f483adb8a0ff29af2 libuuid-2.38.1-4.fc38.x86_64.rpm +79f80f95acb8fceb4beaa6c1343bc07e9cf6b691ec0a77b79c82a2e74c5845f6 libverto-0.3.2-5.fc38.i686.rpm +292791eb37bc312e845e777b2e0e3173e2d951c2bfbbda125bc619dced7f40bc libverto-0.3.2-5.fc38.x86_64.rpm +39851f80ad4890169e7979b248744acba5cadbc46c58e8e0316aaca1fcf386ac libxcrypt-4.4.36-1.fc38.i686.rpm +52d90d6fbd1ad93f81aad2c4aa38aa52b1c1c94b83560ede25c91b5542da97a4 libxcrypt-4.4.36-1.fc38.x86_64.rpm +e5befc91bfd39ab624cf40c8a1ed32caedc0f74f9ea4fb913e4d9915c1d708c6 libxcrypt-compat-4.4.36-1.fc38.x86_64.rpm +06745f933cdacd7ba3ce77a88016d2a16a1d1887a35b09fe97e574b5743cfa25 libxkbcommon-1.5.0-2.fc38.i686.rpm +507ffdb912296768699a70c30169077b531b1612e47041551bfe523a4b7b6c7d libxkbcommon-1.5.0-2.fc38.x86_64.rpm +be808a1034e6cbf1bf789c960c3e9932b256006d3c29cd18ac3e52fb9f636377 libxml2-2.10.4-1.fc38.i686.rpm +13f2ec62e10333000a13123a4cae5ebbda270c32ece03247e45bd2b244e7bba5 libxml2-2.10.4-1.fc38.x86_64.rpm +741fcbe3e5bdbcf59d90c2f89520dc4001ac4816b57c5c77f6e8a06b53d119ab libzstd-1.5.5-1.fc38.i686.rpm +7d9a98372505c9c1dff7dfea558b20a44820fda416a609467790577a848de110 libzstd-1.5.5-1.fc38.x86_64.rpm +27958b2623e06faf37e427fd4c9750a7b9df35ce38365a93caae068d24ebc95b linux-atm-libs-2.5.1-34.fc38.x86_64.rpm +f0a48ec36269d83120425b269e47ba5c86d5a9a44e0de2665c1d55c10732d25b lua-libs-5.4.4-9.fc38.x86_64.rpm +52898293cf358e998ef24ea977d6381b0a7229ac050f1c4599c5c25823256f7b lz4-libs-1.9.4-2.fc38.i686.rpm +96a8f495896c0ff7520c2cc5c9c173d134efc9ef6c6b0364bc7533aefb578d41 lz4-libs-1.9.4-2.fc38.x86_64.rpm +36a1f0412e495e618ccd8636de3dcac9ad37d5d4c287a1acf2c9af4baa7745e0 memstrack-0.2.5-1.fc38.x86_64.rpm +d5e2e8aed96b57db482e556f593efec98a3238c3a697904eb2eeaf2df7ac4d9e mkpasswd-5.5.18-1.fc38.x86_64.rpm +d9196608152ec34832cc82d3cefa90748f30c75986f2250d8cd0fabc3d0ceba2 mokutil-0.6.0-6.fc38.x86_64.rpm +22f217f91fc2d2a666304c0b360520b13adde47761baa6fed1663bb514b6faf5 mpdecimal-2.5.1-6.fc38.x86_64.rpm +e7c9b0c39f77c6fdf68ff04d8714c10532907a8a9c3e76fb377afe546247737f mpfr-4.1.1-3.fc38.x86_64.rpm +1c055813f64e964a2647da2c889fedb183d4ff27c8a4f4b0674bdbedaee9386a mtools-4.0.43-1.fc38.x86_64.rpm +8dce127ee00b28925e68e2790ff923b51df24441839f1551f30b60b3ea642a9e nano-7.2-2.fc38.x86_64.rpm +9a9dde9e280b31ac75e38051782f72f9dd6ea6077c04e0457b087658140895c5 nano-default-editor-7.2-2.fc38.noarch.rpm +602145f27fd017858256c6ee880863ef5be17c6d3c6c1354f7f16f6f6348db57 ncurses-base-6.4-3.20230114.fc38.noarch.rpm +31f5f49aded8a40f1bfa2be77ffb310785d0e3e3ccff1937e762027cb2add12d ncurses-libs-6.4-3.20230114.fc38.i686.rpm +6ce309d9fd208bfff831981ee4298ccb25fa72363cb7464f1da03b8214d4351f ncurses-libs-6.4-3.20230114.fc38.x86_64.rpm +54c76937166849fb0bfc7cf6e9963344a521a50a3608bea485d19997f2106f95 netavark-1.8.0-2.fc38.x86_64.rpm +605d6710ba42104ce0434bb37b0ca9a922a8392c14175bc782f8acb70b94c3aa nettle-3.8-3.fc38.x86_64.rpm +1875738a010be9b1d593f02a95d14afba3742e19354641b851d00b6aaac78246 nftables-1.0.5-2.fc38.x86_64.rpm +c9e8b62c6af7a60a505f881d3cc35294d8b4f51c671c05401133b02ab229c2a7 npth-1.6-12.fc38.x86_64.rpm +db4c16743be18e806ac6e64577a981b227c686ba520161231e52c57831b0188c nvme-cli-2.4-3.fc38.x86_64.rpm +18139e4f2093499d384a1b8c80d51b661c252e4039c1235a6761a280d3260543 openldap-2.6.6-1.fc38.x86_64.rpm +be8a3e233f7a19d84391f2d42f71276cc2053702635e00089b2cbe97372cff18 openssl-libs-3.0.9-2.fc38.i686.rpm +dab96630b0d442164025469b4dce3eccccd482e76ad8ae6f6392045eae147f54 openssl-libs-3.0.9-2.fc38.x86_64.rpm +9cec86553d3cd2facd166d5699fad9624d9b43a39da6fece2d54146bffae5e4d openssl-pkcs11-0.4.12-3.fc38.i686.rpm +cfa3d6feba480abdeb425bc045b525c641c7a864625b1864c2f5721903e364d8 openssl-pkcs11-0.4.12-3.fc38.x86_64.rpm +06d2101874ea4d14b4c73131c5c359d1a2e0ebe0c36a501250026e7b867a0a86 os-prober-1.81-3.fc38.x86_64.rpm +c43e1f6858ba1362c910513355313250dc5930629ce9fcc68f8b19e1118ddd36 p11-kit-0.25.0-1.fc38.i686.rpm +2b30286c9c354bf79f028c8cf7fed390c0cfbd3ff02f6468c6f02f7b533f2f9c p11-kit-0.25.0-1.fc38.x86_64.rpm +ba30116a8a1e82f6b64643cce874ae62fa096c4c240fda73cb25aefd05f78cf7 p11-kit-trust-0.25.0-1.fc38.x86_64.rpm +065b99f3541fd5f1281be2082b77e48b835a591776e92f2327bb0462c67baed0 pam-1.5.2-16.fc38.x86_64.rpm +21c59eeb1ad62c09aadca6a4168f927ff943f82e4f764d589f5acb2ab6efc993 pam-libs-1.5.2-16.fc38.i686.rpm +63e970f7b3f8c54e1dff90661c26519f32a4bf7486c40f2dd38d55e40660230e pam-libs-1.5.2-16.fc38.x86_64.rpm +8d846f866158409c775656b39e372d59cf224936d29972d3b6d14e40d3b832ca parted-3.5-11.fc38.x86_64.rpm +ac6c8b1143a8ebeb6af816189f15dd722e136aff2e6e761e33a5992be59e92b1 passt-0^20231004.gf851084-1.fc38.x86_64.rpm +43603df046850c4cf067960d8e47998de5c33955b1f865df8d66f20c1b7f676a passwd-0.80-14.fc38.x86_64.rpm +f2737b94fa026a56c7a427f8f4221ff379ea4c4c32f2fff9d95a7a7836dcc6c7 pcre2-10.42-1.fc38.1.i686.rpm +cb1caf3e9a4ddc8343c0757c7a2730bf5de2b5f0b4c9ee7d928609566f64f010 pcre2-10.42-1.fc38.1.x86_64.rpm +756f64de1e4673f0f617a9f3f12f74cceef5fc093e309d1b1d5dffef287b7d67 pcre2-syntax-10.42-1.fc38.1.noarch.rpm +48efa34ce50ae936ab9fe437aa59396d4557ff39fa22cf36c5460d3a986e502f pcsc-lite-1.9.9-3.fc38.x86_64.rpm +aa02afed121e9f5fa9590d75c0b237b6c497ae58c91e0022844b38f594feaeb7 pcsc-lite-ccid-1.5.2-1.fc38.x86_64.rpm +07dc5536982278f38c89517465384ef9f376cd27f0b200806268723993da01ad pcsc-lite-libs-1.9.9-3.fc38.x86_64.rpm +e7509cf0ec99ce89e8e88e9352f733eb9ad14a9c77e0bbfd64826a3de0e4a150 pigz-2.7-3.fc38.x86_64.rpm +e521385a42b3350c0d17e3cbddc0b69c9cf4052d1b77cc8bea2179e05b7d374a pinentry-1.2.1-2.fc38.x86_64.rpm +e424ef16ae15b8f30ced47fad734b61c4812c931e26479af3dc91a41dcfefda4 podman-4.7.0-1.fc38.x86_64.rpm +440fc5c6e6a37c47f13d1fb53a03f5cb0155592a5bcf9312e2d083d4bed0ad40 policycoreutils-3.5-1.fc38.x86_64.rpm +716096df1b34d768c3e6a5985de8e1ee58b2183ad9f987aa754e592bd2793c70 polkit-122-3.fc38.1.x86_64.rpm +56705b6a1526960d534b0d3e4247deb4eef2b5fa64ceb03544281b8e9bdc4597 polkit-libs-122-3.fc38.1.x86_64.rpm +7ffa0438229228bf5ba18945936d52c3620c95f4a3ffc5c5f0f8774fececac0a polkit-pkla-compat-0.1-23.fc38.x86_64.rpm +fb3fabd657b8f8603c6e19858beb0d506cf957bbca2f3feb827b64c94563b31f popt-1.19-2.fc38.x86_64.rpm +3d5eeb8914b509cebcdf9eb67a70b701727b0b9a77078cd5f6379d751febb969 procps-ng-3.3.17-11.fc38.x86_64.rpm +8b3f681cd05e071d4c7b21eff4684a3ca7674599ee984cccd6a69a685eb8a41c protobuf-c-1.4.1-4.fc38.x86_64.rpm +6983318d6b2dfd4eea29448e9853b74b1d009ab37be7add3ff304ff0483714cb psmisc-23.6-2.fc38.x86_64.rpm +d27890df4880c00b3358caba4d65e6bef5800c1b14c9570280c494e3faa61023 publicsuffix-list-dafsa-20230812-1.fc38.noarch.rpm +5c1a9224b32d54fc98cafa86f727009189137e3a4a8b5ecdc831878be85f4ca5 python3-3.11.6-1.fc38.x86_64.rpm +ef27ddd1283ffac1725f343c582c10a0aecbf87d859dbd9328821240ca124b8c python3-libs-3.11.6-1.fc38.x86_64.rpm +e59d71a66652002e1bc6331db17a061bd3ceacf1a449be8af9f7cefc50af4ad7 python-pip-wheel-22.3.1-3.fc38.noarch.rpm +7417816bd96d7b49e5a98c85eba313afaa8b8802458d7cd9f5ba72ecc31933e3 python-setuptools-wheel-65.5.1-2.fc38.noarch.rpm +2f82cfcdf1d58f5e6f279da5324bebafc29bbbdd71d20d11a72d3f1c81f0a0fc python-unversioned-command-3.11.6-1.fc38.noarch.rpm +ef2089b02db4ade6c0586f05d322d4913a9871442e471bce7f201ea4565ec55c qemu-user-static-7.2.6-1.fc38.x86_64.rpm +2b89bd7b6c38fe28afa8344cf23472d81503cd2fd0be95932203ec4ddb908086 qemu-user-static-aarch64-7.2.6-1.fc38.x86_64.rpm +7d59284568d4af5abaaec73b569dd5ba4beceffecda160208b1711a3eb30022c qemu-user-static-alpha-7.2.6-1.fc38.x86_64.rpm +8f16229370cbb4f7975b95c06bd932f0e9e8d2e533bdf61a7bd7a01b6e7bf8dd qemu-user-static-arm-7.2.6-1.fc38.x86_64.rpm +85b0a4c37d133de10dd7b1aa935885a4f80c68313c7bc753e0acc22d9db81ed5 qemu-user-static-cris-7.2.6-1.fc38.x86_64.rpm +70338be99d5bb1b71b6782bab9ce5abbf68551ecb5518a477df2d10f9cd479c2 qemu-user-static-hexagon-7.2.6-1.fc38.x86_64.rpm +c4411ca439912e850ef5d1e8d3560414b12d11fb29347aef8f633ae2dcef25a1 qemu-user-static-hppa-7.2.6-1.fc38.x86_64.rpm +492a7621120118432809d266dcdf09e492fb3ff94812c21d3ee921d914ce6040 qemu-user-static-loongarch64-7.2.6-1.fc38.x86_64.rpm +36d4dfc57af6bfd985b2da1b75cbd5d9549f011afddcd78af932884d07d931d5 qemu-user-static-m68k-7.2.6-1.fc38.x86_64.rpm +ae146f77d1a81587846035e585df54325a6b4113e7786275053febf8e06d6840 qemu-user-static-microblaze-7.2.6-1.fc38.x86_64.rpm +590bd24d87bd4a690cbb54588aa819a76717a1d86b338731ce0880e5840710f9 qemu-user-static-mips-7.2.6-1.fc38.x86_64.rpm +a0808cd9fbf91a18f8a683183dc7221c27c3fe968870f56f7b4d1beed7d8341c qemu-user-static-nios2-7.2.6-1.fc38.x86_64.rpm +5bcf7d28465efb51e1b88b02dc3858b8e3d8db3b1507d92382d86858dd5c4658 qemu-user-static-or1k-7.2.6-1.fc38.x86_64.rpm +0f25489b23695395421d1826ec159faff75b3c259595a1c49b458779d7933e52 qemu-user-static-ppc-7.2.6-1.fc38.x86_64.rpm +b2135034b08f4c26bc14b56beaabe56880d6e4c34ccf5152f6841762f2ce7465 qemu-user-static-riscv-7.2.6-1.fc38.x86_64.rpm +0da8ee9922ed80e84614049f43919c492b361f2187f04fc0dcc386ad0ff87c32 qemu-user-static-s390x-7.2.6-1.fc38.x86_64.rpm +4581ab68c205d9d990923b5ed17471461470db8114e8fc50a9a5307ff9e1e140 qemu-user-static-sh4-7.2.6-1.fc38.x86_64.rpm +58bd2bd48a578035af82792b1520c2d8d79bbfc2432100a49ca74865d75279df qemu-user-static-sparc-7.2.6-1.fc38.x86_64.rpm +ea7664d20d64a2e7ed15bae3d161622bb448d5fb1f979f483ae089daff8d37cc qemu-user-static-x86-7.2.6-1.fc38.x86_64.rpm +d730fc60accf0e79deab6bca815fcde11afd4143021f5fcdac4e654d0aa96d1a qemu-user-static-xtensa-7.2.6-1.fc38.x86_64.rpm +0de622e220594daf822dd0015e099b9940b5d92768703ead1b53e1daf06aecf0 qrencode-libs-4.1.1-4.fc38.i686.rpm +49ec489f168c1671a2babb690edfb020a5252f38e8d0b2d96465070abd2b0d70 qrencode-libs-4.1.1-4.fc38.x86_64.rpm +fd681c9e466bbed2411ff931b80bf46f04279b001fe8578abada13952854a774 readline-8.2-3.fc38.i686.rpm +a7099c322c45030738bdd90e3de4402c0c80c6ebd993a1749c2e582cf33ee6f2 readline-8.2-3.fc38.x86_64.rpm +fcd827a4f95db14a707bc604efac1db86973eaa97bd8bf9f78c37a925c09a297 rpm-4.18.1-3.fc38.x86_64.rpm +10196a125eccfe91f3eb187be4e07d7a7d9304f738c3feed087b6f8c4375f305 rpm-libs-4.18.1-3.fc38.x86_64.rpm +edc3846d881c4ad341c5e97f7a2b7ec8f5164c26e73839449374b3c6fc2483fd rpm-plugin-selinux-4.18.1-3.fc38.x86_64.rpm +7d457298e1f13ad8d0020f44bf02b89221ce8e5afb5b44c9a47de1a1c38bf3b2 rpm-sequoia-1.5.0-1.fc38.x86_64.rpm +39f9925ce9c0e5133040a10d043e3e6d22cfcfdfef3d3161f2244c98b555b352 runc-1.1.8-1.fc38.x86_64.rpm +61985efd54550e1fa9f31575eaa57232034d72d4eb169e88e70183d43727962c sbsigntools-0.9.5-1.fc38.x86_64.rpm +a6e01b89e814ec42d1c2c6be79240a97a9bd151c857f82a11e129547e069e27f sed-4.8-12.fc38.x86_64.rpm +842cac54a37a576ad82b23a6ef840a1d8cf4d4e8d5c6c4158e58915c38621720 selinux-policy-38.29-1.fc38.noarch.rpm +e518f02b1c8da3a583b71013ed881a368749bb044b6471f3c82de2001801f133 selinux-policy-targeted-38.29-1.fc38.noarch.rpm +c7efb8634b62cdab9e8894174a8c70d20eb431482277231bc54fa8ca8c3681e3 setup-2.14.3-2.fc38.noarch.rpm +8be96e09e2e44491b287be44b2b6be0c9f8aeab75fe061d3e8a28b9be19144ef shadow-utils-4.13-6.fc38.x86_64.rpm +46eaa576b2c5fcd167d9d88f823c309da0067fa8c4d35b7c535792fe862f05cd shadow-utils-subid-4.13-6.fc38.x86_64.rpm +0cfe6c16aca321625114f3ddc41135cbbb63cff419fe4831df47c6c99ecca5e2 slirp4netns-1.2.1-1.fc38.x86_64.rpm +868aa887826ef8c81162b97ed4440949741ecb0d78bba12291b03bdccb917877 socat-1.7.4.4-2.fc38.x86_64.rpm +be6d9e9b98733494ee5901d45b849e2dc012a6b41c3ff09d0d212002dbe15dce sqlite-libs-3.40.1-2.fc38.x86_64.rpm +dbeb6fa4f9bbc5fac8f4ca141ed21dbf81e585da5f15f47f57bc6f71fcdd09be systemd-253.10-1.fc38.i686.rpm +8af238524ad373d6a6e28b75aea907aa20220f90f48024e081df6becaf41f8b9 systemd-253.10-1.fc38.x86_64.rpm +7c1849c7ad758e71bda961150cba9b0f9eafa795e39b8185c7994299327a2620 systemd-boot-unsigned-253.10-1.fc38.x86_64.rpm +74feae10b640fa462e4a419783afef989de488cb0aba662f1a6745e779062194 systemd-libs-253.10-1.fc38.i686.rpm +c7a4b83d7a279c92b9b70ea4f288dec6738c06b1bf70c2bb8b4a0fba9a8aa9aa systemd-libs-253.10-1.fc38.x86_64.rpm +6444d6c407e1a8ee03217c2c55e7af7b63b89cfbab2bac6dcd1971773eed7def systemd-networkd-253.10-1.fc38.x86_64.rpm +2f0df54d4e7718d89ca355154d18f58ae2048ebeef32a7423ddcbccd7350a0f6 systemd-pam-253.10-1.fc38.i686.rpm +2cf2da54ad0584c958e5a63b007e08bd2529bd8da663da5cacf8f22f7d500ca4 systemd-pam-253.10-1.fc38.x86_64.rpm +5ecbfa1cf76c48179ac7a9cc21e9dc93156ca51cad2d2401f38e35e03e18d583 systemd-resolved-253.10-1.fc38.x86_64.rpm +686648cc0bd07fe98cae71a6eb5533eb70372c6c0f83305c1f9585fcea6221fa systemd-udev-253.10-1.fc38.x86_64.rpm +1d6caa060ef12ab32bf7220b49bc0d9c858c68a8f50b060f5117a2aca63a4dc5 tar-1.34-8.fc38.x86_64.rpm +5cc364cad8cb613a0ec68076f7f8e10b72ef2e266a10d2463bf548b2b0abd13e tpm2-tools-5.5-3.fc38.x86_64.rpm +8adf29af85920514902bc4332ceb896a54f9cf89e08993c9345b62c4140f91d9 tpm2-tss-4.0.1-3.fc38.x86_64.rpm +041e8b9be8a87757da8d5b8a2ced4b5aec8bcafd1c0747234cdfe10206eae27b tzdata-2023c-1.fc38.noarch.rpm +232da16c546617adde46ecaa1d5367acd05f75d04570fb367123b8dd01abdea4 util-linux-2.38.1-4.fc38.i686.rpm +f0f8e33332df97afd911093f28c487bc84cbe4dcc7bb468eac5551d235acee62 util-linux-2.38.1-4.fc38.x86_64.rpm +b57dbbbee14301e89df618b398ef39b7fc841eaba6be1b6346cf37ed7695c26a util-linux-core-2.38.1-4.fc38.x86_64.rpm +c0d033cb2b058d524d881e105e6427c6e59405b440f0024538afe3f714d197a7 vim-common-9.0.1984-1.fc38.x86_64.rpm +ad609754a57c4b17696fd02dc0abd4a6a250437f512f964df2f26966787bd8d9 vim-data-9.0.1984-1.fc38.noarch.rpm +92e97554387e2a5f532ddf1dbbc5b841f42575d91d9706f77af479308aa9d109 vim-enhanced-9.0.1984-1.fc38.x86_64.rpm +efbaa9cc11babc2c161bb5a3acb6f22035f728cdf22cd4ddd354eb555e756f69 vim-filesystem-9.0.1984-1.fc38.noarch.rpm +49750ebd8f565bdb9c94250faa419863d533e826661f19bdbeab40d14461a80c WALinuxAgent-udev-2.9.0.4-1.fc38.noarch.rpm +7f8524d182dacd6bef744c11d225dd63a82100350e95fe3ec414e70cf642c1f1 wget-1.21.3-5.fc38.x86_64.rpm +2c8b143f3cb83efa5a31c85bea1da3164ca2dde5e2d75d25115f3e21ef98b4e0 which-2.21-39.fc38.x86_64.rpm +84f87df3afabe3de8748f172220107e5a5cbb0f0ef954386ecff6b914604aada whois-nls-5.5.18-1.fc38.noarch.rpm +59a7a5a775c196961cdc51fb89440a055295c767a632bfa684760e73650aa9a0 xkeyboard-config-2.38-1.fc38.noarch.rpm +65e12f84a4d20ced0e192e3c551dab049d51f45632560ec26f0ac157d70f8353 xxd-9.0.1984-1.fc38.x86_64.rpm +e911703ffceee37ec1066344820ab0cf9ba8e43d7957395981ba68c4d411a0a4 xz-5.4.1-1.fc38.x86_64.rpm +2b3a57c5ccfd4c99ec78d8420394387782a4ac57946d63800a406a4050c3d214 xz-libs-5.4.1-1.fc38.i686.rpm +bfce8ac2a2a78a23fb931531fb3d8f530a78f4d5b17f6199bf99b93ca21858c0 xz-libs-5.4.1-1.fc38.x86_64.rpm +e6971389d89ab454bbb372859b5aee67469a0b12e57d8657c0818bca78df22f2 yajl-2.1.0-21.fc38.x86_64.rpm +c83464d6c93835b24b2c09d4c851f4a9bdacc70489a6c21447ed33ffd98c0d63 zlib-1.2.13-3.fc38.i686.rpm +c26d4d161f8eddd7cb794075e383d0f4d3a77aa88e453a2db51e53346981f04c zlib-1.2.13-3.fc38.x86_64.rpm diff --git a/image/mirror/dnf.conf b/image/mirror/dnf.conf new file mode 100644 index 0000000000..c298b85877 --- /dev/null +++ b/image/mirror/dnf.conf @@ -0,0 +1,11 @@ +# see `man dnf.conf` for defaults and possible options + +[main] +gpgcheck=True +installonly_limit=3 +clean_requirements_on_remove=True +best=False +skip_if_unavailable=True +tsflags=nodocs +basearch=x86_64 +releasever=38 diff --git a/image/mirror/packages.txt b/image/mirror/packages.txt new file mode 100644 index 0000000000..10768f8308 --- /dev/null +++ b/image/mirror/packages.txt @@ -0,0 +1,39 @@ +conntrack-tools +containerd +containernetworking-plugins +cryptsetup +curl +dbus +device-mapper +e2fsprogs +ec2-utils +efitools +ethtool +google-compute-engine-guest-configs-udev +gzip +iproute +iproute-tc +iptables-nft +kmod +mokutil +nano +nano-default-editor +nvme-cli +passwd +podman +sbsigntools +selinux-policy +selinux-policy-targeted +socat +systemd +systemd-boot +systemd-networkd +systemd-resolved +systemd-udev +tpm2-tools +udev +util-linux +vim +WALinuxAgent-udev +wget +xxd diff --git a/image/mirror/update_packages.sh b/image/mirror/update_packages.sh new file mode 100755 index 0000000000..f2245d2620 --- /dev/null +++ b/image/mirror/update_packages.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash + +set -euo pipefail +shopt -s inherit_errexit + +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) +workspace_dir=$(realpath "${BUILD_WORKSPACE_DIRECTORY:-$(pwd)}") +lockfile_sha256="${workspace_dir}/image/mirror/SHA256SUMS" + +DNF5=${DNF5:-dnf5} +DNF5=$(realpath "$(command -v "${DNF5}")") + +AWS=${AWS:-aws} +AWS=$(realpath "$(command -v "${AWS}")") + +DNF_CONF=${DNF_CONF:-${script_dir}/dnf.conf} +DNF_CONF=$(realpath "${DNF_CONF}") +PACKAGES=${PACKAGES:-${script_dir}/packages.txt} +PACKAGES=$(realpath "${PACKAGES}") +REPOSDIR=${REPOSDIR:-${script_dir}/upstream-repos} +if [[ ! -d ${REPOSDIR} ]]; then + REPOSDIR=$(dirname "${REPOSDIR}") +fi +REPOSDIR=$(realpath "${REPOSDIR}") +OUTDIR="${OUTDIR:-$(mktemp -d)}" +OUTDIR=$(realpath "${OUTDIR}") + +echo "Writing rpms to ${OUTDIR}" +echo "Lockfile location ${lockfile_sha256}" + +lockfile_backup=$(mktemp) +lockfile_tmp=$(mktemp) +new_packages=$(mktemp) +cp "${lockfile_sha256}" "${lockfile_backup}" + +download() { + mkdir -p "${OUTDIR}" + # shellcheck disable=SC2046 + "${DNF5}" \ + "--config=${DNF_CONF}" \ + "--setopt=reposdir=${REPOSDIR}" \ + "--releasever=38" \ + download \ + "--destdir=${OUTDIR}" \ + --resolve --alldeps \ + $(tr '\n' ' ' < "${PACKAGES}") +} + +mirror() { + local sha256 + local rpm + while IFS="" read -r rpm; do + rpm="${OUTDIR}/${rpm}" + sha256=$(sha256sum "${rpm}" | cut -d' ' -f1) + if "${AWS}" s3 ls "s3://cdn-constellation-backend/constellation/cas/sha256/${sha256}"; then + echo "${sha256}" "${rpm}" already mirrored + else + echo "${sha256}" "${rpm}" mirroring... + "${AWS}" s3 cp "${rpm}" "s3://cdn-constellation-backend/constellation/cas/sha256/${sha256}" + fi + done < "${new_packages}" +} + +lockfile() { + touch "${lockfile_tmp}" + cd "${OUTDIR}" && sha256sum -- *.rpm > "${lockfile_tmp}" && cd - +} + +overwrite_lockfile() { + cp "${lockfile_tmp}" "${lockfile_sha256}" +} + +diff_and_exit_if_lockfile_unchanged() { + if cmp --silent "${lockfile_backup}" "${lockfile_tmp}"; then + echo "No changes to lockfile" + exit 0 + fi + diff -Naur "${lockfile_backup}" "${lockfile_tmp}" || true + comm -13 <(sort "${lockfile_backup}") <(sort "${lockfile_tmp}") | cut -d' ' -f3 > "${new_packages}" + echo "New packages:" + cat "${new_packages}" +} + +download +lockfile +diff_and_exit_if_lockfile_unchanged +mirror +overwrite_lockfile diff --git a/image/base/reposdir/amzn2-core.repo b/image/mirror/upstream-repos/amzn2-core.repo similarity index 100% rename from image/base/reposdir/amzn2-core.repo rename to image/mirror/upstream-repos/amzn2-core.repo diff --git a/image/mirror/upstream-repos/fedora-updates.repo b/image/mirror/upstream-repos/fedora-updates.repo new file mode 100644 index 0000000000..9d9f2fd4b7 --- /dev/null +++ b/image/mirror/upstream-repos/fedora-updates.repo @@ -0,0 +1,36 @@ +[updates] +name=Fedora $releasever - $basearch - Updates +#baseurl=http://download.example/pub/fedora/linux/updates/$releasever/Everything/$basearch/ +metalink=https://mirrors.fedoraproject.org/metalink?repo=updates-released-f$releasever&arch=$basearch +enabled=1 +countme=1 +repo_gpgcheck=0 +type=rpm +gpgcheck=1 +metadata_expire=6h +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch +skip_if_unavailable=False + +[updates-debuginfo] +name=Fedora $releasever - $basearch - Updates - Debug +#baseurl=http://download.example/pub/fedora/linux/updates/$releasever/Everything/$basearch/debug/ +metalink=https://mirrors.fedoraproject.org/metalink?repo=updates-released-debug-f$releasever&arch=$basearch +enabled=0 +repo_gpgcheck=0 +type=rpm +gpgcheck=1 +metadata_expire=6h +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch +skip_if_unavailable=False + +[updates-source] +name=Fedora $releasever - Updates Source +#baseurl=http://download.example/pub/fedora/linux/updates/$releasever/Everything/SRPMS/ +metalink=https://mirrors.fedoraproject.org/metalink?repo=updates-released-source-f$releasever&arch=$basearch +enabled=0 +repo_gpgcheck=0 +type=rpm +gpgcheck=1 +metadata_expire=6h +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch +skip_if_unavailable=False diff --git a/image/mirror/upstream-repos/fedora.repo b/image/mirror/upstream-repos/fedora.repo new file mode 100644 index 0000000000..f9bfbb179a --- /dev/null +++ b/image/mirror/upstream-repos/fedora.repo @@ -0,0 +1,36 @@ +[fedora] +name=Fedora $releasever - $basearch +#baseurl=http://download.example/pub/fedora/linux/releases/$releasever/Everything/$basearch/os/ +metalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch +enabled=1 +countme=1 +metadata_expire=7d +repo_gpgcheck=0 +type=rpm +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch +skip_if_unavailable=False + +[fedora-debuginfo] +name=Fedora $releasever - $basearch - Debug +#baseurl=http://download.example/pub/fedora/linux/releases/$releasever/Everything/$basearch/debug/tree/ +metalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-debug-$releasever&arch=$basearch +enabled=0 +metadata_expire=7d +repo_gpgcheck=0 +type=rpm +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch +skip_if_unavailable=False + +[fedora-source] +name=Fedora $releasever - Source +#baseurl=http://download.example/pub/fedora/linux/releases/$releasever/Everything/source/tree/ +metalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-source-$releasever&arch=$basearch +enabled=0 +metadata_expire=7d +repo_gpgcheck=0 +type=rpm +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch +skip_if_unavailable=False From a9f245752ca92afbaeb2d017914382c5e08d0d89 Mon Sep 17 00:00:00 2001 From: Malte Poll Date: Mon, 16 Oct 2023 17:05:02 +0200 Subject: [PATCH 06/17] ci: update rpm lockfile once per week --- .github/workflows/update-rpms.yml | 57 +++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/update-rpms.yml diff --git a/.github/workflows/update-rpms.yml b/.github/workflows/update-rpms.yml new file mode 100644 index 0000000000..6cdf83d68d --- /dev/null +++ b/.github/workflows/update-rpms.yml @@ -0,0 +1,57 @@ +name: Update locked rpms + +on: + workflow_dispatch: + schedule: + - cron: "0 8 * * 0" # every sunday at 8am + +jobs: + update-rpms: + runs-on: "ubuntu-22.04" + permissions: + id-token: write + contents: read + steps: + - name: Checkout + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + + - name: Assume AWS role to upload Bazel dependencies to S3 + uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0 + with: + role-to-assume: arn:aws:iam::795746500882:role/GithubConstellationMirrorWrite + aws-region: eu-central-1 + + - name: Setup bazel + uses: ./.github/actions/setup_bazel_nix + with: + useCache: "true" + buildBuddyApiKey: ${{ secrets.BUILDBUDDY_ORG_API_KEY }} + + - name: Update rpms + run: bazel run //image/mirror:update_packages + + - name: Check if there are any changes + id: git-check + run: | + if git diff --quiet; then + echo "commitChanges=false" | tee -a "${GITHUB_OUTPUT}" + else + echo "commitChanges=true" | tee -a "${GITHUB_OUTPUT}" + fi + + - name: Create pull request + uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5.0.2 + with: + branch: "image/automated/update-rpms-${{ github.run_number }}" + base: main + title: "image: update locked rpms" + body: | + :robot: *This is an automated PR.* :robot: + + The PR is triggered as part of the scheduled rpm update workflow. + It updates the locked rpm packages that form the Constellation OS images. + commit-message: "image: update locked rpms" + committer: edgelessci + labels: dependency + # We need to push changes using a token, otherwise triggers like on:push and on:pull_request won't work. + token: ${{ !github.event.pull_request.head.repo.fork && secrets.CI_COMMIT_PUSH_PR || '' }} From d9bd870dbdb8aab1de17dfb64323778c7de9d56b Mon Sep 17 00:00:00 2001 From: edgelessci <71088502+edgelessci@users.noreply.github.com> Date: Tue, 17 Oct 2023 09:42:00 +0200 Subject: [PATCH 07/17] image: update locked rpms (#2463) Co-authored-by: malt3 --- image/mirror/SHA256SUMS | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/image/mirror/SHA256SUMS b/image/mirror/SHA256SUMS index 479234b31b..bbb2cc5817 100644 --- a/image/mirror/SHA256SUMS +++ b/image/mirror/SHA256SUMS @@ -1,3 +1,4 @@ +49750ebd8f565bdb9c94250faa419863d533e826661f19bdbeab40d14461a80c WALinuxAgent-udev-2.9.0.4-1.fc38.noarch.rpm bfa3b16b3e9f2d71d47309188a29ec3527f6f0378fc249ab60834da726cd37b9 aardvark-dns-1.8.0-1.fc38.x86_64.rpm 53572033e3dbec252fc928cd04eb93a49509f4a62a31582853248bead759d0a8 alternatives-1.25-1.fc38.x86_64.rpm 779024c2f731c55677d591e626ad7c22aa33ad8388af8b2459091b022f3af962 audit-libs-3.1.2-1.fc38.i686.rpm @@ -12,11 +13,11 @@ d2f324c915eb5e14542a55636c8e49c1cd75b17db1a8c7b11693a989629a250b authselect-lib 152f433af3f709a3f5060505f5b3b009e3da8ea455d6d1dab5c3ed2173f9e016 catatonit-0.1.7-14.fc38.x86_64.rpm e88a67a488d2aede3fdc1135e76205169c2ff98216320a89b93614ada73caabf conmon-2.1.7-2.fc38.x86_64.rpm 17dfa6a009e8e80119911c2fbde44b43425f2bfe9abf677f68657f0d96603d2a conntrack-tools-1.4.7-1.fc38.x86_64.rpm +6d4e35e88de6637f7f86ddd500aca05531b7076b1dc6d6382456214a92e6b081 container-selinux-2.222.0-1.fc38.noarch.rpm d9fb9be5604e68ea59a87c14c09f89cdbaddba7ec5761ecded9211ad976db910 containerd-1.6.19-1.fc38.x86_64.rpm db4691e52e7d8b9614445428ef8f43a91b0d7069ffd07000c606e7d562df9804 containernetworking-plugins-1.3.0-2.fc38.x86_64.rpm cd9583500892b5f50f51167f5c4b16855e15808a79e5137e0c6674e19f305930 containers-common-1-89.fc38.noarch.rpm 66f5951f5ae9c8ec9cddf1ff87b0c8aea29298c763712bfec0ed507f57b6d3e6 containers-common-extra-1-89.fc38.noarch.rpm -6d4e35e88de6637f7f86ddd500aca05531b7076b1dc6d6382456214a92e6b081 container-selinux-2.222.0-1.fc38.noarch.rpm 1100feca307bb7159a00f336fa5303fd52ded761fc25c77fdc5f095e2add29b3 coreutils-single-9.1-12.fc38.x86_64.rpm 75ecf8c60bea53432b973d9391e3bdb1b6cdc1f1cad0a7b56aabfb8b70252832 cpio-2.13-14.fc38.x86_64.rpm 65ed3fa87ea138a51a5280848aac565e996c3217fa9cd9eafa8a4a2c05f7b37d cracklib-2.9.11-1.fc38.i686.rpm @@ -65,11 +66,11 @@ bacf386d747343cb10a2c3847c426d3e044b7de1742b4918e3b1b78cc1b54bc4 fedora-release 83bb8576a018ae028ff1434c32e1f3dfab66a781e1af9057862a2d7f3d725f58 file-libs-5.44-3.fc38.x86_64.rpm b0fc6c55f5989aebf6e71279541206070b32b3b28b708a249bd3bdeaa6c088a4 filesystem-3.18-3.fc38.x86_64.rpm 79986f917ef1bae7ca2378b16515ba44c19160f5a5eae4f6b697eda160bc26c1 findutils-4.9.0-3.fc38.x86_64.rpm -55ca555fe815bd360b08500889b652f50fe4c56dfafbed0cc459f2362641f1a0 fuse3-3.14.1-1.fc38.x86_64.rpm -f54340fec047cc359a6a164a1ce88d0d7ffcd8f7d6334b50dc5b3d234e3a19ac fuse3-libs-3.14.1-1.fc38.x86_64.rpm d5ae6a7d99826a17d163d9846c2705442b5792a7ccacc5169e4986cdf4b6bae2 fuse-common-3.14.1-1.fc38.x86_64.rpm 56df47937646df892dad25c6b9ae63d111328febfe86eb93096b8b0a11700b60 fuse-libs-2.9.9-16.fc38.x86_64.rpm 1e9e8b6447c2650a306e8d107dbdcdaa4f81d4175012eea0c87846faecd64c70 fuse-overlayfs-1.12-1.fc38.x86_64.rpm +55ca555fe815bd360b08500889b652f50fe4c56dfafbed0cc459f2362641f1a0 fuse3-3.14.1-1.fc38.x86_64.rpm +f54340fec047cc359a6a164a1ce88d0d7ffcd8f7d6334b50dc5b3d234e3a19ac fuse3-libs-3.14.1-1.fc38.x86_64.rpm e607df61803999da46a199d23d4acadb45b290f29b5b644e583c5526d8081178 gawk-5.1.1-5.fc38.x86_64.rpm 254c6789b4a98b96a53172e1b3fb866f71ea29a5b5fa7b39f072f33c27d897bc gawk-all-langpacks-5.1.1-5.fc38.x86_64.rpm eb376264750aae673aa2a3218c291756023dea980640b30a3efe0f2199ff3889 gdbm-libs-1.23-3.fc38.x86_64.rpm @@ -201,9 +202,9 @@ c70b6fa3ddd6765e2b1c9a8eddb4c0446214c55afa85fed7e79dfc98d91d10ee libstdc++-13.2 8b49dd88579f1c37e05780202e81022c9400422b830d9bdd9087161683628b22 libtasn1-4.19.0-2.fc38.x86_64.rpm 6dddc9faad74bfb86029c0676cdd34741077ef118308de1bb8f6067dbf9e90c9 libtirpc-1.3.3-1.rc2.fc38.x86_64.rpm aa187ea45be32306620ad8ec6318d755075b2cad99fba7c01dc4763228a98190 libtool-ltdl-2.4.7-6.fc38.x86_64.rpm +c4012952872a08b9662963b13e29f89388ce6e695e68fa8c37eb6e62bad62441 libunistring-1.1-3.fc38.x86_64.rpm 882075e0d35161170ea243a7d318e51dd45c51b9c7b749ef7d8cbbab4ab75895 libunistring1.0-1.0-1.fc38.i686.rpm cd0e8eb5d983a985f7df99718fde6245997bdf088fb6086442a883ddb9ed03e3 libunistring1.0-1.0-1.fc38.x86_64.rpm -c4012952872a08b9662963b13e29f89388ce6e695e68fa8c37eb6e62bad62441 libunistring-1.1-3.fc38.x86_64.rpm 805da27b46f0d8cca2cf21a30e52401ae61ca472ae7c2d096de1cfb4b7a0d15c libusb1-1.0.26-2.fc38.x86_64.rpm 1a3ce5232d21b6f41c4bf290768684f2e665ca600d95eddfba4c4ada02854b86 libuser-0.64-2.fc38.x86_64.rpm 8ad1a4a44f1052c66318ca789042bedf43d7eea6282ab7872bfecd693b1393a0 libutempter-1.2.1-8.fc38.i686.rpm @@ -274,11 +275,11 @@ fb3fabd657b8f8603c6e19858beb0d506cf957bbca2f3feb827b64c94563b31f popt-1.19-2.fc 8b3f681cd05e071d4c7b21eff4684a3ca7674599ee984cccd6a69a685eb8a41c protobuf-c-1.4.1-4.fc38.x86_64.rpm 6983318d6b2dfd4eea29448e9853b74b1d009ab37be7add3ff304ff0483714cb psmisc-23.6-2.fc38.x86_64.rpm d27890df4880c00b3358caba4d65e6bef5800c1b14c9570280c494e3faa61023 publicsuffix-list-dafsa-20230812-1.fc38.noarch.rpm -5c1a9224b32d54fc98cafa86f727009189137e3a4a8b5ecdc831878be85f4ca5 python3-3.11.6-1.fc38.x86_64.rpm -ef27ddd1283ffac1725f343c582c10a0aecbf87d859dbd9328821240ca124b8c python3-libs-3.11.6-1.fc38.x86_64.rpm e59d71a66652002e1bc6331db17a061bd3ceacf1a449be8af9f7cefc50af4ad7 python-pip-wheel-22.3.1-3.fc38.noarch.rpm 7417816bd96d7b49e5a98c85eba313afaa8b8802458d7cd9f5ba72ecc31933e3 python-setuptools-wheel-65.5.1-2.fc38.noarch.rpm 2f82cfcdf1d58f5e6f279da5324bebafc29bbbdd71d20d11a72d3f1c81f0a0fc python-unversioned-command-3.11.6-1.fc38.noarch.rpm +5c1a9224b32d54fc98cafa86f727009189137e3a4a8b5ecdc831878be85f4ca5 python3-3.11.6-1.fc38.x86_64.rpm +ef27ddd1283ffac1725f343c582c10a0aecbf87d859dbd9328821240ca124b8c python3-libs-3.11.6-1.fc38.x86_64.rpm ef2089b02db4ade6c0586f05d322d4913a9871442e471bce7f201ea4565ec55c qemu-user-static-7.2.6-1.fc38.x86_64.rpm 2b89bd7b6c38fe28afa8344cf23472d81503cd2fd0be95932203ec4ddb908086 qemu-user-static-aarch64-7.2.6-1.fc38.x86_64.rpm 7d59284568d4af5abaaec73b569dd5ba4beceffecda160208b1711a3eb30022c qemu-user-static-alpha-7.2.6-1.fc38.x86_64.rpm @@ -339,7 +340,6 @@ c0d033cb2b058d524d881e105e6427c6e59405b440f0024538afe3f714d197a7 vim-common-9.0 ad609754a57c4b17696fd02dc0abd4a6a250437f512f964df2f26966787bd8d9 vim-data-9.0.1984-1.fc38.noarch.rpm 92e97554387e2a5f532ddf1dbbc5b841f42575d91d9706f77af479308aa9d109 vim-enhanced-9.0.1984-1.fc38.x86_64.rpm efbaa9cc11babc2c161bb5a3acb6f22035f728cdf22cd4ddd354eb555e756f69 vim-filesystem-9.0.1984-1.fc38.noarch.rpm -49750ebd8f565bdb9c94250faa419863d533e826661f19bdbeab40d14461a80c WALinuxAgent-udev-2.9.0.4-1.fc38.noarch.rpm 7f8524d182dacd6bef744c11d225dd63a82100350e95fe3ec414e70cf642c1f1 wget-1.21.3-5.fc38.x86_64.rpm 2c8b143f3cb83efa5a31c85bea1da3164ca2dde5e2d75d25115f3e21ef98b4e0 which-2.21-39.fc38.x86_64.rpm 84f87df3afabe3de8748f172220107e5a5cbb0f0ef954386ecff6b914604aada whois-nls-5.5.18-1.fc38.noarch.rpm From c424ec88254c67a7f0bdf2d971d2755f6d43ec6c Mon Sep 17 00:00:00 2001 From: Malte Poll <1780588+malt3@users.noreply.github.com> Date: Tue, 17 Oct 2023 09:46:37 +0200 Subject: [PATCH 08/17] ci: fix PR label for rpm updates (#2464) --- .github/workflows/update-rpms.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-rpms.yml b/.github/workflows/update-rpms.yml index 6cdf83d68d..388ebb4df0 100644 --- a/.github/workflows/update-rpms.yml +++ b/.github/workflows/update-rpms.yml @@ -52,6 +52,6 @@ jobs: It updates the locked rpm packages that form the Constellation OS images. commit-message: "image: update locked rpms" committer: edgelessci - labels: dependency + labels: dependencies # We need to push changes using a token, otherwise triggers like on:push and on:pull_request won't work. token: ${{ !github.event.pull_request.head.repo.fork && secrets.CI_COMMIT_PUSH_PR || '' }} From a8605d7294e8010834ecccd21a8f641ed8666f0b Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:35:54 +0200 Subject: [PATCH 09/17] cli: use custom byte-slice marshalling for state file (#2460) * custom byte slice marshalling Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * byte slice compatibility Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * other byte slice compat test Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add missing dep Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * export byte type alias Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * regenerate exported type Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * test marshal and unmarshal together Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> --------- Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> --- bazel/toolchains/go_module_deps.bzl | 4 +- cli/internal/state/BUILD.bazel | 2 + cli/internal/state/state.go | 35 +++++++- cli/internal/state/state_doc.go | 4 +- cli/internal/state/state_test.go | 93 ++++++++++++++++++++ cli/internal/terraform/BUILD.bazel | 1 + cli/internal/terraform/terraform_test.go | 3 +- go.mod | 2 +- go.sum | 4 +- hack/go.mod | 2 +- hack/go.sum | 4 +- operators/constellation-node-operator/go.mod | 2 +- operators/constellation-node-operator/go.sum | 4 +- 13 files changed, 144 insertions(+), 16 deletions(-) diff --git a/bazel/toolchains/go_module_deps.bzl b/bazel/toolchains/go_module_deps.bzl index 1df2a8b47c..2d7683cee3 100644 --- a/bazel/toolchains/go_module_deps.bzl +++ b/bazel/toolchains/go_module_deps.bzl @@ -7401,8 +7401,8 @@ def go_dependencies(): build_file_generation = "on", build_file_proto_mode = "disable_global", importpath = "golang.org/x/net", - sum = "h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=", - version = "v0.17.0", + sum = "h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=", + version = "v0.16.0", ) go_repository( name = "org_golang_x_oauth2", diff --git a/cli/internal/state/BUILD.bazel b/cli/internal/state/BUILD.bazel index 5d5e626070..55613a0f16 100644 --- a/cli/internal/state/BUILD.bazel +++ b/cli/internal/state/BUILD.bazel @@ -26,5 +26,7 @@ go_test( "@com_github_siderolabs_talos_pkg_machinery//config/encoder", "@com_github_spf13_afero//:afero", "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", + "@in_gopkg_yaml_v3//:yaml_v3", ], ) diff --git a/cli/internal/state/state.go b/cli/internal/state/state.go index e1283d4a21..14bbb59cc3 100644 --- a/cli/internal/state/state.go +++ b/cli/internal/state/state.go @@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only package state import ( + "encoding/hex" "fmt" "dario.cat/mergo" @@ -63,7 +64,7 @@ type ClusterValues struct { OwnerID string `yaml:"ownerID"` // description: | // Salt used to generate the ClusterID on the bootstrapping node. - MeasurementSalt []byte `yaml:"measurementSalt"` + MeasurementSalt HexBytes `yaml:"measurementSalt"` } // Infrastructure describe the state related to the cloud resources of the cluster. @@ -76,7 +77,7 @@ type Infrastructure struct { ClusterEndpoint string `yaml:"clusterEndpoint"` // description: | // Secret used to authenticate the bootstrapping node. - InitSecret []byte `yaml:"initSecret"` + InitSecret HexBytes `yaml:"initSecret"` // description: | // List of Subject Alternative Names (SANs) to add to the Kubernetes API server certificate. // If no SANs should be added, this field can be left empty. @@ -164,3 +165,33 @@ func (s *State) Merge(other *State) (*State, error) { } return s, nil } + +// HexBytes is a byte slice that is marshalled to and from a hex string. +type HexBytes []byte + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (h *HexBytes) UnmarshalYAML(unmarshal func(any) error) error { + var hexString string + if err := unmarshal(&hexString); err != nil { + // TODO(msanft): Remove with v2.14.0 + // fall back to unmarshalling as a byte slice for backwards compatibility + var oldHexBytes []byte + if err := unmarshal(&oldHexBytes); err != nil { + return fmt.Errorf("unmarshalling hex bytes: %w", err) + } + hexString = hex.EncodeToString(oldHexBytes) + } + + bytes, err := hex.DecodeString(hexString) + if err != nil { + return fmt.Errorf("decoding hex bytes: %w", err) + } + + *h = bytes + return nil +} + +// MarshalYAML implements the yaml.Marshaler interface. +func (h HexBytes) MarshalYAML() (any, error) { + return hex.EncodeToString(h), nil +} diff --git a/cli/internal/state/state_doc.go b/cli/internal/state/state_doc.go index c57088ac3b..ff9455e980 100644 --- a/cli/internal/state/state_doc.go +++ b/cli/internal/state/state_doc.go @@ -60,7 +60,7 @@ func init() { ClusterValuesDoc.Fields[1].Description = "Unique identifier of the owner of the cluster." ClusterValuesDoc.Fields[1].Comments[encoder.LineComment] = "Unique identifier of the owner of the cluster." ClusterValuesDoc.Fields[2].Name = "measurementSalt" - ClusterValuesDoc.Fields[2].Type = "[]byte" + ClusterValuesDoc.Fields[2].Type = "HexBytes" ClusterValuesDoc.Fields[2].Note = "" ClusterValuesDoc.Fields[2].Description = "Salt used to generate the ClusterID on the bootstrapping node." ClusterValuesDoc.Fields[2].Comments[encoder.LineComment] = "Salt used to generate the ClusterID on the bootstrapping node." @@ -86,7 +86,7 @@ func init() { InfrastructureDoc.Fields[1].Description = "Endpoint the cluster can be reached at." InfrastructureDoc.Fields[1].Comments[encoder.LineComment] = "Endpoint the cluster can be reached at." InfrastructureDoc.Fields[2].Name = "initSecret" - InfrastructureDoc.Fields[2].Type = "[]byte" + InfrastructureDoc.Fields[2].Type = "HexBytes" InfrastructureDoc.Fields[2].Note = "" InfrastructureDoc.Fields[2].Description = "Secret used to authenticate the bootstrapping node." InfrastructureDoc.Fields[2].Comments[encoder.LineComment] = "Secret used to authenticate the bootstrapping node." diff --git a/cli/internal/state/state_test.go b/cli/internal/state/state_test.go index 96311977e8..891b031eb2 100644 --- a/cli/internal/state/state_test.go +++ b/cli/internal/state/state_test.go @@ -14,6 +14,8 @@ import ( "github.com/siderolabs/talos/pkg/machinery/config/encoder" "github.com/spf13/afero" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) var defaultState = &State{ @@ -326,3 +328,94 @@ func TestMerge(t *testing.T) { }) } } + +func TestMarshalHexBytes(t *testing.T) { + testCases := map[string]struct { + in HexBytes + expected string + wantErr bool + }{ + "success": { + in: []byte{0xab, 0xcd, 0xef}, + expected: "abcdef\n", + }, + "empty": { + in: []byte{}, + expected: "\"\"\n", + }, + "nil": { + in: nil, + expected: "\"\"\n", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + actual, err := yaml.Marshal(tc.in) + + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + assert.Equal(tc.expected, string(actual)) + } + }) + } +} + +func TestUnmarshalHexBytes(t *testing.T) { + testCases := map[string]struct { + in string + expected HexBytes + wantErr bool + }{ + "success": { + in: "abcdef", + expected: []byte{0xab, 0xcd, 0xef}, + }, + "empty": { + in: "", + expected: nil, + }, + "byte slice compat": { + in: "[0xab, 0xcd, 0xef]", + expected: []byte{0xab, 0xcd, 0xef}, + }, + "byte slice compat 2": { + in: "[00, 12, 34]", + expected: []byte{0x00, 0x0c, 0x22}, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + var actual HexBytes + err := yaml.Unmarshal([]byte(tc.in), &actual) + + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + assert.Equal(tc.expected, actual) + } + }) + } +} + +func TestMarshalUnmarshalHexBytes(t *testing.T) { + in := HexBytes{0xab, 0xcd, 0xef} + expected := "abcdef\n" + + actual, err := yaml.Marshal(in) + require.NoError(t, err) + assert.Equal(t, expected, string(actual)) + + var actual2 HexBytes + err = yaml.Unmarshal(actual, &actual2) + require.NoError(t, err) + assert.Equal(t, in, actual2) +} diff --git a/cli/internal/terraform/BUILD.bazel b/cli/internal/terraform/BUILD.bazel index 56e9998186..2078d35b95 100644 --- a/cli/internal/terraform/BUILD.bazel +++ b/cli/internal/terraform/BUILD.bazel @@ -106,6 +106,7 @@ go_test( ], embed = [":terraform"], deps = [ + "//cli/internal/state", "//internal/cloud/cloudprovider", "//internal/constants", "//internal/file", diff --git a/cli/internal/terraform/terraform_test.go b/cli/internal/terraform/terraform_test.go index dd9a94be3c..09e91d3003 100644 --- a/cli/internal/terraform/terraform_test.go +++ b/cli/internal/terraform/terraform_test.go @@ -16,6 +16,7 @@ import ( "strings" "testing" + "github.com/edgelesssys/constellation/v2/cli/internal/state" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" @@ -477,7 +478,7 @@ func TestCreateCluster(t *testing.T) { } assert.NoError(err) assert.Equal("192.0.2.100", infraState.ClusterEndpoint) - assert.Equal([]byte("initSecret"), infraState.InitSecret) + assert.Equal(state.HexBytes("initSecret"), infraState.InitSecret) assert.Equal("12345abc", infraState.UID) if tc.provider == cloudprovider.Azure { assert.Equal(tc.expectedAttestationURL, infraState.Azure.AttestationURL) diff --git a/go.mod b/go.mod index d781a3ffc1..fdb46dd7bf 100644 --- a/go.mod +++ b/go.mod @@ -322,7 +322,7 @@ require ( go.starlark.net v0.0.0-20220223235035-243c74974e97 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect - golang.org/x/net v0.17.0 // indirect + golang.org/x/net v0.16.0 // indirect golang.org/x/oauth2 v0.9.0 // indirect golang.org/x/sync v0.4.0 // indirect golang.org/x/term v0.13.0 // indirect diff --git a/go.sum b/go.sum index 991f8e083a..432dee8367 100644 --- a/go.sum +++ b/go.sum @@ -1274,8 +1274,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/hack/go.mod b/hack/go.mod index bacd4e3da4..8de51c9d08 100644 --- a/hack/go.mod +++ b/hack/go.mod @@ -274,7 +274,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.14.0 // indirect golang.org/x/mod v0.13.0 // indirect - golang.org/x/net v0.17.0 // indirect + golang.org/x/net v0.16.0 // indirect golang.org/x/oauth2 v0.9.0 // indirect golang.org/x/sync v0.4.0 // indirect golang.org/x/sys v0.13.0 // indirect diff --git a/hack/go.sum b/hack/go.sum index 138c43573e..89b1311131 100644 --- a/hack/go.sum +++ b/hack/go.sum @@ -1215,8 +1215,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/operators/constellation-node-operator/go.mod b/operators/constellation-node-operator/go.mod index d0447ef476..c759b924bb 100644 --- a/operators/constellation-node-operator/go.mod +++ b/operators/constellation-node-operator/go.mod @@ -106,7 +106,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/crypto v0.14.0 // indirect - golang.org/x/net v0.17.0 // indirect + golang.org/x/net v0.16.0 // indirect golang.org/x/oauth2 v0.9.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect diff --git a/operators/constellation-node-operator/go.sum b/operators/constellation-node-operator/go.sum index 0b7f0e5753..4e4cd9355c 100644 --- a/operators/constellation-node-operator/go.sum +++ b/operators/constellation-node-operator/go.sum @@ -435,8 +435,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= From b2f3f72488db90bdda616c28bae0ed00c1d1975d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:47:10 +0200 Subject: [PATCH 10/17] deps: update fedora:38 Docker digest to 8285246 (#2467) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- debugd/filebeat/Dockerfile | 2 +- debugd/logstash/Dockerfile | 4 ++-- debugd/metricbeat/Dockerfile | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/debugd/filebeat/Dockerfile b/debugd/filebeat/Dockerfile index 2a0c3cb4fc..58ba79e816 100644 --- a/debugd/filebeat/Dockerfile +++ b/debugd/filebeat/Dockerfile @@ -1,4 +1,4 @@ -FROM fedora:38@sha256:6fc00f83a1b6526b1c6562e30f552d109ba8e269259c6742a26efab1b7aef59e AS release +FROM fedora:38@sha256:8285246bd5fad4e76e17a71c88dee34c49e2f227dab4ce7df704b592f8e72d41 AS release RUN dnf install -y https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-8.6.2-x86_64.rpm diff --git a/debugd/logstash/Dockerfile b/debugd/logstash/Dockerfile index f7ac6a2068..45bf358879 100644 --- a/debugd/logstash/Dockerfile +++ b/debugd/logstash/Dockerfile @@ -1,11 +1,11 @@ -FROM fedora:38@sha256:6fc00f83a1b6526b1c6562e30f552d109ba8e269259c6742a26efab1b7aef59e AS build +FROM fedora:38@sha256:8285246bd5fad4e76e17a71c88dee34c49e2f227dab4ce7df704b592f8e72d41 AS build ARG LOGSTASH_VER=8.6.1 RUN curl -fsSLO https://artifacts.opensearch.org/logstash/logstash-oss-with-opensearch-output-plugin-$LOGSTASH_VER-linux-x64.tar.gz RUN tar -zxvf logstash-oss-with-opensearch-output-plugin-$LOGSTASH_VER-linux-x64.tar.gz -FROM fedora:38@sha256:6fc00f83a1b6526b1c6562e30f552d109ba8e269259c6742a26efab1b7aef59e AS release +FROM fedora:38@sha256:8285246bd5fad4e76e17a71c88dee34c49e2f227dab4ce7df704b592f8e72d41 AS release COPY --from=build logstash-* /usr/share/logstash diff --git a/debugd/metricbeat/Dockerfile b/debugd/metricbeat/Dockerfile index 121bf14c64..e4f84123a8 100644 --- a/debugd/metricbeat/Dockerfile +++ b/debugd/metricbeat/Dockerfile @@ -1,4 +1,4 @@ -FROM fedora:38@sha256:61f921e0c7b51e162e6f94b14ef4e6b0d38eac5987286fe4f52a2c1158cc2399 AS release +FROM fedora:38@sha256:8285246bd5fad4e76e17a71c88dee34c49e2f227dab4ce7df704b592f8e72d41 AS release RUN dnf install -y https://artifacts.elastic.co/downloads/beats/metricbeat/metricbeat-8.9.2-x86_64.rpm From 63ebdd9292b6abe72158db51e92050d27e0093c8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:47:48 +0200 Subject: [PATCH 11/17] deps: update docker.io/k8scloudprovider/openstack-cloud-controller-manager Docker tag to v1.26.4 (#2466) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- internal/versions/versions.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/versions/versions.go b/internal/versions/versions.go index 576331d543..7e03aa832d 100644 --- a/internal/versions/versions.go +++ b/internal/versions/versions.go @@ -240,7 +240,7 @@ var VersionConfigs = map[ValidK8sVersion]KubernetesVersion{ // TODO(3u13r): use newer "cloud-provider-gcp" from https://github.com/kubernetes/cloud-provider-gcp when newer releases are available. CloudControllerManagerImageGCP: "ghcr.io/edgelesssys/cloud-provider-gcp:v26.4.0@sha256:dbe983cceabb3df98112b083d844229c85a1bbdfef2060c79f4cd49afe2a07f3", // renovate:container // CloudControllerManagerImageOpenStack is the CCM image used on OpenStack. - CloudControllerManagerImageOpenStack: "docker.io/k8scloudprovider/openstack-cloud-controller-manager:v1.26.3@sha256:65f0945ea9fc17e64812befbf3fc52b06c13df1c3407cb8022e8110a2fe08a4a", // renovate:container + CloudControllerManagerImageOpenStack: "docker.io/k8scloudprovider/openstack-cloud-controller-manager:v1.26.4@sha256:05e846fb13481b6dbe4a1e50491feb219e8f5101af6cf662a086115735624db0", // renovate:container // External service image. Depends on k8s version. // Check for new versions at https://github.com/kubernetes/autoscaler/releases. ClusterAutoscalerImage: "registry.k8s.io/autoscaling/cluster-autoscaler:v1.26.4@sha256:f771284ff54ecfedf40c7af70c5450600786c98989aeb69cdcf7e7bb7ac5a20d", // renovate:container @@ -291,7 +291,7 @@ var VersionConfigs = map[ValidK8sVersion]KubernetesVersion{ // TODO(3u13r): use newer "cloud-provider-gcp" from https://github.com/kubernetes/cloud-provider-gcp when newer releases are available. CloudControllerManagerImageGCP: "ghcr.io/edgelesssys/cloud-provider-gcp:v27.1.6@sha256:b097b4e5382ea1987db5996a9eaffb94fa224639b3464876f0b1b17f64509ac4", // renovate:container // CloudControllerManagerImageOpenStack is the CCM image used on OpenStack. - CloudControllerManagerImageOpenStack: "docker.io/k8scloudprovider/openstack-cloud-controller-manager:v1.26.3@sha256:65f0945ea9fc17e64812befbf3fc52b06c13df1c3407cb8022e8110a2fe08a4a", // renovate:container + CloudControllerManagerImageOpenStack: "docker.io/k8scloudprovider/openstack-cloud-controller-manager:v1.26.4@sha256:05e846fb13481b6dbe4a1e50491feb219e8f5101af6cf662a086115735624db0", // renovate:container // External service image. Depends on k8s version. // Check for new versions at https://github.com/kubernetes/autoscaler/releases. ClusterAutoscalerImage: "registry.k8s.io/autoscaling/cluster-autoscaler:v1.27.3@sha256:0e1ab1bfeb1beaa82f59356ef36364503df22aeb8f8d0d7383bac449b4e808fb", // renovate:container @@ -342,7 +342,7 @@ var VersionConfigs = map[ValidK8sVersion]KubernetesVersion{ // TODO(3u13r): use newer "cloud-provider-gcp" from https://github.com/kubernetes/cloud-provider-gcp when newer releases are available. CloudControllerManagerImageGCP: "ghcr.io/edgelesssys/cloud-provider-gcp:v27.1.6@sha256:b097b4e5382ea1987db5996a9eaffb94fa224639b3464876f0b1b17f64509ac4", // renovate:container // CloudControllerManagerImageOpenStack is the CCM image used on OpenStack. - CloudControllerManagerImageOpenStack: "docker.io/k8scloudprovider/openstack-cloud-controller-manager:v1.26.3@sha256:65f0945ea9fc17e64812befbf3fc52b06c13df1c3407cb8022e8110a2fe08a4a", // renovate:container + CloudControllerManagerImageOpenStack: "docker.io/k8scloudprovider/openstack-cloud-controller-manager:v1.26.4@sha256:05e846fb13481b6dbe4a1e50491feb219e8f5101af6cf662a086115735624db0", // renovate:container // External service image. Depends on k8s version. // Check for new versions at https://github.com/kubernetes/autoscaler/releases. ClusterAutoscalerImage: "registry.k8s.io/autoscaling/cluster-autoscaler:v1.27.3@sha256:0e1ab1bfeb1beaa82f59356ef36364503df22aeb8f8d0d7383bac449b4e808fb", // renovate:container From 4fbf94ceb8d5128ba64611f51a7f888a180dfdfc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:48:38 +0200 Subject: [PATCH 12/17] deps: update golang:1.21.3 Docker digest to 24a0937 (#2468) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/actions/versionsapi/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/versionsapi/Dockerfile b/.github/actions/versionsapi/Dockerfile index f6e670534b..8c3c37a9bc 100644 --- a/.github/actions/versionsapi/Dockerfile +++ b/.github/actions/versionsapi/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21.3@sha256:02d7116222536a5cf0fcf631f90b507758b669648e0f20186d2dc94a9b419a9b as builder +FROM golang:1.21.3@sha256:24a09375a6216764a3eda6a25490a88ac178b5fcb9511d59d0da5ebf9e496474 as builder # Download project root dependencies WORKDIR /workspace From abbe3853cb1b07ea2744da73d5e529399584a87e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:48:52 +0200 Subject: [PATCH 13/17] deps: update cachix/install-nix-action action to v23 (#2469) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/actions/setup_bazel_nix/action.yml | 2 +- .github/workflows/aws-snp-launchmeasurement.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/setup_bazel_nix/action.yml b/.github/actions/setup_bazel_nix/action.yml index 38be76c2c4..c25e508c8b 100644 --- a/.github/actions/setup_bazel_nix/action.yml +++ b/.github/actions/setup_bazel_nix/action.yml @@ -63,7 +63,7 @@ runs: - name: Install nix if: steps.check_inputs.outputs.nixPreinstalled == 'false' - uses: cachix/install-nix-action@6ed004b9ccb68dbc28e7c85bee15fa93dbd214ac # v22 + uses: cachix/install-nix-action@6a9a9e84a173d90b3ffb42c5ddaf9ea033fad011 # v23 - name: Set $USER if not set shell: bash diff --git a/.github/workflows/aws-snp-launchmeasurement.yml b/.github/workflows/aws-snp-launchmeasurement.yml index 2c7b195b2a..a5b303c03a 100644 --- a/.github/workflows/aws-snp-launchmeasurement.yml +++ b/.github/workflows/aws-snp-launchmeasurement.yml @@ -23,7 +23,7 @@ jobs: sudo python3 -m pip install --user --require-hashes -r constellation/.github/workflows/aws-snp-launchmeasurements-requirements.txt - name: Install Nix - uses: cachix/install-nix-action@6ed004b9ccb68dbc28e7c85bee15fa93dbd214ac # v22 + uses: cachix/install-nix-action@6a9a9e84a173d90b3ffb42c5ddaf9ea033fad011 # v23 - name: Download Firmware release id: download-firmware From bad9edb99b2555f9b1c0cae15236a1f5bafb8d91 Mon Sep 17 00:00:00 2001 From: Malte Poll <1780588+malt3@users.noreply.github.com> Date: Tue, 17 Oct 2023 12:44:19 +0200 Subject: [PATCH 14/17] image: move mkosi settings into their actual sections (#2471) mkosi now warns about what settings are defined in what sections. Soon, the config parsing might fail when settings are in the wrong sections. --- image/base/mkosi.conf | 4 ++-- image/initrd/mkosi.conf | 4 ++-- image/system/mkosi.conf | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/image/base/mkosi.conf b/image/base/mkosi.conf index a299547853..3f041eac2b 100644 --- a/image/base/mkosi.conf +++ b/image/base/mkosi.conf @@ -4,9 +4,10 @@ Release=38 [Output] Format=tar -SourceDateEpoch=0 +Seed=b04a9a33-4559-4af4-8b38-9249cf933229 [Content] +SourceDateEpoch=0 Bootable=no Packages=systemd systemd-boot @@ -67,4 +68,3 @@ RemoveFiles=/etc/pki/ca-trust/extracted/java/cacerts RemoveFiles=/etc/issue RemoveFiles=/etc/issue.net CleanPackageMetadata=true -Seed=b04a9a33-4559-4af4-8b38-9249cf933229 diff --git a/image/initrd/mkosi.conf b/image/initrd/mkosi.conf index 45c315cfe3..173691555a 100644 --- a/image/initrd/mkosi.conf +++ b/image/initrd/mkosi.conf @@ -5,9 +5,10 @@ Release=38 [Output] Format=cpio Output=image -SourceDateEpoch=0 +Seed=b04a9a33-4559-4af4-8b38-9249cf933229 [Content] +SourceDateEpoch=0 MakeInitrd=yes Bootable=no Packages=systemd @@ -38,4 +39,3 @@ RemoveFiles=/etc/pki/ca-trust/extracted/java/cacerts # https://github.com/authselect/authselect/pull/348 # RemoveFiles=/etc/authselect/* CleanPackageMetadata=true -Seed=b04a9a33-4559-4af4-8b38-9249cf933229 diff --git a/image/system/mkosi.conf b/image/system/mkosi.conf index 317899e824..c45f0cc233 100644 --- a/image/system/mkosi.conf +++ b/image/system/mkosi.conf @@ -8,9 +8,9 @@ ManifestFormat=json Output=constellation ImageId=constellation Seed=0e9a6fe0-68f6-408c-bbeb-136054d20445 -SourceDateEpoch=0 [Content] +SourceDateEpoch=0 Bootable=yes Bootloader=uki KernelCommandLine=preempt=full rd.shell=0 rd.emergency=reboot loglevel=8 console=ttyS0 From ac8a464d7e4364016459286f1b23c20ccab80ea2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 13:15:08 +0200 Subject: [PATCH 15/17] deps: update K8s constrained Azure versions (#2465) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- internal/versions/versions.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/versions/versions.go b/internal/versions/versions.go index 7e03aa832d..b938330ea4 100644 --- a/internal/versions/versions.go +++ b/internal/versions/versions.go @@ -232,10 +232,10 @@ var VersionConfigs = map[ValidK8sVersion]KubernetesVersion{ CloudControllerManagerImageAWS: "registry.k8s.io/provider-aws/cloud-controller-manager:v1.26.6@sha256:33445ab57f48938fe989ffe311dacee0044b82f2bd23cb7f7b563275926f0ce9", // renovate:container // CloudControllerManagerImageAzure is the CCM image used on Azure. // Check for newer versions at https://github.com/kubernetes-sigs/cloud-provider-azure/blob/master/README.md. - CloudControllerManagerImageAzure: "mcr.microsoft.com/oss/kubernetes/azure-cloud-controller-manager:v1.26.13@sha256:d44cd3490d3ab7a4bf11faa4c8b918864be041f8b183dcedc75caf6fb9d1fdf1", // renovate:container + CloudControllerManagerImageAzure: "mcr.microsoft.com/oss/kubernetes/azure-cloud-controller-manager:v1.26.16@sha256:92abc79a8a339cc7ab47abae35075b4f9771e5a25a9ada7c5040b1b3c7c7046e", // renovate:container // CloudNodeManagerImageAzure is the cloud-node-manager image used on Azure. // Check for newer versions at https://github.com/kubernetes-sigs/cloud-provider-azure/blob/master/README.md. - CloudNodeManagerImageAzure: "mcr.microsoft.com/oss/kubernetes/azure-cloud-node-manager:v1.26.13@sha256:ba8c73fc49495ed69d4242eee3068609ff2752d4c3f51de740385b05a4c303f1", // renovate:container + CloudNodeManagerImageAzure: "mcr.microsoft.com/oss/kubernetes/azure-cloud-node-manager:v1.26.16@sha256:82ae9ba5483c4dd900f65c008cbeb390f62d93983374ec601f269d3597d4da8b", // renovate:container // CloudControllerManagerImageGCP is the CCM image used on GCP. // TODO(3u13r): use newer "cloud-provider-gcp" from https://github.com/kubernetes/cloud-provider-gcp when newer releases are available. CloudControllerManagerImageGCP: "ghcr.io/edgelesssys/cloud-provider-gcp:v26.4.0@sha256:dbe983cceabb3df98112b083d844229c85a1bbdfef2060c79f4cd49afe2a07f3", // renovate:container @@ -283,10 +283,10 @@ var VersionConfigs = map[ValidK8sVersion]KubernetesVersion{ CloudControllerManagerImageAWS: "registry.k8s.io/provider-aws/cloud-controller-manager:v1.27.2@sha256:42be09a2b13b4e69b42905639d6b005ebe1ca490aabefad427256abf2cc892c7", // renovate:container // CloudControllerManagerImageAzure is the CCM image used on Azure. // Check for newer versions at https://github.com/kubernetes-sigs/cloud-provider-azure/blob/master/README.md. - CloudControllerManagerImageAzure: "mcr.microsoft.com/oss/kubernetes/azure-cloud-controller-manager:v1.27.7@sha256:e27c4ddc8b9efdac8509a2f681eaa98152309f6b2f08d14230b11c60a9b8b87f", // renovate:container + CloudControllerManagerImageAzure: "mcr.microsoft.com/oss/kubernetes/azure-cloud-controller-manager:v1.27.10@sha256:3366e0e51c56643968c7e607cb27c2545948cfab5bff3bed85e314d93a689d8e", // renovate:container // CloudNodeManagerImageAzure is the cloud-node-manager image used on Azure. // Check for newer versions at https://github.com/kubernetes-sigs/cloud-provider-azure/blob/master/README.md. - CloudNodeManagerImageAzure: "mcr.microsoft.com/oss/kubernetes/azure-cloud-node-manager:v1.27.7@sha256:998453989b03ac6c24e53aabbf271fa181e054b3a061fe6caa565186ae79bd0c", // renovate:container + CloudNodeManagerImageAzure: "mcr.microsoft.com/oss/kubernetes/azure-cloud-node-manager:v1.27.10@sha256:754d4eb709d0c5955af8bc46f5beccf0fa8c09551855a3810145b09af27d6656", // renovate:container // CloudControllerManagerImageGCP is the CCM image used on GCP. // TODO(3u13r): use newer "cloud-provider-gcp" from https://github.com/kubernetes/cloud-provider-gcp when newer releases are available. CloudControllerManagerImageGCP: "ghcr.io/edgelesssys/cloud-provider-gcp:v27.1.6@sha256:b097b4e5382ea1987db5996a9eaffb94fa224639b3464876f0b1b17f64509ac4", // renovate:container @@ -334,10 +334,10 @@ var VersionConfigs = map[ValidK8sVersion]KubernetesVersion{ CloudControllerManagerImageAWS: "registry.k8s.io/provider-aws/cloud-controller-manager:v1.28.0@sha256:47eb1c1e6a3bd6d0fb44ac4992885b6218f1448ea339de778d8b703df463c06f", // renovate:container // CloudControllerManagerImageAzure is the CCM image used on Azure. // Check for newer versions at https://github.com/kubernetes-sigs/cloud-provider-azure/blob/master/README.md. - CloudControllerManagerImageAzure: "mcr.microsoft.com/oss/kubernetes/azure-cloud-controller-manager:v1.27.7@sha256:e27c4ddc8b9efdac8509a2f681eaa98152309f6b2f08d14230b11c60a9b8b87f", // renovate:container + CloudControllerManagerImageAzure: "mcr.microsoft.com/oss/kubernetes/azure-cloud-controller-manager:v1.27.10@sha256:3366e0e51c56643968c7e607cb27c2545948cfab5bff3bed85e314d93a689d8e", // renovate:container // CloudNodeManagerImageAzure is the cloud-node-manager image used on Azure. // Check for newer versions at https://github.com/kubernetes-sigs/cloud-provider-azure/blob/master/README.md. - CloudNodeManagerImageAzure: "mcr.microsoft.com/oss/kubernetes/azure-cloud-node-manager:v1.27.7@sha256:998453989b03ac6c24e53aabbf271fa181e054b3a061fe6caa565186ae79bd0c", // renovate:container + CloudNodeManagerImageAzure: "mcr.microsoft.com/oss/kubernetes/azure-cloud-node-manager:v1.27.10@sha256:754d4eb709d0c5955af8bc46f5beccf0fa8c09551855a3810145b09af27d6656", // renovate:container // CloudControllerManagerImageGCP is the CCM image used on GCP. // TODO(3u13r): use newer "cloud-provider-gcp" from https://github.com/kubernetes/cloud-provider-gcp when newer releases are available. CloudControllerManagerImageGCP: "ghcr.io/edgelesssys/cloud-provider-gcp:v27.1.6@sha256:b097b4e5382ea1987db5996a9eaffb94fa224639b3464876f0b1b17f64509ac4", // renovate:container From e93de82c0bc53786b7bf20e01752c8b6af60b5c9 Mon Sep 17 00:00:00 2001 From: Malte Poll <1780588+malt3@users.noreply.github.com> Date: Tue, 17 Oct 2023 13:26:07 +0200 Subject: [PATCH 16/17] image: use systemd-dissect from the host when calculating measurements (#2473) * image: use systemd-dissect from the host when calculating measurements * ci: setup bazel and nix toolchains before merging os image measurements --- .github/workflows/build-os-image.yml | 4 ++++ image/measured-boot/cmd/BUILD.bazel | 11 ++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-os-image.yml b/.github/workflows/build-os-image.yml index 5f9dcb1616..8bbdeafe63 100644 --- a/.github/workflows/build-os-image.yml +++ b/.github/workflows/build-os-image.yml @@ -545,6 +545,10 @@ jobs: with: ref: ${{ inputs.ref || github.head_ref }} + - uses: ./.github/actions/setup_bazel_nix + with: + useCache: "false" + - name: Download measurements uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 with: diff --git a/image/measured-boot/cmd/BUILD.bazel b/image/measured-boot/cmd/BUILD.bazel index f2245c0258..0afb9de86f 100644 --- a/image/measured-boot/cmd/BUILD.bazel +++ b/image/measured-boot/cmd/BUILD.bazel @@ -21,8 +21,13 @@ go_binary( ], embed = [":cmd_lib"], # keep - env = { - "DISSECT_TOOLCHAIN": "$(rootpath @systemd//:bin/systemd-dissect)", - }, + # TODO(malt3): The commented out env variable + # means we are using `systemd-dissect` from the host. + # `systemd-dissect` from nixpkgs breaks GitHub actions runners + # for unknown reasons. + # Fix this. + # env = { + # "DISSECT_TOOLCHAIN": "$(rootpath @systemd//:bin/systemd-dissect)", + # }, visibility = ["//visibility:public"], ) From 1a141c39728c88d9a5c8b36ab453cf055e3968dc Mon Sep 17 00:00:00 2001 From: Malte Poll <1780588+malt3@users.noreply.github.com> Date: Tue, 17 Oct 2023 14:04:41 +0200 Subject: [PATCH 17/17] image: add rpm database as build output (#2442) For reproducibility reasons, the final OS image does not ship the rpm database in sqlite format. For supply chain security and license compliance reasons, we want to keep the rpm database of os images as a detached build artifact. We now ship a reproducible, human readable manifest of installed rpms in the image under "/usr/share/constellation/packagemanifest" and upload the full rpm database as a build artifact (rpmdb.tar). --- .github/workflows/build-os-image.yml | 36 ++++++++++++++++++++++++++++ image/base/BUILD.bazel | 26 ++++++++++++++++++++ image/base/mkosi.conf | 4 ---- image/base/mkosi.postinst | 8 +++++++ image/initrd/mkosi.conf | 2 -- image/system/BUILD.bazel | 2 +- image/system/mkosi.conf | 2 -- 7 files changed, 71 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-os-image.yml b/.github/workflows/build-os-image.yml index 8bbdeafe63..38b0105c68 100644 --- a/.github/workflows/build-os-image.yml +++ b/.github/workflows/build-os-image.yml @@ -172,6 +172,7 @@ jobs: bazel build "${TARGET}" { echo "image-dir=$(bazel cquery --output=files "$TARGET")" + echo "rpmdb=$(bazel cquery --output=files //image/base:rpmdb)" } | tee -a "$GITHUB_OUTPUT" echo "::endgroup::" @@ -190,6 +191,12 @@ jobs: ${{ steps.build.outputs.image-dir }}/constellation.initrd ${{ steps.build.outputs.image-dir }}/constellation.vmlinuz + - name: Upload sbom info as artifact + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + with: + name: sbom-${{ matrix.csp }}-${{ matrix.attestation_variant }} + path: ${{ steps.build.outputs.rpmdb }} + upload-os-image: name: "Upload OS image to CSP" needs: [build-settings, make-os-image] @@ -616,6 +623,35 @@ jobs: --signature measurements.json.sig echo "::endgroup::" + upload-sbom: + name: "Upload SBOM" + needs: [build-settings, make-os-image] + permissions: + id-token: write + contents: read + runs-on: ubuntu-22.04 + steps: + - name: Login to AWS + uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0 + with: + role-to-assume: arn:aws:iam::795746500882:role/GitHubConstellationImagePipeline + aws-region: eu-central-1 + + - name: Download sbom + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + # downloading / using only the QEMU manifest is fine + # since the images only differ in the ESP partition + name: sbom-qemu-qemu-vtpm + + - name: Upload SBOMs to S3 + shell: bash + run: | + aws s3 cp \ + rpmdb.tar \ + "s3://cdn-constellation-backend/${{needs.build-settings.outputs.imageApiBasePath}}/${file}" \ + --no-progress + upload-artifacts: name: "Upload image lookup table and CLI compatibility info" runs-on: ubuntu-22.04 diff --git a/image/base/BUILD.bazel b/image/base/BUILD.bazel index 20155ca04e..b226f472df 100644 --- a/image/base/BUILD.bazel +++ b/image/base/BUILD.bazel @@ -1,5 +1,6 @@ load("@aspect_bazel_lib//lib:copy_file.bzl", "copy_file") load("@aspect_bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory") +load("@rules_pkg//:pkg.bzl", "pkg_tar") load("//bazel/mkosi:mkosi_image.bzl", "mkosi_image") copy_to_directory( @@ -40,6 +41,11 @@ mkosi_image( outs = [ "image", "image.tar", + "image-.rpm.lock", + "image-packagemanifest", + "image-rpmdb.sqlite", + "image-rpmdb.sqlite-shm", + "image-rpmdb.sqlite-wal", ], extra_trees = [ "//image:sysroot_tar", @@ -58,3 +64,23 @@ mkosi_image( ], visibility = ["//visibility:public"], ) + +pkg_tar( + name = "rpmdb", + srcs = [ + "image-.rpm.lock", + "image-packagemanifest", + "image-rpmdb.sqlite", + "image-rpmdb.sqlite-shm", + "image-rpmdb.sqlite-wal", + ], + remap_paths = { + "/image-.rpm.lock": "/var/lib/rpm/.rpm.lock", + "/image-packagemanifest": "/usr/share/constellation/packagemanifest", + "/image-rpmdb.sqlite": "/var/lib/rpm/rpmdb.sqlite", + "/image-rpmdb.sqlite-shm": "/var/lib/rpm/rpmdb.sqlite-shm", + "/image-rpmdb.sqlite-wal": "/var/lib/rpm/image-rpmdb.sqlite-wal", + }, + tags = ["manual"], + visibility = ["//visibility:public"], +) diff --git a/image/base/mkosi.conf b/image/base/mkosi.conf index 3f041eac2b..8d00493269 100644 --- a/image/base/mkosi.conf +++ b/image/base/mkosi.conf @@ -61,10 +61,6 @@ Packages=passwd RemoveFiles=/var/log RemoveFiles=/var/cache RemoveFiles=/etc/pki/ca-trust/extracted/java/cacerts - /usr/lib/sysimage/libdnf5/transaction_history.sqlite* /var/cache/ldconfig/aux-cache -# https://github.com/authselect/authselect/pull/348 -# RemoveFiles=/etc/authselect/* RemoveFiles=/etc/issue RemoveFiles=/etc/issue.net -CleanPackageMetadata=true diff --git a/image/base/mkosi.postinst b/image/base/mkosi.postinst index 99a2ec0bcb..9e02d6c321 100755 --- a/image/base/mkosi.postinst +++ b/image/base/mkosi.postinst @@ -7,3 +7,11 @@ mkdir -p "${BUILDROOT}"/etc/{cni,kubernetes} # move issue files away from /etc # to allow /run/issue and /run/issue.d to take precedence mv "${BUILDROOT}/etc/issue.d" "${BUILDROOT}/usr/lib/issue.d" || true + +# generate reproducible package manifest +mkdir -p "${BUILDROOT}/usr/share/constellation" +rpm -qa --qf '%{name};%{version};%{license}\n' --dbpath "${BUILDROOT}/var/lib/rpm/" | LC_ALL=C sort | tee "${BUILDROOT}/usr/share/constellation/packagemanifest" +cp "${BUILDROOT}/usr/share/constellation/packagemanifest" "${OUTPUTDIR}/" + +# copy rpmdb to outputs +cp "${BUILDROOT}"/var/lib/rpm/{rpmdb.sqlite-wal,rpmdb.sqlite-shm,rpmdb.sqlite,.rpm.lock} "${OUTPUTDIR}/" diff --git a/image/initrd/mkosi.conf b/image/initrd/mkosi.conf index 173691555a..9c32e11ad6 100644 --- a/image/initrd/mkosi.conf +++ b/image/initrd/mkosi.conf @@ -36,6 +36,4 @@ RemoveFiles=/var/cache RemoveFiles=/etc/pki/ca-trust/extracted/java/cacerts /usr/lib/sysimage/libdnf5/transaction_history.sqlite* /var/cache/ldconfig/aux-cache -# https://github.com/authselect/authselect/pull/348 -# RemoveFiles=/etc/authselect/* CleanPackageMetadata=true diff --git a/image/system/BUILD.bazel b/image/system/BUILD.bazel index a1be956980..ddc7ae621b 100644 --- a/image/system/BUILD.bazel +++ b/image/system/BUILD.bazel @@ -15,7 +15,7 @@ load(":variants.bzl", "CSPS", "STREAMS", "VARIANTS", "autologin", "constellation stream, ), base_trees = [ - "//image/base", + "//image/base:image.tar", ], extra_trees = constellation_packages(stream), initrds = [ diff --git a/image/system/mkosi.conf b/image/system/mkosi.conf index c45f0cc233..f49c9ebd8c 100644 --- a/image/system/mkosi.conf +++ b/image/system/mkosi.conf @@ -19,6 +19,4 @@ RemoveFiles=/var/cache RemoveFiles=/etc/pki/ca-trust/extracted/java/cacerts /usr/lib/sysimage/libdnf5/transaction_history.sqlite* /var/cache/ldconfig/aux-cache -# https://github.com/authselect/authselect/pull/348 -# RemoveFiles=/etc/authselect/* CleanPackageMetadata=true