Merge pull request #703 from fluxcd/target-condition-in-result
summarize: Consider obj status condition in result
This commit is contained in:
commit
4e30a4f63e
|
@ -52,7 +52,12 @@ const (
|
||||||
// can be implemented to build custom results based on the context of the
|
// can be implemented to build custom results based on the context of the
|
||||||
// reconciler.
|
// reconciler.
|
||||||
type RuntimeResultBuilder interface {
|
type RuntimeResultBuilder interface {
|
||||||
|
// BuildRuntimeResult analyzes the result and error to return a runtime
|
||||||
|
// result.
|
||||||
BuildRuntimeResult(rr Result, err error) ctrl.Result
|
BuildRuntimeResult(rr Result, err error) ctrl.Result
|
||||||
|
// IsSuccess returns if a given runtime result is success for a
|
||||||
|
// RuntimeResultBuilder.
|
||||||
|
IsSuccess(ctrl.Result) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// AlwaysRequeueResultBuilder implements a RuntimeResultBuilder for always
|
// AlwaysRequeueResultBuilder implements a RuntimeResultBuilder for always
|
||||||
|
@ -82,6 +87,12 @@ func (r AlwaysRequeueResultBuilder) BuildRuntimeResult(rr Result, err error) ctr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsSuccess returns true if the given Result has the same RequeueAfter value
|
||||||
|
// as of the AlwaysRequeueResultBuilder.
|
||||||
|
func (r AlwaysRequeueResultBuilder) IsSuccess(result ctrl.Result) bool {
|
||||||
|
return result.RequeueAfter == r.RequeueAfter
|
||||||
|
}
|
||||||
|
|
||||||
// ComputeReconcileResult analyzes the reconcile results (result + error),
|
// ComputeReconcileResult analyzes the reconcile results (result + error),
|
||||||
// updates the status conditions of the object with any corrections and returns
|
// updates the status conditions of the object with any corrections and returns
|
||||||
// object patch configuration, runtime result and runtime error. The caller is
|
// object patch configuration, runtime result and runtime error. The caller is
|
||||||
|
|
|
@ -118,16 +118,6 @@ func TestComputeReconcileResult(t *testing.T) {
|
||||||
t.Expect(patchOpts.IncludeStatusObservedGeneration).To(BeFalse())
|
t.Expect(patchOpts.IncludeStatusObservedGeneration).To(BeFalse())
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "requeue result",
|
|
||||||
result: ResultRequeue,
|
|
||||||
recErr: nil,
|
|
||||||
wantResult: ctrl.Result{Requeue: true},
|
|
||||||
wantErr: false,
|
|
||||||
afterFunc: func(t *WithT, obj conditions.Setter, patchOpts *patch.HelperOptions) {
|
|
||||||
t.Expect(patchOpts.IncludeStatusObservedGeneration).To(BeFalse())
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "stalling error",
|
name: "stalling error",
|
||||||
result: ResultEmpty,
|
result: ResultEmpty,
|
||||||
|
@ -203,6 +193,49 @@ func TestComputeReconcileResult(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAlwaysRequeueResultBuilder_IsSuccess(t *testing.T) {
|
||||||
|
interval := 5 * time.Second
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
resultBuilder AlwaysRequeueResultBuilder
|
||||||
|
runtimeResult ctrl.Result
|
||||||
|
result bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "success result",
|
||||||
|
resultBuilder: AlwaysRequeueResultBuilder{RequeueAfter: interval},
|
||||||
|
runtimeResult: ctrl.Result{RequeueAfter: interval},
|
||||||
|
result: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "requeue result",
|
||||||
|
resultBuilder: AlwaysRequeueResultBuilder{RequeueAfter: interval},
|
||||||
|
runtimeResult: ctrl.Result{Requeue: true},
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero result",
|
||||||
|
resultBuilder: AlwaysRequeueResultBuilder{RequeueAfter: interval},
|
||||||
|
runtimeResult: ctrl.Result{},
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "different requeue after",
|
||||||
|
resultBuilder: AlwaysRequeueResultBuilder{RequeueAfter: interval},
|
||||||
|
runtimeResult: ctrl.Result{RequeueAfter: time.Second},
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
g.Expect(tt.resultBuilder.IsSuccess(tt.runtimeResult)).To(Equal(tt.result))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFailureRecovery(t *testing.T) {
|
func TestFailureRecovery(t *testing.T) {
|
||||||
failCondns := []string{
|
failCondns := []string{
|
||||||
"FooFailed",
|
"FooFailed",
|
||||||
|
|
|
@ -18,12 +18,14 @@ package summarize
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
kerrors "k8s.io/apimachinery/pkg/util/errors"
|
kerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||||
kuberecorder "k8s.io/client-go/tools/record"
|
kuberecorder "k8s.io/client-go/tools/record"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
|
||||||
|
"github.com/fluxcd/pkg/apis/meta"
|
||||||
"github.com/fluxcd/pkg/runtime/conditions"
|
"github.com/fluxcd/pkg/runtime/conditions"
|
||||||
"github.com/fluxcd/pkg/runtime/patch"
|
"github.com/fluxcd/pkg/runtime/patch"
|
||||||
|
|
||||||
|
@ -204,6 +206,18 @@ func (h *Helper) SummarizeAndPatch(ctx context.Context, obj conditions.Setter, o
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If object is not stalled, result is success and runtime error is nil,
|
||||||
|
// ensure that Ready=True. Else, use the Ready failure message as the
|
||||||
|
// runtime error message. This ensures that the reconciliation would be
|
||||||
|
// retried as the object isn't ready.
|
||||||
|
// NOTE: This is applicable to Ready condition only because it is a special
|
||||||
|
// condition in kstatus that reflects the overall state of an object.
|
||||||
|
if isNonStalledSuccess(obj, opts.ResultBuilder, result, recErr) {
|
||||||
|
if !conditions.IsReady(obj) {
|
||||||
|
recErr = errors.New(conditions.GetMessage(obj, meta.ReadyCondition))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Finally, patch the resource.
|
// Finally, patch the resource.
|
||||||
if err := h.patchHelper.Patch(ctx, obj, patchOpts...); err != nil {
|
if err := h.patchHelper.Patch(ctx, obj, patchOpts...); err != nil {
|
||||||
// Ignore patch error "not found" when the object is being deleted.
|
// 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
|
||||||
|
|
||||||
return result, recErr
|
return result, recErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isNonStalledSuccess checks if the reconciliation was successful and has not
|
||||||
|
// resulted in stalled situation.
|
||||||
|
func isNonStalledSuccess(obj conditions.Setter, rb reconcile.RuntimeResultBuilder, result ctrl.Result, recErr error) bool {
|
||||||
|
if !conditions.IsStalled(obj) && recErr == nil {
|
||||||
|
// Without result builder, it can't be determined if the result is
|
||||||
|
// success.
|
||||||
|
if rb != nil {
|
||||||
|
return rb.IsSuccess(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package summarize
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -27,6 +28,7 @@ import (
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/client-go/tools/record"
|
"k8s.io/client-go/tools/record"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||||
|
|
||||||
|
@ -91,18 +93,19 @@ func TestSummarizeAndPatch(t *testing.T) {
|
||||||
afterFunc func(t *WithT, obj client.Object)
|
afterFunc func(t *WithT, obj client.Object)
|
||||||
assertConditions []metav1.Condition
|
assertConditions []metav1.Condition
|
||||||
}{
|
}{
|
||||||
// Success/Fail indicates if a reconciliation succeeded or failed. On
|
// Success/Fail indicates if a reconciliation succeeded or failed.
|
||||||
// a successful reconciliation, the object generation is expected to
|
// The object generation is expected to match the observed generation in
|
||||||
// match the observed generation in the object status.
|
// the object status if Ready=True or Stalled=True at the end.
|
||||||
// All the cases have some Ready condition set, even if a test case is
|
// All the cases have some Ready condition set, even if a test case is
|
||||||
// unrelated to the conditions, because it's neseccary for a valid
|
// unrelated to the conditions, because it's neseccary for a valid
|
||||||
// status.
|
// status.
|
||||||
{
|
{
|
||||||
name: "Success, no extra conditions",
|
name: "Success, Ready=True",
|
||||||
generation: 4,
|
generation: 4,
|
||||||
beforeFunc: func(obj conditions.Setter) {
|
beforeFunc: func(obj conditions.Setter) {
|
||||||
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "test-msg")
|
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "test-msg")
|
||||||
},
|
},
|
||||||
|
result: reconcile.ResultSuccess,
|
||||||
conditions: []Conditions{testReadyConditions},
|
conditions: []Conditions{testReadyConditions},
|
||||||
assertConditions: []metav1.Condition{
|
assertConditions: []metav1.Condition{
|
||||||
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "test-msg"),
|
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "test-msg"),
|
||||||
|
@ -111,20 +114,6 @@ func TestSummarizeAndPatch(t *testing.T) {
|
||||||
t.Expect(obj).To(HaveStatusObservedGeneration(4))
|
t.Expect(obj).To(HaveStatusObservedGeneration(4))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "Success, Ready=True",
|
|
||||||
generation: 5,
|
|
||||||
beforeFunc: func(obj conditions.Setter) {
|
|
||||||
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "created")
|
|
||||||
},
|
|
||||||
conditions: []Conditions{testReadyConditions},
|
|
||||||
assertConditions: []metav1.Condition{
|
|
||||||
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "created"),
|
|
||||||
},
|
|
||||||
afterFunc: func(t *WithT, obj client.Object) {
|
|
||||||
t.Expect(obj).To(HaveStatusObservedGeneration(5))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "Success, removes reconciling for successful result",
|
name: "Success, removes reconciling for successful result",
|
||||||
generation: 2,
|
generation: 2,
|
||||||
|
@ -216,7 +205,22 @@ func TestSummarizeAndPatch(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Success, multiple conditions summary",
|
name: "Success, multiple target conditions summary",
|
||||||
|
generation: 3,
|
||||||
|
beforeFunc: func(obj conditions.Setter) {
|
||||||
|
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "test-msg")
|
||||||
|
conditions.MarkTrue(obj, "AAA", "ZZZ", "zzz") // Positive polarity True.
|
||||||
|
},
|
||||||
|
conditions: []Conditions{testReadyConditions, testFooConditions},
|
||||||
|
result: reconcile.ResultSuccess,
|
||||||
|
assertConditions: []metav1.Condition{
|
||||||
|
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "test-msg"),
|
||||||
|
*conditions.TrueCondition("Foo", "ZZZ", "zzz"), // True summary.
|
||||||
|
*conditions.TrueCondition("AAA", "ZZZ", "zzz"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Success, multiple target conditions, False non-Ready summary don't affect result",
|
||||||
generation: 3,
|
generation: 3,
|
||||||
beforeFunc: func(obj conditions.Setter) {
|
beforeFunc: func(obj conditions.Setter) {
|
||||||
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "test-msg")
|
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "test-msg")
|
||||||
|
@ -232,6 +236,20 @@ func TestSummarizeAndPatch(t *testing.T) {
|
||||||
*conditions.TrueCondition("AAA", "ZZZ", "zzz"),
|
*conditions.TrueCondition("AAA", "ZZZ", "zzz"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Fail, success result but Ready=False",
|
||||||
|
generation: 3,
|
||||||
|
beforeFunc: func(obj conditions.Setter) {
|
||||||
|
conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "NewRevision", "new index revision")
|
||||||
|
},
|
||||||
|
conditions: []Conditions{testReadyConditions},
|
||||||
|
result: reconcile.ResultSuccess,
|
||||||
|
assertConditions: []metav1.Condition{
|
||||||
|
*conditions.FalseCondition(meta.ReadyCondition, "NewRevision", "new index revision"),
|
||||||
|
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new index revision"),
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -291,6 +309,8 @@ func TestSummarizeAndPatch(t *testing.T) {
|
||||||
// This tests the scenario where SummarizeAndPatch is used in the middle of
|
// This tests the scenario where SummarizeAndPatch is used in the middle of
|
||||||
// reconciliation.
|
// reconciliation.
|
||||||
func TestSummarizeAndPatch_Intermediate(t *testing.T) {
|
func TestSummarizeAndPatch_Intermediate(t *testing.T) {
|
||||||
|
interval := 5 * time.Second
|
||||||
|
|
||||||
var testStageAConditions = Conditions{
|
var testStageAConditions = Conditions{
|
||||||
Target: "StageA",
|
Target: "StageA",
|
||||||
Owned: []string{"StageA", "A1", "A2", "A3"},
|
Owned: []string{"StageA", "A1", "A2", "A3"},
|
||||||
|
@ -335,7 +355,7 @@ func TestSummarizeAndPatch_Intermediate(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple Conditions",
|
name: "multiple Conditions, mixed results",
|
||||||
conditions: []Conditions{testStageAConditions, testStageBConditions},
|
conditions: []Conditions{testStageAConditions, testStageBConditions},
|
||||||
beforeFunc: func(obj conditions.Setter) {
|
beforeFunc: func(obj conditions.Setter) {
|
||||||
conditions.MarkTrue(obj, "A3", "ZZZ", "zzz") // Negative polarity True.
|
conditions.MarkTrue(obj, "A3", "ZZZ", "zzz") // Negative polarity True.
|
||||||
|
@ -365,7 +385,7 @@ func TestSummarizeAndPatch_Intermediate(t *testing.T) {
|
||||||
GenerateName: "test-",
|
GenerateName: "test-",
|
||||||
},
|
},
|
||||||
Spec: sourcev1.GitRepositorySpec{
|
Spec: sourcev1.GitRepositorySpec{
|
||||||
Interval: metav1.Duration{Duration: 5 * time.Second},
|
Interval: metav1.Duration{Duration: interval},
|
||||||
},
|
},
|
||||||
Status: sourcev1.GitRepositoryStatus{
|
Status: sourcev1.GitRepositoryStatus{
|
||||||
Conditions: []metav1.Condition{
|
Conditions: []metav1.Condition{
|
||||||
|
@ -386,6 +406,7 @@ func TestSummarizeAndPatch_Intermediate(t *testing.T) {
|
||||||
summaryHelper := NewHelper(record.NewFakeRecorder(32), patchHelper)
|
summaryHelper := NewHelper(record.NewFakeRecorder(32), patchHelper)
|
||||||
summaryOpts := []Option{
|
summaryOpts := []Option{
|
||||||
WithConditions(tt.conditions...),
|
WithConditions(tt.conditions...),
|
||||||
|
WithResultBuilder(reconcile.AlwaysRequeueResultBuilder{RequeueAfter: interval}),
|
||||||
}
|
}
|
||||||
_, err = summaryHelper.SummarizeAndPatch(ctx, obj, summaryOpts...)
|
_, err = summaryHelper.SummarizeAndPatch(ctx, obj, summaryOpts...)
|
||||||
g.Expect(err).ToNot(HaveOccurred())
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
@ -394,3 +415,62 @@ func TestSummarizeAndPatch_Intermediate(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsNonStalledSuccess(t *testing.T) {
|
||||||
|
interval := 5 * time.Second
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
beforeFunc func(obj conditions.Setter)
|
||||||
|
rb reconcile.RuntimeResultBuilder
|
||||||
|
recResult ctrl.Result
|
||||||
|
recErr error
|
||||||
|
wantResult bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "non stalled success",
|
||||||
|
rb: reconcile.AlwaysRequeueResultBuilder{RequeueAfter: interval},
|
||||||
|
recResult: ctrl.Result{RequeueAfter: interval},
|
||||||
|
wantResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stalled success",
|
||||||
|
beforeFunc: func(obj conditions.Setter) {
|
||||||
|
conditions.MarkStalled(obj, "FooReason", "test-msg")
|
||||||
|
},
|
||||||
|
rb: reconcile.AlwaysRequeueResultBuilder{RequeueAfter: interval},
|
||||||
|
recResult: ctrl.Result{RequeueAfter: interval},
|
||||||
|
wantResult: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error result",
|
||||||
|
rb: reconcile.AlwaysRequeueResultBuilder{RequeueAfter: interval},
|
||||||
|
recResult: ctrl.Result{RequeueAfter: interval},
|
||||||
|
recErr: errors.New("some-error"),
|
||||||
|
wantResult: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non success result",
|
||||||
|
rb: reconcile.AlwaysRequeueResultBuilder{RequeueAfter: interval},
|
||||||
|
recResult: ctrl.Result{RequeueAfter: 2 * time.Second},
|
||||||
|
wantResult: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no result builder",
|
||||||
|
recResult: ctrl.Result{RequeueAfter: interval},
|
||||||
|
wantResult: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
obj := &sourcev1.GitRepository{}
|
||||||
|
if tt.beforeFunc != nil {
|
||||||
|
tt.beforeFunc(obj)
|
||||||
|
}
|
||||||
|
g.Expect(isNonStalledSuccess(obj, tt.rb, tt.recResult, tt.recErr)).To(Equal(tt.wantResult))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue