Skip to content

Commit

Permalink
Allow users to inject a postrenderer
Browse files Browse the repository at this point in the history
  • Loading branch information
ludydoo committed Jun 28, 2023
1 parent c16a400 commit 0b0af54
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 10 deletions.
84 changes: 78 additions & 6 deletions pkg/reconciler/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"errors"
"fmt"
"helm.sh/helm/v3/pkg/postrender"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -86,6 +87,8 @@ type Reconciler struct {
installAnnotations map[string]annotation.Install
upgradeAnnotations map[string]annotation.Upgrade
uninstallAnnotations map[string]annotation.Uninstall

postRendererFn PostRendererFn
}

// New creates a new Reconciler that reconciles custom resources that define a
Expand Down Expand Up @@ -482,6 +485,20 @@ func WithSelector(s metav1.LabelSelector) Option {
}
}

type PostRendererContext struct {
Obj *unstructured.Unstructured
Vals map[string]interface{}
}

type PostRendererFn func(ctx context.Context, renderCtx PostRendererContext) (postrender.PostRenderer, error)

func WithPostRenderer(f PostRendererFn) Option {
return func(r *Reconciler) error {
r.postRendererFn = f
return nil
}
}

// Reconcile reconciles a CR that defines a Helm v3 release.
//
// - If a release does not exist for this CR, a new release is installed.
Expand Down Expand Up @@ -580,7 +597,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.
return ctrl.Result{}, err
}

rel, state, err := r.getReleaseState(actionClient, obj, vals.AsMap())
rel, state, err := r.getReleaseState(ctx, actionClient, obj, vals.AsMap())
if err != nil {
u.UpdateStatus(
updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionTrue, conditions.ReasonErrorGettingReleaseState, err)),
Expand All @@ -600,13 +617,13 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.

switch state {
case stateNeedsInstall:
rel, err = r.doInstall(actionClient, &u, obj, vals.AsMap(), log)
rel, err = r.doInstall(ctx, actionClient, &u, obj, vals.AsMap(), log)
if err != nil {
return ctrl.Result{}, err
}

case stateNeedsUpgrade:
rel, err = r.doUpgrade(actionClient, &u, obj, vals.AsMap(), log)
rel, err = r.doUpgrade(ctx, actionClient, &u, obj, vals.AsMap(), log)
if err != nil {
return ctrl.Result{}, err
}
Expand Down Expand Up @@ -694,7 +711,7 @@ func (r *Reconciler) handleDeletion(ctx context.Context, actionClient helmclient
return nil
}

func (r *Reconciler) getReleaseState(client helmclient.ActionInterface, obj metav1.Object, vals map[string]interface{}) (*release.Release, helmReleaseState, error) {
func (r *Reconciler) getReleaseState(ctx context.Context, client helmclient.ActionInterface, obj *unstructured.Unstructured, vals map[string]interface{}) (*release.Release, helmReleaseState, error) {
currentRelease, err := client.Get(obj.GetName())
if err != nil && !errors.Is(err, driver.ErrReleaseNotFound) {
return nil, stateError, err
Expand All @@ -720,6 +737,12 @@ func (r *Reconciler) getReleaseState(client helmclient.ActionInterface, obj meta
u.DryRun = true
return nil
})

opts, err = r.appendUpgradePostRendererOption(ctx, obj, opts, vals)
if err != nil {
return nil, stateError, err
}

specRelease, err := client.Upgrade(obj.GetName(), obj.GetNamespace(), r.chrt, vals, opts...)
if err != nil {
return currentRelease, stateError, err
Expand All @@ -732,13 +755,20 @@ func (r *Reconciler) getReleaseState(client helmclient.ActionInterface, obj meta
return currentRelease, stateUnchanged, nil
}

func (r *Reconciler) doInstall(actionClient helmclient.ActionInterface, u *updater.Updater, obj *unstructured.Unstructured, vals map[string]interface{}, log logr.Logger) (*release.Release, error) {
func (r *Reconciler) doInstall(ctx context.Context, actionClient helmclient.ActionInterface, u *updater.Updater, obj *unstructured.Unstructured, vals map[string]interface{}, log logr.Logger) (*release.Release, error) {
var opts []helmclient.InstallOption
for name, annot := range r.installAnnotations {
if v, ok := obj.GetAnnotations()[name]; ok {
opts = append(opts, annot.InstallOption(v))
}
}

var err error
opts, err = r.appendInstallPostRendererOption(ctx, obj, opts, vals)
if err != nil {
return nil, err
}

rel, err := actionClient.Install(obj.GetName(), obj.GetNamespace(), r.chrt, vals, opts...)
if err != nil {
u.UpdateStatus(
Expand All @@ -759,7 +789,7 @@ func (r *Reconciler) doInstall(actionClient helmclient.ActionInterface, u *updat
return rel, nil
}

func (r *Reconciler) doUpgrade(actionClient helmclient.ActionInterface, u *updater.Updater, obj *unstructured.Unstructured, vals map[string]interface{}, log logr.Logger) (*release.Release, error) {
func (r *Reconciler) doUpgrade(ctx context.Context, actionClient helmclient.ActionInterface, u *updater.Updater, obj *unstructured.Unstructured, vals map[string]interface{}, log logr.Logger) (*release.Release, error) {
var opts []helmclient.UpgradeOption
if r.maxHistory > 0 {
opts = append(opts, func(u *action.Upgrade) error {
Expand All @@ -779,6 +809,11 @@ func (r *Reconciler) doUpgrade(actionClient helmclient.ActionInterface, u *updat
return nil, fmt.Errorf("could not get the current Helm Release: %w", err)
}

opts, err = r.appendUpgradePostRendererOption(ctx, obj, opts, vals)
if err != nil {
return nil, err
}

rel, err := actionClient.Upgrade(obj.GetName(), obj.GetNamespace(), r.chrt, vals, opts...)
if err != nil {
u.UpdateStatus(
Expand Down Expand Up @@ -959,3 +994,40 @@ func ensureDeployedRelease(u *updater.Updater, rel *release.Release) {
updater.EnsureDeployedRelease(rel),
)
}

func (r *Reconciler) getPostRenderer(ctx context.Context, obj *unstructured.Unstructured, vals map[string]interface{}) (postrender.PostRenderer, error) {
if r.postRendererFn == nil {
return nil, nil
}
var postRendererContext = PostRendererContext{
Obj: obj,
Vals: vals,
}
postRenderer, err := r.postRendererFn(ctx, postRendererContext)
if err != nil {
return nil, err
}
return postRenderer, nil
}

func (r *Reconciler) appendInstallPostRendererOption(ctx context.Context, obj *unstructured.Unstructured, opts []helmclient.InstallOption, vals map[string]interface{}) ([]helmclient.InstallOption, error) {
postRenderer, err := r.getPostRenderer(ctx, obj, vals)
if err != nil {
return nil, err
}
if postRenderer == nil {
return opts, nil
}
return append(opts, helmclient.AppendInstallPostRenderer(postRenderer)), nil
}

func (r *Reconciler) appendUpgradePostRendererOption(ctx context.Context, obj *unstructured.Unstructured, opts []helmclient.UpgradeOption, vals map[string]interface{}) ([]helmclient.UpgradeOption, error) {
postRenderer, err := r.getPostRenderer(ctx, obj, vals)
if err != nil {
return nil, err
}
if postRenderer == nil {
return opts, nil
}
return append(opts, helmclient.AppendUpgradePostRenderer(postRenderer)), nil
}
73 changes: 69 additions & 4 deletions pkg/reconciler/reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"context"
"errors"
"fmt"
"helm.sh/helm/v3/pkg/postrender"
"sort"
"strconv"
"time"

Expand Down Expand Up @@ -441,6 +443,12 @@ var _ = Describe("Reconciler", func() {
Expect(r.selectorPredicate.Generic(event.GenericEvent{Object: objUnlabeled})).To(BeFalse())
})
})
var _ = Describe("WithPostRenderer", func() {
It("should set the reconciler post renderer", func() {
Expect(WithPostRenderer(withAnnotatingPostRenderer)(r)).To(Succeed())
Expect(r.postRendererFn).NotTo(BeNil())
})
})
})

var _ = Describe("Reconcile", func() {
Expand Down Expand Up @@ -514,6 +522,7 @@ var _ = Describe("Reconciler", func() {
WithInstallAnnotations(annotation.InstallDescription{}),
WithUpgradeAnnotations(annotation.UpgradeDescription{}),
WithUninstallAnnotations(annotation.UninstallDescription{}),
WithPostRenderer(withAnnotatingPostRenderer),
WithOverrideValues(map[string]string{
"image.repository": "custom-nginx",
}),
Expand All @@ -528,6 +537,7 @@ var _ = Describe("Reconciler", func() {
WithInstallAnnotations(annotation.InstallDescription{}),
WithUpgradeAnnotations(annotation.UpgradeDescription{}),
WithUninstallAnnotations(annotation.UninstallDescription{}),
WithPostRenderer(withAnnotatingPostRenderer),
WithOverrideValues(map[string]string{
"image.repository": "custom-nginx",
}),
Expand Down Expand Up @@ -780,6 +790,13 @@ var _ = Describe("Reconciler", func() {
`Chart value "image.repository" overridden to "custom-nginx" by operator`)
})

By("verifying the postRenderer was called", func() {
objs := manifestToObjects(rel.Manifest)
for _, obj := range objs {
Expect(obj.GetAnnotations()["foo"]).To(Equal("bar"))
}
})

By("ensuring the uninstall finalizer is present", func() {
Expect(obj.GetFinalizers()).To(ContainElement(uninstallFinalizer))
})
Expand Down Expand Up @@ -1111,6 +1128,13 @@ var _ = Describe("Reconciler", func() {
`Chart value "image.repository" overridden to "custom-nginx" by operator`)
})

By("verifying the postRenderer was called", func() {
objs := manifestToObjects(rel.Manifest)
for _, obj := range objs {
Expect(obj.GetAnnotations()["foo"]).To(Equal("bar"))
}
})

By("ensuring the uninstall finalizer is present", func() {
Expect(obj.GetFinalizers()).To(ContainElement(uninstallFinalizer))
})
Expand Down Expand Up @@ -1190,10 +1214,10 @@ var _ = Describe("Reconciler", func() {

labels := u.GetLabels()
labels["app.kubernetes.io/managed-by"] = "Unmanaged"
u.SetLabels(labels)

err = mgr.GetClient().Update(ctx, u)
Expect(err).To(BeNil())
time.Sleep(1 * time.Second)
}
})

Expand Down Expand Up @@ -1348,12 +1372,20 @@ type objStatus struct {
}

func manifestToObjects(manifest string) []client.Object {
objs := []client.Object{}
for _, m := range releaseutil.SplitManifests(manifest) {
objMap := map[string]client.Object{}
var keys []string
for key, m := range releaseutil.SplitManifests(manifest) {
u := &unstructured.Unstructured{}
err := yaml.Unmarshal([]byte(m), u)
Expect(err).To(BeNil())
objs = append(objs, u)
objMap[key] = u
keys = append(keys, key)
}
// ensure that the objects are returned in a deterministic order
sort.Strings(keys)
objs := make([]client.Object, len(keys))
for i, key := range keys {
objs[i] = objMap[key]
}
return objs
}
Expand Down Expand Up @@ -1459,6 +1491,39 @@ func verifyHooksCalled(ctx context.Context, r *Reconciler, req reconcile.Request
})
}

type annotatingPostRenderer struct {
}

func withAnnotatingPostRenderer(_ context.Context, _ PostRendererContext) (postrender.PostRenderer, error) {
return &annotatingPostRenderer{}, nil
}

func (a annotatingPostRenderer) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) {
// Add foo=bar annotation to the rendered manifests.
objs := manifestToObjects(renderedManifests.String())
var manifests = make([]string, len(objs))
for i, obj := range objs {
var annotations = obj.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations["foo"] = "bar"
obj.SetAnnotations(annotations)
manifest, err := yaml.Marshal(obj)
if err != nil {
return nil, err
}
manifests[i] = string(manifest)
}
var buf bytes.Buffer
for _, manifest := range manifests {
buf.WriteString("---\n")
buf.WriteString(manifest)
}

return &buf, nil
}

func verifyEvent(ctx context.Context, cl client.Reader, obj metav1.Object, eventType, reason, message string) {
events := &v1.EventList{}
Expect(cl.List(ctx, events, client.InNamespace(obj.GetNamespace()))).To(Succeed())
Expand Down

0 comments on commit 0b0af54

Please sign in to comment.