Skip to content

Commit 3b0fa4d

Browse files
authored
Merge pull request #31 from ecordell/unpause
pause: support unpausing object by removing the label
2 parents 238e64e + 060cf9c commit 3b0fa4d

File tree

6 files changed

+220
-15
lines changed

6 files changed

+220
-15
lines changed

component/ensure_component_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ type MyObject struct {
3131
// this implements the conditions interface for MyObject, but note that
3232
// this is not supported by kube codegen at the moment (don't try to use
3333
// this in a real controller)
34-
conditions.StatusWithConditions[MyObjectStatus] `json:"-"`
34+
conditions.StatusWithConditions[*MyObjectStatus] `json:"-"`
3535
}
3636
type MyObjectStatus struct {
3737
ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,3,opt,name=observedGeneration"`

conditions/conditions.go

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ type StatusConditions struct {
1616
}
1717

1818
// GetStatusConditions returns a pointer to all status conditions.
19-
func (c StatusConditions) GetStatusConditions() *[]metav1.Condition {
19+
func (c *StatusConditions) GetStatusConditions() *[]metav1.Condition {
2020
return &c.Conditions
2121
}
2222

2323
type HasConditions interface {
24+
comparable
2425
GetStatusConditions() *[]metav1.Condition
2526
}
2627

@@ -30,13 +31,20 @@ type StatusWithConditions[S HasConditions] struct {
3031
}
3132

3233
// GetStatusConditions returns all status conditions.
33-
func (s StatusWithConditions[S]) GetStatusConditions() []metav1.Condition {
34-
return *s.Status.GetStatusConditions()
34+
func (s *StatusWithConditions[S]) GetStatusConditions() *[]metav1.Condition {
35+
var zero S
36+
if s.Status == zero {
37+
return nil
38+
}
39+
return s.Status.GetStatusConditions()
3540
}
3641

3742
// FindStatusCondition finds the conditionType in conditions.
3843
func (s StatusWithConditions[S]) FindStatusCondition(conditionType string) *metav1.Condition {
39-
return meta.FindStatusCondition(s.GetStatusConditions(), conditionType)
44+
if s.GetStatusConditions() == nil {
45+
return nil
46+
}
47+
return meta.FindStatusCondition(*s.GetStatusConditions(), conditionType)
4048
}
4149

4250
// SetStatusCondition sets the corresponding condition in conditions to newCondition.
@@ -56,17 +64,26 @@ func (s StatusWithConditions[S]) RemoveStatusCondition(conditionType string) {
5664

5765
// IsStatusConditionTrue returns true when the conditionType is present and set to `metav1.ConditionTrue`
5866
func (s StatusWithConditions[S]) IsStatusConditionTrue(conditionType string) bool {
59-
return meta.IsStatusConditionTrue(s.GetStatusConditions(), conditionType)
67+
if s.GetStatusConditions() == nil {
68+
return false
69+
}
70+
return meta.IsStatusConditionTrue(*s.GetStatusConditions(), conditionType)
6071
}
6172

6273
// IsStatusConditionFalse returns true when the conditionType is present and set to `metav1.ConditionFalse`
6374
func (s StatusWithConditions[S]) IsStatusConditionFalse(conditionType string) bool {
64-
return meta.IsStatusConditionFalse(s.GetStatusConditions(), conditionType)
75+
if s.GetStatusConditions() == nil {
76+
return false
77+
}
78+
return meta.IsStatusConditionFalse(*s.GetStatusConditions(), conditionType)
6579
}
6680

6781
// IsStatusConditionPresentAndEqual returns true when conditionType is present and equal to status.
6882
func (s StatusWithConditions[S]) IsStatusConditionPresentAndEqual(conditionType string, status metav1.ConditionStatus) bool {
69-
return meta.IsStatusConditionPresentAndEqual(s.GetStatusConditions(), conditionType, status)
83+
if s.GetStatusConditions() == nil {
84+
return false
85+
}
86+
return meta.IsStatusConditionPresentAndEqual(*s.GetStatusConditions(), conditionType, status)
7087
}
7188

7289
// IsStatusConditionChanged returns true if the passed in condition is different

metrics/condition_metrics.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func (c *ConditionStatusCollector[K]) CollectWithStability(ch chan<- metrics.Met
103103
objectsWithCondition := map[string]uint16{}
104104
for _, o := range objs {
105105
objectName := types.NamespacedName{Name: o.GetName(), Namespace: o.GetNamespace()}.String()
106-
for _, condition := range o.GetStatusConditions() {
106+
for _, condition := range *o.GetStatusConditions() {
107107
objectsWithCondition[condition.Type]++
108108

109109
timeInCondition := collectTime.Sub(condition.LastTransitionTime.Time)

metrics/condition_metrics_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ type MyObject struct {
1414
// this implements the conditions interface for MyObject, but note that
1515
// this is not supported by kube codegen at the moment (don't try to use
1616
// this in a real controller)
17-
conditions.StatusWithConditions[MyObjectStatus] `json:"-"`
17+
conditions.StatusWithConditions[*MyObjectStatus] `json:"-"`
1818
}
1919
type MyObjectStatus struct {
2020
ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,3,opt,name=observedGeneration"`

pause/pause.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ type HasStatusConditions interface {
3636
metav1.Object
3737
FindStatusCondition(conditionType string) *metav1.Condition
3838
SetStatusCondition(metav1.Condition)
39-
GetStatusConditions() []metav1.Condition
39+
RemoveStatusCondition(conditionType string)
40+
GetStatusConditions() *[]metav1.Condition
4041
}
4142

4243
func IsPaused(object metav1.Object, pausedLabelKey string) bool {
@@ -80,17 +81,32 @@ func (p *Handler[K]) pause(ctx context.Context, object K) {
8081
object.SetStatusCondition(NewPausedCondition(p.PausedLabelKey))
8182
object.SetManagedFields(nil)
8283
if err := p.PatchStatus(ctx, object); err != nil {
83-
p.ctrls.MustValue(ctx).RequeueErr(err)
84+
p.ctrls.MustValue(ctx).RequeueAPIErr(err)
8485
return
8586
}
8687
p.ctrls.MustValue(ctx).Done()
8788
}
8889

8990
func (p *Handler[K]) Handle(ctx context.Context) {
90-
if obj := p.Object.MustValue(ctx); IsPaused(obj, p.PausedLabelKey) {
91+
obj := p.Object.MustValue(ctx)
92+
if IsPaused(obj, p.PausedLabelKey) {
9193
p.pause(ctx, obj)
9294
return
9395
}
96+
97+
// unpaused and no paused condition, continue
98+
if obj.FindStatusCondition(ConditionTypePaused) == nil {
99+
p.Next.Handle(ctx)
100+
return
101+
}
102+
103+
// remove the paused condition
104+
obj.RemoveStatusCondition(ConditionTypePaused)
105+
obj.SetManagedFields(nil)
106+
if err := p.PatchStatus(ctx, obj); err != nil {
107+
p.ctrls.MustValue(ctx).RequeueAPIErr(err)
108+
return
109+
}
94110
p.Next.Handle(ctx)
95111
}
96112

@@ -135,7 +151,7 @@ func (p *SelfPauseHandler[K]) Handle(ctx context.Context) {
135151
object.SetLabels(labels)
136152
if err := p.Patch(ctx, object); err != nil {
137153
utilruntime.HandleError(err)
138-
p.ctrls.MustValue(ctx).Requeue()
154+
p.ctrls.MustValue(ctx).RequeueAPIErr(err)
139155
return
140156
}
141157
p.ctrls.MustValue(ctx).Done()

pause/pause_test.go

Lines changed: 173 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ package pause
22

33
import (
44
"context"
5+
"fmt"
6+
"testing"
57

8+
"github.com/stretchr/testify/require"
9+
"golang.org/x/exp/slices"
610
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
711

812
"github.com/authzed/controller-idioms/conditions"
913
"github.com/authzed/controller-idioms/handler"
1014
"github.com/authzed/controller-idioms/queue"
15+
"github.com/authzed/controller-idioms/queue/fake"
1116
"github.com/authzed/controller-idioms/typedctx"
1217
)
1318

@@ -18,7 +23,7 @@ type MyObject struct {
1823
// this implements the conditions interface for MyObject, but note that
1924
// this is not supported by kube codegen at the moment (don't try to use
2025
// this in a real controller)
21-
conditions.StatusWithConditions[MyObjectStatus] `json:"-"`
26+
conditions.StatusWithConditions[*MyObjectStatus] `json:"-"`
2227
}
2328
type MyObjectStatus struct {
2429
ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,3,opt,name=observedGeneration"`
@@ -47,3 +52,170 @@ func ExampleNewPauseContextHandler() {
4752
pauseHandler.Handle(ctx)
4853
// Output:
4954
}
55+
56+
func TestPauseHandler(t *testing.T) {
57+
var nextKey handler.Key = "next"
58+
const PauseLabelKey = "com.my-controller/controller-paused"
59+
tests := []struct {
60+
name string
61+
62+
obj *MyObject
63+
patchError error
64+
65+
expectNext handler.Key
66+
expectEvents []string
67+
expectPatchStatus bool
68+
expectConditions []metav1.Condition
69+
expectRequeue bool
70+
expectDone bool
71+
}{
72+
{
73+
name: "pauses when label found",
74+
obj: &MyObject{
75+
ObjectMeta: metav1.ObjectMeta{
76+
Labels: map[string]string{
77+
PauseLabelKey: "",
78+
},
79+
},
80+
StatusWithConditions: conditions.StatusWithConditions[*MyObjectStatus]{
81+
Status: &MyObjectStatus{
82+
StatusConditions: conditions.StatusConditions{
83+
Conditions: []metav1.Condition{},
84+
},
85+
},
86+
},
87+
},
88+
expectPatchStatus: true,
89+
expectConditions: []metav1.Condition{NewPausedCondition(PauseLabelKey)},
90+
expectDone: true,
91+
},
92+
{
93+
name: "requeues on pause patch error",
94+
obj: &MyObject{
95+
ObjectMeta: metav1.ObjectMeta{
96+
Labels: map[string]string{
97+
PauseLabelKey: "",
98+
},
99+
},
100+
StatusWithConditions: conditions.StatusWithConditions[*MyObjectStatus]{
101+
Status: &MyObjectStatus{
102+
StatusConditions: conditions.StatusConditions{
103+
Conditions: []metav1.Condition{},
104+
},
105+
},
106+
},
107+
},
108+
patchError: fmt.Errorf("error patching"),
109+
expectPatchStatus: true,
110+
expectRequeue: true,
111+
},
112+
{
113+
name: "no-op when label found and status is already paused",
114+
obj: &MyObject{
115+
ObjectMeta: metav1.ObjectMeta{
116+
Labels: map[string]string{
117+
PauseLabelKey: "",
118+
},
119+
},
120+
StatusWithConditions: conditions.StatusWithConditions[*MyObjectStatus]{
121+
Status: &MyObjectStatus{
122+
StatusConditions: conditions.StatusConditions{
123+
Conditions: []metav1.Condition{NewPausedCondition(PauseLabelKey)},
124+
},
125+
},
126+
},
127+
},
128+
expectDone: true,
129+
},
130+
{
131+
name: "removes condition when label is removed",
132+
obj: &MyObject{
133+
StatusWithConditions: conditions.StatusWithConditions[*MyObjectStatus]{
134+
Status: &MyObjectStatus{
135+
StatusConditions: conditions.StatusConditions{
136+
Conditions: []metav1.Condition{NewPausedCondition(PauseLabelKey)},
137+
},
138+
},
139+
},
140+
},
141+
expectPatchStatus: true,
142+
expectConditions: []metav1.Condition{},
143+
expectNext: nextKey,
144+
},
145+
{
146+
name: "removes self-pause condition when label is removed",
147+
obj: &MyObject{
148+
StatusWithConditions: conditions.StatusWithConditions[*MyObjectStatus]{
149+
Status: &MyObjectStatus{
150+
StatusConditions: conditions.StatusConditions{
151+
Conditions: []metav1.Condition{NewSelfPausedCondition(PauseLabelKey)},
152+
},
153+
},
154+
},
155+
},
156+
expectPatchStatus: true,
157+
expectConditions: []metav1.Condition{},
158+
expectNext: nextKey,
159+
},
160+
{
161+
name: "requeues on unpause patch error",
162+
obj: &MyObject{
163+
StatusWithConditions: conditions.StatusWithConditions[*MyObjectStatus]{
164+
Status: &MyObjectStatus{
165+
StatusConditions: conditions.StatusConditions{
166+
Conditions: []metav1.Condition{NewPausedCondition(PauseLabelKey)},
167+
},
168+
},
169+
},
170+
},
171+
patchError: fmt.Errorf("error patching"),
172+
expectPatchStatus: true,
173+
expectRequeue: true,
174+
},
175+
{
176+
name: "no-op, no pause label, no pause status",
177+
obj: &MyObject{},
178+
expectNext: nextKey,
179+
},
180+
}
181+
for _, tt := range tests {
182+
t.Run(tt.name, func(t *testing.T) {
183+
ctrls := &fake.FakeInterface{}
184+
patchCalled := false
185+
186+
patchStatus := func(ctx context.Context, patch *MyObject) error {
187+
patchCalled = true
188+
189+
if tt.patchError != nil {
190+
return tt.patchError
191+
}
192+
193+
require.Truef(t, slices.EqualFunc(tt.expectConditions, patch.Status.Conditions, func(a, b metav1.Condition) bool {
194+
return a.Type == b.Type &&
195+
a.Status == b.Status &&
196+
a.ObservedGeneration == b.ObservedGeneration &&
197+
a.Message == b.Message &&
198+
a.Reason == b.Reason
199+
}), "conditions not equal:\na: %#v\nb: %#v", tt.expectConditions, patch.Status.Conditions)
200+
201+
return nil
202+
}
203+
queueOps := queue.NewQueueOperationsCtx()
204+
ctxMyObject := typedctx.WithDefault[*MyObject](nil)
205+
206+
ctx := context.Background()
207+
ctx = queueOps.WithValue(ctx, ctrls)
208+
ctx = ctxMyObject.WithValue(ctx, tt.obj)
209+
var called handler.Key
210+
211+
NewPauseContextHandler(queueOps.Key, PauseLabelKey, ctxMyObject, patchStatus, handler.ContextHandlerFunc(func(ctx context.Context) {
212+
called = nextKey
213+
})).Handle(ctx)
214+
215+
require.Equal(t, tt.expectPatchStatus, patchCalled)
216+
require.Equal(t, tt.expectNext, called)
217+
require.Equal(t, tt.expectRequeue, ctrls.RequeueAPIErrCallCount() == 1)
218+
require.Equal(t, tt.expectDone, ctrls.DoneCallCount() == 1)
219+
})
220+
}
221+
}

0 commit comments

Comments
 (0)