Skip to content

Commit 6405b79

Browse files
committed
enable event recording for changed conditions
1 parent 3a5387f commit 6405b79

File tree

2 files changed

+181
-35
lines changed

2 files changed

+181
-35
lines changed

pkg/conditions/updater.go

Lines changed: 165 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,46 @@
11
package conditions
22

33
import (
4+
"reflect"
45
"slices"
56
"strings"
67

8+
"github.com/openmcp-project/controller-utils/pkg/collections"
9+
corev1 "k8s.io/api/core/v1"
710
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
"k8s.io/apimachinery/pkg/runtime"
812
"k8s.io/apimachinery/pkg/util/sets"
13+
"k8s.io/client-go/tools/record"
14+
)
15+
16+
type EventVerbosity string
17+
18+
const (
19+
// EventPerChange causes one event to be recorded for each condition that has changed.
20+
// This is the most verbose setting. The old and new status of each changed condition will be visible in the event message.
21+
EventPerChange EventVerbosity = "perChange"
22+
// EventPerNewStatus causes one event to be recorded for each new status that any condition has reached.
23+
// This means that at max three events will be recorded:
24+
// - the following conditions changed to True
25+
// - the following conditions changed to False
26+
// - the following conditions changed to Unknown
27+
// The old status of the conditions will not be part of the event message.
28+
EventPerNewStatus EventVerbosity = "perNewStatus"
29+
// EventIfChanged causes a single event to be recorded if any condition's status has changed.
30+
// All changed conditions will be listed, but not their old or new status.
31+
EventIfChanged EventVerbosity = "ifChanged"
932
)
1033

1134
// conditionUpdater is a helper struct for updating a list of Conditions.
1235
// Use the ConditionUpdater constructor for initializing.
1336
type conditionUpdater struct {
14-
Now metav1.Time
15-
conditions map[string]metav1.Condition
16-
updated sets.Set[string]
17-
changed bool
37+
Now metav1.Time
38+
conditions map[string]metav1.Condition
39+
original map[string]metav1.Condition
40+
eventRecoder record.EventRecorder
41+
eventVerbosity EventVerbosity
42+
updates map[string]metav1.ConditionStatus
43+
removeUntouched bool
1844
}
1945

2046
// ConditionUpdater creates a builder-like helper struct for updating a list of Conditions.
@@ -31,19 +57,29 @@ type conditionUpdater struct {
3157
// status.conditions = ConditionUpdater(status.conditions, true).UpdateCondition(...).UpdateCondition(...).Conditions()
3258
func ConditionUpdater(conditions []metav1.Condition, removeUntouched bool) *conditionUpdater {
3359
res := &conditionUpdater{
34-
Now: metav1.Now(),
35-
conditions: make(map[string]metav1.Condition, len(conditions)),
36-
changed: false,
60+
Now: metav1.Now(),
61+
conditions: make(map[string]metav1.Condition, len(conditions)),
62+
updates: make(map[string]metav1.ConditionStatus),
63+
removeUntouched: removeUntouched,
64+
original: make(map[string]metav1.Condition, len(conditions)),
3765
}
3866
for _, con := range conditions {
3967
res.conditions[con.Type] = con
40-
}
41-
if removeUntouched {
42-
res.updated = sets.New[string]()
68+
res.original[con.Type] = con
4369
}
4470
return res
4571
}
4672

73+
// WithEventRecorder enables event recording for condition changes.
74+
// Note that this method must be called before any UpdateCondition calls, otherwise the events for the conditions will not be recorded.
75+
// The verbosity argument controls how many events are recorded and what information they contain.
76+
// If the event recorder is nil, no events will be recorded.
77+
func (c *conditionUpdater) WithEventRecorder(recorder record.EventRecorder, verbosity EventVerbosity) *conditionUpdater {
78+
c.eventRecoder = recorder
79+
c.eventVerbosity = verbosity
80+
return c
81+
}
82+
4783
// UpdateCondition updates or creates the condition with the specified type.
4884
// All fields of the condition are updated with the values given in the arguments, but the condition's LastTransitionTime is only updated (with the timestamp contained in the receiver struct) if the status changed.
4985
// Returns the receiver for easy chaining.
@@ -61,17 +97,8 @@ func (c *conditionUpdater) UpdateCondition(conType string, status metav1.Conditi
6197
// update LastTransitionTime only if status changed
6298
con.LastTransitionTime = old.LastTransitionTime
6399
}
64-
if !c.changed {
65-
if ok {
66-
c.changed = old.Status != con.Status || old.Reason != con.Reason || old.Message != con.Message
67-
} else {
68-
c.changed = true
69-
}
70-
}
100+
c.updates[conType] = status
71101
c.conditions[conType] = con
72-
if c.updated != nil {
73-
c.updated.Insert(conType)
74-
}
75102
return c
76103
}
77104

@@ -83,7 +110,8 @@ func (c *conditionUpdater) UpdateConditionFromTemplate(con metav1.Condition) *co
83110
// HasCondition returns true if a condition with the given type exists in the updated condition list.
84111
func (c *conditionUpdater) HasCondition(conType string) bool {
85112
_, ok := c.conditions[conType]
86-
return ok && (c.updated == nil || c.updated.Has(conType))
113+
_, updated := c.updates[conType]
114+
return ok && (!c.removeUntouched || updated)
87115
}
88116

89117
// RemoveCondition removes the condition with the given type from the updated condition list.
@@ -92,10 +120,7 @@ func (c *conditionUpdater) RemoveCondition(conType string) *conditionUpdater {
92120
return c
93121
}
94122
delete(c.conditions, conType)
95-
if c.updated != nil {
96-
c.updated.Delete(conType)
97-
}
98-
c.changed = true
123+
delete(c.updates, conType)
99124
return c
100125
}
101126

@@ -105,20 +130,126 @@ func (c *conditionUpdater) RemoveCondition(conType string) *conditionUpdater {
105130
// The conditions are returned sorted by their type.
106131
// The second return value indicates whether the condition list has actually changed.
107132
func (c *conditionUpdater) Conditions() ([]metav1.Condition, bool) {
133+
res := c.updatedConditions()
134+
slices.SortStableFunc(res, func(a, b metav1.Condition) int {
135+
return strings.Compare(a.Type, b.Type)
136+
})
137+
return res, c.changed(res)
138+
}
139+
140+
func (c *conditionUpdater) updatedConditions() []metav1.Condition {
108141
res := make([]metav1.Condition, 0, len(c.conditions))
109142
for _, con := range c.conditions {
110-
if c.updated == nil {
143+
if _, updated := c.updates[con.Type]; updated || !c.removeUntouched {
111144
res = append(res, con)
112-
continue
113145
}
114-
if c.updated.Has(con.Type) {
115-
res = append(res, con)
116-
} else {
117-
c.changed = true
146+
}
147+
return res
148+
}
149+
150+
func (c *conditionUpdater) changed(newCons []metav1.Condition) bool {
151+
if len(c.original) != len(newCons) {
152+
return true
153+
}
154+
for _, newCon := range newCons {
155+
oldCon, found := c.original[newCon.Type]
156+
if !found || !reflect.DeepEqual(newCon, oldCon) {
157+
return true
118158
}
119159
}
120-
slices.SortStableFunc(res, func(a, b metav1.Condition) int {
121-
return strings.Compare(a.Type, b.Type)
160+
return false
161+
}
162+
163+
// Record records events for the updated conditions on the given object.
164+
// Which events are recorded depends on the eventVerbosity setting.
165+
// In any setting, events are only recorded for conditions that have somehow changed.
166+
// This is a no-op if either the event recorder or the given object is nil.
167+
// Note that events will be duplicated if this method is called multiple times.
168+
// Returns the receiver for easy chaining.
169+
func (c *conditionUpdater) Record(obj runtime.Object) *conditionUpdater {
170+
if c.eventRecoder == nil || obj == nil {
171+
return c
172+
}
173+
174+
updatedCons := c.updatedConditions()
175+
if !c.changed(updatedCons) {
176+
// nothing to do if there are no changes
177+
return c
178+
}
179+
lostCons := collections.ProjectMapToMap(c.original, func(conType string, con metav1.Condition) (string, metav1.ConditionStatus) {
180+
return conType, con.Status
122181
})
123-
return res, c.changed
182+
for _, con := range updatedCons {
183+
delete(lostCons, con.Type)
184+
}
185+
186+
switch c.eventVerbosity {
187+
case EventPerChange:
188+
for _, con := range updatedCons {
189+
oldCon, found := c.original[con.Type]
190+
if !found {
191+
c.eventRecoder.Eventf(obj, corev1.EventTypeNormal, "Condition '%s' added with status '%s'", con.Type, string(con.Status))
192+
continue
193+
}
194+
if con.Status != oldCon.Status {
195+
c.eventRecoder.Eventf(obj, corev1.EventTypeNormal, "Condition '%s' changed from '%s' to '%s'", con.Type, string(oldCon.Status), string(con.Status))
196+
continue
197+
}
198+
}
199+
for conType, oldStatus := range lostCons {
200+
c.eventRecoder.Eventf(obj, corev1.EventTypeNormal, "Condition '%s' with status '%s' removed", conType, string(oldStatus))
201+
}
202+
203+
case EventPerNewStatus:
204+
trueCons := sets.New[string]()
205+
falseCons := sets.New[string]()
206+
unknownCons := sets.New[string]()
207+
208+
for _, con := range updatedCons {
209+
// only add conditions that have changed
210+
if oldCon, found := c.original[con.Type]; found && con.Status == oldCon.Status {
211+
continue
212+
}
213+
switch con.Status {
214+
case metav1.ConditionTrue:
215+
trueCons.Insert(con.Type)
216+
case metav1.ConditionFalse:
217+
falseCons.Insert(con.Type)
218+
case metav1.ConditionUnknown:
219+
unknownCons.Insert(con.Type)
220+
}
221+
}
222+
223+
if trueCons.Len() > 0 {
224+
c.eventRecoder.Eventf(obj, corev1.EventTypeNormal, "The following conditions changed to 'True': %s", strings.Join(sets.List(trueCons), ", "))
225+
}
226+
if falseCons.Len() > 0 {
227+
c.eventRecoder.Eventf(obj, corev1.EventTypeNormal, "The following conditions changed to 'False': %s", strings.Join(sets.List(falseCons), ", "))
228+
}
229+
if unknownCons.Len() > 0 {
230+
c.eventRecoder.Eventf(obj, corev1.EventTypeNormal, "The following conditions changed to 'Unknown': %s", strings.Join(sets.List(unknownCons), ", "))
231+
}
232+
if len(lostCons) > 0 {
233+
c.eventRecoder.Eventf(obj, corev1.EventTypeNormal, "The following conditions were removed: %s", strings.Join(sets.List(sets.KeySet(lostCons)), ", "))
234+
}
235+
236+
case EventIfChanged:
237+
changedCons := sets.New[string]()
238+
for _, con := range updatedCons {
239+
if oldCon, found := c.original[con.Type]; !found || con.Status != oldCon.Status {
240+
changedCons.Insert(con.Type)
241+
}
242+
}
243+
for conType := range lostCons {
244+
changedCons.Insert(conType)
245+
}
246+
if changedCons.Len() > 0 {
247+
c.eventRecoder.Eventf(obj, corev1.EventTypeNormal, "The following conditions have changed: %s", strings.Join(sets.List(changedCons), ", "))
248+
}
249+
}
250+
251+
ns := &corev1.Namespace{}
252+
ns.GetObjectKind()
253+
254+
return c
124255
}

pkg/controller/status_updater.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/openmcp-project/controller-utils/pkg/errors"
1111

1212
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"k8s.io/client-go/tools/record"
1314
ctrl "sigs.k8s.io/controller-runtime"
1415
"sigs.k8s.io/controller-runtime/pkg/client"
1516
)
@@ -90,6 +91,15 @@ func (b *StatusUpdaterBuilder[Obj]) WithConditionUpdater(removeUntouchedConditio
9091
return b
9192
}
9293

94+
// WithConditionEvents sets the event recorder and the verbosity that is used for recording events for changed conditions.
95+
// If the event recorder is nil, no events are recorded.
96+
// Note that this has no effect if condition updates are enabled, see WithConditionUpdater().
97+
func (b *StatusUpdaterBuilder[Obj]) WithConditionEvents(eventRecorder record.EventRecorder, verbosity conditions.EventVerbosity) *StatusUpdaterBuilder[Obj] {
98+
b.internal.eventRecorder = eventRecorder
99+
b.internal.eventVerbosity = verbosity
100+
return b
101+
}
102+
93103
// WithPhaseUpdateFunc sets the function that determines the phase of the object.
94104
// It is strongly recommended to either disable the phase field or override this function, because the default will simply set the Phase to an empty string.
95105
// The function is called with a deep copy of the object, after all other status updates have been applied (except for the custom update).
@@ -146,6 +156,8 @@ type statusUpdater[Obj client.Object] struct {
146156
phaseUpdateFunc func(obj Obj, rr ReconcileResult[Obj]) (string, error)
147157
customUpdateFunc func(obj Obj, rr ReconcileResult[Obj]) error
148158
removeUntouchedConditions bool
159+
eventRecorder record.EventRecorder
160+
eventVerbosity conditions.EventVerbosity
149161
}
150162

151163
func newStatusUpdater[Obj client.Object]() *statusUpdater[Obj] {
@@ -214,11 +226,14 @@ func (s *statusUpdater[Obj]) UpdateStatus(ctx context.Context, c client.Client,
214226
if s.fieldNames[STATUS_FIELD_CONDITIONS] != "" {
215227
oldCons := GetField(status, s.fieldNames[STATUS_FIELD_CONDITIONS], false).([]metav1.Condition)
216228
cu := conditions.ConditionUpdater(oldCons, s.removeUntouchedConditions)
229+
if s.eventRecorder != nil {
230+
cu.WithEventRecorder(s.eventRecorder, s.eventVerbosity)
231+
}
217232
cu.Now = now
218233
for _, con := range rr.Conditions {
219234
cu.UpdateConditionFromTemplate(con)
220235
}
221-
newCons, _ := cu.Conditions()
236+
newCons, _ := cu.Record(rr.Object).Conditions()
222237
SetField(status, s.fieldNames[STATUS_FIELD_CONDITIONS], newCons)
223238
}
224239
if s.fieldNames[STATUS_FIELD_PHASE] != "" {

0 commit comments

Comments
 (0)