-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Make patch helper support v1beta2 conditions
- Loading branch information
1 parent
3217411
commit f821534
Showing
7 changed files
with
2,524 additions
and
55 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,248 @@ | ||
/* | ||
Copyright 2024 The Kubernetes Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package v1beta2 | ||
|
||
import ( | ||
"reflect" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
"github.com/pkg/errors" | ||
"k8s.io/apimachinery/pkg/api/meta" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
|
||
"sigs.k8s.io/cluster-api/util" | ||
) | ||
|
||
// Patch defines a list of operations to change a list of conditions into another. | ||
type Patch []PatchOperation | ||
|
||
// PatchOperation define an operation that changes a single condition. | ||
type PatchOperation struct { | ||
Before *metav1.Condition | ||
After *metav1.Condition | ||
Op PatchOperationType | ||
} | ||
|
||
// PatchOperationType defines patch operation types. | ||
type PatchOperationType string | ||
|
||
const ( | ||
// AddConditionPatch defines an add condition patch operation. | ||
AddConditionPatch PatchOperationType = "Add" | ||
|
||
// ChangeConditionPatch defines an change condition patch operation. | ||
ChangeConditionPatch PatchOperationType = "Change" | ||
|
||
// RemoveConditionPatch defines a remove condition patch operation. | ||
RemoveConditionPatch PatchOperationType = "Remove" | ||
) | ||
|
||
// NewPatch returns the Patch required to align source conditions to after conditions. | ||
func NewPatch(before, after runtime.Object) (Patch, error) { | ||
var patch Patch | ||
|
||
if util.IsNil(before) { | ||
return nil, errors.New("error creating patch: before object is nil") | ||
} | ||
beforeConditions, err := GetAll(before) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "error creating patch: failed to get conditions from before object") | ||
} | ||
|
||
if util.IsNil(after) { | ||
return nil, errors.New("error creating patch: after object is nil") | ||
} | ||
afterConditions, err := GetAll(after) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "error creating patch: failed to get conditions from after object") | ||
} | ||
|
||
// Identify AddCondition and ModifyCondition changes. | ||
for i := range afterConditions { | ||
afterCondition := afterConditions[i] | ||
beforeCondition := meta.FindStatusCondition(beforeConditions, afterCondition.Type) | ||
if beforeCondition == nil { | ||
patch = append(patch, PatchOperation{Op: AddConditionPatch, After: &afterCondition}) | ||
continue | ||
} | ||
|
||
if !reflect.DeepEqual(&afterCondition, beforeCondition) { | ||
patch = append(patch, PatchOperation{Op: ChangeConditionPatch, After: &afterCondition, Before: beforeCondition}) | ||
} | ||
} | ||
|
||
// Identify RemoveCondition changes. | ||
for i := range beforeConditions { | ||
beforeCondition := beforeConditions[i] | ||
afterCondition := meta.FindStatusCondition(afterConditions, beforeCondition.Type) | ||
if afterCondition == nil { | ||
patch = append(patch, PatchOperation{Op: RemoveConditionPatch, Before: &beforeCondition}) | ||
} | ||
} | ||
return patch, nil | ||
} | ||
|
||
// applyOptions allows to set strategies for patch apply. | ||
type applyOptions struct { | ||
ownedConditions []string | ||
forceOverwrite bool | ||
} | ||
|
||
func (o *applyOptions) isOwnedCondition(t string) bool { | ||
for _, i := range o.ownedConditions { | ||
if i == t { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
// ApplyOption defines an option for applying a condition patch. | ||
type ApplyOption func(*applyOptions) | ||
|
||
// WithOwnedConditions allows to define condition types owned by the controller. | ||
// In case of conflicts for the owned conditions, the patch helper will always use the value provided by the controller. | ||
func WithOwnedConditions(t ...string) ApplyOption { | ||
return func(c *applyOptions) { | ||
c.ownedConditions = t | ||
} | ||
} | ||
|
||
// WithForceOverwrite In case of conflicts for the owned conditions, the patch helper will always use the value provided by the controller. | ||
func WithForceOverwrite(v bool) ApplyOption { | ||
return func(c *applyOptions) { | ||
c.forceOverwrite = v | ||
} | ||
} | ||
|
||
// Apply executes a three-way merge of a list of Patch. | ||
// When merge conflicts are detected (latest deviated from before in an incompatible way), an error is returned. | ||
func (p Patch) Apply(latest runtime.Object, options ...ApplyOption) error { | ||
if p.IsZero() { | ||
return nil | ||
} | ||
|
||
if util.IsNil(latest) { | ||
return errors.New("error patching conditions: latest object is nil") | ||
} | ||
latestConditions, err := GetAll(latest) | ||
if err != nil { | ||
return errors.Wrap(err, "error creating patch: failed to get conditions from latest object") | ||
} | ||
|
||
applyOpt := &applyOptions{} | ||
for _, o := range options { | ||
if util.IsNil(o) { | ||
return errors.New("error patching conditions: ApplyOption is nil") | ||
} | ||
o(applyOpt) | ||
} | ||
|
||
for _, conditionPatch := range p { | ||
switch conditionPatch.Op { | ||
case AddConditionPatch: | ||
// If the conditions is owned, always keep the after value. | ||
if applyOpt.forceOverwrite || applyOpt.isOwnedCondition(conditionPatch.After.Type) { | ||
meta.SetStatusCondition(&latestConditions, *conditionPatch.After) | ||
continue | ||
} | ||
|
||
// If the condition is already on latest, check if latest and after agree on the change; if not, this is a conflict. | ||
if latestCondition := meta.FindStatusCondition(latestConditions, conditionPatch.After.Type); latestCondition != nil { | ||
// If latest and after agree on the change, then it is a conflict. | ||
if !hasSameState(latestCondition, conditionPatch.After) { | ||
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)) | ||
} | ||
// otherwise, the latest is already as intended. | ||
// NOTE: We are preserving LastTransitionTime from the latest in order to avoid altering the existing value. | ||
continue | ||
} | ||
// If the condition does not exists on the latest, add the new after condition. | ||
meta.SetStatusCondition(&latestConditions, *conditionPatch.After) | ||
|
||
case ChangeConditionPatch: | ||
// If the conditions is owned, always keep the after value. | ||
if applyOpt.forceOverwrite || applyOpt.isOwnedCondition(conditionPatch.After.Type) { | ||
meta.SetStatusCondition(&latestConditions, *conditionPatch.After) | ||
continue | ||
} | ||
|
||
latestCondition := meta.FindStatusCondition(latestConditions, conditionPatch.After.Type) | ||
|
||
// If the condition does not exist anymore on the latest, this is a conflict. | ||
if latestCondition == nil { | ||
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) | ||
} | ||
|
||
// If the condition on the latest is different from the base condition, check if | ||
// the after state corresponds to the desired value. If not this is a conflict (unless we should ignore conflicts for this condition type). | ||
if !reflect.DeepEqual(latestCondition, conditionPatch.Before) { | ||
if !hasSameState(latestCondition, conditionPatch.After) { | ||
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)) | ||
} | ||
// Otherwise the latest is already as intended. | ||
// NOTE: We are preserving LastTransitionTime from the latest in order to avoid altering the existing value. | ||
continue | ||
} | ||
// Otherwise apply the new after condition. | ||
meta.SetStatusCondition(&latestConditions, *conditionPatch.After) | ||
|
||
case RemoveConditionPatch: | ||
// If latestConditions is nil or empty, nothing to remove. | ||
if len(latestConditions) == 0 { | ||
continue | ||
} | ||
|
||
// If the conditions is owned, always keep the after value (condition should be deleted). | ||
if applyOpt.forceOverwrite || applyOpt.isOwnedCondition(conditionPatch.Before.Type) { | ||
meta.RemoveStatusCondition(&latestConditions, conditionPatch.Before.Type) | ||
continue | ||
} | ||
|
||
// If the condition is still on the latest, check if it is changed in the meantime; | ||
// if so then this is a conflict. | ||
if latestCondition := meta.FindStatusCondition(latestConditions, conditionPatch.Before.Type); latestCondition != nil { | ||
if !hasSameState(latestCondition, conditionPatch.Before) { | ||
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)) | ||
} | ||
} | ||
// Otherwise the latest and after agreed on the delete operation, so there's nothing to change. | ||
meta.RemoveStatusCondition(&latestConditions, conditionPatch.Before.Type) | ||
} | ||
} | ||
|
||
return SetAll(latest, latestConditions) | ||
} | ||
|
||
// IsZero returns true if the patch is nil or has no changes. | ||
func (p Patch) IsZero() bool { | ||
if p == nil { | ||
return true | ||
} | ||
return len(p) == 0 | ||
} | ||
|
||
// hasSameState returns true if a condition has the same state of another; state is defined | ||
// by the union of following fields: Type, Status, Reason, ObservedGeneration and Message (it excludes LastTransitionTime). | ||
func hasSameState(i, j *metav1.Condition) bool { | ||
return i.Type == j.Type && | ||
i.Status == j.Status && | ||
i.ObservedGeneration == j.ObservedGeneration && | ||
i.Reason == j.Reason && | ||
i.Message == j.Message | ||
} |
Oops, something went wrong.