Skip to content

Commit

Permalink
Include chart annotations as event metadata
Browse files Browse the repository at this point in the history
Extend the registered event after the Helm reconciliation to include
the chart annotations (if any) in the existing metadata field of the
event body. `event` function defines now a new
 `metadata *chart.Metadata` parameter with this metadata.

The fields defined in the chart annotations are merged to the already
defined `meta` map in the `event` function, along with the already
existing `revision` field. These fields are merged at the root level;
so the `meta` map will have n + 1 fields, where n is the number of
annotations the chart has defined.

With the current notifications, is hard to be aware of what exactly was
deployed, as just the Helm chart revision is included in the payload.
If I wanted to know what specific change (or changeset) has been rolled
out, it wouldn't be possible with the current setup. A workaround could
be to abuse the chart `version` semver, but of course with several
drawbacks, like needing to keep a 1-1 relationship between the char and
app versions, having to come up with some specific encoding, having it
to decode on the other end if a generic webhook receiver has been
configured, and just probably being a bad practice.

It's probably reasonable to be able to plug some arbitrary data into
the event delivered by Flux, specially considering that the Helm charts
already provide annotations for this.

By including the chart annotations as part of the metadata, users can
enrich their notifications as they wish by including the data they
consider necessary for their own use cases.

Doing it with the chart annotations, the user experience doesn't
change, as the chart needs to be updated for making a release
anyways, and the data can be set at that point; or just left it
empty otherwise if it's not needed.

The annotations must be in string:string format according to the Helm
specification itself, so no complex nested structures are allowed.
Prior to these changes, if nested annotations are specified in the
chart, the Helm upgrade already fails with no registered event, so
there's no check done regarding this matter.

Signed-off-by: Julen Pardo <julen.pardo@datarobot.com>
  • Loading branch information
eljulians committed Aug 10, 2022
1 parent a214763 commit aec55b2
Showing 1 changed file with 31 additions and 20 deletions.
51 changes: 31 additions & 20 deletions controllers/helmrelease_controller.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/*
Copyright 2020 The Flux authors
Copyright 2022, DataRobot, Inc. Modified the original to include Helm chart
annotations as event metadata.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -225,20 +227,20 @@ func (r *HelmReleaseReconciler) reconcile(ctx context.Context, hr v2.HelmRelease
if reconcileErr != nil {
if acl.IsAccessDenied(reconcileErr) {
log.Error(reconcileErr, "access denied to cross-namespace source")
r.event(ctx, hr, hr.Status.LastAttemptedRevision, events.EventSeverityError, reconcileErr.Error())
r.event(ctx, hr, hr.Status.LastAttemptedRevision, events.EventSeverityError, reconcileErr.Error(), nil)
return v2.HelmReleaseNotReady(hr, apiacl.AccessDeniedReason, reconcileErr.Error()),
ctrl.Result{RequeueAfter: hr.Spec.Interval.Duration}, nil
}

msg := fmt.Sprintf("chart reconciliation failed: %s", reconcileErr.Error())
r.event(ctx, hr, hr.Status.LastAttemptedRevision, events.EventSeverityError, msg)
r.event(ctx, hr, hr.Status.LastAttemptedRevision, events.EventSeverityError, msg, nil)
return v2.HelmReleaseNotReady(hr, v2.ArtifactFailedReason, msg), ctrl.Result{Requeue: true}, reconcileErr
}

// Check chart readiness
if hc.Generation != hc.Status.ObservedGeneration || !apimeta.IsStatusConditionTrue(hc.Status.Conditions, meta.ReadyCondition) {
msg := fmt.Sprintf("HelmChart '%s/%s' is not ready", hc.GetNamespace(), hc.GetName())
r.event(ctx, hr, hr.Status.LastAttemptedRevision, events.EventSeverityInfo, msg)
r.event(ctx, hr, hr.Status.LastAttemptedRevision, events.EventSeverityInfo, msg, nil)
log.Info(msg)
// Do not requeue immediately, when the artifact is created
// the watcher should trigger a reconciliation.
Expand All @@ -250,7 +252,7 @@ func (r *HelmReleaseReconciler) reconcile(ctx context.Context, hr v2.HelmRelease
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(ctx, hr, hc.GetArtifact().Revision, events.EventSeverityInfo, msg)
r.event(ctx, hr, hc.GetArtifact().Revision, events.EventSeverityInfo, msg, nil)
log.Info(msg)

// Exponential backoff would cause execution to be prolonged too much,
Expand All @@ -264,22 +266,22 @@ func (r *HelmReleaseReconciler) reconcile(ctx context.Context, hr v2.HelmRelease
// Compose values
values, err := r.composeValues(ctx, hr)
if err != nil {
r.event(ctx, hr, hr.Status.LastAttemptedRevision, events.EventSeverityError, err.Error())
r.event(ctx, hr, hr.Status.LastAttemptedRevision, events.EventSeverityError, err.Error(), nil)
return v2.HelmReleaseNotReady(hr, v2.InitFailedReason, err.Error()), ctrl.Result{Requeue: true}, nil
}

// Load chart from artifact
chart, err := r.loadHelmChart(hc)
if err != nil {
r.event(ctx, hr, hr.Status.LastAttemptedRevision, events.EventSeverityError, err.Error())
r.event(ctx, hr, hr.Status.LastAttemptedRevision, events.EventSeverityError, err.Error(), nil)
return v2.HelmReleaseNotReady(hr, v2.ArtifactFailedReason, err.Error()), ctrl.Result{Requeue: true}, nil
}

// Reconcile Helm release
reconciledHr, reconcileErr := r.reconcileRelease(ctx, *hr.DeepCopy(), chart, values)
if reconcileErr != nil {
r.event(ctx, hr, hc.GetArtifact().Revision, events.EventSeverityError,
fmt.Sprintf("reconciliation failed: %s", reconcileErr.Error()))
fmt.Sprintf("reconciliation failed: %s", reconcileErr.Error()), nil)
}
return reconciledHr, ctrl.Result{RequeueAfter: hr.Spec.Interval.Duration}, reconcileErr
}
Expand Down Expand Up @@ -359,18 +361,19 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context,

// Deploy the release.
var deployAction v2.DeploymentAction

if rel == nil {
r.event(ctx, hr, revision, events.EventSeverityInfo, "Helm install has started")
r.event(ctx, hr, revision, events.EventSeverityInfo, "Helm install has started", chart.Metadata)
deployAction = hr.Spec.GetInstall()
rel, err = run.Install(hr, chart, values)
err = r.handleHelmActionResult(ctx, &hr, revision, err, deployAction.GetDescription(),
v2.ReleasedCondition, v2.InstallSucceededReason, v2.InstallFailedReason)
v2.ReleasedCondition, v2.InstallSucceededReason, v2.InstallFailedReason, chart.Metadata)
} else {
r.event(ctx, hr, revision, events.EventSeverityInfo, "Helm upgrade has started")
r.event(ctx, hr, revision, events.EventSeverityInfo, "Helm upgrade has started", chart.Metadata)
deployAction = hr.Spec.GetUpgrade()
rel, err = run.Upgrade(hr, chart, values)
err = r.handleHelmActionResult(ctx, &hr, revision, err, deployAction.GetDescription(),
v2.ReleasedCondition, v2.UpgradeSucceededReason, v2.UpgradeFailedReason)
v2.ReleasedCondition, v2.UpgradeSucceededReason, v2.UpgradeFailedReason, chart.Metadata)
}
remediation := deployAction.GetRemediation()

Expand All @@ -383,7 +386,7 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context,
if err == nil && hr.Spec.GetTest().Enable {
_, testErr := run.Test(hr)
testErr = r.handleHelmActionResult(ctx, &hr, revision, testErr, "test",
v2.TestSuccessCondition, v2.TestSucceededReason, v2.TestFailedReason)
v2.TestSuccessCondition, v2.TestSucceededReason, v2.TestFailedReason, chart.Metadata)

// Propagate any test error if not marked ignored.
if testErr != nil && !remediation.MustIgnoreTestFailures(hr.Spec.GetTest().IgnoreFailures) {
Expand Down Expand Up @@ -413,11 +416,11 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context,
case v2.RollbackRemediationStrategy:
rollbackErr := run.Rollback(hr)
remediationErr = r.handleHelmActionResult(ctx, &hr, revision, rollbackErr, "rollback",
v2.RemediatedCondition, v2.RollbackSucceededReason, v2.RollbackFailedReason)
v2.RemediatedCondition, v2.RollbackSucceededReason, v2.RollbackFailedReason, chart.Metadata)
case v2.UninstallRemediationStrategy:
uninstallErr := run.Uninstall(hr)
remediationErr = r.handleHelmActionResult(ctx, &hr, revision, uninstallErr, "uninstall",
v2.RemediatedCondition, v2.UninstallSucceededReason, v2.UninstallFailedReason)
v2.RemediatedCondition, v2.UninstallSucceededReason, v2.UninstallFailedReason, chart.Metadata)
}
if remediationErr != nil {
err = remediationErr
Expand Down Expand Up @@ -651,7 +654,7 @@ func (r *HelmReleaseReconciler) reconcileDelete(ctx context.Context, hr v2.HelmR
}

func (r *HelmReleaseReconciler) handleHelmActionResult(ctx context.Context,
hr *v2.HelmRelease, revision string, err error, action string, condition string, succeededReason string, failedReason string) error {
hr *v2.HelmRelease, revision string, err error, action string, condition string, succeededReason string, failedReason string, metadata *chart.Metadata) error {
if err != nil {
err = fmt.Errorf("Helm %s failed: %w", action, err)
msg := err.Error()
Expand All @@ -665,7 +668,7 @@ func (r *HelmReleaseReconciler) handleHelmActionResult(ctx context.Context,
Message: msg,
}
apimeta.SetStatusCondition(hr.GetStatusConditions(), newCondition)
r.event(ctx, *hr, revision, events.EventSeverityError, msg)
r.event(ctx, *hr, revision, events.EventSeverityError, msg, metadata)
return &ConditionError{Reason: failedReason, Err: err}
} else {
msg := fmt.Sprintf("Helm %s succeeded", action)
Expand All @@ -676,7 +679,7 @@ func (r *HelmReleaseReconciler) handleHelmActionResult(ctx context.Context,
Message: msg,
}
apimeta.SetStatusCondition(hr.GetStatusConditions(), newCondition)
r.event(ctx, *hr, revision, events.EventSeverityInfo, msg)
r.event(ctx, *hr, revision, events.EventSeverityInfo, msg, metadata)
return nil
}
}
Expand Down Expand Up @@ -721,10 +724,18 @@ func (r *HelmReleaseReconciler) requestsForHelmChartChange(o client.Object) []re
}

// event emits a Kubernetes event and forwards the event to notification controller if configured.
func (r *HelmReleaseReconciler) event(_ context.Context, hr v2.HelmRelease, revision, severity, msg string) {
var meta map[string]string
// If the chart contains an annotations section, it will be included in the event as part of the
// metadata.
func (r *HelmReleaseReconciler) event(_ context.Context, hr v2.HelmRelease, revision, severity, msg string, metadata *chart.Metadata) {
meta := make(map[string]string)
if revision != "" {
meta = map[string]string{v2.GroupVersion.Group + "/revision": revision}
meta[v2.GroupVersion.Group+"/revision"] = revision
}

if metadata != nil {
for key, value := range metadata.Annotations {
meta[v2.GroupVersion.Group+"/"+key] = value
}
}
eventtype := "Normal"
if severity == events.EventSeverityError {
Expand Down

0 comments on commit aec55b2

Please sign in to comment.