Implement JSON path filter for kyaml.RNode
Change introduces kyaml.RNode Pipe Filter which uses k8s go-client JSON path parser. This enables to use JSON queries defined by https://goessner.net/articles/JsonPath/ Change-Id: I6c2276f27652190ed9d183cea0e45eb118346c6b Relates-To: #340
This commit is contained in:
parent
4b8209f100
commit
025c2172d6
69
pkg/document/plugin/kyamlutils/errors.go
Normal file
69
pkg/document/plugin/kyamlutils/errors.go
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package kyamlutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// ErrIndexOutOfBound returned if JSON path points to a wrong index of a list
|
||||
type ErrIndexOutOfBound struct {
|
||||
Index int
|
||||
Length int
|
||||
}
|
||||
|
||||
func (e ErrIndexOutOfBound) Error() string {
|
||||
return fmt.Sprintf("array index out of bounds: index %d, length %d", e.Index, e.Length)
|
||||
}
|
||||
|
||||
// ErrBadQueryFormat raised for JSON query errors
|
||||
type ErrBadQueryFormat struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e ErrBadQueryFormat) Error() string {
|
||||
return e.Msg
|
||||
}
|
||||
|
||||
// ErrLookup raised if error occurs during walk through yaml resource
|
||||
type ErrLookup struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e ErrLookup) Error() string {
|
||||
return e.Msg
|
||||
}
|
||||
|
||||
// ErrNotScalar returned if value defined by key in JSON path is not scalar
|
||||
// Error can be returned for filter queries
|
||||
type ErrNotScalar struct {
|
||||
Node *yaml.Node
|
||||
}
|
||||
|
||||
func (e ErrNotScalar) Error() string {
|
||||
return fmt.Sprintf("%#v is not scalar", e.Node)
|
||||
}
|
||||
|
||||
// ErrQueryConversion returned by query conversion function
|
||||
type ErrQueryConversion struct {
|
||||
Msg string
|
||||
Query string
|
||||
}
|
||||
|
||||
func (e ErrQueryConversion) Error() string {
|
||||
return fmt.Sprintf("failed to convert v1 path '%s' to jsonpath. %s", e.Query, e.Msg)
|
||||
}
|
528
pkg/document/plugin/kyamlutils/jsonpathfilter.go
Normal file
528
pkg/document/plugin/kyamlutils/jsonpathfilter.go
Normal file
@ -0,0 +1,528 @@
|
||||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package kyamlutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"k8s.io/client-go/util/jsonpath"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// JSONPathFilter returns RNode under JSON path
|
||||
type JSONPathFilter struct {
|
||||
Kind string `yaml:"kind,omitempty"`
|
||||
// Path is jsonpath query. See http://goessner.net/articles/JsonPath/ for
|
||||
// details.
|
||||
Path string `yaml:"path,omitempty"`
|
||||
// Mutator is a function which changes filtered node
|
||||
Mutator func([]*yaml.RNode) error
|
||||
// Create empty struct if path element does not exist
|
||||
Create bool
|
||||
}
|
||||
|
||||
type boundaries struct {
|
||||
min, max, step int
|
||||
}
|
||||
|
||||
// Filter returns RNode identified by JSON path
|
||||
func (l JSONPathFilter) Filter(rn *yaml.RNode) (*yaml.RNode, error) {
|
||||
query, err := convertFromLegacyQuery(l.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jp, err := jsonpath.Parse("parser", query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(jp.Root.Nodes) != 1 {
|
||||
return nil, ErrBadQueryFormat{Msg: "query must contain one expression"}
|
||||
}
|
||||
|
||||
fieldList, ok := jp.Root.Nodes[0].(*jsonpath.ListNode)
|
||||
if !ok {
|
||||
return nil, ErrBadQueryFormat{Msg: "failed to convert root node to ListNode type"}
|
||||
}
|
||||
// Query result can be a list of values (e.g filter queries) since
|
||||
// walk is recursive we should pass list of RNode even for initial step
|
||||
rns, err := l.walk([]*yaml.RNode{rn}, fieldList)
|
||||
if len(rns) == 0 || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if l.Mutator != nil {
|
||||
if err := l.Mutator(rns); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Return first element if filtered list contins only one RNode
|
||||
// otherwise create a SequenceNode
|
||||
if len(rns) == 1 {
|
||||
return rns[0], nil
|
||||
}
|
||||
nodes := make([]*yaml.Node, len(rns))
|
||||
for i := range rns {
|
||||
nodes[i] = rns[i].YNode()
|
||||
}
|
||||
return yaml.NewRNode(&yaml.Node{Kind: yaml.SequenceNode, Content: nodes}), nil
|
||||
}
|
||||
|
||||
func (l JSONPathFilter) walk(nodes []*yaml.RNode, fieldList *jsonpath.ListNode) (res []*yaml.RNode, err error) {
|
||||
res = nodes
|
||||
for _, field := range fieldList.Nodes {
|
||||
res, err = l.getByField(res, field)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (l JSONPathFilter) getByField(rns []*yaml.RNode, field jsonpath.Node) ([]*yaml.RNode, error) {
|
||||
switch fieldT := field.(type) {
|
||||
// Can be encountered in filter expressions e.g. [?(10 == 10)]; 10 is IntNode
|
||||
case *jsonpath.IntNode:
|
||||
return []*yaml.RNode{
|
||||
yaml.NewRNode(&yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!int",
|
||||
Value: strconv.Itoa(fieldT.Value),
|
||||
}),
|
||||
}, nil
|
||||
// Same as IntNode
|
||||
case *jsonpath.FloatNode:
|
||||
return []*yaml.RNode{
|
||||
yaml.NewRNode(&yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!float",
|
||||
Value: strconv.FormatFloat(fieldT.Value, 'f', -1, 64),
|
||||
}),
|
||||
}, nil
|
||||
// Same as IntNode
|
||||
case *jsonpath.BoolNode:
|
||||
return []*yaml.RNode{yaml.NewScalarRNode(strconv.FormatBool(fieldT.Value))}, nil
|
||||
// Same as IntNode
|
||||
case *jsonpath.TextNode:
|
||||
return []*yaml.RNode{yaml.NewScalarRNode(fieldT.Text)}, nil
|
||||
// For example spec.containers.metadata 'spec', 'containers' and
|
||||
// 'metadata' have FieldNode type
|
||||
case *jsonpath.FieldNode:
|
||||
res, err := l.doField(rns, fieldT)
|
||||
// Create field node if needed
|
||||
if len(res) == 0 && l.Create {
|
||||
res = []*yaml.RNode{yaml.NewMapRNode(nil)}
|
||||
return res, l.updateMapRNodes(rns, fieldT.Value, res[0])
|
||||
}
|
||||
return res, err
|
||||
// Field is accessing to array via direct element number or by getting
|
||||
// a slice e.g. [20] or [2:10:3]
|
||||
case *jsonpath.ArrayNode:
|
||||
return l.doArray(rns, fieldT)
|
||||
// Filter some elements of the array e.g. [?(.name == 'someName')] will
|
||||
// return all array elements which contain name field equal to 'someName'
|
||||
case *jsonpath.FilterNode:
|
||||
return l.doFilter(rns, fieldT)
|
||||
// Return all elements e.g. spec.containers[*]
|
||||
case *jsonpath.WildcardNode:
|
||||
return l.doWildcard(rns)
|
||||
}
|
||||
return nil, ErrBadQueryFormat{Msg: fmt.Sprintf("unsupported jsonpath expression %s", l.Path)}
|
||||
}
|
||||
|
||||
func (l JSONPathFilter) doField(rns []*yaml.RNode, field *jsonpath.FieldNode) ([]*yaml.RNode, error) {
|
||||
// NOTE this code block was adapted from k8s.io/client-go/util/jsonpath
|
||||
// see evalField from jsonpath.go
|
||||
result := []*yaml.RNode{}
|
||||
if len(rns) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
if field.Value == "" {
|
||||
return rns, nil
|
||||
}
|
||||
for _, rn := range rns {
|
||||
res, err := rn.Pipe(yaml.Get(field.Value))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res != nil {
|
||||
res.AppendToFieldPath(append(rn.FieldPath(), field.Value)...)
|
||||
result = append(result, res)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (l JSONPathFilter) doArray(rns []*yaml.RNode, field *jsonpath.ArrayNode) ([]*yaml.RNode, error) {
|
||||
// NOTE this code block was adapted from k8s.io/client-go/util/jsonpath
|
||||
// see evalArray from jsonpath.go
|
||||
result := []*yaml.RNode{}
|
||||
for _, rn := range rns {
|
||||
if err := yaml.ErrorIfInvalid(rn, yaml.SequenceNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bounds, err := l.defineArrayBoundaries(field.Params, len(rn.Content()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bounds.min == bounds.max {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
for i := bounds.min; i < bounds.max; i += bounds.step {
|
||||
result = append(result, yaml.NewRNode(rn.Content()[i]))
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (l JSONPathFilter) defineArrayBoundaries(params [3]jsonpath.ParamsEntry, maxLen int) (boundaries, error) {
|
||||
res := boundaries{
|
||||
min: 0,
|
||||
max: maxLen,
|
||||
step: 1,
|
||||
}
|
||||
if params[0].Known {
|
||||
res.min = params[0].Value
|
||||
}
|
||||
if params[0].Value < 0 {
|
||||
res.min = params[0].Value + maxLen
|
||||
}
|
||||
if params[1].Known {
|
||||
res.max = params[1].Value
|
||||
}
|
||||
|
||||
if params[1].Value < 0 || (params[1].Value == 0 && params[1].Derived) {
|
||||
res.max = params[1].Value + maxLen
|
||||
}
|
||||
|
||||
if res.min >= maxLen || res.min < 0 {
|
||||
return boundaries{min: -1, max: -1, step: -1}, ErrIndexOutOfBound{Index: params[0].Value, Length: maxLen}
|
||||
}
|
||||
if res.max > maxLen || res.max < 0 {
|
||||
return boundaries{min: -1, max: -1, step: -1}, ErrIndexOutOfBound{Index: params[1].Value - 1, Length: maxLen}
|
||||
}
|
||||
if res.min > res.max {
|
||||
return boundaries{min: -1, max: -1, step: -1}, ErrLookup{
|
||||
fmt.Sprintf("starting index %d is greater than ending index %d",
|
||||
params[0].Value, params[1].Value),
|
||||
}
|
||||
}
|
||||
|
||||
if params[2].Known {
|
||||
if params[2].Value <= 0 {
|
||||
return boundaries{min: -1, max: -1, step: -1}, ErrLookup{fmt.Sprintf("step must be > 0")}
|
||||
}
|
||||
res.step = params[2].Value
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (l JSONPathFilter) doFilter(rns []*yaml.RNode, field *jsonpath.FilterNode) ([]*yaml.RNode, error) {
|
||||
// NOTE this cond block was adapted from k8s.io/client-go/util/jsonpath
|
||||
// see evalFilter from from jsonpath.go
|
||||
result := []*yaml.RNode{}
|
||||
for _, rn := range rns {
|
||||
// Elements() will return an error if rn is not SequenceNode
|
||||
// therefore no need to check it explicitly
|
||||
items, err := rn.Elements()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var left, right []*yaml.RNode
|
||||
for _, item := range items {
|
||||
pass := false
|
||||
// Evaluate left part of the filter expression against each
|
||||
// array item
|
||||
left, err = l.walk([]*yaml.RNode{item}, field.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(left) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(left) > 1 {
|
||||
return nil, ErrBadQueryFormat{Msg: "can only compare one element at a time"}
|
||||
}
|
||||
|
||||
// Evaluate right part of the filter expression against each
|
||||
// array item
|
||||
right, err = l.walk([]*yaml.RNode{item}, field.Right)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(right) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(right) > 1 {
|
||||
return nil, ErrBadQueryFormat{Msg: "can only compare one element at a time"}
|
||||
}
|
||||
|
||||
pass, err = l.compare(field.Operator, left[0], right[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pass {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (l JSONPathFilter) compare(op string, left, right *yaml.RNode) (bool, error) {
|
||||
err := yaml.ErrorIfAnyInvalidAndNonNull(yaml.ScalarNode, left, right)
|
||||
if op != "==" && err != nil {
|
||||
return false, err
|
||||
}
|
||||
switch op {
|
||||
case "<":
|
||||
return lt(left, right)
|
||||
case ">":
|
||||
return gt(left, right)
|
||||
case "==":
|
||||
return eq(left, right)
|
||||
case "!=":
|
||||
pass, err := eq(left, right)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return !pass, nil
|
||||
case "<=":
|
||||
return le(left, right)
|
||||
case ">=":
|
||||
return ge(left, right)
|
||||
}
|
||||
return false, ErrLookup{Msg: fmt.Sprintf("unrecognized filter operator %s", op)}
|
||||
}
|
||||
|
||||
func (l JSONPathFilter) doWildcard(rns []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
// NOTE this code block was adapted from k8s.io/client-go/util/jsonpath
|
||||
// see evalArray from jsonpath.go
|
||||
result := []*yaml.RNode{}
|
||||
for _, rn := range rns {
|
||||
switch rn.YNode().Kind {
|
||||
case yaml.SequenceNode:
|
||||
items, err := rn.Elements()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, items...)
|
||||
case yaml.MappingNode:
|
||||
keys, err := rn.Fields()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, key := range keys {
|
||||
result = append(result, rn.Field(key).Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (l JSONPathFilter) updateMapRNodes(rns []*yaml.RNode, name string, node *yaml.RNode) error {
|
||||
for _, rn := range rns {
|
||||
_, err := rn.Pipe(yaml.SetField(name, node))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func eq(a, b *yaml.RNode) (bool, error) {
|
||||
return deepEq(a.YNode(), b.YNode())
|
||||
}
|
||||
|
||||
func lt(a, b *yaml.RNode) (bool, error) {
|
||||
if a.YNode().Tag == "!!float" || b.YNode().Tag == "!!float" {
|
||||
af, err := strconv.ParseFloat(a.YNode().Value, 64)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
bf, err := strconv.ParseFloat(b.YNode().Value, 64)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return af < bf, nil
|
||||
}
|
||||
|
||||
if a.YNode().Tag == "!!int" || b.YNode().Tag == "!!int" {
|
||||
ai, err := strconv.Atoi(a.YNode().Value)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
bi, err := strconv.Atoi(b.YNode().Value)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return ai < bi, nil
|
||||
}
|
||||
return a.YNode().Value < b.YNode().Value, nil
|
||||
}
|
||||
|
||||
func le(a, b *yaml.RNode) (bool, error) {
|
||||
if less, err := lt(a, b); less || err != nil {
|
||||
return less, err
|
||||
}
|
||||
return eq(a, b)
|
||||
}
|
||||
|
||||
func gt(a, b *yaml.RNode) (bool, error) {
|
||||
return lt(b, a)
|
||||
}
|
||||
|
||||
func ge(a, b *yaml.RNode) (bool, error) {
|
||||
return le(b, a)
|
||||
}
|
||||
|
||||
func scalarEq(a, b *yaml.Node) (bool, error) {
|
||||
if a.Tag == "!!float" || b.Tag == "!!float" {
|
||||
af, err := strconv.ParseFloat(a.Value, 64)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
bf, err := strconv.ParseFloat(b.Value, 64)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return af == bf, nil
|
||||
}
|
||||
|
||||
if a.Tag == "!!int" || b.Tag == "!!int" {
|
||||
ai, err := strconv.Atoi(a.Value)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
bi, err := strconv.Atoi(b.Value)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return ai == bi, nil
|
||||
}
|
||||
return a.Value == b.Value, nil
|
||||
}
|
||||
|
||||
func deepEq(a, b *yaml.Node) (bool, error) {
|
||||
if a == nil || b == nil {
|
||||
return a == b, nil
|
||||
}
|
||||
if a.Kind == yaml.ScalarNode || b.Kind == yaml.ScalarNode {
|
||||
return scalarEq(a, b)
|
||||
}
|
||||
if len(a.Content) != len(b.Content) {
|
||||
return false, nil
|
||||
}
|
||||
for i, item := range a.Content {
|
||||
pass, err := deepEq(item, b.Content[i])
|
||||
if !pass || err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func convertFromLegacyQuery(query string) (string, error) {
|
||||
path := strings.TrimSpace(query)
|
||||
if strings.HasPrefix(query, "{") {
|
||||
return path, nil
|
||||
}
|
||||
var result []string
|
||||
// NOTE this code is borrowed from k8sdeps module of kustomize api
|
||||
start := 0
|
||||
insideParentheses := false
|
||||
for i, c := range path {
|
||||
switch c {
|
||||
case '.':
|
||||
if !insideParentheses {
|
||||
result = convertField(path[start:i], result)
|
||||
start = i + 1
|
||||
}
|
||||
case '[':
|
||||
if !insideParentheses {
|
||||
if path[start:i] != "" {
|
||||
result = append(result, path[start:i])
|
||||
}
|
||||
start = i + 1
|
||||
insideParentheses = true
|
||||
} else {
|
||||
return "", ErrQueryConversion{Msg: "nested parentheses are not allowed", Query: path}
|
||||
}
|
||||
case ']':
|
||||
if insideParentheses {
|
||||
// Assign this index to the current
|
||||
// PathSection, save it to the result, then begin
|
||||
// a new PathSection
|
||||
result = convertFiltering(path[start:i], result)
|
||||
|
||||
start = i + 1
|
||||
insideParentheses = false
|
||||
} else {
|
||||
return "", ErrQueryConversion{Msg: "invalid field path", Query: path}
|
||||
}
|
||||
}
|
||||
}
|
||||
if start < len(path) {
|
||||
result = convertField(path[start:], result)
|
||||
}
|
||||
return "{." + strings.Join(result, ".") + "}", nil
|
||||
}
|
||||
|
||||
func convertFiltering(query string, path []string) []string {
|
||||
_, err := strconv.Atoi(query)
|
||||
result := path
|
||||
switch {
|
||||
case err == nil:
|
||||
// We have detected an integer so an array.
|
||||
result[len(result)-1] = result[len(result)-1] + "[" + query + "]"
|
||||
case strings.Contains(query, "="):
|
||||
kvFilterParts := strings.Split(query, "=")
|
||||
q := fmt.Sprintf("[?(.%s == '%s')]",
|
||||
strings.ReplaceAll(kvFilterParts[0], ".", "\\."), kvFilterParts[1])
|
||||
result[len(result)-1] += q
|
||||
default:
|
||||
// e.g. spec['containers']
|
||||
key := strings.Trim(query, "'")
|
||||
key = strings.Trim(key, "\"")
|
||||
key = strings.ReplaceAll(key, ".", "\\.")
|
||||
result = append(result, key)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func convertField(query string, path []string) []string {
|
||||
if query == "" {
|
||||
return path
|
||||
}
|
||||
_, err := strconv.Atoi(query)
|
||||
if err != nil {
|
||||
return append(path, query)
|
||||
}
|
||||
|
||||
if len(path) == 0 {
|
||||
return []string{"[" + query + "]"}
|
||||
}
|
||||
|
||||
result := append([]string(nil), path...)
|
||||
result[len(result)-1] += "[" + query + "]"
|
||||
return result
|
||||
}
|
477
pkg/document/plugin/kyamlutils/jsonpathfilter_test.go
Normal file
477
pkg/document/plugin/kyamlutils/jsonpathfilter_test.go
Normal file
@ -0,0 +1,477 @@
|
||||
/*
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package kyamlutils_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/document/plugin/kyamlutils"
|
||||
)
|
||||
|
||||
func TestGetterFilter(t *testing.T) {
|
||||
obj, err := yaml.Parse(`apiVersion: apps/v1
|
||||
kind: SomeKind
|
||||
metadata:
|
||||
name: resource-name
|
||||
spec:
|
||||
path:
|
||||
to:
|
||||
some:
|
||||
scalar:
|
||||
value: 300
|
||||
nonScalar:
|
||||
key: val
|
||||
array:
|
||||
- i1
|
||||
- i2
|
||||
keyWith.dot:
|
||||
- subkeyWith.dot: val1
|
||||
name: obj1
|
||||
- subkeyWith.dot: val2
|
||||
name: obj2
|
||||
listOfStrings:
|
||||
- string1
|
||||
- string2
|
||||
- string3
|
||||
listOfObjects:
|
||||
- name: obj1
|
||||
value: 10
|
||||
- name: obj2
|
||||
value: 20
|
||||
- name: obj3
|
||||
value: 30
|
||||
listOfComplexObjects:
|
||||
- name: o1
|
||||
value:
|
||||
someKey1: valO11
|
||||
someKey2: valO12
|
||||
value1:
|
||||
someKey1: valO11
|
||||
someKey2: valO12
|
||||
- name: o2
|
||||
value:
|
||||
someKey0: false
|
||||
someKey1: valO21
|
||||
someKey2: valO22
|
||||
sameObjects:
|
||||
- name: otherName
|
||||
val: otherValue
|
||||
- name: o1
|
||||
val: val1
|
||||
- name: o1
|
||||
val: val1`)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
query string
|
||||
expectedErr string
|
||||
expected string
|
||||
}{
|
||||
// Parser cases
|
||||
{
|
||||
name: "Multiple expressions",
|
||||
query: "{.a.b} {.x.y}",
|
||||
expectedErr: "query must contain one expression",
|
||||
},
|
||||
// Field cases
|
||||
{
|
||||
name: "Path to scalar value",
|
||||
query: "{.spec.path.to.some.scalar.value}",
|
||||
expected: "300",
|
||||
},
|
||||
{
|
||||
name: "Key name with dot",
|
||||
query: "{.keyWith\\.dot}",
|
||||
expected: `
|
||||
- subkeyWith.dot: val1
|
||||
name: obj1
|
||||
- subkeyWith.dot: val2
|
||||
name: obj2`[1:],
|
||||
},
|
||||
{
|
||||
name: "Field from array of scalar values",
|
||||
query: "{.listOfStrings.value}",
|
||||
expectedErr: "wrong Node Kind for listOfStrings " +
|
||||
"expected: MappingNode was SequenceNode: value: {- string1\n- string2\n- string3}",
|
||||
},
|
||||
{
|
||||
name: "Non Existent path",
|
||||
query: "{.a.b}",
|
||||
},
|
||||
// Array cases
|
||||
{
|
||||
name: "Array element negative",
|
||||
query: "{.listOfObjects[-1]}",
|
||||
expected: `
|
||||
name: obj3
|
||||
value: 30`[1:],
|
||||
},
|
||||
{
|
||||
name: "Array element",
|
||||
query: "{.listOfObjects[0]}",
|
||||
expected: `
|
||||
name: obj1
|
||||
value: 10`[1:],
|
||||
},
|
||||
{
|
||||
name: "Array element value",
|
||||
query: "{.listOfObjects[0].value}",
|
||||
expected: "10",
|
||||
},
|
||||
{
|
||||
name: "From element to end",
|
||||
query: "{.listOfObjects[1:]}",
|
||||
expected: `
|
||||
- name: obj2
|
||||
value: 20
|
||||
- name: obj3
|
||||
value: 30`[1:],
|
||||
},
|
||||
{
|
||||
name: "Segment",
|
||||
query: "{.listOfObjects[1:3]}",
|
||||
expected: `
|
||||
- name: obj2
|
||||
value: 20
|
||||
- name: obj3
|
||||
value: 30`[1:],
|
||||
},
|
||||
{
|
||||
name: "Segment with negative right",
|
||||
query: "{.listOfObjects[1:-1]}",
|
||||
expected: `
|
||||
name: obj2
|
||||
value: 20`[1:],
|
||||
},
|
||||
{
|
||||
name: "From start to element",
|
||||
query: "{.listOfObjects[:2]}",
|
||||
expected: `
|
||||
- name: obj1
|
||||
value: 10
|
||||
- name: obj2
|
||||
value: 20`[1:],
|
||||
},
|
||||
{
|
||||
name: "Iterate with step",
|
||||
query: "{.listOfObjects[::2]}",
|
||||
expected: `
|
||||
- name: obj1
|
||||
value: 10
|
||||
- name: obj3
|
||||
value: 30`[1:],
|
||||
},
|
||||
{
|
||||
name: "Empty list",
|
||||
query: "{.listOfObjects[1:1]}",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "Access to non array",
|
||||
query: "{.metadata[1]}",
|
||||
expectedErr: "wrong Node Kind for metadata expected: SequenceNode was MappingNode: value: {name: resource-name}",
|
||||
},
|
||||
{
|
||||
name: "Out of left bound",
|
||||
query: "{.listOfObjects[100]}",
|
||||
expectedErr: "array index out of bounds: index 100, length 3",
|
||||
},
|
||||
{
|
||||
name: "Out of right bound",
|
||||
query: "{.listOfObjects[:100]}",
|
||||
expectedErr: "array index out of bounds: index 99, length 3",
|
||||
},
|
||||
{
|
||||
name: "Bad bounds",
|
||||
query: "{.listOfObjects[2:1]}",
|
||||
expectedErr: "starting index 2 is greater than ending index 1",
|
||||
},
|
||||
{
|
||||
name: "Negative step",
|
||||
query: "{.listOfObjects[::-2]}",
|
||||
expectedErr: "step must be > 0",
|
||||
},
|
||||
// Filter cases
|
||||
{
|
||||
name: "Filter by key",
|
||||
query: "{.listOfObjects[?(.name == 'obj1')]}",
|
||||
expected: `
|
||||
name: obj1
|
||||
value: 10`[1:],
|
||||
},
|
||||
{
|
||||
name: "Filter by key name with dot",
|
||||
query: "{.keyWith\\.dot[?(.subkeyWith\\.dot == 'val2')]}",
|
||||
expected: `
|
||||
subkeyWith.dot: val2
|
||||
name: obj2`[1:],
|
||||
},
|
||||
{
|
||||
name: "Filter eq int",
|
||||
query: "{.listOfObjects[?(.value == 10)]}",
|
||||
expected: `
|
||||
name: obj1
|
||||
value: 10`[1:],
|
||||
},
|
||||
{
|
||||
name: "Filter by content",
|
||||
query: "{.listOfStrings[?(. == 'string1')]}",
|
||||
expected: "string1",
|
||||
},
|
||||
{
|
||||
name: "Query parse error",
|
||||
query: "{.listOfStrings[?(. == 'string1'",
|
||||
expectedErr: "unterminated filter",
|
||||
},
|
||||
{
|
||||
name: "Filter non-array",
|
||||
query: "{.metadata[?(.name == 'name')]}",
|
||||
expectedErr: "wrong Node Kind for metadata expected: SequenceNode was MappingNode: value: {name: resource-name}",
|
||||
},
|
||||
{
|
||||
name: "Filter condition non-array",
|
||||
query: "{.listOfObjects[?(.[0] == 'name')]}",
|
||||
expectedErr: "wrong Node Kind for expected: SequenceNode was MappingNode: value: {name: obj1\nvalue: 10}",
|
||||
},
|
||||
{
|
||||
name: "Filter eq non-scalars",
|
||||
query: "{.listOfComplexObjects[?(.value == 'name')]}",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "Filter compare non-scalars",
|
||||
query: "{.listOfComplexObjects[?(.value > 'name')]}",
|
||||
expectedErr: "wrong Node Kind for value expected: ScalarNode was MappingNode: value: {someKey1: valO11\n" +
|
||||
"someKey2: valO12}",
|
||||
},
|
||||
{
|
||||
name: "Filter unknown operator",
|
||||
query: "{.listOfStrings[?(. >> 'name')]}",
|
||||
expectedErr: "unrecognized filter operator >>",
|
||||
},
|
||||
{
|
||||
name: "Filter gt int",
|
||||
query: "{.listOfObjects[?(.value > 10)]}",
|
||||
expected: `
|
||||
- name: obj2
|
||||
value: 20
|
||||
- name: obj3
|
||||
value: 30`[1:],
|
||||
},
|
||||
{
|
||||
name: "Filter gt float",
|
||||
query: "{.listOfObjects[?(.value > 7.5)]}",
|
||||
expected: `
|
||||
- name: obj1
|
||||
value: 10
|
||||
- name: obj2
|
||||
value: 20
|
||||
- name: obj3
|
||||
value: 30`[1:],
|
||||
},
|
||||
{
|
||||
name: "Filter ge float",
|
||||
query: "{.listOfObjects[?(.value >= 20.0)]}",
|
||||
expected: `
|
||||
- name: obj2
|
||||
value: 20
|
||||
- name: obj3
|
||||
value: 30`[1:],
|
||||
},
|
||||
{
|
||||
name: "Filter lt int",
|
||||
query: "{.listOfObjects[?(20 < .value)]}",
|
||||
expected: `
|
||||
name: obj3
|
||||
value: 30`[1:],
|
||||
},
|
||||
{
|
||||
name: "Filter eq objects",
|
||||
query: "{.listOfObjects[?(@ == @)]}",
|
||||
expected: `
|
||||
- name: obj1
|
||||
value: 10
|
||||
- name: obj2
|
||||
value: 20
|
||||
- name: obj3
|
||||
value: 30`[1:],
|
||||
},
|
||||
{
|
||||
name: "Filter eq sub-objects",
|
||||
query: "{.listOfComplexObjects[?(.value == .value1)]}",
|
||||
expected: `
|
||||
name: o1
|
||||
value:
|
||||
someKey1: valO11
|
||||
someKey2: valO12
|
||||
value1:
|
||||
someKey1: valO11
|
||||
someKey2: valO12`[1:],
|
||||
},
|
||||
{
|
||||
name: "Filter not eq error",
|
||||
query: "{.listOfObjects[?(.name != 10)]}",
|
||||
expectedErr: `strconv.Atoi: parsing "obj1": invalid syntax`,
|
||||
},
|
||||
{
|
||||
name: "Filter eq bool",
|
||||
query: "{.listOfComplexObjects[?(.value.someKey0 == false)]}",
|
||||
expected: `
|
||||
name: o2
|
||||
value:
|
||||
someKey0: false
|
||||
someKey1: valO21
|
||||
someKey2: valO22`[1:],
|
||||
},
|
||||
// Wildcard cases
|
||||
{
|
||||
name: "Get map by wildcard",
|
||||
query: "{.spec.path.to.some.*}",
|
||||
expected: `
|
||||
- value: 300
|
||||
- key: val
|
||||
- - i1
|
||||
- i2`[1:],
|
||||
},
|
||||
{
|
||||
name: "Wildcard list",
|
||||
query: "{.listOfStrings.*}",
|
||||
expected: `
|
||||
- string1
|
||||
- string2
|
||||
- string3`[1:],
|
||||
},
|
||||
// v1 queries cases
|
||||
{
|
||||
name: "Array element v1",
|
||||
query: "listOfObjects[0]",
|
||||
expected: `
|
||||
name: obj1
|
||||
value: 10`[1:],
|
||||
},
|
||||
{
|
||||
name: "Array element value v1",
|
||||
query: "listOfObjects[0].value",
|
||||
expected: "10",
|
||||
},
|
||||
{
|
||||
name: "Array element by numeric key v1",
|
||||
query: "listOfObjects.0",
|
||||
expected: `
|
||||
name: obj1
|
||||
value: 10`[1:],
|
||||
},
|
||||
{
|
||||
name: "Array element value by numeric key v1",
|
||||
query: "listOfObjects.0.value",
|
||||
expected: "10",
|
||||
},
|
||||
{
|
||||
name: "Array filter v1",
|
||||
query: "listOfObjects[name=obj1]",
|
||||
expected: `
|
||||
name: obj1
|
||||
value: 10`[1:],
|
||||
},
|
||||
{
|
||||
name: "Array filter get all value v1",
|
||||
query: "listOfObjects[name=obj1].value",
|
||||
expected: "10",
|
||||
},
|
||||
{
|
||||
name: "Array filter get same value v1",
|
||||
query: "sameObjects[name=o1].val",
|
||||
expected: `
|
||||
- val1
|
||||
- val1`[1:],
|
||||
},
|
||||
{
|
||||
name: "Array filter get first value v1",
|
||||
query: "['keyWith.dot'][subkeyWith.dot=val1]",
|
||||
expected: `
|
||||
subkeyWith.dot: val1
|
||||
name: obj1`[1:],
|
||||
},
|
||||
{
|
||||
name: "Nested parentheses",
|
||||
query: "spec[path[0]]",
|
||||
expectedErr: "failed to convert v1 path 'spec[path[0]]' to jsonpath. nested parentheses are not allowed",
|
||||
},
|
||||
{
|
||||
name: "Invalid closing parentheses",
|
||||
query: "spec[path]]",
|
||||
expectedErr: "failed to convert v1 path 'spec[path]]' to jsonpath. invalid field path",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
tt := tc
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
getter := kyamlutils.JSONPathFilter{Path: tt.query}
|
||||
actualObj, err := obj.Pipe(getter)
|
||||
errS := ""
|
||||
if err != nil {
|
||||
errS = err.Error()
|
||||
}
|
||||
assert.Equal(t, tc.expectedErr, errS)
|
||||
actualString, err := actualObj.String()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expected, strings.TrimSuffix(actualString, "\n"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterCreate(t *testing.T) {
|
||||
data := `apiVersion: apps/v1
|
||||
kind: SomeKind
|
||||
metadata:
|
||||
name: resource-name`
|
||||
testCases := []struct {
|
||||
name string
|
||||
query string
|
||||
create bool
|
||||
}{
|
||||
{
|
||||
name: "Create if not exists",
|
||||
query: "{.spec.not.exists}",
|
||||
create: true,
|
||||
},
|
||||
{
|
||||
name: "Not exists",
|
||||
query: "{.spec.not.exists}",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tt := tc
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
obj, err := yaml.Parse(data)
|
||||
require.NoError(t, err)
|
||||
getter := kyamlutils.JSONPathFilter{Path: tt.query, Create: tt.create}
|
||||
actualObj, err := obj.Pipe(getter)
|
||||
require.NoError(t, err)
|
||||
if tt.create {
|
||||
assert.NotNil(t, actualObj)
|
||||
} else {
|
||||
assert.Nil(t, actualObj)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -106,11 +106,12 @@ func (e ErrPatternSubstring) Error() string {
|
||||
|
||||
// ErrIndexOutOfBound returned if JSON path points to a wrong index of a list
|
||||
type ErrIndexOutOfBound struct {
|
||||
Index int
|
||||
Index int
|
||||
Length int
|
||||
}
|
||||
|
||||
func (e ErrIndexOutOfBound) Error() string {
|
||||
return fmt.Sprintf("index %v is out of bound", e.Index)
|
||||
return fmt.Sprintf("array index out of bounds: index %d, length %d", e.Index, e.Length)
|
||||
}
|
||||
|
||||
// ErrMapNotFound returned if map specified in fieldRef option was not found in a list
|
||||
|
@ -328,7 +328,7 @@ func updateSliceField(m []interface{}, pathToField []string, replacement interfa
|
||||
}
|
||||
|
||||
if len(m) < index || index < 0 {
|
||||
return ErrIndexOutOfBound{Index: index}
|
||||
return ErrIndexOutOfBound{Index: index, Length: len(m)}
|
||||
}
|
||||
if len(pathToField) == 1 {
|
||||
m[index] = replacement
|
||||
|
@ -852,7 +852,7 @@ spec:
|
||||
containers:
|
||||
- name: myapp-container
|
||||
image: busybox`,
|
||||
expectedErr: "index 10 is out of bound",
|
||||
expectedErr: "array index out of bounds: index 10, length 1",
|
||||
},
|
||||
{
|
||||
cfg: `
|
||||
|
Loading…
Reference in New Issue
Block a user