From 80c566389b476d067e4f5cb21f977bf1f2f595e4 Mon Sep 17 00:00:00 2001 From: decleaver <85503726+decleaver@users.noreply.github.com> Date: Tue, 28 May 2024 16:58:23 -0600 Subject: [PATCH] feat: uds config validation (#618) Co-authored-by: UncleGedd <42304551+UncleGedd@users.noreply.github.com> --- src/cmd/common.go | 24 ++++++++ src/cmd/root.go | 59 ++++++++++++++++++ src/cmd/uds.go | 44 ------------- src/cmd/uds_test.go | 61 +++++++++++++++++++ .../07-helm-overrides/uds-config-invalid.yaml | 6 ++ src/test/e2e/bundle_test.go | 11 ++++ src/types/options.go | 1 + 7 files changed, 162 insertions(+), 44 deletions(-) create mode 100644 src/cmd/uds_test.go create mode 100644 src/test/bundles/07-helm-overrides/uds-config-invalid.yaml diff --git a/src/cmd/common.go b/src/cmd/common.go index 12c498b9..fc35d86f 100644 --- a/src/cmd/common.go +++ b/src/cmd/common.go @@ -20,6 +20,30 @@ import ( "github.com/spf13/cobra" ) +type configOption string + +// Valid values for options in uds_config.yaml +const ( + confirm configOption = "confirm" + insecure configOption = "insecure" + cachePath configOption = "uds_cache" + tempDirectory configOption = "tmp_dir" + logLevelOption configOption = "log_level" + architecture configOption = "architecture" + noLogFile configOption = "no_log_file" + noProgress configOption = "no_progress" +) + +// isValidConfigOption checks if a string is a valid config option +func isValidConfigOption(str string) bool { + switch configOption(str) { + case confirm, insecure, cachePath, tempDirectory, logLevelOption, architecture, noLogFile, noProgress: + return true + default: + return false + } +} + // deploy performs validation, confirmation and deployment of a bundle func deploy(bndlClient *bundle.Bundle) { _, _, _, err := bndlClient.PreDeployValidation() diff --git a/src/cmd/root.go b/src/cmd/root.go index 3b1e0b7d..31184f5a 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -7,12 +7,14 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/defenseunicorns/uds-cli/src/config" "github.com/defenseunicorns/uds-cli/src/config/lang" "github.com/defenseunicorns/uds-cli/src/types" zarfCommon "github.com/defenseunicorns/zarf/src/cmd/common" "github.com/defenseunicorns/zarf/src/pkg/message" + goyaml "github.com/goccy/go-yaml" "github.com/spf13/cobra" ) @@ -67,6 +69,14 @@ func init() { initViper() + // load uds-config if it exists + if v.ConfigFileUsed() != "" { + if err := loadViperConfig(); err != nil { + message.Fatalf(err, "Failed to load uds-config: %s", err.Error()) + return + } + } + v.SetDefault(V_LOG_LEVEL, "info") v.SetDefault(V_ARCHITECTURE, "") v.SetDefault(V_NO_LOG_FILE, false) @@ -87,3 +97,52 @@ func init() { rootCmd.PersistentFlags().BoolVar(&config.CommonOptions.Insecure, "insecure", v.GetBool(V_INSECURE), lang.RootCmdFlagInsecure) rootCmd.PersistentFlags().IntVar(&config.CommonOptions.OCIConcurrency, "oci-concurrency", v.GetInt(V_BNDL_OCI_CONCURRENCY), lang.CmdBundleFlagConcurrency) } + +// loadViperConfig reads the config file and unmarshals the relevant config into DeployOpts.Variables +func loadViperConfig() error { + // get config file from Viper + configFile, err := os.ReadFile(v.ConfigFileUsed()) + if err != nil { + return err + } + + err = unmarshalAndValidateConfig(configFile, &bundleCfg) + if err != nil { + return err + } + + // ensure the DeployOpts.Variables pkg vars are uppercase + for pkgName, pkgVar := range bundleCfg.DeployOpts.Variables { + for varName, varValue := range pkgVar { + // delete the lowercase var and replace with uppercase + delete(bundleCfg.DeployOpts.Variables[pkgName], varName) + bundleCfg.DeployOpts.Variables[pkgName][strings.ToUpper(varName)] = varValue + } + } + + // ensure the DeployOpts.SharedVariables vars are uppercase + for varName, varValue := range bundleCfg.DeployOpts.SharedVariables { + // delete the lowercase var and replace with uppercase + delete(bundleCfg.DeployOpts.SharedVariables, varName) + bundleCfg.DeployOpts.SharedVariables[strings.ToUpper(varName)] = varValue + } + + return nil +} + +func unmarshalAndValidateConfig(configFile []byte, bundleCfg *types.BundleConfig) error { + // read relevant config into DeployOpts.Variables + // need to use goyaml because Viper doesn't preserve case: https://github.com/spf13/viper/issues/1014 + // unmarshalling into DeployOpts because we want to check all of the top level config keys which are currently defined in DeployOpts + err := goyaml.UnmarshalWithOptions(configFile, &bundleCfg.DeployOpts, goyaml.Strict()) + if err != nil { + return err + } + // validate config options + for optionName := range bundleCfg.DeployOpts.Options { + if !isValidConfigOption(optionName) { + return fmt.Errorf("invalid config option: %s", optionName) + } + } + return nil +} diff --git a/src/cmd/uds.go b/src/cmd/uds.go index 247c3b83..396c4a8e 100644 --- a/src/cmd/uds.go +++ b/src/cmd/uds.go @@ -10,14 +10,12 @@ import ( "io" "os" "path/filepath" - "strings" "github.com/AlecAivazis/survey/v2" "github.com/defenseunicorns/uds-cli/src/config" "github.com/defenseunicorns/uds-cli/src/config/lang" "github.com/defenseunicorns/uds-cli/src/pkg/bundle" "github.com/defenseunicorns/zarf/src/pkg/message" - goyaml "github.com/goccy/go-yaml" "github.com/spf13/cobra" ) @@ -58,14 +56,6 @@ var deployCmd = &cobra.Command{ bundleCfg.DeployOpts.Source = chooseBundle(args) configureZarf() - // load uds-config if it exists - if v.ConfigFileUsed() != "" { - if err := loadViperConfig(); err != nil { - message.Fatalf(err, "Failed to load uds-config: %s", err.Error()) - return - } - } - // create new bundle client and deploy bndlClient := bundle.NewOrDie(&bundleCfg) defer bndlClient.ClearPaths() @@ -185,40 +175,6 @@ var logsCmd = &cobra.Command{ }, } -// loadViperConfig reads the config file and unmarshals the relevant config into DeployOpts.Variables -func loadViperConfig() error { - // get config file from Viper - configFile, err := os.ReadFile(v.ConfigFileUsed()) - if err != nil { - return err - } - - // read relevant config into DeployOpts.Variables - // need to use goyaml because Viper doesn't preserve case: https://github.com/spf13/viper/issues/1014 - err = goyaml.Unmarshal(configFile, &bundleCfg.DeployOpts) - if err != nil { - return err - } - - // ensure the DeployOpts.Variables pkg vars are uppercase - for pkgName, pkgVar := range bundleCfg.DeployOpts.Variables { - for varName, varValue := range pkgVar { - // delete the lowercase var and replace with uppercase - delete(bundleCfg.DeployOpts.Variables[pkgName], varName) - bundleCfg.DeployOpts.Variables[pkgName][strings.ToUpper(varName)] = varValue - } - } - - // ensure the DeployOpts.SharedVariables vars are uppercase - for varName, varValue := range bundleCfg.DeployOpts.SharedVariables { - // delete the lowercase var and replace with uppercase - delete(bundleCfg.DeployOpts.SharedVariables, varName) - bundleCfg.DeployOpts.SharedVariables[strings.ToUpper(varName)] = varValue - } - - return nil -} - func init() { initViper() diff --git a/src/cmd/uds_test.go b/src/cmd/uds_test.go new file mode 100644 index 00000000..ef0b8659 --- /dev/null +++ b/src/cmd/uds_test.go @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023-Present The UDS Authors + +// Package cmd contains the CLI commands for UDS. +package cmd + +import ( + "testing" + + "github.com/defenseunicorns/uds-cli/src/types" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalAndValidateConfig(t *testing.T) { + type args struct { + configFile []byte + bundleCfg *types.BundleConfig + } + tests := []struct { + name string + args args + wantErr bool + errContains string + }{ + { + name: "Invalid option key", + args: args{ + configFile: []byte(` +options: + log_levelx: debug +`), + bundleCfg: &types.BundleConfig{}, + }, + wantErr: true, + errContains: "invalid config option: log_levelx", + }, + { + name: "Option typo", + args: args{ + configFile: []byte(` +optionx: + log_level: debug +`), + bundleCfg: &types.BundleConfig{}, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := unmarshalAndValidateConfig(tt.args.configFile, tt.args.bundleCfg) + if tt.wantErr { + require.NotNil(t, err, "Expected error") + require.Contains(t, err.Error(), tt.errContains, "Error message should contain the expected string") + } else { + require.Nil(t, err, "Expected no error") + } + }) + } +} diff --git a/src/test/bundles/07-helm-overrides/uds-config-invalid.yaml b/src/test/bundles/07-helm-overrides/uds-config-invalid.yaml new file mode 100644 index 00000000..a93de69f --- /dev/null +++ b/src/test/bundles/07-helm-overrides/uds-config-invalid.yaml @@ -0,0 +1,6 @@ +options: + log_levelx: debug + +variables: + helm-overrides: + ui_color: "orange" diff --git a/src/test/e2e/bundle_test.go b/src/test/e2e/bundle_test.go index 6274d81b..6520c150 100644 --- a/src/test/e2e/bundle_test.go +++ b/src/test/e2e/bundle_test.go @@ -610,6 +610,17 @@ func TestBundleTmpDir(t *testing.T) { require.NoError(t, err) } +func TestInvalidConfig(t *testing.T) { + os.Setenv("UDS_CONFIG", filepath.Join("src/test/bundles/07-helm-overrides", "uds-config-invalid.yaml")) + zarfPkgPath := "src/test/packages/helm" + e2e.HelmDepUpdate(t, fmt.Sprintf("%s/unicorn-podinfo", zarfPkgPath)) + args := strings.Split(fmt.Sprintf("zarf package create %s -o %s --confirm", zarfPkgPath, zarfPkgPath), " ") + _, stdErr, err := e2e.UDS(args...) + require.Error(t, err) + require.Contains(t, stdErr, "invalid config option: log_levelx") + os.Unsetenv("UDS_CONFIG") +} + func TestInvalidBundle(t *testing.T) { deployZarfInit(t) zarfPkgPath := "src/test/packages/helm" diff --git a/src/types/options.go b/src/types/options.go index 0c9285ae..7a3ef415 100644 --- a/src/types/options.go +++ b/src/types/options.go @@ -34,6 +34,7 @@ type BundleDeployOptions struct { Variables map[string]map[string]interface{} `yaml:"variables,omitempty"` SharedVariables map[string]interface{} `yaml:"shared,omitempty"` Retries int `yaml:"retries"` + Options map[string]interface{} `yaml:"options,omitempty"` } // BundleInspectOptions is the options for the bundler.Inspect() function