Skip to content

Commit

Permalink
Support basic path expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
phanimarupaka committed Nov 17, 2020
1 parent 8ea1a23 commit 1832221
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 42 deletions.
2 changes: 1 addition & 1 deletion internal/cmdsearch/cmdsearch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ deployment.yaml: metadata.namespace: otherspace
args: []string{"--by-value", "mysql"},
out: `${baseDir}/mysql/
matched 1 field(s)
deployment.yaml: spec.template.spec.containers.name: mysql
deployment.yaml: spec.template.spec.containers[0].name: mysql
${baseDir}/mysql/nosetters/
matched 0 field(s)
Expand Down
82 changes: 82 additions & 0 deletions internal/cmdsearch/searchreplace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,88 @@ spec:
---
apiVersion: apps/v1
kind: Service
metadata:
name: nginx-service
`,
},
{
name: "search by array path",
args: []string{"--by-path", "spec.foo[1]"},
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
foo:
- a
- b
---
apiVersion: apps/v1
kind: Service
metadata:
name: nginx-service
`,
out: `${baseDir}/
matched 1 field(s)
${filePath}: spec.foo[1]: b
`,
expectedResources: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
foo:
- a
- b
---
apiVersion: apps/v1
kind: Service
metadata:
name: nginx-service
`,
},
{
name: "search by array objects path",
args: []string{"--by-path", "spec.foo[1].c"},
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
foo:
- c: thing0
- c: thing1
- c: thing2
---
apiVersion: apps/v1
kind: Service
metadata:
name: nginx-service
`,
out: `${baseDir}/
matched 1 field(s)
${filePath}: spec.foo[1].c: thing1
`,
expectedResources: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
foo:
- c: thing0
- c: thing1
- c: thing2
---
apiVersion: apps/v1
kind: Service
metadata:
name: nginx-service
`,
Expand Down
40 changes: 40 additions & 0 deletions internal/util/search/pathparser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package search

import (
"strings"
)

// pathMatch checks if the traversed yaml path matches with the user input path
// checks if user input path is valid
func (sr *SearchReplace) pathMatch(yamlPath string) bool {
if sr.ByPath == "" {
return false
}
inputElems := strings.Split(sr.ByPath, PathDelimiter)
traversedElems := strings.Split(strings.Trim(yamlPath, PathDelimiter), PathDelimiter)
if len(inputElems) != len(traversedElems) {
return false
}
for i, inputElem := range inputElems {
if inputElem != "*" && inputElem != traversedElems[i] {
return false
}
}
return true
}

// isAbsPath checks if input path is absolute and not a path expression
// only supported path format is e.g. foo.bar.baz
func isAbsPath(path string) bool {
pathElem := strings.Split(path, PathDelimiter)
if len(pathElem) == 0 {
return false
}
for _, elem := range pathElem {
// more checks can be added in future
if elem == "" || strings.Contains(elem, "*") {
return false
}
}
return true
}
62 changes: 62 additions & 0 deletions internal/util/search/pathparser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package search

import (
"testing"

"github.com/golangplus/testing/assert"
)

type test struct {
name string
byPath string
traversedPath string
shouldMatch bool
}

var tests = []test{
{
name: "simple path match",
byPath: "a.b.c",
traversedPath: "a.b.c",
shouldMatch: true,
},
{
name: "simple path no match",
byPath: "a.b.c",
traversedPath: "a.c.b",
shouldMatch: false,
},
{
name: "simple path match with *",
byPath: "a.*.c.*",
traversedPath: "a.b.c.d",
shouldMatch: true,
},
{
name: "simple path no match with *",
byPath: "a.*.c.*",
traversedPath: "a.b.c",
shouldMatch: false,
},
{
name: "simple array path match",
byPath: "a.c[0]",
traversedPath: "a.c[0]",
shouldMatch: true,
},
}

func TestPathMatch(t *testing.T) {
for i := range tests {
test := tests[i]
t.Run(test.name, func(t *testing.T) {
sr := SearchReplace{
ByPath: test.byPath,
}
actual := sr.pathMatch(test.traversedPath)
if !assert.Equal(t, "", actual, test.shouldMatch) {
t.FailNow()
}
})
}
}
48 changes: 9 additions & 39 deletions internal/util/search/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,18 +112,17 @@ func (sr *SearchReplace) visitMapping(object *yaml.RNode, path string) error {

// visitSequence parses sequence node
func (sr *SearchReplace) visitSequence(object *yaml.RNode, path string) error {
// TODO: pmarupaka support sequence nodes
return nil
}

// visitScalar parses scalar node
func (sr *SearchReplace) visitScalar(object *yaml.RNode, path string) error {
pathMatch, err := sr.pathMatch(path)
if err != nil {
return err
}
return sr.matchAndReplace(object.Document(), path)
}

valueMatch := object.Document().Value == sr.ByValue || sr.regexMatch(object.Document().Value)
func (sr *SearchReplace) matchAndReplace(node *yaml.Node, path string) error {
pathMatch := sr.pathMatch(path)
valueMatch := node.Value == sr.ByValue || sr.regexMatch(node.Value)

// at least one of path or value must be matched
if (valueMatch && pathMatch) || (valueMatch && sr.ByPath == "") ||
Expand All @@ -133,7 +132,7 @@ func (sr *SearchReplace) visitScalar(object *yaml.RNode, path string) error {
if sr.PutLiteral != "" {
// TODO: pmarupaka Check if the new value honors the openAPI schema and/or
// current field type, throw error if it doesn't
object.Document().Value = sr.PutLiteral
node.Value = sr.PutLiteral
}

if sr.PutPattern != "" {
Expand All @@ -142,7 +141,7 @@ func (sr *SearchReplace) visitScalar(object *yaml.RNode, path string) error {
if err != nil {
return err
}
pattern := object.Document().Value
pattern := node.Value
// derive the pattern from the field value by replacing setter values
// with setter name markers
// e.g. if field value is "my-project-foo", input PutPattern is "${project]-*", and
Expand All @@ -151,11 +150,11 @@ func (sr *SearchReplace) visitScalar(object *yaml.RNode, path string) error {
for sn, sv := range settersValues {
pattern = strings.ReplaceAll(clean(pattern), clean(sv), fmt.Sprintf("${%s}", sn))
}
object.Document().LineComment = fmt.Sprintf(`{"$kpt-set":%q}`, pattern)
node.LineComment = fmt.Sprintf(`{"$kpt-set":%q}`, pattern)
}

if sr.filePath != "" {
pathVal := fmt.Sprintf("%s: %s", strings.TrimPrefix(path, PathDelimiter), object.Document().Value)
pathVal := fmt.Sprintf("%s: %s", strings.TrimPrefix(path, PathDelimiter), node.Value)
sr.Match[sr.filePath] = append(sr.Match[sr.filePath], pathVal)
}
}
Expand All @@ -171,35 +170,6 @@ func (sr *SearchReplace) regexMatch(value string) bool {
return sr.regex.Match([]byte(value))
}

// pathMatch checks if the traversed yaml path matches with the user input path
// checks if user input path is valid
func (sr *SearchReplace) pathMatch(yamlPath string) (bool, error) {
if sr.ByPath == "" {
return false, nil
}
// TODO: pmarupaka Path expressions should be supported
if !isAbsPath(sr.ByPath) {
return false, errors.Errorf(`invalid input path, must follow pattern e.g. foo.bar.baz`)
}
return sr.ByPath == strings.TrimPrefix(yamlPath, PathDelimiter), nil
}

// isAbsPath checks if input path is absolute and not a path expression
// only supported path format is e.g. foo.bar.baz
func isAbsPath(path string) bool {
pathElem := strings.Split(path, PathDelimiter)
if len(pathElem) == 0 {
return false
}
for _, elem := range pathElem {
// more checks can be added in future
if elem == "" || strings.Contains(elem, "*") {
return false
}
}
return true
}

// putLiteral puts the literal in the user specified sr.ByPath
func (sr *SearchReplace) putLiteral(object *yaml.RNode) error {
path := strings.Split(sr.ByPath, PathDelimiter)
Expand Down
22 changes: 20 additions & 2 deletions internal/util/search/walk.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
package search

import (
"fmt"

"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/yaml"
)

Expand Down Expand Up @@ -68,13 +71,28 @@ func acceptImpl(v visitor, object *yaml.RNode, p string) error {
return err
}
// get the schema for the elements
return object.VisitElements(func(node *yaml.RNode) error {
return VisitElements(object, func(node *yaml.RNode, i int) error {
// Traverse each list element
return acceptImpl(v, node, p)
return acceptImpl(v, node, p+fmt.Sprintf("[%d]", i))
})
case yaml.ScalarNode:
// Visit the scalar field
return v.visitScalar(object, p)
}
return nil
}

// VisitElements calls fn for each element in a SequenceNode.
// Returns an error for non-SequenceNodes
func VisitElements(rn *yaml.RNode, fn func(node *yaml.RNode, i int) error) error {
elements, err := rn.Elements()
if err != nil {
return errors.Wrap(err)
}
for i := range elements {
if err := fn(elements[i], i); err != nil {
return errors.Wrap(err)
}
}
return nil
}

0 comments on commit 1832221

Please sign in to comment.