11package conditions
22
33import (
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.
1336type 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()
3258func 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.
84111func (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.
107132func (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}
0 commit comments