mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-05-17 18:25:26 +00:00
* Ahead-of-time wildcard path expansion solution * Wrapped PathGetter solution This approach doesn't work when multiple existing sequence elements should match, i.e. because the sequence contains maps and we're searching on a key they all contain (target all containers with a certain image would be one use case for this). PathGetter just takes the first match in that case, which is not what we want. * Add creation support to PathMatcher * Regression test for existing bug when creation is enabled and sequence query should match multiple elements * PathMatcher Create tests and support for sequence appending * revert hyphen append support PathGetter treats it as meaning 'last' not 'append' and does not have test coverage for its handling of this when create is set. Semantics are dubious given that multiple Replacement fieldPaths may be specified, which would cause successive appends. * This also provides a solution to issue 1493 * Review feedback
334 lines
8.2 KiB
Go
334 lines
8.2 KiB
Go
// Copyright 2019 The Kubernetes Authors.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package yaml
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"sigs.k8s.io/kustomize/kyaml/errors"
|
|
"sigs.k8s.io/kustomize/kyaml/internal/forked/github.com/go-yaml/yaml"
|
|
)
|
|
|
|
// PathMatcher returns all RNodes matching the path wrapped in a SequenceNode.
|
|
// Lists may have multiple elements matching the path, and each matching element
|
|
// is added to the return result.
|
|
// If Path points to a SequenceNode, the SequenceNode is wrapped in another SequenceNode
|
|
// If Path does not contain any lists, the result is still wrapped in a SequenceNode of len == 1
|
|
type PathMatcher struct {
|
|
Kind string `yaml:"kind,omitempty"`
|
|
|
|
// Path is a slice of parts leading to the RNode to lookup.
|
|
// Each path part may be one of:
|
|
// * FieldMatcher -- e.g. "spec"
|
|
// * Map Key -- e.g. "app.k8s.io/version"
|
|
// * List Entry -- e.g. "[name=nginx]" or "[=-jar]" or "0"
|
|
//
|
|
// Map Keys and Fields are equivalent.
|
|
// See FieldMatcher for more on Fields and Map Keys.
|
|
//
|
|
// List Entries are specified as map entry to match [fieldName=fieldValue].
|
|
// See Elem for more on List Entries.
|
|
//
|
|
// Examples:
|
|
// * spec.template.spec.container with matching name: [name=nginx] -- match 'name': 'nginx'
|
|
// * spec.template.spec.container.argument matching a value: [=-jar] -- match '-jar'
|
|
Path []string `yaml:"path,omitempty"`
|
|
|
|
// Matches is set by PathMatch to publish the matched element values for each node.
|
|
// After running PathMatcher.Filter, each node from the SequenceNode result may be
|
|
// looked up in Matches to find the field values that were matched.
|
|
Matches map[*Node][]string
|
|
|
|
// StripComments may be set to remove the comments on the matching Nodes.
|
|
// This is useful for if the nodes are to be printed in FlowStyle.
|
|
StripComments bool
|
|
|
|
// Create will cause missing path parts to be created as they are walked.
|
|
//
|
|
// * The leaf Node (final path) will be created with a Kind matching Create
|
|
// * Intermediary Nodes will be created as either a MappingNodes or
|
|
// SequenceNodes as appropriate for each's Path location.
|
|
// * Nodes identified by an index will only be created if the index indicates
|
|
// an append operation (i.e. index=len(list))
|
|
Create yaml.Kind `yaml:"create,omitempty"`
|
|
|
|
val *RNode
|
|
field string
|
|
matchRegex string
|
|
}
|
|
|
|
func (p *PathMatcher) stripComments(n *Node) {
|
|
if n == nil {
|
|
return
|
|
}
|
|
if p.StripComments {
|
|
n.LineComment = ""
|
|
n.HeadComment = ""
|
|
n.FootComment = ""
|
|
for i := range n.Content {
|
|
p.stripComments(n.Content[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *PathMatcher) Filter(rn *RNode) (*RNode, error) {
|
|
val, err := p.filter(rn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p.stripComments(val.YNode())
|
|
return val, err
|
|
}
|
|
|
|
func (p *PathMatcher) filter(rn *RNode) (*RNode, error) {
|
|
p.Matches = map[*Node][]string{}
|
|
|
|
if len(p.Path) == 0 {
|
|
// return the element wrapped in a SequenceNode
|
|
p.appendRNode("", rn)
|
|
return p.val, nil
|
|
}
|
|
|
|
if IsIdxNumber(p.Path[0]) {
|
|
return p.doIndexSeq(rn)
|
|
}
|
|
|
|
if IsListIndex(p.Path[0]) {
|
|
// match seq elements
|
|
return p.doSeq(rn)
|
|
}
|
|
|
|
if IsWildcard(p.Path[0]) {
|
|
// match every elements (*)
|
|
return p.doMatchEvery(rn)
|
|
}
|
|
// match a field
|
|
return p.doField(rn)
|
|
}
|
|
|
|
func (p *PathMatcher) doMatchEvery(rn *RNode) (*RNode, error) {
|
|
if err := rn.VisitElements(p.visitEveryElem); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return p.val, nil
|
|
}
|
|
|
|
func (p *PathMatcher) visitEveryElem(elem *RNode) error {
|
|
fieldName := p.Path[0]
|
|
// recurse on the matching element
|
|
pm := &PathMatcher{Path: p.Path[1:], Create: p.Create}
|
|
add, err := pm.filter(elem)
|
|
for k, v := range pm.Matches {
|
|
p.Matches[k] = v
|
|
}
|
|
if err != nil || add == nil {
|
|
return err
|
|
}
|
|
p.append(fieldName, add.Content()...)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *PathMatcher) doField(rn *RNode) (*RNode, error) {
|
|
// lookup the field
|
|
field, err := rn.Pipe(Get(p.Path[0]))
|
|
if err != nil || (!IsCreate(p.Create) && field == nil) {
|
|
return nil, err
|
|
}
|
|
|
|
if IsCreate(p.Create) && field == nil {
|
|
var nextPart string
|
|
if len(p.Path) > 1 {
|
|
nextPart = p.Path[1]
|
|
}
|
|
nextPartKind := getPathPartKind(nextPart, p.Create)
|
|
field = &RNode{value: &yaml.Node{Kind: nextPartKind}}
|
|
err := rn.PipeE(SetField(p.Path[0], field))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// recurse on the field, removing the first element of the path
|
|
pm := &PathMatcher{Path: p.Path[1:], Create: p.Create}
|
|
p.val, err = pm.filter(field)
|
|
p.Matches = pm.Matches
|
|
return p.val, err
|
|
}
|
|
|
|
// doIndexSeq iterates over a sequence and appends elements matching the index p.Val
|
|
func (p *PathMatcher) doIndexSeq(rn *RNode) (*RNode, error) {
|
|
// parse to index number
|
|
idx, err := strconv.Atoi(p.Path[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
elements, err := rn.Elements()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(elements) == idx && IsCreate(p.Create) {
|
|
var nextPart string
|
|
if len(p.Path) > 1 {
|
|
nextPart = p.Path[1]
|
|
}
|
|
elem := &yaml.Node{Kind: getPathPartKind(nextPart, p.Create)}
|
|
err = rn.PipeE(Append(elem))
|
|
if err != nil {
|
|
return nil, errors.WrapPrefixf(err, "failed to append element for %q", p.Path[0])
|
|
}
|
|
elements = append(elements, NewRNode(elem))
|
|
}
|
|
|
|
if len(elements) < idx+1 {
|
|
return nil, fmt.Errorf("index %d specified but only %d elements found", idx, len(elements))
|
|
}
|
|
// get target element
|
|
element := elements[idx]
|
|
|
|
// recurse on the matching element
|
|
pm := &PathMatcher{Path: p.Path[1:], Create: p.Create}
|
|
add, err := pm.filter(element)
|
|
for k, v := range pm.Matches {
|
|
p.Matches[k] = v
|
|
}
|
|
if err != nil || add == nil {
|
|
return nil, err
|
|
}
|
|
p.append("", add.Content()...)
|
|
return p.val, nil
|
|
}
|
|
|
|
// doSeq iterates over a sequence and appends elements matching the path regex to p.Val
|
|
func (p *PathMatcher) doSeq(rn *RNode) (*RNode, error) {
|
|
// parse the field + match pair
|
|
var err error
|
|
p.field, p.matchRegex, err = SplitIndexNameValue(p.Path[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
primitiveElement := len(p.field) == 0
|
|
if primitiveElement {
|
|
err = rn.VisitElements(p.visitPrimitiveElem)
|
|
} else {
|
|
err = rn.VisitElements(p.visitElem)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !p.val.IsNil() && len(p.val.YNode().Content) == 0 {
|
|
p.val = nil
|
|
}
|
|
|
|
if !IsCreate(p.Create) || p.val != nil {
|
|
return p.val, nil
|
|
}
|
|
|
|
var elem *yaml.Node
|
|
valueNode := NewScalarRNode(p.matchRegex).YNode()
|
|
if primitiveElement {
|
|
elem = valueNode
|
|
} else {
|
|
elem = &yaml.Node{
|
|
Kind: yaml.MappingNode,
|
|
Content: []*yaml.Node{{Kind: yaml.ScalarNode, Value: p.field}, valueNode},
|
|
}
|
|
}
|
|
err = rn.PipeE(Append(elem))
|
|
if err != nil {
|
|
return nil, errors.WrapPrefixf(err, "failed to create element for %q", p.Path[0])
|
|
}
|
|
// re-do the sequence search; this time we'll find the element we just created
|
|
return p.doSeq(rn)
|
|
}
|
|
|
|
func (p *PathMatcher) visitPrimitiveElem(elem *RNode) error {
|
|
r, err := regexp.Compile(p.matchRegex)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
str, err := elem.String()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
str = strings.TrimSpace(str)
|
|
if !r.MatchString(str) {
|
|
return nil
|
|
}
|
|
|
|
p.appendRNode("", elem)
|
|
return nil
|
|
}
|
|
|
|
func (p *PathMatcher) visitElem(elem *RNode) error {
|
|
r, err := regexp.Compile(p.matchRegex)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// check if this elements field matches the regex
|
|
val := elem.Field(p.field)
|
|
if val == nil || val.Value == nil {
|
|
return nil
|
|
}
|
|
str, err := val.Value.String()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
str = strings.TrimSpace(str)
|
|
if !r.MatchString(str) {
|
|
return nil
|
|
}
|
|
|
|
// recurse on the matching element
|
|
pm := &PathMatcher{Path: p.Path[1:], Create: p.Create}
|
|
add, err := pm.filter(elem)
|
|
for k, v := range pm.Matches {
|
|
p.Matches[k] = v
|
|
}
|
|
if err != nil || add == nil {
|
|
return err
|
|
}
|
|
p.append(str, add.Content()...)
|
|
return nil
|
|
}
|
|
|
|
func (p *PathMatcher) appendRNode(path string, node *RNode) {
|
|
p.append(path, node.YNode())
|
|
}
|
|
|
|
func (p *PathMatcher) append(path string, nodes ...*Node) {
|
|
if p.val == nil {
|
|
p.val = NewRNode(&Node{Kind: SequenceNode})
|
|
}
|
|
for i := range nodes {
|
|
node := nodes[i]
|
|
p.val.YNode().Content = append(p.val.YNode().Content, node)
|
|
// record the path if specified
|
|
if path != "" {
|
|
p.Matches[node] = append(p.Matches[node], path)
|
|
}
|
|
}
|
|
}
|
|
|
|
func cleanPath(path []string) []string {
|
|
var p []string
|
|
for _, elem := range path {
|
|
elem = strings.TrimSpace(elem)
|
|
if len(elem) == 0 {
|
|
continue
|
|
}
|
|
p = append(p, elem)
|
|
}
|
|
return p
|
|
}
|