summarize: consider bipolarity in status condition

This introduces the consideration of bipolarity conditions in the status
condition summary for Ready condition. The summarize.HelperOptions can
now be configured with a list of bipolarity conditions which are used in
SummarizeAndPatch() to set the Ready condition to failing bipolarity
condition with the highest priority.

Bipolarity condition is not a typical status property. It is a mix of
positive and negative polarities. It's "normal-true" and
"abnormal-false". Failing bipolarity conditions are prioritized over
other conditions to show the actual reason of failure on the Ready
status.

Signed-off-by: Sunny <darkowlzz@protonmail.com>
This commit is contained in:
Sunny 2022-09-22 03:27:53 +05:30
parent c9a5a56cfb
commit e5d3aa3701
2 changed files with 111 additions and 9 deletions

View File

@ -90,6 +90,9 @@ type HelperOptions struct {
// PatchFieldOwner defines the field owner configuration for the Kubernetes // PatchFieldOwner defines the field owner configuration for the Kubernetes
// patch operation. // patch operation.
PatchFieldOwner string PatchFieldOwner string
// BiPolarityConditionTypes is a list of bipolar conditions in the order
// of priority.
BiPolarityConditionTypes []string
} }
// Option is configuration that modifies SummarizeAndPatch. // Option is configuration that modifies SummarizeAndPatch.
@ -149,6 +152,14 @@ func WithPatchFieldOwner(fieldOwner string) Option {
} }
} }
// WithBiPolarityConditionTypes sets the BiPolarityConditionTypes used to
// calculate the value of Ready condition in SummarizeAndPatch.
func WithBiPolarityConditionTypes(types ...string) Option {
return func(s *HelperOptions) {
s.BiPolarityConditionTypes = types
}
}
// SummarizeAndPatch summarizes and patches the result to the target object. // SummarizeAndPatch summarizes and patches the result to the target object.
// When used at the very end of a reconciliation, the result builder must be // When used at the very end of a reconciliation, the result builder must be
// specified using the Option WithResultBuilder(). The returned result and error // specified using the Option WithResultBuilder(). The returned result and error
@ -206,6 +217,26 @@ func (h *Helper) SummarizeAndPatch(ctx context.Context, obj conditions.Setter, o
) )
} }
// Check any BiPolarity conditions in the status that are False. Failing
// BiPolarity condition should be set as the Ready condition value to
// reflect the actual cause of the reconciliation failure.
// NOTE: This is applicable to Ready condition only because it is a special
// condition in kstatus that reflects the overall state of an object.
// IMPLEMENTATION NOTE: An implementation of this within the
// conditions.merge() exists in fluxcd/pkg repo branch `bipolarity`
// (https://github.com/fluxcd/pkg/commit/756b9e6d253a4fae93c05419b7019d0169454858).
// If that gets added to conditions.merge, the following can be removed.
var failedBiPolarity []string
for _, c := range opts.BiPolarityConditionTypes {
if conditions.IsFalse(obj, c) {
failedBiPolarity = append(failedBiPolarity, c)
}
}
if len(failedBiPolarity) > 0 {
topFailedBiPolarity := conditions.Get(obj, failedBiPolarity[0])
conditions.MarkFalse(obj, meta.ReadyCondition, topFailedBiPolarity.Reason, topFailedBiPolarity.Message)
}
// If object is not stalled, result is success and runtime error is nil, // 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 // ensure that Ready=True. Else, use the Ready failure message as the
// runtime error message. This ensures that the reconciliation would be // runtime error message. This ensures that the reconciliation would be

View File

@ -44,11 +44,16 @@ import (
// This tests the scenario where SummarizeAndPatch is used at the very end of a // This tests the scenario where SummarizeAndPatch is used at the very end of a
// reconciliation. // reconciliation.
func TestSummarizeAndPatch(t *testing.T) { func TestSummarizeAndPatch(t *testing.T) {
testBipolarCondition1 := "FooChecked1"
testBipolarCondition2 := "FooChecked2"
var testReadyConditions = Conditions{ var testReadyConditions = Conditions{
Target: meta.ReadyCondition, Target: meta.ReadyCondition,
Owned: []string{ Owned: []string{
sourcev1.FetchFailedCondition, sourcev1.FetchFailedCondition,
sourcev1.ArtifactOutdatedCondition, sourcev1.ArtifactOutdatedCondition,
sourcev1.SourceVerifiedCondition,
testBipolarCondition1,
testBipolarCondition2,
meta.ReadyCondition, meta.ReadyCondition,
meta.ReconcilingCondition, meta.ReconcilingCondition,
meta.StalledCondition, meta.StalledCondition,
@ -56,6 +61,9 @@ func TestSummarizeAndPatch(t *testing.T) {
Summarize: []string{ Summarize: []string{
sourcev1.FetchFailedCondition, sourcev1.FetchFailedCondition,
sourcev1.ArtifactOutdatedCondition, sourcev1.ArtifactOutdatedCondition,
sourcev1.SourceVerifiedCondition,
testBipolarCondition1,
testBipolarCondition2,
meta.StalledCondition, meta.StalledCondition,
meta.ReconcilingCondition, meta.ReconcilingCondition,
}, },
@ -66,6 +74,7 @@ func TestSummarizeAndPatch(t *testing.T) {
meta.ReconcilingCondition, meta.ReconcilingCondition,
}, },
} }
var testBipolarConditions = []string{sourcev1.SourceVerifiedCondition, testBipolarCondition1, testBipolarCondition2}
var testFooConditions = Conditions{ var testFooConditions = Conditions{
Target: "Foo", Target: "Foo",
Owned: []string{ Owned: []string{
@ -83,15 +92,16 @@ func TestSummarizeAndPatch(t *testing.T) {
} }
tests := []struct { tests := []struct {
name string name string
generation int64 generation int64
beforeFunc func(obj conditions.Setter) beforeFunc func(obj conditions.Setter)
result reconcile.Result result reconcile.Result
reconcileErr error reconcileErr error
conditions []Conditions conditions []Conditions
wantErr bool bipolarConditions []string
afterFunc func(t *WithT, obj client.Object) wantErr bool
assertConditions []metav1.Condition afterFunc func(t *WithT, obj client.Object)
assertConditions []metav1.Condition
}{ }{
// Success/Fail indicates if a reconciliation succeeded or failed. // Success/Fail indicates if a reconciliation succeeded or failed.
// The object generation is expected to match the observed generation in // The object generation is expected to match the observed generation in
@ -250,6 +260,64 @@ func TestSummarizeAndPatch(t *testing.T) {
}, },
wantErr: true, wantErr: true,
}, },
{
name: "Fail, reconciling with bipolar condition False, Ready gets bipolar failure value",
generation: 2,
beforeFunc: func(obj conditions.Setter) {
conditions.MarkReconciling(obj, "NewRevision", "new index revision")
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, "VerifyFailed", "verify failed")
},
result: reconcile.ResultEmpty,
reconcileErr: errors.New("failed to verify source"),
conditions: []Conditions{testReadyConditions},
bipolarConditions: testBipolarConditions,
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.FalseCondition(meta.ReadyCondition, "VerifyFailed", "verify failed"),
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "VerifyFailed", "verify failed"),
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new index revision"),
},
},
{
name: "Fail, bipolar condition True, negative polarity True, Ready gets negative polarity value",
generation: 2,
beforeFunc: func(obj conditions.Setter) {
conditions.MarkReconciling(obj, "NewGeneration", "new obj gen")
conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest")
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, "Success", "verified")
},
result: reconcile.ResultEmpty,
reconcileErr: errors.New("failed to create dir"),
conditions: []Conditions{testReadyConditions},
bipolarConditions: testBipolarConditions,
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.FalseCondition(meta.ReadyCondition, "NewRevision", "new digest"),
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest"),
*conditions.TrueCondition(meta.ReconcilingCondition, "NewGeneration", "new obj gen"),
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, "Success", "verified"),
},
},
{
name: "Fail, multiple bipolar conditions False, Ready gets the bipolar with high priority",
generation: 2,
beforeFunc: func(obj conditions.Setter) {
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, "Success", "verified")
conditions.MarkFalse(obj, testBipolarCondition1, "AAA", "aaa")
conditions.MarkFalse(obj, testBipolarCondition2, "BBB", "bbb")
},
result: reconcile.ResultEmpty,
reconcileErr: errors.New("some failure"),
conditions: []Conditions{testReadyConditions},
bipolarConditions: testBipolarConditions,
wantErr: true,
assertConditions: []metav1.Condition{
*conditions.FalseCondition(meta.ReadyCondition, "AAA", "aaa"),
*conditions.FalseCondition(testBipolarCondition1, "AAA", "aaa"),
*conditions.FalseCondition(testBipolarCondition2, "BBB", "bbb"),
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, "Success", "verified"),
},
},
} }
for _, tt := range tests { for _, tt := range tests {
@ -289,6 +357,9 @@ func TestSummarizeAndPatch(t *testing.T) {
WithProcessors(RecordContextualError, RecordReconcileReq), WithProcessors(RecordContextualError, RecordReconcileReq),
WithResultBuilder(reconcile.AlwaysRequeueResultBuilder{RequeueAfter: obj.Spec.Interval.Duration}), WithResultBuilder(reconcile.AlwaysRequeueResultBuilder{RequeueAfter: obj.Spec.Interval.Duration}),
} }
if tt.bipolarConditions != nil {
summaryOpts = append(summaryOpts, WithBiPolarityConditionTypes(tt.bipolarConditions...))
}
_, gotErr := summaryHelper.SummarizeAndPatch(ctx, obj, summaryOpts...) _, gotErr := summaryHelper.SummarizeAndPatch(ctx, obj, summaryOpts...)
g.Expect(gotErr != nil).To(Equal(tt.wantErr)) g.Expect(gotErr != nil).To(Equal(tt.wantErr))