From 8fba90613d8b119d07e395feea608a2a1f5877d8 Mon Sep 17 00:00:00 2001 From: unclegedd Date: Fri, 29 Mar 2024 14:53:55 -0500 Subject: [PATCH] feat: enables setting namespaces in bundled Helm charts --- docs/overrides.md | 23 +++++++++++ src/pkg/bundle/deploy.go | 40 +++++++++++++------ src/pkg/bundle/remove.go | 2 +- src/pkg/sources/common.go | 23 +++++++++++ src/pkg/sources/new.go | 4 +- src/pkg/sources/remote.go | 2 + src/pkg/sources/tarball.go | 5 +++ .../namespace/uds-bundle.yaml | 17 ++++++++ src/test/e2e/bundle_test.go | 2 +- src/test/e2e/commands_test.go | 8 +++- src/test/e2e/variable_test.go | 35 ++++++++++++++++ src/types/bundle.go | 1 + uds.schema.json | 4 ++ 13 files changed, 149 insertions(+), 17 deletions(-) create mode 100644 src/pkg/sources/common.go create mode 100644 src/test/bundles/07-helm-overrides/namespace/uds-bundle.yaml diff --git a/docs/overrides.md b/docs/overrides.md index a7ec018d..502b8df3 100644 --- a/docs/overrides.md +++ b/docs/overrides.md @@ -9,6 +9,7 @@ Bundle overrides provide a mechanism to customize Helm charts inside of Zarf pac - [Syntax](#syntax) - [Values](#values) - [Variables](#variables) + - [Namespace](#namespace) ## Quickstart @@ -214,3 +215,25 @@ Variable precedence is as follows: 1. Environment variables 1. `uds-config.yaml` variables 1. Variables `default` in the`uds-bundle.yaml` + +### 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: + +```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: + namespace: custom-podinfo # custom namespace! + values: + - path: "podinfo.replicaCount" + value: 1 +``` diff --git a/src/pkg/bundle/deploy.go b/src/pkg/bundle/deploy.go index 64187b5a..e6bed04e 100644 --- a/src/pkg/bundle/deploy.go +++ b/src/pkg/bundle/deploy.go @@ -29,8 +29,8 @@ import ( "helm.sh/helm/v3/pkg/getter" ) -// ZarfOverrideMap is a map of Zarf packages -> components -> Helm charts -> values -type ZarfOverrideMap map[string]map[string]map[string]interface{} +// PkgOverrideMap is a map of Zarf packages -> components -> Helm charts -> values/namespace +type PkgOverrideMap map[string]map[string]map[string]interface{} // templatedVarRegex is the regex for templated variables var templatedVarRegex = regexp.MustCompile(`\${([^}]+)}`) @@ -109,7 +109,7 @@ func deployPackages(packages []types.Package, resume bool, b *Bundle) error { Retries: b.cfg.DeployOpts.Retries, } - valuesOverrides, err := b.loadChartOverrides(pkg, pkgVars) + valuesOverrides, nsOverrides, err := b.loadChartOverrides(pkg, pkgVars) if err != nil { return err } @@ -128,7 +128,7 @@ func deployPackages(packages []types.Package, resume bool, b *Bundle) error { // Automatically confirm the package deployment zarfConfig.CommonOptions.Confirm = true - source, err := sources.New(b.cfg.DeployOpts.Source, b.cfg.DeployOpts.ZarfPackageNameMap[pkg.Name], opts, sha) + source, err := sources.New(b.cfg.DeployOpts.Source, b.cfg.DeployOpts.ZarfPackageNameMap[pkg.Name], opts, sha, nsOverrides) if err != nil { return err } @@ -231,10 +231,11 @@ func (b *Bundle) ConfirmBundleDeploy() (confirm bool) { } // loadChartOverrides converts a helm path to a ValuesOverridesMap config for Zarf -func (b *Bundle) loadChartOverrides(pkg types.Package, pkgVars map[string]string) (ZarfOverrideMap, error) { +func (b *Bundle) loadChartOverrides(pkg types.Package, pkgVars map[string]string) (PkgOverrideMap, sources.NamespaceOverrideMap, error) { - // Create a nested map to hold the values + // Create nested maps to hold the overrides overrideMap := make(map[string]map[string]*values.Options) + nsOverrides := make(sources.NamespaceOverrideMap) // Loop through each package component's charts and process overrides for componentName, component := range pkg.Overrides { @@ -242,18 +243,19 @@ func (b *Bundle) loadChartOverrides(pkg types.Package, pkgVars map[string]string chartCopy := chart // Create a copy of the chart err := b.processOverrideValues(&overrideMap, &chartCopy.Values, componentName, chartName, pkgVars) if err != nil { - return nil, err + return nil, nil, err } err = b.processOverrideVariables(&overrideMap, pkg.Name, &chartCopy.Variables, componentName, chartName) if err != nil { - return nil, err + return nil, nil, err } + b.processOverrideNamespaces(nsOverrides, chartCopy.Namespace, componentName, chartName) } } - processed := make(ZarfOverrideMap) + processed := make(PkgOverrideMap) - // Convert the options.Values map to the ZarfOverrideMap format + // Convert the options.Values map (located in chart.MergeValues) to the PkgOverrideMap format for componentName, component := range overrideMap { // Create a map to hold all the charts in the component componentMap := make(map[string]map[string]interface{}) @@ -267,7 +269,7 @@ func (b *Bundle) loadChartOverrides(pkg types.Package, pkgVars map[string]string // Merge the chart values with Helm data, err := chart.MergeValues(getter.Providers{}) if err != nil { - return nil, err + return nil, nil, err } // Add the chart values to the component map @@ -278,7 +280,7 @@ func (b *Bundle) loadChartOverrides(pkg types.Package, pkgVars map[string]string processed[componentName] = componentMap } - return processed, nil + return processed, nsOverrides, nil } // PreDeployValidation validates the bundle before deployment @@ -336,6 +338,18 @@ func (b *Bundle) PreDeployValidation() (string, string, string, error) { return bundleName, string(bundleYAML), source, err } +// processOverrideNamespaces processes a bundles namespace overrides and adds them to the override map +func (b *Bundle) processOverrideNamespaces(overrideMap sources.NamespaceOverrideMap, ns string, componentName string, chartName string) { + if ns == "" { + return // no namespace override + } + // check if component exists in override map + if _, ok := overrideMap[componentName]; !ok { + overrideMap[componentName] = make(map[string]string) + } + overrideMap[componentName][chartName] = ns +} + // 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, pkgVars map[string]string) error { for _, v := range *values { @@ -394,7 +408,7 @@ func (b *Bundle) processOverrideVariables(overrideMap *map[string]map[string]*va return nil } -// addOverrideValue adds a value to a ZarfOverrideMap +// 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 { // Create the component map if it doesn't exist if _, ok := overrides[component]; !ok { diff --git a/src/pkg/bundle/remove.go b/src/pkg/bundle/remove.go index 2dd6d126..04cfcf87 100644 --- a/src/pkg/bundle/remove.go +++ b/src/pkg/bundle/remove.go @@ -104,7 +104,7 @@ func removePackages(packagesToRemove []types.Package, b *Bundle, zarfPackageName } sha := strings.Split(pkg.Ref, "sha256:")[1] - source, err := sources.New(b.cfg.RemoveOpts.Source, zarfPackageName, opts, sha) + source, err := sources.New(b.cfg.RemoveOpts.Source, zarfPackageName, opts, sha, nil) if err != nil { return err } diff --git a/src/pkg/sources/common.go b/src/pkg/sources/common.go new file mode 100644 index 00000000..21ee1e04 --- /dev/null +++ b/src/pkg/sources/common.go @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023-Present The UDS Authors + +// Package sources contains Zarf packager sources +package sources + +import zarfTypes "github.com/defenseunicorns/zarf/src/types" + +// addNamespaceOverrides checks if pkg components have charts with namespace overrides and adds them +func addNamespaceOverrides(pkg *zarfTypes.ZarfPackage, nsOverrides NamespaceOverrideMap) { + if len(nsOverrides) == 0 { + return + } + for i, comp := range pkg.Components { + if _, exists := nsOverrides[comp.Name]; exists { + for j, chart := range comp.Charts { + if _, exists = nsOverrides[comp.Name][chart.Name]; exists { + pkg.Components[i].Charts[j].Namespace = nsOverrides[comp.Name][comp.Charts[j].Name] + } + } + } + } +} diff --git a/src/pkg/sources/new.go b/src/pkg/sources/new.go index f65730a6..f89d0820 100644 --- a/src/pkg/sources/new.go +++ b/src/pkg/sources/new.go @@ -16,7 +16,7 @@ import ( ) // New creates a new package source based on pkgLocation -func New(pkgLocation string, pkgName string, opts zarfTypes.ZarfPackageOptions, sha string) (zarfSources.PackageSource, error) { +func New(pkgLocation string, pkgName string, opts zarfTypes.ZarfPackageOptions, sha string, nsOverrides NamespaceOverrideMap) (zarfSources.PackageSource, error) { var source zarfSources.PackageSource if strings.Contains(pkgLocation, "tar.zst") { source = &TarballBundle{ @@ -25,6 +25,7 @@ func New(pkgLocation string, pkgName string, opts zarfTypes.ZarfPackageOptions, PkgManifestSHA: sha, TmpDir: opts.PackageSource, BundleLocation: pkgLocation, + nsOverrides: nsOverrides, } } else { platform := ocispec.Platform{ @@ -41,6 +42,7 @@ func New(pkgLocation string, pkgName string, opts zarfTypes.ZarfPackageOptions, PkgManifestSHA: sha, TmpDir: opts.PackageSource, Remote: remote.OrasRemote, + nsOverrides: nsOverrides, } } return source, nil diff --git a/src/pkg/sources/remote.go b/src/pkg/sources/remote.go index 727903b7..14fb050c 100644 --- a/src/pkg/sources/remote.go +++ b/src/pkg/sources/remote.go @@ -37,6 +37,7 @@ type RemoteBundle struct { TmpDir string Remote *oci.OrasRemote isPartial bool + nsOverrides NamespaceOverrideMap } // LoadPackage loads a Zarf package from a remote bundle @@ -88,6 +89,7 @@ func (r *RemoteBundle) LoadPackage(dst *layout.PackagePaths, filter filters.Comp } } } + addNamespaceOverrides(&pkg, r.nsOverrides) return pkg, nil, err } diff --git a/src/pkg/sources/tarball.go b/src/pkg/sources/tarball.go index 6a7ad0ef..83507e76 100644 --- a/src/pkg/sources/tarball.go +++ b/src/pkg/sources/tarball.go @@ -28,6 +28,9 @@ import ( "github.com/defenseunicorns/uds-cli/src/pkg/utils" ) +// NamespaceOverrideMap is a map of component names to a map of chart names to namespace overrides +type NamespaceOverrideMap = map[string]map[string]string + // TarballBundle is a package source for local tarball bundles that implements Zarf's packager.PackageSource type TarballBundle struct { PkgOpts *zarfTypes.ZarfPackageOptions @@ -36,6 +39,7 @@ type TarballBundle struct { BundleLocation string PkgName string isPartial bool + nsOverrides NamespaceOverrideMap } // LoadPackage loads a Zarf package from a local tarball bundle @@ -88,6 +92,7 @@ func (t *TarballBundle) LoadPackage(dst *layout.PackagePaths, filter filters.Com } } } + addNamespaceOverrides(&pkg, t.nsOverrides) packageSpinner.Successf("Loaded bundled Zarf package: %s", t.PkgName) return pkg, nil, err } diff --git a/src/test/bundles/07-helm-overrides/namespace/uds-bundle.yaml b/src/test/bundles/07-helm-overrides/namespace/uds-bundle.yaml new file mode 100644 index 00000000..2daeaf59 --- /dev/null +++ b/src/test/bundles/07-helm-overrides/namespace/uds-bundle.yaml @@ -0,0 +1,17 @@ +kind: UDSBundle +metadata: + name: override-namespace + description: testing a bundle with a specified namespace + version: 0.0.1 + +packages: + - name: helm-overrides + path: "../../../packages/helm" + ref: 0.0.1 + overrides: + podinfo-component: + unicorn-podinfo: + namespace: "override-namespace" + values: + - path: "podinfo.replicaCount" + value: 1 diff --git a/src/test/e2e/bundle_test.go b/src/test/e2e/bundle_test.go index aa6bf7c4..d17c94c3 100644 --- a/src/test/e2e/bundle_test.go +++ b/src/test/e2e/bundle_test.go @@ -110,7 +110,7 @@ func TestBundleWithLocalAndRemotePkgs(t *testing.T) { publishInsecure(t, bundlePath, bundleRef.Registry) pull(t, bundleRef.String(), tarballPath) // note that pull pulls the bundle into the build dir publishInsecure(t, filepath.Join("build", filepath.Base(bundlePath)), "oci://localhost:889") - deployAndRemoveRemoteInsecure(t, bundleRef.String()) + deployInsecure(t, bundleRef.String()) }) } diff --git a/src/test/e2e/commands_test.go b/src/test/e2e/commands_test.go index 336da1ba..a89e99bb 100644 --- a/src/test/e2e/commands_test.go +++ b/src/test/e2e/commands_test.go @@ -143,12 +143,18 @@ func removePackagesFlag(tarballPath string, packages string) (stdout string, std return stdout, stderr } -func deployAndRemoveRemoteInsecure(t *testing.T, ref string) { +func deployInsecure(t *testing.T, ref string) { cmd := strings.Split(fmt.Sprintf("deploy %s --insecure --confirm --no-tea", ref), " ") _, _, err := e2e.UDS(cmd...) require.NoError(t, err) } +func removeInsecure(t *testing.T, remote string) { + cmd := strings.Split(fmt.Sprintf("remove %s --insecure --confirm", remote), " ") + _, _, err := e2e.UDS(cmd...) + require.NoError(t, err) +} + func deployAndRemoveLocalAndRemoteInsecure(t *testing.T, ref string, tarballPath string) { var cmd []string // test both paths because we want to test that the pulled tarball works as well diff --git a/src/test/e2e/variable_test.go b/src/test/e2e/variable_test.go index 6a82aa8d..f0626ac7 100644 --- a/src/test/e2e/variable_test.go +++ b/src/test/e2e/variable_test.go @@ -157,6 +157,41 @@ func TestBundleWithHelmOverrides(t *testing.T) { remove(t, bundlePath) } +func TestBundleWithOverridenNamespace(t *testing.T) { + deployZarfInit(t) + e2e.HelmDepUpdate(t, "src/test/packages/helm/unicorn-podinfo") + e2e.CreateZarfPkg(t, "src/test/packages/helm", false) + name := "override-namespace" + bundleDir := "src/test/bundles/07-helm-overrides/namespace" + bundlePath := filepath.Join(bundleDir, fmt.Sprintf("uds-bundle-%s-%s-0.0.1.tar.zst", name, e2e.Arch)) + e2e.SetupDockerRegistry(t, 888) + defer e2e.TeardownRegistry(t, 888) + // remove namespace after tests + defer func() { + cmd := strings.Split("zarf tools kubectl delete ns override-namespace", " ") + _, _, _ = e2e.UDS(cmd...) + }() + + createLocal(t, bundleDir, e2e.Arch) + + t.Run("test namespace override in local bundle", func(t *testing.T) { + deploy(t, bundlePath) + cmd := strings.Split("zarf tools kubectl get deploy -n override-namespace unicorn-podinfo -o=jsonpath='{.metadata.name}'", " ") + deployments, _, _ := e2e.UDS(cmd...) + require.Contains(t, deployments, "unicorn-podinfo") + remove(t, bundlePath) + }) + + t.Run("test namespace override in remote bundle", func(t *testing.T) { + publishInsecure(t, bundlePath, "localhost:888") + deployInsecure(t, fmt.Sprintf("localhost:888/%s:0.0.1", name)) + cmd := strings.Split("zarf tools kubectl get deploy -n override-namespace unicorn-podinfo -o=jsonpath='{.metadata.name}'", " ") + deployments, _, _ := e2e.UDS(cmd...) + require.Contains(t, deployments, "unicorn-podinfo") + removeInsecure(t, fmt.Sprintf("localhost:888/%s:0.0.1", name)) + }) +} + func TestBundleWithEnvVarHelmOverrides(t *testing.T) { // set up configs and env vars deployZarfInit(t) diff --git a/src/types/bundle.go b/src/types/bundle.go index f9413f64..1a6ad320 100644 --- a/src/types/bundle.go +++ b/src/types/bundle.go @@ -29,6 +29,7 @@ type Package struct { 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"` diff --git a/uds.schema.json b/uds.schema.json index a1628ea6..fbef1ff4 100644 --- a/uds.schema.json +++ b/uds.schema.json @@ -19,6 +19,10 @@ }, "type": "array", "description": "List of Helm chart variables to set via UDS variables" + }, + "namespace": { + "type": "string", + "description": "The namespace to deploy the Helm chart to" } }, "additionalProperties": false,