mirror of
https://github.com/kyverno/kyverno.git
synced 2024-12-14 11:57:48 +00:00
* Added jsonpointer package that supports parsing of paths and JSON pointers that can yield either a JSON pointer string or JMESPath string. * Replaced the use of `strings.Split` and `strings.Join` in places where paths are converted to JMESPaths. Signed-off-by: Tobias Dahlberg <tobias.dahlberg@sinch.com> Signed-off-by: Tobias Dahlberg <tobias.dahlberg@sinch.com> Co-authored-by: shuting <shuting@nirmata.com> Co-authored-by: Prateek Pandey <prateek.pandey@nirmata.com> Co-authored-by: Vyankatesh Kudtarkar <vyankateshkd@gmail.com>
This commit is contained in:
parent
977dcc38a2
commit
19f0e7ebfe
9 changed files with 848 additions and 74 deletions
|
@ -14,11 +14,11 @@ import (
|
|||
"github.com/kyverno/kyverno/pkg/cosign"
|
||||
"github.com/kyverno/kyverno/pkg/engine/context"
|
||||
"github.com/kyverno/kyverno/pkg/engine/response"
|
||||
engineUtils "github.com/kyverno/kyverno/pkg/engine/utils"
|
||||
"github.com/kyverno/kyverno/pkg/engine/variables"
|
||||
"github.com/kyverno/kyverno/pkg/logging"
|
||||
"github.com/kyverno/kyverno/pkg/registryclient"
|
||||
apiutils "github.com/kyverno/kyverno/pkg/utils/api"
|
||||
"github.com/kyverno/kyverno/pkg/utils/jsonpointer"
|
||||
"github.com/kyverno/kyverno/pkg/utils/wildcard"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/multierr"
|
||||
|
@ -170,8 +170,8 @@ func (iv *imageVerifier) verify(imageVerify kyvernov1.ImageVerification, images
|
|||
continue
|
||||
}
|
||||
|
||||
jmespath := engineUtils.JsonPointerToJMESPath(imageInfo.Pointer)
|
||||
changed, err := iv.policyContext.JSONContext.HasChanged(jmespath)
|
||||
pointer := jsonpointer.ParsePath(imageInfo.Pointer).JMESPath()
|
||||
changed, err := iv.policyContext.JSONContext.HasChanged(pointer)
|
||||
if err == nil && !changed {
|
||||
iv.logger.V(4).Info("no change in image, skipping check", "image", image)
|
||||
continue
|
||||
|
|
|
@ -3,6 +3,7 @@ package jsonutils
|
|||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kyverno/kyverno/pkg/utils"
|
||||
)
|
||||
|
@ -101,7 +102,7 @@ func (t *Traversal) traverseObject(object map[string]interface{}, path string) (
|
|||
}
|
||||
}
|
||||
|
||||
value, err := t.traverseJSON(element, path+"/"+key)
|
||||
value, err := t.traverseJSON(element, path+"/"+strings.ReplaceAll(key, "/", `\/`))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -1587,3 +1587,369 @@ func Test_RuleSelectorMutate(t *testing.T) {
|
|||
t.Error("rule 1 patches dont match")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SpecialCharacters(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
policyRaw []byte
|
||||
documentRaw []byte
|
||||
want [][]byte
|
||||
}{
|
||||
{
|
||||
name: "regex_replace",
|
||||
policyRaw: []byte(`{
|
||||
"apiVersion": "kyverno.io/v1",
|
||||
"kind": "ClusterPolicy",
|
||||
"metadata": {
|
||||
"name": "regex-replace-all-demo"
|
||||
},
|
||||
"spec": {
|
||||
"background": false,
|
||||
"rules": [
|
||||
{
|
||||
"name": "retention-adjust",
|
||||
"match": {
|
||||
"any": [
|
||||
{
|
||||
"resources": {
|
||||
"kinds": [
|
||||
"Deployment"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"mutate": {
|
||||
"patchStrategicMerge": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"retention": "{{ regex_replace_all('([0-9])([0-9])', '{{ @ }}', '${1}0') }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`),
|
||||
documentRaw: []byte(`{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"name": "busybox",
|
||||
"labels": {
|
||||
"app": "busybox",
|
||||
"retention": "days_37"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 3,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "busybox"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "busybox"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"image": "busybox:1.28",
|
||||
"name": "busybox",
|
||||
"command": [
|
||||
"sleep",
|
||||
"9999"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
want: [][]byte{
|
||||
[]byte(`{"op":"replace","path":"/metadata/labels/retention","value":"days_30"}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "regex_replace_with_slash",
|
||||
policyRaw: []byte(`{
|
||||
"apiVersion": "kyverno.io/v1",
|
||||
"kind": "ClusterPolicy",
|
||||
"metadata": {
|
||||
"name": "regex-replace-all-demo"
|
||||
},
|
||||
"spec": {
|
||||
"background": false,
|
||||
"rules": [
|
||||
{
|
||||
"name": "retention-adjust",
|
||||
"match": {
|
||||
"any": [
|
||||
{
|
||||
"resources": {
|
||||
"kinds": [
|
||||
"Deployment"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"mutate": {
|
||||
"patchStrategicMerge": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"corp.com/retention": "{{ regex_replace_all('([0-9])([0-9])', '{{ @ }}', '${1}0') }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`),
|
||||
documentRaw: []byte(`{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"name": "busybox",
|
||||
"labels": {
|
||||
"app": "busybox",
|
||||
"corp.com/retention": "days_37"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 3,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "busybox"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "busybox"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"image": "busybox:1.28",
|
||||
"name": "busybox",
|
||||
"command": [
|
||||
"sleep",
|
||||
"9999"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
want: [][]byte{
|
||||
[]byte(`{"op":"replace","path":"/metadata/labels/corp.com~1retention","value":"days_30"}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "regex_replace_with_hyphen",
|
||||
policyRaw: []byte(`{
|
||||
"apiVersion": "kyverno.io/v1",
|
||||
"kind": "ClusterPolicy",
|
||||
"metadata": {
|
||||
"name": "regex-replace-all-demo"
|
||||
},
|
||||
"spec": {
|
||||
"background": false,
|
||||
"rules": [
|
||||
{
|
||||
"name": "retention-adjust",
|
||||
"match": {
|
||||
"any": [
|
||||
{
|
||||
"resources": {
|
||||
"kinds": [
|
||||
"Deployment"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"mutate": {
|
||||
"patchStrategicMerge": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"corp-retention": "{{ regex_replace_all('([0-9])([0-9])', '{{ @ }}', '${1}0') }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`),
|
||||
documentRaw: []byte(`{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"name": "busybox",
|
||||
"labels": {
|
||||
"app": "busybox",
|
||||
"corp-retention": "days_37"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 3,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "busybox"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "busybox"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"image": "busybox:1.28",
|
||||
"name": "busybox",
|
||||
"command": [
|
||||
"sleep",
|
||||
"9999"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
want: [][]byte{
|
||||
[]byte(`{"op":"replace","path":"/metadata/labels/corp-retention","value":"days_30"}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "to_upper_with_hyphen",
|
||||
policyRaw: []byte(`{
|
||||
"apiVersion": "kyverno.io/v1",
|
||||
"kind": "ClusterPolicy",
|
||||
"metadata": {
|
||||
"name": "to-upper-demo"
|
||||
},
|
||||
"spec": {
|
||||
"rules": [
|
||||
{
|
||||
"name": "format-deploy-zone",
|
||||
"match": {
|
||||
"any": [
|
||||
{
|
||||
"resources": {
|
||||
"kinds": [
|
||||
"Deployment"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"mutate": {
|
||||
"patchStrategicMerge": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"deploy-zone": "{{ to_upper('{{@}}') }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`),
|
||||
documentRaw: []byte(`{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"name": "busybox",
|
||||
"labels": {
|
||||
"app": "busybox",
|
||||
"deploy-zone": "eu-central-1"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 3,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "busybox"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "busybox"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"image": "busybox:1.28",
|
||||
"name": "busybox",
|
||||
"command": [
|
||||
"sleep",
|
||||
"9999"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
want: [][]byte{
|
||||
[]byte(`{"op":"replace","path":"/metadata/labels/deploy-zone","value":"EU-CENTRAL-1"}`),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Parse policy document.
|
||||
var policy kyverno.ClusterPolicy
|
||||
if err := json.Unmarshal(tt.policyRaw, &policy); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Parse resource document.
|
||||
resource, err := utils.ConvertToUnstructured(tt.documentRaw)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertToUnstructured() error = %v", err)
|
||||
}
|
||||
|
||||
// Create JSON context and add the resource.
|
||||
ctx := context.NewContext()
|
||||
err = ctx.AddResource(resource.Object)
|
||||
if err != nil {
|
||||
t.Fatalf("ctx.AddResource() error = %v", err)
|
||||
}
|
||||
|
||||
// Create policy context.
|
||||
policyContext := &PolicyContext{
|
||||
Policy: &policy,
|
||||
JSONContext: ctx,
|
||||
NewResource: *resource,
|
||||
}
|
||||
|
||||
// Mutate and make sure that we got the expected amount of rules.
|
||||
patches := Mutate(policyContext).GetPatches()
|
||||
if !reflect.DeepEqual(patches, tt.want) {
|
||||
t.Errorf("Mutate() got patches %s, expected %s", patches, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch/v5"
|
||||
commonAnchor "github.com/kyverno/kyverno/pkg/engine/anchor"
|
||||
"github.com/kyverno/kyverno/pkg/logging"
|
||||
|
@ -72,28 +68,3 @@ func GetAnchorsFromMap(anchorsMap map[string]interface{}) map[string]interface{}
|
|||
|
||||
return result
|
||||
}
|
||||
|
||||
func JsonPointerToJMESPath(jsonPointer string) string {
|
||||
var sb strings.Builder
|
||||
tokens := strings.Split(jsonPointer, "/")
|
||||
i := 0
|
||||
for _, t := range tokens {
|
||||
if t == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := strconv.Atoi(t); err == nil {
|
||||
sb.WriteString(fmt.Sprintf("[%s]", t))
|
||||
continue
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
sb.WriteString(".")
|
||||
}
|
||||
|
||||
sb.WriteString(t)
|
||||
i++
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
|
|
@ -27,11 +27,3 @@ func TestGetAnchorsFromMap_ThereAreNoAnchors(t *testing.T) {
|
|||
actualMap := GetAnchorsFromMap(unmarshalled)
|
||||
assert.Equal(t, len(actualMap), 0)
|
||||
}
|
||||
|
||||
func Test_JsonPointerToJMESPath(t *testing.T) {
|
||||
assert.Equal(t, "a.b.c[1].d", JsonPointerToJMESPath("a/b/c/1//d"))
|
||||
assert.Equal(t, "a.b.c[1].d", JsonPointerToJMESPath("/a/b/c/1/d"))
|
||||
assert.Equal(t, "a.b.c[1].d", JsonPointerToJMESPath("/a/b/c/1/d/"))
|
||||
assert.Equal(t, "a[1].b.c[1].d", JsonPointerToJMESPath("a/1/b/c/1/d"))
|
||||
assert.Equal(t, "a[1].b.c[1].d[2]", JsonPointerToJMESPath("/a/1/b/c/1/d/2/"))
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/kyverno/kyverno/pkg/engine/context"
|
||||
jsonUtils "github.com/kyverno/kyverno/pkg/engine/jsonutils"
|
||||
"github.com/kyverno/kyverno/pkg/engine/operator"
|
||||
"github.com/kyverno/kyverno/pkg/utils/jsonpointer"
|
||||
)
|
||||
|
||||
var RegexVariables = regexp.MustCompile(`(?:^|[^\\])(\{\{(?:\{[^{}]*\}|[^{}])*\}\})`)
|
||||
|
@ -352,13 +353,11 @@ func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface, vr Var
|
|||
if _, err := ctx.Query("target"); err != nil {
|
||||
pathPrefix = "request.object"
|
||||
}
|
||||
path := getJMESPath(data.Path)
|
||||
var val string
|
||||
if strings.HasPrefix(path, "[") {
|
||||
val = fmt.Sprintf("%s%s", pathPrefix, path)
|
||||
} else {
|
||||
val = fmt.Sprintf("%s.%s", pathPrefix, path)
|
||||
}
|
||||
|
||||
// Convert path to JMESPath for current identifier.
|
||||
// Skip 2 elements (e.g. mutate.overlay | validate.pattern) plus "foreach" if it is part of the pointer.
|
||||
// Prefix the pointer with pathPrefix.
|
||||
val := jsonpointer.ParsePath(data.Path).SkipPast("foreach").SkipN(2).Prepend(strings.Split(pathPrefix, ".")...).JMESPath()
|
||||
|
||||
variable = strings.Replace(variable, "@", val, -1)
|
||||
}
|
||||
|
@ -421,20 +420,6 @@ func IsDeleteRequest(ctx context.EvalInterface) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
var regexPathDigit = regexp.MustCompile(`\.?([\d])\.?`)
|
||||
|
||||
// getJMESPath converts path to JMESPath format
|
||||
func getJMESPath(rawPath string) string {
|
||||
tokens := strings.Split(rawPath, "/")[3:] // skip "/" + 2 elements (e.g. mutate.overlay | validate.pattern)
|
||||
if strings.Contains(rawPath, "foreach") {
|
||||
tokens = strings.Split(rawPath, "/")[5:] // skip "/" + 4 elements (e.g. mutate.foreach/list/overlay | validate.mutate.foreach/list/pattern)
|
||||
}
|
||||
path := strings.Join(tokens, ".")
|
||||
b := regexPathDigit.ReplaceAll([]byte(path), []byte("[$1]."))
|
||||
result := strings.Trim(string(b), ".")
|
||||
return result
|
||||
}
|
||||
|
||||
func substituteVarInPattern(prefix, pattern, variable string, value interface{}) (string, error) {
|
||||
var stringToSubstitute string
|
||||
|
||||
|
|
|
@ -1177,15 +1177,3 @@ func Test_ReplacingEscpNestedVariableWhenDeleting(t *testing.T) {
|
|||
|
||||
assert.Equal(t, fmt.Sprintf("%v", pattern), "{{request.object.metadata.annotations.target}}")
|
||||
}
|
||||
|
||||
func Test_getJMESPath(t *testing.T) {
|
||||
assert.Equal(t, "spec.containers[0]", getJMESPath("/validate/pattern/spec/containers/0"))
|
||||
assert.Equal(t, "spec.containers[0].volumes[1]", getJMESPath("/validate/pattern/spec/containers/0/volumes/1"))
|
||||
assert.Equal(t, "[0]", getJMESPath("/mutate/overlay/0"))
|
||||
}
|
||||
|
||||
func Test_getJMESPathForForeach(t *testing.T) {
|
||||
assert.Equal(t, "spec.containers[0]", getJMESPath("/validate/foreach/0/pattern/spec/containers/0"))
|
||||
assert.Equal(t, "spec.containers[0].volumes[1]", getJMESPath("/validate/foreach/0/pattern/spec/containers/0/volumes/1"))
|
||||
assert.Equal(t, "[0]", getJMESPath("/mutate/foreach/0/overlay/0"))
|
||||
}
|
||||
|
|
244
pkg/utils/jsonpointer/pointer.go
Normal file
244
pkg/utils/jsonpointer/pointer.go
Normal file
|
@ -0,0 +1,244 @@
|
|||
package jsonpointer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"k8s.io/utils/strings/slices"
|
||||
)
|
||||
|
||||
// Pointer is a JSON pointer that can be retrieved as either as a RFC6901 string or as a JMESPath formatted string.
|
||||
type Pointer []string
|
||||
|
||||
// unquoted identifiers must only contain these characters.
|
||||
var unquotedFirstCharRangeTable = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{Lo: '(', Hi: '(', Stride: 1}, // Special non-standard. Used by policy documents for matching attributes.
|
||||
{Lo: 'A', Hi: 'Z', Stride: 1},
|
||||
{Lo: '_', Hi: '_', Stride: 1},
|
||||
{Lo: 'a', Hi: 'z', Stride: 1},
|
||||
},
|
||||
}
|
||||
|
||||
// unquoted identifiers can contain any combination of these runes.
|
||||
var unquotedStringRangeTable = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{Lo: ')', Hi: ')', Stride: 1}, // Special non-standard. Used by policy documents for matching attributes.
|
||||
{Lo: '0', Hi: '9', Stride: 1},
|
||||
{Lo: 'A', Hi: 'Z', Stride: 1},
|
||||
{Lo: '_', Hi: '_', Stride: 1},
|
||||
{Lo: 'a', Hi: 'z', Stride: 1},
|
||||
},
|
||||
}
|
||||
|
||||
// a quoted identifier can contain any of these characters as is.
|
||||
var unescapedCharRangeTable = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{Lo: 0x20, Hi: 0x21, Stride: 1},
|
||||
{Lo: 0x23, Hi: 0x5B, Stride: 1},
|
||||
{Lo: 0x5D, Hi: 0x1EFF, Stride: 1},
|
||||
},
|
||||
R32: []unicode.Range32{
|
||||
{Lo: 0x1000, Hi: 0x10FFFF, Stride: 1},
|
||||
},
|
||||
LatinOffset: 0x1EFF - unicode.MaxLatin1,
|
||||
}
|
||||
|
||||
// some special characters must be escaped to be possible to use inside a quoted identifier.
|
||||
var escapeCharMap = map[rune]string{
|
||||
'"': `\"`, // quotation mark
|
||||
'\\': `\\`, // reverse solidus
|
||||
'/': `\/`, // solidus
|
||||
'\b': `\b`, // backspace
|
||||
'\f': `\f`, // form feed
|
||||
'\n': `\n`, // line feed
|
||||
'\r': `\r`, // carriage return
|
||||
'\t': `\t`, // tab
|
||||
}
|
||||
|
||||
const initialCapacity = 10 // pointers should start with a non-zero capacity to lower the amount of re-allocations done by append.
|
||||
|
||||
// New will return an empty Pointer.
|
||||
func New() Pointer {
|
||||
return make([]string, 0, initialCapacity)
|
||||
}
|
||||
|
||||
// Parse will parse the string as a JSON pointer according to RFC 6901.
|
||||
func Parse(s string) Pointer {
|
||||
pointer := New()
|
||||
|
||||
replacer := strings.NewReplacer("~1", "/", "~0", "~", `\\`, `\`, `\"`, `"`)
|
||||
|
||||
for _, component := range strings.FieldsFunc(s, func(r rune) bool {
|
||||
return r == '/'
|
||||
}) {
|
||||
pointer = append(pointer, replacer.Replace(component))
|
||||
}
|
||||
|
||||
return pointer
|
||||
}
|
||||
|
||||
// ParsePath will parse the raw path and return it in the form of a Pointer.
|
||||
func ParsePath(rawPath string) Pointer {
|
||||
// Start with a slice with a non-zero capacity to avoid reallocation for most paths.
|
||||
pointer := New()
|
||||
|
||||
// Use a string builder and a flush function to append path components to the slice.
|
||||
sb := strings.Builder{}
|
||||
|
||||
flush := func() {
|
||||
s := sb.String()
|
||||
if s != "" {
|
||||
pointer = append(pointer, s)
|
||||
}
|
||||
sb.Reset()
|
||||
}
|
||||
|
||||
var pos int
|
||||
var escaped, quoted bool
|
||||
|
||||
for i, width := 0, 0; i <= len(rawPath); i += width {
|
||||
var r rune
|
||||
r, width = utf8.DecodeRuneInString(rawPath[i:])
|
||||
if r == utf8.RuneError && width == 1 {
|
||||
break
|
||||
}
|
||||
|
||||
switch {
|
||||
case escaped: // previous character was a backslash.
|
||||
sb.WriteRune(r)
|
||||
escaped = !escaped
|
||||
case r == '\\': // escape character
|
||||
escaped = !escaped
|
||||
case r == '"': // quoted strings
|
||||
if quoted {
|
||||
s, _ := strconv.Unquote(rawPath[pos : i+width])
|
||||
sb.WriteString(s)
|
||||
}
|
||||
quoted = !quoted
|
||||
case r == '/' && !quoted:
|
||||
flush()
|
||||
case r == utf8.RuneError: // end of string
|
||||
flush()
|
||||
return pointer
|
||||
default:
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
|
||||
pos = i + width
|
||||
}
|
||||
|
||||
// This is unreachable but we must return something.
|
||||
return pointer
|
||||
}
|
||||
|
||||
// JMESPath will return the Pointer in the form of a JMESPath string.
|
||||
func (p Pointer) JMESPath() string {
|
||||
sb := strings.Builder{}
|
||||
|
||||
for _, component := range p {
|
||||
// Components that are valid unsigned integers are treated as indices.
|
||||
if _, err := strconv.ParseUint(component, 10, 64); err == nil {
|
||||
sb.WriteRune('[')
|
||||
sb.WriteString(component)
|
||||
sb.WriteRune(']')
|
||||
continue
|
||||
}
|
||||
|
||||
// Write a dot before we write anything, as long as buffer is not empty.
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteRune('.')
|
||||
}
|
||||
|
||||
// If the component starts with a character that is valid as an initial character for an identifier
|
||||
// and the remaining characters are also valid for an unquoted identifier then we can append it to
|
||||
// the JMESPath as is.
|
||||
if ch, _ := utf8.DecodeRuneInString(component); unicode.Is(unquotedFirstCharRangeTable, ch) &&
|
||||
strings.IndexFunc(component, func(r rune) bool {
|
||||
return !unicode.Is(unquotedStringRangeTable, r)
|
||||
}) == -1 {
|
||||
sb.WriteString(component)
|
||||
continue
|
||||
}
|
||||
|
||||
// The component contains characters that are not allowed for unquoted identifiers, so we need to take some extra
|
||||
// steps to ensure that it's a valid, quoted identifier.
|
||||
sb.WriteRune('"')
|
||||
for _, r := range component {
|
||||
// Any character in the range table of allowed runes can be written as is.
|
||||
if unicode.Is(unescapedCharRangeTable, r) {
|
||||
sb.WriteRune(r)
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert special characters to their escaped sequence.
|
||||
if escaped, ok := escapeCharMap[r]; ok {
|
||||
sb.WriteString(escaped)
|
||||
continue
|
||||
}
|
||||
|
||||
// All other characters must be written as unicode escape sequences ay 16 bits a piece.
|
||||
if i := utf8.RuneLen(r); i <= 2 {
|
||||
// Rune is 1 or 2 bytes.
|
||||
_, _ = fmt.Fprintf(&sb, "\\u%04x", r&0xffff)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(&sb, "\\u%04x", r&0xffff)
|
||||
_, _ = fmt.Fprintf(&sb, "\\u%04x", r>>16)
|
||||
}
|
||||
}
|
||||
sb.WriteRune('"')
|
||||
}
|
||||
|
||||
// Return the JMESPath.
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// String will return the pointer as a string (RFC6901).
|
||||
func (p Pointer) String() string {
|
||||
sb := strings.Builder{}
|
||||
|
||||
replacer := strings.NewReplacer("~", "~0", "/", "~1", `\`, `\\`, `"`, `\"`)
|
||||
|
||||
for _, component := range p {
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteRune('/')
|
||||
}
|
||||
|
||||
_, _ = replacer.WriteString(&sb, component)
|
||||
}
|
||||
|
||||
// Return the pointer.
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// Append will return a Pointer with the strings appended.
|
||||
func (p Pointer) Append(s ...string) Pointer {
|
||||
return append(p, s...)
|
||||
}
|
||||
|
||||
// Prepend will return a Pointer prefixed with the specified strings.
|
||||
func (p Pointer) Prepend(s ...string) Pointer {
|
||||
return append(s, p...)
|
||||
}
|
||||
|
||||
// AppendPath will parse the string as a JSON pointer and return a new pointer.
|
||||
func (p Pointer) AppendPath(s string) Pointer {
|
||||
return append(p, ParsePath(s)...)
|
||||
}
|
||||
|
||||
// SkipN will return a new Pointer where the first N element are stripped.
|
||||
func (p Pointer) SkipN(n int) Pointer {
|
||||
if n > len(p)-1 {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return p[n:]
|
||||
}
|
||||
|
||||
// SkipPast will return a new Pointer where every element upto and including the specified string has been stripped off.
|
||||
func (p Pointer) SkipPast(s string) Pointer {
|
||||
return p[slices.Index(p, s)+1:]
|
||||
}
|
227
pkg/utils/jsonpointer/pointer_test.go
Normal file
227
pkg/utils/jsonpointer/pointer_test.go
Normal file
|
@ -0,0 +1,227 @@
|
|||
package jsonpointer
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParsePath(t *testing.T) {
|
||||
type args struct {
|
||||
rawPath string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want Pointer
|
||||
}{
|
||||
{
|
||||
name: "plain",
|
||||
args: args{
|
||||
rawPath: "a/b/c",
|
||||
},
|
||||
want: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "hyphen",
|
||||
args: args{
|
||||
rawPath: "a/b-b/c",
|
||||
},
|
||||
want: []string{"a", "b-b", "c"},
|
||||
},
|
||||
{
|
||||
name: "quotes",
|
||||
args: args{
|
||||
rawPath: `a/"b/b"/c`,
|
||||
},
|
||||
want: []string{"a", "b/b", "c"},
|
||||
},
|
||||
{
|
||||
name: "escaped_slash",
|
||||
args: args{
|
||||
rawPath: `a/b\/b/c`,
|
||||
},
|
||||
want: []string{"a", "b/b", "c"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ParsePath(tt.args.rawPath); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ParsePath() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPointer_Append(t *testing.T) {
|
||||
type args struct {
|
||||
s []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
p Pointer
|
||||
args args
|
||||
want Pointer
|
||||
}{
|
||||
{
|
||||
p: []string{"a", "b"},
|
||||
args: args{
|
||||
s: []string{"c", "d"},
|
||||
},
|
||||
want: []string{"a", "b", "c", "d"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.p.Append(tt.args.s...); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Append() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPointer_AppendPath(t *testing.T) {
|
||||
type args struct {
|
||||
s string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
p Pointer
|
||||
args args
|
||||
want Pointer
|
||||
}{
|
||||
{
|
||||
name: "",
|
||||
p: []string{"a", "b", "c"},
|
||||
args: args{
|
||||
s: `d/e\/e/f`,
|
||||
},
|
||||
want: []string{"a", "b", "c", "d", "e/e", "f"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.p.AppendPath(tt.args.s); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("AppendPath() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPointer_JMESPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
p Pointer
|
||||
want string
|
||||
}{
|
||||
{
|
||||
p: []string{"a", "b", "c", "3", "e/e", "f"},
|
||||
want: `a.b.c[3]."e/e".f`,
|
||||
},
|
||||
{
|
||||
p: []string{"a", "b", "c", "3", "e/e", "f"},
|
||||
want: `a.b.c[3]."e/e".f`,
|
||||
},
|
||||
{
|
||||
name: "hangul",
|
||||
p: []string{"a", "바나나", "c", "3", "e/e", "f"},
|
||||
want: `a."바나나".c[3]."e/e".f`,
|
||||
},
|
||||
{
|
||||
name: "tab",
|
||||
p: []string{"a", "a\tb", "c"},
|
||||
want: `a."a\tb".c`,
|
||||
},
|
||||
{
|
||||
name: "bell",
|
||||
p: []string{"a", "a\aa", "c", "3", "e/e", "f"},
|
||||
want: `a."a\u0007a".c[3]."e/e".f`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.p.JMESPath(); got != tt.want {
|
||||
t.Errorf("JMESPath() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPointer_String(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
p Pointer
|
||||
want string
|
||||
}{
|
||||
{
|
||||
p: []string{"a", "b", "c"},
|
||||
want: "a/b/c",
|
||||
},
|
||||
{
|
||||
p: []string{"a", "b/b", "c~c"},
|
||||
want: `a/b~1b/c~0c`,
|
||||
},
|
||||
{
|
||||
p: []string{"a", `b\b`, `c"c`},
|
||||
want: `a/b\\b/c\"c`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.p.String(); got != tt.want {
|
||||
t.Errorf("String() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPointer_Prepend(t *testing.T) {
|
||||
type args struct {
|
||||
s []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
p Pointer
|
||||
args args
|
||||
want Pointer
|
||||
}{
|
||||
{
|
||||
p: []string{"c", "d", "e"},
|
||||
args: args{
|
||||
s: []string{"a", "b"},
|
||||
},
|
||||
want: []string{"a", "b", "c", "d", "e"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.p.Prepend(tt.args.s...); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Prepend() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
type args struct {
|
||||
s string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want Pointer
|
||||
}{
|
||||
{
|
||||
args: args{
|
||||
s: "a/b~1c/~0d",
|
||||
},
|
||||
want: []string{"a", "b/c", "~d"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := Parse(tt.args.s); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Parse() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue