Skip to content

Commit b06755e

Browse files
misbernerludydoo
authored andcommitted
ROX-7242: Make the operator preserve custom statuses, and allow updating custom status through extensions (#17)
1 parent 712a89d commit b06755e

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
@@ -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,11 +110,8 @@ func (u *Updater) Apply(ctx context.Context, obj *unstructured.Unstructured) err
94110
needsStatusUpdate = f(st) || needsStatusUpdate
95111
}
96112
if needsStatusUpdate {
97-
uSt, err := runtime.DefaultUnstructuredConverter.ToUnstructured(st)
98-
if err != nil {
99-
return err
100-
}
101-
obj.Object["status"] = uSt
113+
st.updateStatusObject()
114+
obj.Object["status"] = st.StatusObject
102115
return u.client.Status().Update(ctx, obj)
103116
}
104117
return nil
@@ -167,10 +180,25 @@ func RemoveDeployedRelease() UpdateStatusFunc {
167180
}
168181

169182
type helmAppStatus struct {
183+
StatusObject map[string]interface{} `json:"-"`
184+
170185
Conditions status.Conditions `json:"conditions"`
171186
DeployedRelease *helmAppRelease `json:"deployedRelease,omitempty"`
172187
}
173188

189+
func (s *helmAppStatus) updateStatusObject() {
190+
unstructuredHelmAppStatus, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(s)
191+
if s.StatusObject == nil {
192+
s.StatusObject = make(map[string]interface{})
193+
}
194+
s.StatusObject["conditions"] = unstructuredHelmAppStatus["conditions"]
195+
if deployedRelease := unstructuredHelmAppStatus["deployedRelease"]; deployedRelease != nil {
196+
s.StatusObject["deployedRelease"] = deployedRelease
197+
} else {
198+
delete(s.StatusObject, "deployedRelease")
199+
}
200+
}
201+
174202
type helmAppRelease struct {
175203
Name string `json:"name,omitempty"`
176204
Manifest string `json:"manifest,omitempty"`
@@ -193,6 +221,7 @@ func statusFor(obj *unstructured.Unstructured) *helmAppStatus {
193221
case map[string]interface{}:
194222
out := &helmAppStatus{}
195223
_ = runtime.DefaultUnstructuredConverter.FromUnstructured(s, out)
224+
out.StatusObject = s
196225
return out
197226
default:
198227
return &helmAppStatus{}

pkg/reconciler/internal/updater/updater_test.go

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

@@ -228,8 +293,9 @@ var _ = Describe("statusFor", func() {
228293
})
229294

230295
It("should handle map[string]interface{}", func() {
231-
obj.Object["status"] = map[string]interface{}{}
232-
Expect(statusFor(obj)).To(Equal(&helmAppStatus{}))
296+
uSt := map[string]interface{}{}
297+
obj.Object["status"] = uSt
298+
Expect(statusFor(obj)).To(Equal(&helmAppStatus{StatusObject: uSt}))
233299
})
234300

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

pkg/reconciler/reconciler.go

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

668668
for _, ext := range r.preExtensions {
669-
if err := ext(ctx, obj, r.log); err != nil {
669+
if err := ext(ctx, obj, u.UpdateStatusCustom, r.log); err != nil {
670670
u.UpdateStatus(
671671
updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionTrue, conditions.ReasonReconcileError, err)),
672672
updater.EnsureConditionUnknown(conditions.TypeReleaseFailed),
@@ -742,7 +742,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.
742742
}
743743

744744
for _, ext := range r.postExtensions {
745-
if err := ext(ctx, obj, r.log); err != nil {
745+
if err := ext(ctx, obj, u.UpdateStatusCustom, r.log); err != nil {
746746
u.UpdateStatus(
747747
updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionTrue, conditions.ReasonReconcileError, err)),
748748
updater.EnsureConditionUnknown(conditions.TypeReleaseFailed),
@@ -1011,7 +1011,7 @@ func (r *Reconciler) doUninstall(ctx context.Context, actionClient helmclient.Ac
10111011
}
10121012

10131013
for _, ext := range r.postExtensions {
1014-
if err := ext(ctx, obj, r.log); err != nil {
1014+
if err := ext(ctx, obj, u.UpdateStatusCustom, r.log); err != nil {
10151015
u.UpdateStatus(
10161016
updater.EnsureCondition(conditions.Irreconcilable(corev1.ConditionTrue, conditions.ReasonReconcileError, err)),
10171017
updater.EnsureConditionUnknown(conditions.TypeReleaseFailed),

0 commit comments

Comments
 (0)