Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enables setting namespaces in bundled Helm charts #539

Merged
merged 1 commit into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/overrides.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
```
40 changes: 27 additions & 13 deletions src/pkg/bundle/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(`\${([^}]+)}`)
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -231,29 +231,31 @@ 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 {
for chartName, chart := range component {
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{})
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/pkg/bundle/remove.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
23 changes: 23 additions & 0 deletions src/pkg/sources/common.go
Original file line number Diff line number Diff line change
@@ -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]
}
}
}
}
}
4 changes: 3 additions & 1 deletion src/pkg/sources/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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{
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/pkg/sources/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -88,6 +89,7 @@ func (r *RemoteBundle) LoadPackage(dst *layout.PackagePaths, filter filters.Comp
}
}
}
addNamespaceOverrides(&pkg, r.nsOverrides)
return pkg, nil, err
}

Expand Down
5 changes: 5 additions & 0 deletions src/pkg/sources/tarball.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
17 changes: 17 additions & 0 deletions src/test/bundles/07-helm-overrides/namespace/uds-bundle.yaml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion src/test/e2e/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
})
}

Expand Down
8 changes: 7 additions & 1 deletion src/test/e2e/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions src/test/e2e/variable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/types/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
4 changes: 4 additions & 0 deletions uds.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading