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

✨ add VersionTemplate support for HelmChartProxy #292

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions api/v1alpha1/helmchartproxy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ type HelmChartProxySpec struct {
// +optional
Version string `json:"version,omitempty"`

// VersionTemplate is an inline Go template representing the version for the Helm chart. This template supports Go templating
// to reference fields from each selected workload Cluster and programatically create and set the version.
// If the Version is specified, VersionTemplate will take precedence.
// +optional
VersionTemplate string `json:"versionTemplate,omitempty"`

// ValuesTemplate is an inline YAML representing the values for the Helm chart. This YAML supports Go templating to reference
// fields from each selected workload Cluster and programatically create and set values.
// +optional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,12 @@ spec:
Version is the version of the Helm chart. If it is not specified, the chart will use
and be kept up to date with the latest version.
type: string
versionTemplate:
description: |-
VersionTemplate is an inline Go template representing the version for the Helm chart. This template supports Go templating
to reference fields from each selected workload Cluster and programatically create and set the version.
If the Version is specified, VersionTemplate will take precedence.
type: string
required:
- chartName
- clusterSelector
Expand Down
31 changes: 26 additions & 5 deletions controllers/helmchartproxy/helmchartproxy_controller_phases.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package helmchartproxy
import (
"context"
"fmt"
"strings"

"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
Expand Down Expand Up @@ -95,7 +96,18 @@ func (r *HelmChartProxyReconciler) reconcileForCluster(ctx context.Context, helm
}

log.V(2).Info("Values for cluster", "cluster", cluster.Name, "values", values)
if err := r.createOrUpdateHelmReleaseProxy(ctx, existingHelmReleaseProxy, helmChartProxy, &cluster, values); err != nil {

version := helmChartProxy.Spec.Version
if helmChartProxy.Spec.VersionTemplate != "" {
version, err = internal.ParseVersion(ctx, r.Client, helmChartProxy.Spec, &cluster)
if err != nil {
conditions.MarkFalse(helmChartProxy, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition, addonsv1alpha1.ValueParsingFailedReason, clusterv1.ConditionSeverityError, err.Error())

return errors.Wrapf(err, "failed to parse version on cluster %s", cluster.Name)
}
}

if err := r.createOrUpdateHelmReleaseProxy(ctx, existingHelmReleaseProxy, helmChartProxy, &cluster, values, version); err != nil {
conditions.MarkFalse(helmChartProxy, addonsv1alpha1.HelmReleaseProxySpecsUpToDateCondition, addonsv1alpha1.HelmReleaseProxyCreationFailedReason, clusterv1.ConditionSeverityError, err.Error())

return errors.Wrapf(err, "failed to create or update HelmReleaseProxy on cluster %s", cluster.Name)
Expand Down Expand Up @@ -139,9 +151,9 @@ func (r *HelmChartProxyReconciler) getExistingHelmReleaseProxy(ctx context.Conte
}

// createOrUpdateHelmReleaseProxy creates or updates the HelmReleaseProxy for the given cluster.
func (r *HelmChartProxyReconciler) createOrUpdateHelmReleaseProxy(ctx context.Context, existing *addonsv1alpha1.HelmReleaseProxy, helmChartProxy *addonsv1alpha1.HelmChartProxy, cluster *clusterv1.Cluster, parsedValues string) error {
func (r *HelmChartProxyReconciler) createOrUpdateHelmReleaseProxy(ctx context.Context, existing *addonsv1alpha1.HelmReleaseProxy, helmChartProxy *addonsv1alpha1.HelmChartProxy, cluster *clusterv1.Cluster, parsedValues, parsedVersion string) error {
log := ctrl.LoggerFrom(ctx)
helmReleaseProxy := constructHelmReleaseProxy(existing, helmChartProxy, parsedValues, cluster)
helmReleaseProxy := constructHelmReleaseProxy(existing, helmChartProxy, parsedValues, parsedVersion, cluster)
if helmReleaseProxy == nil {
log.V(2).Info("HelmReleaseProxy is up to date, nothing to do", "helmReleaseProxy", existing.Name, "cluster", cluster.Name)
return nil
Expand Down Expand Up @@ -178,8 +190,14 @@ func (r *HelmChartProxyReconciler) deleteHelmReleaseProxy(ctx context.Context, h

// constructHelmReleaseProxy constructs a new HelmReleaseProxy for the given Cluster or updates the existing HelmReleaseProxy if needed.
// If no update is needed, this returns nil. Note that this does not check if we need to reinstall the HelmReleaseProxy, i.e. immutable fields changed.
func constructHelmReleaseProxy(existing *addonsv1alpha1.HelmReleaseProxy, helmChartProxy *addonsv1alpha1.HelmChartProxy, parsedValues string, cluster *clusterv1.Cluster) *addonsv1alpha1.HelmReleaseProxy {
func constructHelmReleaseProxy(existing *addonsv1alpha1.HelmReleaseProxy, helmChartProxy *addonsv1alpha1.HelmChartProxy, parsedValues, parsedVersion string, cluster *clusterv1.Cluster) *addonsv1alpha1.HelmReleaseProxy {
helmReleaseProxy := &addonsv1alpha1.HelmReleaseProxy{}

// If it's not set, then use the as-is version to make tests happy without breaking the logic.
if parsedVersion == "" {
parsedVersion = helmChartProxy.Spec.Version
}

if existing == nil {
helmReleaseProxy.GenerateName = fmt.Sprintf("%s-%s-", helmChartProxy.Spec.ChartName, cluster.Name)
helmReleaseProxy.Namespace = helmChartProxy.Namespace
Expand Down Expand Up @@ -212,13 +230,16 @@ func constructHelmReleaseProxy(existing *addonsv1alpha1.HelmReleaseProxy, helmCh
if !cmp.Equal(existing.Spec.Values, parsedValues) {
changed = true
}
if !cmp.Equal(existing.Spec.Version, parsedVersion) {
changed = true
}

if !changed {
return nil
}
}

helmReleaseProxy.Spec.Version = helmChartProxy.Spec.Version
helmReleaseProxy.Spec.Version = strings.TrimSpace(parsedVersion)
helmReleaseProxy.Spec.Values = parsedValues
helmReleaseProxy.Spec.Options = helmChartProxy.Spec.Options
helmReleaseProxy.Spec.Credentials = helmChartProxy.Spec.Credentials
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ var (
RepoURL: "https://test-repo-url",
ReleaseNamespace: "test-release-namespace",
Version: "test-version",
VersionTemplate: "{{ if eq .Cluster.metadata.labels.version \"1.26\" }}v2{{ else }}v1{{ end }}",
ValuesTemplate: "cidrBlockList: {{ .Cluster.spec.clusterNetwork.pods.cidrBlocks | join \",\" }}",
Options: addonsv1alpha1.HelmOptions{
EnableClientCache: true,
Expand Down Expand Up @@ -164,6 +165,9 @@ var (
ObjectMeta: metav1.ObjectMeta{
Name: "test-cluster",
Namespace: "test-namespace",
Labels: map[string]string{
"version": "1.26",
},
},
Spec: clusterv1.ClusterSpec{
ClusterNetwork: &clusterv1.ClusterNetwork{
Expand Down Expand Up @@ -280,6 +284,17 @@ func TestReconcileForCluster(t *testing.T) {
},
expectedError: "",
},
{
name: "updates a HelmReleaseProxy when versionTemplate value changes",
helmChartProxy: fakeHelmChartProxy2,
existingHelmReleaseProxy: fakeHelmReleaseProxy,
cluster: fakeCluster2,
expectHelmReleaseProxyToExist: true,
expect: func(g *WithT, hcp *addonsv1alpha1.HelmChartProxy, hrp *addonsv1alpha1.HelmReleaseProxy) {
g.Expect(hrp.Spec.Version).To(Equal("v2"))
},
expectedError: "",
},
{
name: "set condition when failing to parse values for a HelmChartProxy",
helmChartProxy: fakeInvalidHelmChartProxy,
Expand Down Expand Up @@ -368,6 +383,7 @@ func TestConstructHelmReleaseProxy(t *testing.T) {
existing *addonsv1alpha1.HelmReleaseProxy
helmChartProxy *addonsv1alpha1.HelmChartProxy
parsedValues string
parsedVersion string
cluster *clusterv1.Cluster
expected *addonsv1alpha1.HelmReleaseProxy
}{
Expand Down Expand Up @@ -1097,7 +1113,7 @@ func TestConstructHelmReleaseProxy(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
g := NewWithT(t)

result := constructHelmReleaseProxy(tc.existing, tc.helmChartProxy, tc.parsedValues, tc.cluster)
result := constructHelmReleaseProxy(tc.existing, tc.helmChartProxy, tc.parsedValues, tc.parsedVersion, tc.cluster)
diff := cmp.Diff(tc.expected, result)
g.Expect(diff).To(BeEmpty())
})
Expand Down
6 changes: 5 additions & 1 deletion docs/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ metadata:
namespace: default
labels:
nginxIngressChart: enabled
version: 1.26
spec:
clusterNetwork:
services:
Expand Down Expand Up @@ -105,6 +106,7 @@ spec:
timeout: 5m
install:
createNamespace: true
versionTemplate: {{ if eq .Cluster.metadata.labels.version "1.26" }}v2{{ else }}v1{{ end }}
valuesTemplate: |
controller:
name: "{{ .ControlPlane.metadata.name }}-nginx"
Expand All @@ -117,6 +119,7 @@ We use the `clusterSelector` to select the workload cluster to install the chart
The `repoURL` and `chartName` are used to specify the chart to install.
User shall specify chart-path `oci://repo-url/chart-name` as `repoURL: oci://repo-url` and `chartName: chart-name` in HCP CR. This format is consistent with other types of charts as well (e.g. `https://repo-url/chart-name` as `repoURL: https://repo-url` and `chartName: chart-name`).
The `valuesTemplate` is used to specify the values to use when installing the chart. It supports Go templating, and here we set `controller.name` to the name of the selected cluster + `-nginx`. We also set `controller.nginxStatus.allowCidrs` to include the first entry in the workload cluster's pod CIDR blocks.
The `versionTemplate` is used to specify the version of the chart to install. It supports Go templating, and here we set `metadata.labels.version` to the name of the Kubernetes version. Eventually, we can use different chart versions based on the Kubernetes versions.

Helm options like `wait`, `skipCrds`, `timeout`, `waitForJobs`, etc. can be specified with `options` field as shown in above mentioned example, to control behaviour of helm operations(Install, Upgrade, Delete, etc). Please check CRD spec for all supported helm options and its behaviour.

Expand Down Expand Up @@ -211,14 +214,15 @@ spec:
namespace: default
releaseName: nginx-ingress-1665181073
repoURL: https://helm.nginx.com/stable
version: v2
values: |
controller:
name: "default-23995-nginx"
nginxStatus:
allowCidrs: 127.0.0.1,::1,192.168.0.0/16
```

Notice that a release name is generated for us, and the Go template we specified in `valuesTemplate` has been replaced with the actual values from the Cluster definition.
Notice that a release name is generated for us, and the Go template we specified in `valuesTemplate` and `versionTemplate` has been replaced with the actual values from the Cluster definition.

### 6. Uninstall `nginx-ingress` from the workload cluster

Expand Down
43 changes: 43 additions & 0 deletions internal/value_substitutions.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,46 @@ func ParseValues(ctx context.Context, c ctrlClient.Client, spec addonsv1alpha1.H

return expandedTemplate, nil
}

func ParseVersion(ctx context.Context, c ctrlClient.Client, spec addonsv1alpha1.HelmChartProxySpec, cluster *clusterv1.Cluster) (string, error) {
log := ctrl.LoggerFrom(ctx)

log.V(2).Info("Rendering templating in values:", "version", spec.VersionTemplate)
references := map[string]corev1.ObjectReference{
"Cluster": {
APIVersion: cluster.APIVersion,
Kind: cluster.Kind,
Namespace: cluster.Namespace,
Name: cluster.Name,
},
}

if cluster.Spec.ControlPlaneRef != nil {
references["ControlPlane"] = *cluster.Spec.ControlPlaneRef
}
if cluster.Spec.InfrastructureRef != nil {
references["InfraCluster"] = *cluster.Spec.InfrastructureRef
}
// TODO: would we want to add ControlPlaneMachineTemplate?

valueLookUp, err := initializeBuiltins(ctx, c, references, cluster)
if err != nil {
return "", err
}

tmpl, err := template.New(spec.ChartName + "-" + cluster.GetName()).
Funcs(sprig.TxtFuncMap()).
Parse(spec.VersionTemplate)
if err != nil {
return "", err
}
var buffer bytes.Buffer

if err := tmpl.Execute(&buffer, valueLookUp); err != nil {
return "", errors.Wrapf(err, "error executing template string '%s' on cluster '%s'", spec.VersionTemplate, cluster.GetName())
}
expandedTemplate := buffer.String()
log.V(2).Info("Expanded version to", "result", expandedTemplate)

return expandedTemplate, nil
}
29 changes: 28 additions & 1 deletion test/e2e/helm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ var newNginxValues = `controller:
nginxStatus:
allowCidrs: 127.0.0.1,::1,{{ index .Cluster.spec.clusterNetwork.pods.cidrBlocks 0 }}`

var nginxVersion = "v1"

var newNginxVersion = `{{ if eq .Cluster.metadata.labels.version "1.26" }}v2{{ else }}v1{{ end }}`

var _ = Describe("Workload cluster creation", func() {
var (
ctx = context.Background()
Expand Down Expand Up @@ -135,6 +139,9 @@ var _ = Describe("Workload cluster creation", func() {
ObjectMeta: metav1.ObjectMeta{
Name: "nginx-ingress",
Namespace: namespace.Name,
Labels: map[string]string{
"version": "1.26",
},
},
Spec: addonsv1alpha1.HelmChartProxySpec{
ClusterSelector: metav1.LabelSelector{
Expand All @@ -146,6 +153,7 @@ var _ = Describe("Workload cluster creation", func() {
ReleaseNamespace: "nginx-namespace",
ChartName: "nginx-ingress",
RepoURL: "https://helm.nginx.com/stable",
VersionTemplate: nginxVersion,
ValuesTemplate: nginxValues,
},
}
Expand All @@ -162,7 +170,7 @@ var _ = Describe("Workload cluster creation", func() {
})
})

// Update existing Helm chart
// Update existing Helm chart with new values
By("Updating nginx HelmChartProxy valuesTemplate", func() {
hcp.Spec.ValuesTemplate = newNginxValues
HelmUpgradeSpec(ctx, func() HelmUpgradeInput {
Expand All @@ -176,6 +184,20 @@ var _ = Describe("Workload cluster creation", func() {
})
})

// Update existing Helm chart with new version
By("Updating nginx HelmChartProxy versionTemplate", func() {
hcp.Spec.VersionTemplate = newNginxVersion
HelmUpgradeSpec(ctx, func() HelmUpgradeInput {
return HelmUpgradeInput{
BootstrapClusterProxy: bootstrapClusterProxy,
Namespace: namespace,
ClusterName: clusterName,
HelmChartProxy: hcp,
ExpectedRevision: 3,
}
})
})

// Force reinstall of existing Helm chart by changing the release namespace
By("Updating HelmChartProxy release namespace", func() {
hcp.Spec.ReleaseNamespace = "new-nginx-namespace"
Expand Down Expand Up @@ -266,17 +288,22 @@ var _ = Describe("Workload cluster creation", func() {
ObjectMeta: metav1.ObjectMeta{
Name: "nginx-ingress",
Namespace: namespace.Name,
Labels: map[string]string{
"version": "1.26",
},
},
Spec: addonsv1alpha1.HelmChartProxySpec{
ClusterSelector: metav1.LabelSelector{
MatchLabels: map[string]string{
"nginxIngress": "enabled",
"version": "1.26",
},
},
ReleaseName: "nginx-ingress",
ReleaseNamespace: "nginx-namespace",
ChartName: "nginx-ingress",
RepoURL: "https://helm.nginx.com/stable",
VersionTemplate: nginxVersion,
ValuesTemplate: nginxValues,
},
}
Expand Down
Loading