Skip to content

Commit 2240106

Browse files
committed
summarize: Consider obj status condition in result
SummarizeAndPatch() should also consider the object's status conditions when computing and returning the runtime results to avoid any inconsistency in the runtime result and status condition of the object. When an object's Ready condition is False, the reconciler should retry unless it's in stalled condition. Signed-off-by: Sunny <[email protected]>
1 parent 065a760 commit 2240106

File tree

4 files changed

+182
-31
lines changed

4 files changed

+182
-31
lines changed

internal/reconcile/reconcile.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,12 @@ const (
5252
// can be implemented to build custom results based on the context of the
5353
// reconciler.
5454
type RuntimeResultBuilder interface {
55+
// BuildRuntimeResult analyzes the result and error to return a runtime
56+
// result.
5557
BuildRuntimeResult(rr Result, err error) ctrl.Result
58+
// IsSuccess returns if a given runtime result is success for a
59+
// RuntimeResultBuilder.
60+
IsSuccess(ctrl.Result) bool
5661
}
5762

5863
// AlwaysRequeueResultBuilder implements a RuntimeResultBuilder for always
@@ -82,6 +87,12 @@ func (r AlwaysRequeueResultBuilder) BuildRuntimeResult(rr Result, err error) ctr
8287
}
8388
}
8489

90+
// IsSuccess returns true if the given Result has the same RequeueAfter value
91+
// as of the AlwaysRequeueResultBuilder.
92+
func (r AlwaysRequeueResultBuilder) IsSuccess(result ctrl.Result) bool {
93+
return result.RequeueAfter == r.RequeueAfter
94+
}
95+
8596
// ComputeReconcileResult analyzes the reconcile results (result + error),
8697
// updates the status conditions of the object with any corrections and returns
8798
// object patch configuration, runtime result and runtime error. The caller is

internal/reconcile/reconcile_test.go

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,16 +118,6 @@ func TestComputeReconcileResult(t *testing.T) {
118118
t.Expect(patchOpts.IncludeStatusObservedGeneration).To(BeFalse())
119119
},
120120
},
121-
{
122-
name: "requeue result",
123-
result: ResultRequeue,
124-
recErr: nil,
125-
wantResult: ctrl.Result{Requeue: true},
126-
wantErr: false,
127-
afterFunc: func(t *WithT, obj conditions.Setter, patchOpts *patch.HelperOptions) {
128-
t.Expect(patchOpts.IncludeStatusObservedGeneration).To(BeFalse())
129-
},
130-
},
131121
{
132122
name: "stalling error",
133123
result: ResultEmpty,
@@ -203,6 +193,49 @@ func TestComputeReconcileResult(t *testing.T) {
203193
}
204194
}
205195

196+
func TestAlwaysRequeueResultBuilder_IsSuccess(t *testing.T) {
197+
interval := 5 * time.Second
198+
199+
tests := []struct {
200+
name string
201+
resultBuilder AlwaysRequeueResultBuilder
202+
runtimeResult ctrl.Result
203+
result bool
204+
}{
205+
{
206+
name: "success result",
207+
resultBuilder: AlwaysRequeueResultBuilder{RequeueAfter: interval},
208+
runtimeResult: ctrl.Result{RequeueAfter: interval},
209+
result: true,
210+
},
211+
{
212+
name: "requeue result",
213+
resultBuilder: AlwaysRequeueResultBuilder{RequeueAfter: interval},
214+
runtimeResult: ctrl.Result{Requeue: true},
215+
result: false,
216+
},
217+
{
218+
name: "zero result",
219+
resultBuilder: AlwaysRequeueResultBuilder{RequeueAfter: interval},
220+
runtimeResult: ctrl.Result{},
221+
result: false,
222+
},
223+
{
224+
name: "different requeue after",
225+
resultBuilder: AlwaysRequeueResultBuilder{RequeueAfter: interval},
226+
runtimeResult: ctrl.Result{RequeueAfter: time.Second},
227+
result: false,
228+
},
229+
}
230+
231+
for _, tt := range tests {
232+
t.Run(tt.name, func(t *testing.T) {
233+
g := NewWithT(t)
234+
g.Expect(tt.resultBuilder.IsSuccess(tt.runtimeResult)).To(Equal(tt.result))
235+
})
236+
}
237+
}
238+
206239
func TestFailureRecovery(t *testing.T) {
207240
failCondns := []string{
208241
"FooFailed",

internal/reconcile/summarize/summary.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ package summarize
1818

1919
import (
2020
"context"
21+
"errors"
2122

2223
apierrors "k8s.io/apimachinery/pkg/api/errors"
2324
kerrors "k8s.io/apimachinery/pkg/util/errors"
2425
kuberecorder "k8s.io/client-go/tools/record"
2526
ctrl "sigs.k8s.io/controller-runtime"
2627

28+
"github.com/fluxcd/pkg/apis/meta"
2729
"github.com/fluxcd/pkg/runtime/conditions"
2830
"github.com/fluxcd/pkg/runtime/patch"
2931

@@ -204,6 +206,18 @@ func (h *Helper) SummarizeAndPatch(ctx context.Context, obj conditions.Setter, o
204206
)
205207
}
206208

209+
// If object is not stalled, result is success and runtime error is nil,
210+
// ensure that Ready=True. Else, use the Ready failure message as the
211+
// runtime error message. This ensures that the reconciliation would be
212+
// retried as the object isn't ready.
213+
// NOTE: This is applicable to Ready condition only because it is a special
214+
// condition in kstatus that reflects the overall state of an object.
215+
if isNonStalledSuccess(obj, opts.ResultBuilder, result, recErr) {
216+
if !conditions.IsReady(obj) {
217+
recErr = errors.New(conditions.GetMessage(obj, meta.ReadyCondition))
218+
}
219+
}
220+
207221
// Finally, patch the resource.
208222
if err := h.patchHelper.Patch(ctx, obj, patchOpts...); err != nil {
209223
// Ignore patch error "not found" when the object is being deleted.
@@ -215,3 +229,16 @@ func (h *Helper) SummarizeAndPatch(ctx context.Context, obj conditions.Setter, o
215229

216230
return result, recErr
217231
}
232+
233+
// isNonStalledSuccess checks if the reconciliation was successful and has not
234+
// resulted in stalled situation.
235+
func isNonStalledSuccess(obj conditions.Setter, rb reconcile.RuntimeResultBuilder, result ctrl.Result, recErr error) bool {
236+
if !conditions.IsStalled(obj) && recErr == nil {
237+
// Without result builder, it can't be determined if the result is
238+
// success.
239+
if rb != nil {
240+
return rb.IsSuccess(result)
241+
}
242+
}
243+
return false
244+
}

internal/reconcile/summarize/summary_test.go

Lines changed: 101 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package summarize
1818

1919
import (
2020
"context"
21+
"errors"
2122
"fmt"
2223
"testing"
2324
"time"
@@ -27,6 +28,7 @@ import (
2728
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2829
"k8s.io/apimachinery/pkg/runtime"
2930
"k8s.io/client-go/tools/record"
31+
ctrl "sigs.k8s.io/controller-runtime"
3032
"sigs.k8s.io/controller-runtime/pkg/client"
3133
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
3234

@@ -91,18 +93,19 @@ func TestSummarizeAndPatch(t *testing.T) {
9193
afterFunc func(t *WithT, obj client.Object)
9294
assertConditions []metav1.Condition
9395
}{
94-
// Success/Fail indicates if a reconciliation succeeded or failed. On
95-
// a successful reconciliation, the object generation is expected to
96-
// match the observed generation in the object status.
96+
// Success/Fail indicates if a reconciliation succeeded or failed.
97+
// The object generation is expected to match the observed generation in
98+
// the object status if Ready=True or Stalled=True at the end.
9799
// All the cases have some Ready condition set, even if a test case is
98100
// unrelated to the conditions, because it's neseccary for a valid
99101
// status.
100102
{
101-
name: "Success, no extra conditions",
103+
name: "Success, Ready=True",
102104
generation: 4,
103105
beforeFunc: func(obj conditions.Setter) {
104106
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "test-msg")
105107
},
108+
result: reconcile.ResultSuccess,
106109
conditions: []Conditions{testReadyConditions},
107110
assertConditions: []metav1.Condition{
108111
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "test-msg"),
@@ -111,20 +114,6 @@ func TestSummarizeAndPatch(t *testing.T) {
111114
t.Expect(obj).To(HaveStatusObservedGeneration(4))
112115
},
113116
},
114-
{
115-
name: "Success, Ready=True",
116-
generation: 5,
117-
beforeFunc: func(obj conditions.Setter) {
118-
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "created")
119-
},
120-
conditions: []Conditions{testReadyConditions},
121-
assertConditions: []metav1.Condition{
122-
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "created"),
123-
},
124-
afterFunc: func(t *WithT, obj client.Object) {
125-
t.Expect(obj).To(HaveStatusObservedGeneration(5))
126-
},
127-
},
128117
{
129118
name: "Success, removes reconciling for successful result",
130119
generation: 2,
@@ -216,7 +205,22 @@ func TestSummarizeAndPatch(t *testing.T) {
216205
},
217206
},
218207
{
219-
name: "Success, multiple conditions summary",
208+
name: "Success, multiple target conditions summary",
209+
generation: 3,
210+
beforeFunc: func(obj conditions.Setter) {
211+
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "test-msg")
212+
conditions.MarkTrue(obj, "AAA", "ZZZ", "zzz") // Positive polarity True.
213+
},
214+
conditions: []Conditions{testReadyConditions, testFooConditions},
215+
result: reconcile.ResultSuccess,
216+
assertConditions: []metav1.Condition{
217+
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "test-msg"),
218+
*conditions.TrueCondition("Foo", "ZZZ", "zzz"), // True summary.
219+
*conditions.TrueCondition("AAA", "ZZZ", "zzz"),
220+
},
221+
},
222+
{
223+
name: "Success, multiple target conditions, False non-Ready summary don't affect result",
220224
generation: 3,
221225
beforeFunc: func(obj conditions.Setter) {
222226
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "test-msg")
@@ -232,6 +236,20 @@ func TestSummarizeAndPatch(t *testing.T) {
232236
*conditions.TrueCondition("AAA", "ZZZ", "zzz"),
233237
},
234238
},
239+
{
240+
name: "Fail, success result but Ready=False",
241+
generation: 3,
242+
beforeFunc: func(obj conditions.Setter) {
243+
conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "NewRevision", "new index revision")
244+
},
245+
conditions: []Conditions{testReadyConditions},
246+
result: reconcile.ResultSuccess,
247+
assertConditions: []metav1.Condition{
248+
*conditions.FalseCondition(meta.ReadyCondition, "NewRevision", "new index revision"),
249+
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new index revision"),
250+
},
251+
wantErr: true,
252+
},
235253
}
236254

237255
for _, tt := range tests {
@@ -291,6 +309,8 @@ func TestSummarizeAndPatch(t *testing.T) {
291309
// This tests the scenario where SummarizeAndPatch is used in the middle of
292310
// reconciliation.
293311
func TestSummarizeAndPatch_Intermediate(t *testing.T) {
312+
interval := 5 * time.Second
313+
294314
var testStageAConditions = Conditions{
295315
Target: "StageA",
296316
Owned: []string{"StageA", "A1", "A2", "A3"},
@@ -335,7 +355,7 @@ func TestSummarizeAndPatch_Intermediate(t *testing.T) {
335355
},
336356
},
337357
{
338-
name: "multiple Conditions",
358+
name: "multiple Conditions, mixed results",
339359
conditions: []Conditions{testStageAConditions, testStageBConditions},
340360
beforeFunc: func(obj conditions.Setter) {
341361
conditions.MarkTrue(obj, "A3", "ZZZ", "zzz") // Negative polarity True.
@@ -365,7 +385,7 @@ func TestSummarizeAndPatch_Intermediate(t *testing.T) {
365385
GenerateName: "test-",
366386
},
367387
Spec: sourcev1.GitRepositorySpec{
368-
Interval: metav1.Duration{Duration: 5 * time.Second},
388+
Interval: metav1.Duration{Duration: interval},
369389
},
370390
Status: sourcev1.GitRepositoryStatus{
371391
Conditions: []metav1.Condition{
@@ -386,6 +406,7 @@ func TestSummarizeAndPatch_Intermediate(t *testing.T) {
386406
summaryHelper := NewHelper(record.NewFakeRecorder(32), patchHelper)
387407
summaryOpts := []Option{
388408
WithConditions(tt.conditions...),
409+
WithResultBuilder(reconcile.AlwaysRequeueResultBuilder{RequeueAfter: interval}),
389410
}
390411
_, err = summaryHelper.SummarizeAndPatch(ctx, obj, summaryOpts...)
391412
g.Expect(err).ToNot(HaveOccurred())
@@ -394,3 +415,62 @@ func TestSummarizeAndPatch_Intermediate(t *testing.T) {
394415
})
395416
}
396417
}
418+
419+
func TestIsNonStalledSuccess(t *testing.T) {
420+
interval := 5 * time.Second
421+
422+
tests := []struct {
423+
name string
424+
beforeFunc func(obj conditions.Setter)
425+
rb reconcile.RuntimeResultBuilder
426+
recResult ctrl.Result
427+
recErr error
428+
wantResult bool
429+
}{
430+
{
431+
name: "non stalled success",
432+
rb: reconcile.AlwaysRequeueResultBuilder{RequeueAfter: interval},
433+
recResult: ctrl.Result{RequeueAfter: interval},
434+
wantResult: true,
435+
},
436+
{
437+
name: "stalled success",
438+
beforeFunc: func(obj conditions.Setter) {
439+
conditions.MarkStalled(obj, "FooReason", "test-msg")
440+
},
441+
rb: reconcile.AlwaysRequeueResultBuilder{RequeueAfter: interval},
442+
recResult: ctrl.Result{RequeueAfter: interval},
443+
wantResult: false,
444+
},
445+
{
446+
name: "error result",
447+
rb: reconcile.AlwaysRequeueResultBuilder{RequeueAfter: interval},
448+
recResult: ctrl.Result{RequeueAfter: interval},
449+
recErr: errors.New("some-error"),
450+
wantResult: false,
451+
},
452+
{
453+
name: "non success result",
454+
rb: reconcile.AlwaysRequeueResultBuilder{RequeueAfter: interval},
455+
recResult: ctrl.Result{RequeueAfter: 2 * time.Second},
456+
wantResult: false,
457+
},
458+
{
459+
name: "no result builder",
460+
recResult: ctrl.Result{RequeueAfter: interval},
461+
wantResult: false,
462+
},
463+
}
464+
465+
for _, tt := range tests {
466+
t.Run(tt.name, func(t *testing.T) {
467+
g := NewWithT(t)
468+
469+
obj := &sourcev1.GitRepository{}
470+
if tt.beforeFunc != nil {
471+
tt.beforeFunc(obj)
472+
}
473+
g.Expect(isNonStalledSuccess(obj, tt.rb, tt.recResult, tt.recErr)).To(Equal(tt.wantResult))
474+
})
475+
}
476+
}

0 commit comments

Comments
 (0)