diff --git a/docs/overrides.md b/docs/overrides.md index bd9bf887..5935af92 100644 --- a/docs/overrides.md +++ b/docs/overrides.md @@ -130,6 +130,34 @@ The `value` is the value to set at the `path`. Values can be simple values such value: customAnnotation: "customValue" ``` +If using a variable that has been [exported](../README.md#importingexporting-variables) from another package, that variable can also be used to set a value, using the syntax `${...}`. In the example below the `COLOR` variable is being used to set the `podinfo.ui.color` value. +``` +kind: UDSBundle +metadata: + name: export-vars + description: Example for using an imported variable to set an overrides value + version: 0.0.1 + +packages: + - name: output-var + repository: localhost:888/output-var + ref: 0.0.1 + exports: + - name: COLOR + + - name: helm-overrides + path: "../../packages/helm" + ref: 0.0.1 + + overrides: + podinfo-component: + unicorn-podinfo: + values: + - path: "podinfo.replicaCount" + value: 1 + - path: "podinfo.ui.color" + value: ${COLOR} +``` ### Variables Variables are similar to [values](#values) in that they allow users to override values in a Zarf package component's underlying Helm chart; they also share a similar syntax. However, unlike `values`, `variables` can be overridden at deploy time. For example, consider the following `variables`: diff --git a/src/pkg/bundle/deploy.go b/src/pkg/bundle/deploy.go index a370f3a8..97274525 100644 --- a/src/pkg/bundle/deploy.go +++ b/src/pkg/bundle/deploy.go @@ -10,6 +10,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strings" "github.com/AlecAivazis/survey/v2" @@ -30,6 +31,9 @@ import ( // ZarfOverrideMap is a map of Zarf packages -> components -> Helm charts -> values type ZarfOverrideMap map[string]map[string]map[string]interface{} +// templatedVarRegex is the regex for templated variables +var templatedVarRegex = regexp.MustCompile(`\${([^}]+)}`) + // Deploy deploys a bundle // // : create a new provider @@ -166,7 +170,7 @@ func deployPackages(packages []types.Package, resume bool, b *Bundle, zarfPackag SetVariables: pkgVars, } - valuesOverrides, err := b.loadChartOverrides(pkg) + valuesOverrides, err := b.loadChartOverrides(pkg, pkgVars) if err != nil { return err } @@ -201,6 +205,10 @@ func deployPackages(packages []types.Package, resume bool, b *Bundle, zarfPackag // save exported vars pkgExportedVars := make(map[string]string) for _, exp := range pkg.Exports { + // ensure if variable exists in package + if _, ok := pkgCfg.SetVariableMap[exp.Name]; !ok { + return fmt.Errorf("cannot export variable %s because it does not exist in package %s", exp.Name, pkg.Name) + } pkgExportedVars[strings.ToUpper(exp.Name)] = pkgCfg.SetVariableMap[exp.Name].Value } bundleExportedVars[pkg.Name] = pkgExportedVars @@ -281,7 +289,7 @@ func (b *Bundle) confirmBundleDeploy() (confirm bool) { } // loadChartOverrides converts a helm path to a ValuesOverridesMap config for Zarf -func (b *Bundle) loadChartOverrides(pkg types.Package) (ZarfOverrideMap, error) { +func (b *Bundle) loadChartOverrides(pkg types.Package, pkgVars map[string]string) (ZarfOverrideMap, error) { // Create a nested map to hold the values overrideMap := make(map[string]map[string]*values.Options) @@ -290,7 +298,7 @@ func (b *Bundle) loadChartOverrides(pkg types.Package) (ZarfOverrideMap, error) for componentName, component := range pkg.Overrides { for chartName, chart := range component { chartCopy := chart // Create a copy of the chart - err := b.processOverrideValues(&overrideMap, &chartCopy.Values, componentName, chartName) + err := b.processOverrideValues(&overrideMap, &chartCopy.Values, componentName, chartName, pkgVars) if err != nil { return nil, err } @@ -332,10 +340,10 @@ func (b *Bundle) loadChartOverrides(pkg types.Package) (ZarfOverrideMap, error) } // processOverrideValues processes a bundles values overrides and adds them to the override map -func (b *Bundle) processOverrideValues(overrideMap *map[string]map[string]*values.Options, values *[]types.BundleChartValue, componentName string, chartName string) error { +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); err != nil { + if err := addOverrideValue(*overrideMap, componentName, chartName, v.Path, v.Value, pkgVars); err != nil { return err } } @@ -350,7 +358,7 @@ func (b *Bundle) processOverrideVariables(overrideMap *map[string]map[string]*va v.Name = strings.ToUpper(v.Name) // check for override in env vars if envVarOverride, exists := os.LookupEnv(strings.ToUpper(config.EnvVarPrefix + v.Name)); exists { - if err := addOverrideValue(*overrideMap, componentName, chartName, v.Path, envVarOverride); err != nil { + if err := addOverrideValue(*overrideMap, componentName, chartName, v.Path, envVarOverride, nil); err != nil { return err } continue @@ -373,7 +381,7 @@ func (b *Bundle) processOverrideVariables(overrideMap *map[string]map[string]*va } // Add the override to the map, or return an error if the path is invalid - if err := addOverrideValue(*overrideMap, componentName, chartName, v.Path, overrideVal); err != nil { + if err := addOverrideValue(*overrideMap, componentName, chartName, v.Path, overrideVal, nil); err != nil { return err } @@ -382,7 +390,7 @@ func (b *Bundle) processOverrideVariables(overrideMap *map[string]map[string]*va } // addOverrideValue adds a value to a ZarfOverrideMap -func addOverrideValue(overrides map[string]map[string]*values.Options, component string, chart string, valuePath string, value interface{}) error { +func addOverrideValue(overrides map[string]map[string]*values.Options, component string, chart string, valuePath string, 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) @@ -408,6 +416,9 @@ func addOverrideValue(overrides map[string]map[string]*values.Options, component } // use JSONValues because we can easily marshal the YAML to JSON and Helm understands it jsonVals := fmt.Sprintf("%s=[%s]", valuePath, strings.Join(jsonStrs, ",")) + if pkgVars != nil { + jsonVals = setTemplatedVariables(jsonVals, pkgVars) + } overrides[component][chart].JSONValues = append(overrides[component][chart].JSONValues, jsonVals) case map[string]interface{}: // handle objects by parsing them as json and appending to Options.JSONValues @@ -417,11 +428,34 @@ func addOverrideValue(overrides map[string]map[string]*values.Options, component } // use JSONValues because we can easily marshal the YAML to JSON and Helm understands it val := fmt.Sprintf("%s=%s", valuePath, j) + if pkgVars != nil { + val = setTemplatedVariables(val, pkgVars) + } overrides[component][chart].JSONValues = append(overrides[component][chart].JSONValues, val) default: + // Check for any templated variables if pkgVars set + if pkgVars != nil { + templatedVariable := fmt.Sprintf("%v", v) + value = setTemplatedVariables(templatedVariable, pkgVars) + } // 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 } + +// 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 + replacedValue := templatedVarRegex.ReplaceAllStringFunc(templatedVariables, func(match string) string { + // returns slice with the templated variable and the variable name + variableName := templatedVarRegex.FindStringSubmatch(match)[1] + // If we have a templated variable, get the value from pkgVars + if varValue, ok := pkgVars[variableName]; ok { + return varValue + } + return fmt.Sprintf("${%s_not_found}", variableName) + }) + return replacedValue +} diff --git a/src/test/bundles/12-exported-pkg-vars/uds-bundle.yaml b/src/test/bundles/12-exported-pkg-vars/uds-bundle.yaml new file mode 100644 index 00000000..b22c7275 --- /dev/null +++ b/src/test/bundles/12-exported-pkg-vars/uds-bundle.yaml @@ -0,0 +1,46 @@ +kind: UDSBundle +metadata: + name: export-vars + description: testing a bundle using exported vars from zarf package + version: 0.0.1 + +packages: + - name: output-var + repository: localhost:888/output-var + ref: 0.0.1 + exports: + - name: COLOR + - name: ANNOTATION + - name: DEFENSE + - name: BOOL + + - name: helm-overrides + path: "src/test/packages/helm" + ref: 0.0.1 + imports: + - name: COLOR + package: output-var + - name: ANNOTATION + package: output-var + + overrides: + podinfo-component: + unicorn-podinfo: + values: + - path: "podinfo.replicaCount" + value: 1 + - path: "podinfo.ui.color" + value: ${COLOR} + - path: podinfo.podAnnotations + value: + customAnnotation: ${COLOR}${ANNOTATION} + - path: "podinfo.tolerations" + value: + - key: "unicorn" + operator: "Equal" + value: ${DEFENSE} + effect: "NoSchedule" + - key: "uds" + operator: "Equal" + value: ${BOOL} + effect: "NoSchedule" diff --git a/src/test/e2e/commands_test.go b/src/test/e2e/commands_test.go index 1cf9e501..90eb0e1d 100644 --- a/src/test/e2e/commands_test.go +++ b/src/test/e2e/commands_test.go @@ -30,7 +30,7 @@ func createLocal(t *testing.T, bundlePath string, arch string) { require.NoError(t, err) } -func createLocalError(t *testing.T, bundlePath string, arch string) (stderr string) { +func createLocalError(bundlePath string, arch string) (stderr string) { cmd := strings.Split(fmt.Sprintf("create %s --insecure --confirm -a %s", bundlePath, arch), " ") _, stderr, _ = e2e.UDS(cmd...) return stderr diff --git a/src/test/e2e/variable_test.go b/src/test/e2e/variable_test.go index f97b3083..038bb2ed 100644 --- a/src/test/e2e/variable_test.go +++ b/src/test/e2e/variable_test.go @@ -51,7 +51,7 @@ func TestBundleVariables(t *testing.T) { // Test with bad variable name in import bundleDir = "src/test/bundles/02-simple-vars/import-all-bad-name" - stderr = createLocalError(t, bundleDir, e2e.Arch) + stderr = createLocalError(bundleDir, e2e.Arch) require.Contains(t, stderr, "does not have a matching export") // Test name collisions with exported variables @@ -234,3 +234,46 @@ func TestVariablePrecedence(t *testing.T) { remove(t, bundlePath) } + +func TestZarfPackageExportVarsAsGlobalBundleVars(t *testing.T) { + deployZarfInit(t) + zarfPkgPath1 := "src/test/packages/no-cluster/output-var" + e2e.CreateZarfPkg(t, zarfPkgPath1, false) + + e2e.SetupDockerRegistry(t, 888) + defer e2e.TeardownRegistry(t, 888) + + pkg := filepath.Join(zarfPkgPath1, fmt.Sprintf("zarf-package-output-var-%s-0.0.1.tar.zst", e2e.Arch)) + zarfPublish(t, pkg, "localhost:888") + + e2e.HelmDepUpdate(t, "src/test/packages/helm/unicorn-podinfo") + e2e.CreateZarfPkg(t, "src/test/packages/helm", false) + bundleDir := "src/test/bundles/12-exported-pkg-vars" + bundlePath := filepath.Join(bundleDir, fmt.Sprintf("uds-bundle-export-vars-%s-0.0.1.tar.zst", e2e.Arch)) + + createLocal(t, bundleDir, e2e.Arch) + deploy(t, bundlePath) + + // check templated variables overrides in values + cmd := strings.Split("zarf tools kubectl get deploy -n podinfo unicorn-podinfo -o=jsonpath='{.spec.template.spec.containers[0].env[?(@.name==\"PODINFO_UI_COLOR\")].value}'", " ") + outputUIColor, _, err := e2e.UDS(cmd...) + require.Equal(t, "'orange'", outputUIColor) + require.NoError(t, err) + + // check multiple templated variables as object overrides in values + cmd = strings.Split("zarf tools kubectl get deployment -n podinfo unicorn-podinfo -o=jsonpath='{.spec.template.metadata.annotations}'", " ") + annotations, _, err := e2e.UDS(cmd...) + require.Contains(t, annotations, "\"customAnnotation\":\"orangeAnnotation\"") + require.NoError(t, err) + + // check templated variable list-type overrides in values + cmd = strings.Split("zarf tools kubectl get deployment -n podinfo unicorn-podinfo -o=jsonpath='{.spec.template.spec.tolerations}'", " ") + tolerations, _, err := e2e.UDS(cmd...) + require.Contains(t, tolerations, "\"key\":\"uds\"") + require.Contains(t, tolerations, "\"value\":\"true\"") + require.Contains(t, tolerations, "\"key\":\"unicorn\"") + require.Contains(t, tolerations, "\"value\":\"defense\"") + require.NoError(t, err) + + remove(t, bundlePath) +} diff --git a/src/test/packages/no-cluster/output-var/zarf.yaml b/src/test/packages/no-cluster/output-var/zarf.yaml index a958f4fc..a54b3477 100644 --- a/src/test/packages/no-cluster/output-var/zarf.yaml +++ b/src/test/packages/no-cluster/output-var/zarf.yaml @@ -31,3 +31,15 @@ components: echo "shared var in output-var pkg: "${ZARF_VAR_DOMAIN}"" - cmd: | echo "output-var SPECIFIC_PKG_VAR = "${ZARF_VAR_SPECIFIC_PKG_VAR}"" + - cmd: echo "orange" + setVariables: + - name: COLOR + - cmd: echo "Annotation" + setVariables: + - name: ANNOTATION + - cmd: echo "defense" + setVariables: + - name: DEFENSE + - cmd: echo "true" + setVariables: + - name: BOOL