diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 1865685..3d8e324 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -66,6 +66,16 @@ jobs: kubectl -n helm-system apply -f config/testdata/dependencies kubectl -n helm-system wait helmreleases/backend --for=condition=ready --timeout=4m kubectl -n helm-system wait helmreleases/frontend --for=condition=ready --timeout=4m + - name: Run values test + run: | + kubectl -n helm-system apply -f config/testdata/valuesfrom + kubectl -n helm-system wait helmreleases/valuesfrom --for=condition=ready --timeout=4m + + RESULT=$(helm -n helm-system get values valuesfrom) + EXPECTED=$(cat config/testdata/valuesfrom/result.txt) + if [ "$RESULT" != "$EXPECTED" ]; then + echo -e "$RESULT\n\ndoes not equal\n\n$EXPECTED" + fi - name: Logs run: | kubectl -n helm-system logs deploy/source-controller diff --git a/api/v2alpha1/reference_types.go b/api/v2alpha1/reference_types.go index 9f53a2f..6834bff 100644 --- a/api/v2alpha1/reference_types.go +++ b/api/v2alpha1/reference_types.go @@ -55,3 +55,11 @@ type ValuesReference struct { // +optional ValuesKey string `json:"valuesKey,omitempty"` } + +// GetValuesKey returns the defined ValuesKey or the default ('values.yaml'). +func (in ValuesReference) GetValuesKey() string { + if in.ValuesKey == "" { + return "values.yaml" + } + return in.ValuesKey +} diff --git a/config/testdata/valuesfrom/configmap.yaml b/config/testdata/valuesfrom/configmap.yaml new file mode 100644 index 0000000..bf63440 --- /dev/null +++ b/config/testdata/valuesfrom/configmap.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: valuesfrom-config +data: + resources.limits: | + resources: + limits: + cpu: 200m + memory: 128Mi + resources.requests: | + resources: + requests: + cpu: 100m + memory: 64Mi diff --git a/config/testdata/valuesfrom/helmrelease.yaml b/config/testdata/valuesfrom/helmrelease.yaml new file mode 100644 index 0000000..3b5adbf --- /dev/null +++ b/config/testdata/valuesfrom/helmrelease.yaml @@ -0,0 +1,28 @@ +apiVersion: helm.fluxcd.io/v2alpha1 +kind: HelmRelease +metadata: + name: valuesfrom +spec: + interval: 5m + chart: + name: podinfo + version: '^4.0.0' + sourceRef: + kind: HelmRepository + name: valuesfrom + interval: 1m + test: + enable: true + rollback: + enable: true + valuesFrom: + - kind: ConfigMap + name: valuesfrom-config + valuesKey: resources.limits + - kind: ConfigMap + name: valuesfrom-config + valuesKey: resources.requests + - kind: Secret + name: valuesfrom-secret + values: + replicaCount: 2 diff --git a/config/testdata/valuesfrom/helmrepository.yaml b/config/testdata/valuesfrom/helmrepository.yaml new file mode 100644 index 0000000..b163db6 --- /dev/null +++ b/config/testdata/valuesfrom/helmrepository.yaml @@ -0,0 +1,7 @@ +apiVersion: source.fluxcd.io/v1alpha1 +kind: HelmRepository +metadata: + name: valuesfrom +spec: + interval: 1m + url: https://stefanprodan.github.io/podinfo diff --git a/config/testdata/valuesfrom/result.txt b/config/testdata/valuesfrom/result.txt new file mode 100644 index 0000000..e670960 --- /dev/null +++ b/config/testdata/valuesfrom/result.txt @@ -0,0 +1,11 @@ +USER-SUPPLIED VALUES: +replicaCount: 2 +resources: + limits: + cpu: 200m + memory: 128Mi + requests: + cpu: 100m + memory: 64Mi +serviceAccount: + enabled: true diff --git a/config/testdata/valuesfrom/secret.yaml b/config/testdata/valuesfrom/secret.yaml new file mode 100644 index 0000000..67ef616 --- /dev/null +++ b/config/testdata/valuesfrom/secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: valuesfrom-secret +type: Opaque +data: + values.yaml: c2VydmljZUFjY291bnQ6CiAgZW5hYmxlZDogdHJ1ZQo= diff --git a/controllers/helmrelease_controller.go b/controllers/helmrelease_controller.go index 9d979e4..35d4fc8 100644 --- a/controllers/helmrelease_controller.go +++ b/controllers/helmrelease_controller.go @@ -33,6 +33,7 @@ import ( "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage/driver" corev1 "k8s.io/api/core/v1" @@ -180,7 +181,19 @@ func (r *HelmReleaseReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) log.Info("all dependencies are ready, proceeding with release") } - reconciledHr, reconcileErr := r.release(log, *hr.DeepCopy(), hc) + // Compose values + values, err := r.composeValues(ctx, hr) + if err != nil { + hr = v2.HelmReleaseNotReady(hr, hr.Status.LastAttemptedRevision, hr.Status.LastReleaseRevision, v2.InitFailedReason, err.Error()) + r.event(hr, hr.Status.LastAttemptedRevision, recorder.EventSeverityError, err.Error()) + if err := r.Status().Update(ctx, &hr); err != nil { + log.Error(err, "unable to update status") + return ctrl.Result{Requeue: true}, err + } + return ctrl.Result{}, nil + } + + reconciledHr, reconcileErr := r.release(log, *hr.DeepCopy(), hc, values) if reconcileErr != nil { r.event(hr, hc.GetArtifact().Revision, recorder.EventSeverityError, fmt.Sprintf("reconciliation failed: %s", reconcileErr.Error())) } @@ -264,7 +277,7 @@ func (r *HelmReleaseReconciler) reconcileChart(ctx context.Context, hr *v2.HelmR return &helmChart, true, nil } -func (r *HelmReleaseReconciler) release(log logr.Logger, hr v2.HelmRelease, source sourcev1.Source) (v2.HelmRelease, error) { +func (r *HelmReleaseReconciler) release(log logr.Logger, hr v2.HelmRelease, source sourcev1.Source, values chartutil.Values) (v2.HelmRelease, error) { // Acquire lock unlock, err := lock(fmt.Sprintf("%s-%s", hr.GetName(), hr.GetNamespace())) if err != nil { @@ -307,11 +320,11 @@ func (r *HelmReleaseReconciler) release(log logr.Logger, hr v2.HelmRelease, sour // Install or upgrade the release success := true if errors.Is(err, driver.ErrNoDeployedReleases) { - rel, err = install(cfg, loadedChart, hr) + rel, err = install(cfg, loadedChart, hr, values) r.handleHelmActionResult(hr, source, err, "install", v2.InstalledCondition, v2.InstallSucceededReason, v2.InstallFailedReason) success = err == nil } else if v2.ShouldUpgrade(hr, source.GetArtifact().Revision, rel.Version) { - rel, err = upgrade(cfg, loadedChart, hr) + rel, err = upgrade(cfg, loadedChart, hr, values) r.handleHelmActionResult(hr, source, err, "upgrade", v2.UpgradedCondition, v2.UpgradeSucceededReason, v2.UpgradeFailedReason) success = err == nil } @@ -421,6 +434,50 @@ func (r *HelmReleaseReconciler) gc(ctx context.Context, log logr.Logger, hr v2.H } } +func (r *HelmReleaseReconciler) composeValues(ctx context.Context, hr v2.HelmRelease) (chartutil.Values, error) { + var result chartutil.Values + for _, v := range hr.Spec.ValuesFrom { + namespacedName := types.NamespacedName{Namespace: hr.Namespace, Name: v.Name} + var valsData []byte + switch v.Kind { + case "ConfigMap": + var resource corev1.ConfigMap + if err := r.Get(ctx, namespacedName, &resource); err != nil { + if apierrors.IsNotFound(err) { + return nil, fmt.Errorf("could not find %s '%s'", v.Kind, namespacedName) + } + return nil, err + } + if data, ok := resource.Data[v.GetValuesKey()]; !ok { + return nil, fmt.Errorf("missing key '%s' in %s '%s'", v.GetValuesKey(), v.Kind, namespacedName) + } else { + valsData = []byte(data) + } + case "Secret": + var resource corev1.Secret + if err := r.Get(ctx, namespacedName, &resource); err != nil { + if apierrors.IsNotFound(err) { + return nil, fmt.Errorf("could not find %s '%s'", v.Kind, namespacedName) + } + return nil, err + } + if data, ok := resource.Data[v.GetValuesKey()]; !ok { + return nil, fmt.Errorf("missing key '%s' in %s '%s'", v.GetValuesKey(), v.Kind, namespacedName) + } else { + valsData = data + } + default: + return nil, fmt.Errorf("unsupported ValuesReference kind '%s'", v.Kind) + } + values, err := chartutil.ReadValues(valsData) + if err != nil { + return nil, fmt.Errorf("unable to read values from key '%s' in %s '%s': %w", v.GetValuesKey(), v.Kind, namespacedName, err) + } + result = chartutil.CoalesceTables(result, values) + } + return chartutil.CoalesceTables(result, hr.GetValues()), nil +} + func (r *HelmReleaseReconciler) handleHelmActionResult(hr v2.HelmRelease, source sourcev1.Source, err error, action string, condition string, succeededReason string, failedReason string) { if err != nil { v2.SetHelmReleaseCondition(&hr, condition, corev1.ConditionFalse, failedReason, err.Error()) @@ -493,7 +550,7 @@ func helmChartRequiresUpdate(hr v2.HelmRelease, chart sourcev1.HelmChart) bool { } } -func install(cfg *action.Configuration, chart *chart.Chart, hr v2.HelmRelease) (*release.Release, error) { +func install(cfg *action.Configuration, chart *chart.Chart, hr v2.HelmRelease, values chartutil.Values) (*release.Release, error) { install := action.NewInstall(cfg) install.ReleaseName = hr.GetReleaseName() install.Namespace = hr.GetReleaseNamespace() @@ -504,10 +561,10 @@ func install(cfg *action.Configuration, chart *chart.Chart, hr v2.HelmRelease) ( install.Replace = hr.Spec.Install.Replace install.SkipCRDs = hr.Spec.Install.SkipCRDs - return install.Run(chart, hr.GetValues()) + return install.Run(chart, values.AsMap()) } -func upgrade(cfg *action.Configuration, chart *chart.Chart, hr v2.HelmRelease) (*release.Release, error) { +func upgrade(cfg *action.Configuration, chart *chart.Chart, hr v2.HelmRelease, values chartutil.Values) (*release.Release, error) { upgrade := action.NewUpgrade(cfg) upgrade.Namespace = hr.GetReleaseNamespace() upgrade.ResetValues = !hr.Spec.Upgrade.PreserveValues @@ -519,7 +576,7 @@ func upgrade(cfg *action.Configuration, chart *chart.Chart, hr v2.HelmRelease) ( upgrade.Force = hr.Spec.Upgrade.Force upgrade.CleanupOnFail = hr.Spec.Upgrade.CleanupOnFail - return upgrade.Run(hr.GetReleaseName(), chart, hr.GetValues()) + return upgrade.Run(hr.GetReleaseName(), chart, values.AsMap()) } func test(cfg *action.Configuration, hr v2.HelmRelease) (*release.Release, error) {