Skip to content

Commit 8c88319

Browse files
committed
Introduce a condtions package with a light weight manager to help enable coordinated condition updates
Signed-off-by: Scott Nichols <[email protected]>
1 parent db9545b commit 8c88319

File tree

4 files changed

+217
-2
lines changed

4 files changed

+217
-2
lines changed

apis/common/v1/zz_generated.deepcopy.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

hack/boilerplate.go.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2019 The Crossplane Authors.
2+
Copyright 2025 The Crossplane Authors.
33

44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.

pkg/conditions/manager.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
Copyright 2025 The Crossplane Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package conditions enables consistent interactions with an object's status conditions.
18+
package conditions
19+
20+
import (
21+
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
22+
"github.com/crossplane/crossplane-runtime/pkg/resource"
23+
)
24+
25+
// ObjectWithConditions is the interface definition that allows.
26+
type ObjectWithConditions interface {
27+
resource.Object
28+
resource.Conditioned
29+
}
30+
31+
// Manager is an interface for a stateless factory-like object that produces ConditionSet objects.
32+
type Manager interface {
33+
// For returns an implementation of a ConditionSet to operate on a specific ObjectWithConditions.
34+
For(o ObjectWithConditions) ConditionSet
35+
}
36+
37+
// ConditionSet holds operations for interacting with an object's conditions.
38+
type ConditionSet interface {
39+
// Mark adds or updates the conditions onto the managed resource object. Unlike a "Set" method, this also can add
40+
// contextual updates to the condition such as propagating the correct observedGeneration to the conditions being
41+
// changed.
42+
Mark(condition ...xpv1.Condition)
43+
}
44+
45+
// New returns an implementation of a Manager.
46+
func New() Manager {
47+
return &managerImpl{}
48+
}
49+
50+
// Check that conditionsImpl implements ConditionManager.
51+
var _ Manager = (*managerImpl)(nil)
52+
53+
// managerImpl is the top level factor for producing a ConditionSet on behalf of a ObjectWithConditions resource.
54+
// managerImpl implements Manager.
55+
type managerImpl struct{}
56+
57+
// For implements Manager.For.
58+
func (m managerImpl) For(o ObjectWithConditions) ConditionSet {
59+
return &conditionSet{o: o}
60+
}
61+
62+
// Check that conditionSet implements ConditionSet.
63+
var _ ConditionSet = (*conditionSet)(nil)
64+
65+
type conditionSet struct {
66+
o ObjectWithConditions
67+
}
68+
69+
// Mark implements ConditionSet.Mark.
70+
func (c *conditionSet) Mark(condition ...xpv1.Condition) {
71+
if c == nil || c.o == nil {
72+
return
73+
}
74+
// Foreach condition we have been sent to mark, update the observed generation.
75+
for i := range condition {
76+
condition[i].ObservedGeneration = c.o.GetGeneration()
77+
}
78+
c.o.SetConditions(condition...)
79+
}

pkg/conditions/manager_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
Copyright 2025 The Crossplane Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package conditions
18+
19+
import (
20+
"reflect"
21+
"testing"
22+
"time"
23+
24+
"github.com/google/go-cmp/cmp"
25+
"github.com/google/go-cmp/cmp/cmpopts"
26+
27+
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
28+
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
29+
"github.com/crossplane/crossplane-runtime/pkg/test"
30+
)
31+
32+
func TestNew(t *testing.T) {
33+
tests := []struct {
34+
name string
35+
want Manager
36+
}{{
37+
name: "New returns a non-nil manager.",
38+
want: &managerImpl{},
39+
}}
40+
for _, tt := range tests {
41+
t.Run(tt.name, func(t *testing.T) {
42+
if got := New(); !reflect.DeepEqual(got, tt.want) {
43+
t.Errorf("New() = %v, want %v", got, tt.want)
44+
}
45+
})
46+
}
47+
}
48+
49+
func Test_conditionSet_Mark(t *testing.T) {
50+
manager := New()
51+
52+
tests := []struct {
53+
name string
54+
start []xpv1.Condition
55+
mark []xpv1.Condition
56+
want []xpv1.Condition
57+
}{{
58+
name: "provide no conditions",
59+
start: nil,
60+
mark: nil,
61+
want: nil,
62+
}, {
63+
name: "empty status, a new condition is appended",
64+
start: nil,
65+
mark: []xpv1.Condition{xpv1.ReconcileSuccess()},
66+
want: []xpv1.Condition{xpv1.ReconcileSuccess().WithObservedGeneration(42)},
67+
}, {
68+
name: "existing status, attempt to mark nothing",
69+
start: []xpv1.Condition{xpv1.Available().WithObservedGeneration(1)},
70+
mark: nil,
71+
want: []xpv1.Condition{xpv1.Available().WithObservedGeneration(1)},
72+
}, {
73+
name: "existing status, an existing condition is updated",
74+
start: []xpv1.Condition{xpv1.ReconcileSuccess().WithObservedGeneration(1)},
75+
mark: []xpv1.Condition{xpv1.ReconcileSuccess()},
76+
want: []xpv1.Condition{xpv1.ReconcileSuccess().WithObservedGeneration(42)},
77+
}, {
78+
name: "existing status, a new condition is appended",
79+
start: []xpv1.Condition{xpv1.Available().WithObservedGeneration(1)},
80+
mark: []xpv1.Condition{xpv1.ReconcileSuccess()},
81+
want: []xpv1.Condition{xpv1.Available().WithObservedGeneration(1), xpv1.ReconcileSuccess().WithObservedGeneration(42)},
82+
}}
83+
for _, tt := range tests {
84+
t.Run(tt.name, func(t *testing.T) {
85+
ut := newManaged(42, tt.start...)
86+
c := manager.For(ut)
87+
c.Mark(tt.mark...)
88+
if diff := cmp.Diff(tt.want, ut.Conditions, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" {
89+
reason := "Failed to update conditions."
90+
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
91+
}
92+
})
93+
}
94+
95+
t.Run("Manage a nil object", func(t *testing.T) {
96+
c := manager.For(nil)
97+
if c == nil {
98+
t.Errorf("manager.For(nil) = %v, want non-nil", c)
99+
}
100+
// Test that Marking on a Manager that has a nil object does not end up panicking.
101+
c.Mark(xpv1.ReconcileSuccess())
102+
// Success!
103+
})
104+
}
105+
106+
func Test_managerImpl_For(t *testing.T) {
107+
tests := []struct {
108+
name string
109+
o ObjectWithConditions
110+
want ConditionSet
111+
}{{
112+
name: "Nil object returns a non-nil manager.",
113+
want: &conditionSet{},
114+
}, {
115+
name: "Object propagates into manager.",
116+
o: &fake.Managed{},
117+
want: &conditionSet{
118+
o: &fake.Managed{},
119+
},
120+
}}
121+
for _, tt := range tests {
122+
t.Run(tt.name, func(t *testing.T) {
123+
m := managerImpl{}
124+
if got := m.For(tt.o); !reflect.DeepEqual(got, tt.want) {
125+
t.Errorf("For() = %v, want %v", got, tt.want)
126+
}
127+
})
128+
}
129+
}
130+
131+
func newManaged(generation int64, conditions ...xpv1.Condition) *fake.Managed {
132+
mg := &fake.Managed{}
133+
mg.Generation = generation
134+
mg.SetConditions(conditions...)
135+
return mg
136+
}

0 commit comments

Comments
 (0)