Skip to content

Commit 1a138cf

Browse files
authored
Merge pull request #459 from n3wscott/conditions-metav1
Fork and update of Condition Sets from Knative, attributed
2 parents 2a1fab2 + c15b405 commit 1a138cf

8 files changed

+1714
-2
lines changed

Diff for: api/v1alpha1/conditions.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
package v1alpha1
1515

1616
import (
17-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1817
"slices"
18+
19+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1920
)
2021

2122
// ConditionType is a type of condition for a resource.

Diff for: go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/go-logr/logr v1.4.2
77
github.com/gobuffalo/flect v1.0.2
88
github.com/google/cel-go v0.24.1
9+
github.com/google/go-cmp v0.6.0
910
github.com/onsi/ginkgo/v2 v2.20.0
1011
github.com/onsi/gomega v1.34.1
1112
github.com/prometheus/client_golang v1.19.1
@@ -47,7 +48,6 @@ require (
4748
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
4849
github.com/golang/protobuf v1.5.4 // indirect
4950
github.com/google/gnostic-models v0.6.8 // indirect
50-
github.com/google/go-cmp v0.6.0 // indirect
5151
github.com/google/gofuzz v1.2.0 // indirect
5252
github.com/google/licenseclassifier/v2 v2.0.0 // indirect
5353
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect

Diff for: pkg/apis/condition.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2025 The Kube Resource Orchestrator Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
// Inspired by https://github.com/knative/pkg/tree/97c7258e3a98b81459936bc7a29dc6a9540fa357/apis,
15+
// but we chose to diverge due to the unacceptably large dependency closure of knative/pkg.
16+
17+
package apis
18+
19+
import (
20+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
"sigs.k8s.io/controller-runtime/pkg/client"
22+
)
23+
24+
// Condition aliases the upstream type and adds additional helper methods
25+
type Condition metav1.Condition
26+
27+
type Object interface {
28+
client.Object
29+
GetConditions() []Condition
30+
SetConditions([]Condition)
31+
}
32+
33+
// ConditionType is an upper-camel-cased condition type.
34+
type ConditionType string
35+
36+
const (
37+
// ConditionReady specifies that the resource is ready.
38+
// For long-running resources.
39+
ConditionReady = "Ready"
40+
// ConditionSucceeded specifies that the resource has finished.
41+
// For resource which run to completion.
42+
ConditionSucceeded = "Succeeded"
43+
)
44+
45+
func (c *Condition) IsTrue() bool {
46+
if c == nil {
47+
return false
48+
}
49+
return c.Status == metav1.ConditionTrue
50+
}
51+
52+
func (c *Condition) IsFalse() bool {
53+
if c == nil {
54+
return false
55+
}
56+
return c.Status == metav1.ConditionFalse
57+
}
58+
59+
func (c *Condition) IsUnknown() bool {
60+
if c == nil {
61+
return true
62+
}
63+
return c.Status == metav1.ConditionUnknown
64+
}
65+
66+
func (c *Condition) GetStatus() metav1.ConditionStatus {
67+
if c == nil {
68+
return metav1.ConditionUnknown
69+
}
70+
return c.Status
71+
}

Diff for: pkg/apis/condition_set.go

+272
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
// Copyright 2025 The Kube Resource Orchestrator Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
// Inspired by https://github.com/knative/pkg/tree/97c7258e3a98b81459936bc7a29dc6a9540fa357/apis,
15+
// but we chose to diverge due to the unacceptably large dependency closure of knative/pkg.
16+
17+
package apis
18+
19+
import (
20+
"fmt"
21+
"reflect"
22+
"sort"
23+
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
)
26+
27+
// ConditionSet provides methods for evaluating Conditions.
28+
// +k8s:deepcopy-gen=false
29+
type ConditionSet struct {
30+
ConditionTypes
31+
object Object
32+
}
33+
34+
// Root returns the root Condition, typically "Ready" or "Succeeded"
35+
func (c ConditionSet) Root() *Condition {
36+
if c.object == nil {
37+
return nil
38+
}
39+
return c.Get(c.root)
40+
}
41+
42+
func (c ConditionSet) List() []Condition {
43+
if c.object == nil {
44+
return nil
45+
}
46+
return c.object.GetConditions()
47+
}
48+
49+
// Get finds and returns the Condition that matches the ConditionType
50+
// previously set on Conditions.
51+
func (c ConditionSet) Get(t string) *Condition {
52+
if c.object == nil {
53+
return nil
54+
}
55+
for _, c := range c.object.GetConditions() {
56+
if c.Type == t {
57+
return &c
58+
}
59+
}
60+
return nil
61+
}
62+
63+
// IsTrue returns true if all condition types are true.
64+
func (c ConditionSet) IsTrue(conditionTypes ...string) bool {
65+
for _, conditionType := range conditionTypes {
66+
if !c.Get(conditionType).IsTrue() {
67+
return false
68+
}
69+
}
70+
return true
71+
}
72+
73+
// IsDependentCondition returns true if the provided condition is involved in calculating the root condition.
74+
func (c ConditionSet) IsDependentCondition(t string) bool {
75+
for _, cond := range c.dependents {
76+
if cond == t {
77+
return true
78+
}
79+
}
80+
return t == c.root
81+
}
82+
83+
// Set sets or updates the Condition on Conditions for Condition.Type.
84+
// If there is an update, Conditions are stored back sorted.
85+
func (c ConditionSet) Set(condition Condition) (modified bool) {
86+
if c.object == nil {
87+
return false
88+
}
89+
90+
var conditions []Condition
91+
var foundCondition bool
92+
93+
condition.ObservedGeneration = c.object.GetGeneration()
94+
for _, cond := range c.object.GetConditions() {
95+
if cond.Type != condition.Type {
96+
// If we are deleting, we just bump all the observed generations
97+
if !c.object.GetDeletionTimestamp().IsZero() {
98+
cond.ObservedGeneration = c.object.GetGeneration()
99+
}
100+
conditions = append(conditions, cond)
101+
} else {
102+
foundCondition = true
103+
if condition.Status == cond.Status {
104+
condition.LastTransitionTime = cond.LastTransitionTime
105+
} else {
106+
condition.LastTransitionTime = metav1.Now()
107+
}
108+
if reflect.DeepEqual(condition, cond) {
109+
return false
110+
}
111+
}
112+
}
113+
if !foundCondition {
114+
// Dependent conditions should always be set, so if it's not found, that means
115+
// that we are initializing the condition type, and it's last "transition" was object creation
116+
if c.IsDependentCondition(condition.Type) {
117+
condition.LastTransitionTime = c.object.GetCreationTimestamp()
118+
} else {
119+
condition.LastTransitionTime = metav1.Now()
120+
}
121+
}
122+
conditions = append(conditions, condition)
123+
// Sorted for convenience of the consumer, i.e. kubectl.
124+
sort.SliceStable(conditions, func(i, j int) bool {
125+
// Order the root status condition at the end
126+
if conditions[i].Type == c.root || conditions[j].Type == c.root {
127+
return conditions[j].Type == c.root
128+
}
129+
return conditions[i].LastTransitionTime.Time.Before(conditions[j].LastTransitionTime.Time)
130+
})
131+
c.object.SetConditions(conditions)
132+
133+
// Recompute the root condition after setting any other condition
134+
c.recomputeRootCondition(condition.Type)
135+
return true
136+
}
137+
138+
// Clear removes the independent condition that matches the ConditionType
139+
// Not implemented for dependent conditions
140+
func (c ConditionSet) Clear(t string) error {
141+
if c.object == nil {
142+
return nil
143+
}
144+
145+
var conditions []Condition
146+
147+
// Dependent conditions are not handled as they can't be nil
148+
if c.IsDependentCondition(t) {
149+
return fmt.Errorf("clearing dependent conditions not implemented")
150+
}
151+
cond := c.Get(t)
152+
if cond == nil {
153+
return nil
154+
}
155+
for _, c := range c.object.GetConditions() {
156+
if c.Type != t {
157+
conditions = append(conditions, c)
158+
}
159+
}
160+
161+
// Sorted for convenience of the consumer, i.e. kubectl.
162+
sort.Slice(conditions, func(i, j int) bool { return conditions[i].Type < conditions[j].Type })
163+
c.object.SetConditions(conditions)
164+
165+
return nil
166+
}
167+
168+
// SetTrue sets the status of conditionType to true with the reason, and then marks the root condition to
169+
// true if all other dependents are also true.
170+
func (c ConditionSet) SetTrue(conditionType string) (modified bool) {
171+
return c.SetTrueWithReason(conditionType, conditionType, "")
172+
}
173+
174+
// SetTrueWithReason sets the status of conditionType to true with the reason, and then marks the root condition to
175+
// true if all other dependents are also true.
176+
func (c ConditionSet) SetTrueWithReason(conditionType string, reason, message string) (modified bool) {
177+
return c.Set(Condition{
178+
Type: conditionType,
179+
Status: metav1.ConditionTrue,
180+
Reason: reason,
181+
Message: message,
182+
})
183+
}
184+
185+
// SetUnknown sets the status of conditionType to Unknown and also sets the root condition
186+
// to Unknown if no other dependent condition is in an error state.
187+
func (c ConditionSet) SetUnknown(conditionType string) (modified bool) {
188+
return c.SetUnknownWithReason(conditionType, "AwaitingReconciliation",
189+
fmt.Sprintf("condition %q is awaiting reconciliation", conditionType))
190+
}
191+
192+
// SetUnknownWithReason sets the status of conditionType to Unknown with the reason, and also sets the root condition
193+
// to Unknown if no other dependent condition is in an error state.
194+
func (c ConditionSet) SetUnknownWithReason(conditionType string, reason, message string) (modified bool) {
195+
return c.Set(Condition{
196+
Type: conditionType,
197+
Status: metav1.ConditionUnknown,
198+
Reason: reason,
199+
Message: message,
200+
})
201+
}
202+
203+
// SetFalse sets the status of conditionType and the root condition to False.
204+
func (c ConditionSet) SetFalse(conditionType string, reason, message string) (modified bool) {
205+
return c.Set(Condition{
206+
Type: conditionType,
207+
Status: metav1.ConditionFalse,
208+
Reason: reason,
209+
Message: message,
210+
})
211+
}
212+
213+
// recomputeRootCondition marks the root condition to true if all other dependents are also true.
214+
func (c ConditionSet) recomputeRootCondition(conditionType string) {
215+
if conditionType == c.root {
216+
return
217+
}
218+
if conditions := c.findUnhealthyDependents(); len(conditions) == 0 {
219+
c.SetTrue(c.root)
220+
} else if unhealthy, found := findMostUnhealthy(conditions); found {
221+
c.Set(Condition{
222+
Type: c.root,
223+
Status: unhealthy.Status,
224+
Reason: unhealthy.Reason,
225+
Message: unhealthy.Message,
226+
})
227+
}
228+
}
229+
230+
func findMostUnhealthy(deps []Condition) (Condition, bool) {
231+
// Sort set conditions by time.
232+
sort.Slice(deps, func(i, j int) bool {
233+
return deps[i].LastTransitionTime.Time.After(deps[j].LastTransitionTime.Time)
234+
})
235+
236+
// First check the conditions with Status == False.
237+
for _, c := range deps {
238+
// False conditions trump Unknown.
239+
if c.IsFalse() {
240+
return c, true
241+
}
242+
}
243+
// Second check for conditions with Status == Unknown.
244+
for _, c := range deps {
245+
if c.IsUnknown() {
246+
return c, true
247+
}
248+
}
249+
250+
// All dependents are fine.
251+
return Condition{}, false
252+
}
253+
254+
func (c ConditionSet) findUnhealthyDependents() []Condition {
255+
if len(c.dependents) == 0 {
256+
return nil
257+
}
258+
deps := make([]Condition, 0, len(c.object.GetConditions()))
259+
for _, dep := range c.object.GetConditions() {
260+
if c.DependsOn(dep.Type) {
261+
if dep.IsFalse() || dep.IsUnknown() || dep.ObservedGeneration != c.object.GetGeneration() {
262+
deps = append(deps, dep)
263+
}
264+
}
265+
}
266+
267+
// Sort set conditions by time.
268+
sort.Slice(deps, func(i, j int) bool {
269+
return deps[i].LastTransitionTime.After(deps[j].LastTransitionTime.Time)
270+
})
271+
return deps
272+
}

0 commit comments

Comments
 (0)