Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions status/condition_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ type ConditionSet struct {
// ForOptions configures a ConditionSet.
type ForOptions struct {
Clock clock.Clock
// ObservedOnly builds the set from conditions already present, without
// initializing absent root/dependent conditions to Unknown. See WithObservedOnly.
ObservedOnly bool
}

// ForOption is a functional option for For.
Expand All @@ -65,6 +68,15 @@ func WithClock(c clock.Clock) ForOption {
return func(o *ForOptions) { o.Clock = c }
}

// WithObservedOnly builds the ConditionSet from the conditions already present on
// the object, skipping initialization of missing root/dependent conditions to
// Unknown. Use this when the set is a read-only reflection of observed state (e.g.
// metrics emission for an object the caller does not own), so absent conditions are
// not fabricated.
func WithObservedOnly() ForOption {
return func(o *ForOptions) { o.ObservedOnly = true }
}

// For creates a ConditionSet from an object using the original
// ConditionTypes as a reference. Status must be a pointer to a struct.
func (r ConditionTypes) For(object Object, opts ...ForOption) ConditionSet {
Expand All @@ -73,6 +85,9 @@ func (r ConditionTypes) For(object Object, opts ...ForOption) ConditionSet {
opt(&o)
}
cs := ConditionSet{object: object, ConditionTypes: r, clock: o.Clock}
if o.ObservedOnly {
return cs
}
// Set known conditions Unknown if not set.
// Set the root condition first to get consistent timing for LastTransitionTime
for _, t := range append([]string{r.root}, r.dependents...) {
Expand Down
24 changes: 24 additions & 0 deletions status/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,30 @@ var _ = Describe("Generic Controller", func() {
Expect(GetMetric("operator_testgenericobject_status_condition_current_status_seconds", conditionLabels(ConditionTypeBar, metav1.ConditionFalse))).To(BeNil())
Expect(GetMetric("operator_testgenericobject_status_condition_current_status_seconds", conditionLabels(ConditionTypeBar, metav1.ConditionUnknown))).To(BeNil())
})
It("should not emit a synthetic Ready condition metric for objects without one", func() {
testObject := test.Object(&TestGenericObject{})
gvk := object.GVK(testObject)
testObject.Status = TestGenericStatus{
Conditions: []metav1.Condition{
{Type: ConditionTypeFoo, Status: metav1.ConditionTrue},
},
}

ExpectApplied(ctx, kubeClient, testObject)
ExpectReconciled(ctx, genericController, testObject)

// The observed Foo condition is emitted.
Expect(GetMetric("operator_status_condition_count", conditionLabelsWithGroupKind(gvk, ConditionTypeFoo, metav1.ConditionTrue)).GetGauge().GetValue()).To(BeEquivalentTo(1))

// No Ready condition is present on the object, so the generic controller must not
// fabricate a Ready=Unknown metric for it, across either metric family.
Expect(GetMetric("operator_status_condition_count", conditionLabelsWithGroupKind(gvk, status.ConditionReady, metav1.ConditionTrue))).To(BeNil())
Expect(GetMetric("operator_status_condition_count", conditionLabelsWithGroupKind(gvk, status.ConditionReady, metav1.ConditionFalse))).To(BeNil())
Expect(GetMetric("operator_status_condition_count", conditionLabelsWithGroupKind(gvk, status.ConditionReady, metav1.ConditionUnknown))).To(BeNil())
Expect(GetMetric("operator_status_condition_current_status_seconds", conditionLabelsWithGroupKind(gvk, status.ConditionReady, metav1.ConditionUnknown))).To(BeNil())
Expect(GetMetric("operator_testgenericobject_status_condition_count", conditionLabels(status.ConditionReady, metav1.ConditionUnknown))).To(BeNil())
Expect(GetMetric("operator_testgenericobject_status_condition_current_status_seconds", conditionLabels(status.ConditionReady, metav1.ConditionUnknown))).To(BeNil())
})
It("should emit transition total metrics for abnormal conditions", func() {
testObject := test.Object(&TestGenericObject{})
gvk := object.GVK(testObject)
Expand Down
6 changes: 5 additions & 1 deletion status/unstructured_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,9 @@ func (u *UnstructuredAdapter[T]) StatusConditions(opts ...ForOption) ConditionSe
conditionTypes := lo.Map(u.GetConditions(), func(condition Condition, _ int) string {
return condition.Type
})
return NewReadyConditions(conditionTypes...).For(u, opts...)
// The adapter is a read-only reflection of an object the controller does not
// own, used to emit metrics for whatever conditions are present. Build the set
// from observed conditions only so we don't fabricate a root "Ready" condition
// (and a phantom Ready=Unknown metric) for objects that never define one.
return NewReadyConditions(conditionTypes...).For(u, append(opts, WithObservedOnly())...)
}