...

Source file src/sigs.k8s.io/kustomize/kyaml/kio/filters/merge3.go

Documentation: sigs.k8s.io/kustomize/kyaml/kio/filters

     1  // Copyright 2019 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package filters
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"sigs.k8s.io/kustomize/kyaml/kio"
    10  	"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
    11  	"sigs.k8s.io/kustomize/kyaml/yaml"
    12  	"sigs.k8s.io/kustomize/kyaml/yaml/merge3"
    13  )
    14  
    15  const (
    16  	mergeSourceAnnotation = "config.kubernetes.io/merge-source"
    17  	mergeSourceOriginal   = "original"
    18  	mergeSourceUpdated    = "updated"
    19  	mergeSourceDest       = "dest"
    20  )
    21  
    22  // ResourceMatcher interface is used to match two resources based on IsSameResource implementation
    23  // This is the way to group same logical resources in upstream, local and origin for merge
    24  // The default way to group them is using GVKNN similar to how kubernetes server identifies resources
    25  // Users of this library might have their own interpretation of grouping similar resources
    26  // for e.g. if consumer adds a name-prefix to local resource, it should not be treated as new resource
    27  // for updates etc.
    28  // Hence, the callers of this library may pass different implementation for IsSameResource
    29  type ResourceMatcher interface {
    30  	IsSameResource(node1, node2 *yaml.RNode) bool
    31  }
    32  
    33  // ResourceMergeStrategy is the return type from the Handle function in the
    34  // ResourceHandler interface. It determines which version of a resource should
    35  // be included in the output (if any).
    36  type ResourceMergeStrategy int
    37  
    38  const (
    39  	// Merge means the output to dest should be the 3-way merge of original,
    40  	// updated and dest.
    41  	Merge ResourceMergeStrategy = iota
    42  	// KeepDest means the version of the resource in dest should be the output.
    43  	KeepDest
    44  	// KeepUpdated means the version of the resource in updated should be the
    45  	// output.
    46  	KeepUpdated
    47  	// KeepOriginal means the version of the resource in original should be the
    48  	// output.
    49  	KeepOriginal
    50  	// Skip means the resource should not be included in the output.
    51  	Skip
    52  )
    53  
    54  // ResourceHandler interface is used to determine what should be done for a
    55  // resource once the versions in original, updated and dest has been
    56  // identified based on the ResourceMatcher. This allows users to customize
    57  // what should be the result in dest if a resource has been deleted from
    58  // upstream.
    59  type ResourceHandler interface {
    60  	Handle(original, updated, dest *yaml.RNode) (ResourceMergeStrategy, error)
    61  }
    62  
    63  // Merge3 performs a 3-way merge on the original, updated, and destination packages.
    64  type Merge3 struct {
    65  	OriginalPath   string
    66  	UpdatedPath    string
    67  	DestPath       string
    68  	MatchFilesGlob []string
    69  	Matcher        ResourceMatcher
    70  	Handler        ResourceHandler
    71  }
    72  
    73  func (m Merge3) Merge() error {
    74  	// Read the destination package.  The ReadWriter will take take of deleting files
    75  	// for removed resources.
    76  	var inputs []kio.Reader
    77  	dest := &kio.LocalPackageReadWriter{
    78  		PackagePath:    m.DestPath,
    79  		MatchFilesGlob: m.MatchFilesGlob,
    80  		SetAnnotations: map[string]string{mergeSourceAnnotation: mergeSourceDest},
    81  	}
    82  	inputs = append(inputs, dest)
    83  
    84  	// Read the original package
    85  	inputs = append(inputs, kio.LocalPackageReader{
    86  		PackagePath:    m.OriginalPath,
    87  		MatchFilesGlob: m.MatchFilesGlob,
    88  		SetAnnotations: map[string]string{mergeSourceAnnotation: mergeSourceOriginal},
    89  	})
    90  
    91  	// Read the updated package
    92  	inputs = append(inputs, kio.LocalPackageReader{
    93  		PackagePath:    m.UpdatedPath,
    94  		MatchFilesGlob: m.MatchFilesGlob,
    95  		SetAnnotations: map[string]string{mergeSourceAnnotation: mergeSourceUpdated},
    96  	})
    97  
    98  	return kio.Pipeline{
    99  		Inputs:  inputs,
   100  		Filters: []kio.Filter{m},
   101  		Outputs: []kio.Writer{dest},
   102  	}.Execute()
   103  }
   104  
   105  // Filter combines Resources with the same GVK + N + NS into tuples, and then merges them
   106  func (m Merge3) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
   107  	// index the nodes by their identity
   108  	matcher := m.Matcher
   109  	if matcher == nil {
   110  		matcher = &DefaultGVKNNMatcher{MergeOnPath: true}
   111  	}
   112  	handler := m.Handler
   113  	if handler == nil {
   114  		handler = &DefaultResourceHandler{}
   115  	}
   116  
   117  	tl := tuples{matcher: matcher}
   118  	for i := range nodes {
   119  		if err := tl.add(nodes[i]); err != nil {
   120  			return nil, err
   121  		}
   122  	}
   123  
   124  	// iterate over the inputs, merging as needed
   125  	var output []*yaml.RNode
   126  	for i := range tl.list {
   127  		t := tl.list[i]
   128  		strategy, err := handler.Handle(t.original, t.updated, t.dest)
   129  		if err != nil {
   130  			return nil, err
   131  		}
   132  		switch strategy {
   133  		case Merge:
   134  			node, err := t.merge()
   135  			if err != nil {
   136  				return nil, err
   137  			}
   138  			if node != nil {
   139  				output = append(output, node)
   140  			}
   141  		case KeepDest:
   142  			output = append(output, t.dest)
   143  		case KeepUpdated:
   144  			output = append(output, t.updated)
   145  		case KeepOriginal:
   146  			output = append(output, t.original)
   147  		case Skip:
   148  			// do nothing
   149  		}
   150  	}
   151  	return output, nil
   152  }
   153  
   154  // tuples combines nodes with the same GVK + N + NS
   155  type tuples struct {
   156  	list []*tuple
   157  
   158  	// matcher matches the resources for merge
   159  	matcher ResourceMatcher
   160  }
   161  
   162  // DefaultGVKNNMatcher holds the default matching of resources implementation based on
   163  // Group, Version, Kind, Name and Namespace of the resource
   164  type DefaultGVKNNMatcher struct {
   165  	// MergeOnPath will use the relative filepath as part of the merge key.
   166  	// This may be necessary if the directory contains multiple copies of
   167  	// the same resource, or resources patches.
   168  	MergeOnPath bool
   169  }
   170  
   171  // IsSameResource returns true if metadata of node1 and metadata of node2 belongs to same logical resource
   172  func (dm *DefaultGVKNNMatcher) IsSameResource(node1, node2 *yaml.RNode) bool {
   173  	if node1 == nil || node2 == nil {
   174  		return false
   175  	}
   176  	if err := kioutil.CopyLegacyAnnotations(node1); err != nil {
   177  		return false
   178  	}
   179  	if err := kioutil.CopyLegacyAnnotations(node2); err != nil {
   180  		return false
   181  	}
   182  
   183  	meta1, err := node1.GetMeta()
   184  	if err != nil {
   185  		return false
   186  	}
   187  
   188  	meta2, err := node2.GetMeta()
   189  	if err != nil {
   190  		return false
   191  	}
   192  
   193  	if meta1.Name != meta2.Name {
   194  		return false
   195  	}
   196  	if meta1.Namespace != meta2.Namespace {
   197  		return false
   198  	}
   199  	if meta1.APIVersion != meta2.APIVersion {
   200  		return false
   201  	}
   202  	if meta1.Kind != meta2.Kind {
   203  		return false
   204  	}
   205  	if dm.MergeOnPath {
   206  		// directories may contain multiple copies of a resource with the same
   207  		// name, namespace, apiVersion and kind -- e.g. kustomize patches, or
   208  		// multiple environments
   209  		// mergeOnPath configures the merge logic to use the path as part of the
   210  		// resource key
   211  		if meta1.Annotations[kioutil.PathAnnotation] != meta2.Annotations[kioutil.PathAnnotation] {
   212  			return false
   213  		}
   214  	}
   215  	return true
   216  }
   217  
   218  // add adds a node to the list, combining it with an existing matching Resource if found
   219  func (ts *tuples) add(node *yaml.RNode) error {
   220  	for i := range ts.list {
   221  		t := ts.list[i]
   222  		if ts.matcher.IsSameResource(addedNode(t), node) {
   223  			return t.add(node)
   224  		}
   225  	}
   226  	t := &tuple{}
   227  	if err := t.add(node); err != nil {
   228  		return err
   229  	}
   230  	ts.list = append(ts.list, t)
   231  	return nil
   232  }
   233  
   234  // addedNode returns one on the existing added nodes in the tuple
   235  func addedNode(t *tuple) *yaml.RNode {
   236  	if t.updated != nil {
   237  		return t.updated
   238  	}
   239  	if t.original != nil {
   240  		return t.original
   241  	}
   242  	return t.dest
   243  }
   244  
   245  // tuple wraps an original, updated, and dest tuple for a given Resource
   246  type tuple struct {
   247  	original *yaml.RNode
   248  	updated  *yaml.RNode
   249  	dest     *yaml.RNode
   250  }
   251  
   252  // add sets the corresponding tuple field for the node
   253  func (t *tuple) add(node *yaml.RNode) error {
   254  	meta, err := node.GetMeta()
   255  	if err != nil {
   256  		return err
   257  	}
   258  	switch meta.Annotations[mergeSourceAnnotation] {
   259  	case mergeSourceDest:
   260  		if t.dest != nil {
   261  			return duplicateError("local", meta.Annotations[kioutil.PathAnnotation])
   262  		}
   263  		t.dest = node
   264  	case mergeSourceOriginal:
   265  		if t.original != nil {
   266  			return duplicateError("original upstream", meta.Annotations[kioutil.PathAnnotation])
   267  		}
   268  		t.original = node
   269  	case mergeSourceUpdated:
   270  		if t.updated != nil {
   271  			return duplicateError("updated upstream", meta.Annotations[kioutil.PathAnnotation])
   272  		}
   273  		t.updated = node
   274  	default:
   275  		return fmt.Errorf("no source annotation for Resource")
   276  	}
   277  	return nil
   278  }
   279  
   280  // merge performs a 3-way merge on the tuple
   281  func (t *tuple) merge() (*yaml.RNode, error) {
   282  	return merge3.Merge(t.dest, t.original, t.updated)
   283  }
   284  
   285  // duplicateError returns duplicate resources error
   286  func duplicateError(source, filePath string) error {
   287  	return fmt.Errorf(`found duplicate %q resources in file %q, please refer to "update" documentation for the fix`, source, filePath)
   288  }
   289  
   290  // DefaultResourceHandler is the default implementation of the ResourceHandler
   291  // interface. It uses the following rules:
   292  // * Keep dest if resource only exists in dest.
   293  // * Keep updated if resource added in updated.
   294  // * Delete dest if updated has been deleted.
   295  // * Don't add the resource back if removed from dest.
   296  // * Otherwise merge.
   297  type DefaultResourceHandler struct{}
   298  
   299  func (*DefaultResourceHandler) Handle(original, updated, dest *yaml.RNode) (ResourceMergeStrategy, error) {
   300  	switch {
   301  	case original == nil && updated == nil && dest != nil:
   302  		// added locally -- keep dest
   303  		return KeepDest, nil
   304  	case updated != nil && dest == nil:
   305  		// added in the update -- add update
   306  		return KeepUpdated, nil
   307  	case original != nil && updated == nil:
   308  		// deleted in the update
   309  		return Skip, nil
   310  	case original != nil && dest == nil:
   311  		// deleted locally
   312  		return Skip, nil
   313  	default:
   314  		// dest and updated are non-nil -- merge them
   315  		return Merge, nil
   316  	}
   317  }
   318  

View as plain text