Skip to content

Commit

Permalink
Add jsonnet lens
Browse files Browse the repository at this point in the history
  • Loading branch information
Marko Mikulicic committed Jul 27, 2020
1 parent 6905dcc commit de6ee0a
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 1 deletion.
2 changes: 1 addition & 1 deletion cmd/knot8/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ func checkFields(fields Fields) error {
func openFields(paths []string, schema string) (*ManifestSet, error) {
var (
manifests Manifests
fields Fields
fields Fields
)
if len(paths) == 0 {
paths = []string{"-"}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.14
require (
github.com/alecthomas/kong v0.2.11
github.com/go-openapi/jsonpointer v0.19.3
github.com/google/go-jsonnet v0.16.0
github.com/hashicorp/go-getter v1.4.1
github.com/mattn/go-isatty v0.0.12
github.com/mkmik/multierror v0.3.0
Expand Down
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
Expand All @@ -42,6 +43,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-jsonnet v0.16.0 h1:Nb4EEOp+rdeGGyB1rQ5eisgSAqrTnhf9ip+X6lzZbY0=
github.com/google/go-jsonnet v0.16.0/go.mod h1:sOcuej3UW1vpPTZOr8L7RQimqai1a57bt5j22LzGZCw=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
Expand Down Expand Up @@ -74,7 +77,10 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
Expand All @@ -92,11 +98,13 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/ulikunitz/xz v0.5.5 h1:pFrO0lVpTBXLpYw+pnLj6TbvHuyjXMfjGeCwSqCVwok=
github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/vmware-labs/go-yaml-edit v0.3.0 h1:ZfMz110UY6Ke0XoYpoKEWOiKc4sAAlwP/84bQqT59U8=
Expand Down Expand Up @@ -141,12 +149,14 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand Down Expand Up @@ -192,9 +202,11 @@ google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ij
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
184 changes: 184 additions & 0 deletions pkg/lensed/jsonnet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Copyright 2020 VMware, Inc.
// SPDX-License-Identifier: BSD-2-Clause

package lensed

import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"

"github.com/go-openapi/jsonpointer"
"github.com/google/go-jsonnet"
"github.com/google/go-jsonnet/ast"
"github.com/vmware-labs/go-yaml-edit/splice"
"golang.org/x/text/transform"
)

// Jsonnet implements the "jsonnet" lens.
//
// The jsonnet lens implementation is still in its early stages, it only supports:
// 1. double quoted string scalars
// 2. single level ~{"foo":"bar", "baz":"quz"} matchers
type Jsonnet struct{}

// Apply implements the Lens interface.
func (Jsonnet) Apply(src []byte, vals []Setter) ([]byte, error) {
var ops []splice.Op

root, err := jsonnet.SnippetToAST("-", string(src))
if err != nil {
return nil, err
}

for _, v := range vals {
p, err := jsonpointer.New(v.Pointer)
if err != nil {
return nil, err
}
path := p.DecodedTokens()

n, err := findJsonnetNode(root, path)
if err != nil {
return nil, err
}
start, err := lineColToPos(src, n.Loc().Begin.Line, n.Loc().Begin.Column+1)
if err != nil {
return nil, err
}
end, err := lineColToPos(src, n.Loc().End.Line, n.Loc().End.Column+1)
if err != nil {
return nil, err
}

var oldval []byte
switch n := n.(type) {
case *ast.LiteralString:
oldval = []byte(n.Value)
default:
return nil, fmt.Errorf("unhandled node type %T", n)
}

newval, err := v.Value.Transform(oldval)
if err != nil {
return nil, err
}

ops = append(ops, splice.Span(start, end).With(fmt.Sprintf("%q", string(newval))))
}
b, _, err := transform.Bytes(splice.T(ops...), src)
return b, err
}

func findJsonnetNode(root ast.Node, path []string) (ast.Node, error) {
if len(path) == 0 {
return root, nil
}
p := path[0]

switch n := root.(type) {
case *ast.DesugaredObject:
for _, f := range n.Fields {
if k, ok := f.Name.(*ast.LiteralString); !ok {
continue
} else if p == k.Value {
return findJsonnetNode(f.Body, path[1:])
}
}
case *ast.Array:
var exprs []ast.Node
for _, e := range n.Elements {
exprs = append(exprs, e.Expr)
}

e, err := matchJsonnetArrayItem(p, exprs)
if err != nil {
return nil, err
}
return findJsonnetNode(e, path[1:])
case *ast.Local:
return findJsonnetNode(n.Body, path)
default:
return nil, fmt.Errorf("unsupported jsonnet node type: %T", n)
}

return nil, fmt.Errorf("cannot find field %q in object", p)
}

func lineColToPos(src []byte, line, column int) (int, error) {
l, c := 1, 1
for i, r := range string(src) {
c++
if r == '\n' {
l++
c = 1
}
if l == line && c == column {
return i, nil
}
}
return 0, io.EOF
}

func matchJsonnetArrayItem(p string, exprs []ast.Node) (ast.Node, error) {
if strings.HasPrefix(p, "~{") {
var m map[string]string
if err := json.Unmarshal([]byte(p[1:]), &m); err != nil {
return nil, err
}
nodes, err := filterJsonnetArrayItems(exprs, isTreeSubsetPred(m))
if err != nil {
return nil, err
}
if got, want := len(nodes), 1; got != want {
return nil, fmt.Errorf("bad number of subtree matches: got=%d, want=%d", got, want)
}
return nodes[0], nil
}
i, err := strconv.Atoi(p)
if err != nil {
return nil, err
}
return exprs[i], nil
}

type jsonnetNodePredicate func(ast.Node) bool

func isTreeSubsetPred(a map[string]string) jsonnetNodePredicate {
return func(b ast.Node) bool {
return isTreeSubset(a, b)
}
}

func isTreeSubset(a map[string]string, b ast.Node) bool {
for k, v := range a {
if !jsonnetObjectHasField(b, k, v) {
return false
}
}
return true
}

func jsonnetObjectHasField(b ast.Node, name, value string) bool {
if o, ok := b.(*ast.DesugaredObject); ok {
for _, f := range o.Fields {
if n, ok := f.Name.(*ast.LiteralString); ok && n.Value == name {
v, ok := f.Body.(*ast.LiteralString)
return ok && v.Value == value
}
}
}
return false
}

func filterJsonnetArrayItems(nodes []ast.Node, pred jsonnetNodePredicate) ([]ast.Node, error) {
var res []ast.Node
for _, n := range nodes {
if pred(n) {
res = append(res, n)
}
}
return res, nil
}
82 changes: 82 additions & 0 deletions pkg/lensed/jsonnet_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2020 VMware, Inc.
// SPDX-License-Identifier: BSD-2-Clause

package lensed

import (
"fmt"
"testing"
)

func TestJsonnet(t *testing.T) {
testCases := []struct {
src string
t []Mapping
want string
}{
{
`{foo:{bar:"xyz"}}`,
[]Mapping{
{"~(jsonnet)/foo/bar", "abc"},
},
`{foo:{bar:"abc"}}`,
},
{
`{foo:
{bar:"xyz"}}`,
[]Mapping{
{"~(jsonnet)/foo/bar", "abc"},
},
`{foo:
{bar:"abc"}}`,
},
{
`{foo:["xyz"]}`,
[]Mapping{
{"~(jsonnet)/foo/0", "abc"},
},
`{foo:["abc"]}`,
},
{
`{foo:[{bar:"xyz"}]}`,
[]Mapping{
{"~(jsonnet)/foo/0/bar", "abc"},
},
`{foo:[{bar:"abc"}]}`,
},
{
`{foo:[{name:"wrong",bar:"ppp"},{name:"right",bar:"xyz"}]}`,
[]Mapping{
{`~(jsonnet)/foo/~{"name":"right"}/bar`, "abc"},
},
`{foo:[{name:"wrong",bar:"ppp"},{name:"right",bar:"abc"}]}`,
},
{
`{local a="b",foo:{bar:"xyz"}}`,
[]Mapping{
{"~(jsonnet)/foo/bar", "abc"},
},
`{local a="b",foo:{bar:"abc"}}`,
},
{
`local a=1; {foo:{bar:"xyz"}}`,
[]Mapping{
{"~(jsonnet)/foo/bar", "abc"},
},
`local a=1; {foo:{bar:"abc"}}`,
},
}

for i, tc := range testCases {
t.Run(fmt.Sprint(i), func(t *testing.T) {
got, err := Default.Apply([]byte(tc.src), tc.t)
if err != nil {
t.Fatal(err)
}

if got, want := string(got), tc.want; got != want {
t.Errorf("got: %q, want: %q", got, want)
}
})
}
}
1 change: 1 addition & 0 deletions pkg/lensed/lensed.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ var (
"regexp": RegexpLens{},
"ociImageRef": OCIImageRef{}, // deprecated
"oci": OCIImageRef{},
"jsonnet": Jsonnet{},
}
)

Expand Down

0 comments on commit de6ee0a

Please sign in to comment.