Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add support negative polarity conditions #10550

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 11 additions & 25 deletions docs/proposals/20200506-conditions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ reviewers:
- "@vincepri"
- "@ncdc"
creation-date: 2020-05-06
last-updated: 2020-05-20
last-updated: 2024-05-03
status: implementable
see-also:
replaces:
Expand Down Expand Up @@ -232,7 +232,7 @@ const (
)
```

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

Condition types SHOULD have one of the following suffix:

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

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

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

##### Controller changes

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

- Risk: Cluster API presents some specific challenges that are not common to the core Kubernetes objects.
- Mitigation: To allow a minimal set of carefully evaluated differences between Cluster API and Kubernetes
Expand All @@ -480,25 +484,6 @@ enhance the condition utilities to handle those situations in a generalized way.
- Risk: This proposal allows for implementing conditions in incremental fashion, and this makes it complex
to ensure a consistent approach across all objects.
- Mitigation: Ensure all the implementations comply with the defined set of constraints/design principles.

- Risk: Having a consistent polarity ensures a simple and clear contract with the consumers, and it allows
processing conditions in a simple and consistent way without being forced to implement specific logic
for each condition type. However, we are aware about the fact that enforcing of consistent polarity (truthy)
combined with the usage of recommended suffix for condition types can lead to verbal contortions to express
conditions, especially in case of conditions designed to signal problems or in case of conditions
that might exist or not.
- Mitigation: We are relaxing the rule about recommended suffix and allowing usage of custom suffix.
- Mitigation: We are recommending the condition adhere to the design principle to express the operational state
of the component, and this should help in avoiding conditions name to surface internal implementation details.
- Mitigation: We should recommend condition implementers to clearly document the meaning of Unknown state, because as
discussed also in the recent [Kubernetes KEP about standardizing conditions](https://github.com/kubernetes/enhancements/pull/1624#pullrequestreview-388777427),
_"Unknown" is a fact about the writer of the condition, and not a claim about the object_.
- Mitigation: We should recommend developers of code relying on conditions to treat Unknown as a separated state vs
assimilating it to True or False, because this can vary case by case and generate confusion in readers.

As a final consideration about the risk related to using a consistent polarity, it is important to notice that a
consistent polarity ensure a clear meaning for True or o False states, which is already an improvement vs having
different interpretations for all the three possible condition states.

## Alternatives

Expand Down Expand Up @@ -569,5 +554,6 @@ NA

## Implementation History

- [ ] 2020-04-27: Compile a Google Doc following the CAEP template
- [ ] 2020-05-06: Create CAEP PR
- [x] 2020-04-27: Compile a Google Doc following the CAEP template
- [x] 2020-05-06: Create CAEP PR
- [x] 2024-05-03: Edited allowing conditions with negative polarity
14 changes: 14 additions & 0 deletions util/conditions/getter.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func GetLastTransitionTime(from Getter, t clusterv1.ConditionType) *metav1.Time

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

Expand Down Expand Up @@ -147,8 +148,18 @@ func summary(from Getter, options ...MergeOption) *clusterv1.Condition {
}
}

// Keep track of the polarity of the condition we are starting from.
polarity := PositivePolarity
for _, t := range mergeOpt.negativeConditionTypes {
if c.Type == t {
polarity = NegativePolarity
break
}
}

conditionsInScope = append(conditionsInScope, localizedCondition{
Condition: &c,
Polarity: polarity,
Getter: from,
})
}
Expand Down Expand Up @@ -210,6 +221,7 @@ func WithFallbackValue(fallbackValue bool, reason string, severity clusterv1.Con

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

conditionsInScope = append(conditionsInScope, localizedCondition{
Condition: condition,
Polarity: PositivePolarity,
Getter: from[i],
})
}
Expand Down
53 changes: 50 additions & 3 deletions util/conditions/getter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"testing"

. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/util/sets"

clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
)
Expand All @@ -31,6 +32,13 @@ var (
falseInfo1 = FalseCondition("falseInfo1", "reason falseInfo1", clusterv1.ConditionSeverityInfo, "message falseInfo1")
falseWarning1 = FalseCondition("falseWarning1", "reason falseWarning1", clusterv1.ConditionSeverityWarning, "message falseWarning1")
falseError1 = FalseCondition("falseError1", "reason falseError1", clusterv1.ConditionSeverityError, "message falseError1")

negativePolarityConditions = sets.New("false1-negative-polarity", "unknown1-negative-polarity", "trueInfo1-negative-polarity", "trueWarning1-negative-polarity", "trueError1-negative-polarity")
false1WithNegativePolarity = FalseConditionWithNegativePolarity("false1-negative-polarity")
unknown1WithNegativePolarity = UnknownCondition("unknown1-negative-polarity", "reason unknown1-negative-polarity", "message unknown1-negative-polarity")
trueInfo1WithNegativePolarity = TrueConditionWithNegativePolarity("trueInfo1-negative-polarity", "reason trueInfo1-negative-polarity", clusterv1.ConditionSeverityInfo, "message trueInfo1-negative-polarity")
trueWarning1WithNegativePolarity = TrueConditionWithNegativePolarity("trueWarning1-negative-polarity", "reason trueWarning1-negative-polarity", clusterv1.ConditionSeverityWarning, "message trueWarning1-negative-polarity")
trueError1WithNegativePolarity = TrueConditionWithNegativePolarity("trueError1-negative-polarity", "reason trueError1-negative-polarity", clusterv1.ConditionSeverityError, "message trueError1-negative-polarity")
)

func TestGetAndHas(t *testing.T) {
Expand All @@ -50,41 +58,54 @@ func TestGetAndHas(t *testing.T) {
func TestIsMethods(t *testing.T) {
g := NewWithT(t)

obj := getterWithConditions(nil1, true1, unknown1, falseInfo1, falseWarning1, falseError1)
obj := getterWithConditions(nil1, true1, unknown1, falseInfo1, falseWarning1, falseError1, false1WithNegativePolarity, unknown1WithNegativePolarity, trueInfo1WithNegativePolarity, trueWarning1WithNegativePolarity, trueError1WithNegativePolarity)

// test isTrue
g.Expect(IsTrue(obj, "nil1")).To(BeFalse())
g.Expect(IsTrue(obj, "true1")).To(BeTrue())
g.Expect(IsTrue(obj, "falseInfo1")).To(BeFalse())
g.Expect(IsTrue(obj, "unknown1")).To(BeFalse())
g.Expect(IsTrue(obj, "false1-negative-polarity")).To(BeFalse())
g.Expect(IsTrue(obj, "trueInfo1-negative-polarity")).To(BeTrue())
g.Expect(IsTrue(obj, "unknown1-negative-polarity")).To(BeFalse())

// test isFalse
g.Expect(IsFalse(obj, "nil1")).To(BeFalse())
g.Expect(IsFalse(obj, "true1")).To(BeFalse())
g.Expect(IsFalse(obj, "falseInfo1")).To(BeTrue())
g.Expect(IsFalse(obj, "unknown1")).To(BeFalse())
g.Expect(IsFalse(obj, "false1-negative-polarity")).To(BeTrue())
g.Expect(IsFalse(obj, "trueInfo1-negative-polarity")).To(BeFalse())
g.Expect(IsFalse(obj, "unknown1-negative-polarity")).To(BeFalse())

// test isUnknown
g.Expect(IsUnknown(obj, "nil1")).To(BeTrue())
g.Expect(IsUnknown(obj, "true1")).To(BeFalse())
g.Expect(IsUnknown(obj, "falseInfo1")).To(BeFalse())
g.Expect(IsUnknown(obj, "unknown1")).To(BeTrue())
g.Expect(IsUnknown(obj, "false1-negative-polarity")).To(BeFalse())
g.Expect(IsUnknown(obj, "trueInfo1-negative-polarity")).To(BeFalse())
g.Expect(IsUnknown(obj, "unknown1-negative-polarity")).To(BeTrue())

// test GetReason
g.Expect(GetReason(obj, "nil1")).To(Equal(""))
g.Expect(GetReason(obj, "falseInfo1")).To(Equal("reason falseInfo1"))
g.Expect(GetReason(obj, "trueInfo1-negative-polarity")).To(Equal("reason trueInfo1-negative-polarity"))

// test GetMessage
g.Expect(GetMessage(obj, "nil1")).To(Equal(""))
g.Expect(GetMessage(obj, "falseInfo1")).To(Equal("message falseInfo1"))
g.Expect(GetMessage(obj, "trueInfo1-negative-polarity")).To(Equal("message trueInfo1-negative-polarity"))

// test GetSeverity
expectedSeverity := clusterv1.ConditionSeverityInfo
g.Expect(GetSeverity(obj, "nil1")).To(BeNil())
severity := GetSeverity(obj, "falseInfo1")
expectedSeverity := clusterv1.ConditionSeverityInfo
g.Expect(severity).To(Equal(&expectedSeverity))
severity = GetSeverity(obj, "trueInfo1-negative-polarity")
g.Expect(severity).To(Equal(&expectedSeverity))

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

tests := []struct {
Expand All @@ -150,6 +173,18 @@ func TestSummary(t *testing.T) {
from: getterWithConditions(foo, bar),
want: FalseCondition(clusterv1.ReadyCondition, "reason falseInfo1", clusterv1.ConditionSeverityInfo, "message falseInfo1"),
},
{
name: "Returns ready condition with the summary of existing conditions with negative polarity (with default options)",
from: getterWithConditions(fooWithNegativePolarity, barWithNegativePolarity),
options: []MergeOption{WithNegativePolarityConditions("foo-negative-polarity", "bar-negative-polarity")},
want: FalseCondition(clusterv1.ReadyCondition, "reason trueInfo1-negative-polarity", clusterv1.ConditionSeverityInfo, "message trueInfo1-negative-polarity"),
},
{
name: "Returns ready condition with the summary of existing conditions with mixed polarity (with default options)",
from: getterWithConditions(foo, bar, fooWithNegativePolarity, barWithNegativePolarity),
options: []MergeOption{WithNegativePolarityConditions("foo-negative-polarity", "bar-negative-polarity")},
want: FalseCondition(clusterv1.ReadyCondition, "reason falseInfo1", clusterv1.ConditionSeverityInfo, "message falseInfo1"), // bar take precedence on barWithNegativePolarity because it is listed first
},
{
name: "Returns ready condition with the summary of existing conditions (using WithStepCounter options)",
from: getterWithConditions(foo, bar),
Expand Down Expand Up @@ -186,6 +221,18 @@ func TestSummary(t *testing.T) {
options: []MergeOption{WithConditions("foo")}, // bar should be ignored
want: TrueCondition(clusterv1.ReadyCondition),
},
{
name: "Returns ready condition with the summary of selected conditions with negative polarity (using WithConditions options)",
from: getterWithConditions(fooWithNegativePolarity, barWithNegativePolarity),
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
want: TrueCondition(clusterv1.ReadyCondition),
},
{
name: "Returns ready condition with the summary of selected conditions with mixed polarity (using WithConditions options)",
from: getterWithConditions(foo, bar, fooWithNegativePolarity, barWithNegativePolarity),
options: []MergeOption{WithConditions("foo", "foo-negative-polarity", "bar-negative-polarity"), WithNegativePolarityConditions("foo-negative-polarity", "bar-negative-polarity")},
want: FalseCondition(clusterv1.ReadyCondition, "reason trueInfo1-negative-polarity", clusterv1.ConditionSeverityInfo, "message trueInfo1-negative-polarity"),
},
{
name: "Returns ready condition with the summary of selected conditions (using WithConditions and WithStepCounter options)",
from: getterWithConditions(foo, bar, baz),
Expand Down
Loading
Loading