Skip to content

Commit

Permalink
Merge pull request #4766 from fluxcd/reconcile-hr-with-chartref
Browse files Browse the repository at this point in the history
Add support for HelmRelease v2 in `flux reconcile` and `flux create`
  • Loading branch information
souleb committed May 12, 2024
2 parents 86b3581 + 5feee5c commit dd7ef7d
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 35 deletions.
71 changes: 50 additions & 21 deletions cmd/flux/create_helmrelease.go
Expand Up @@ -36,6 +36,8 @@ import (
helmv2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/transform"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2"

"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils"
Expand Down Expand Up @@ -104,7 +106,17 @@ var createHelmReleaseCmd = &cobra.Command{
--source=HelmRepository/podinfo \
--chart=podinfo \
--values=./values.yaml \
--export > podinfo-release.yaml`,
--export > podinfo-release.yaml
# Create a HelmRelease using a chart from a HelmChart resource
flux create hr podinfo \
--namespace=default \
--chart-ref=HelmChart/podinfo.flux-system \
# Create a HelmRelease using a chart from an OCIRepository resource
flux create hr podinfo \
--namespace=default \
--chart-ref=OCIRepository/podinfo.flux-system`,
RunE: createHelmReleaseCmdRun,
}

Expand All @@ -114,6 +126,7 @@ type helmReleaseFlags struct {
dependsOn []string
chart string
chartVersion string
chartRef string
targetNamespace string
createNamespace bool
valuesFiles []string
Expand All @@ -129,6 +142,8 @@ var helmReleaseArgs helmReleaseFlags

var supportedHelmReleaseValuesFromKinds = []string{"Secret", "ConfigMap"}

var supportedHelmReleaseReferenceKinds = []string{sourcev1b2.OCIRepositoryKind, sourcev1.HelmChartKind}

func init() {
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.name, "release-name", "", "name used for the Helm release, defaults to a composition of '[<target-namespace>-]<HelmRelease-name>'")
createHelmReleaseCmd.Flags().Var(&helmReleaseArgs.source, "source", helmReleaseArgs.source.Description())
Expand All @@ -144,14 +159,15 @@ func init() {
createHelmReleaseCmd.Flags().StringSliceVar(&helmReleaseArgs.valuesFrom, "values-from", nil, "a Kubernetes object reference that contains the values.yaml data key in the format '<kind>/<name>', where kind must be one of: (Secret,ConfigMap)")
createHelmReleaseCmd.Flags().Var(&helmReleaseArgs.crds, "crds", helmReleaseArgs.crds.Description())
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.kubeConfigSecretRef, "kubeconfig-secret-ref", "", "the name of the Kubernetes Secret that contains a key with the kubeconfig file for connecting to a remote cluster")
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.chartRef, "chart-ref", "", "the name of the HelmChart resource to use as source for the HelmRelease, in the format '<kind>/<name>.<namespace>', where kind must be one of: (OCIRepository,HelmChart)")
createCmd.AddCommand(createHelmReleaseCmd)
}

func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
name := args[0]

if helmReleaseArgs.chart == "" {
return fmt.Errorf("chart name or path is required")
if helmReleaseArgs.chart == "" && helmReleaseArgs.chartRef == "" {
return fmt.Errorf("chart or chart-ref is required")
}

sourceLabels, err := parseLabels()
Expand Down Expand Up @@ -181,21 +197,40 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
Duration: createArgs.interval,
},
TargetNamespace: helmReleaseArgs.targetNamespace,
Suspend: false,
},
}

Chart: &helmv2.HelmChartTemplate{
Spec: helmv2.HelmChartTemplateSpec{
Chart: helmReleaseArgs.chart,
Version: helmReleaseArgs.chartVersion,
SourceRef: helmv2.CrossNamespaceObjectReference{
Kind: helmReleaseArgs.source.Kind,
Name: helmReleaseArgs.source.Name,
Namespace: helmReleaseArgs.source.Namespace,
},
ReconcileStrategy: helmReleaseArgs.reconcileStrategy,
switch {
case helmReleaseArgs.chart != "":
helmRelease.Spec.Chart = &helmv2.HelmChartTemplate{
Spec: helmv2.HelmChartTemplateSpec{
Chart: helmReleaseArgs.chart,
Version: helmReleaseArgs.chartVersion,
SourceRef: helmv2.CrossNamespaceObjectReference{
Kind: helmReleaseArgs.source.Kind,
Name: helmReleaseArgs.source.Name,
Namespace: helmReleaseArgs.source.Namespace,
},
ReconcileStrategy: helmReleaseArgs.reconcileStrategy,
},
Suspend: false,
},
}
if helmReleaseArgs.chartInterval != 0 {
helmRelease.Spec.Chart.Spec.Interval = &metav1.Duration{
Duration: helmReleaseArgs.chartInterval,
}
}
case helmReleaseArgs.chartRef != "":
kind, name, ns := utils.ParseObjectKindNameNamespace(helmReleaseArgs.chartRef)
if kind != sourcev1.HelmChartKind && kind != sourcev1b2.OCIRepositoryKind {
return fmt.Errorf("chart reference kind '%s' is not supported, must be one of: %s",
kind, strings.Join(supportedHelmReleaseReferenceKinds, ", "))
}
helmRelease.Spec.ChartRef = &helmv2.CrossNamespaceSourceReference{
Kind: kind,
Name: name,
Namespace: ns,
}
}

if helmReleaseArgs.kubeConfigSecretRef != "" {
Expand All @@ -206,12 +241,6 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
}
}

if helmReleaseArgs.chartInterval != 0 {
helmRelease.Spec.Chart.Spec.Interval = &metav1.Duration{
Duration: helmReleaseArgs.chartInterval,
}
}

if helmReleaseArgs.createNamespace {
if helmRelease.Spec.Install == nil {
helmRelease.Spec.Install = &helmv2.Install{}
Expand Down
86 changes: 86 additions & 0 deletions cmd/flux/create_helmrelease_test.go
@@ -0,0 +1,86 @@
//go:build unit
// +build unit

/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import "testing"

func TestCreateHelmRelease(t *testing.T) {
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
setupHRSource(t, tmpl)

tests := []struct {
name string
args string
assert assertFunc
}{
{
name: "missing name",
args: "create helmrelease --export",
assert: assertError("name is required"),
},
{
name: "missing chart template and chartRef",
args: "create helmrelease podinfo --export",
assert: assertError("chart or chart-ref is required"),
},
{
name: "unknown source kind",
args: "create helmrelease podinfo --source foobar/podinfo --chart podinfo --export",
assert: assertError(`invalid argument "foobar/podinfo" for "--source" flag: source kind 'foobar' is not supported, must be one of: HelmRepository, GitRepository, Bucket`),
},
{
name: "unknown chart reference kind",
args: "create helmrelease podinfo --chart-ref foobar/podinfo --export",
assert: assertError(`chart reference kind 'foobar' is not supported, must be one of: OCIRepository, HelmChart`),
},
{
name: "basic helmrelease",
args: "create helmrelease podinfo --source Helmrepository/podinfo --chart podinfo --interval=1m0s --export",
assert: assertGoldenTemplateFile("testdata/create_hr/basic.yaml", tmpl),
},
{
name: "chart with OCIRepository source",
args: "create helmrelease podinfo --chart-ref OCIRepository/podinfo --interval=1m0s --export",
assert: assertGoldenTemplateFile("testdata/create_hr/or_basic.yaml", tmpl),
},
{
name: "chart with HelmChart source",
args: "create helmrelease podinfo --chart-ref HelmChart/podinfo --interval=1m0s --export",
assert: assertGoldenTemplateFile("testdata/create_hr/hc_basic.yaml", tmpl),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.args + " -n " + tmpl["fluxns"],
assert: tt.assert,
}
cmd.runTestCmd(t)
})
}
}

func setupHRSource(t *testing.T, tmpl map[string]string) {
t.Helper()
testEnv.CreateObjectFile("./testdata/create_hr/setup-source.yaml", tmpl, t)
}
55 changes: 41 additions & 14 deletions cmd/flux/reconcile_helmrelease.go
Expand Up @@ -24,6 +24,7 @@ import (

helmv2 "github.com/fluxcd/helm-controller/api/v2"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2"
)

var reconcileHrCmd = &cobra.Command{
Expand Down Expand Up @@ -68,20 +69,46 @@ func (obj helmReleaseAdapter) reconcileSource() bool {
}

func (obj helmReleaseAdapter) getSource() (reconcileSource, types.NamespacedName) {
cmd := reconcileWithSourceCommand{
apiType: helmChartType,
object: helmChartAdapter{&sourcev1.HelmChart{}},
force: true,
}

ns := obj.Spec.Chart.Spec.SourceRef.Namespace
if ns == "" {
ns = obj.Namespace
}

return cmd, types.NamespacedName{
Name: fmt.Sprintf("%s-%s", obj.Namespace, obj.Name),
Namespace: ns,
var (
name string
ns string
)
switch {
case obj.Spec.ChartRef != nil:
name, ns = obj.Spec.ChartRef.Name, obj.Spec.ChartRef.Namespace
if ns == "" {
ns = obj.Namespace
}
namespacedName := types.NamespacedName{
Name: name,
Namespace: ns,
}
if obj.Spec.ChartRef.Kind == sourcev1.HelmChartKind {
return reconcileWithSourceCommand{
apiType: helmChartType,
object: helmChartAdapter{&sourcev1.HelmChart{}},
force: true,
}, namespacedName
}
return reconcileCommand{
apiType: ociRepositoryType,
object: ociRepositoryAdapter{&sourcev1b2.OCIRepository{}},
}, namespacedName
default:
// default case assumes the HelmRelease is using a HelmChartTemplate
ns = obj.Spec.Chart.Spec.SourceRef.Namespace
if ns == "" {
ns = obj.Namespace
}
name = fmt.Sprintf("%s-%s", obj.Namespace, obj.Name)
return reconcileWithSourceCommand{
apiType: helmChartType,
object: helmChartAdapter{&sourcev1.HelmChart{}},
force: true,
}, types.NamespacedName{
Name: name,
Namespace: ns,
}
}
}

Expand Down
15 changes: 15 additions & 0 deletions cmd/flux/testdata/create_hr/basic.yaml
@@ -0,0 +1,15 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: podinfo
namespace: {{ .fluxns }}
spec:
chart:
spec:
chart: podinfo
reconcileStrategy: ChartVersion
sourceRef:
kind: HelmRepository
name: podinfo
interval: 1m0s
11 changes: 11 additions & 0 deletions cmd/flux/testdata/create_hr/hc_basic.yaml
@@ -0,0 +1,11 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: podinfo
namespace: {{ .fluxns }}
spec:
chartRef:
kind: HelmChart
name: podinfo
interval: 1m0s
11 changes: 11 additions & 0 deletions cmd/flux/testdata/create_hr/or_basic.yaml
@@ -0,0 +1,11 @@
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: podinfo
namespace: {{ .fluxns }}
spec:
chartRef:
kind: OCIRepository
name: podinfo
interval: 1m0s
39 changes: 39 additions & 0 deletions cmd/flux/testdata/create_hr/setup-source.yaml
@@ -0,0 +1,39 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: {{ .fluxns }}
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: podinfo
namespace: {{ .fluxns }}
spec:
interval: 1m0s
provider: generic
type: oci
url: oci://ghcr.io/stefanprodan/charts
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmChart
metadata:
name: podinfo
namespace: {{ .fluxns }}
spec:
interval: 1m0s
chart: podinfo
sourceRef:
kind: HelmRepository
name: podinfo
---
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: OCIRepository
metadata:
name: podinfo
namespace: flux-system
spec:
interval: 10m
url: oci://ghcr.io/stefanprodan/manifests/podinfo
ref:
tag: latest

0 comments on commit dd7ef7d

Please sign in to comment.