Skip to content
Draft
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
14 changes: 13 additions & 1 deletion apis/conditionset.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ type ConditionManager interface {
InitializeConditions()
}

// ConditionManagerSetter allows the condition manager to be accessed from a resource.
// It is implemented by the default Status subresource implementation.
type ConditionManagerSetter interface {
SetConditionManager(ConditionManager)
}

// NewLivingConditionSet returns a ConditionSet to hold the conditions for the
// living resource. ConditionReady is used as the happy condition.
// The set of condition types provided are those of the terminal subconditions.
Expand Down Expand Up @@ -164,11 +170,17 @@ func (r ConditionSet) Manage(status ConditionsAccessor) ConditionManager {
// Manage creates a ConditionManager from an accessor object using the original
// ConditionSet as a reference. Status must be a pointer to a struct.
func (r ConditionSet) ManageWithContext(ctx context.Context, status ConditionsAccessor) ConditionManager {
return conditionsImpl{
cm := conditionsImpl{
accessor: status,
ConditionSet: r,
now: rtime.RetrieveNow(ctx),
}

if s, isConditionManagerSetter := status.(ConditionManagerSetter); isConditionManagerSetter {
s.SetConditionManager(cm)
}

return cm
}

// IsHappy looks at the happy condition and returns true if that condition is
Expand Down
25 changes: 25 additions & 0 deletions apis/status_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,30 @@ type Status struct {
// +patchMergeKey=type
// +patchStrategy=merge
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`

conditionManagerWrapper `json:"-"` // for an explanation why this exists, see below
}

// conditionManagerWrapper exists so as to allow the Status struct to implement the ConditionManager interface.
// The ConditionManager interface cannot be embedded directly because the `deepcopy-gen` generator then fails.
// Embedding an unexported wrapper struct and marking the field as excluded via the `json:"-"` struct tag allows
// the code generation to succeed.
// This wouldn't be necessary if fields could be excluded from `deepcopy-gen`.
// +k8s:deepcopy-gen=false
type conditionManagerWrapper struct {
ConditionManager
}

// DeepCopyInto is defined as a no-op. It's necessary because `deepcopy-gen` isn't running for unexported structs
// but will still generate a call to it in the parent struct.
func (w *conditionManagerWrapper) DeepCopyInto(_ *conditionManagerWrapper) {}

var _ ConditionsAccessor = (*Status)(nil)

var _ ConditionManager = (*Status)(nil)

var _ ConditionManagerSetter = (*Status)(nil)

// GetConditions implements ConditionsAccessor
func (s *Status) GetConditions() []metav1.Condition {
return s.Conditions
Expand All @@ -64,3 +84,8 @@ func (s *Status) GetCondition(t string) *metav1.Condition {
}
return nil
}

// SetConditionManager satisfies the ConditionManagerSetter interface.
func (s *Status) SetConditionManager(cm ConditionManager) {
s.conditionManagerWrapper = conditionManagerWrapper{ConditionManager: cm}
}
105 changes: 105 additions & 0 deletions apis/status_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package apis

import (
"context"
"testing"
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
rtime "reconciler.io/runtime/time"
)

const (
ConditionCreated string = "Created"
ConditionConfigured string = "Configured"
)

var conditionSet = NewLivingConditionSet(ConditionCreated, ConditionConfigured)

type TestStatus struct {
Status `json:",inline"`
}

func (s *TestStatus) InitializeConditions(ctx context.Context) {
conditionSet.ManageWithContext(ctx, s).InitializeConditions()
}

func TestStatusConditionManager(t *testing.T) {
now := metav1.Date(2025, time.March, 1, 10, 0, 0, 0, time.UTC)

tests := []struct {
name string
ctx context.Context
run func(*testing.T, *TestStatus)
wantErr error
}{
{
name: "unhappy and ready condition unknown when just initialized",
ctx: context.Background(),
run: func(t *testing.T, s *TestStatus) {
if actual := s.IsHappy(); actual == true {
t.Errorf("%s: IsHappy() actually = %v, expected %v", t.Name(), actual, false)
}

readyCondition := s.GetCondition(ConditionReady)
if actual := ConditionIsUnknown(readyCondition); actual != true {
t.Errorf("%s: ConditionIsUnknown() actually = %v, expected %v", t.Name(), actual, true)
}
},
},
{
name: "setting Created condition to false",
ctx: context.Background(),
run: func(t *testing.T, s *TestStatus) {
s.MarkFalse(ConditionCreated, ConditionCreated, "")

readyCondition := s.GetCondition(ConditionReady)
if actual := ConditionIsFalse(readyCondition); actual != true {
t.Errorf("%s: ConditionIsFalse() actually = %v, expected %v", t.Name(), actual, true)
}

conditionCreated := s.GetCondition(ConditionCreated)
if actual := ConditionIsFalse(conditionCreated); actual != true {
t.Errorf("%s: ConditionIsFalse() actually = %v, expected %v", t.Name(), actual, true)
}

conditionConfigured := s.GetCondition(ConditionConfigured)
if actual := ConditionIsUnknown(conditionConfigured); actual != true {
t.Errorf("%s: ConditionIsUnknown() actually = %v, expected %v", t.Name(), actual, true)
}
},
},
{
name: "setting all conditions sets the ready condition to true and happy",
ctx: rtime.StashNow(context.Background(), now.Time),
run: func(t *testing.T, s *TestStatus) {
s.MarkTrue(ConditionCreated, ConditionCreated, "")
s.MarkTrue(ConditionConfigured, ConditionConfigured, "")

readyCondition := s.GetCondition(ConditionReady)
if actual := ConditionIsTrue(readyCondition); actual != true {
t.Errorf("%s: ConditionIsTrue() actually = %v, expected %v", t.Name(), actual, true)
}

// failure to update this test will break it when adding a new terminal condition
if actual := s.IsHappy(); actual != true {
t.Errorf("%s: IsHappy() actually = %v, expected %v", t.Name(), actual, true)
}

// check time as well
for _, condition := range s.GetConditions() {
if equal := condition.LastTransitionTime.Equal(&now); !equal {
t.Errorf("%s: LastTransitionTime.Equal() actually = %v, expected %v", t.Name(), equal, true)
}
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &TestStatus{}
s.InitializeConditions(tt.ctx)
tt.run(t, s)
})
}
}
1 change: 1 addition & 0 deletions apis/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.