Skip to content

Commit fe1c38e

Browse files
authored
Merge pull request #10550 from fabriziopandini/support-negative-polarity-conditions
✨ Add support negative polarity conditions
2 parents 54c8e46 + 4f1637e commit fe1c38e

File tree

8 files changed

+334
-76
lines changed

8 files changed

+334
-76
lines changed

docs/proposals/20200506-conditions.md

+11-25
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ reviewers:
66
- "@vincepri"
77
- "@ncdc"
88
creation-date: 2020-05-06
9-
last-updated: 2020-05-20
9+
last-updated: 2024-05-03
1010
status: implementable
1111
see-also:
1212
replaces:
@@ -232,7 +232,7 @@ const (
232232
)
233233
```
234234

235-
Condition types MUST have a consistent polarity (i.e. "True = good");
235+
Condition type names should make sense for humans; neither positive nor negative polarity can be recommended as a general rule.
236236

237237
Condition types SHOULD have one of the following suffix:
238238

@@ -243,8 +243,9 @@ When the above suffix are not adequate for a specific condition type, other suff
243243
(e.g. `Completed`, `Healthy`); however, it is recommended to balance this flexibility with the objective to provide
244244
a consistent condition naming across all the Cluster API objects.
245245

246-
The `Severity` field MUST be set only when `Status=False` and it is designed to provide a standard classification
247-
of possible conditions failure `Reason`.
246+
The `Severity` field MUST be set only when `Status=False` for conditions with positive polarity, or when `Status=True`
247+
for conditions with negative polarity; severity is designed to provide a standard classification of possible
248+
conditions failure `Reason`.
248249

249250
Please note that the combination of `Reason` and `Severity` gives different meaning to a condition failure
250251
allowing to detect when a long-running task is still ongoing:
@@ -298,6 +299,8 @@ the following constraints/design principles MUST be applied:
298299
of the underlying `Machines.Status.Conditions[Ready]` conditions.
299300
- A corollary of the above set of constraints is that an object SHOULD NEVER be in status `Ready=True`
300301
if one of the object's conditions are `false` or if one of the object dependents is in status `Ready=False`.
302+
- Condition that do not represent the operational state of the component, MUST not be included in the `Ready` condition
303+
(e.g. `Paused`, which represent a state of the controller that manages the component).
301304

302305
##### Controller changes
303306

@@ -471,6 +474,7 @@ enhance the condition utilities to handle those situations in a generalized way.
471474
- Risk: This proposal aims to be consistent with the target state of conditions in Kubernetes, but this
472475
is still under definition (see [KEP](https://github.com/kubernetes/enhancements/pull/1624)).
473476
- Mitigation: Periodically re-evaluate this proposal vs the Kubernetes KEP.
477+
- 2024-05-03: Change to allow conditions without positive polarity goes into this direction
474478

475479
- Risk: Cluster API presents some specific challenges that are not common to the core Kubernetes objects.
476480
- Mitigation: To allow a minimal set of carefully evaluated differences between Cluster API and Kubernetes
@@ -480,25 +484,6 @@ enhance the condition utilities to handle those situations in a generalized way.
480484
- Risk: This proposal allows for implementing conditions in incremental fashion, and this makes it complex
481485
to ensure a consistent approach across all objects.
482486
- Mitigation: Ensure all the implementations comply with the defined set of constraints/design principles.
483-
484-
- Risk: Having a consistent polarity ensures a simple and clear contract with the consumers, and it allows
485-
processing conditions in a simple and consistent way without being forced to implement specific logic
486-
for each condition type. However, we are aware about the fact that enforcing of consistent polarity (truthy)
487-
combined with the usage of recommended suffix for condition types can lead to verbal contortions to express
488-
conditions, especially in case of conditions designed to signal problems or in case of conditions
489-
that might exist or not.
490-
- Mitigation: We are relaxing the rule about recommended suffix and allowing usage of custom suffix.
491-
- Mitigation: We are recommending the condition adhere to the design principle to express the operational state
492-
of the component, and this should help in avoiding conditions name to surface internal implementation details.
493-
- Mitigation: We should recommend condition implementers to clearly document the meaning of Unknown state, because as
494-
discussed also in the recent [Kubernetes KEP about standardizing conditions](https://github.com/kubernetes/enhancements/pull/1624#pullrequestreview-388777427),
495-
_"Unknown" is a fact about the writer of the condition, and not a claim about the object_.
496-
- Mitigation: We should recommend developers of code relying on conditions to treat Unknown as a separated state vs
497-
assimilating it to True or False, because this can vary case by case and generate confusion in readers.
498-
499-
As a final consideration about the risk related to using a consistent polarity, it is important to notice that a
500-
consistent polarity ensure a clear meaning for True or o False states, which is already an improvement vs having
501-
different interpretations for all the three possible condition states.
502487

503488
## Alternatives
504489

@@ -569,5 +554,6 @@ NA
569554

570555
## Implementation History
571556

572-
- [ ] 2020-04-27: Compile a Google Doc following the CAEP template
573-
- [ ] 2020-05-06: Create CAEP PR
557+
- [x] 2020-04-27: Compile a Google Doc following the CAEP template
558+
- [x] 2020-05-06: Create CAEP PR
559+
- [x] 2024-05-03: Edited allowing conditions with negative polarity

util/conditions/getter.go

+14
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ func GetLastTransitionTime(from Getter, t clusterv1.ConditionType) *metav1.Time
117117

118118
// summary returns a Ready condition with the summary of all the conditions existing
119119
// on an object. If the object does not have other conditions, no summary condition is generated.
120+
// NOTE: The resulting Ready condition will have positive polarity; the conditions we are starting from might have positive or negative polarity.
120121
func summary(from Getter, options ...MergeOption) *clusterv1.Condition {
121122
conditions := from.GetConditions()
122123

@@ -147,8 +148,18 @@ func summary(from Getter, options ...MergeOption) *clusterv1.Condition {
147148
}
148149
}
149150

151+
// Keep track of the polarity of the condition we are starting from.
152+
polarity := PositivePolarity
153+
for _, t := range mergeOpt.negativeConditionTypes {
154+
if c.Type == t {
155+
polarity = NegativePolarity
156+
break
157+
}
158+
}
159+
150160
conditionsInScope = append(conditionsInScope, localizedCondition{
151161
Condition: &c,
162+
Polarity: polarity,
152163
Getter: from,
153164
})
154165
}
@@ -210,6 +221,7 @@ func WithFallbackValue(fallbackValue bool, reason string, severity clusterv1.Con
210221

211222
// mirror mirrors the Ready condition from a dependent object into the target condition;
212223
// if the Ready condition does not exists in the source object, no target conditions is generated.
224+
// NOTE: Considering that we are mirroring Ready conditions with positive polarity, also the resulting condition will have positive polarity.
213225
func mirror(from Getter, targetCondition clusterv1.ConditionType, options ...MirrorOptions) *clusterv1.Condition {
214226
mirrorOpt := &mirrorOptions{}
215227
for _, o := range options {
@@ -237,13 +249,15 @@ func mirror(from Getter, targetCondition clusterv1.ConditionType, options ...Mir
237249
// Aggregates all the Ready condition from a list of dependent objects into the target object;
238250
// if the Ready condition does not exists in one of the source object, the object is excluded from
239251
// the aggregation; if none of the source object have ready condition, no target conditions is generated.
252+
// NOTE: Considering that we are aggregating Ready conditions with positive polarity, also the resulting condition will have positive polarity.
240253
func aggregate(from []Getter, targetCondition clusterv1.ConditionType, options ...MergeOption) *clusterv1.Condition {
241254
conditionsInScope := make([]localizedCondition, 0, len(from))
242255
for i := range from {
243256
condition := Get(from[i], clusterv1.ReadyCondition)
244257

245258
conditionsInScope = append(conditionsInScope, localizedCondition{
246259
Condition: condition,
260+
Polarity: PositivePolarity,
247261
Getter: from[i],
248262
})
249263
}

util/conditions/getter_test.go

+50-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"testing"
2121

2222
. "github.com/onsi/gomega"
23+
"k8s.io/apimachinery/pkg/util/sets"
2324

2425
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
2526
)
@@ -31,6 +32,13 @@ var (
3132
falseInfo1 = FalseCondition("falseInfo1", "reason falseInfo1", clusterv1.ConditionSeverityInfo, "message falseInfo1")
3233
falseWarning1 = FalseCondition("falseWarning1", "reason falseWarning1", clusterv1.ConditionSeverityWarning, "message falseWarning1")
3334
falseError1 = FalseCondition("falseError1", "reason falseError1", clusterv1.ConditionSeverityError, "message falseError1")
35+
36+
negativePolarityConditions = sets.New("false1-negative-polarity", "unknown1-negative-polarity", "trueInfo1-negative-polarity", "trueWarning1-negative-polarity", "trueError1-negative-polarity")
37+
false1WithNegativePolarity = FalseConditionWithNegativePolarity("false1-negative-polarity")
38+
unknown1WithNegativePolarity = UnknownCondition("unknown1-negative-polarity", "reason unknown1-negative-polarity", "message unknown1-negative-polarity")
39+
trueInfo1WithNegativePolarity = TrueConditionWithNegativePolarity("trueInfo1-negative-polarity", "reason trueInfo1-negative-polarity", clusterv1.ConditionSeverityInfo, "message trueInfo1-negative-polarity")
40+
trueWarning1WithNegativePolarity = TrueConditionWithNegativePolarity("trueWarning1-negative-polarity", "reason trueWarning1-negative-polarity", clusterv1.ConditionSeverityWarning, "message trueWarning1-negative-polarity")
41+
trueError1WithNegativePolarity = TrueConditionWithNegativePolarity("trueError1-negative-polarity", "reason trueError1-negative-polarity", clusterv1.ConditionSeverityError, "message trueError1-negative-polarity")
3442
)
3543

3644
func TestGetAndHas(t *testing.T) {
@@ -50,41 +58,54 @@ func TestGetAndHas(t *testing.T) {
5058
func TestIsMethods(t *testing.T) {
5159
g := NewWithT(t)
5260

53-
obj := getterWithConditions(nil1, true1, unknown1, falseInfo1, falseWarning1, falseError1)
61+
obj := getterWithConditions(nil1, true1, unknown1, falseInfo1, falseWarning1, falseError1, false1WithNegativePolarity, unknown1WithNegativePolarity, trueInfo1WithNegativePolarity, trueWarning1WithNegativePolarity, trueError1WithNegativePolarity)
5462

5563
// test isTrue
5664
g.Expect(IsTrue(obj, "nil1")).To(BeFalse())
5765
g.Expect(IsTrue(obj, "true1")).To(BeTrue())
5866
g.Expect(IsTrue(obj, "falseInfo1")).To(BeFalse())
5967
g.Expect(IsTrue(obj, "unknown1")).To(BeFalse())
68+
g.Expect(IsTrue(obj, "false1-negative-polarity")).To(BeFalse())
69+
g.Expect(IsTrue(obj, "trueInfo1-negative-polarity")).To(BeTrue())
70+
g.Expect(IsTrue(obj, "unknown1-negative-polarity")).To(BeFalse())
6071

6172
// test isFalse
6273
g.Expect(IsFalse(obj, "nil1")).To(BeFalse())
6374
g.Expect(IsFalse(obj, "true1")).To(BeFalse())
6475
g.Expect(IsFalse(obj, "falseInfo1")).To(BeTrue())
6576
g.Expect(IsFalse(obj, "unknown1")).To(BeFalse())
77+
g.Expect(IsFalse(obj, "false1-negative-polarity")).To(BeTrue())
78+
g.Expect(IsFalse(obj, "trueInfo1-negative-polarity")).To(BeFalse())
79+
g.Expect(IsFalse(obj, "unknown1-negative-polarity")).To(BeFalse())
6680

6781
// test isUnknown
6882
g.Expect(IsUnknown(obj, "nil1")).To(BeTrue())
6983
g.Expect(IsUnknown(obj, "true1")).To(BeFalse())
7084
g.Expect(IsUnknown(obj, "falseInfo1")).To(BeFalse())
7185
g.Expect(IsUnknown(obj, "unknown1")).To(BeTrue())
86+
g.Expect(IsUnknown(obj, "false1-negative-polarity")).To(BeFalse())
87+
g.Expect(IsUnknown(obj, "trueInfo1-negative-polarity")).To(BeFalse())
88+
g.Expect(IsUnknown(obj, "unknown1-negative-polarity")).To(BeTrue())
7289

7390
// test GetReason
7491
g.Expect(GetReason(obj, "nil1")).To(Equal(""))
7592
g.Expect(GetReason(obj, "falseInfo1")).To(Equal("reason falseInfo1"))
93+
g.Expect(GetReason(obj, "trueInfo1-negative-polarity")).To(Equal("reason trueInfo1-negative-polarity"))
7694

7795
// test GetMessage
7896
g.Expect(GetMessage(obj, "nil1")).To(Equal(""))
7997
g.Expect(GetMessage(obj, "falseInfo1")).To(Equal("message falseInfo1"))
98+
g.Expect(GetMessage(obj, "trueInfo1-negative-polarity")).To(Equal("message trueInfo1-negative-polarity"))
8099

81100
// test GetSeverity
101+
expectedSeverity := clusterv1.ConditionSeverityInfo
82102
g.Expect(GetSeverity(obj, "nil1")).To(BeNil())
83103
severity := GetSeverity(obj, "falseInfo1")
84-
expectedSeverity := clusterv1.ConditionSeverityInfo
104+
g.Expect(severity).To(Equal(&expectedSeverity))
105+
severity = GetSeverity(obj, "trueInfo1-negative-polarity")
85106
g.Expect(severity).To(Equal(&expectedSeverity))
86107

87-
// test GetMessage
108+
// test GetLastTransitionTime
88109
g.Expect(GetLastTransitionTime(obj, "nil1")).To(BeNil())
89110
g.Expect(GetLastTransitionTime(obj, "falseInfo1")).ToNot(BeNil())
90111
}
@@ -132,6 +153,8 @@ func TestSummary(t *testing.T) {
132153
foo := TrueCondition("foo")
133154
bar := FalseCondition("bar", "reason falseInfo1", clusterv1.ConditionSeverityInfo, "message falseInfo1")
134155
baz := FalseCondition("baz", "reason falseInfo2", clusterv1.ConditionSeverityInfo, "message falseInfo2")
156+
fooWithNegativePolarity := FalseConditionWithNegativePolarity("foo-negative-polarity")
157+
barWithNegativePolarity := TrueConditionWithNegativePolarity("bar-negative-polarity", "reason trueInfo1-negative-polarity", clusterv1.ConditionSeverityInfo, "message trueInfo1-negative-polarity")
135158
existingReady := FalseCondition(clusterv1.ReadyCondition, "reason falseError1", clusterv1.ConditionSeverityError, "message falseError1") // NB. existing ready has higher priority than other conditions
136159

137160
tests := []struct {
@@ -150,6 +173,18 @@ func TestSummary(t *testing.T) {
150173
from: getterWithConditions(foo, bar),
151174
want: FalseCondition(clusterv1.ReadyCondition, "reason falseInfo1", clusterv1.ConditionSeverityInfo, "message falseInfo1"),
152175
},
176+
{
177+
name: "Returns ready condition with the summary of existing conditions with negative polarity (with default options)",
178+
from: getterWithConditions(fooWithNegativePolarity, barWithNegativePolarity),
179+
options: []MergeOption{WithNegativePolarityConditions("foo-negative-polarity", "bar-negative-polarity")},
180+
want: FalseCondition(clusterv1.ReadyCondition, "reason trueInfo1-negative-polarity", clusterv1.ConditionSeverityInfo, "message trueInfo1-negative-polarity"),
181+
},
182+
{
183+
name: "Returns ready condition with the summary of existing conditions with mixed polarity (with default options)",
184+
from: getterWithConditions(foo, bar, fooWithNegativePolarity, barWithNegativePolarity),
185+
options: []MergeOption{WithNegativePolarityConditions("foo-negative-polarity", "bar-negative-polarity")},
186+
want: FalseCondition(clusterv1.ReadyCondition, "reason falseInfo1", clusterv1.ConditionSeverityInfo, "message falseInfo1"), // bar take precedence on barWithNegativePolarity because it is listed first
187+
},
153188
{
154189
name: "Returns ready condition with the summary of existing conditions (using WithStepCounter options)",
155190
from: getterWithConditions(foo, bar),
@@ -186,6 +221,18 @@ func TestSummary(t *testing.T) {
186221
options: []MergeOption{WithConditions("foo")}, // bar should be ignored
187222
want: TrueCondition(clusterv1.ReadyCondition),
188223
},
224+
{
225+
name: "Returns ready condition with the summary of selected conditions with negative polarity (using WithConditions options)",
226+
from: getterWithConditions(fooWithNegativePolarity, barWithNegativePolarity),
227+
options: []MergeOption{WithConditions("foo-negative-polarity"), WithNegativePolarityConditions("foo-negative-polarity", "bar-negative-polarity")}, // bar-negative-polarity should be ignored because it is not listed in WithConditions
228+
want: TrueCondition(clusterv1.ReadyCondition),
229+
},
230+
{
231+
name: "Returns ready condition with the summary of selected conditions with mixed polarity (using WithConditions options)",
232+
from: getterWithConditions(foo, bar, fooWithNegativePolarity, barWithNegativePolarity),
233+
options: []MergeOption{WithConditions("foo", "foo-negative-polarity", "bar-negative-polarity"), WithNegativePolarityConditions("foo-negative-polarity", "bar-negative-polarity")},
234+
want: FalseCondition(clusterv1.ReadyCondition, "reason trueInfo1-negative-polarity", clusterv1.ConditionSeverityInfo, "message trueInfo1-negative-polarity"),
235+
},
189236
{
190237
name: "Returns ready condition with the summary of selected conditions (using WithConditions and WithStepCounter options)",
191238
from: getterWithConditions(foo, bar, baz),

0 commit comments

Comments
 (0)