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:
parent
c9a5a56cfb
commit
e5d3aa3701
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue