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  }