Skip to content

Commit

Permalink
Add templateValues to HelmApp, Bundle, etc
Browse files Browse the repository at this point in the history
Signed-off-by: Danil-Grigorev <[email protected]>
  • Loading branch information
Danil-Grigorev committed Jan 30, 2025
1 parent 9a7eeb7 commit 126b010
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 5 deletions.
108 changes: 108 additions & 0 deletions charts/fleet-crd/templates/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,24 @@ spec:
description: TakeOwnership makes helm skip the check for
its own annotations
type: boolean
templateValues:
additionalProperties:
type: string
description: 'Template Values passed to Helm. It is possible
to specify the keys and values
as go template strings. Unlike .values, content of each
key will be templated
first, before serializing to yaml. This allows to template
complex values,
like ranges and maps.
templateValues keys have precedence over values keys in
case of conflict.'
nullable: true
type: object
timeoutSeconds:
description: TimeoutSeconds is the time to wait for Helm
operations.
Expand Down Expand Up @@ -695,6 +713,24 @@ spec:
description: TakeOwnership makes helm skip the check for
its own annotations
type: boolean
templateValues:
additionalProperties:
type: string
description: 'Template Values passed to Helm. It is possible
to specify the keys and values
as go template strings. Unlike .values, content of each
key will be templated
first, before serializing to yaml. This allows to template
complex values,
like ranges and maps.
templateValues keys have precedence over values keys in
case of conflict.'
nullable: true
type: object
timeoutSeconds:
description: TimeoutSeconds is the time to wait for Helm
operations.
Expand Down Expand Up @@ -1568,6 +1604,24 @@ spec:
description: TakeOwnership makes helm skip the check for its
own annotations
type: boolean
templateValues:
additionalProperties:
type: string
description: 'Template Values passed to Helm. It is possible
to specify the keys and values
as go template strings. Unlike .values, content of each key
will be templated
first, before serializing to yaml. This allows to template
complex values,
like ranges and maps.
templateValues keys have precedence over values keys in case
of conflict.'
nullable: true
type: object
timeoutSeconds:
description: TimeoutSeconds is the time to wait for Helm operations.
type: integer
Expand Down Expand Up @@ -2433,6 +2487,24 @@ spec:
description: TakeOwnership makes helm skip the check for
its own annotations
type: boolean
templateValues:
additionalProperties:
type: string
description: 'Template Values passed to Helm. It is possible
to specify the keys and values
as go template strings. Unlike .values, content of each
key will be templated
first, before serializing to yaml. This allows to template
complex values,
like ranges and maps.
templateValues keys have precedence over values keys
in case of conflict.'
nullable: true
type: object
timeoutSeconds:
description: TimeoutSeconds is the time to wait for Helm
operations.
Expand Down Expand Up @@ -7307,6 +7379,24 @@ spec:
description: TakeOwnership makes helm skip the check for its
own annotations
type: boolean
templateValues:
additionalProperties:
type: string
description: 'Template Values passed to Helm. It is possible
to specify the keys and values
as go template strings. Unlike .values, content of each key
will be templated
first, before serializing to yaml. This allows to template
complex values,
like ranges and maps.
templateValues keys have precedence over values keys in case
of conflict.'
nullable: true
type: object
timeoutSeconds:
description: TimeoutSeconds is the time to wait for Helm operations.
type: integer
Expand Down Expand Up @@ -8191,6 +8281,24 @@ spec:
description: TakeOwnership makes helm skip the check for
its own annotations
type: boolean
templateValues:
additionalProperties:
type: string
description: 'Template Values passed to Helm. It is possible
to specify the keys and values
as go template strings. Unlike .values, content of each
key will be templated
first, before serializing to yaml. This allows to template
complex values,
like ranges and maps.
templateValues keys have precedence over values keys
in case of conflict.'
nullable: true
type: object
timeoutSeconds:
description: TimeoutSeconds is the time to wait for Helm
operations.
Expand Down
6 changes: 6 additions & 0 deletions internal/cmd/controller/options/calculate.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"maps"

fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
"github.com/rancher/wrangler/v3/pkg/data"
Expand Down Expand Up @@ -52,6 +53,11 @@ func Merge(base, custom fleet.BundleDeploymentOptions) fleet.BundleDeploymentOpt
} else if custom.Helm.Values != nil {
result.Helm.Values.Data = data.MergeMaps(result.Helm.Values.Data, custom.Helm.Values.Data)
}
if result.Helm.TemplateValues == nil {
result.Helm.TemplateValues = custom.Helm.TemplateValues
} else if custom.Helm.TemplateValues != nil {
maps.Copy(result.Helm.TemplateValues, custom.Helm.TemplateValues)
}
if custom.Helm.ValuesFrom != nil {
result.Helm.ValuesFrom = append(result.Helm.ValuesFrom, custom.Helm.ValuesFrom...)
}
Expand Down
55 changes: 51 additions & 4 deletions internal/cmd/controller/target/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package target

import (
"bytes"
"cmp"
"context"
"fmt"
"maps"
"sort"
"strings"
"text/template"
Expand Down Expand Up @@ -182,10 +184,15 @@ func preprocessHelmValues(logger logr.Logger, opts *fleet.BundleDeploymentOption
}

opts.Helm = opts.Helm.DeepCopy()
if opts.Helm.Values == nil || opts.Helm.Values.Data == nil {
opts.Helm.Values = &fleet.GenericMap{
Data: map[string]interface{}{},
}
opts.Helm.Values = cmp.Or(opts.Helm.Values, &fleet.GenericMap{
Data: map[string]interface{}{},
})

if opts.Helm.Values.Data == nil {
opts.Helm.Values.Data = map[string]interface{}{}
}

if opts.Helm.TemplateValues == nil && len(opts.Helm.Values.Data) == 0 {
return nil
}

Expand All @@ -211,6 +218,14 @@ func preprocessHelmValues(logger logr.Logger, opts *fleet.BundleDeploymentOption
if err != nil {
return err
}

templatedData, err := processTemplateValuesData(opts.Helm.TemplateValues, values)
if err != nil {
return err
}

maps.Copy(opts.Helm.Values.Data, templatedData)

logger.V(4).Info("preProcess completed", "releaseName", opts.Helm.ReleaseName)
}

Expand Down Expand Up @@ -278,6 +293,38 @@ func tplFuncMap() template.FuncMap {
return f
}

func processTemplateValuesData(helmTemplateData map[string]string, templateContext map[string]interface{}) (map[string]interface{}, error) {
renderedValues := make(map[string]interface{}, len(helmTemplateData))

for k, v := range helmTemplateData {
// fleet.yaml must be valid yaml, however '{}[]' are YAML control
// characters and will be interpreted as JSON data structures. This
// causes issues when parsing the fleet.yaml so we change the delims
// for templating to '${ }'
tmpl := template.New("values").Funcs(tplFuncMap()).Option("missingkey=error").Delims("${", "}")
tmpl, err := tmpl.Parse(v)
if err != nil {
return nil, fmt.Errorf("failed to parse helm values template: %w", err)
}

var b bytes.Buffer
err = tmpl.Execute(&b, templateContext)
if err != nil {
return nil, fmt.Errorf("failed to render helm values template: %w", err)
}

var value interface{}
err = kyaml.Unmarshal(b.Bytes(), &value)
if err != nil {
return nil, fmt.Errorf("failed to interpret rendered template as helm values: %s, %v", b.String(), err)
}

renderedValues[k] = value
}

return renderedValues, nil
}

func processTemplateValues(helmValues map[string]interface{}, templateContext map[string]interface{}) (map[string]interface{}, error) {
data, err := kyaml.Marshal(helmValues)
if err != nil {
Expand Down
81 changes: 80 additions & 1 deletion internal/cmd/controller/target/target_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,17 @@ func TestProcessLabelValues(t *testing.T) {
const bundleYamlWithTemplate = `namespace: default
helm:
releaseName: labels
templateValues:
mapData: |
${- range $key := .ClusterValues.items }
"${ $key }":
nested: "true"
${- end}
listData: |
${- range $key := .ClusterValues.items }
- "${ $key }":
nested: "true"
${- end}
values:
clusterName: "${ .ClusterLabels.name }"
fromAnnotation: "${ .ClusterAnnotations.testAnnotation }"
Expand Down Expand Up @@ -154,6 +165,10 @@ func TestProcessTemplateValues(t *testing.T) {
"thirdTier": "bar",
},
},
"items": []string{
"one",
"two",
},
"list": []string{
"alpha",
"beta",
Expand Down Expand Up @@ -187,7 +202,7 @@ func TestProcessTemplateValues(t *testing.T) {

templatedValues, err := processTemplateValues(bundle.Helm.Values.Data, values)
if err != nil {
t.Fatalf("error during label processing %v", err)
t.Fatalf("error during templated values processing %v", err)
}

clusterName, ok := templatedValues["clusterName"]
Expand Down Expand Up @@ -314,6 +329,70 @@ func TestProcessTemplateValues(t *testing.T) {
t.Fatal("join func was not right")
}

templatedValuesData, err := processTemplateValuesData(bundle.Helm.TemplateValues, values)
if err != nil {
t.Fatalf("error during label processing %v", err)
}

mapData, ok := templatedValuesData["mapData"].(map[string]interface{})
if !ok {
t.Fatal("mapData not found")
}

one, ok := mapData["one"]
if !ok {
t.Fatal("unable to find key one")
}

oneData, ok := one.(map[string]interface{})
if !ok {
t.Fatal("one key was not right")
}

if oneData["nested"].(string) != "true" {
t.Fatal("one value was not right")
}

two, ok := mapData["two"]
if !ok {
t.Fatal("unable to find key two")
}

twoData, ok := two.(map[string]interface{})
if !ok {
t.Fatal("two key was not right")
}

if twoData["nested"].(string) != "true" {
t.Fatal("two value was not right")
}

listData, ok := templatedValuesData["listData"].([]interface{})
if !ok {
t.Fatal("listData not found")
}

if len(listData) != 2 {
t.Fatal("unable to find all listData keys")
}

oneListData, ok := listData[0].(map[string]interface{})
if !ok {
t.Fatal("oneListData key is not right")
}

if oneListData["nested"] != "true" {
t.Fatal("oneListData item is missing")
}

twoListData, ok := listData[1].(map[string]interface{})
if !ok {
t.Fatal("twoListData key is not right")
}

if twoListData["nested"] != "true" {
t.Fatal("twoListData item is missing")
}
}

const clusterYamlWithTemplateValues = `apiVersion: fleet.cattle.io/v1alpha1
Expand Down
8 changes: 8 additions & 0 deletions pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,14 @@ type HelmOptions struct {
// +kubebuilder:validation:XPreserveUnknownFields
Values *GenericMap `json:"values,omitempty"`

// Template Values passed to Helm. It is possible to specify the keys and values
// as go template strings. Unlike .values, content of each key will be templated
// first, before serializing to yaml. This allows to template complex values,
// like ranges and maps.
// templateValues keys have precedence over values keys in case of conflict.
// +nullable
TemplateValues map[string]string `json:"templateValues,omitempty"`

// +nullable
// ValuesFrom loads the values from configmaps and secrets.
ValuesFrom []ValuesFrom `json:"valuesFrom,omitempty"`
Expand Down
7 changes: 7 additions & 0 deletions pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 126b010

Please sign in to comment.