Skip to content

Commit 879617d

Browse files
authored
Merge pull request #11150 from fabriziopandini/v1beta2-conditions-patch-helper
🌱 Add support for v1beta2 conditions to patch helper
2 parents bd83261 + 7a55bc4 commit 879617d

File tree

9 files changed

+2681
-62
lines changed

9 files changed

+2681
-62
lines changed

internal/test/builder/v1beta2_transition.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,13 +223,16 @@ type Phase2ObjStatusDeprecatedV1Beta1 struct {
223223

224224
// GetConditions returns the set of conditions for this object.
225225
func (o *Phase2Obj) GetConditions() clusterv1.Conditions {
226+
if o.Status.Deprecated == nil || o.Status.Deprecated.V1Beta1 == nil {
227+
return nil
228+
}
226229
return o.Status.Deprecated.V1Beta1.Conditions
227230
}
228231

229232
// SetConditions sets the conditions on this object.
230233
func (o *Phase2Obj) SetConditions(conditions clusterv1.Conditions) {
231234
if o.Status.Deprecated == nil && conditions != nil {
232-
o.Status.Deprecated = &Phase2ObjStatusDeprecated{}
235+
o.Status.Deprecated = &Phase2ObjStatusDeprecated{V1Beta1: &Phase2ObjStatusDeprecatedV1Beta1{}}
233236
}
234237
if o.Status.Deprecated.V1Beta1 == nil && conditions != nil {
235238
o.Status.Deprecated.V1Beta1 = &Phase2ObjStatusDeprecatedV1Beta1{}

util/conditions/v1beta2/options.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,18 @@ type StepCounter bool
8686
func (t StepCounter) ApplyToSummary(opts *SummaryOptions) {
8787
opts.stepCounter = bool(t)
8888
}
89+
90+
// OwnedConditionTypes allows to define condition types owned by the controller when performing patch apply.
91+
// In case of conflicts for the owned conditions, the patch helper will always use the value provided by the controller.
92+
func OwnedConditionTypes(conditionTypes ...string) ApplyOption {
93+
return func(c *applyOptions) {
94+
c.ownedConditionTypes = conditionTypes
95+
}
96+
}
97+
98+
// ForceOverwrite instructs patch apply to always use the value provided by the controller (no matter of what value exists currently).
99+
func ForceOverwrite(v bool) ApplyOption {
100+
return func(c *applyOptions) {
101+
c.forceOverwrite = v
102+
}
103+
}

util/conditions/v1beta2/patch.go

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1beta2
18+
19+
import (
20+
"reflect"
21+
22+
"github.com/google/go-cmp/cmp"
23+
"github.com/pkg/errors"
24+
"k8s.io/apimachinery/pkg/api/meta"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
27+
"sigs.k8s.io/cluster-api/util"
28+
)
29+
30+
// Patch defines a list of operations to change a list of conditions into another.
31+
type Patch []PatchOperation
32+
33+
// PatchOperation defines an operation that changes a single condition.
34+
type PatchOperation struct {
35+
Before *metav1.Condition
36+
After *metav1.Condition
37+
Op PatchOperationType
38+
}
39+
40+
// PatchOperationType defines a condition patch operation type.
41+
type PatchOperationType string
42+
43+
const (
44+
// AddConditionPatch defines an add condition patch operation.
45+
AddConditionPatch PatchOperationType = "Add"
46+
47+
// ChangeConditionPatch defines an change condition patch operation.
48+
ChangeConditionPatch PatchOperationType = "Change"
49+
50+
// RemoveConditionPatch defines a remove condition patch operation.
51+
RemoveConditionPatch PatchOperationType = "Remove"
52+
)
53+
54+
// NewPatch returns the Patch required to align source conditions to after conditions.
55+
func NewPatch(before, after Getter) (Patch, error) {
56+
var patch Patch
57+
58+
if util.IsNil(before) {
59+
return nil, errors.New("error creating patch: before object is nil")
60+
}
61+
beforeConditions := before.GetV1Beta2Conditions()
62+
63+
if util.IsNil(after) {
64+
return nil, errors.New("error creating patch: after object is nil")
65+
}
66+
afterConditions := after.GetV1Beta2Conditions()
67+
68+
// Identify AddCondition and ModifyCondition changes.
69+
for i := range afterConditions {
70+
afterCondition := afterConditions[i]
71+
beforeCondition := meta.FindStatusCondition(beforeConditions, afterCondition.Type)
72+
if beforeCondition == nil {
73+
patch = append(patch, PatchOperation{Op: AddConditionPatch, After: &afterCondition})
74+
continue
75+
}
76+
77+
if !reflect.DeepEqual(&afterCondition, beforeCondition) {
78+
patch = append(patch, PatchOperation{Op: ChangeConditionPatch, After: &afterCondition, Before: beforeCondition})
79+
}
80+
}
81+
82+
// Identify RemoveCondition changes.
83+
for i := range beforeConditions {
84+
beforeCondition := beforeConditions[i]
85+
afterCondition := meta.FindStatusCondition(afterConditions, beforeCondition.Type)
86+
if afterCondition == nil {
87+
patch = append(patch, PatchOperation{Op: RemoveConditionPatch, Before: &beforeCondition})
88+
}
89+
}
90+
return patch, nil
91+
}
92+
93+
// applyOptions allows to set strategies for patch apply.
94+
type applyOptions struct {
95+
ownedConditionTypes []string
96+
forceOverwrite bool
97+
}
98+
99+
func (o *applyOptions) isOwnedConditionType(conditionType string) bool {
100+
for _, i := range o.ownedConditionTypes {
101+
if i == conditionType {
102+
return true
103+
}
104+
}
105+
return false
106+
}
107+
108+
// ApplyOption defines an option for applying a condition patch.
109+
type ApplyOption func(*applyOptions)
110+
111+
// Apply executes a three-way merge of a list of Patch.
112+
// When merge conflicts are detected (latest deviated from before in an incompatible way), an error is returned.
113+
func (p Patch) Apply(latest Setter, options ...ApplyOption) error {
114+
if p.IsZero() {
115+
return nil
116+
}
117+
118+
if util.IsNil(latest) {
119+
return errors.New("error patching conditions: latest object is nil")
120+
}
121+
latestConditions := latest.GetV1Beta2Conditions()
122+
123+
applyOpt := &applyOptions{}
124+
for _, o := range options {
125+
if util.IsNil(o) {
126+
return errors.New("error patching conditions: ApplyOption is nil")
127+
}
128+
o(applyOpt)
129+
}
130+
131+
for _, conditionPatch := range p {
132+
switch conditionPatch.Op {
133+
case AddConditionPatch:
134+
// If the condition is owned, always keep the after value.
135+
if applyOpt.forceOverwrite || applyOpt.isOwnedConditionType(conditionPatch.After.Type) {
136+
meta.SetStatusCondition(&latestConditions, *conditionPatch.After)
137+
continue
138+
}
139+
140+
// If the condition is already on latest, check if latest and after agree on the change; if not, this is a conflict.
141+
if latestCondition := meta.FindStatusCondition(latestConditions, conditionPatch.After.Type); latestCondition != nil {
142+
// If latest and after disagree on the change, then it is a conflict
143+
if !hasSameState(latestCondition, conditionPatch.After) {
144+
return errors.Errorf("error patching conditions: The condition %q was modified by a different process and this caused a merge/AddCondition conflict: %v", conditionPatch.After.Type, cmp.Diff(latestCondition, conditionPatch.After))
145+
}
146+
// otherwise, the latest is already as intended.
147+
// NOTE: We are preserving LastTransitionTime from the latest in order to avoid altering the existing value.
148+
continue
149+
}
150+
// If the condition does not exists on the latest, add the new after condition.
151+
meta.SetStatusCondition(&latestConditions, *conditionPatch.After)
152+
153+
case ChangeConditionPatch:
154+
// If the conditions is owned, always keep the after value.
155+
if applyOpt.forceOverwrite || applyOpt.isOwnedConditionType(conditionPatch.After.Type) {
156+
meta.SetStatusCondition(&latestConditions, *conditionPatch.After)
157+
continue
158+
}
159+
160+
latestCondition := meta.FindStatusCondition(latestConditions, conditionPatch.After.Type)
161+
162+
// If the condition does not exist anymore on the latest, this is a conflict.
163+
if latestCondition == nil {
164+
return errors.Errorf("error patching conditions: The condition %q was deleted by a different process and this caused a merge/ChangeCondition conflict", conditionPatch.After.Type)
165+
}
166+
167+
// If the condition on the latest is different from the base condition, check if
168+
// the after state corresponds to the desired value. If not this is a conflict (unless we should ignore conflicts for this condition type).
169+
if !reflect.DeepEqual(latestCondition, conditionPatch.Before) {
170+
if !hasSameState(latestCondition, conditionPatch.After) {
171+
return errors.Errorf("error patching conditions: The condition %q was modified by a different process and this caused a merge/ChangeCondition conflict: %v", conditionPatch.After.Type, cmp.Diff(latestCondition, conditionPatch.After))
172+
}
173+
// Otherwise the latest is already as intended.
174+
// NOTE: We are preserving LastTransitionTime from the latest in order to avoid altering the existing value.
175+
continue
176+
}
177+
// Otherwise apply the new after condition.
178+
meta.SetStatusCondition(&latestConditions, *conditionPatch.After)
179+
180+
case RemoveConditionPatch:
181+
// If latestConditions is nil or empty, nothing to remove.
182+
if len(latestConditions) == 0 {
183+
continue
184+
}
185+
186+
// If the conditions is owned, always keep the after value (condition should be deleted).
187+
if applyOpt.forceOverwrite || applyOpt.isOwnedConditionType(conditionPatch.Before.Type) {
188+
meta.RemoveStatusCondition(&latestConditions, conditionPatch.Before.Type)
189+
continue
190+
}
191+
192+
// If the condition is still on the latest, check if it is changed in the meantime;
193+
// if so then this is a conflict.
194+
if latestCondition := meta.FindStatusCondition(latestConditions, conditionPatch.Before.Type); latestCondition != nil {
195+
if !hasSameState(latestCondition, conditionPatch.Before) {
196+
return errors.Errorf("error patching conditions: The condition %q was modified by a different process and this caused a merge/RemoveCondition conflict: %v", conditionPatch.Before.Type, cmp.Diff(latestCondition, conditionPatch.Before))
197+
}
198+
}
199+
// Otherwise the latest and after agreed on the delete operation, so there's nothing to change.
200+
meta.RemoveStatusCondition(&latestConditions, conditionPatch.Before.Type)
201+
}
202+
}
203+
204+
latest.SetV1Beta2Conditions(latestConditions)
205+
return nil
206+
}
207+
208+
// IsZero returns true if the patch is nil or has no changes.
209+
func (p Patch) IsZero() bool {
210+
if p == nil {
211+
return true
212+
}
213+
return len(p) == 0
214+
}
215+
216+
// hasSameState returns true if a condition has the same state of another; state is defined
217+
// by the union of following fields: Type, Status, Reason, ObservedGeneration and Message (it excludes LastTransitionTime).
218+
func hasSameState(i, j *metav1.Condition) bool {
219+
return i.Type == j.Type &&
220+
i.Status == j.Status &&
221+
i.ObservedGeneration == j.ObservedGeneration &&
222+
i.Reason == j.Reason &&
223+
i.Message == j.Message
224+
}

0 commit comments

Comments
 (0)