diff --git a/docs/overrides.md b/docs/overrides.md index 4bf74734..50cf6141 100644 --- a/docs/overrides.md +++ b/docs/overrides.md @@ -242,6 +242,56 @@ Variable precedence is as follows: 1. `uds-config.yaml` variables 1. Variables `default` in the`uds-bundle.yaml` +#### Variable Types +Variables can be of either type `raw` or `file`. The type will default to raw if not set explicitly. + +> [!WARNING] +> If a variable is set to accept a file as its value, but is missing the `file` type, then the file will not be processed. + +```yaml +kind: UDSBundle +metadata: + name: example-bundle + version: 0.0.1 + +packages: + - name: helm-overrides-package + path: "../../packages/helm" + ref: 0.0.1 + overrides: + podinfo-component: + unicorn-podinfo: + variables: + - name: UI_COLOR + path: "ui.color" + description: "variable UI_COLOR accepts a raw value (e.g. a string, int, map) like "purple", which is passed to the ui.color helm path" + type: raw + - name: test_secret + path: "testSecret" + description: "variable TEST_SECRET will resolve to the contents of a file (e.g. test.cert), which gets passed to the testSecret helm path" + type: file +``` + +**File Paths** + +If a file path is not absolute, it will be set as relative to the `uds-config.yaml` directory. + +e.g. the following `uds-config.yaml` is in [`src/test/bundles/07-helm-overrides/variable-files/`](../src/test/bundles/07-helm-overrides/variable-files/uds-config.yaml) +```yaml +variables: + helm-overrides: + test_secret: test.cert +``` + +This means when `test.cert` is evalutated it will first be appended to the config path like so `src/test/bundles/07-helm-overrides/variable-files/test.cert`. + +If the file path is already set to the same relative path as the config, then no merging will take place. + +> [!NOTE] +> UDS CLI does not encrypt or base64 encode any file contents before passing said data to Zarf or Helm. +> For example, if the file contains a key to be used in a Kubernetes secret, it must be base64 encoded before being ingested by UDS CLI. + + ### Namespace It's also possible to specify a namespace for a packaged Helm chart to be installed in. For example, to deploy the a chart in the `custom-podinfo` namespace, you can specify the `namespace` in the `overrides` block: diff --git a/src/cmd/uds.go b/src/cmd/uds.go index 396c4a8e..9474247f 100644 --- a/src/cmd/uds.go +++ b/src/cmd/uds.go @@ -56,6 +56,11 @@ var deployCmd = &cobra.Command{ bundleCfg.DeployOpts.Source = chooseBundle(args) configureZarf() + // set DeployOptions.Config if exists + if config := v.ConfigFileUsed(); config != "" { + bundleCfg.DeployOpts.Config = config + } + // create new bundle client and deploy bndlClient := bundle.NewOrDie(&bundleCfg) defer bndlClient.ClearPaths() diff --git a/src/pkg/bundle/deploy.go b/src/pkg/bundle/deploy.go index 95bb9c14..500a45b6 100644 --- a/src/pkg/bundle/deploy.go +++ b/src/pkg/bundle/deploy.go @@ -54,10 +54,11 @@ func (b *Bundle) Deploy() error { if len(userSpecifiedPackages) != len(packagesToDeploy) { return fmt.Errorf("invalid zarf packages specified by --packages") } - return deployPackages(packagesToDeploy, resume, b) + } else { + packagesToDeploy = b.bundle.Packages } - return deployPackages(b.bundle.Packages, resume, b) + return deployPackages(packagesToDeploy, resume, b) } func deployPackages(packages []types.Package, resume bool, b *Bundle) error { @@ -340,7 +341,7 @@ func (b *Bundle) processOverrideNamespaces(overrideMap sources.NamespaceOverride func (b *Bundle) processOverrideValues(overrideMap *map[string]map[string]*values.Options, values *[]types.BundleChartValue, componentName string, chartName string, pkgVars map[string]string) error { for _, v := range *values { // Add the override to the map, or return an error if the path is invalid - if err := addOverrideValue(*overrideMap, componentName, chartName, v.Path, v.Value, pkgVars); err != nil { + if err := addOverride(*overrideMap, componentName, chartName, v, v.Value, pkgVars); err != nil { return err } } @@ -361,32 +362,38 @@ func (b *Bundle) processOverrideVariables(overrideMap *map[string]map[string]*va setVal := strings.Split(k, ".") if setVal[0] == pkgName && strings.ToUpper(setVal[1]) == v.Name { overrideVal = val + v.Source = b.getSourcePath(types.CLI) } } else if strings.ToUpper(k) == v.Name { overrideVal = val + v.Source = b.getSourcePath(types.CLI) } } // check for override in env vars if not in --set if envVarOverride, exists := os.LookupEnv(strings.ToUpper(config.EnvVarPrefix + v.Name)); overrideVal == nil && exists { overrideVal = envVarOverride + v.Source = b.getSourcePath(types.Env) } // if not in --set or an env var, use the following precedence: configFile, sharedConfig, default if overrideVal == nil { if configFileOverride, existsInConfig := b.cfg.DeployOpts.Variables[pkgName][v.Name]; existsInConfig { overrideVal = configFileOverride + v.Source = b.getSourcePath(types.Config) } else if sharedConfigOverride, existsInSharedConfig := b.cfg.DeployOpts.SharedVariables[v.Name]; existsInSharedConfig { overrideVal = sharedConfigOverride + v.Source = b.getSourcePath(types.Config) } else if v.Default != nil { overrideVal = v.Default + v.Source = b.getSourcePath(types.Bundle) } else { continue } } // Add the override to the map, or return an error if the path is invalid - if err := addOverrideValue(*overrideMap, componentName, chartName, v.Path, overrideVal, nil); err != nil { + if err := addOverride(*overrideMap, componentName, chartName, v, overrideVal, nil); err != nil { return err } @@ -394,8 +401,8 @@ func (b *Bundle) processOverrideVariables(overrideMap *map[string]map[string]*va return nil } -// addOverrideValue adds a value to a PkgOverrideMap -func addOverrideValue(overrides map[string]map[string]*values.Options, component string, chart string, valuePath string, value interface{}, pkgVars map[string]string) error { +// addOverride adds a value or variable to a PkgOverrideMap +func addOverride[T types.ChartOverride](overrides map[string]map[string]*values.Options, component string, chart string, override T, value interface{}, pkgVars map[string]string) error { // Create the component map if it doesn't exist if _, ok := overrides[component]; !ok { overrides[component] = make(map[string]*values.Options) @@ -406,6 +413,23 @@ func addOverrideValue(overrides map[string]map[string]*values.Options, component overrides[component][chart] = &values.Options{} } + var valuePath string + + switch v := any(override).(type) { + case types.BundleChartValue: + valuePath = v.Path + case types.BundleChartVariable: + valuePath = v.Path + if v.Type == types.File { + if fileVals, err := addFileValue(overrides[component][chart].FileValues, value.(string), v); err == nil { + overrides[component][chart].FileValues = fileVals + } else { + return err + } + return nil + } + } + // Add the value to the chart map switch v := value.(type) { case []interface{}: @@ -443,13 +467,31 @@ func addOverrideValue(overrides map[string]map[string]*values.Options, component templatedVariable := fmt.Sprintf("%v", v) value = setTemplatedVariables(templatedVariable, pkgVars) } - // handle default case of simple values like strings and numbers + + // Handle default case of simple values like strings and numbers helmVal := fmt.Sprintf("%s=%v", valuePath, value) overrides[component][chart].Values = append(overrides[component][chart].Values, helmVal) } return nil } +// getSourcePath returns the path from where a value is set +func (b *Bundle) getSourcePath(pathType types.ValueSources) string { + var sourcePath string + switch pathType { + case types.CLI: + sourcePath, _ = os.Getwd() + case types.Env: + sourcePath, _ = os.Getwd() + case types.Bundle: + sourcePath = filepath.Dir(b.cfg.DeployOpts.Source) + case types.Config: + sourcePath = filepath.Dir(b.cfg.DeployOpts.Config) + } + + return sourcePath +} + // setTemplatedVariables sets the value for the templated variables func setTemplatedVariables(templatedVariables string, pkgVars map[string]string) string { // Use ReplaceAllStringFunc to handle all occurrences of templated variables @@ -464,3 +506,34 @@ func setTemplatedVariables(templatedVariables string, pkgVars map[string]string) }) return replacedValue } + +// addFileValue adds a key=filepath string to helm FileValues +func addFileValue(helmFileVals []string, filePath string, override types.BundleChartVariable) ([]string, error) { + verifiedPath, err := formFilePath(override.Source, filePath) + if err != nil { + return nil, err + } + helmVal := fmt.Sprintf("%s=%v", override.Path, verifiedPath) + return append(helmFileVals, helmVal), nil +} + +// formFilePath merges relative paths together to form full path and checks if the file exists +func formFilePath(anchorPath string, filePath string) (string, error) { + if !filepath.IsAbs(filePath) { + // set path relative to anchorPath (i.e. cwd or config), unless they are the same + if anchorPath != filepath.Dir(filePath) { + filePath = filepath.Join(anchorPath, filePath) + } + } + + if helpers.InvalidPath(filePath) { + return "", fmt.Errorf("unable to find file %s", filePath) + } + + _, err := helpers.IsTextFile(filePath) + if err != nil { + return "", err + } + + return filePath, nil +} diff --git a/src/pkg/bundle/deploy_test.go b/src/pkg/bundle/deploy_test.go index 6658f710..b7b2a909 100644 --- a/src/pkg/bundle/deploy_test.go +++ b/src/pkg/bundle/deploy_test.go @@ -1,7 +1,9 @@ package bundle import ( + "fmt" "os" + "path/filepath" "testing" "github.com/defenseunicorns/uds-cli/src/types" @@ -429,3 +431,184 @@ func TestHelmOverrideVariablePrecedence(t *testing.T) { }) } } + +func TestFileVariableHandlers(t *testing.T) { + cwd, _ := os.Getwd() + const ( + componentName = "test-component" + chartName = "test-chart" + pkgName = "test-package" + varName = "CERT" + path = "test.Cert" + relativePath = "../../../src/test/bundles/07-helm-overrides/variable-files/" + ) + + type args struct { + pkgName string + variables *[]types.BundleChartVariable + componentName string + chartName string + } + testCases := []struct { + name string + bundle Bundle + args args + loadEnv bool + requireNoErr bool + expected string + }{ + { + name: "with --set", + bundle: Bundle{ + cfg: &types.BundleConfig{ + DeployOpts: types.BundleDeployOptions{ + SetVariables: map[string]string{ + varName: fmt.Sprintf("%s/test.cert", relativePath), + }, + }, + }, + }, + args: args{ + pkgName: pkgName, + variables: &[]types.BundleChartVariable{ + { + Name: varName, + Path: path, + Type: types.File, + Description: "set the var from cli, so source path is current working directory (eg. /home/user/repos/uds-cli/...)", + }, + }, + componentName: componentName, + chartName: chartName, + }, + requireNoErr: true, + expected: fmt.Sprintf("%s=%s", path, filepath.Join(cwd, fmt.Sprintf("%s/test.cert", relativePath))), + }, + { + name: "with UDS_VAR", + bundle: Bundle{ + cfg: &types.BundleConfig{ + DeployOpts: types.BundleDeployOptions{}, + }, + }, + args: args{ + pkgName: pkgName, + variables: &[]types.BundleChartVariable{ + { + Name: varName, + Path: path, + Type: types.File, + Description: "set the var from env, so source path is current working directory (eg. /home/user/repos/uds-cli/...)", + }, + }, + componentName: componentName, + chartName: chartName, + }, + loadEnv: true, + requireNoErr: true, + expected: fmt.Sprintf("%s=%s", path, filepath.Join(cwd, fmt.Sprintf("%s/test.cert", relativePath))), + }, + { + name: "with Config", + bundle: Bundle{ + cfg: &types.BundleConfig{ + DeployOpts: types.BundleDeployOptions{ + Config: fmt.Sprintf("%s/uds-config.yaml", relativePath), + Variables: map[string]map[string]interface{}{ + pkgName: { + varName: "test.cert", + }, + }, + }, + }, + }, + args: args{ + pkgName: pkgName, + variables: &[]types.BundleChartVariable{ + { + Name: varName, + Path: path, + Type: types.File, + Description: "set the var from config, so source path is config directory", + }, + }, + componentName: componentName, + chartName: chartName, + }, + requireNoErr: true, + expected: fmt.Sprintf("%s=%s", path, fmt.Sprintf("%stest.cert", relativePath)), + }, + { + name: "with Bundle", + bundle: Bundle{ + cfg: &types.BundleConfig{ + DeployOpts: types.BundleDeployOptions{ + + Source: fmt.Sprintf("%s/uds-bundle-helm-overrides-amd64-0.0.1.tar.zst", relativePath), + }, + }, + }, + args: args{ + pkgName: pkgName, + variables: &[]types.BundleChartVariable{ + { + Name: varName, + Path: path, + Type: types.File, + Description: "set the var from bundle default, so source path is bundle directory", + Default: "test.cert", + }, + }, + componentName: componentName, + chartName: chartName, + }, + requireNoErr: true, + expected: fmt.Sprintf("%s=%s", path, fmt.Sprintf("%stest.cert", relativePath)), + }, + { + name: "file not found", + bundle: Bundle{ + cfg: &types.BundleConfig{ + DeployOpts: types.BundleDeployOptions{ + Source: fmt.Sprintf("%s/uds-bundle-helm-overrides-amd64-0.0.1.tar.zst", relativePath), + }, + }, + }, + args: args{ + pkgName: pkgName, + variables: &[]types.BundleChartVariable{ + { + Name: varName, + Path: path, + Type: types.File, + Description: "set the var from bundle default, so source path is bundle directory", + Default: "not-there-test.cert", + }, + }, + componentName: componentName, + chartName: chartName, + }, + requireNoErr: false, + expected: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + os.Unsetenv("UDS_CERT") + if tc.loadEnv { + os.Setenv("UDS_CERT", fmt.Sprintf("%s/test.cert", relativePath)) + } + + overrideMap := map[string]map[string]*values.Options{} + err := tc.bundle.processOverrideVariables(&overrideMap, tc.args.pkgName, tc.args.variables, tc.args.componentName, tc.args.chartName) + + if tc.requireNoErr { + require.NoError(t, err) + require.Equal(t, tc.expected, overrideMap[componentName][chartName].FileValues[0]) + } else { + require.Contains(t, err.Error(), "unable to find") + } + }) + } +} diff --git a/src/test/bundles/07-helm-overrides/uds-bundle.yaml b/src/test/bundles/07-helm-overrides/uds-bundle.yaml index dd385516..3dd87610 100644 --- a/src/test/bundles/07-helm-overrides/uds-bundle.yaml +++ b/src/test/bundles/07-helm-overrides/uds-bundle.yaml @@ -52,3 +52,7 @@ packages: - name: HOSTS path: "podinfo.ingress.hosts" description: "just testing a a list of objects (doesn't actually do ingress things)" + - name: SECRET_FILE_VAL + path: "testFileSecret" + description: "testing setting secret with file" + type: "file" diff --git a/src/test/bundles/07-helm-overrides/uds-config.yaml b/src/test/bundles/07-helm-overrides/uds-config.yaml index a688023d..299679dc 100644 --- a/src/test/bundles/07-helm-overrides/uds-config.yaml +++ b/src/test/bundles/07-helm-overrides/uds-config.yaml @@ -24,3 +24,4 @@ variables: paths: - path: "/" pathType: "Prefix" + secret_file_val: "./variable-files/test.cert" diff --git a/src/test/bundles/07-helm-overrides/variable-files/test.cert b/src/test/bundles/07-helm-overrides/variable-files/test.cert new file mode 100644 index 00000000..d1c7a393 --- /dev/null +++ b/src/test/bundles/07-helm-overrides/variable-files/test.cert @@ -0,0 +1 @@ +c3NoLXJzYSBBQUFBQjNOemFDMXljMkVBQUFBREFRQUJBQUFCZ1FDS2MyK2JHMjF5YkNUdUMwVTg4RlB2ZWF2cWFkbjlTckJqa0NHcGFISU5xMmhaOThlVmxlNXlJbk9PU3ExbEdNQzg5Rnl4N1ZDbFl3R0sxRHhNS3RSVzZ1K0hVV29PanN4Smg0R0dJY2xyUkc0eEdPU1dRazdWUjJrUnFPM3pLK3hnRUFVbzR5UW85TCtXUHA5R01HR3VNWnVNRkxzS3lwVUpuM1AyWFZVS2d5Mm9TQk1UcVZ1SFpqWjRubjR1YlhPYjlXZk96LzBBRll2c0lCdUpNTXEvSzNxZmZXQWN5RzJ6KzNmQTBQTUJGZmpnOGhpM1FMazdyeFpXdVl4bUtHcklHY3V2NXNYYjN5Q0pNUmF2NmtpbHhZQmxyREVORm5QYjkxUC93MUZBWG8zcnlDU3NYVDRkWFdUT0IvWkdBNHhTRTVGWmFnYUJMSEdrUmxFd0xCT3V5SFhEQ2hUNkI4WWhQYmxTZHp2S1dIZmdNUi9aWnFrLysyNkpEWkFFaVExUXJaNkMzOWNDSUpYUm5RSHlydGRtYWM3YXBrUEpDYlRFbk5RSmRKNTdkS3ltbVBUNmtFZ0dVSDlSLzJmbEtHcnludzFTQ0Q5ckIxT1QvZmJnamhCbGRJMzhNMjBNQUdxdFJvQ0VSaEJBTzd0WVdQLzBGanU2Qi9jQjRWWEZqajA9IHRyaXN0YW5AdHJpc3Rhbi1OVUMxM0FOSGk3 diff --git a/src/test/e2e/variable_test.go b/src/test/e2e/variable_test.go index 7ff5be77..c348e8d2 100644 --- a/src/test/e2e/variable_test.go +++ b/src/test/e2e/variable_test.go @@ -5,6 +5,7 @@ package test import ( + "encoding/base64" "fmt" "os" "path/filepath" @@ -75,7 +76,7 @@ func TestBundleWithHelmOverrides(t *testing.T) { e2e.CreateZarfPkg(t, "src/test/packages/helm", false) bundleDir := "src/test/bundles/07-helm-overrides" bundlePath := filepath.Join(bundleDir, fmt.Sprintf("uds-bundle-helm-overrides-%s-0.0.1.tar.zst", e2e.Arch)) - err := os.Setenv("UDS_CONFIG", filepath.Join("src/test/bundles/07-helm-overrides", "uds-config.yaml")) + err := os.Setenv("UDS_CONFIG", filepath.Join(bundleDir, "uds-config.yaml")) require.NoError(t, err) createLocal(t, bundleDir, e2e.Arch) @@ -105,7 +106,6 @@ func TestBundleWithHelmOverrides(t *testing.T) { require.Contains(t, tolerations, "\"key\":\"unicorn\"") require.Contains(t, tolerations, "\"effect\":\"NoSchedule\"") require.NoError(t, err) - }) // test variables overrides @@ -154,6 +154,15 @@ func TestBundleWithHelmOverrides(t *testing.T) { require.Contains(t, hosts, "podinfo.unicorns") }) + t.Run("check variables overrides with a file type value", func(t *testing.T) { + cmd := strings.Split("zarf tools kubectl get secret -n podinfo test-file-secret -o=jsonpath={.data.test}", " ") + stdout, _, err := e2e.UDS(cmd...) + require.NoError(t, err) + decoded, err := base64.StdEncoding.DecodeString(stdout) + require.NoError(t, err) + require.Contains(t, string(decoded), "ssh-rsa") + }) + remove(t, bundlePath) } diff --git a/src/test/packages/helm/unicorn-podinfo/templates/file-secret.yaml b/src/test/packages/helm/unicorn-podinfo/templates/file-secret.yaml new file mode 100644 index 00000000..03a8951d --- /dev/null +++ b/src/test/packages/helm/unicorn-podinfo/templates/file-secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: test-file-secret +type: Opaque +data: + test: {{ .Values.testFileSecret }} diff --git a/src/test/packages/helm/unicorn-podinfo/values.yaml b/src/test/packages/helm/unicorn-podinfo/values.yaml index 776919ee..0085e8d6 100644 --- a/src/test/packages/helm/unicorn-podinfo/values.yaml +++ b/src/test/packages/helm/unicorn-podinfo/values.yaml @@ -1,4 +1,5 @@ testSecret: "dGVzdC1zZWNyZXQ=" # test-secret +testFileSecret: "" podinfo: ui: color: "purple" diff --git a/src/types/bundle.go b/src/types/bundle.go index 8ff296c6..a64496b4 100644 --- a/src/types/bundle.go +++ b/src/types/bundle.go @@ -4,6 +4,22 @@ // Package types contains all the types used by UDS. package types +type ChartVariableType string + +const ( + File ChartVariableType = "file" + Raw ChartVariableType = "raw" +) + +type ValueSources string + +const ( + Config ValueSources = "config" + Env ValueSources = "env" + CLI ValueSources = "cli" + Bundle ValueSources = "bundle" +) + // UDSBundle is the top-level structure of a UDS bundle type UDSBundle struct { Kind string `json:"kind" jsonschema:"description=The kind of UDS package,enum=UDSBundle"` @@ -34,16 +50,22 @@ type BundleChartOverrides struct { ValuesFiles []string `json:"valuesFiles,omitempty" jsonschema:"description=List of Helm chart value file paths to set statically"` } +type ChartOverride interface { + BundleChartVariable | BundleChartValue +} + type BundleChartValue struct { Path string `json:"path" jsonschema:"name=Path to the Helm chart value to set. The format is , example=controller.service.type"` Value interface{} `json:"value" jsonschema:"name=The value to set"` } type BundleChartVariable struct { - Path string `json:"path" jsonschema:"name=Path to the Helm chart value to set. The format is , example=controller.service.type"` - Name string `json:"name" jsonschema:"name=Name of the variable to set"` - Description string `json:"description,omitempty" jsonschema:"name=Description of the variable"` - Default interface{} `json:"default,omitempty" jsonschema:"name=The default value to set"` + Path string `json:"path" jsonschema:"name=Path to the Helm chart value to set. The format is , example=controller.service.type"` + Name string `json:"name" jsonschema:"name=Name of the variable to set"` + Description string `json:"description,omitempty" jsonschema:"name=Description of the variable"` + Default interface{} `json:"default,omitempty" jsonschema:"name=The default value to set"` + Type ChartVariableType `json:"type,omitempty" jsonschema:"description=The type of value to be processed,enum=raw,enum=file"` + Source string `json:"source,omitempty" jsonschema:"description=Where the value is set from"` } // BundleVariableImport represents variables in the bundle diff --git a/src/types/options.go b/src/types/options.go index 7a3ef415..a88dd22e 100644 --- a/src/types/options.go +++ b/src/types/options.go @@ -27,6 +27,7 @@ type BundleCreateOptions struct { type BundleDeployOptions struct { Resume bool Source string + Config string Packages []string PublicKeyPath string SetVariables map[string]string `json:"setVariables" jsonschema:"description=Key-Value map of variable names and their corresponding values that will be used by Zarf packages in a bundle"` diff --git a/uds.schema.json b/uds.schema.json index 1030a301..96c03734 100644 --- a/uds.schema.json +++ b/uds.schema.json @@ -68,6 +68,18 @@ }, "default": { "additionalProperties": true + }, + "type": { + "enum": [ + "raw", + "file" + ], + "type": "string", + "description": "The type of value to be processed" + }, + "source": { + "type": "string", + "description": "Where the value is set from" } }, "additionalProperties": false,