diff --git a/docs/overrides.md b/docs/overrides.md index 5722e35c..4bf74734 100644 --- a/docs/overrides.md +++ b/docs/overrides.md @@ -59,6 +59,8 @@ packages: overrides: helm-overrides-component: podinfo: + valuesFiles: + - file: values.yaml values: - path: "replicaCount" value: 2 @@ -69,7 +71,13 @@ packages: default: "purple" ``` -This bundle will deploy the `helm-overrides-package` Zarf package and override the `replicaCount` and `ui.color` values in the `podinfo` chart. The `values` can't be modified after the bundle has been created. However, at deploy time, users can override the `UI_COLOR` and other `variables` using a environment variable called `UDS_UI_COLOR` or by specifying it in a `uds-config.yaml` like so: +```yaml +#values.yaml +podAnnotations: + customAnnotation: "customValue" +``` + +This bundle will deploy the `helm-overrides-package` Zarf package and override the `replicaCount`, `ui.color`, and `podAnnotations` values in the `podinfo` chart. The `values` can't be modified after the bundle has been created. However, at deploy time, users can override the `UI_COLOR` and other `variables` using a environment variable called `UDS_UI_COLOR` or by specifying it in a `uds-config.yaml` like so: ```yaml variables: @@ -92,6 +100,8 @@ packages: overrides: helm-overrides-component: # component name inside of the helm-overrides-package Zarf pkg podinfo: # chart name from the helm-overrides-component component + valuesFiles: + - file: values.yaml values: - path: "replicaCount" value: 2 @@ -102,7 +112,16 @@ packages: default: "purple" ``` -In this example, the `helm-overrides-package` Zarf package has a component called `helm-overrides-component` which contains a Helm chart called `podinfo`; note how these names are keys in the `overrides` block. The `podinfo` chart has a `replicaCount` value that is overridden to `2` and a variable called `UI_COLOR` that is overridden to `purple`. +```yaml +#values.yaml +podAnnotations: + customAnnotation: "customValue" +``` +In this example, the `helm-overrides-package` Zarf package has a component called `helm-overrides-component` which contains a Helm chart called `podinfo`; note how these names are keys in the `overrides` block. The `podinfo` chart has a `replicaCount` value that is overridden to `2`, a `podAnnotations` value that is overridden to include `customAnnotation: "customValue"` and a variable called `UI_COLOR` that is overridden to `purple`. + +### Values Files + +The `valuesFiles` in an `overrides` block are a list of `file`'s. It allows users to override multiple values in a Zarf package component's underlying Helm chart, by providing a file with those values instead of having to include them all indiviually in the `overrides` block. ### Values @@ -160,6 +179,12 @@ packages: value: ${COLOR} ``` +#### Value Precedence +Value precedence is as follows: +1. The `values` in an `overrides` block +1. `values` set in the last `valuesFile` (if more than one specified) +1. `values` set in the previous `valuesFile` (if more than one specified) + ### 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 `variables` key in the following `uds-bundle.yaml`: diff --git a/src/pkg/bundle/create.go b/src/pkg/bundle/create.go index b8d64653..e06b56b6 100644 --- a/src/pkg/bundle/create.go +++ b/src/pkg/bundle/create.go @@ -11,11 +11,13 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/defenseunicorns/uds-cli/src/config" "github.com/defenseunicorns/uds-cli/src/pkg/bundler" + "github.com/defenseunicorns/uds-cli/src/types" zarfConfig "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/pkg/interactive" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/pterm/pterm" + "helm.sh/helm/v3/pkg/chartutil" ) // Create creates a bundle @@ -26,6 +28,11 @@ func (b *Bundle) Create() error { return err } + // Populate values from valuesFiles if provided + if err := b.processValuesFiles(); err != nil { + return err + } + // confirm creation if ok := b.confirmBundleCreation(); !ok { return fmt.Errorf("bundle creation cancelled") @@ -107,3 +114,64 @@ func (b *Bundle) confirmBundleCreation() (confirm bool) { } return true } + +// processValuesFiles reads values from valuesFiles and updates the bundle with the override values +func (b *Bundle) processValuesFiles() error { + // Populate values from valuesFiles if provided + for i, pkg := range b.bundle.Packages { + for componentName, overrides := range pkg.Overrides { + for chartName, bundleChartOverrides := range overrides { + valuesFilesToMerge := make([][]types.BundleChartValue, 0) + // Iterate over valuesFiles in reverse order to ensure subsequent value files takes precedence over previous ones + for _, valuesFile := range bundleChartOverrides.ValuesFiles { + // Check relative vs absolute path + fileName := filepath.Join(b.cfg.CreateOpts.SourceDirectory, valuesFile) + if filepath.IsAbs(valuesFile) { + fileName = valuesFile + } + // read values from valuesFile + values, err := chartutil.ReadValuesFile(fileName) + if err != nil { + return err + } + if len(values) > 0 { + // populate BundleChartValue slice to use for merging existing values + valuesFileValues := make([]types.BundleChartValue, 0, len(values)) + for key, value := range values { + valuesFileValues = append(valuesFileValues, types.BundleChartValue{Path: key, Value: value}) + } + valuesFilesToMerge = append(valuesFilesToMerge, valuesFileValues) + } + } + override := b.bundle.Packages[i].Overrides[componentName][chartName] + // add override values to the end of the list of values to merge since we want them to take precedence + valuesFilesToMerge = append(valuesFilesToMerge, override.Values) + override.Values = mergeBundleChartValues(valuesFilesToMerge...) + b.bundle.Packages[i].Overrides[componentName][chartName] = override + } + } + } + return nil +} + +// mergeBundleChartValues merges lists of BundleChartValue using the values from the last list if there are any duplicates +// such that values from the last list will take precedence over the values from previous lists +func mergeBundleChartValues(bundleChartValueLists ...[]types.BundleChartValue) []types.BundleChartValue { + mergedMap := make(map[string]types.BundleChartValue) + + // Iterate over each list in order + for _, bundleChartValues := range bundleChartValueLists { + // Add entries from the current list to the merged map, overwriting any existing entries + for _, bundleChartValue := range bundleChartValues { + mergedMap[bundleChartValue.Path] = bundleChartValue + } + } + + // Convert the map to a slice + merged := make([]types.BundleChartValue, 0, len(mergedMap)) + for _, value := range mergedMap { + merged = append(merged, value) + } + + return merged +} diff --git a/src/test/bundles/07-helm-overrides/values-file/uds-bundle.yaml b/src/test/bundles/07-helm-overrides/values-file/uds-bundle.yaml new file mode 100644 index 00000000..46550cfa --- /dev/null +++ b/src/test/bundles/07-helm-overrides/values-file/uds-bundle.yaml @@ -0,0 +1,44 @@ +kind: UDSBundle +metadata: + name: helm-values-file + description: testing a bundle with Helm overrides + version: 0.0.1 + +packages: + - name: helm-overrides + path: "../../../packages/helm" + ref: 0.0.1 + + overrides: + podinfo-component: + unicorn-podinfo: + valuesFiles: + - values.yaml + - values2.yaml + values: + - path: "podinfo.replicaCount" + value: 2 + variables: + - name: log_level + path: "podinfo.logLevel" + description: "Set the log level for podinfo" + default: "debug" # not overwritten! + - name: ui_color + path: "podinfo.ui.color" + description: "Set the color for podinfo's UI" + default: "blue" + - name: UI_MSG + path: "podinfo.ui.message" + description: "Set the message for podinfo's UI" + - name: SECRET_VAL + path: "testSecret" + description: "testing a secret value" + - name: SECURITY_CTX + path: "podinfo.securityContext" + description: "testing an object" + default: + runAsUser: 1000 + runAsGroup: 3000 + - name: HOSTS + path: "podinfo.ingress.hosts" + description: "just testing a a list of objects (doesn't actually do ingress things)" diff --git a/src/test/bundles/07-helm-overrides/values-file/values.yaml b/src/test/bundles/07-helm-overrides/values-file/values.yaml new file mode 100644 index 00000000..8905cc5b --- /dev/null +++ b/src/test/bundles/07-helm-overrides/values-file/values.yaml @@ -0,0 +1,12 @@ +podinfo.replicaCount: 3 +podinfo.tolerations: + - key: "unicorn" + operator: "Equal" + value: "defense" + effect: "NoSchedule" + - key: "uds" + operator: "Equal" + value: "true" + effect: "NoSchedule" +podinfo.podAnnotations: + customAnnotation: "customValue" diff --git a/src/test/bundles/07-helm-overrides/values-file/values2.yaml b/src/test/bundles/07-helm-overrides/values-file/values2.yaml new file mode 100644 index 00000000..e6e8d4fd --- /dev/null +++ b/src/test/bundles/07-helm-overrides/values-file/values2.yaml @@ -0,0 +1,3 @@ +podinfo.replicaCount: 4 +podinfo.podAnnotations: + customAnnotation: "customValue2" diff --git a/src/test/e2e/variable_test.go b/src/test/e2e/variable_test.go index 69e8466e..f79772ec 100644 --- a/src/test/e2e/variable_test.go +++ b/src/test/e2e/variable_test.go @@ -157,6 +157,45 @@ func TestBundleWithHelmOverrides(t *testing.T) { remove(t, bundlePath) } +func TestBundleWithHelmOverridesValuesFile(t *testing.T) { + deployZarfInit(t) + e2e.HelmDepUpdate(t, "src/test/packages/helm/unicorn-podinfo") + e2e.CreateZarfPkg(t, "src/test/packages/helm", false) + bundleDir := "src/test/bundles/07-helm-overrides/values-file" + bundlePath := filepath.Join(bundleDir, fmt.Sprintf("uds-bundle-helm-values-file-%s-0.0.1.tar.zst", e2e.Arch)) + err := os.Setenv("UDS_CONFIG", filepath.Join("src/test/bundles/07-helm-overrides", "uds-config.yaml")) + require.NoError(t, err) + + createLocal(t, bundleDir, e2e.Arch) + deploy(t, bundlePath) + + // test values overrides + t.Run("check values overrides", func(t *testing.T) { + cmd := strings.Split("zarf tools kubectl get deploy -n podinfo unicorn-podinfo -o=jsonpath='{.spec.replicas}'", " ") + outputNumReplicas, _, err := e2e.UDS(cmd...) + require.Equal(t, "'2'", outputNumReplicas) + require.NoError(t, err) + }) + + t.Run("check object-type override in values", func(t *testing.T) { + 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\":\"customValue2\"") + require.NoError(t, err) + }) + + t.Run("check list-type override in values", func(t *testing.T) { + 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\":\"defense\"") + require.Contains(t, tolerations, "\"key\":\"unicorn\"") + require.Contains(t, tolerations, "\"effect\":\"NoSchedule\"") + require.NoError(t, err) + + }) +} + func TestBundleWithDupPkgs(t *testing.T) { deployZarfInit(t) e2e.SetupDockerRegistry(t, 888) diff --git a/src/types/bundle.go b/src/types/bundle.go index 3e7ef9e8..8ff296c6 100644 --- a/src/types/bundle.go +++ b/src/types/bundle.go @@ -28,27 +28,17 @@ type Package struct { // BundleChartOverrides represents a Helm chart override to set via UDS variables type BundleChartOverrides struct { - Values []BundleChartValue `json:"values,omitempty" jsonschema:"description=List of Helm chart values to set statically"` - Variables []BundleChartVariable `json:"variables,omitempty" jsonschema:"description=List of Helm chart variables to set via UDS variables"` - Namespace string `json:"namespace,omitempty" jsonschema:"description=The namespace to deploy the Helm chart to"` - - // EXPERIMENTAL, not yet implemented - //ValueFiles []BundleChartValueFile `json:"value-files,omitempty" jsonschema:"description=List of Helm chart value files to set statically"` + Values []BundleChartValue `json:"values,omitempty" jsonschema:"description=List of Helm chart values to set statically"` + Variables []BundleChartVariable `json:"variables,omitempty" jsonschema:"description=List of Helm chart variables to set via UDS variables"` + Namespace string `json:"namespace,omitempty" jsonschema:"description=The namespace to deploy the Helm chart to"` + ValuesFiles []string `json:"valuesFiles,omitempty" jsonschema:"description=List of Helm chart value file paths to set statically"` } -// BundleChartValue represents a Helm chart value to path mapping to set via UDS variables 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"` } -// BundleChartValueFile - EXPERIMENTAL - represents a Helm chart value file to override -type BundleChartValueFile struct { - Path string `json:"path" jsonschema:"name=Path to the Helm chart to set. The format is /, example=my-component/my-cool-chart"` - File string `json:"file" jsonschema:"name=The path to the values file to add to the Helm chart"` -} - -// BundleChartVariable - EXPERIMENTAL - represents a Helm chart variable and its path 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"` diff --git a/uds.schema.json b/uds.schema.json index ac1e39f9..1030a301 100644 --- a/uds.schema.json +++ b/uds.schema.json @@ -23,6 +23,13 @@ "namespace": { "type": "string", "description": "The namespace to deploy the Helm chart to" + }, + "valuesFiles": { + "items": { + "type": "string" + }, + "type": "array", + "description": "List of Helm chart value file paths to set statically" } }, "additionalProperties": false,