Skip to content

Commit 621572e

Browse files
Add support negative polarity conditions
1 parent 9dfe342 commit 621572e

File tree

8 files changed

+328
-76
lines changed

8 files changed

+328
-76
lines changed

docs/proposals/20200506-conditions.md

Lines changed: 10 additions & 25 deletions
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,8 @@ 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` + positive polarity or when `Status=True` + negative polarity;
247+
severity it is designed to provide a standard classification of possible conditions failure `Reason`.
248248

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

302304
##### Controller changes
303305

@@ -471,6 +473,7 @@ enhance the condition utilities to handle those situations in a generalized way.
471473
- Risk: This proposal aims to be consistent with the target state of conditions in Kubernetes, but this
472474
is still under definition (see [KEP](https://github.com/kubernetes/enhancements/pull/1624)).
473475
- Mitigation: Periodically re-evaluate this proposal vs the Kubernetes KEP.
476+
- 2024-05-03: Change allowing conditions with negative polarity goes into this direction
474477

475478
- Risk: Cluster API presents some specific challenges that are not common to the core Kubernetes objects.
476479
- Mitigation: To allow a minimal set of carefully evaluated differences between Cluster API and Kubernetes
@@ -480,25 +483,6 @@ enhance the condition utilities to handle those situations in a generalized way.
480483
- Risk: This proposal allows for implementing conditions in incremental fashion, and this makes it complex
481484
to ensure a consistent approach across all objects.
482485
- 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.
502486

503487
## Alternatives
504488

@@ -569,5 +553,6 @@ NA
569553

570554
## Implementation History
571555

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

util/conditions/getter.go

Lines changed: 14 additions & 0 deletions
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 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

Lines changed: 51 additions & 3 deletions
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("positive-false1", "negative-unknown1", "negative-trueInfo1", "negative-trueWarning1", "negative-trueError1")
37+
positiveFalse1 = PositiveFalseCondition("positive-false1")
38+
negativeUnknown1 = UnknownCondition("negative-unknown1", "reason negative-unknown1", "message negative-unknown1")
39+
negativeTrueInfo1 = NegativeTrueCondition("negative-trueInfo1", "reason negative-trueInfo1", clusterv1.ConditionSeverityInfo, "message negative-trueInfo1")
40+
negativeTrueWarning1 = NegativeTrueCondition("negative-trueWarning1", "reason negative-trueWarning1", clusterv1.ConditionSeverityWarning, "message negative-trueWarning1")
41+
negativeTrueError1 = NegativeTrueCondition("negative-trueError1", "reason negative-trueError1", clusterv1.ConditionSeverityError, "message negative-trueError1")
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, positiveFalse1, negativeUnknown1, negativeTrueInfo1, negativeTrueWarning1, negativeTrueError1)
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, "positive-false1")).To(BeFalse())
69+
g.Expect(IsTrue(obj, "negative-trueInfo1")).To(BeTrue())
70+
g.Expect(IsTrue(obj, "negative-unknown1")).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, "positive-false1")).To(BeTrue())
78+
g.Expect(IsFalse(obj, "negative-trueInfo1")).To(BeFalse())
79+
g.Expect(IsFalse(obj, "negative-unknown1")).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, "positive-false1")).To(BeFalse())
87+
g.Expect(IsUnknown(obj, "negative-trueInfo1")).To(BeFalse())
88+
g.Expect(IsUnknown(obj, "negative-unknown1")).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, "negative-trueInfo1")).To(Equal("reason negative-trueInfo1"))
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, "negative-trueInfo1")).To(Equal("message negative-trueInfo1"))
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, "negative-trueInfo1")
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,9 @@ 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+
negativeFoo := PositiveFalseCondition("negative-foo")
157+
negativeBar := NegativeTrueCondition("negative-bar", "reason negative-falseInfo1", clusterv1.ConditionSeverityInfo, "message negative-falseInfo1")
158+
// negativeBaz := NegativeTrueCondition("negative-baz", "reason negative-falseInfo2", clusterv1.ConditionSeverityInfo, "message negative-falseInfo2")
135159
existingReady := FalseCondition(clusterv1.ReadyCondition, "reason falseError1", clusterv1.ConditionSeverityError, "message falseError1") // NB. existing ready has higher priority than other conditions
136160

137161
tests := []struct {
@@ -150,6 +174,18 @@ func TestSummary(t *testing.T) {
150174
from: getterWithConditions(foo, bar),
151175
want: FalseCondition(clusterv1.ReadyCondition, "reason falseInfo1", clusterv1.ConditionSeverityInfo, "message falseInfo1"),
152176
},
177+
{
178+
name: "Returns ready condition with the summary of existing conditions with negative polarity (with default options)",
179+
from: getterWithConditions(negativeFoo, negativeBar),
180+
options: []MergeOption{WithNegativePolarityConditions("negative-foo", "negative-bar")},
181+
want: FalseCondition(clusterv1.ReadyCondition, "reason negative-falseInfo1", clusterv1.ConditionSeverityInfo, "message negative-falseInfo1"),
182+
},
183+
{
184+
name: "Returns ready condition with the summary of existing conditions with mixed polarity (with default options)",
185+
from: getterWithConditions(foo, bar, negativeFoo, negativeBar),
186+
options: []MergeOption{WithNegativePolarityConditions("negative-foo", "negative-bar")},
187+
want: FalseCondition(clusterv1.ReadyCondition, "reason falseInfo1", clusterv1.ConditionSeverityInfo, "message falseInfo1"), // bar take precedence on negativeBar because it is listed first
188+
},
153189
{
154190
name: "Returns ready condition with the summary of existing conditions (using WithStepCounter options)",
155191
from: getterWithConditions(foo, bar),
@@ -186,6 +222,18 @@ func TestSummary(t *testing.T) {
186222
options: []MergeOption{WithConditions("foo")}, // bar should be ignored
187223
want: TrueCondition(clusterv1.ReadyCondition),
188224
},
225+
{
226+
name: "Returns ready condition with the summary of selected conditions with negative polarity (using WithConditions options)",
227+
from: getterWithConditions(negativeFoo, negativeBar),
228+
options: []MergeOption{WithConditions("negative-foo"), WithNegativePolarityConditions("negative-foo", "negative-bar")}, // negative-bar should be ignored
229+
want: TrueCondition(clusterv1.ReadyCondition),
230+
},
231+
{
232+
name: "Returns ready condition with the summary of selected conditions with mixed polarity (using WithConditions options)",
233+
from: getterWithConditions(foo, bar, negativeFoo, negativeBar),
234+
options: []MergeOption{WithConditions("foo", "negative-foo", "negative-bar"), WithNegativePolarityConditions("negative-foo", "negative-bar")}, // bar should be ignored
235+
want: FalseCondition(clusterv1.ReadyCondition, "reason negative-falseInfo1", clusterv1.ConditionSeverityInfo, "message negative-falseInfo1"),
236+
},
189237
{
190238
name: "Returns ready condition with the summary of selected conditions (using WithConditions and WithStepCounter options)",
191239
from: getterWithConditions(foo, bar, baz),

0 commit comments

Comments
 (0)