Skip to content

Commit de6ee0a

Browse files
author
Marko Mikulicic
committed
Add jsonnet lens
1 parent 6905dcc commit de6ee0a

File tree

6 files changed

+281
-1
lines changed

6 files changed

+281
-1
lines changed

cmd/knot8/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ func checkFields(fields Fields) error {
411411
func openFields(paths []string, schema string) (*ManifestSet, error) {
412412
var (
413413
manifests Manifests
414-
fields Fields
414+
fields Fields
415415
)
416416
if len(paths) == 0 {
417417
paths = []string{"-"}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.14
55
require (
66
github.com/alecthomas/kong v0.2.11
77
github.com/go-openapi/jsonpointer v0.19.3
8+
github.com/google/go-jsonnet v0.16.0
89
github.com/hashicorp/go-getter v1.4.1
910
github.com/mattn/go-isatty v0.0.12
1011
github.com/mkmik/multierror v0.3.0

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
2424
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2525
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2626
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
27+
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
2728
github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w=
2829
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
2930
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
@@ -42,6 +43,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
4243
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
4344
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
4445
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
46+
github.com/google/go-jsonnet v0.16.0 h1:Nb4EEOp+rdeGGyB1rQ5eisgSAqrTnhf9ip+X6lzZbY0=
47+
github.com/google/go-jsonnet v0.16.0/go.mod h1:sOcuej3UW1vpPTZOr8L7RQimqai1a57bt5j22LzGZCw=
4548
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
4649
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
4750
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@@ -74,7 +77,10 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN
7477
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8=
7578
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
7679
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
80+
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
7781
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
82+
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
83+
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
7884
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
7985
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
8086
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
@@ -92,11 +98,13 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
9298
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
9399
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
94100
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
101+
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
95102
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
96103
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
97104
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
98105
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
99106
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
107+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
100108
github.com/ulikunitz/xz v0.5.5 h1:pFrO0lVpTBXLpYw+pnLj6TbvHuyjXMfjGeCwSqCVwok=
101109
github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
102110
github.com/vmware-labs/go-yaml-edit v0.3.0 h1:ZfMz110UY6Ke0XoYpoKEWOiKc4sAAlwP/84bQqT59U8=
@@ -141,12 +149,14 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
141149
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
142150
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
143151
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
152+
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
144153
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
145154
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
146155
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
147156
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
148157
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
149158
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
159+
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
150160
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
151161
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
152162
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -192,9 +202,11 @@ google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ij
192202
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
193203
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
194204
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
205+
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
195206
gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
196207
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
197208
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
209+
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
198210
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
199211
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
200212
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

pkg/lensed/jsonnet.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// Copyright 2020 VMware, Inc.
2+
// SPDX-License-Identifier: BSD-2-Clause
3+
4+
package lensed
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"io"
10+
"strconv"
11+
"strings"
12+
13+
"github.com/go-openapi/jsonpointer"
14+
"github.com/google/go-jsonnet"
15+
"github.com/google/go-jsonnet/ast"
16+
"github.com/vmware-labs/go-yaml-edit/splice"
17+
"golang.org/x/text/transform"
18+
)
19+
20+
// Jsonnet implements the "jsonnet" lens.
21+
//
22+
// The jsonnet lens implementation is still in its early stages, it only supports:
23+
// 1. double quoted string scalars
24+
// 2. single level ~{"foo":"bar", "baz":"quz"} matchers
25+
type Jsonnet struct{}
26+
27+
// Apply implements the Lens interface.
28+
func (Jsonnet) Apply(src []byte, vals []Setter) ([]byte, error) {
29+
var ops []splice.Op
30+
31+
root, err := jsonnet.SnippetToAST("-", string(src))
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
for _, v := range vals {
37+
p, err := jsonpointer.New(v.Pointer)
38+
if err != nil {
39+
return nil, err
40+
}
41+
path := p.DecodedTokens()
42+
43+
n, err := findJsonnetNode(root, path)
44+
if err != nil {
45+
return nil, err
46+
}
47+
start, err := lineColToPos(src, n.Loc().Begin.Line, n.Loc().Begin.Column+1)
48+
if err != nil {
49+
return nil, err
50+
}
51+
end, err := lineColToPos(src, n.Loc().End.Line, n.Loc().End.Column+1)
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
var oldval []byte
57+
switch n := n.(type) {
58+
case *ast.LiteralString:
59+
oldval = []byte(n.Value)
60+
default:
61+
return nil, fmt.Errorf("unhandled node type %T", n)
62+
}
63+
64+
newval, err := v.Value.Transform(oldval)
65+
if err != nil {
66+
return nil, err
67+
}
68+
69+
ops = append(ops, splice.Span(start, end).With(fmt.Sprintf("%q", string(newval))))
70+
}
71+
b, _, err := transform.Bytes(splice.T(ops...), src)
72+
return b, err
73+
}
74+
75+
func findJsonnetNode(root ast.Node, path []string) (ast.Node, error) {
76+
if len(path) == 0 {
77+
return root, nil
78+
}
79+
p := path[0]
80+
81+
switch n := root.(type) {
82+
case *ast.DesugaredObject:
83+
for _, f := range n.Fields {
84+
if k, ok := f.Name.(*ast.LiteralString); !ok {
85+
continue
86+
} else if p == k.Value {
87+
return findJsonnetNode(f.Body, path[1:])
88+
}
89+
}
90+
case *ast.Array:
91+
var exprs []ast.Node
92+
for _, e := range n.Elements {
93+
exprs = append(exprs, e.Expr)
94+
}
95+
96+
e, err := matchJsonnetArrayItem(p, exprs)
97+
if err != nil {
98+
return nil, err
99+
}
100+
return findJsonnetNode(e, path[1:])
101+
case *ast.Local:
102+
return findJsonnetNode(n.Body, path)
103+
default:
104+
return nil, fmt.Errorf("unsupported jsonnet node type: %T", n)
105+
}
106+
107+
return nil, fmt.Errorf("cannot find field %q in object", p)
108+
}
109+
110+
func lineColToPos(src []byte, line, column int) (int, error) {
111+
l, c := 1, 1
112+
for i, r := range string(src) {
113+
c++
114+
if r == '\n' {
115+
l++
116+
c = 1
117+
}
118+
if l == line && c == column {
119+
return i, nil
120+
}
121+
}
122+
return 0, io.EOF
123+
}
124+
125+
func matchJsonnetArrayItem(p string, exprs []ast.Node) (ast.Node, error) {
126+
if strings.HasPrefix(p, "~{") {
127+
var m map[string]string
128+
if err := json.Unmarshal([]byte(p[1:]), &m); err != nil {
129+
return nil, err
130+
}
131+
nodes, err := filterJsonnetArrayItems(exprs, isTreeSubsetPred(m))
132+
if err != nil {
133+
return nil, err
134+
}
135+
if got, want := len(nodes), 1; got != want {
136+
return nil, fmt.Errorf("bad number of subtree matches: got=%d, want=%d", got, want)
137+
}
138+
return nodes[0], nil
139+
}
140+
i, err := strconv.Atoi(p)
141+
if err != nil {
142+
return nil, err
143+
}
144+
return exprs[i], nil
145+
}
146+
147+
type jsonnetNodePredicate func(ast.Node) bool
148+
149+
func isTreeSubsetPred(a map[string]string) jsonnetNodePredicate {
150+
return func(b ast.Node) bool {
151+
return isTreeSubset(a, b)
152+
}
153+
}
154+
155+
func isTreeSubset(a map[string]string, b ast.Node) bool {
156+
for k, v := range a {
157+
if !jsonnetObjectHasField(b, k, v) {
158+
return false
159+
}
160+
}
161+
return true
162+
}
163+
164+
func jsonnetObjectHasField(b ast.Node, name, value string) bool {
165+
if o, ok := b.(*ast.DesugaredObject); ok {
166+
for _, f := range o.Fields {
167+
if n, ok := f.Name.(*ast.LiteralString); ok && n.Value == name {
168+
v, ok := f.Body.(*ast.LiteralString)
169+
return ok && v.Value == value
170+
}
171+
}
172+
}
173+
return false
174+
}
175+
176+
func filterJsonnetArrayItems(nodes []ast.Node, pred jsonnetNodePredicate) ([]ast.Node, error) {
177+
var res []ast.Node
178+
for _, n := range nodes {
179+
if pred(n) {
180+
res = append(res, n)
181+
}
182+
}
183+
return res, nil
184+
}

pkg/lensed/jsonnet_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2020 VMware, Inc.
2+
// SPDX-License-Identifier: BSD-2-Clause
3+
4+
package lensed
5+
6+
import (
7+
"fmt"
8+
"testing"
9+
)
10+
11+
func TestJsonnet(t *testing.T) {
12+
testCases := []struct {
13+
src string
14+
t []Mapping
15+
want string
16+
}{
17+
{
18+
`{foo:{bar:"xyz"}}`,
19+
[]Mapping{
20+
{"~(jsonnet)/foo/bar", "abc"},
21+
},
22+
`{foo:{bar:"abc"}}`,
23+
},
24+
{
25+
`{foo:
26+
{bar:"xyz"}}`,
27+
[]Mapping{
28+
{"~(jsonnet)/foo/bar", "abc"},
29+
},
30+
`{foo:
31+
{bar:"abc"}}`,
32+
},
33+
{
34+
`{foo:["xyz"]}`,
35+
[]Mapping{
36+
{"~(jsonnet)/foo/0", "abc"},
37+
},
38+
`{foo:["abc"]}`,
39+
},
40+
{
41+
`{foo:[{bar:"xyz"}]}`,
42+
[]Mapping{
43+
{"~(jsonnet)/foo/0/bar", "abc"},
44+
},
45+
`{foo:[{bar:"abc"}]}`,
46+
},
47+
{
48+
`{foo:[{name:"wrong",bar:"ppp"},{name:"right",bar:"xyz"}]}`,
49+
[]Mapping{
50+
{`~(jsonnet)/foo/~{"name":"right"}/bar`, "abc"},
51+
},
52+
`{foo:[{name:"wrong",bar:"ppp"},{name:"right",bar:"abc"}]}`,
53+
},
54+
{
55+
`{local a="b",foo:{bar:"xyz"}}`,
56+
[]Mapping{
57+
{"~(jsonnet)/foo/bar", "abc"},
58+
},
59+
`{local a="b",foo:{bar:"abc"}}`,
60+
},
61+
{
62+
`local a=1; {foo:{bar:"xyz"}}`,
63+
[]Mapping{
64+
{"~(jsonnet)/foo/bar", "abc"},
65+
},
66+
`local a=1; {foo:{bar:"abc"}}`,
67+
},
68+
}
69+
70+
for i, tc := range testCases {
71+
t.Run(fmt.Sprint(i), func(t *testing.T) {
72+
got, err := Default.Apply([]byte(tc.src), tc.t)
73+
if err != nil {
74+
t.Fatal(err)
75+
}
76+
77+
if got, want := string(got), tc.want; got != want {
78+
t.Errorf("got: %q, want: %q", got, want)
79+
}
80+
})
81+
}
82+
}

pkg/lensed/lensed.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ var (
2020
"regexp": RegexpLens{},
2121
"ociImageRef": OCIImageRef{}, // deprecated
2222
"oci": OCIImageRef{},
23+
"jsonnet": Jsonnet{},
2324
}
2425
)
2526

0 commit comments

Comments
 (0)