Skip to content

Commit 7857f66

Browse files
authored
ROX-7242: Make the operator preserve custom statuses, and allow updating custom status through extensions (#17)
1 parent e0130a4 commit 7857f66

File tree

4 files changed

+111
-11
lines changed

4 files changed

+111
-11
lines changed

pkg/extensions/types.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@ package extensions
22

33
import (
44
"context"
5+
56
"github.com/go-logr/logr"
67
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
78
)
89

10+
// UpdateStatusFunc is a function that updates an unstructured status. If the status has been modified,
11+
// true must be returned, false otherwise.
12+
type UpdateStatusFunc func(*unstructured.Unstructured) bool
13+
914
// ReconcileExtension is an arbitrary extension that can be implemented to run either before
1015
// or after the main Helm reconciliation action.
1116
// An error returned by a ReconcileExtension will cause the Reconcile to fail, unlike a hook error.
12-
type ReconcileExtension func(context.Context, *unstructured.Unstructured, logr.Logger) error
17+
type ReconcileExtension func(context.Context, *unstructured.Unstructured, func(UpdateStatusFunc), logr.Logger) error

pkg/reconciler/internal/updater/updater.go

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"k8s.io/client-go/util/retry"
2727
"sigs.k8s.io/controller-runtime/pkg/client"
2828

29+
"github.com/joelanford/helm-operator/pkg/extensions"
2930
"github.com/joelanford/helm-operator/pkg/internal/sdk/controllerutil"
3031
"github.com/joelanford/helm-operator/pkg/internal/sdk/status"
3132
)
@@ -53,6 +54,21 @@ func (u *Updater) UpdateStatus(fs ...UpdateStatusFunc) {
5354
u.updateStatusFuncs = append(u.updateStatusFuncs, fs...)
5455
}
5556

57+
func (u *Updater) UpdateStatusCustom(f extensions.UpdateStatusFunc) {
58+
updateFn := func(status *helmAppStatus) bool {
59+
status.updateStatusObject()
60+
61+
unstructuredStatus := unstructured.Unstructured{Object: status.StatusObject}
62+
if !f(&unstructuredStatus) {
63+
return false
64+
}
65+
_ = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredStatus.Object, status)
66+
status.StatusObject = unstructuredStatus.Object
67+
return true
68+
}
69+
u.UpdateStatus(updateFn)
70+
}
71+
5672
func (u *Updater) Apply(ctx context.Context, obj *unstructured.Unstructured) error {
5773
backoff := retry.DefaultRetry
5874

@@ -66,11 +82,8 @@ func (u *Updater) Apply(ctx context.Context, obj *unstructured.Unstructured) err
6682
needsStatusUpdate = f(st) || needsStatusUpdate
6783
}
6884
if needsStatusUpdate {
69-
uSt, err := runtime.DefaultUnstructuredConverter.ToUnstructured(st)
70-
if err != nil {
71-
return err
72-
}
73-
obj.Object["status"] = uSt
85+
st.updateStatusObject()
86+
obj.Object["status"] = st.StatusObject
7487
return u.client.Status().Update(ctx, obj)
7588
}
7689
return nil
@@ -149,10 +162,25 @@ func RemoveDeployedRelease() UpdateStatusFunc {
149162
}
150163

151164
type helmAppStatus struct {
165+
StatusObject map[string]interface{} `json:"-"`
166+
152167
Conditions status.Conditions `json:"conditions"`
153168
DeployedRelease *helmAppRelease `json:"deployedRelease,omitempty"`
154169
}
155170

171+
func (s *helmAppStatus) updateStatusObject() {
172+
unstructuredHelmAppStatus, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(s)
173+
if s.StatusObject == nil {
174+
s.StatusObject = make(map[string]interface{})
175+
}
176+
s.StatusObject["conditions"] = unstructuredHelmAppStatus["conditions"]
177+
if deployedRelease := unstructuredHelmAppStatus["deployedRelease"]; deployedRelease != nil {
178+
s.StatusObject["deployedRelease"] = deployedRelease
179+
} else {
180+
delete(s.StatusObject, "deployedRelease")
181+
}
182+
}
183+
156184
type helmAppRelease struct {
157185
Name string `json:"name,omitempty"`
158186
Manifest string `json:"manifest,omitempty"`
@@ -175,6 +203,7 @@ func statusFor(obj *unstructured.Unstructured) *helmAppStatus {
175203
case map[string]interface{}:
176204
out := &helmAppStatus{}
177205
_ = runtime.DefaultUnstructuredConverter.FromUnstructured(s, out)
206+
out.StatusObject = s
178207
return out
179208
default:
180209
return &helmAppStatus{}

pkg/reconciler/internal/updater/updater_test.go

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,71 @@ var _ = Describe("Updater", func() {
8686
Expect((obj.Object["status"].(map[string]interface{}))["conditions"]).To(HaveLen(1))
8787
Expect(obj.GetResourceVersion()).NotTo(Equal(resourceVersion))
8888
})
89+
90+
It("should support a mix of standard and custom status updates", func() {
91+
u.UpdateStatus(EnsureCondition(conditions.Deployed(corev1.ConditionTrue, "", "")))
92+
u.UpdateStatusCustom(func(uSt *unstructured.Unstructured) bool {
93+
Expect(unstructured.SetNestedMap(uSt.Object, map[string]interface{}{"bar": "baz"}, "foo")).To(Succeed())
94+
return true
95+
})
96+
u.UpdateStatus(EnsureCondition(conditions.Irreconcilable(corev1.ConditionFalse, "", "")))
97+
u.UpdateStatusCustom(func(uSt *unstructured.Unstructured) bool {
98+
Expect(unstructured.SetNestedField(uSt.Object, "quux", "foo", "qux")).To(Succeed())
99+
return true
100+
})
101+
u.UpdateStatus(EnsureCondition(conditions.Initialized(corev1.ConditionTrue, "", "")))
102+
103+
Expect(u.Apply(context.TODO(), obj)).To(Succeed())
104+
Expect(client.Get(context.TODO(), types.NamespacedName{Namespace: "testNamespace", Name: "testDeployment"}, obj)).To(Succeed())
105+
Expect((obj.Object["status"].(map[string]interface{}))["conditions"]).To(HaveLen(3))
106+
_, found, err := unstructured.NestedFieldNoCopy(obj.Object, "status", "deployedRelease")
107+
Expect(found).To(BeFalse())
108+
Expect(err).To(Not(HaveOccurred()))
109+
110+
val, found, err := unstructured.NestedString(obj.Object, "status", "foo", "bar")
111+
Expect(val).To(Equal("baz"))
112+
Expect(found).To(BeTrue())
113+
Expect(err).To(Not(HaveOccurred()))
114+
115+
val, found, err = unstructured.NestedString(obj.Object, "status", "foo", "qux")
116+
Expect(val).To(Equal("quux"))
117+
Expect(found).To(BeTrue())
118+
Expect(err).To(Not(HaveOccurred()))
119+
})
120+
121+
It("should preserve any custom status across multiple apply calls", func() {
122+
u.UpdateStatusCustom(func(uSt *unstructured.Unstructured) bool {
123+
Expect(unstructured.SetNestedMap(uSt.Object, map[string]interface{}{"bar": "baz"}, "foo")).To(Succeed())
124+
return true
125+
})
126+
Expect(u.Apply(context.TODO(), obj)).To(Succeed())
127+
128+
Expect(client.Get(context.TODO(), types.NamespacedName{Namespace: "testNamespace", Name: "testDeployment"}, obj)).To(Succeed())
129+
130+
_, found, err := unstructured.NestedFieldNoCopy(obj.Object, "status", "deployedRelease")
131+
Expect(found).To(BeFalse())
132+
Expect(err).To(Not(HaveOccurred()))
133+
134+
val, found, err := unstructured.NestedString(obj.Object, "status", "foo", "bar")
135+
Expect(val).To(Equal("baz"))
136+
Expect(found).To(BeTrue())
137+
Expect(err).To(Succeed())
138+
139+
u.UpdateStatus(EnsureCondition(conditions.Deployed(corev1.ConditionTrue, "", "")))
140+
Expect(u.Apply(context.TODO(), obj)).To(Succeed())
141+
142+
Expect(client.Get(context.TODO(), types.NamespacedName{Namespace: "testNamespace", Name: "testDeployment"}, obj)).To(Succeed())
143+
Expect((obj.Object["status"].(map[string]interface{}))["conditions"]).To(HaveLen(1))
144+
145+
_, found, err = unstructured.NestedFieldNoCopy(obj.Object, "status", "deployedRelease")
146+
Expect(found).To(BeFalse())
147+
Expect(err).To(Not(HaveOccurred()))
148+
149+
val, found, err = unstructured.NestedString(obj.Object, "status", "foo", "bar")
150+
Expect(val).To(Equal("baz"))
151+
Expect(found).To(BeTrue())
152+
Expect(err).To(Succeed())
153+
})
89154
})
90155
})
91156

@@ -241,8 +306,9 @@ var _ = Describe("statusFor", func() {
241306
})
242307

243308
It("should handle map[string]interface{}", func() {
244-
obj.Object["status"] = map[string]interface{}{}
245-
Expect(statusFor(obj)).To(Equal(&helmAppStatus{}))
309+
uSt := map[string]interface{}{}
310+
obj.Object["status"] = uSt
311+
Expect(statusFor(obj)).To(Equal(&helmAppStatus{StatusObject: uSt}))
246312
})
247313

248314
It("should handle arbitrary types", func() {

pkg/reconciler/reconciler.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.
546546
u.UpdateStatus(updater.EnsureCondition(conditions.Initialized(corev1.ConditionTrue, "", "")))
547547

548548
for _, ext := range r.preExtensions {
549-
if err := ext(ctx, obj, r.log); err != nil {
549+
if err := ext(ctx, obj, u.UpdateStatusCustom, r.log); err != nil {
550550
u.UpdateStatus(
551551
updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionTrue, conditions.ReasonReconcileError, err)),
552552
updater.EnsureConditionUnknown(conditions.TypeReleaseFailed),
@@ -619,7 +619,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.
619619
}
620620

621621
for _, ext := range r.postExtensions {
622-
if err := ext(ctx, obj, r.log); err != nil {
622+
if err := ext(ctx, obj, u.UpdateStatusCustom, r.log); err != nil {
623623
u.UpdateStatus(
624624
updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionTrue, conditions.ReasonReconcileError, err)),
625625
updater.EnsureConditionUnknown(conditions.TypeReleaseFailed),
@@ -868,7 +868,7 @@ func (r *Reconciler) doUninstall(ctx context.Context, actionClient helmclient.Ac
868868
}
869869

870870
for _, ext := range r.postExtensions {
871-
if err := ext(ctx, obj, r.log); err != nil {
871+
if err := ext(ctx, obj, u.UpdateStatusCustom, r.log); err != nil {
872872
u.UpdateStatus(
873873
updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionTrue, conditions.ReasonReconcileError, err)),
874874
updater.EnsureConditionUnknown(conditions.TypeReleaseFailed),

0 commit comments

Comments
 (0)