...

Source file src/edge-infra.dev/pkg/k8s/runtime/conditions/patch.go

Documentation: edge-infra.dev/pkg/k8s/runtime/conditions

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

View as plain text