From aec55b217e35298d728bc730a1c35821413af021 Mon Sep 17 00:00:00 2001 From: Julen Pardo Date: Sat, 23 Jul 2022 17:20:11 +0200 Subject: [PATCH] Include chart annotations as event metadata 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 --- controllers/helmrelease_controller.go | 51 ++++++++++++++++----------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/controllers/helmrelease_controller.go b/controllers/helmrelease_controller.go index 606de949b..33fabfb53 100644 --- a/controllers/helmrelease_controller.go +++ b/controllers/helmrelease_controller.go @@ -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. @@ -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. @@ -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, @@ -264,14 +266,14 @@ 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 } @@ -279,7 +281,7 @@ func (r *HelmReleaseReconciler) reconcile(ctx context.Context, hr v2.HelmRelease 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 } @@ -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() @@ -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) { @@ -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 @@ -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() @@ -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) @@ -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 } } @@ -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 {