From 4a834e1d2d1a57792e0137580677e0e8ea4149c3 Mon Sep 17 00:00:00 2001 From: Dylan Arbour Date: Sat, 6 Mar 2021 14:28:22 -0500 Subject: [PATCH] Add `ValuesFiles` to HelmChart spec Signed-off-by: Dylan Arbour --- api/v1beta1/helmchart_types.go | 25 +- api/v1beta1/zz_generated.deepcopy.go | 7 +- .../source.toolkit.fluxcd.io_helmcharts.yaml | 13 +- .../helmchart_gitrepository.yaml | 4 +- .../helmchart_helmrepository.yaml | 4 +- controllers/helmchart_controller.go | 90 ++++-- controllers/helmchart_controller_test.go | 265 ++++++++++++++++++ docs/api/source.md | 42 ++- docs/spec/v1beta1/helmcharts.md | 52 +++- 9 files changed, 469 insertions(+), 33 deletions(-) diff --git a/api/v1beta1/helmchart_types.go b/api/v1beta1/helmchart_types.go index 64518f9e9..96f027800 100644 --- a/api/v1beta1/helmchart_types.go +++ b/api/v1beta1/helmchart_types.go @@ -45,9 +45,19 @@ type HelmChartSpec struct { // +required Interval metav1.Duration `json:"interval"` - // Alternative values file to use as the default chart values, expected to be a - // relative path in the SourceRef. Ignored when omitted. + // Alternative list of values files to use as the chart values (values.yaml + // is not included by default), expected to be a relative path in the SourceRef. + // Values files are merged in the order of this list with the last file overriding + // the first. Ignored when omitted. // +optional + ValuesFiles []string `json:"valuesFiles,omitempty"` + + // Alternative values file to use as the default chart values, expected to + // be a relative path in the SourceRef. Deprecated in favor of ValuesFiles, + // for backwards compatibility the file defined here is merged before the + // ValuesFiles items. Ignored when omitted. + // +optional + // +deprecated ValuesFile string `json:"valuesFile,omitempty"` // This flag tells the controller to suspend the reconciliation of this source. @@ -168,6 +178,17 @@ func (in *HelmChart) GetInterval() metav1.Duration { return in.Spec.Interval } +// GetValuesFiles returns a merged list of ValuesFiles. +func (in *HelmChart) GetValuesFiles() []string { + valuesFiles := in.Spec.ValuesFiles + + // Prepend the deprecated ValuesFile to the list + if in.Spec.ValuesFile != "" { + valuesFiles = append([]string{in.Spec.ValuesFile}, valuesFiles...) + } + return valuesFiles +} + // +genclient // +genclient:Namespaced // +kubebuilder:object:root=true diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 24e929f38..d35b366b3 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -324,7 +324,7 @@ func (in *HelmChart) DeepCopyInto(out *HelmChart) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -383,6 +383,11 @@ func (in *HelmChartSpec) DeepCopyInto(out *HelmChartSpec) { *out = *in out.SourceRef = in.SourceRef out.Interval = in.Interval + if in.ValuesFiles != nil { + in, out := &in.ValuesFiles, &out.ValuesFiles + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChartSpec. diff --git a/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml b/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml index 4baeb7078..8d8215d96 100644 --- a/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml +++ b/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml @@ -94,8 +94,19 @@ spec: type: boolean valuesFile: description: Alternative values file to use as the default chart values, - expected to be a relative path in the SourceRef. Ignored when omitted. + expected to be a relative path in the SourceRef. Deprecated in favor + of ValuesFiles, for backwards compatibility the file defined here + is merged before the ValuesFiles items. Ignored when omitted. type: string + valuesFiles: + description: Alternative list of values files to use as the chart + values (values.yaml is not included by default), expected to be + a relative path in the SourceRef. Values files are merged in the + order of this list with the last file overriding the first. Ignored + when omitted. + items: + type: string + type: array version: default: '*' description: The chart version semver expression, ignored for charts diff --git a/config/testdata/helmchart-valuesfile/helmchart_gitrepository.yaml b/config/testdata/helmchart-valuesfile/helmchart_gitrepository.yaml index 911132d84..4483f0ca8 100644 --- a/config/testdata/helmchart-valuesfile/helmchart_gitrepository.yaml +++ b/config/testdata/helmchart-valuesfile/helmchart_gitrepository.yaml @@ -8,4 +8,6 @@ spec: kind: GitRepository name: podinfo chart: charts/podinfo - valuesFile: charts/podinfo/values-prod.yaml + valuesFile: charts/podinfo/values.yaml + valuesFiles: + - charts/podinfo/values-prod.yaml diff --git a/config/testdata/helmchart-valuesfile/helmchart_helmrepository.yaml b/config/testdata/helmchart-valuesfile/helmchart_helmrepository.yaml index 4674622b0..fdf34f6bf 100644 --- a/config/testdata/helmchart-valuesfile/helmchart_helmrepository.yaml +++ b/config/testdata/helmchart-valuesfile/helmchart_helmrepository.yaml @@ -8,4 +8,6 @@ spec: kind: HelmRepository name: podinfo chart: podinfo - valuesFile: values-prod.yaml + valuesFile: values.yaml + valuesFiles: + - values-prod.yaml diff --git a/controllers/helmchart_controller.go b/controllers/helmchart_controller.go index 3d153b3d8..bb2b93e33 100644 --- a/controllers/helmchart_controller.go +++ b/controllers/helmchart_controller.go @@ -49,11 +49,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" + "sigs.k8s.io/yaml" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/runtime/events" "github.com/fluxcd/pkg/runtime/metrics" "github.com/fluxcd/pkg/runtime/predicates" + "github.com/fluxcd/pkg/runtime/transform" "github.com/fluxcd/pkg/untar" sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" @@ -380,11 +382,13 @@ func (r *HelmChartReconciler) reconcileFromHelmRepository(ctx context.Context, readyMessage = fmt.Sprintf("Fetched revision: %s", newArtifact.Revision) ) switch { - case chart.Spec.ValuesFile != "" && chart.Spec.ValuesFile != chartutil.ValuesfileName: + case len(chart.GetValuesFiles()) > 0: var ( tmpDir string pkgPath string ) + valuesMap := make(map[string]interface{}) + // Load the chart helmChart, err := loader.LoadArchive(res) if err != nil { @@ -392,18 +396,43 @@ func (r *HelmChartReconciler) reconcileFromHelmRepository(ctx context.Context, return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err } - // Find override file and retrieve contents - var valuesData []byte - cfn := filepath.Clean(chart.Spec.ValuesFile) - for _, f := range helmChart.Files { - if f.Name == cfn { - valuesData = f.Data - break + for _, v := range chart.GetValuesFiles() { + if v == "values.yaml" { + valuesMap = transform.MergeMaps(valuesMap, helmChart.Values) + continue + } + + var valuesData []byte + cfn := filepath.Clean(v) + for _, f := range helmChart.Files { + if f.Name == cfn { + valuesData = f.Data + break + } } + if valuesData == nil { + err = fmt.Errorf("invalid values file path: %s", v) + return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err + } + + yamlMap := make(map[string]interface{}) + err = yaml.Unmarshal(valuesData, &yamlMap) + if err != nil { + err = fmt.Errorf("unmarshaling values from %s failed: %w", v, err) + return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err + } + + valuesMap = transform.MergeMaps(valuesMap, yamlMap) + } + + yamlBytes, err := yaml.Marshal(valuesMap) + if err != nil { + err = fmt.Errorf("marshaling values failed: %w", err) + return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPackageFailedReason, err.Error()), err } // Overwrite values file - if changed, err := helm.OverwriteChartDefaultValues(helmChart, valuesData); err != nil { + if changed, err := helm.OverwriteChartDefaultValues(helmChart, yamlBytes); err != nil { return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPackageFailedReason, err.Error()), err } else if !changed { // No changes, skip to write original package to storage @@ -508,22 +537,41 @@ func (r *HelmChartReconciler) reconcileFromTarballArtifact(ctx context.Context, // or write the chart directly to storage. pkgPath := chartPath isValuesFileOverriden := false - if chart.Spec.ValuesFile != "" { - srcPath, err := securejoin.SecureJoin(tmpDir, chart.Spec.ValuesFile) - if err != nil { - return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err - } - if f, err := os.Stat(srcPath); os.IsNotExist(err) || !f.Mode().IsRegular() { - err = fmt.Errorf("invalid values file path: %s", chart.Spec.ValuesFile) - return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err + if len(chart.GetValuesFiles()) > 0 { + valuesMap := make(map[string]interface{}) + for _, v := range chart.GetValuesFiles() { + srcPath, err := securejoin.SecureJoin(tmpDir, v) + if err != nil { + return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err + } + if f, err := os.Stat(srcPath); os.IsNotExist(err) || !f.Mode().IsRegular() { + err = fmt.Errorf("invalid values file path: %s", v) + return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err + } + + valuesData, err := ioutil.ReadFile(srcPath) + if err != nil { + err = fmt.Errorf("failed to read from values file '%s': %w", v, err) + return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err + } + + yamlMap := make(map[string]interface{}) + err = yaml.Unmarshal(valuesData, &yamlMap) + if err != nil { + err = fmt.Errorf("unmarshaling values from %s failed: %w", v, err) + return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err + } + + valuesMap = transform.MergeMaps(valuesMap, yamlMap) } - valuesData, err := ioutil.ReadFile(srcPath) + yamlBytes, err := yaml.Marshal(valuesMap) if err != nil { - err = fmt.Errorf("failed to read from values file '%s': %w", chart.Spec.ValuesFile, err) - return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err + err = fmt.Errorf("marshaling values failed: %w", err) + return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPackageFailedReason, err.Error()), err } - isValuesFileOverriden, err = helm.OverwriteChartDefaultValues(helmChart, valuesData) + + isValuesFileOverriden, err = helm.OverwriteChartDefaultValues(helmChart, yamlBytes) if err != nil { return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPackageFailedReason, err.Error()), err } diff --git a/controllers/helmchart_controller_test.go b/controllers/helmchart_controller_test.go index ddda8d49e..485ca6639 100644 --- a/controllers/helmchart_controller_test.go +++ b/controllers/helmchart_controller_test.go @@ -151,10 +151,125 @@ var _ = Describe("HelmChartReconciler", func() { !storage.ArtifactExist(*got.Status.Artifact) }, timeout, interval).Should(BeTrue()) + When("Setting valid valuesFiles attribute", func() { + updated := &sourcev1.HelmChart{} + Expect(k8sClient.Get(context.Background(), key, updated)).To(Succeed()) + updated.Spec.ValuesFiles = []string{ + "values.yaml", + "override.yaml", + } + Expect(k8sClient.Update(context.Background(), updated)).To(Succeed()) + got := &sourcev1.HelmChart{} + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), key, got) + return got.Status.Artifact.Checksum != updated.Status.Artifact.Checksum && + storage.ArtifactExist(*got.Status.Artifact) + }, timeout, interval).Should(BeTrue()) + f, err := os.Stat(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(f.Size()).To(BeNumerically(">", 0)) + helmChart, err := loader.Load(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(helmChart.Values["testDefault"]).To(BeTrue()) + Expect(helmChart.Values["testOverride"]).To(BeTrue()) + }) + + When("Setting invalid valuesFiles attribute", func() { + updated := &sourcev1.HelmChart{} + Expect(k8sClient.Get(context.Background(), key, updated)).To(Succeed()) + updated.Spec.ValuesFiles = []string{ + "values.yaml", + "invalid.yaml", + } + Expect(k8sClient.Update(context.Background(), updated)).To(Succeed()) + got := &sourcev1.HelmChart{} + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), key, got) + return got.Status.ObservedGeneration > updated.Status.ObservedGeneration && + storage.ArtifactExist(*got.Status.Artifact) + }, timeout, interval).Should(BeTrue()) + f, err := os.Stat(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(f.Size()).To(BeNumerically(">", 0)) + helmChart, err := loader.Load(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(helmChart.Values["testDefault"]).To(BeTrue()) + Expect(helmChart.Values["testOverride"]).To(BeTrue()) + }) + + When("Setting valid valuesFiles and valuesFile attribute", func() { + updated := &sourcev1.HelmChart{} + Expect(k8sClient.Get(context.Background(), key, updated)).To(Succeed()) + updated.Spec.ValuesFile = "values.yaml" + updated.Spec.ValuesFiles = []string{ + "override.yaml", + } + Expect(k8sClient.Update(context.Background(), updated)).To(Succeed()) + got := &sourcev1.HelmChart{} + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), key, got) + return got.Status.Artifact.Checksum != updated.Status.Artifact.Checksum && + storage.ArtifactExist(*got.Status.Artifact) + }, timeout, interval).Should(BeTrue()) + f, err := os.Stat(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(f.Size()).To(BeNumerically(">", 0)) + helmChart, err := loader.Load(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(helmChart.Values["testDefault"]).To(BeTrue()) + Expect(helmChart.Values["testOverride"]).To(BeTrue()) + }) + + When("Setting valid valuesFile attribute", func() { + updated := &sourcev1.HelmChart{} + Expect(k8sClient.Get(context.Background(), key, updated)).To(Succeed()) + updated.Spec.ValuesFile = "override.yaml" + updated.Spec.ValuesFiles = []string{} + Expect(k8sClient.Update(context.Background(), updated)).To(Succeed()) + got := &sourcev1.HelmChart{} + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), key, got) + return got.Status.Artifact.Checksum != updated.Status.Artifact.Checksum && + storage.ArtifactExist(*got.Status.Artifact) + }, timeout, interval).Should(BeTrue()) + f, err := os.Stat(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(f.Size()).To(BeNumerically(">", 0)) + helmChart, err := loader.Load(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + _, exists := helmChart.Values["testDefault"] + Expect(exists).To(BeFalse()) + Expect(helmChart.Values["testOverride"]).To(BeTrue()) + }) + + When("Setting invalid valuesFile attribute", func() { + updated := &sourcev1.HelmChart{} + Expect(k8sClient.Get(context.Background(), key, updated)).To(Succeed()) + updated.Spec.ValuesFile = "invalid.yaml" + updated.Spec.ValuesFiles = []string{} + Expect(k8sClient.Update(context.Background(), updated)).To(Succeed()) + got := &sourcev1.HelmChart{} + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), key, got) + return got.Status.ObservedGeneration > updated.Status.ObservedGeneration && + storage.ArtifactExist(*got.Status.Artifact) + }, timeout, interval).Should(BeTrue()) + f, err := os.Stat(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(f.Size()).To(BeNumerically(">", 0)) + helmChart, err := loader.Load(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + _, exists := helmChart.Values["testDefault"] + Expect(exists).To(BeFalse()) + Expect(helmChart.Values["testOverride"]).To(BeTrue()) + }) + By("Expecting missing HelmRepository error") updated := &sourcev1.HelmChart{} Expect(k8sClient.Get(context.Background(), key, updated)).Should(Succeed()) updated.Spec.SourceRef.Name = "invalid" + updated.Spec.ValuesFile = "" + updated.Spec.ValuesFiles = []string{} Expect(k8sClient.Update(context.Background(), updated)).Should(Succeed()) Eventually(func() bool { _ = k8sClient.Get(context.Background(), key, updated) @@ -601,10 +716,80 @@ var _ = Describe("HelmChartReconciler", func() { Expect(helmChart.Values["testDefault"]).To(BeTrue()) Expect(helmChart.Values["testOverride"]).To(BeFalse()) + When("Setting valid valuesFiles attribute", func() { + updated := &sourcev1.HelmChart{} + Expect(k8sClient.Get(context.Background(), key, updated)).To(Succeed()) + updated.Spec.ValuesFiles = []string{ + "./testdata/charts/helmchart/values.yaml", + "./testdata/charts/helmchart/override.yaml", + } + Expect(k8sClient.Update(context.Background(), updated)).To(Succeed()) + got := &sourcev1.HelmChart{} + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), key, got) + return got.Status.Artifact.Checksum != updated.Status.Artifact.Checksum && + storage.ArtifactExist(*got.Status.Artifact) + }, timeout, interval).Should(BeTrue()) + f, err := os.Stat(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(f.Size()).To(BeNumerically(">", 0)) + helmChart, err := loader.Load(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(helmChart.Values["testDefault"]).To(BeTrue()) + Expect(helmChart.Values["testOverride"]).To(BeTrue()) + }) + + When("Setting invalid valuesFiles attribute", func() { + updated := &sourcev1.HelmChart{} + Expect(k8sClient.Get(context.Background(), key, updated)).To(Succeed()) + updated.Spec.ValuesFiles = []string{ + "./testdata/charts/helmchart/values.yaml", + "./testdata/charts/helmchart/invalid.yaml", + } + Expect(k8sClient.Update(context.Background(), updated)).To(Succeed()) + got := &sourcev1.HelmChart{} + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), key, got) + return got.Status.ObservedGeneration > updated.Status.ObservedGeneration && + storage.ArtifactExist(*got.Status.Artifact) + }, timeout, interval).Should(BeTrue()) + f, err := os.Stat(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(f.Size()).To(BeNumerically(">", 0)) + helmChart, err := loader.Load(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(helmChart.Values["testDefault"]).To(BeTrue()) + Expect(helmChart.Values["testOverride"]).To(BeTrue()) + }) + + When("Setting valid valuesFiles and valuesFile attribute", func() { + updated := &sourcev1.HelmChart{} + Expect(k8sClient.Get(context.Background(), key, updated)).To(Succeed()) + updated.Spec.ValuesFile = "./testdata/charts/helmchart/values.yaml" + updated.Spec.ValuesFiles = []string{ + "./testdata/charts/helmchart/override.yaml", + } + Expect(k8sClient.Update(context.Background(), updated)).To(Succeed()) + got := &sourcev1.HelmChart{} + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), key, got) + return got.Status.Artifact.Checksum != updated.Status.Artifact.Checksum && + storage.ArtifactExist(*got.Status.Artifact) + }, timeout, interval).Should(BeTrue()) + f, err := os.Stat(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(f.Size()).To(BeNumerically(">", 0)) + helmChart, err := loader.Load(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(helmChart.Values["testDefault"]).To(BeTrue()) + Expect(helmChart.Values["testOverride"]).To(BeTrue()) + }) + When("Setting valid valuesFile attribute", func() { updated := &sourcev1.HelmChart{} Expect(k8sClient.Get(context.Background(), key, updated)).To(Succeed()) updated.Spec.ValuesFile = "./testdata/charts/helmchart/override.yaml" + updated.Spec.ValuesFiles = []string{} Expect(k8sClient.Update(context.Background(), updated)).To(Succeed()) got := &sourcev1.HelmChart{} Eventually(func() bool { @@ -617,6 +802,8 @@ var _ = Describe("HelmChartReconciler", func() { Expect(f.Size()).To(BeNumerically(">", 0)) helmChart, err := loader.Load(storage.LocalPath(*got.Status.Artifact)) Expect(err).NotTo(HaveOccurred()) + _, exists := helmChart.Values["testDefault"] + Expect(exists).To(BeFalse()) Expect(helmChart.Values["testOverride"]).To(BeTrue()) }) @@ -624,6 +811,7 @@ var _ = Describe("HelmChartReconciler", func() { updated := &sourcev1.HelmChart{} Expect(k8sClient.Get(context.Background(), key, updated)).To(Succeed()) updated.Spec.ValuesFile = "./testdata/charts/helmchart/invalid.yaml" + updated.Spec.ValuesFiles = []string{} Expect(k8sClient.Update(context.Background(), updated)).To(Succeed()) got := &sourcev1.HelmChart{} Eventually(func() bool { @@ -636,6 +824,8 @@ var _ = Describe("HelmChartReconciler", func() { Expect(f.Size()).To(BeNumerically(">", 0)) helmChart, err := loader.Load(storage.LocalPath(*got.Status.Artifact)) Expect(err).NotTo(HaveOccurred()) + _, exists := helmChart.Values["testDefault"] + Expect(exists).To(BeFalse()) Expect(helmChart.Values["testOverride"]).To(BeTrue()) }) }) @@ -987,10 +1177,80 @@ var _ = Describe("HelmChartReconciler", func() { Expect(helmChart.Values["testDefault"]).To(BeTrue()) Expect(helmChart.Values["testOverride"]).To(BeFalse()) + When("Setting valid valuesFiles attribute", func() { + updated := &sourcev1.HelmChart{} + Expect(k8sClient.Get(context.Background(), key, updated)).To(Succeed()) + updated.Spec.ValuesFiles = []string{ + "./testdata/charts/helmchartwithdeps/values.yaml", + "./testdata/charts/helmchartwithdeps/override.yaml", + } + Expect(k8sClient.Update(context.Background(), updated)).To(Succeed()) + got := &sourcev1.HelmChart{} + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), key, got) + return got.Status.Artifact.Checksum != updated.Status.Artifact.Checksum && + storage.ArtifactExist(*got.Status.Artifact) + }, timeout, interval).Should(BeTrue()) + f, err := os.Stat(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(f.Size()).To(BeNumerically(">", 0)) + helmChart, err := loader.Load(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(helmChart.Values["testDefault"]).To(BeTrue()) + Expect(helmChart.Values["testOverride"]).To(BeTrue()) + }) + + When("Setting invalid valuesFiles attribute", func() { + updated := &sourcev1.HelmChart{} + Expect(k8sClient.Get(context.Background(), key, updated)).To(Succeed()) + updated.Spec.ValuesFiles = []string{ + "./testdata/charts/helmchartwithdeps/values.yaml", + "./testdata/charts/helmchartwithdeps/invalid.yaml", + } + Expect(k8sClient.Update(context.Background(), updated)).To(Succeed()) + got := &sourcev1.HelmChart{} + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), key, got) + return got.Status.ObservedGeneration > updated.Status.ObservedGeneration && + storage.ArtifactExist(*got.Status.Artifact) + }, timeout, interval).Should(BeTrue()) + f, err := os.Stat(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(f.Size()).To(BeNumerically(">", 0)) + helmChart, err := loader.Load(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(helmChart.Values["testDefault"]).To(BeTrue()) + Expect(helmChart.Values["testOverride"]).To(BeTrue()) + }) + + When("Setting valid valuesFiles and valuesFile attribute", func() { + updated := &sourcev1.HelmChart{} + Expect(k8sClient.Get(context.Background(), key, updated)).To(Succeed()) + updated.Spec.ValuesFile = "./testdata/charts/helmchartwithdeps/values.yaml" + updated.Spec.ValuesFiles = []string{ + "./testdata/charts/helmchartwithdeps/override.yaml", + } + Expect(k8sClient.Update(context.Background(), updated)).To(Succeed()) + got := &sourcev1.HelmChart{} + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), key, got) + return got.Status.Artifact.Checksum != updated.Status.Artifact.Checksum && + storage.ArtifactExist(*got.Status.Artifact) + }, timeout, interval).Should(BeTrue()) + f, err := os.Stat(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(f.Size()).To(BeNumerically(">", 0)) + helmChart, err := loader.Load(storage.LocalPath(*got.Status.Artifact)) + Expect(err).NotTo(HaveOccurred()) + Expect(helmChart.Values["testDefault"]).To(BeTrue()) + Expect(helmChart.Values["testOverride"]).To(BeTrue()) + }) + When("Setting valid valuesFile attribute", func() { updated := &sourcev1.HelmChart{} Expect(k8sClient.Get(context.Background(), key, updated)).To(Succeed()) updated.Spec.ValuesFile = "./testdata/charts/helmchartwithdeps/override.yaml" + updated.Spec.ValuesFiles = []string{} Expect(k8sClient.Update(context.Background(), updated)).To(Succeed()) got := &sourcev1.HelmChart{} Eventually(func() bool { @@ -1003,6 +1263,8 @@ var _ = Describe("HelmChartReconciler", func() { Expect(f.Size()).To(BeNumerically(">", 0)) helmChart, err := loader.Load(storage.LocalPath(*got.Status.Artifact)) Expect(err).NotTo(HaveOccurred()) + _, exists := helmChart.Values["testDefault"] + Expect(exists).To(BeFalse()) Expect(helmChart.Values["testOverride"]).To(BeTrue()) }) @@ -1010,6 +1272,7 @@ var _ = Describe("HelmChartReconciler", func() { updated := &sourcev1.HelmChart{} Expect(k8sClient.Get(context.Background(), key, updated)).To(Succeed()) updated.Spec.ValuesFile = "./testdata/charts/helmchartwithdeps/invalid.yaml" + updated.Spec.ValuesFiles = []string{} Expect(k8sClient.Update(context.Background(), updated)).To(Succeed()) got := &sourcev1.HelmChart{} Eventually(func() bool { @@ -1022,6 +1285,8 @@ var _ = Describe("HelmChartReconciler", func() { Expect(f.Size()).To(BeNumerically(">", 0)) helmChart, err := loader.Load(storage.LocalPath(*got.Status.Artifact)) Expect(err).NotTo(HaveOccurred()) + _, exists := helmChart.Values["testDefault"] + Expect(exists).To(BeFalse()) Expect(helmChart.Values["testOverride"]).To(BeTrue()) }) }) diff --git a/docs/api/source.md b/docs/api/source.md index bb9943545..d036b027b 100644 --- a/docs/api/source.md +++ b/docs/api/source.md @@ -542,6 +542,21 @@ Kubernetes meta/v1.Duration +valuesFiles
+ +[]string + + + +(Optional) +

Alternative list of values files to use as the chart values (values.yaml +is not included by default), expected to be a relative path in the SourceRef. +Values files are merged in the order of this list with the last file overriding +the first. Ignored when omitted.

+ + + + valuesFile
string @@ -549,8 +564,10 @@ string (Optional) -

Alternative values file to use as the default chart values, expected to be a -relative path in the SourceRef. Ignored when omitted.

+

Alternative values file to use as the default chart values, expected to +be a relative path in the SourceRef. Deprecated in favor of ValuesFiles, +for backwards compatibility the file defined here is merged before the +ValuesFiles items. Ignored when omitted.

@@ -1481,6 +1498,21 @@ Kubernetes meta/v1.Duration +valuesFiles
+ +[]string + + + +(Optional) +

Alternative list of values files to use as the chart values (values.yaml +is not included by default), expected to be a relative path in the SourceRef. +Values files are merged in the order of this list with the last file overriding +the first. Ignored when omitted.

+ + + + valuesFile
string @@ -1488,8 +1520,10 @@ string (Optional) -

Alternative values file to use as the default chart values, expected to be a -relative path in the SourceRef. Ignored when omitted.

+

Alternative values file to use as the default chart values, expected to +be a relative path in the SourceRef. Deprecated in favor of ValuesFiles, +for backwards compatibility the file defined here is merged before the +ValuesFiles items. Ignored when omitted.

diff --git a/docs/spec/v1beta1/helmcharts.md b/docs/spec/v1beta1/helmcharts.md index 3a96c08fd..067004f93 100644 --- a/docs/spec/v1beta1/helmcharts.md +++ b/docs/spec/v1beta1/helmcharts.md @@ -28,9 +28,19 @@ type HelmChartSpec struct { // +required Interval metav1.Duration `json:"interval"` - // Alternative values file to use as the default chart values, expected to be a - // relative path in the SourceRef. Ignored when omitted. + // Alternative list of values files to use as the chart values (values.yaml + // is not included by default), expected to be a relative path in the SourceRef. + // Values files are merged in the order of this list with the last file overriding + // the first. Ignored when omitted. // +optional + ValuesFiles []string `json:"valuesFiles,omitempty"` + + // Alternative values file to use as the default chart values, expected to + // be a relative path in the SourceRef. Deprecated in favor of ValuesFiles, + // for backwards compatibility the file defined here is merged before the + // ValuesFiles items. Ignored when omitted. + // +optional + // +deprecated ValuesFile string `json:"valuesFile,omitempty"` // This flag tells the controller to suspend the reconciliation of this source. @@ -182,6 +192,44 @@ spec: interval: 10m ``` +Override default values with alternative values files relative to the +path in the SourceRef: + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1beta1 +kind: HelmChart +metadata: + name: redis + namespace: default +spec: + chart: redis + version: 10.5.7 + sourceRef: + name: stable + kind: HelmRepository + interval: 5m + valuesFiles: + - values.yaml + - values-production.yaml +``` + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1beta1 +kind: HelmChart +metadata: + name: podinfo + namespace: default +spec: + chart: ./charts/podinfo + sourceRef: + name: podinfo + kind: GitRepository + interval: 10m + valuesFiles: + - ./charts/podinfo/values.yaml + - ./charts/podinfo/values-production.yaml +``` + ## Status examples Successful chart pull: