diff --git a/cmd/flux/create_helmrelease.go b/cmd/flux/create_helmrelease.go index 56c5edb0e6..7e2bf3b32d 100644 --- a/cmd/flux/create_helmrelease.go +++ b/cmd/flux/create_helmrelease.go @@ -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" @@ -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, } @@ -114,6 +126,7 @@ type helmReleaseFlags struct { dependsOn []string chart string chartVersion string + chartRef string targetNamespace string createNamespace bool valuesFiles []string @@ -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 '[-]'") createHelmReleaseCmd.Flags().Var(&helmReleaseArgs.source, "source", helmReleaseArgs.source.Description()) @@ -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 '/', 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 '/.', 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() @@ -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 != "" { @@ -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{} diff --git a/cmd/flux/create_helmrelease_test.go b/cmd/flux/create_helmrelease_test.go new file mode 100644 index 0000000000..ffdef081d0 --- /dev/null +++ b/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) +} diff --git a/cmd/flux/reconcile_helmrelease.go b/cmd/flux/reconcile_helmrelease.go index 25f7746460..134cfa472f 100644 --- a/cmd/flux/reconcile_helmrelease.go +++ b/cmd/flux/reconcile_helmrelease.go @@ -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{ @@ -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, + } } } diff --git a/cmd/flux/testdata/create_hr/basic.yaml b/cmd/flux/testdata/create_hr/basic.yaml new file mode 100644 index 0000000000..066b1610ee --- /dev/null +++ b/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 diff --git a/cmd/flux/testdata/create_hr/hc_basic.yaml b/cmd/flux/testdata/create_hr/hc_basic.yaml new file mode 100644 index 0000000000..6e99f5d05c --- /dev/null +++ b/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 diff --git a/cmd/flux/testdata/create_hr/or_basic.yaml b/cmd/flux/testdata/create_hr/or_basic.yaml new file mode 100644 index 0000000000..368027a21b --- /dev/null +++ b/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 diff --git a/cmd/flux/testdata/create_hr/setup-source.yaml b/cmd/flux/testdata/create_hr/setup-source.yaml new file mode 100644 index 0000000000..254024a043 --- /dev/null +++ b/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