Skip to content

Commit 201e6bd

Browse files
misbernerporridge
authored andcommitted
ROX-7242: Make the operator preserve custom statuses, and allow updating custom status through extensions (#17)
1 parent 61e6f95 commit 201e6bd

File tree

4 files changed

+111
-12
lines changed

4 files changed

+111
-12
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 & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"k8s.io/client-go/util/retry"
2929
"sigs.k8s.io/controller-runtime/pkg/client"
3030

31+
"github.com/operator-framework/helm-operator/pkg/extensions"
3132
"github.com/operator-framework/helm-operator-plugins/internal/sdk/controllerutil"
3233
"github.com/operator-framework/helm-operator-plugins/pkg/internal/status"
3334
)
@@ -56,6 +57,21 @@ func (u *Updater) UpdateStatus(fs ...UpdateStatusFunc) {
5657
u.updateStatusFuncs = append(u.updateStatusFuncs, fs...)
5758
}
5859

60+
func (u *Updater) UpdateStatusCustom(f extensions.UpdateStatusFunc) {
61+
updateFn := func(status *helmAppStatus) bool {
62+
status.updateStatusObject()
63+
64+
unstructuredStatus := unstructured.Unstructured{Object: status.StatusObject}
65+
if !f(&unstructuredStatus) {
66+
return false
67+
}
68+
_ = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredStatus.Object, status)
69+
status.StatusObject = unstructuredStatus.Object
70+
return true
71+
}
72+
u.UpdateStatus(updateFn)
73+
}
74+
5975
func (u *Updater) CancelUpdates() {
6076
u.isCanceled = true
6177
}
@@ -94,12 +110,8 @@ func (u *Updater) Apply(ctx context.Context, obj *unstructured.Unstructured) err
94110
// we remove the finalizer, updating the status will fail
95111
// because the object and its status will be garbage-collected.
96112
if needsStatusUpdate {
97-
uSt, err := runtime.DefaultUnstructuredConverter.ToUnstructured(st)
98-
if err != nil {
99-
return err
100-
}
101-
obj.Object["status"] = uSt
102-
113+
st.updateStatusObject()
114+
obj.Object["status"] = st.StatusObject
103115
if err := retryOnRetryableUpdateError(backoff, func() error {
104116
return u.client.Status().Update(ctx, obj)
105117
}); err != nil {
@@ -166,10 +178,25 @@ func RemoveDeployedRelease() UpdateStatusFunc {
166178
}
167179

168180
type helmAppStatus struct {
181+
StatusObject map[string]interface{} `json:"-"`
182+
169183
Conditions status.Conditions `json:"conditions"`
170184
DeployedRelease *helmAppRelease `json:"deployedRelease,omitempty"`
171185
}
172186

187+
func (s *helmAppStatus) updateStatusObject() {
188+
unstructuredHelmAppStatus, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(s)
189+
if s.StatusObject == nil {
190+
s.StatusObject = make(map[string]interface{})
191+
}
192+
s.StatusObject["conditions"] = unstructuredHelmAppStatus["conditions"]
193+
if deployedRelease := unstructuredHelmAppStatus["deployedRelease"]; deployedRelease != nil {
194+
s.StatusObject["deployedRelease"] = deployedRelease
195+
} else {
196+
delete(s.StatusObject, "deployedRelease")
197+
}
198+
}
199+
173200
type helmAppRelease struct {
174201
Name string `json:"name,omitempty"`
175202
Manifest string `json:"manifest,omitempty"`
@@ -192,6 +219,7 @@ func statusFor(obj *unstructured.Unstructured) *helmAppStatus {
192219
case map[string]interface{}:
193220
out := &helmAppStatus{}
194221
_ = runtime.DefaultUnstructuredConverter.FromUnstructured(s, out)
222+
out.StatusObject = s
195223
return out
196224
default:
197225
return &helmAppStatus{}

pkg/reconciler/internal/updater/updater_test.go

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,71 @@ var _ = Describe("Updater", func() {
108108
Expect((obj.Object["status"].(map[string]interface{}))["conditions"]).To(HaveLen(1))
109109
Expect(obj.GetResourceVersion()).NotTo(Equal(resourceVersion))
110110
})
111+
112+
It("should support a mix of standard and custom status updates", func() {
113+
u.UpdateStatus(EnsureCondition(conditions.Deployed(corev1.ConditionTrue, "", "")))
114+
u.UpdateStatusCustom(func(uSt *unstructured.Unstructured) bool {
115+
Expect(unstructured.SetNestedMap(uSt.Object, map[string]interface{}{"bar": "baz"}, "foo")).To(Succeed())
116+
return true
117+
})
118+
u.UpdateStatus(EnsureCondition(conditions.Irreconcilable(corev1.ConditionFalse, "", "")))
119+
u.UpdateStatusCustom(func(uSt *unstructured.Unstructured) bool {
120+
Expect(unstructured.SetNestedField(uSt.Object, "quux", "foo", "qux")).To(Succeed())
121+
return true
122+
})
123+
u.UpdateStatus(EnsureCondition(conditions.Initialized(corev1.ConditionTrue, "", "")))
124+
125+
Expect(u.Apply(context.TODO(), obj)).To(Succeed())
126+
Expect(cl.Get(context.TODO(), types.NamespacedName{Namespace: "testNamespace", Name: "testDeployment"}, obj)).To(Succeed())
127+
Expect((obj.Object["status"].(map[string]interface{}))["conditions"]).To(HaveLen(3))
128+
_, found, err := unstructured.NestedFieldNoCopy(obj.Object, "status", "deployedRelease")
129+
Expect(found).To(BeFalse())
130+
Expect(err).To(Not(HaveOccurred()))
131+
132+
val, found, err := unstructured.NestedString(obj.Object, "status", "foo", "bar")
133+
Expect(val).To(Equal("baz"))
134+
Expect(found).To(BeTrue())
135+
Expect(err).To(Not(HaveOccurred()))
136+
137+
val, found, err = unstructured.NestedString(obj.Object, "status", "foo", "qux")
138+
Expect(val).To(Equal("quux"))
139+
Expect(found).To(BeTrue())
140+
Expect(err).To(Not(HaveOccurred()))
141+
})
142+
143+
It("should preserve any custom status across multiple apply calls", func() {
144+
u.UpdateStatusCustom(func(uSt *unstructured.Unstructured) bool {
145+
Expect(unstructured.SetNestedMap(uSt.Object, map[string]interface{}{"bar": "baz"}, "foo")).To(Succeed())
146+
return true
147+
})
148+
Expect(u.Apply(context.TODO(), obj)).To(Succeed())
149+
150+
Expect(cl.Get(context.TODO(), types.NamespacedName{Namespace: "testNamespace", Name: "testDeployment"}, obj)).To(Succeed())
151+
152+
_, found, err := unstructured.NestedFieldNoCopy(obj.Object, "status", "deployedRelease")
153+
Expect(found).To(BeFalse())
154+
Expect(err).To(Not(HaveOccurred()))
155+
156+
val, found, err := unstructured.NestedString(obj.Object, "status", "foo", "bar")
157+
Expect(val).To(Equal("baz"))
158+
Expect(found).To(BeTrue())
159+
Expect(err).To(Succeed())
160+
161+
u.UpdateStatus(EnsureCondition(conditions.Deployed(corev1.ConditionTrue, "", "")))
162+
Expect(u.Apply(context.TODO(), obj)).To(Succeed())
163+
164+
Expect(cl.Get(context.TODO(), types.NamespacedName{Namespace: "testNamespace", Name: "testDeployment"}, obj)).To(Succeed())
165+
Expect((obj.Object["status"].(map[string]interface{}))["conditions"]).To(HaveLen(1))
166+
167+
_, found, err = unstructured.NestedFieldNoCopy(obj.Object, "status", "deployedRelease")
168+
Expect(found).To(BeFalse())
169+
Expect(err).To(Not(HaveOccurred()))
170+
171+
val, found, err = unstructured.NestedString(obj.Object, "status", "foo", "bar")
172+
Expect(val).To(Equal("baz"))
173+
Expect(found).To(BeTrue())
174+
Expect(err).To(Succeed())
175+
})
111176
})
112177
})
113178

@@ -244,8 +309,9 @@ var _ = Describe("statusFor", func() {
244309
})
245310

246311
It("should handle map[string]interface{}", func() {
247-
obj.Object["status"] = map[string]interface{}{}
248-
Expect(statusFor(obj)).To(Equal(&helmAppStatus{}))
312+
uSt := map[string]interface{}{}
313+
obj.Object["status"] = uSt
314+
Expect(statusFor(obj)).To(Equal(&helmAppStatus{StatusObject: uSt}))
249315
})
250316

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

pkg/reconciler/reconciler.go

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

685685
for _, ext := range r.preExtensions {
686-
if err := ext(ctx, obj, r.log); err != nil {
686+
if err := ext(ctx, obj, u.UpdateStatusCustom, r.log); err != nil {
687687
u.UpdateStatus(
688688
updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionTrue, conditions.ReasonReconcileError, err)),
689689
updater.EnsureConditionUnknown(conditions.TypeReleaseFailed),
@@ -759,7 +759,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Re
759759
}
760760

761761
for _, ext := range r.postExtensions {
762-
if err := ext(ctx, obj, r.log); err != nil {
762+
if err := ext(ctx, obj, u.UpdateStatusCustom, r.log); err != nil {
763763
u.UpdateStatus(
764764
updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionTrue, conditions.ReasonReconcileError, err)),
765765
updater.EnsureConditionUnknown(conditions.TypeReleaseFailed),
@@ -1031,7 +1031,7 @@ func (r *Reconciler) doUninstall(ctx context.Context, actionClient helmclient.Ac
10311031
}
10321032

10331033
for _, ext := range r.postExtensions {
1034-
if err := ext(ctx, obj, r.log); err != nil {
1034+
if err := ext(ctx, obj, u.UpdateStatusCustom, r.log); err != nil {
10351035
u.UpdateStatus(
10361036
updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionTrue, conditions.ReasonReconcileError, err)),
10371037
updater.EnsureConditionUnknown(conditions.TypeReleaseFailed),

0 commit comments

Comments
 (0)