-
Notifications
You must be signed in to change notification settings - Fork 420
/
jsonpath.go
148 lines (131 loc) · 4.36 KB
/
jsonpath.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package template
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"reflect"
"regexp"
"strings"
"k8s.io/client-go/util/jsonpath"
)
var (
// tektonVar captures strings that are enclosed in $()
tektonVar = regexp.MustCompile(`\$\(?([^\)]+)\)`)
// jsonRegexp is a regular expression for JSONPath expressions
// with or without the enclosing {} and the leading . inside the curly
// braces e.g. 'a.b' or '.a.b' or '{a.b}' or '{.a.b}'
jsonRegexp = regexp.MustCompile(`^\{\.?([^{}]+)\}$|^\.?([^{}]+)$`)
)
// ParseJSONPath extracts a subset of the given JSON input
// using the provided JSONPath expression.
func ParseJSONPath(input interface{}, expr string) (string, error) {
j := jsonpath.New("").AllowMissingKeys(false)
buf := new(bytes.Buffer)
//First turn the expression into fully valid JSONPath
expr, err := TektonJSONPathExpression(expr)
if err != nil {
return "", err
}
if err := j.Parse(expr); err != nil {
return "", err
}
fullResults, err := j.FindResults(input)
if err != nil {
return "", err
}
for _, r := range fullResults {
if err := printResults(buf, r); err != nil {
return "", err
}
}
return buf.String(), nil
}
// PrintResults writes the results into writer
func printResults(wr io.Writer, values []reflect.Value) error {
results, err := getResults(values)
if err != nil {
return fmt.Errorf("error getting values for jsonpath results: %w", err)
}
if _, err := wr.Write(results); err != nil {
return err
}
return nil
}
func getResults(values []reflect.Value) ([]byte, error) {
if len(values) == 1 {
v := values[0]
t := reflect.TypeOf(v.Interface())
switch {
case t == nil:
return []byte("null"), nil
case t.Kind() == reflect.String:
b, err := json.Marshal(v.Interface())
if err != nil {
return nil, fmt.Errorf("unable to marshal string value %v: %v", v, err)
}
// A valid json string is surrounded by quotation marks; we are using this function to
// create a representation of the json value that can be embedded in a CRD definition and
// we want to leave it up to the user if they want the surrounding quotation marks or not.
return b[1 : len(b)-1], nil
default:
return json.Marshal(v.Interface())
}
}
// More than one result - we need to return a JSON array response
results := []interface{}{}
for _, r := range values {
t := reflect.TypeOf(r.Interface())
if t == nil {
results = append(results, nil)
} else {
// No special case for string here unlike above since its going to be part of a JSON array
results = append(results, r.Interface())
}
}
return json.Marshal(results)
}
// TektonJSONPathExpression returns a valid JSONPath expression. It accepts
// a "RelaxedJSONPath" expression that is wrapped in the Tekton variable
// interpolation syntax i.e. $(). RelaxedJSONPath expressions can optionally
// omit the leading curly braces '{}' and '.'
func TektonJSONPathExpression(expr string) (string, error) {
if !isTektonExpr(expr) {
return "", errors.New("expression not wrapped in $()")
}
unwrapped := strings.TrimSuffix(strings.TrimPrefix(expr, "$("), ")")
return relaxedJSONPathExpression(unwrapped)
}
// RelaxedJSONPathExpression attempts to be flexible with JSONPath expressions, it accepts:
// * metadata.name (no leading '.' or curly braces '{...}'
// * {metadata.name} (no leading '.')
// * .metadata.name (no curly braces '{...}')
// * {.metadata.name} (complete expression)
// And transforms them all into a valid jsonpath expression:
// {.metadata.name}
// This function has been copied as-is from
// https://github.com/kubernetes/kubectl/blob/c273777957bd657233cf867892fb061a6498dab8/pkg/cmd/get/customcolumn.go#L47
func relaxedJSONPathExpression(pathExpression string) (string, error) {
if len(pathExpression) == 0 {
return pathExpression, nil
}
submatches := jsonRegexp.FindStringSubmatch(pathExpression)
if submatches == nil {
return "", fmt.Errorf("unexpected path string, expected a 'name1.name2' or '.name1.name2' or '{name1.name2}' or '{.name1.name2}'")
}
if len(submatches) != 3 {
return "", fmt.Errorf("unexpected submatch list: %v", submatches)
}
var fieldSpec string
if len(submatches[1]) != 0 {
fieldSpec = submatches[1]
} else {
fieldSpec = submatches[2]
}
return fmt.Sprintf("{.%s}", fieldSpec), nil
}
// IsTektonExpr returns true if the expr is wrapped in $()
func isTektonExpr(expr string) bool {
return tektonVar.MatchString(expr)
}