Skip to content

Commit 0a09a81

Browse files
authored
add condition management library (openmcp-project#15)
* implement condition library * make format * conditions library now returns 'changed' flag
1 parent 7b1a8b6 commit 0a09a81

File tree

6 files changed

+492
-2
lines changed

6 files changed

+492
-2
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ bin/*
1313
*.test
1414

1515
# Output of the go coverage tool, specifically when used with LiteIDE
16-
cover.html
16+
cover*.html
1717
*.out
1818

1919
# Go workspace file

README.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,50 @@ The `pkg/collections` package contains multiple interfaces for collections, mode
4141

4242
The package also contains further packages that contain some auxiliary functions for working with slices and maps in golang, e.g. for filtering.
4343

44+
### conditions
45+
46+
The `pkg/conditions` package helps with managing condition lists.
47+
48+
The managed condition implementation must satisfy the `Condition[T comparable]` interface:
49+
```go
50+
type Condition[T comparable] interface {
51+
GetType() string
52+
SetType(conType string)
53+
GetStatus() T
54+
SetStatus(status T)
55+
GetLastTransitionTime() time.Time
56+
SetLastTransitionTime(timestamp time.Time)
57+
GetReason() string
58+
SetReason(reason string)
59+
GetMessage() string
60+
SetMessage(message string)
61+
}
62+
```
63+
64+
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`).
65+
66+
```go
67+
updater := conditions.ConditionUpdater(func() conditions.Condition[bool] { return &conImpl{} }, oldCons, false)
68+
```
69+
70+
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.
71+
72+
Use `UpdateCondition` or `UpdateConditionFromTemplate` to update a condition:
73+
```go
74+
updater.UpdateCondition("myCondition", true, "newReason", "newMessage")
75+
```
76+
77+
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.
78+
The second return value is `true` if the updated list of conditions differs from the original one.
79+
```go
80+
updatedCons, changed := updater.Conditions()
81+
```
82+
83+
For simplicity, all commands can be chained:
84+
```go
85+
updatedCons, changed := conditions.ConditionUpdater(func() conditions.Condition[bool] { return &conImpl{} }, oldCons, false).UpdateCondition("myCondition", true, "newReason", "newMessage").Conditions()
86+
```
87+
4488
### controller
4589

4690
The `pkg/controller` package contains useful functions for setting up and running k8s controllers.
@@ -51,7 +95,6 @@ The `pkg/controller` package contains useful functions for setting up and runnin
5195

5296
### logging
5397

54-
5598
This package contains the logging library from the [Landscaper controller-utils module](https://github.com/gardener/landscaper/tree/master/controller-utils/pkg/logging).
5699

57100
The library provides a wrapper around `logr.Logger`, exposing additional helper functions. The original `logr.Logger` can be retrieved by using the `Logr()` method. Also, it notices when multiple values are added to the logger with the same key - instead of simply overwriting the previous ones (like `logr.Logger` does it), it appends the key with a `_conflict(x)` suffix, where `x` is the number of times this conflict has occurred.

pkg/conditions/conditions.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package conditions
2+
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
21+
22+
// SetReason sets the reason of the condition.
23+
SetReason(reason string)
24+
// GetReason returns the reason of the condition.
25+
GetReason() string
26+
27+
// SetMessage sets the message of the condition.
28+
SetMessage(message string)
29+
// GetMessage returns the message of the condition.
30+
GetMessage() string
31+
}
32+
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+
}
40+
}
41+
return nil
42+
}
43+
44+
// AllConditionsHaveStatus returns true if all conditions have the specified status.
45+
func AllConditionsHaveStatus[T comparable](status T, conditions ...Condition[T]) bool {
46+
for _, con := range conditions {
47+
if con.GetStatus() != status {
48+
return false
49+
}
50+
}
51+
return true
52+
}

pkg/conditions/suite_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package conditions_test
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
func TestConditions(t *testing.T) {
11+
RegisterFailHandler(Fail)
12+
13+
RunSpecs(t, "Conditions Test Suite")
14+
}

pkg/conditions/updater.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package conditions
2+
3+
import (
4+
"slices"
5+
"strings"
6+
"time"
7+
8+
"k8s.io/apimachinery/pkg/util/sets"
9+
)
10+
11+
// conditionUpdater is a helper struct for updating a list of Conditions.
12+
// Use the ConditionUpdater constructor for initializing.
13+
type conditionUpdater[T comparable] struct {
14+
Now time.Time
15+
conditions map[string]Condition[T]
16+
updated sets.Set[string]
17+
constructor func() Condition[T]
18+
changed bool
19+
}
20+
21+
// ConditionUpdater creates a builder-like helper struct for updating a list of Conditions.
22+
// The 'constructor' argument is a function that returns a new (empty) instance of the condition implementation type.
23+
// The 'conditions' argument contains the old condition list.
24+
// If removeUntouched is true, the condition list returned with Conditions() will have all conditions removed that have not been updated.
25+
// If false, all conditions will be kept.
26+
// Note that calling this function stores the current time as timestamp that is used as timestamp if a condition's status changed.
27+
// To overwrite this timestamp, modify the 'Now' field of the returned struct manually.
28+
//
29+
// The given condition list is not modified.
30+
//
31+
// Usage example:
32+
// status.conditions = ConditionUpdater(status.conditions, true).UpdateCondition(...).UpdateCondition(...).Conditions()
33+
func ConditionUpdater[T comparable](constructor 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+
constructor: constructor,
38+
changed: false,
39+
}
40+
for _, con := range conditions {
41+
res.conditions[con.GetType()] = con
42+
}
43+
if removeUntouched {
44+
res.updated = sets.New[string]()
45+
}
46+
return res
47+
}
48+
49+
// UpdateCondition updates or creates the condition with the specified type.
50+
// 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.
51+
// Returns the receiver for easy chaining.
52+
func (c *conditionUpdater[T]) UpdateCondition(conType string, status T, reason, message string) *conditionUpdater[T] {
53+
con := c.constructor()
54+
con.SetType(conType)
55+
con.SetStatus(status)
56+
con.SetReason(reason)
57+
con.SetMessage(message)
58+
con.SetLastTransitionTime(c.Now)
59+
old, ok := c.conditions[conType]
60+
if ok && old.GetStatus() == con.GetStatus() {
61+
// update LastTransitionTime only if status changed
62+
con.SetLastTransitionTime(old.GetLastTransitionTime())
63+
}
64+
if !c.changed {
65+
if ok {
66+
c.changed = old.GetStatus() != con.GetStatus() || old.GetReason() != con.GetReason() || old.GetMessage() != con.GetMessage()
67+
} else {
68+
c.changed = true
69+
}
70+
}
71+
c.conditions[conType] = con
72+
if c.updated != nil {
73+
c.updated.Insert(conType)
74+
}
75+
return c
76+
}
77+
78+
// 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())
81+
}
82+
83+
// 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 {
85+
_, ok := c.conditions[conType]
86+
return ok && (c.updated == nil || c.updated.Has(conType))
87+
}
88+
89+
// RemoveCondition removes the condition with the given type from the updated condition list.
90+
func (c *conditionUpdater[T]) RemoveCondition(conType string) *conditionUpdater[T] {
91+
if !c.HasCondition(conType) {
92+
return c
93+
}
94+
delete(c.conditions, conType)
95+
if c.updated != nil {
96+
c.updated.Delete(conType)
97+
}
98+
c.changed = true
99+
return c
100+
}
101+
102+
// Conditions returns the updated condition list.
103+
// If the condition updater was initialized with removeUntouched=true, this list will only contain the conditions which have been updated
104+
// in between the condition updater creation and this method call. Otherwise, it will potentially also contain old conditions.
105+
// The conditions are returned sorted by their type.
106+
func (c *conditionUpdater[T]) Conditions() ([]Condition[T], bool) {
107+
res := make([]Condition[T], 0, len(c.conditions))
108+
for _, con := range c.conditions {
109+
if c.updated == nil {
110+
res = append(res, con)
111+
continue
112+
}
113+
if c.updated.Has(con.GetType()) {
114+
res = append(res, con)
115+
} else {
116+
c.changed = true
117+
}
118+
}
119+
slices.SortStableFunc(res, func(a, b Condition[T]) int {
120+
return strings.Compare(a.GetType(), b.GetType())
121+
})
122+
return res, c.changed
123+
}

0 commit comments

Comments
 (0)