...

Source file src/sigs.k8s.io/kustomize/kyaml/yaml/match.go

Documentation: sigs.k8s.io/kustomize/kyaml/yaml

     1  // Copyright 2019 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package yaml
     5  
     6  import (
     7  	"fmt"
     8  	"regexp"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"sigs.k8s.io/kustomize/kyaml/errors"
    13  	yaml "sigs.k8s.io/yaml/goyaml.v3"
    14  )
    15  
    16  // PathMatcher returns all RNodes matching the path wrapped in a SequenceNode.
    17  // Lists may have multiple elements matching the path, and each matching element
    18  // is added to the return result.
    19  // If Path points to a SequenceNode, the SequenceNode is wrapped in another SequenceNode
    20  // If Path does not contain any lists, the result is still wrapped in a SequenceNode of len == 1
    21  type PathMatcher struct {
    22  	Kind string `yaml:"kind,omitempty"`
    23  
    24  	// Path is a slice of parts leading to the RNode to lookup.
    25  	// Each path part may be one of:
    26  	// * FieldMatcher -- e.g. "spec"
    27  	// * Map Key -- e.g. "app.k8s.io/version"
    28  	// * List Entry -- e.g. "[name=nginx]" or "[=-jar]" or "0"
    29  	//
    30  	// Map Keys and Fields are equivalent.
    31  	// See FieldMatcher for more on Fields and Map Keys.
    32  	//
    33  	// List Entries are specified as map entry to match [fieldName=fieldValue].
    34  	// See Elem for more on List Entries.
    35  	//
    36  	// Examples:
    37  	// * spec.template.spec.container with matching name: [name=nginx] -- match 'name': 'nginx'
    38  	// * spec.template.spec.container.argument matching a value: [=-jar] -- match '-jar'
    39  	Path []string `yaml:"path,omitempty"`
    40  
    41  	// Matches is set by PathMatch to publish the matched element values for each node.
    42  	// After running  PathMatcher.Filter, each node from the SequenceNode result may be
    43  	// looked up in Matches to find the field values that were matched.
    44  	Matches map[*Node][]string
    45  
    46  	// StripComments may be set to remove the comments on the matching Nodes.
    47  	// This is useful for if the nodes are to be printed in FlowStyle.
    48  	StripComments bool
    49  
    50  	// Create will cause missing path parts to be created as they are walked.
    51  	//
    52  	// * The leaf Node (final path) will be created with a Kind matching Create
    53  	// * Intermediary Nodes will be created as either a MappingNodes or
    54  	//   SequenceNodes as appropriate for each's Path location.
    55  	// * Nodes identified by an index will only be created if the index indicates
    56  	//   an append operation (i.e. index=len(list))
    57  	Create yaml.Kind `yaml:"create,omitempty"`
    58  
    59  	val        *RNode
    60  	field      string
    61  	matchRegex string
    62  }
    63  
    64  func (p *PathMatcher) stripComments(n *Node) {
    65  	if n == nil {
    66  		return
    67  	}
    68  	if p.StripComments {
    69  		n.LineComment = ""
    70  		n.HeadComment = ""
    71  		n.FootComment = ""
    72  		for i := range n.Content {
    73  			p.stripComments(n.Content[i])
    74  		}
    75  	}
    76  }
    77  
    78  func (p *PathMatcher) Filter(rn *RNode) (*RNode, error) {
    79  	val, err := p.filter(rn)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	p.stripComments(val.YNode())
    84  	return val, err
    85  }
    86  
    87  func (p *PathMatcher) filter(rn *RNode) (*RNode, error) {
    88  	p.Matches = map[*Node][]string{}
    89  
    90  	if len(p.Path) == 0 {
    91  		// return the element wrapped in a SequenceNode
    92  		p.appendRNode("", rn)
    93  		return p.val, nil
    94  	}
    95  
    96  	if IsIdxNumber(p.Path[0]) {
    97  		return p.doIndexSeq(rn)
    98  	}
    99  
   100  	if IsListIndex(p.Path[0]) {
   101  		// match seq elements
   102  		return p.doSeq(rn)
   103  	}
   104  
   105  	if IsWildcard(p.Path[0]) {
   106  		// match every elements (*)
   107  		return p.doMatchEvery(rn)
   108  	}
   109  	// match a field
   110  	return p.doField(rn)
   111  }
   112  
   113  func (p *PathMatcher) doMatchEvery(rn *RNode) (*RNode, error) {
   114  	if err := rn.VisitElements(p.visitEveryElem); err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	return p.val, nil
   119  }
   120  
   121  func (p *PathMatcher) visitEveryElem(elem *RNode) error {
   122  	fieldName := p.Path[0]
   123  	// recurse on the matching element
   124  	pm := &PathMatcher{Path: p.Path[1:], Create: p.Create}
   125  	add, err := pm.filter(elem)
   126  	for k, v := range pm.Matches {
   127  		p.Matches[k] = v
   128  	}
   129  	if err != nil || add == nil {
   130  		return err
   131  	}
   132  	p.append(fieldName, add.Content()...)
   133  
   134  	return nil
   135  }
   136  
   137  func (p *PathMatcher) doField(rn *RNode) (*RNode, error) {
   138  	// lookup the field
   139  	field, err := rn.Pipe(Get(p.Path[0]))
   140  	if err != nil || (!IsCreate(p.Create) && field == nil) {
   141  		return nil, err
   142  	}
   143  
   144  	if IsCreate(p.Create) && field == nil {
   145  		var nextPart string
   146  		if len(p.Path) > 1 {
   147  			nextPart = p.Path[1]
   148  		}
   149  		nextPartKind := getPathPartKind(nextPart, p.Create)
   150  		field = &RNode{value: &yaml.Node{Kind: nextPartKind}}
   151  		err := rn.PipeE(SetField(p.Path[0], field))
   152  		if err != nil {
   153  			return nil, err
   154  		}
   155  	}
   156  
   157  	// recurse on the field, removing the first element of the path
   158  	pm := &PathMatcher{Path: p.Path[1:], Create: p.Create}
   159  	p.val, err = pm.filter(field)
   160  	p.Matches = pm.Matches
   161  	return p.val, err
   162  }
   163  
   164  // doIndexSeq iterates over a sequence and appends elements matching the index p.Val
   165  func (p *PathMatcher) doIndexSeq(rn *RNode) (*RNode, error) {
   166  	// parse to index number
   167  	idx, err := strconv.Atoi(p.Path[0])
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	elements, err := rn.Elements()
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  
   177  	if len(elements) == idx && IsCreate(p.Create) {
   178  		var nextPart string
   179  		if len(p.Path) > 1 {
   180  			nextPart = p.Path[1]
   181  		}
   182  		elem := &yaml.Node{Kind: getPathPartKind(nextPart, p.Create)}
   183  		err = rn.PipeE(Append(elem))
   184  		if err != nil {
   185  			return nil, errors.WrapPrefixf(err, "failed to append element for %q", p.Path[0])
   186  		}
   187  		elements = append(elements, NewRNode(elem))
   188  	}
   189  
   190  	if len(elements) < idx+1 {
   191  		return nil, fmt.Errorf("index %d specified but only %d elements found", idx, len(elements))
   192  	}
   193  	// get target element
   194  	element := elements[idx]
   195  
   196  	// recurse on the matching element
   197  	pm := &PathMatcher{Path: p.Path[1:], Create: p.Create}
   198  	add, err := pm.filter(element)
   199  	for k, v := range pm.Matches {
   200  		p.Matches[k] = v
   201  	}
   202  	if err != nil || add == nil {
   203  		return nil, err
   204  	}
   205  	p.append("", add.Content()...)
   206  	return p.val, nil
   207  }
   208  
   209  // doSeq iterates over a sequence and appends elements matching the path regex to p.Val
   210  func (p *PathMatcher) doSeq(rn *RNode) (*RNode, error) {
   211  	// parse the field + match pair
   212  	var err error
   213  	p.field, p.matchRegex, err = SplitIndexNameValue(p.Path[0])
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  
   218  	primitiveElement := len(p.field) == 0
   219  	if primitiveElement {
   220  		err = rn.VisitElements(p.visitPrimitiveElem)
   221  	} else {
   222  		err = rn.VisitElements(p.visitElem)
   223  	}
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  	if !p.val.IsNil() && len(p.val.YNode().Content) == 0 {
   228  		p.val = nil
   229  	}
   230  
   231  	if !IsCreate(p.Create) || p.val != nil {
   232  		return p.val, nil
   233  	}
   234  
   235  	var elem *yaml.Node
   236  	valueNode := NewScalarRNode(p.matchRegex).YNode()
   237  	if primitiveElement {
   238  		elem = valueNode
   239  	} else {
   240  		elem = &yaml.Node{
   241  			Kind:    yaml.MappingNode,
   242  			Content: []*yaml.Node{{Kind: yaml.ScalarNode, Value: p.field}, valueNode},
   243  		}
   244  	}
   245  	err = rn.PipeE(Append(elem))
   246  	if err != nil {
   247  		return nil, errors.WrapPrefixf(err, "failed to create element for %q", p.Path[0])
   248  	}
   249  	// re-do the sequence search; this time we'll find the element we just created
   250  	return p.doSeq(rn)
   251  }
   252  
   253  func (p *PathMatcher) visitPrimitiveElem(elem *RNode) error {
   254  	r, err := regexp.Compile(p.matchRegex)
   255  	if err != nil {
   256  		return err
   257  	}
   258  
   259  	str, err := elem.String()
   260  	if err != nil {
   261  		return err
   262  	}
   263  	str = strings.TrimSpace(str)
   264  	if !r.MatchString(str) {
   265  		return nil
   266  	}
   267  
   268  	p.appendRNode("", elem)
   269  	return nil
   270  }
   271  
   272  func (p *PathMatcher) visitElem(elem *RNode) error {
   273  	r, err := regexp.Compile(p.matchRegex)
   274  	if err != nil {
   275  		return err
   276  	}
   277  
   278  	// check if this elements field matches the regex
   279  	val := elem.Field(p.field)
   280  	if val == nil || val.Value == nil {
   281  		return nil
   282  	}
   283  	str, err := val.Value.String()
   284  	if err != nil {
   285  		return err
   286  	}
   287  	str = strings.TrimSpace(str)
   288  	if !r.MatchString(str) {
   289  		return nil
   290  	}
   291  
   292  	// recurse on the matching element
   293  	pm := &PathMatcher{Path: p.Path[1:], Create: p.Create}
   294  	add, err := pm.filter(elem)
   295  	for k, v := range pm.Matches {
   296  		p.Matches[k] = v
   297  	}
   298  	if err != nil || add == nil {
   299  		return err
   300  	}
   301  	p.append(str, add.Content()...)
   302  	return nil
   303  }
   304  
   305  func (p *PathMatcher) appendRNode(path string, node *RNode) {
   306  	p.append(path, node.YNode())
   307  }
   308  
   309  func (p *PathMatcher) append(path string, nodes ...*Node) {
   310  	if p.val == nil {
   311  		p.val = NewRNode(&Node{Kind: SequenceNode})
   312  	}
   313  	for i := range nodes {
   314  		node := nodes[i]
   315  		p.val.YNode().Content = append(p.val.YNode().Content, node)
   316  		// record the path if specified
   317  		if path != "" {
   318  			p.Matches[node] = append(p.Matches[node], path)
   319  		}
   320  	}
   321  }
   322  
   323  func cleanPath(path []string) []string {
   324  	var p []string
   325  	for _, elem := range path {
   326  		elem = strings.TrimSpace(elem)
   327  		if len(elem) == 0 {
   328  			continue
   329  		}
   330  		p = append(p, elem)
   331  	}
   332  	return p
   333  }
   334  

View as plain text