-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement utils for metav1 conditions
- Loading branch information
1 parent
9c08773
commit 66215ac
Showing
16 changed files
with
2,782 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
/* | ||
Copyright 2024 The Kubernetes Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package experimental | ||
|
||
import ( | ||
"fmt" | ||
|
||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
) | ||
|
||
// AggregateOption is some configuration that modifies options for a aggregate request. | ||
type AggregateOption interface { | ||
// ApplyToAggregate applies this configuration to the given aggregate options. | ||
ApplyToAggregate(option *AggregateOptions) | ||
} | ||
|
||
// AggregateOptions allows to set options for the aggregate operation. | ||
type AggregateOptions struct { | ||
mergeStrategy MergeStrategy | ||
overrideType string | ||
} | ||
|
||
// ApplyOptions applies the given list options on these options, | ||
// and then returns itself (for convenient chaining). | ||
func (o *AggregateOptions) ApplyOptions(opts []AggregateOption) *AggregateOptions { | ||
for _, opt := range opts { | ||
opt.ApplyToAggregate(o) | ||
} | ||
return o | ||
} | ||
|
||
// NewAggregateCondition aggregates a condition from a list of objects; if the given condition does not exist in the source object, | ||
// missing conditions are considered Unknown, reason NotYetReported. | ||
// | ||
// By default, the Aggregate condition has the same type of the source condition, but this can be changed by using | ||
// the OverrideType option. | ||
// | ||
// Additionally, it is possible to inject custom merge strategies using the WithMergeStrategy option. | ||
func NewAggregateCondition(objects []runtime.Object, conditionType string, opts ...AggregateOption) (*metav1.Condition, error) { | ||
aggregateOpt := &AggregateOptions{ | ||
mergeStrategy: newDefaultMergeStrategy(), | ||
} | ||
aggregateOpt.ApplyOptions(opts) | ||
|
||
conditionsInScope := make([]ConditionWithOwnerInfo, 0, len(objects)) | ||
for _, obj := range objects { | ||
// TODO: consider if we want to aggregate all errors before returning | ||
conditions, err := getConditionsWithOwnerInfo(obj) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Drops all the conditions not in scope for the merge operation | ||
hasConditionType := false | ||
for _, condition := range conditions { | ||
if condition.Type != conditionType { | ||
continue | ||
} | ||
conditionsInScope = append(conditionsInScope, condition) | ||
hasConditionType = true | ||
} | ||
|
||
// Add the expected conditions if it does not exist, so we are compliant with K8s guidelines | ||
// (all missing conditions should be considered unknown). | ||
if !hasConditionType { | ||
conditionOwner := getConditionOwnerInfo(obj) | ||
|
||
conditionsInScope = append(conditionsInScope, ConditionWithOwnerInfo{ | ||
OwnerResource: conditionOwner, | ||
Condition: metav1.Condition{ | ||
Type: conditionType, | ||
Status: metav1.ConditionUnknown, | ||
Reason: NotYetReportedReason, | ||
Message: fmt.Sprintf("Condition %s not yet reported from %s", conditionType, conditionOwner.String()), | ||
}, | ||
}) | ||
} | ||
} | ||
|
||
status, reason, message, err := aggregateOpt.mergeStrategy.Merge( | ||
conditionsInScope, | ||
[]string{conditionType}, | ||
nil, // negative conditions | ||
false, // step counter | ||
) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
c := &metav1.Condition{ | ||
Type: conditionType, | ||
Status: status, | ||
Reason: reason, | ||
Message: message, | ||
} | ||
|
||
if aggregateOpt.overrideType != "" { | ||
c.Type = aggregateOpt.overrideType | ||
} | ||
|
||
return c, err | ||
} | ||
|
||
// SetAggregateCondition is a convenience method that calls NewAggregateCondition to create an aggregate condition from the source objects, | ||
// and then calls Set to add the new condition to the target object. | ||
func SetAggregateCondition(sourceObjs []runtime.Object, targetObj runtime.Object, conditionType string, opts ...AggregateOption) error { | ||
mirrorCondition, err := NewAggregateCondition(sourceObjs, conditionType, opts...) | ||
if err != nil { | ||
return err | ||
} | ||
return Set(targetObj, *mirrorCondition) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
/* | ||
Copyright 2024 The Kubernetes Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package experimental | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
. "github.com/onsi/gomega" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
) | ||
|
||
func TestAggregate(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
conditions [][]metav1.Condition | ||
conditionType string | ||
options []AggregateOption | ||
want *metav1.Condition | ||
}{ | ||
{ | ||
name: "One issue", | ||
conditions: [][]metav1.Condition{ | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-1", Message: "Message-1"}}, // obj1 | ||
{{Type: AvailableCondition, Status: metav1.ConditionTrue, Reason: "Reason-99", Message: "Message-99"}}, // obj2 | ||
}, | ||
conditionType: AvailableCondition, | ||
options: []AggregateOption{}, | ||
want: &metav1.Condition{ | ||
Type: AvailableCondition, | ||
Status: metav1.ConditionFalse, // False because there is one issue | ||
Reason: "Reason-1", // Picking the reason from the only existing issue | ||
Message: "(False): Message-1 from default/obj0", // messages from all the issues & unknown conditions (info dropped) | ||
}, | ||
}, | ||
{ | ||
name: "Same issue from up to tree objects", | ||
conditions: [][]metav1.Condition{ | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-1", Message: "Message-1"}}, // obj1 | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-1", Message: "Message-1"}}, // obj2 | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-1", Message: "Message-1"}}, // obj3 | ||
{{Type: AvailableCondition, Status: metav1.ConditionTrue, Reason: "Reason-99", Message: "Message-99"}}, // obj4 | ||
}, | ||
conditionType: AvailableCondition, | ||
options: []AggregateOption{}, | ||
want: &metav1.Condition{ | ||
Type: AvailableCondition, | ||
Status: metav1.ConditionFalse, // False because there is one issue | ||
Reason: ManyIssuesReason, // Using a generic reason | ||
Message: "(False): Message-1 from default/obj0, default/obj1, default/obj2", // messages from all the issues & unknown conditions (info dropped) | ||
}, | ||
}, | ||
{ | ||
name: "Same issue from more than tree objects", | ||
conditions: [][]metav1.Condition{ | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-1", Message: "Message-1"}}, // obj1 | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-1", Message: "Message-1"}}, // obj2 | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-1", Message: "Message-1"}}, // obj3 | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-1", Message: "Message-1"}}, // obj4 | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-B", Message: "Message-1"}}, // obj5 | ||
{{Type: AvailableCondition, Status: metav1.ConditionTrue, Message: "Message-99"}}, // obj6 | ||
}, | ||
conditionType: AvailableCondition, | ||
options: []AggregateOption{}, | ||
want: &metav1.Condition{ | ||
Type: AvailableCondition, | ||
Status: metav1.ConditionFalse, // False because there is one issue | ||
Reason: ManyIssuesReason, // Using a generic reason | ||
Message: "(False): Message-1 from default/obj0, default/obj1, default/obj2 and 2 other V1Beta2ResourceWithConditions", // messages from all the issues & unknown conditions (info dropped) | ||
}, | ||
}, | ||
{ | ||
name: "Up to three different issue messages", | ||
conditions: [][]metav1.Condition{ | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-1", Message: "Message-1"}}, // obj1 | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-2", Message: "Message-2"}}, // obj2 | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-2", Message: "Message-2"}}, // obj3 | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-1", Message: "Message-1"}}, // obj4 | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-1", Message: "Message-1"}}, // obj5 | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-3", Message: "Message-3"}}, // obj6 | ||
{{Type: AvailableCondition, Status: metav1.ConditionTrue, Reason: "Reason-99", Message: "Message-99"}}, // obj7 | ||
}, | ||
conditionType: AvailableCondition, | ||
options: []AggregateOption{}, | ||
want: &metav1.Condition{ | ||
Type: AvailableCondition, | ||
Status: metav1.ConditionFalse, // False because there is one issue | ||
Reason: ManyIssuesReason, // Using a generic reason | ||
Message: "(False): Message-1 from default/obj0, default/obj3, default/obj4; (False): Message-2 from default/obj1, default/obj2; (False): Message-3 from default/obj5", // messages from all the issues & unknown conditions (info dropped) | ||
}, | ||
}, | ||
{ | ||
name: "More than three different issue messages", | ||
conditions: [][]metav1.Condition{ | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-1", Message: "Message-1"}}, // obj1 | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-2", Message: "Message-2"}}, // obj2 | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-4", Message: "Message-4"}}, // obj3 | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-5", Message: "Message-5"}}, // obj4 | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-1", Message: "Message-1"}}, // obj5 | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-3", Message: "Message-3"}}, // obj6 | ||
{{Type: AvailableCondition, Status: metav1.ConditionTrue, Reason: "Reason-99", Message: "Message-99"}}, // obj7 | ||
}, | ||
conditionType: AvailableCondition, | ||
options: []AggregateOption{}, | ||
want: &metav1.Condition{ | ||
Type: AvailableCondition, | ||
Status: metav1.ConditionFalse, // False because there is one issue | ||
Reason: ManyIssuesReason, // Using a generic reason | ||
Message: "(False): Message-1 from default/obj0, default/obj4; (False): Message-2 from default/obj1; (False): Message-3 from default/obj5; other 2 V1Beta2ResourceWithConditions with issues", // messages from all the issues & unknown conditions (info dropped) | ||
}, | ||
}, | ||
{ | ||
name: "Less than 2 issue messages and unknown message", | ||
conditions: [][]metav1.Condition{ | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-1", Message: "Message-1"}}, // obj1 | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-2", Message: "Message-2"}}, // obj2 | ||
{{Type: AvailableCondition, Status: metav1.ConditionUnknown, Reason: "Reason-3", Message: "Message-3"}}, // obj3 | ||
{{Type: AvailableCondition, Status: metav1.ConditionTrue, Reason: "Reason-99", Message: "Message-99"}}, // obj4 | ||
}, | ||
conditionType: AvailableCondition, | ||
options: []AggregateOption{}, | ||
want: &metav1.Condition{ | ||
Type: AvailableCondition, | ||
Status: metav1.ConditionFalse, // False because there is one issue | ||
Reason: ManyIssuesReason, // Using a generic reason | ||
Message: "(False): Message-1 from default/obj0; (False): Message-2 from default/obj1; (Unknown): Message-3 from default/obj2", // messages from all the issues & unknown conditions (info dropped) | ||
}, | ||
}, | ||
{ | ||
name: "At least 3 issue messages and unknown message", | ||
conditions: [][]metav1.Condition{ | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-1", Message: "Message-1"}}, // obj1 | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-2", Message: "Message-2"}}, // obj2 | ||
{{Type: AvailableCondition, Status: metav1.ConditionUnknown, Reason: "Reason-3", Message: "Message-3"}}, // obj3 | ||
{{Type: AvailableCondition, Status: metav1.ConditionFalse, Reason: "Reason-4", Message: "Message-4"}}, // obj3 | ||
{{Type: AvailableCondition, Status: metav1.ConditionTrue, Reason: "Reason-99", Message: "Message-99"}}, // obj4 | ||
}, | ||
conditionType: AvailableCondition, | ||
options: []AggregateOption{}, | ||
want: &metav1.Condition{ | ||
Type: AvailableCondition, | ||
Status: metav1.ConditionFalse, // False because there is one issue | ||
Reason: ManyIssuesReason, // Using a generic reason | ||
Message: "(False): Message-1 from default/obj0; (False): Message-2 from default/obj1; (False): Message-4 from default/obj3; other 1 V1Beta2ResourceWithConditions unknown", // messages from all the issues & unknown conditions (info dropped) | ||
}, | ||
}, | ||
{ | ||
name: "unknown messages", | ||
conditions: [][]metav1.Condition{ | ||
{{Type: AvailableCondition, Status: metav1.ConditionUnknown, Reason: "Reason-1", Message: "Message-1"}}, // obj1 | ||
{{Type: AvailableCondition, Status: metav1.ConditionUnknown, Reason: "Reason-2", Message: "Message-2"}}, // obj2 | ||
{{Type: AvailableCondition, Status: metav1.ConditionUnknown, Reason: "Reason-4", Message: "Message-4"}}, // obj3 | ||
{{Type: AvailableCondition, Status: metav1.ConditionUnknown, Reason: "Reason-5", Message: "Message-5"}}, // obj4 | ||
{{Type: AvailableCondition, Status: metav1.ConditionUnknown, Reason: "Reason-1", Message: "Message-1"}}, // obj5 | ||
{{Type: AvailableCondition, Status: metav1.ConditionUnknown, Reason: "Reason-3", Message: "Message-3"}}, // obj6 | ||
{{Type: AvailableCondition, Status: metav1.ConditionTrue, Reason: "Reason-99", Message: "Message-99"}}, // obj7 | ||
}, | ||
conditionType: AvailableCondition, | ||
options: []AggregateOption{}, | ||
want: &metav1.Condition{ | ||
Type: AvailableCondition, | ||
Status: metav1.ConditionUnknown, // False because there is one issue | ||
Reason: ManyUnknownsReason, // Using a generic reason | ||
Message: "(Unknown): Message-1 from default/obj0, default/obj4; (Unknown): Message-2 from default/obj1; (Unknown): Message-3 from default/obj5; other 2 V1Beta2ResourceWithConditions unknown", // messages from all the issues & unknown conditions (info dropped) | ||
}, | ||
}, | ||
{ | ||
name: "info messages", | ||
conditions: [][]metav1.Condition{ | ||
{{Type: AvailableCondition, Status: metav1.ConditionTrue, Reason: "Reason-1", Message: "Message-1"}}, // obj1 | ||
{{Type: AvailableCondition, Status: metav1.ConditionTrue, Reason: "Reason-2", Message: "Message-2"}}, // obj2 | ||
{{Type: AvailableCondition, Status: metav1.ConditionTrue, Reason: "Reason-4", Message: "Message-4"}}, // obj3 | ||
{{Type: AvailableCondition, Status: metav1.ConditionTrue, Reason: "Reason-5", Message: ""}}, // obj4 | ||
{{Type: AvailableCondition, Status: metav1.ConditionTrue, Reason: "Reason-1", Message: "Message-1"}}, // obj5 | ||
{{Type: AvailableCondition, Status: metav1.ConditionTrue, Reason: "Reason-3", Message: "Message-3"}}, // obj6 | ||
}, | ||
conditionType: AvailableCondition, | ||
options: []AggregateOption{}, | ||
want: &metav1.Condition{ | ||
Type: AvailableCondition, | ||
Status: metav1.ConditionTrue, // False because there is one issue | ||
Reason: ManyInfoReason, // Using a generic reason | ||
Message: "(True): Message-1 from default/obj0, default/obj4; (True): Message-2 from default/obj1; (True): Message-3 from default/obj5; other 1 V1Beta2ResourceWithConditions with info messages", // messages from all the issues & unknown conditions (info dropped) | ||
}, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
g := NewWithT(t) | ||
|
||
objs := make([]runtime.Object, 0, len(tt.conditions)) | ||
for i := range tt.conditions { | ||
objs = append(objs, &V1Beta2ResourceWithConditions{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Namespace: metav1.NamespaceDefault, | ||
Name: fmt.Sprintf("obj%d", i), | ||
}, | ||
Status: struct{ Conditions []metav1.Condition }{ | ||
Conditions: tt.conditions[i], | ||
}, | ||
}) | ||
} | ||
|
||
got, err := NewAggregateCondition(objs, tt.conditionType, tt.options...) | ||
g.Expect(err).ToNot(HaveOccurred()) | ||
|
||
g.Expect(got).To(Equal(tt.want)) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/* | ||
Copyright 2024 The Kubernetes Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
// Package experimental implements experimental condition utilities. | ||
package experimental |
Oops, something went wrong.