helm-controller/controllers/helmrelease_controller.go

803 lines
27 KiB
Go

/*
Copyright 2020 The Flux CD contributors.
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 controllers
import (
"context"
"crypto/sha1"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/go-logr/logr"
"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"
"helm.sh/helm/v3/pkg/strvals"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/rest"
kuberecorder "k8s.io/client-go/tools/record"
"k8s.io/client-go/tools/reference"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"github.com/fluxcd/pkg/lockedfile"
"github.com/fluxcd/pkg/recorder"
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
v2 "github.com/fluxcd/helm-controller/api/v2alpha1"
)
// HelmReleaseReconciler reconciles a HelmRelease object
type HelmReleaseReconciler struct {
client.Client
Config *rest.Config
Log logr.Logger
Scheme *runtime.Scheme
requeueDependency time.Duration
EventRecorder kuberecorder.EventRecorder
ExternalEventRecorder *recorder.EventRecorder
}
// ConditionError represents an error with a status condition reason attached.
type ConditionError struct {
Reason string
Err error
}
func (c ConditionError) Error() string {
return c.Err.Error()
}
// +kubebuilder:rbac:groups=helm.toolkit.fluxcd.io,resources=helmreleases,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=helm.toolkit.fluxcd.io,resources=helmreleases/status,verbs=get;update;patch
func (r *HelmReleaseReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
ctx := context.Background()
start := time.Now()
var hr v2.HelmRelease
if err := r.Get(ctx, req.NamespacedName, &hr); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
log := r.Log.WithValues("controller", strings.ToLower(v2.HelmReleaseKind), "request", req.NamespacedName)
// Examine if the object is under deletion
if hr.ObjectMeta.DeletionTimestamp.IsZero() {
// The object is not being deleted, so if it does not have our finalizer,
// then lets add the finalizer and update the object. This is equivalent
// registering our finalizer.
if !containsString(hr.ObjectMeta.Finalizers, v2.HelmReleaseFinalizer) {
hr.ObjectMeta.Finalizers = append(hr.ObjectMeta.Finalizers, v2.HelmReleaseFinalizer)
if err := r.Update(ctx, &hr); err != nil {
log.Error(err, "unable to register finalizer")
return ctrl.Result{}, err
}
}
} else {
// The object is being deleted
if containsString(hr.ObjectMeta.Finalizers, v2.HelmReleaseFinalizer) {
// Our finalizer is still present, so lets handle garbage collection
if err := r.gc(ctx, log, hr); err != nil {
r.event(hr, hr.Status.LastAttemptedRevision, recorder.EventSeverityError, fmt.Sprintf("garbage collection for deleted resource failed: %s", err.Error()))
// Return the error so we retry the failed garbage collection
return ctrl.Result{}, err
}
// Remove our finalizer from the list and update it
hr.ObjectMeta.Finalizers = removeString(hr.ObjectMeta.Finalizers, v2.HelmReleaseFinalizer)
if err := r.Update(ctx, &hr); err != nil {
return ctrl.Result{}, err
}
}
// Stop reconciliation as the object is being deleted
return ctrl.Result{}, nil
}
hr, result, err := r.reconcile(ctx, log, hr)
// Update status after reconciliation.
if updateStatusErr := r.updateStatus(ctx, &hr); updateStatusErr != nil {
log.Error(updateStatusErr, "unable to update status after reconciliation")
return ctrl.Result{Requeue: true}, updateStatusErr
}
// Log reconciliation duration
log.Info(fmt.Sprintf("reconcilation finished in %s, next run in %s",
time.Now().Sub(start).String(),
hr.Spec.Interval.Duration.String(),
))
return result, err
}
func (r *HelmReleaseReconciler) reconcile(ctx context.Context, log logr.Logger, hr v2.HelmRelease) (v2.HelmRelease, ctrl.Result, error) {
// Observe HelmRelease generation.
if hr.Status.ObservedGeneration != hr.Generation {
hr.Status.ObservedGeneration = hr.Generation
hr = v2.HelmReleaseProgressing(hr)
if updateStatusErr := r.updateStatus(ctx, &hr); updateStatusErr != nil {
log.Error(updateStatusErr, "unable to update status after generation update")
return hr, ctrl.Result{Requeue: true}, updateStatusErr
}
}
if hr.Spec.Suspend {
msg := "HelmRelease is suspended, skipping reconciliation"
log.Info(msg)
return v2.HelmReleaseNotReady(hr, v2.SuspendedReason, msg), ctrl.Result{}, nil
}
// Reconcile chart based on the HelmChartTemplate
hc, ok, reconcileErr := r.reconcileChart(ctx, &hr)
if !ok {
var msg string
if reconcileErr != nil {
msg = fmt.Sprintf("chart reconciliation failed: %s", reconcileErr.Error())
r.event(hr, hr.Status.LastAttemptedRevision, recorder.EventSeverityError, msg)
} else {
msg = "HelmChart is not ready"
r.event(hr, hr.Status.LastAttemptedRevision, recorder.EventSeverityInfo, msg)
}
return v2.HelmReleaseNotReady(hr, v2.ArtifactFailedReason, msg), ctrl.Result{}, reconcileErr
}
// Check chart artifact readiness
if hc.GetArtifact() == nil {
msg := "HelmChart is not ready"
r.event(hr, hr.Status.LastAttemptedRevision, recorder.EventSeverityInfo, msg)
log.Info(msg)
return v2.HelmReleaseNotReady(hr, v2.ArtifactFailedReason, msg), ctrl.Result{}, nil
}
// Check dependencies
if len(hr.Spec.DependsOn) > 0 {
if err := r.checkDependencies(hr); err != nil {
msg := fmt.Sprintf("dependencies do not meet ready condition (%s), retrying in %s", err.Error(), r.requeueDependency.String())
r.event(hr, hc.GetArtifact().Revision, recorder.EventSeverityInfo, msg)
log.Info(msg)
// Exponential backoff would cause execution to be prolonged too much,
// instead we requeue on a fixed interval.
return v2.HelmReleaseNotReady(hr, v2.DependencyNotReadyReason, err.Error()), ctrl.Result{RequeueAfter: r.requeueDependency}, nil
}
log.Info("all dependencies are ready, proceeding with release")
}
// Compose values
values, err := r.composeValues(ctx, hr)
if err != nil {
r.event(hr, hr.Status.LastAttemptedRevision, recorder.EventSeverityError, err.Error())
return v2.HelmReleaseNotReady(hr, v2.InitFailedReason, err.Error()), ctrl.Result{}, nil
}
reconciledHr, reconcileErr := r.release(ctx, log, *hr.DeepCopy(), hc, values)
if reconcileErr != nil {
r.event(hr, hc.GetArtifact().Revision, recorder.EventSeverityError, fmt.Sprintf("reconciliation failed: %s", reconcileErr.Error()))
}
return reconciledHr, ctrl.Result{RequeueAfter: hr.Spec.Interval.Duration}, reconcileErr
}
type HelmReleaseReconcilerOptions struct {
MaxConcurrentReconciles int
DependencyRequeueInterval time.Duration
}
func (r *HelmReleaseReconciler) SetupWithManager(mgr ctrl.Manager, opts HelmReleaseReconcilerOptions) error {
r.requeueDependency = opts.DependencyRequeueInterval
return ctrl.NewControllerManagedBy(mgr).
For(&v2.HelmRelease{}).
WithEventFilter(HelmReleaseReconcileAtPredicate{}).
WithOptions(controller.Options{MaxConcurrentReconciles: opts.MaxConcurrentReconciles}).
Complete(r)
}
func (r *HelmReleaseReconciler) reconcileChart(ctx context.Context, hr *v2.HelmRelease) (*sourcev1.HelmChart, bool, error) {
chartName := types.NamespacedName{
Namespace: hr.Spec.Chart.GetNamespace(hr.Namespace),
Name: hr.GetHelmChartName(),
}
// Garbage collect the previous HelmChart if the namespace named changed.
if hr.Status.HelmChart != "" && hr.Status.HelmChart != chartName.String() {
prevChartNS, prevChartName := hr.Status.GetHelmChart()
var prevHelmChart sourcev1.HelmChart
err := r.Client.Get(ctx, types.NamespacedName{Namespace: prevChartNS, Name: prevChartName}, &prevHelmChart)
switch {
case apierrors.IsNotFound(err):
// noop
case err != nil:
return nil, false, err
default:
if err := r.Client.Delete(ctx, &prevHelmChart); err != nil {
err = fmt.Errorf("failed to garbage collect HelmChart: %w", err)
return nil, false, err
}
}
}
// Continue with the reconciliation of the current template.
var helmChart sourcev1.HelmChart
err := r.Client.Get(ctx, chartName, &helmChart)
if err != nil && !apierrors.IsNotFound(err) {
return nil, false, err
}
hc := helmChartFromTemplate(*hr)
switch {
case apierrors.IsNotFound(err):
if err = r.Client.Create(ctx, hc); err != nil {
return nil, false, err
}
hr.Status.HelmChart = chartName.String()
return nil, false, nil
case helmChartRequiresUpdate(*hr, helmChart):
r.Log.Info("chart diverged from template", strings.ToLower(sourcev1.HelmChartKind), chartName.String())
helmChart.Spec = hc.Spec
if err = r.Client.Update(ctx, &helmChart); err != nil {
return nil, false, err
}
hr.Status.HelmChart = chartName.String()
return nil, false, nil
}
return &helmChart, true, nil
}
func (r *HelmReleaseReconciler) release(ctx context.Context, 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 {
err = fmt.Errorf("lockfile error: %w", err)
return v2.HelmReleaseNotReady(hr, sourcev1.StorageOperationFailedReason, err.Error()), err
}
defer unlock()
// Create temp working dir
tmpDir, err := ioutil.TempDir("", hr.GetReleaseName())
if err != nil {
err = fmt.Errorf("tmp dir error: %w", err)
}
defer os.RemoveAll(tmpDir)
// Download artifact
artifactPath, err := download(source.GetArtifact().URL, tmpDir)
if err != nil {
return v2.HelmReleaseNotReady(hr, v2.ArtifactFailedReason, "artifact acquisition failed"), err
}
// Load chart
loadedChart, err := loader.Load(artifactPath)
if err != nil {
return v2.HelmReleaseNotReady(hr, v2.ArtifactFailedReason, "failed to load chart"), err
}
// Initialize config
cfg, err := newActionCfg(log, r.Config, hr)
if err != nil {
return v2.HelmReleaseNotReady(hr, v2.InitFailedReason, "failed to initialize Helm action configuration"), err
}
// Determine last release revision.
rel, observeLastReleaseErr := observeLastRelease(cfg, hr)
if observeLastReleaseErr != nil {
return v2.HelmReleaseNotReady(hr, v2.GetLastReleaseFailedReason, "failed to get last release revision"), err
}
// Register the current release attempt.
revision := source.GetArtifact().Revision
releaseRevision := getReleaseRevision(rel)
valuesChecksum := calculateValuesChecksum(values)
hr, hasNewState := v2.HelmReleaseAttempted(hr, revision, releaseRevision, valuesChecksum)
if hasNewState {
hr = v2.HelmReleaseProgressing(hr)
if updateStatusErr := r.updateStatus(ctx, &hr); updateStatusErr != nil {
log.Error(updateStatusErr, "unable to update status after state update")
return hr, updateStatusErr
}
}
// Determine release deployment action.
var deployAction v2.DeploymentAction
switch {
// Install if there is no release.
case rel == nil:
deployAction = hr.Spec.GetInstall()
// Fail if the release was due to a failed install (which was not uninstalled).
// The uninstall may have failed, or was not needed due to retries being exhausted
// and remediateLastFailure being false.
case hr.Spec.GetInstall().GetRemediation().GetFailureCount(hr) > 0:
return hr, fmt.Errorf("last install failed but was not uninstalled")
// Skip and mark ready if the observed state was already reconciled.
case hr.Status.ObservedStateReconciled:
return v2.HelmReleaseReady(hr), nil
// Otherwise upgrade.
default:
deployAction = hr.Spec.GetUpgrade()
}
// Check if retries exhausted.
remediation := deployAction.GetRemediation()
if remediation.RetriesExhausted(hr) {
return hr, fmt.Errorf("%s retries exhausted", deployAction.GetDescription())
}
// Deploy the release.
switch a := deployAction.(type) {
case v2.Install:
rel, err = install(cfg, loadedChart, hr, values)
err = r.handleHelmActionResult(&hr, revision, err, a.GetDescription(), v2.InstalledCondition, v2.InstallSucceededReason, v2.InstallFailedReason)
case v2.Upgrade:
rel, err = upgrade(cfg, loadedChart, hr, values)
err = r.handleHelmActionResult(&hr, revision, err, a.GetDescription(), v2.UpgradedCondition, v2.UpgradeSucceededReason, v2.UpgradeFailedReason)
}
// Run tests if enabled and there is a successful new release revision.
if getReleaseRevision(rel) > releaseRevision && err == nil && hr.Spec.GetTest().Enable {
_, testErr := test(cfg, hr)
testErr = r.handleHelmActionResult(&hr, revision, testErr, "test", v2.TestedCondition, v2.TestSucceededReason, v2.TestFailedReason)
// Propagate any test error if not marked ignored.
if testErr != nil && !remediation.MustIgnoreTestFailures(hr.Spec.GetTest().IgnoreFailures) {
err = testErr
}
}
if err != nil {
// Increment failure count for deployment action.
remediation.IncrementFailureCount(&hr)
// Remediate deployment failure if necessary.
if !remediation.RetriesExhausted(hr) || remediation.MustRemediateLastFailure() {
switch {
case getReleaseRevision(rel) <= releaseRevision:
log.Info(fmt.Sprintf("skipping remediation, no new release revision created"))
case remediation.GetStrategy() == v2.RollbackRemediationStrategy:
rollbackErr := rollback(cfg, hr)
rollbackConditionErr := r.handleHelmActionResult(&hr, revision, rollbackErr, "rollback", v2.RolledBackCondition, v2.RollbackSucceededReason, v2.RollbackFailedReason)
if rollbackConditionErr != nil {
err = rollbackConditionErr
}
case remediation.GetStrategy() == v2.UninstallRemediationStrategy:
uninstallErr := uninstall(cfg, hr)
uninstallConditionErr := r.handleHelmActionResult(&hr, revision, uninstallErr, "uninstall", v2.UninstalledCondition, v2.UninstallSucceededReason, v2.UninstallFailedReason)
if uninstallConditionErr != nil {
err = uninstallConditionErr
}
}
// Determine release after remediation.
rel, observeLastReleaseErr = observeLastRelease(cfg, hr)
if observeLastReleaseErr != nil {
err = &ConditionError{
Reason: v2.GetLastReleaseFailedReason,
Err: errors.New("failed to get last release revision after remediation"),
}
}
}
}
hr.Status.LastReleaseRevision = getReleaseRevision(rel)
if err != nil {
reason := v2.ReconciliationFailedReason
var cerr *ConditionError
if errors.As(err, &cerr) {
reason = cerr.Reason
}
return v2.HelmReleaseNotReady(hr, reason, err.Error()), err
}
return v2.HelmReleaseReady(hr), nil
}
func (r *HelmReleaseReconciler) updateStatus(ctx context.Context, hr *v2.HelmRelease) error {
hr.Status.LastObservedTime = v1.Now()
return r.Status().Update(ctx, hr)
}
func (r *HelmReleaseReconciler) checkDependencies(hr v2.HelmRelease) error {
for _, dep := range hr.Spec.DependsOn {
depName := types.NamespacedName{
Namespace: hr.GetNamespace(),
Name: dep,
}
var depHr v2.HelmRelease
err := r.Get(context.Background(), depName, &depHr)
if err != nil {
return fmt.Errorf("unable to get '%s' dependency: %w", depName, err)
}
if len(depHr.Status.Conditions) == 0 || depHr.Generation != depHr.Status.ObservedGeneration {
return fmt.Errorf("dependency '%s' is not ready", depName)
}
for _, condition := range depHr.Status.Conditions {
if condition.Type == v2.ReadyCondition && condition.Status != corev1.ConditionTrue {
return fmt.Errorf("dependency '%s' is not ready", depName)
}
}
}
return nil
}
func (r *HelmReleaseReconciler) gc(ctx context.Context, log logr.Logger, hr v2.HelmRelease) error {
// Garbage collect the HelmChart
if hr.Status.HelmChart != "" {
var hc sourcev1.HelmChart
chartNS, chartName := hr.Status.GetHelmChart()
err := r.Client.Get(ctx, types.NamespacedName{Namespace: chartNS, Name: chartName}, &hc)
switch {
case apierrors.IsNotFound(err):
// noop
case err == nil:
if err = r.Client.Delete(ctx, &hc); err != nil {
return err
}
default:
return err
}
}
// Uninstall the Helm release
var uninstallErr error
if !hr.Spec.Suspend {
cfg, err := newActionCfg(log, r.Config, hr)
if err != nil {
return err
}
_, err = cfg.Releases.Deployed(hr.GetReleaseName())
switch {
case errors.Is(err, driver.ErrNoDeployedReleases):
// noop
case err == nil:
uninstallErr = uninstall(cfg, hr)
default:
return err
}
}
switch uninstallErr {
case nil:
r.event(hr, hr.Status.LastAttemptedRevision, recorder.EventSeverityInfo, "Helm uninstall for deleted resource succeeded")
return nil
default:
return uninstallErr
}
}
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)
}
switch v.TargetPath {
case "":
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 = mergeMaps(result, values)
default:
// TODO(hidde): this is a bit of hack, as it mimics the way the option string is passed
// to Helm from a CLI perspective. Given the parser is however not publicly accessible
// while it contains all logic around parsing the target path, it is a fair trade-off.
singleVal := v.TargetPath + "=" + string(valsData)
if err := strvals.ParseInto(singleVal, result); err != nil {
return nil, fmt.Errorf("unable to merge value from key '%s' in %s '%s' into target path '%s': %w", v.GetValuesKey(), v.Kind, namespacedName, v.TargetPath, err)
}
}
}
return mergeMaps(result, hr.GetValues()), nil
}
func (r *HelmReleaseReconciler) handleHelmActionResult(hr *v2.HelmRelease, revision string, err error, action string, condition string, succeededReason string, failedReason string) error {
if err != nil {
msg := fmt.Sprintf("Helm %s failed: %s", action, err.Error())
v2.SetHelmReleaseCondition(hr, condition, corev1.ConditionFalse, failedReason, msg)
r.event(*hr, revision, recorder.EventSeverityError, msg)
return &ConditionError{Reason: failedReason, Err: errors.New(msg)}
} else {
msg := fmt.Sprintf("Helm %s succeeded", action)
v2.SetHelmReleaseCondition(hr, condition, corev1.ConditionTrue, succeededReason, msg)
r.event(*hr, revision, recorder.EventSeverityInfo, msg)
return nil
}
}
// event emits a Kubernetes event and forwards the event to notification controller if configured.
func (r *HelmReleaseReconciler) event(hr v2.HelmRelease, revision, severity, msg string) {
r.EventRecorder.Event(&hr, "Normal", severity, msg)
objRef, err := reference.GetReference(r.Scheme, &hr)
if err != nil {
r.Log.WithValues(
"request",
fmt.Sprintf("%s/%s", hr.GetNamespace(), hr.GetName()),
).Error(err, "unable to send event")
return
}
if r.ExternalEventRecorder != nil {
var meta map[string]string
if revision != "" {
meta = map[string]string{"revision": revision}
}
if err := r.ExternalEventRecorder.Eventf(*objRef, meta, severity, severity, msg); err != nil {
r.Log.WithValues(
"request",
fmt.Sprintf("%s/%s", hr.GetNamespace(), hr.GetName()),
).Error(err, "unable to send event")
return
}
}
}
func helmChartFromTemplate(hr v2.HelmRelease) *sourcev1.HelmChart {
template := hr.Spec.Chart
return &sourcev1.HelmChart{
ObjectMeta: v1.ObjectMeta{
Name: hr.GetHelmChartName(),
Namespace: hr.Spec.Chart.GetNamespace(hr.Namespace),
},
Spec: sourcev1.HelmChartSpec{
Chart: template.Spec.Chart,
Version: template.Spec.Version,
SourceRef: sourcev1.LocalHelmChartSourceReference{
Name: template.Spec.SourceRef.Name,
Kind: template.Spec.SourceRef.Kind,
},
Interval: template.GetInterval(hr.Spec.Interval),
},
}
}
func helmChartRequiresUpdate(hr v2.HelmRelease, chart sourcev1.HelmChart) bool {
template := hr.Spec.Chart
switch {
case template.Spec.Chart != chart.Spec.Chart:
return true
case template.Spec.Version != chart.Spec.Version:
return true
case template.Spec.SourceRef.Name != chart.Spec.SourceRef.Name:
return true
case template.Spec.SourceRef.Kind != chart.Spec.SourceRef.Kind:
return true
case template.GetInterval(hr.Spec.Interval) != chart.Spec.Interval:
return true
default:
return false
}
}
// observeLastRelease observes the last revision, if there is one, for for actual helm release associated with the given HelmRelease.
func observeLastRelease(cfg *action.Configuration, hr v2.HelmRelease) (*release.Release, error) {
rel, err := cfg.Releases.Last(hr.GetReleaseName())
if err != nil && errors.Is(err, driver.ErrReleaseNotFound) {
err = nil
}
return rel, err
}
// getReleaseRevision returns the revision of the given release.Release.
func getReleaseRevision(rel *release.Release) int {
if rel == nil {
return 0
}
return rel.Version
}
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()
install.Timeout = hr.Spec.GetInstall().GetTimeout(hr.GetTimeout()).Duration
install.Wait = !hr.Spec.GetInstall().DisableWait
install.DisableHooks = hr.Spec.GetInstall().DisableHooks
install.DisableOpenAPIValidation = hr.Spec.GetInstall().DisableOpenAPIValidation
install.Replace = hr.Spec.GetInstall().Replace
install.SkipCRDs = hr.Spec.GetInstall().SkipCRDs
return install.Run(chart, values.AsMap())
}
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.GetUpgrade().PreserveValues
upgrade.ReuseValues = hr.Spec.GetUpgrade().PreserveValues
upgrade.MaxHistory = hr.GetMaxHistory()
upgrade.Timeout = hr.Spec.GetUpgrade().GetTimeout(hr.GetTimeout()).Duration
upgrade.Wait = !hr.Spec.GetUpgrade().DisableWait
upgrade.DisableHooks = hr.Spec.GetUpgrade().DisableHooks
upgrade.Force = hr.Spec.GetUpgrade().Force
upgrade.CleanupOnFail = hr.Spec.GetUpgrade().CleanupOnFail
return upgrade.Run(hr.GetReleaseName(), chart, values.AsMap())
}
func test(cfg *action.Configuration, hr v2.HelmRelease) (*release.Release, error) {
test := action.NewReleaseTesting(cfg)
test.Namespace = hr.GetReleaseNamespace()
test.Timeout = hr.Spec.GetTest().GetTimeout(hr.GetTimeout()).Duration
return test.Run(hr.GetReleaseName())
}
func rollback(cfg *action.Configuration, hr v2.HelmRelease) error {
rollback := action.NewRollback(cfg)
rollback.Timeout = hr.Spec.GetRollback().GetTimeout(hr.GetTimeout()).Duration
rollback.Wait = !hr.Spec.GetRollback().DisableWait
rollback.DisableHooks = hr.Spec.GetRollback().DisableHooks
rollback.Force = hr.Spec.GetRollback().Force
rollback.Recreate = hr.Spec.GetRollback().Recreate
rollback.CleanupOnFail = hr.Spec.GetRollback().CleanupOnFail
return rollback.Run(hr.GetReleaseName())
}
func uninstall(cfg *action.Configuration, hr v2.HelmRelease) error {
uninstall := action.NewUninstall(cfg)
uninstall.Timeout = hr.Spec.GetUninstall().GetTimeout(hr.GetTimeout()).Duration
uninstall.DisableHooks = hr.Spec.GetUninstall().DisableHooks
uninstall.KeepHistory = hr.Spec.GetUninstall().KeepHistory
_, err := uninstall.Run(hr.GetReleaseName())
return err
}
func lock(name string) (unlock func(), err error) {
lockFile := path.Join(os.TempDir(), name+".lock")
mutex := lockedfile.MutexAt(lockFile)
return mutex.Lock()
}
func download(url, tmpDir string) (string, error) {
fp := filepath.Join(tmpDir, "artifact.tar.gz")
out, err := os.Create(fp)
if err != nil {
return "", err
}
defer out.Close()
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fp, fmt.Errorf("artifact '%s' download failed (status code: %s)", url, resp.Status)
}
if _, err = io.Copy(out, resp.Body); err != nil {
return "", err
}
return fp, nil
}
func newActionCfg(log logr.Logger, clusterCfg *rest.Config, hr v2.HelmRelease) (*action.Configuration, error) {
cfg := new(action.Configuration)
ns := hr.GetReleaseNamespace()
err := cfg.Init(&genericclioptions.ConfigFlags{
Namespace: &ns,
APIServer: &clusterCfg.Host,
CAFile: &clusterCfg.CAFile,
BearerToken: &clusterCfg.BearerToken,
}, hr.Namespace, "secret", actionLogger(log))
return cfg, err
}
func actionLogger(logger logr.Logger) func(format string, v ...interface{}) {
return func(format string, v ...interface{}) {
logger.Info(fmt.Sprintf(format, v...))
}
}
// mergeMaps merges map b into given map a and returns the result.
// It allows overwrites of map values with flat values, and vice versa.
// This is copied from https://github.com/helm/helm/blob/v3.3.0/pkg/cli/values/options.go#L88,
// as the public chartutil.CoalesceTables function does not allow
// overwriting maps with flat values.
func mergeMaps(a, b map[string]interface{}) map[string]interface{} {
out := make(map[string]interface{}, len(a))
for k, v := range a {
out[k] = v
}
for k, v := range b {
if v, ok := v.(map[string]interface{}); ok {
if bv, ok := out[k]; ok {
if bv, ok := bv.(map[string]interface{}); ok {
out[k] = mergeMaps(bv, v)
continue
}
}
}
out[k] = v
}
return out
}
// calculateValuesChecksum calculates the SHA1 checksum for the given Values.
func calculateValuesChecksum(values chartutil.Values) string {
s, _ := values.YAML()
return fmt.Sprintf("%x", sha1.Sum([]byte(s)))
}
func containsString(slice []string, s string) bool {
for _, item := range slice {
if item == s {
return true
}
}
return false
}
func removeString(slice []string, s string) (result []string) {
for _, item := range slice {
if item == s {
continue
}
result = append(result, item)
}
return
}