Skip to content

Commit 4e30a4f

Browse files
authored
Merge pull request #703 from fluxcd/target-condition-in-result
summarize: Consider obj status condition in result
2 parents 065a760 + 2240106 commit 4e30a4f

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)