Skip to content

Commit

Permalink
Support JSONPath syntax in setField.field
Browse files Browse the repository at this point in the history
  • Loading branch information
hlubek committed Sep 19, 2023
1 parent 234330b commit 1ca5439
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 12 deletions.
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Responds with status code 200 on success.
* `commands` *array* Commands to perform, one of `setField` and `n.n.` must be set
* `path` *string* Path to the file to patch (relative from repository root)
* `setField` *object* Perform a **set field command** (optional)
* `field` *string* Field to set with dot path syntax
* `field` *string* Field to set with dot path syntax, JSONPath features are supported (see examples)
* `value` *mixed* Value to set the field to
* `create` *boolean* Create the field (and intermediate path) if it doesn't exist (optional, defaults to false)
* `createFile` *object* Perform a **create file command** to create a new file (optional)
Expand Down Expand Up @@ -153,6 +153,35 @@ Content-Type: application/json
}
```

##### Using JSONPath

[JSONPath](https://github.com/vmware-labs/yaml-jsonpath#references) can be used to reference a field by array index, filter expression or other features:

```http request
POST http://localhost:8080/patch/infra-test
Authorization: Bearer [CI_JOB_JWT]
Content-Type: application/json
{
"commit": {
"message": "Bump image to 1.2.5, update BUILD_ID"
},
"commands": [
{
"path": "my-group/my-project/deployment.yml",
"setField": {
"field": "spec.template.spec.containers[0].image",
"value": "registry.example.com/my/image:1.2.5"
},
"setField": {
"field": "spec.template.spec.containers[0].env[?(@.name == 'BUILD_ID')].value",
"value": "987654"
}
}
]
}
```

Using Curl is a convenient way to integrate Vignet into GitLab CI:

```shell
Expand Down Expand Up @@ -188,7 +217,7 @@ Content-Type: application/json
"commands": [
{
"path": "my-group/my-project/new.yml",
"createField": {
"createFile": {
"content": "---\nversion: 1.2.3\n"
}
}
Expand Down
2 changes: 1 addition & 1 deletion end_to_end_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ spec:
}
`,
expectedStatus: 422,
expectedError: `key "spec" not found`,
expectedError: `no nodes matched path`,
},
{
name: "invalid setField with non-existing file",
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/open-policy-agent/opa v0.50.1
github.com/stretchr/testify v1.8.2
github.com/urfave/cli/v2 v2.11.1
github.com/vmware-labs/yaml-jsonpath v0.3.2
gopkg.in/yaml.v3 v3.0.1
)

Expand All @@ -30,6 +31,7 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
Expand Down
13 changes: 13 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960 h1:aRd8M7HJVZOqn/vhOzrGcQH0lNAMkqMn+pXUYkatmcA=
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
Expand Down Expand Up @@ -93,6 +95,7 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/imdario/mergo v0.3.14 h1:fOqeC1+nCuuk6PKQdg9YmosXX7Y7mHX6R/0ZldI9iHo=
Expand Down Expand Up @@ -142,7 +145,11 @@ github.com/networkteam/apexlogutils v0.2.0 h1:HHnB+GR6anJn2T0822Ac5pAY8mVL+exG+R
github.com/networkteam/apexlogutils v0.2.0/go.mod h1:4YBjzjVa4hiL3yhEUStU3t33Cxer+57r4NHwNxuYgdk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.2 h1:uqH7bpe+ERSiDa34FDOF7RikN6RzXgduUF8yarlZp94=
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/open-policy-agent/opa v0.50.1 h1:ZQOqmzTUjcdX7Bu6gnmWZ6ghFTAQI0rI1fR7AqaOW70=
github.com/open-policy-agent/opa v0.50.1/go.mod h1:9jKfDk0L5b9rnhH4M0nq10cGHbYOxqygxzTT3dsvhec=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
Expand Down Expand Up @@ -178,6 +185,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
Expand All @@ -195,6 +203,8 @@ github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPf
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
github.com/urfave/cli/v2 v2.11.1 h1:UKK6SP7fV3eKOefbS87iT9YHefv7iB/53ih6e+GNAsE=
github.com/urfave/cli/v2 v2.11.1/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo=
github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk=
github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
Expand Down Expand Up @@ -292,7 +302,9 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
Expand All @@ -301,6 +313,7 @@ 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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
9 changes: 5 additions & 4 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,12 @@ func (c patchRequestCommand) Validate() error {
}

type setFieldPatchRequestCommand struct {
// Field path to set (dot separated)
// Field path to set (in YAMLPath syntax).
Field string `json:"field"`
// Value to set
// Value to set.
Value any `json:"value"`
// Create missing keys for field if they don't exist, if set to true
// Create missing keys for field if they don't exist, if set to true.
// Field must be a simple dot separated path then - JSONPath is not supported.
Create bool `json:"create"`
}

Expand Down Expand Up @@ -522,7 +523,7 @@ func (h *Handler) applyPatchCommand(ctx context.Context, fs billy.Filesystem, cm
return fmt.Errorf("reading YAML: %w", err)
}

err = patcher.SetField(strings.Split(cmd.SetField.Field, "."), cmd.SetField.Value, cmd.SetField.Create)
err = patcher.SetField(cmd.SetField.Field, cmd.SetField.Value, cmd.SetField.Create)
if err != nil {
return clientError{fmt.Errorf("setting field %q: %w", cmd.SetField.Field, err), http.StatusUnprocessableEntity}
}
Expand Down
40 changes: 36 additions & 4 deletions yaml/patcher.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package yaml

import (
"errors"
"fmt"
"io"
"strings"

"github.com/vmware-labs/yaml-jsonpath/pkg/yamlpath"
goyaml "gopkg.in/yaml.v3"
)

Expand All @@ -23,10 +26,38 @@ func NewPatcher(r io.Reader) (*Patcher, error) {
}, nil
}

func (p *Patcher) SetField(path []string, value any, createKeys bool) error {
valueNode, err := recurseNodeByPath(p.node, path, createKeys)
func (p *Patcher) SetField(path string, value any, createKeys bool) error {
parsedPath, err := yamlpath.NewPath(path)
if err != nil {
return fmt.Errorf("retrieving node by path: %w", err)
return fmt.Errorf("parsing path: %w", err)
}

matchedNodes, err := parsedPath.Find(p.node)
if err != nil {
return fmt.Errorf("finding value node: %w", err)
}

var valueNode *goyaml.Node

if len(matchedNodes) == 0 {
if createKeys {
pathParts := strings.Split(path, ".")
// Note: we do not support JSONPath expressions in the path if createKeys is executed!
valueNode, err = recurseNodeByPath(p.node, pathParts, true)
if err != nil {
return fmt.Errorf("creating path: %w", err)
}
} else {
return errors.New("no nodes matched path")
}
} else if len(matchedNodes) > 1 {
return errors.New("multiple nodes matched path")
} else {
valueNode = matchedNodes[0]
}

if valueNode.Kind != goyaml.ScalarNode {
return fmt.Errorf("expected scalar node, got %s (at %d:%d)", kindToStr(valueNode.Kind), valueNode.Line, valueNode.Column)
}

err = valueNode.Encode(value)
Expand Down Expand Up @@ -79,7 +110,8 @@ func handleScalarNode(node *goyaml.Node) (*goyaml.Node, error) {

func handleMappingNode(node *goyaml.Node, path []string, createKeys bool) (*goyaml.Node, error) {
for i := 0; i < len(node.Content); i += 2 {
if node.Content[i].Value == path[0] {
key := node.Content[i].Value
if key == path[0] {
return recurseNodeByPath(node.Content[i+1], path[1:], createKeys)
}
}
Expand Down
46 changes: 45 additions & 1 deletion yaml/patcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,50 @@ spec:
expectedYAML: `spec:
image:
tag: 0.2.0
`,
},
{
name: "yaml with array index key",
inputYAML: `spec:
template:
spec:
containers:
- name: test
image: test.example.com:latest
`,
fieldPath: "spec.template.spec.containers[0].image",
value: "test.example.com:0.1.0",
expectedYAML: `spec:
template:
spec:
containers:
- name: test
image: test.example.com:0.1.0
`,
},
{
name: "yaml with filter by name",
inputYAML: `spec:
template:
spec:
containers:
- env:
- name: FOO
value: '1'
- name: BAR
value: '2'
`,
fieldPath: "spec.template.spec.containers[0].env[?(@.name=='BAR')].value",
value: "3",
expectedYAML: `spec:
template:
spec:
containers:
- env:
- name: FOO
value: '1'
- name: BAR
value: "3"
`,
},
}
Expand All @@ -101,7 +145,7 @@ spec:
patcher, err := yaml.NewPatcher(strings.NewReader(tt.inputYAML))
require.NoError(t, err)

err = patcher.SetField(strings.Split(tt.fieldPath, "."), tt.value, tt.createKeys)
err = patcher.SetField(tt.fieldPath, tt.value, tt.createKeys)
if tt.expectErr {
assert.Error(t, err)
return
Expand Down

0 comments on commit 1ca5439

Please sign in to comment.