Skip to content

Commit bef8fba

Browse files
Consider an absent condition to be Unknown (#106)
1 parent 9365434 commit bef8fba

File tree

7 files changed

+77
-22
lines changed

7 files changed

+77
-22
lines changed

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ require (
1717
k8s.io/apimachinery v0.31.2
1818
k8s.io/client-go v0.31.2
1919
k8s.io/klog/v2 v2.130.1
20-
k8s.io/kubernetes v1.31.3
2120
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
2221
sigs.k8s.io/controller-runtime v0.19.1
2322
sigs.k8s.io/yaml v1.4.0

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,6 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
190190
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
191191
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
192192
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
193-
k8s.io/kubernetes v1.31.3 h1:oqb7HdfnTelrGlZ6ziNugvQ/L/aJWR704114EAhUn9Q=
194-
k8s.io/kubernetes v1.31.3/go.mod h1:9xmT2buyTYj8TRKwRae7FcuY8k5+xlxv7VivvO0KKfs=
195193
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
196194
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
197195
sigs.k8s.io/controller-runtime v0.19.1 h1:Son+Q40+Be3QWb+niBXAg2vFiYWolDjjRfO8hn/cxOk=

object/object.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import (
1010
"k8s.io/apimachinery/pkg/runtime"
1111
"k8s.io/apimachinery/pkg/runtime/schema"
1212
"k8s.io/apimachinery/pkg/types"
13+
"k8s.io/apimachinery/pkg/util/dump"
1314
"k8s.io/client-go/kubernetes/scheme"
14-
"k8s.io/kubernetes/pkg/util/hash"
1515
"sigs.k8s.io/controller-runtime/pkg/client"
1616
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
1717
"sigs.k8s.io/yaml"
@@ -56,6 +56,7 @@ func Unmarshal[T any](raw []byte) *T {
5656

5757
func Hash(o any) string {
5858
h := fnv.New64a()
59-
hash.DeepHashObject(h, o)
60-
return strconv.FormatUint(uint64(h.Sum64()), 10)
59+
h.Reset()
60+
fmt.Fprintf(h, "%v", dump.ForHash(o))
61+
return strconv.FormatUint(h.Sum64(), 10)
6162
}

status/condition_set.go

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ type ConditionSet struct {
5454
func (r ConditionTypes) For(object Object) ConditionSet {
5555
cs := ConditionSet{object: object, ConditionTypes: r}
5656
// Set known conditions Unknown if not set.
57-
for _, t := range append(r.dependents, r.root) {
57+
// Set the root condition first to get consistent timing for LastTransitionTime
58+
for _, t := range append([]string{r.root}, r.dependents...) {
5859
if cs.Get(t) == nil {
5960
cs.SetUnknown(t)
6061
}
@@ -99,25 +100,41 @@ func (c ConditionSet) IsTrue(conditionTypes ...string) bool {
99100
return true
100101
}
101102

103+
func (c ConditionSet) IsDependentCondition(t string) bool {
104+
return t == c.root || lo.Contains(c.dependents, t)
105+
}
106+
102107
// Set sets or updates the Condition on Conditions for Condition.Type.
103108
// If there is an update, Conditions are stored back sorted.
104109
func (c ConditionSet) Set(condition Condition) (modified bool) {
105-
conditionType := condition.Type
106110
var conditions []Condition
107-
condition.LastTransitionTime = metav1.Now()
111+
var foundCondition bool
112+
108113
condition.ObservedGeneration = c.object.GetGeneration()
109114
for _, cond := range c.object.GetConditions() {
110-
if cond.Type != conditionType {
115+
if cond.Type != condition.Type {
111116
conditions = append(conditions, cond)
112117
} else {
113-
if condition.Status == cond.Status && !cond.LastTransitionTime.IsZero() {
118+
foundCondition = true
119+
if condition.Status == cond.Status {
114120
condition.LastTransitionTime = cond.LastTransitionTime
121+
} else {
122+
condition.LastTransitionTime = metav1.Now()
115123
}
116124
if reflect.DeepEqual(condition, cond) {
117125
return false
118126
}
119127
}
120128
}
129+
if !foundCondition {
130+
// Dependent conditions should always be set, so if it's not found, that means
131+
// that we are initializing the condition type, and it's last "transition" was object creation
132+
if c.IsDependentCondition(condition.Type) {
133+
condition.LastTransitionTime = c.object.GetCreationTimestamp()
134+
} else {
135+
condition.LastTransitionTime = metav1.Now()
136+
}
137+
}
121138
conditions = append(conditions, condition)
122139
// Sorted for convenience of the consumer, i.e. kubectl.
123140
sort.SliceStable(conditions, func(i, j int) bool {
@@ -130,21 +147,21 @@ func (c ConditionSet) Set(condition Condition) (modified bool) {
130147
c.object.SetConditions(conditions)
131148

132149
// Recompute the root condition after setting any other condition
133-
c.recomputeRootCondition(conditionType)
150+
c.recomputeRootCondition(condition.Type)
134151
return true
135152
}
136153

137-
// Clear removes the abnormal condition that matches the ConditionType
138-
// Not implemented for normal conditions
154+
// Clear removes the independent condition that matches the ConditionType
155+
// Not implemented for dependent conditions
139156
func (c ConditionSet) Clear(t string) error {
140157
var conditions []Condition
141158

142159
if c.object == nil {
143160
return nil
144161
}
145-
// Normal conditions are not handled as they can't be nil
146-
if t == c.root || lo.Contains(c.dependents, t) {
147-
return fmt.Errorf("clearing normal conditions not implemented")
162+
// Dependent conditions are not handled as they can't be nil
163+
if c.IsDependentCondition(t) {
164+
return fmt.Errorf("clearing dependent conditions not implemented")
148165
}
149166
cond := c.Get(t)
150167
if cond == nil {

status/condition_set_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ import (
1313

1414
var _ = Describe("Conditions", func() {
1515
It("should correctly toggle conditions", func() {
16-
testObject := test.CustomObject{}
17-
testObject.Generation = 1
16+
testObject := test.Object(&test.CustomObject{})
1817
// Conditions should be initialized
1918
conditions := testObject.StatusConditions()
2019
Expect(conditions.Get(test.ConditionTypeFoo).GetStatus()).To(Equal(metav1.ConditionUnknown))

status/controller_test.go

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"github.com/awslabs/operatorpkg/status"
1111
"github.com/awslabs/operatorpkg/test"
1212
. "github.com/awslabs/operatorpkg/test/expectations"
13-
"github.com/onsi/ginkgo/v2"
1413
. "github.com/onsi/ginkgo/v2"
1514
. "github.com/onsi/gomega"
1615
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -44,7 +43,7 @@ var _ = Describe("Controller", func() {
4443
BeforeEach(func() {
4544
recorder = record.NewFakeRecorder(10)
4645
kubeClient = fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
47-
ctx = log.IntoContext(context.Background(), ginkgo.GinkgoLogr)
46+
ctx = log.IntoContext(context.Background(), GinkgoLogr)
4847
controller = status.NewController[*test.CustomObject](kubeClient, recorder, status.EmitDeprecatedMetrics)
4948
})
5049
AfterEach(func() {
@@ -517,14 +516,54 @@ var _ = Describe("Controller", func() {
517516
}()
518517
}
519518
})
519+
It("should set LastTransitionTime for status conditions on initialization to CreationTimestamp", func() {
520+
testObject := test.Object(&test.CustomObject{})
521+
testObject.StatusConditions() // initialize conditions after applying and setting CreationTimestamp
522+
523+
Expect(testObject.StatusConditions().Get(test.ConditionTypeFoo).LastTransitionTime.Time).To(Equal(testObject.GetCreationTimestamp().Time))
524+
Expect(testObject.StatusConditions().Get(test.ConditionTypeBar).LastTransitionTime.Time).To(Equal(testObject.GetCreationTimestamp().Time))
525+
Expect(testObject.StatusConditions().Get(status.ConditionReady).LastTransitionTime.Time).To(Equal(testObject.GetCreationTimestamp().Time))
526+
})
527+
It("should consider status conditions that aren't set as unknown", func() {
528+
// This mimics an object creation
529+
testObject := test.Object(&test.CustomObject{})
530+
ExpectApplied(ctx, kubeClient, testObject)
531+
ExpectReconciled(ctx, controller, testObject)
532+
533+
// Then the status conditions gets initialized and a condition is set to True
534+
testObject.StatusConditions().SetTrue(test.ConditionTypeFoo)
535+
testObject.SetCreationTimestamp(metav1.Time{Time: time.Now().Add(time.Hour)})
536+
ExpectApplied(ctx, kubeClient, testObject)
537+
ExpectReconciled(ctx, controller, testObject)
538+
539+
Expect(GetMetric("operator_customobject_status_condition_transition_seconds", conditionLabels(status.ConditionReady, metav1.ConditionTrue))).To(BeNil())
540+
Expect(GetMetric("operator_customobject_status_condition_transition_seconds", conditionLabels(status.ConditionReady, metav1.ConditionFalse))).To(BeNil())
541+
Expect(GetMetric("operator_customobject_status_condition_transition_seconds", conditionLabels(status.ConditionReady, metav1.ConditionUnknown))).To(BeNil())
542+
Expect(GetMetric("operator_customobject_status_condition_transition_seconds", conditionLabels(ConditionTypeFoo, metav1.ConditionTrue))).To(BeNil())
543+
Expect(GetMetric("operator_customobject_status_condition_transition_seconds", conditionLabels(ConditionTypeFoo, metav1.ConditionFalse))).To(BeNil())
544+
Expect(GetMetric("operator_customobject_status_condition_transition_seconds", conditionLabels(ConditionTypeFoo, metav1.ConditionUnknown)).GetHistogram().GetSampleCount()).To(BeNumerically(">", 0))
545+
Expect(GetMetric("operator_customobject_status_condition_transition_seconds", conditionLabels(ConditionTypeBar, metav1.ConditionTrue))).To(BeNil())
546+
Expect(GetMetric("operator_customobject_status_condition_transition_seconds", conditionLabels(ConditionTypeBar, metav1.ConditionFalse))).To(BeNil())
547+
Expect(GetMetric("operator_customobject_status_condition_transition_seconds", conditionLabels(ConditionTypeBar, metav1.ConditionUnknown))).To(BeNil())
548+
549+
Expect(GetMetric("operator_customobject_status_condition_transitions_total", conditionLabels(status.ConditionReady, metav1.ConditionTrue))).To(BeNil())
550+
Expect(GetMetric("operator_customobject_status_condition_transitions_total", conditionLabels(status.ConditionReady, metav1.ConditionFalse))).To(BeNil())
551+
Expect(GetMetric("operator_customobject_status_condition_transitions_total", conditionLabels(status.ConditionReady, metav1.ConditionUnknown))).To(BeNil())
552+
Expect(GetMetric("operator_customobject_status_condition_transitions_total", conditionLabels(ConditionTypeFoo, metav1.ConditionTrue)).GetCounter().GetValue()).To(BeEquivalentTo(1))
553+
Expect(GetMetric("operator_customobject_status_condition_transitions_total", conditionLabels(ConditionTypeFoo, metav1.ConditionFalse))).To(BeNil())
554+
Expect(GetMetric("operator_customobject_status_condition_transitions_total", conditionLabels(ConditionTypeFoo, metav1.ConditionUnknown))).To(BeNil())
555+
Expect(GetMetric("operator_customobject_status_condition_transitions_total", conditionLabels(ConditionTypeBar, metav1.ConditionTrue))).To(BeNil())
556+
Expect(GetMetric("operator_customobject_status_condition_transitions_total", conditionLabels(ConditionTypeBar, metav1.ConditionFalse))).To(BeNil())
557+
Expect(GetMetric("operator_customobject_status_condition_transitions_total", conditionLabels(ConditionTypeBar, metav1.ConditionUnknown))).To(BeNil())
558+
})
520559
})
521560

522561
var _ = Describe("Generic Controller", func() {
523562
var genericController *status.GenericObjectController[*TestGenericObject]
524563
BeforeEach(func() {
525564
recorder = record.NewFakeRecorder(10)
526565
kubeClient = fake.NewClientBuilder().WithScheme(scheme.Scheme).Build()
527-
ctx = log.IntoContext(context.Background(), ginkgo.GinkgoLogr)
566+
ctx = log.IntoContext(context.Background(), GinkgoLogr)
528567
genericController = status.NewGenericObjectController[*TestGenericObject](kubeClient, recorder, status.EmitDeprecatedMetrics)
529568
})
530569
AfterEach(func() {

test/object.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@ func Object[T client.Object](base T, overrides ...T) T {
2929
dest := reflect.New(reflect.TypeOf(base).Elem()).Interface().(T)
3030
dest.SetName(RandomName())
3131
dest.SetNamespace(Namespace.Name)
32+
dest.SetGeneration(1)
3233
dest.SetLabels(lo.Assign(dest.GetLabels(), map[string]string{DiscoveryLabel: dest.GetName()}))
3334
for _, src := range append([]T{base}, overrides...) {
3435
lo.Must0(mergo.Merge(dest, src, mergo.WithOverride))
3536
}
37+
dest.SetCreationTimestamp(metav1.Now())
3638
return dest
3739
}
3840

0 commit comments

Comments
 (0)