Skip to content

Commit 80a0261

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 target conditions are not ready/True, the reconciler should retry unless it's in stalled condition. Signed-off-by: Sunny <darkowlzz@protonmail.com>
1 parent 89a4e52 commit 80a0261

File tree

4 files changed

+192
-13
lines changed

4 files changed

+192
-13
lines changed

internal/reconcile/reconcile.go

+11
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

+43-10
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

+27
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ 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"
@@ -204,6 +205,19 @@ func (h *Helper) SummarizeAndPatch(ctx context.Context, obj conditions.Setter, o
204205
)
205206
}
206207

208+
// If object is not stalled, result is success and runtime error is nil,
209+
// ensure that the TargetCondition=True. Else, use the TargetCondition
210+
// failure message as the runtime error message. This ensures that the
211+
// reconciliation would be retried as the object isn't ready.
212+
if isNonStalledSuccess(obj, opts.ResultBuilder, result, recErr) {
213+
for _, c := range opts.Conditions {
214+
if !conditions.IsTrue(obj, c.Target) {
215+
err := errors.New(conditions.GetMessage(obj, c.Target))
216+
recErr = kerrors.NewAggregate([]error{recErr, err})
217+
}
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. If there's a
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

+111-3
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

@@ -216,7 +218,22 @@ func TestSummarizeAndPatch(t *testing.T) {
216218
},
217219
},
218220
{
219-
name: "Success, multiple conditions summary",
221+
name: "Success, multiple conditions True summary",
222+
generation: 3,
223+
beforeFunc: func(obj conditions.Setter) {
224+
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "test-msg")
225+
conditions.MarkTrue(obj, "AAA", "ZZZ", "zzz") // Positive polarity True.
226+
},
227+
conditions: []Conditions{testReadyConditions, testFooConditions},
228+
result: reconcile.ResultSuccess,
229+
assertConditions: []metav1.Condition{
230+
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "test-msg"),
231+
*conditions.TrueCondition("Foo", "ZZZ", "zzz"), // True summary.
232+
*conditions.TrueCondition("AAA", "ZZZ", "zzz"),
233+
},
234+
},
235+
{
236+
name: "Fail, multiple conditions mixed summary",
220237
generation: 3,
221238
beforeFunc: func(obj conditions.Setter) {
222239
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "test-msg")
@@ -231,6 +248,35 @@ func TestSummarizeAndPatch(t *testing.T) {
231248
*conditions.TrueCondition("BBB", "YYY", "yyy"),
232249
*conditions.TrueCondition("AAA", "ZZZ", "zzz"),
233250
},
251+
wantErr: true,
252+
},
253+
{
254+
name: "Fail, success result but Ready=False",
255+
generation: 3,
256+
beforeFunc: func(obj conditions.Setter) {
257+
conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "NewRevision", "new index revision")
258+
},
259+
conditions: []Conditions{testReadyConditions},
260+
result: reconcile.ResultSuccess,
261+
assertConditions: []metav1.Condition{
262+
*conditions.FalseCondition(meta.ReadyCondition, "NewRevision", "new index revision"),
263+
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new index revision"),
264+
},
265+
wantErr: true,
266+
},
267+
{
268+
name: "Success, success result but Ready=False",
269+
generation: 3,
270+
beforeFunc: func(obj conditions.Setter) {
271+
conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "NewRevision", "new index revision")
272+
},
273+
conditions: []Conditions{testReadyConditions},
274+
result: reconcile.ResultSuccess,
275+
assertConditions: []metav1.Condition{
276+
*conditions.FalseCondition(meta.ReadyCondition, "NewRevision", "new index revision"),
277+
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new index revision"),
278+
},
279+
wantErr: true,
234280
},
235281
}
236282

@@ -291,6 +337,8 @@ func TestSummarizeAndPatch(t *testing.T) {
291337
// This tests the scenario where SummarizeAndPatch is used in the middle of
292338
// reconciliation.
293339
func TestSummarizeAndPatch_Intermediate(t *testing.T) {
340+
interval := 5 * time.Second
341+
294342
var testStageAConditions = Conditions{
295343
Target: "StageA",
296344
Owned: []string{"StageA", "A1", "A2", "A3"},
@@ -335,7 +383,7 @@ func TestSummarizeAndPatch_Intermediate(t *testing.T) {
335383
},
336384
},
337385
{
338-
name: "multiple Conditions",
386+
name: "multiple Conditions, mixed results",
339387
conditions: []Conditions{testStageAConditions, testStageBConditions},
340388
beforeFunc: func(obj conditions.Setter) {
341389
conditions.MarkTrue(obj, "A3", "ZZZ", "zzz") // Negative polarity True.
@@ -365,7 +413,7 @@ func TestSummarizeAndPatch_Intermediate(t *testing.T) {
365413
GenerateName: "test-",
366414
},
367415
Spec: sourcev1.GitRepositorySpec{
368-
Interval: metav1.Duration{Duration: 5 * time.Second},
416+
Interval: metav1.Duration{Duration: interval},
369417
},
370418
Status: sourcev1.GitRepositoryStatus{
371419
Conditions: []metav1.Condition{
@@ -386,6 +434,7 @@ func TestSummarizeAndPatch_Intermediate(t *testing.T) {
386434
summaryHelper := NewHelper(record.NewFakeRecorder(32), patchHelper)
387435
summaryOpts := []Option{
388436
WithConditions(tt.conditions...),
437+
WithResultBuilder(reconcile.AlwaysRequeueResultBuilder{RequeueAfter: interval}),
389438
}
390439
_, err = summaryHelper.SummarizeAndPatch(ctx, obj, summaryOpts...)
391440
g.Expect(err).ToNot(HaveOccurred())
@@ -394,3 +443,62 @@ func TestSummarizeAndPatch_Intermediate(t *testing.T) {
394443
})
395444
}
396445
}
446+
447+
func TestIsNonStalledSuccess(t *testing.T) {
448+
interval := 5 * time.Second
449+
450+
tests := []struct {
451+
name string
452+
beforeFunc func(obj conditions.Setter)
453+
rb reconcile.RuntimeResultBuilder
454+
recResult ctrl.Result
455+
recErr error
456+
wantResult bool
457+
}{
458+
{
459+
name: "non stalled success",
460+
rb: reconcile.AlwaysRequeueResultBuilder{RequeueAfter: interval},
461+
recResult: ctrl.Result{RequeueAfter: interval},
462+
wantResult: true,
463+
},
464+
{
465+
name: "stalled success",
466+
beforeFunc: func(obj conditions.Setter) {
467+
conditions.MarkStalled(obj, "FooReason", "test-msg")
468+
},
469+
rb: reconcile.AlwaysRequeueResultBuilder{RequeueAfter: interval},
470+
recResult: ctrl.Result{RequeueAfter: interval},
471+
wantResult: false,
472+
},
473+
{
474+
name: "error result",
475+
rb: reconcile.AlwaysRequeueResultBuilder{RequeueAfter: interval},
476+
recResult: ctrl.Result{RequeueAfter: interval},
477+
recErr: errors.New("some-error"),
478+
wantResult: false,
479+
},
480+
{
481+
name: "non success result",
482+
rb: reconcile.AlwaysRequeueResultBuilder{RequeueAfter: interval},
483+
recResult: ctrl.Result{RequeueAfter: 2 * time.Second},
484+
wantResult: false,
485+
},
486+
{
487+
name: "no result builder",
488+
recResult: ctrl.Result{RequeueAfter: interval},
489+
wantResult: false,
490+
},
491+
}
492+
493+
for _, tt := range tests {
494+
t.Run(tt.name, func(t *testing.T) {
495+
g := NewWithT(t)
496+
497+
obj := &sourcev1.GitRepository{}
498+
if tt.beforeFunc != nil {
499+
tt.beforeFunc(obj)
500+
}
501+
g.Expect(isNonStalledSuccess(obj, tt.rb, tt.recResult, tt.recErr)).To(Equal(tt.wantResult))
502+
})
503+
}
504+
}

0 commit comments

Comments
 (0)