Skip to content

Commit 43a9436

Browse files
committed
use metav1.Condition instead of a custom Condition interface in the conditions and status updater packages
1 parent 3272225 commit 43a9436

File tree

10 files changed

+491
-539
lines changed

10 files changed

+491
-539
lines changed

docs/libs/status.md

Lines changed: 23 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,19 @@
33
## Conditions
44

55
The `pkg/conditions` package helps with managing condition lists which can often be found in the status of k8s resources.
6+
It expects the conditions to be of the `Condition` type from the `k8s.io/apimachinery/pkg/apis/meta/v1` package.
67

7-
The managed condition implementation must satisfy the `Condition[T comparable]` interface:
8-
```go
9-
type Condition[T comparable] interface {
10-
GetType() string
11-
SetType(conType string)
12-
GetStatus() T
13-
SetStatus(status T)
14-
GetLastTransitionTime() time.Time
15-
SetLastTransitionTime(timestamp time.Time)
16-
GetReason() string
17-
SetReason(reason string)
18-
GetMessage() string
19-
SetMessage(message string)
20-
}
21-
```
22-
23-
To manage conditions, use the `ConditionUpdater` function and pass in a constructor function for your condition implementation and the old list of conditions. The bool argument determines whether old conditions that are not updated remain in the returned list (`false`) or are removed, so that the returned list contains only the conditions that were touched (`true`).
8+
To manage conditions, use the `ConditionUpdater` function and pass in the old list of conditions. The bool argument determines whether old conditions that are not updated remain in the returned list (`false`) or are removed, so that the returned list contains only the conditions that were touched (`true`).
249

2510
```go
26-
updater := conditions.ConditionUpdater(func() conditions.Condition[bool] { return &conImpl{} }, oldCons, false)
11+
updater := conditions.ConditionUpdater(oldCons, false)
2712
```
2813

2914
Note that the `ConditionUpdater` stores the current time upon initialization and will set each updated condition's timestamp to this value, if the status of that condition changed as a result of the update. To use a different timestamp, manually overwrite the `Now` field of the updater.
3015

3116
Use `UpdateCondition` or `UpdateConditionFromTemplate` to update a condition:
3217
```go
33-
updater.UpdateCondition("myCondition", true, "newReason", "newMessage")
18+
updater.UpdateCondition("myCondition", conditions.FromBool(true), myObj.Generation, "newReason", "newMessage")
3419
```
3520

3621
If all conditions are updated, use the `Conditions` method to generate the new list of conditions. The originally passed in list of conditions is not modified by the updater.
@@ -41,7 +26,7 @@ updatedCons, changed := updater.Conditions()
4126

4227
For simplicity, all commands can be chained:
4328
```go
44-
updatedCons, changed := conditions.ConditionUpdater(func() conditions.Condition[bool] { return &conImpl{} }, oldCons, false).UpdateCondition("myCondition", true, "newReason", "newMessage").Conditions()
29+
updatedCons, changed := conditions.ConditionUpdater(oldCons, false).UpdateCondition("myCondition", conditions.FromBool(true), myObj.Generation, "newReason", "newMessage").Conditions()
4530
```
4631

4732
## Status Updater
@@ -68,7 +53,7 @@ type MyStatus struct {
6853

6954
// Conditions contains the conditions.
7055
// +optional
71-
Conditions []MyCondition `json:"conditions,omitempty"`
56+
Conditions []metav1.Condition `json:"conditions,omitempty"`
7257
}
7358
```
7459

@@ -82,78 +67,66 @@ import (
8267
)
8368

8469
// optional, using a type alias removes the need to specify the type arguments every time
85-
type ReconcileResult = ctrlutils.ReconcileResult[*v1alpha1.MyResource, v1alpha1.ConditionStatus]
70+
type ReconcileResult = ctrlutils.ReconcileResult[*v1alpha1.MyResource]
8671

8772
// this is the method called by the controller-runtime
88-
func (r *GardenerMyResourceReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
73+
func (r *MyResourceReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
8974
rr := r.reconcile(ctx, req)
9075
// status update
91-
return ctrlutils.NewStatusUpdaterBuilder[*v1alpha1.MyResource, v1alpha1.MyResourcePhase, v1alpha1.ConditionStatus]().
92-
WithPhaseUpdateFunc(func(obj *v1alpha1.MyResource, rr ctrlutils.ReconcileResult[*v1alpha1.MyResource, v1alpha1.ConditionStatus]) (v1alpha1.MyResourcePhase, error) {
76+
return ctrlutils.NewStatusUpdaterBuilder[*v1alpha1.MyResource]().
77+
WithPhaseUpdateFunc(func(obj *v1alpha1.MyResource, rr ReconcileResult) (v1alpha1.MyResourcePhase, error) {
9378
if rr.ReconcileError != nil {
94-
return v1alpha1.PROVIDER_CONFIG_PHASE_FAILED, nil
79+
return v1alpha1.MY_PHASE_FAILED, nil
9580
}
9681
if len(rr.Conditions) > 0 {
9782
for _, con := range rr.Conditions {
9883
if con.GetStatus() != v1alpha1.CONDITION_TRUE {
99-
return v1alpha1.PROVIDER_CONFIG_PHASE_FAILED, nil
84+
return v1alpha1.MY_PHASE_FAILED, nil
10085
}
10186
}
10287
}
103-
return v1alpha1.PROVIDER_CONFIG_PHASE_SUCCEEDED, nil
88+
return v1alpha1.MY_PHASE_SUCCEEDED, nil
10489
}).
105-
WithConditionUpdater(func() conditions.Condition[v1alpha1.ConditionStatus] {
106-
return &v1alpha1.Condition{}
107-
}, true).
90+
WithConditionUpdater(true).
10891
Build().
10992
UpdateStatus(ctx, r.PlatformCluster.Client(), rr)
11093
}
11194

112-
func (r *GardenerProviderConfigReconciler) reconcile(ctx context.Context, req reconcile.Request) ReconcileResult {
95+
func (r *MyResourceReconciler) reconcile(ctx context.Context, req reconcile.Request) ReconcileResult {
11396
// actual reconcile logic here
11497
}
11598
```
11699

117-
Some information regarding the example:
118-
- `v1alpha1.MyResource` is the resource type being reconciled in this example.
119-
- `v1alpha1.MyResourcePhase` is the type of the `Phase` field used in the status of `MyResource`.
120-
- It must be a string-like type, e.g. `type MyResourcePhase string`.
121-
- If the resource status doesn't have a `Phase` or updating it is not desired, simply set this type argument to `string`.
122-
- `v1alpha1.ConditionStatus` is the type of the `Status` field within the conditions. It must be `comparable`.
123-
- Usually, this will either be a boolean or a string-like type.
124-
- If the resource status doesn't have conditions or updating them is not desired, simply set this type argument to `bool`.
125-
- The conditions must be a list of a type `T`, where either `T` or `*T` implements the `conditions.Condition[ConType]` interface.
126-
- `ConType` is `v1alpha1.ConditionStatus` in this example.
127-
100+
`v1alpha1.MyResource` is the resource type being reconciled in this example.
128101

129102
### How to use the status updater
130103

131104
It is recommended to move the actual reconciliation logic into a helper function (`reconcile` in the example). This makes it easier to ensure that the status updater is always called, no matter where the reconciliation exits, e.g. due to an error. This helper function should then return the `ReconcileResult` required by the status updater.
132105

133106
First, initialize a new `StatusUpdaterBuilder`:
134107
```go
135-
ctrlutils.NewStatusUpdaterBuilder[*v1alpha1.MyResource, v1alpha1.MyResourcePhase, v1alpha1.ConditionStatus]()
108+
ctrlutils.NewStatusUpdaterBuilder[*v1alpha1.MyResource]()
136109
```
137-
It takes the type of the reconciled resource, the type of its `Phase` attribute and the type of the `Status` attribute of its conditions as type arguments.
110+
It takes the type of the reconciled resource as type argument.
138111

139112
If you want to update the phase, you have to pass in a function that computes the new phase based on the the current state of the object and the returned reconcile result. Note that the function just has to return the phase, not to set it in the object. Failing to provide this function causes the updater to use a dummy implementation that sets the phase to the empty string.
140113
```go
141-
WithPhaseUpdateFunc(func(obj *v1alpha1.MyResource, rr ctrlutils.ReconcileResult[*v1alpha1.MyResource, v1alpha1.ConditionStatus]) (v1alpha1.MyResourcePhase, error) {
114+
WithPhaseUpdateFunc(func(obj *v1alpha1.MyResource, rr ctrlutils.ReconcileResult[*v1alpha1.MyResource]) (v1alpha1.MyResourcePhase, error) {
142115
if rr.ReconcileError != nil {
143-
return v1alpha1.PROVIDER_CONFIG_PHASE_FAILED, nil
116+
return v1alpha1.MY_PHASE_FAILED, nil
144117
}
145118
if len(rr.Conditions) > 0 {
146119
for _, con := range rr.Conditions {
147120
if con.GetStatus() != v1alpha1.CONDITION_TRUE {
148-
return v1alpha1.PROVIDER_CONFIG_PHASE_FAILED, nil
121+
return v1alpha1.MY_PHASE_FAILED, nil
149122
}
150123
}
151124
}
152-
return v1alpha1.PROVIDER_CONFIG_PHASE_SUCCEEDED, nil
125+
return v1alpha1.MY_PHASE_SUCCEEDED, nil
153126
})
154127
```
155128

156-
If the conditions should be updated, the `WithConditionUpdater` method must be called. Similarly to the condition updater from the `conditions` package - which is used internally - it requires a constructor function that returns a new, empty instance of the controller-specific `conditions.Condition` implementation. The second argument specifies whether existing conditions that are not part of the updated conditions in the `ReconcileResult` should be removed or kept.
129+
If the conditions should be updated, the `WithConditionUpdater` method must be called. The argument specifies whether existing conditions that are not part of the updated conditions in the `ReconcileResult` should be removed or kept.
157130

158131
You can then `Build()` the status updater and run `UpdateStatus()` to do the actual status update. The return values of this method are meant to be returned by the `Reconcile` function.
159132

pkg/conditions/conditions.go

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,59 @@
11
package conditions
22

3-
import "time"
4-
5-
// Condition represents a condition consisting of type, status, reason, message and a last transition timestamp.
6-
type Condition[T comparable] interface {
7-
// SetStatus sets the status of the condition.
8-
SetStatus(status T)
9-
// GetStatus returns the status of the condition.
10-
GetStatus() T
11-
12-
// SetType sets the type of the condition.
13-
SetType(conType string)
14-
// GetType returns the type of the condition.
15-
GetType() string
16-
17-
// SetLastTransitionTime sets the timestamp of the condition.
18-
SetLastTransitionTime(timestamp time.Time)
19-
// GetLastTransitionTime returns the timestamp of the condition.
20-
GetLastTransitionTime() time.Time
3+
import (
4+
apimeta "k8s.io/apimachinery/pkg/api/meta"
5+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
6+
)
7+
8+
// GetCondition is an alias for apimeta.FindStatusCondition.
9+
// It returns a pointer to the condition of the specified type from the given conditions slice, or nil if not found.
10+
func GetCondition(conditions []metav1.Condition, conditionType string) *metav1.Condition {
11+
return apimeta.FindStatusCondition(conditions, conditionType)
12+
}
2113

22-
// SetReason sets the reason of the condition.
23-
SetReason(reason string)
24-
// GetReason returns the reason of the condition.
25-
GetReason() string
14+
// FromBoolPointer returns the metav1.ConditionStatus that matches the given bool pointer.
15+
// nil = ConditionUnknown
16+
// true = ConditionTrue
17+
// false = ConditionFalse
18+
func FromBoolPointer(status *bool) metav1.ConditionStatus {
19+
if status == nil {
20+
return metav1.ConditionUnknown
21+
}
22+
if *status {
23+
return metav1.ConditionTrue
24+
}
25+
return metav1.ConditionFalse
26+
}
2627

27-
// SetMessage sets the message of the condition.
28-
SetMessage(message string)
29-
// GetMessage returns the message of the condition.
30-
GetMessage() string
28+
// FromBool returns the metav1.ConditionStatus that matches the given bool value.
29+
// true = ConditionTrue
30+
// false = ConditionFalse
31+
func FromBool(status bool) metav1.ConditionStatus {
32+
return FromBoolPointer(&status)
3133
}
3234

33-
// GetCondition returns a pointer to the condition for the given type, if it exists.
34-
// Otherwise, nil is returned.
35-
func GetCondition[T comparable](ccl []Condition[T], t string) Condition[T] {
36-
for i := range ccl {
37-
if ccl[i].GetType() == t {
38-
return ccl[i]
39-
}
35+
// ToBoolPointer is the inverse of FromBoolPointer.
36+
// It returns a pointer to a bool that matches the given ConditionStatus.
37+
// If the status is ConditionTrue, it returns a pointer to true.
38+
// If the status is ConditionFalse, it returns a pointer to false.
39+
// If the status is ConditionUnknown or any unknown value, it returns nil.
40+
func ToBoolPointer(status metav1.ConditionStatus) *bool {
41+
var res *bool
42+
switch status {
43+
case metav1.ConditionTrue:
44+
tmp := true
45+
res = &tmp
46+
case metav1.ConditionFalse:
47+
tmp := false
48+
res = &tmp
4049
}
41-
return nil
50+
return res
4251
}
4352

4453
// AllConditionsHaveStatus returns true if all conditions have the specified status.
45-
func AllConditionsHaveStatus[T comparable](status T, conditions ...Condition[T]) bool {
54+
func AllConditionsHaveStatus(status metav1.ConditionStatus, conditions ...metav1.Condition) bool {
4655
for _, con := range conditions {
47-
if con.GetStatus() != status {
56+
if con.Status != status {
4857
return false
4958
}
5059
}

pkg/conditions/updater.go

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,17 @@ package conditions
33
import (
44
"slices"
55
"strings"
6-
"time"
76

7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
88
"k8s.io/apimachinery/pkg/util/sets"
99
)
1010

1111
// conditionUpdater is a helper struct for updating a list of Conditions.
1212
// Use the ConditionUpdater constructor for initializing.
13-
type conditionUpdater[T comparable] struct {
14-
Now time.Time
15-
conditions map[string]Condition[T]
13+
type conditionUpdater struct {
14+
Now metav1.Time
15+
conditions map[string]metav1.Condition
1616
updated sets.Set[string]
17-
construct func() Condition[T]
1817
changed bool
1918
}
2019

@@ -30,15 +29,14 @@ type conditionUpdater[T comparable] struct {
3029
//
3130
// Usage example:
3231
// status.conditions = ConditionUpdater(status.conditions, true).UpdateCondition(...).UpdateCondition(...).Conditions()
33-
func ConditionUpdater[T comparable](construct func() Condition[T], conditions []Condition[T], removeUntouched bool) *conditionUpdater[T] {
34-
res := &conditionUpdater[T]{
35-
Now: time.Now(),
36-
conditions: make(map[string]Condition[T], len(conditions)),
37-
construct: construct,
32+
func ConditionUpdater(conditions []metav1.Condition, removeUntouched bool) *conditionUpdater {
33+
res := &conditionUpdater{
34+
Now: metav1.Now(),
35+
conditions: make(map[string]metav1.Condition, len(conditions)),
3836
changed: false,
3937
}
4038
for _, con := range conditions {
41-
res.conditions[con.GetType()] = con
39+
res.conditions[con.Type] = con
4240
}
4341
if removeUntouched {
4442
res.updated = sets.New[string]()
@@ -49,21 +47,23 @@ func ConditionUpdater[T comparable](construct func() Condition[T], conditions []
4947
// UpdateCondition updates or creates the condition with the specified type.
5048
// 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.
5149
// Returns the receiver for easy chaining.
52-
func (c *conditionUpdater[T]) UpdateCondition(conType string, status T, reason, message string) *conditionUpdater[T] {
53-
con := c.construct()
54-
con.SetType(conType)
55-
con.SetStatus(status)
56-
con.SetReason(reason)
57-
con.SetMessage(message)
58-
con.SetLastTransitionTime(c.Now)
50+
func (c *conditionUpdater) UpdateCondition(conType string, status metav1.ConditionStatus, observedGeneration int64, reason, message string) *conditionUpdater {
51+
con := metav1.Condition{
52+
Type: conType,
53+
Status: status,
54+
Reason: reason,
55+
Message: message,
56+
ObservedGeneration: observedGeneration,
57+
LastTransitionTime: c.Now,
58+
}
5959
old, ok := c.conditions[conType]
60-
if ok && old.GetStatus() == con.GetStatus() {
60+
if ok && old.Status == con.Status {
6161
// update LastTransitionTime only if status changed
62-
con.SetLastTransitionTime(old.GetLastTransitionTime())
62+
con.LastTransitionTime = old.LastTransitionTime
6363
}
6464
if !c.changed {
6565
if ok {
66-
c.changed = old.GetStatus() != con.GetStatus() || old.GetReason() != con.GetReason() || old.GetMessage() != con.GetMessage()
66+
c.changed = old.Status != con.Status || old.Reason != con.Reason || old.Message != con.Message
6767
} else {
6868
c.changed = true
6969
}
@@ -76,18 +76,18 @@ func (c *conditionUpdater[T]) UpdateCondition(conType string, status T, reason,
7676
}
7777

7878
// UpdateConditionFromTemplate is a convenience wrapper around UpdateCondition which allows it to be called with a preconstructed ComponentCondition.
79-
func (c *conditionUpdater[T]) UpdateConditionFromTemplate(con Condition[T]) *conditionUpdater[T] {
80-
return c.UpdateCondition(con.GetType(), con.GetStatus(), con.GetReason(), con.GetMessage())
79+
func (c *conditionUpdater) UpdateConditionFromTemplate(con metav1.Condition) *conditionUpdater {
80+
return c.UpdateCondition(con.Type, con.Status, con.ObservedGeneration, con.Reason, con.Message)
8181
}
8282

8383
// HasCondition returns true if a condition with the given type exists in the updated condition list.
84-
func (c *conditionUpdater[T]) HasCondition(conType string) bool {
84+
func (c *conditionUpdater) HasCondition(conType string) bool {
8585
_, ok := c.conditions[conType]
8686
return ok && (c.updated == nil || c.updated.Has(conType))
8787
}
8888

8989
// RemoveCondition removes the condition with the given type from the updated condition list.
90-
func (c *conditionUpdater[T]) RemoveCondition(conType string) *conditionUpdater[T] {
90+
func (c *conditionUpdater) RemoveCondition(conType string) *conditionUpdater {
9191
if !c.HasCondition(conType) {
9292
return c
9393
}
@@ -104,21 +104,21 @@ func (c *conditionUpdater[T]) RemoveCondition(conType string) *conditionUpdater[
104104
// in between the condition updater creation and this method call. Otherwise, it will potentially also contain old conditions.
105105
// The conditions are returned sorted by their type.
106106
// The second return value indicates whether the condition list has actually changed.
107-
func (c *conditionUpdater[T]) Conditions() ([]Condition[T], bool) {
108-
res := make([]Condition[T], 0, len(c.conditions))
107+
func (c *conditionUpdater) Conditions() ([]metav1.Condition, bool) {
108+
res := make([]metav1.Condition, 0, len(c.conditions))
109109
for _, con := range c.conditions {
110110
if c.updated == nil {
111111
res = append(res, con)
112112
continue
113113
}
114-
if c.updated.Has(con.GetType()) {
114+
if c.updated.Has(con.Type) {
115115
res = append(res, con)
116116
} else {
117117
c.changed = true
118118
}
119119
}
120-
slices.SortStableFunc(res, func(a, b Condition[T]) int {
121-
return strings.Compare(a.GetType(), b.GetType())
120+
slices.SortStableFunc(res, func(a, b metav1.Condition) int {
121+
return strings.Compare(a.Type, b.Type)
122122
})
123123
return res, c.changed
124124
}

0 commit comments

Comments
 (0)