sigs.k8s.io/cluster-api@v1.7.1/util/conditions/patch.go (about) 1 /* 2 Copyright 2020 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 conditions 18 19 import ( 20 "reflect" 21 22 "github.com/google/go-cmp/cmp" 23 "github.com/pkg/errors" 24 25 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 26 "sigs.k8s.io/cluster-api/util" 27 ) 28 29 // Patch defines a list of operations to change a list of conditions into another. 30 type Patch []PatchOperation 31 32 // PatchOperation define an operation that changes a single condition. 33 type PatchOperation struct { 34 Before *clusterv1.Condition 35 After *clusterv1.Condition 36 Op PatchOperationType 37 } 38 39 // PatchOperationType defines patch operation types. 40 type PatchOperationType string 41 42 const ( 43 // AddConditionPatch defines an add condition patch operation. 44 AddConditionPatch PatchOperationType = "Add" 45 46 // ChangeConditionPatch defines an change condition patch operation. 47 ChangeConditionPatch PatchOperationType = "Change" 48 49 // RemoveConditionPatch defines a remove condition patch operation. 50 RemoveConditionPatch PatchOperationType = "Remove" 51 ) 52 53 // NewPatch returns the Patch required to align source conditions to after conditions. 54 func NewPatch(before Getter, after Getter) (Patch, error) { 55 var patch Patch 56 57 if util.IsNil(before) { 58 return nil, errors.New("error creating patch: before object is nil") 59 } 60 if util.IsNil(after) { 61 return nil, errors.New("error creating patch: after object is nil") 62 } 63 64 // Identify AddCondition and ModifyCondition changes. 65 targetConditions := after.GetConditions() 66 for i := range targetConditions { 67 targetCondition := targetConditions[i] 68 currentCondition := Get(before, targetCondition.Type) 69 if currentCondition == nil { 70 patch = append(patch, PatchOperation{Op: AddConditionPatch, After: &targetCondition}) 71 continue 72 } 73 74 if !reflect.DeepEqual(&targetCondition, currentCondition) { 75 patch = append(patch, PatchOperation{Op: ChangeConditionPatch, After: &targetCondition, Before: currentCondition}) 76 } 77 } 78 79 // Identify RemoveCondition changes. 80 baseConditions := before.GetConditions() 81 for i := range baseConditions { 82 baseCondition := baseConditions[i] 83 targetCondition := Get(after, baseCondition.Type) 84 if targetCondition == nil { 85 patch = append(patch, PatchOperation{Op: RemoveConditionPatch, Before: &baseCondition}) 86 } 87 } 88 return patch, nil 89 } 90 91 // applyOptions allows to set strategies for patch apply. 92 type applyOptions struct { 93 ownedConditions []clusterv1.ConditionType 94 forceOverwrite bool 95 } 96 97 func (o *applyOptions) isOwnedCondition(t clusterv1.ConditionType) bool { 98 for _, i := range o.ownedConditions { 99 if i == t { 100 return true 101 } 102 } 103 return false 104 } 105 106 // ApplyOption defines an option for applying a condition patch. 107 type ApplyOption func(*applyOptions) 108 109 // WithOwnedConditions allows to define condition types owned by the controller. 110 // In case of conflicts for the owned conditions, the patch helper will always use the value provided by the controller. 111 func WithOwnedConditions(t ...clusterv1.ConditionType) ApplyOption { 112 return func(c *applyOptions) { 113 c.ownedConditions = t 114 } 115 } 116 117 // WithForceOverwrite In case of conflicts for the owned conditions, the patch helper will always use the value provided by the controller. 118 func WithForceOverwrite(v bool) ApplyOption { 119 return func(c *applyOptions) { 120 c.forceOverwrite = v 121 } 122 } 123 124 // Apply executes a three-way merge of a list of Patch. 125 // When merge conflicts are detected (latest deviated from before in an incompatible way), an error is returned. 126 func (p Patch) Apply(latest Setter, options ...ApplyOption) error { 127 if p.IsZero() { 128 return nil 129 } 130 131 if util.IsNil(latest) { 132 return errors.New("error patching conditions: latest object was nil") 133 } 134 135 applyOpt := &applyOptions{} 136 for _, o := range options { 137 if util.IsNil(o) { 138 return errors.New("error patching conditions: ApplyOption was nil") 139 } 140 o(applyOpt) 141 } 142 143 for _, conditionPatch := range p { 144 switch conditionPatch.Op { 145 case AddConditionPatch: 146 // If the conditions is owned, always keep the after value. 147 if applyOpt.forceOverwrite || applyOpt.isOwnedCondition(conditionPatch.After.Type) { 148 Set(latest, conditionPatch.After) 149 continue 150 } 151 152 // If the condition is already on latest, check if latest and after agree on the change; if not, this is a conflict. 153 if latestCondition := Get(latest, conditionPatch.After.Type); latestCondition != nil { 154 // If latest and after agree on the change, then it is a conflict. 155 if !hasSameState(latestCondition, conditionPatch.After) { 156 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)) 157 } 158 // otherwise, the latest is already as intended. 159 // NOTE: We are preserving LastTransitionTime from the latest in order to avoid altering the existing value. 160 continue 161 } 162 // If the condition does not exists on the latest, add the new after condition. 163 Set(latest, conditionPatch.After) 164 165 case ChangeConditionPatch: 166 // If the conditions is owned, always keep the after value. 167 if applyOpt.forceOverwrite || applyOpt.isOwnedCondition(conditionPatch.After.Type) { 168 Set(latest, conditionPatch.After) 169 continue 170 } 171 172 latestCondition := Get(latest, conditionPatch.After.Type) 173 174 // If the condition does not exist anymore on the latest, this is a conflict. 175 if latestCondition == nil { 176 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) 177 } 178 179 // If the condition on the latest is different from the base condition, check if 180 // the after state corresponds to the desired value. If not this is a conflict (unless we should ignore conflicts for this condition type). 181 if !reflect.DeepEqual(latestCondition, conditionPatch.Before) { 182 if !hasSameState(latestCondition, conditionPatch.After) { 183 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)) 184 } 185 // Otherwise the latest is already as intended. 186 // NOTE: We are preserving LastTransitionTime from the latest in order to avoid altering the existing value. 187 continue 188 } 189 // Otherwise apply the new after condition. 190 Set(latest, conditionPatch.After) 191 192 case RemoveConditionPatch: 193 // If the conditions is owned, always keep the after value (condition should be deleted). 194 if applyOpt.forceOverwrite || applyOpt.isOwnedCondition(conditionPatch.Before.Type) { 195 Delete(latest, conditionPatch.Before.Type) 196 continue 197 } 198 199 // If the condition is still on the latest, check if it is changed in the meantime; 200 // if so then this is a conflict. 201 if latestCondition := Get(latest, conditionPatch.Before.Type); latestCondition != nil { 202 if !hasSameState(latestCondition, conditionPatch.Before) { 203 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)) 204 } 205 } 206 // Otherwise the latest and after agreed on the delete operation, so there's nothing to change. 207 Delete(latest, conditionPatch.Before.Type) 208 } 209 } 210 return nil 211 } 212 213 // IsZero returns true if the patch is nil or has no changes. 214 func (p Patch) IsZero() bool { 215 if p == nil { 216 return true 217 } 218 return len(p) == 0 219 }