diff --git a/cmd/build_manifest.go b/cmd/build_manifest.go index 451ca39aa..2ecae7a44 100644 --- a/cmd/build_manifest.go +++ b/cmd/build_manifest.go @@ -1,6 +1,8 @@ package cmd import ( + "github.com/cppforlife/go-patch/patch" + boshtpl "github.com/cloudfoundry/bosh-cli/director/template" boshui "github.com/cloudfoundry/bosh-cli/ui" ) @@ -20,6 +22,13 @@ func (c BuildManifestCmd) Run(opts BuildManifestOpts) error { ops := opts.OpsFlags.AsOps() evalOpts := boshtpl.EvaluateOpts{ExpectAllKeys: opts.VarErrors} + if opts.Path != nil { + ops = patch.Ops{ops, patch.FindOp{Path: *opts.Path}} + + // Printing YAML indented multiline strings (eg SSH key) is not useful + evalOpts.UnescapedMultiline = true + } + bytes, err := tpl.Evaluate(vars, ops, evalOpts) if err != nil { return err diff --git a/cmd/build_manifest_test.go b/cmd/build_manifest_test.go index f798d0e47..9c405c674 100644 --- a/cmd/build_manifest_test.go +++ b/cmd/build_manifest_test.go @@ -61,6 +61,61 @@ var _ = Describe("BuildManifestCmd", func() { Expect(ui.Blocks).To(Equal([]string{bytes})) }) + It("returns portion of the template if out path is given", func() { + opts.Args.Manifest = FileBytesArg{ + Bytes: []byte("name1: ((name1))\nname2: ((name2))"), + } + + opts.VarKVs = []boshtpl.VarKV{ + {Name: "name1", Value: "val1-from-kv"}, + } + + opts.VarsFiles = []boshtpl.VarsFileArg{ + {Vars: boshtpl.Variables(map[string]interface{}{"var": "var-val"})}, + } + + opts.OpsFiles = []OpsFileArg{ + { + Ops: patch.Ops([]patch.Op{ + patch.ReplaceOp{Path: patch.MustNewPointerFromString("/name2"), Value: "((var))"}, + }), + }, + } + + ptr := patch.MustNewPointerFromString("/name2") + opts.Path = &ptr + + err := act() + Expect(err).ToNot(HaveOccurred()) + Expect(ui.Blocks).To(Equal([]string{"var-val\n"})) + }) + + It("returns portion of the template formatting multiline string without YAML indent", func() { + opts.Args.Manifest = FileBytesArg{ + Bytes: []byte(`key: "line1\nline2"`), + } + + ptr := patch.MustNewPointerFromString("/key") + opts.Path = &ptr + + err := act() + Expect(err).ToNot(HaveOccurred()) + Expect(ui.Blocks).To(Equal([]string{"line1\nline2\n"})) + }) + + It("returns portion of the template formatting result as regular YAML", func() { + opts.Args.Manifest = FileBytesArg{ + Bytes: []byte("key:\n subkey:\n subsubkey: key"), + } + + ptr := patch.MustNewPointerFromString("/key") + opts.Path = &ptr + + err := act() + Expect(err).ToNot(HaveOccurred()) + Expect(ui.Blocks).To(Equal([]string{"subkey:\n subsubkey: key\n"})) + }) + It("returns error if variables are not found in templated manifest if var-errors flag is set", func() { opts.Args.Manifest = FileBytesArg{ Bytes: []byte("name1: ((name1))\nname2: ((name2))"), diff --git a/cmd/factory_test.go b/cmd/factory_test.go index abacd2127..48a78b897 100644 --- a/cmd/factory_test.go +++ b/cmd/factory_test.go @@ -269,6 +269,7 @@ var _ = Describe("Factory", func() { boshOpts.ExportRelease = ExportReleaseOpts{} boshOpts.RunErrand = RunErrandOpts{} boshOpts.Logs = LogsOpts{} + boshOpts.BuildManifest = BuildManifestOpts{} boshOpts.InitRelease = InitReleaseOpts{} boshOpts.ResetRelease = ResetReleaseOpts{} boshOpts.GenerateJob = GenerateJobOpts{} diff --git a/cmd/opts.go b/cmd/opts.go index 16ea92d3a..c55321eb1 100644 --- a/cmd/opts.go +++ b/cmd/opts.go @@ -1,6 +1,8 @@ package cmd import ( + "github.com/cppforlife/go-patch/patch" + boshdir "github.com/cloudfoundry/bosh-cli/director" boshrel "github.com/cloudfoundry/bosh-cli/release" ) @@ -272,9 +274,13 @@ type AttachDiskArgs struct { type BuildManifestOpts struct { Args BuildManifestArgs `positional-args:"true" required:"true"` + VarFlags OpsFlags - VarErrors bool `long:"var-errs" description:"Expect all variables to be found, otherwise error"` + + Path *patch.Pointer `long:"path" value-name:"OP-PATH" description:"Extract value out of template (e.g.: /private_key)"` + VarErrors bool `long:"var-errs" description:"Expect all variables to be found, otherwise error"` + cmd } diff --git a/cmd/opts_test.go b/cmd/opts_test.go index bf655cb64..e5fcdf74c 100644 --- a/cmd/opts_test.go +++ b/cmd/opts_test.go @@ -1013,6 +1013,14 @@ var _ = Describe("Opts", func() { }) }) + Describe("Path", func() { + It("contains desired values", func() { + Expect(getStructTagForName("Path", opts)).To(Equal( + `long:"path" value-name:"OP-PATH" description:"Extract value out of template (e.g.: /private_key)"`, + )) + }) + }) + Describe("VarErrors", func() { It("contains desired values", func() { Expect(getStructTagForName("VarErrors", opts)).To(Equal( diff --git a/deps.txt b/deps.txt index c3e8e8a09..f21055b0b 100644 --- a/deps.txt +++ b/deps.txt @@ -22,7 +22,7 @@ github.com/jmespath/go-jmespath:0b12d6b github.com/charlievieth/fs/...:1c3b3f1 github.com/hashicorp/errwrap/...:7554cd9 github.com/bmatcuk/doublestar/...:044df0a -github.com/cppforlife/go-patch/patch:661a67c +github.com/cppforlife/go-patch/patch:a15438b github.com/pivotal-golang/s3cli:90561c0 github.com/golang/protobuf/proto:98fa357 github.com/aws/aws-sdk-go:1a651d9 diff --git a/director/template/template.go b/director/template/template.go index ffa7c30c1..a4dcbd81e 100644 --- a/director/template/template.go +++ b/director/template/template.go @@ -17,7 +17,8 @@ type Template struct { } type EvaluateOpts struct { - ExpectAllKeys bool + ExpectAllKeys bool + UnescapedMultiline bool } func NewTemplate(bytes []byte) Template { @@ -53,6 +54,12 @@ func (t Template) Evaluate(vars Variables, ops patch.Ops, opts EvaluateOpts) ([] return []byte{}, fmt.Errorf("Expected to find variables: %s", strings.Join(missingVarKeys, ", ")) } + if opts.UnescapedMultiline { + if _, ok := obj.(string); ok { + return []byte(fmt.Sprintf("%s\n", obj)), nil + } + } + bytes, err := yaml.Marshal(obj) if err != nil { return []byte{}, err diff --git a/director/template/template_test.go b/director/template/template_test.go index 250e355b0..a69ada57b 100644 --- a/director/template/template_test.go +++ b/director/template/template_test.go @@ -228,4 +228,16 @@ array: Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("Expected to find a map key 'x' for path '/x'")) }) + + It("returns raw bytes of a string if UnescapedMultiline is true", func() { + template := NewTemplate([]byte("value")) + + result, err := template.Evaluate(Variables{}, patch.Ops{}, EvaluateOpts{}) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]byte("value\n"))) + + result, err = template.Evaluate(Variables{}, patch.Ops{}, EvaluateOpts{UnescapedMultiline: true}) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]byte("value\n"))) + }) }) diff --git a/vendor/github.com/cppforlife/go-patch/patch/find_op.go b/vendor/github.com/cppforlife/go-patch/patch/find_op.go new file mode 100644 index 000000000..24eeecae5 --- /dev/null +++ b/vendor/github.com/cppforlife/go-patch/patch/find_op.go @@ -0,0 +1,122 @@ +package patch + +import ( + "fmt" +) + +type FindOp struct { + Path Pointer +} + +func (op FindOp) Apply(doc interface{}) (interface{}, error) { + tokens := op.Path.Tokens() + + if len(tokens) == 1 { + return doc, nil + } + + obj := doc + + for i, token := range tokens[1:] { + isLast := i == len(tokens)-2 + + switch typedToken := token.(type) { + case IndexToken: + idx := typedToken.Index + + typedObj, ok := obj.([]interface{}) + if !ok { + return nil, newOpArrayMismatchTypeErr(tokens[:i+2], obj) + } + + if idx >= len(typedObj) { + errMsg := "Expected to find array index '%d' but found array of length '%d'" + return nil, fmt.Errorf(errMsg, idx, len(typedObj)) + } + + if isLast { + return typedObj[idx], nil + } else { + obj = typedObj[idx] + } + + case AfterLastIndexToken: + errMsg := "Expected not to find after last index token in path '%s' (not supported in find operations)" + return nil, fmt.Errorf(errMsg, op.Path) + + case MatchingIndexToken: + typedObj, ok := obj.([]interface{}) + if !ok { + return nil, newOpArrayMismatchTypeErr(tokens[:i+2], obj) + } + + var idxs []int + + for itemIdx, item := range typedObj { + typedItem, ok := item.(map[interface{}]interface{}) + if ok { + if typedItem[typedToken.Key] == typedToken.Value { + idxs = append(idxs, itemIdx) + } + } + } + + if typedToken.Optional && len(idxs) == 0 { + obj = map[interface{}]interface{}{typedToken.Key: typedToken.Value} + + if isLast { + return obj, nil + } + } else { + if len(idxs) != 1 { + errMsg := "Expected to find exactly one matching array item for path '%s' but found %d" + return nil, fmt.Errorf(errMsg, NewPointer(tokens[:i+2]), len(idxs)) + } + + idx := idxs[0] + + if isLast { + return typedObj[idx], nil + } else { + obj = typedObj[idx] + } + } + + case KeyToken: + typedObj, ok := obj.(map[interface{}]interface{}) + if !ok { + return nil, newOpMapMismatchTypeErr(tokens[:i+2], obj) + } + + var found bool + + obj, found = typedObj[typedToken.Key] + if !found && !typedToken.Optional { + errMsg := "Expected to find a map key '%s' for path '%s'" + return nil, fmt.Errorf(errMsg, typedToken.Key, NewPointer(tokens[:i+2])) + } + + if isLast { + return typedObj[typedToken.Key], nil + } else { + if !found { + // Determine what type of value to create based on next token + switch tokens[i+2].(type) { + case MatchingIndexToken: + obj = []interface{}{} + case KeyToken: + obj = map[interface{}]interface{}{} + default: + errMsg := "Expected to find key or matching index token at path '%s'" + return nil, fmt.Errorf(errMsg, NewPointer(tokens[:i+3])) + } + } + } + + default: + return nil, fmt.Errorf("Expected to not find token '%T' at '%s'", token, NewPointer(tokens[:i+2])) + } + } + + return doc, nil +} diff --git a/vendor/github.com/cppforlife/go-patch/patch/find_op_test.go b/vendor/github.com/cppforlife/go-patch/patch/find_op_test.go new file mode 100644 index 000000000..8c8e95543 --- /dev/null +++ b/vendor/github.com/cppforlife/go-patch/patch/find_op_test.go @@ -0,0 +1,324 @@ +package patch_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/cppforlife/go-patch/patch" +) + +var _ = Describe("FindOp.Apply", func() { + It("returns document if path is for the entire document", func() { + res, err := FindOp{Path: MustNewPointerFromString("")}.Apply("a") + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal("a")) + }) + + Describe("array item", func() { + It("finds array item", func() { + res, err := FindOp{Path: MustNewPointerFromString("/0")}.Apply([]interface{}{1, 2, 3}) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal(1)) + + res, err = FindOp{Path: MustNewPointerFromString("/1")}.Apply([]interface{}{1, 2, 3}) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal(2)) + + res, err = FindOp{Path: MustNewPointerFromString("/2")}.Apply([]interface{}{1, 2, 3}) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal(3)) + }) + + It("finds nested array item", func() { + doc := []interface{}{[]interface{}{10, 11, 12}, 2, 3} + + res, err := FindOp{Path: MustNewPointerFromString("/0/1")}.Apply(doc) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal(11)) + }) + + It("finds array item from an array that is inside a map", func() { + doc := map[interface{}]interface{}{ + "abc": []interface{}{1, 2, 3}, + } + + res, err := FindOp{Path: MustNewPointerFromString("/abc/1")}.Apply(doc) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal(2)) + }) + + It("returns an error if it's not an array when index is being accessed", func() { + _, err := FindOp{Path: MustNewPointerFromString("/0")}.Apply(map[interface{}]interface{}{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected to find an array at path '/0' but found 'map[interface {}]interface {}'")) + + _, err = FindOp{Path: MustNewPointerFromString("/0/1")}.Apply(map[interface{}]interface{}{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected to find an array at path '/0' but found 'map[interface {}]interface {}'")) + }) + + It("returns an error if the index is out of bounds", func() { + _, err := FindOp{Path: MustNewPointerFromString("/1")}.Apply([]interface{}{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected to find array index '1' but found array of length '0'")) + + _, err = FindOp{Path: MustNewPointerFromString("/1/1")}.Apply([]interface{}{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected to find array index '1' but found array of length '0'")) + }) + }) + + Describe("array with after last item", func() { + It("returns an error as after-last-index tokens are not supported", func() { + _, err := FindOp{Path: MustNewPointerFromString("/-")}.Apply([]interface{}{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected not to find after last index token in path '/-' (not supported in find operations)")) + }) + + It("returns an error for nested array item", func() { + doc := []interface{}{[]interface{}{10, 11, 12}, 2, 3} + + _, err := FindOp{Path: MustNewPointerFromString("/0/-")}.Apply(doc) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected not to find after last index token in path '/0/-' (not supported in find operations)")) + }) + + It("returns an error array item from an array that is inside a map", func() { + doc := map[interface{}]interface{}{ + "abc": []interface{}{1, 2, 3}, + } + + _, err := FindOp{Path: MustNewPointerFromString("/abc/-")}.Apply(doc) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected not to find after last index token in path '/abc/-' (not supported in find operations)")) + }) + + It("returns an error if after last index token is not last", func() { + ptr := NewPointer([]Token{RootToken{}, AfterLastIndexToken{}, KeyToken{}}) + + _, err := FindOp{Path: ptr}.Apply([]interface{}{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected not to find after last index token in path '/-/' (not supported in find operations)")) + }) + + It("returns an error if it's not an array being accessed", func() { + _, err := FindOp{Path: MustNewPointerFromString("/-")}.Apply(map[interface{}]interface{}{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected not to find after last index token in path '/-' (not supported in find operations)")) + + doc := map[interface{}]interface{}{"key": map[interface{}]interface{}{}} + + _, err = FindOp{Path: MustNewPointerFromString("/key/-")}.Apply(doc) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected not to find after last index token in path '/key/-' (not supported in find operations)")) + }) + }) + + Describe("array item with matching key and value", func() { + It("finds array item if found", func() { + doc := []interface{}{ + map[interface{}]interface{}{"key": "val"}, + map[interface{}]interface{}{"key": "val2"}, + } + + res, err := FindOp{Path: MustNewPointerFromString("/key=val")}.Apply(doc) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal(map[interface{}]interface{}{"key": "val"})) + }) + + It("returns an error if no items found and matching is not optional", func() { + doc := []interface{}{ + map[interface{}]interface{}{"key": "val2"}, + map[interface{}]interface{}{"key2": "val"}, + } + + _, err := FindOp{Path: MustNewPointerFromString("/key=val")}.Apply(doc) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected to find exactly one matching array item for path '/key=val' but found 0")) + }) + + It("returns an error if multiple items found", func() { + doc := []interface{}{ + map[interface{}]interface{}{"key": "val"}, + map[interface{}]interface{}{"key": "val"}, + } + + _, err := FindOp{Path: MustNewPointerFromString("/key=val")}.Apply(doc) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected to find exactly one matching array item for path '/key=val' but found 2")) + }) + + It("finds array item even if not all items are maps", func() { + doc := []interface{}{ + 3, + map[interface{}]interface{}{"key": "val"}, + } + + res, err := FindOp{Path: MustNewPointerFromString("/key=val")}.Apply(doc) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal(map[interface{}]interface{}{"key": "val"})) + }) + + It("finds nested matching item", func() { + doc := []interface{}{ + map[interface{}]interface{}{ + "key": "val", + "items": []interface{}{ + map[interface{}]interface{}{"nested-key": "val"}, + map[interface{}]interface{}{"nested-key": "val2"}, + }, + }, + map[interface{}]interface{}{"key": "val2"}, + } + + res, err := FindOp{Path: MustNewPointerFromString("/key=val/items/nested-key=val")}.Apply(doc) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal(map[interface{}]interface{}{"nested-key": "val"})) + }) + + It("finds missing matching item if it does not exist", func() { + doc := []interface{}{map[interface{}]interface{}{"xyz": "xyz"}} + + res, err := FindOp{Path: MustNewPointerFromString("/name=val?/efg")}.Apply(doc) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(BeNil()) + }) + + It("finds nested missing matching item if it does not exist", func() { + doc := []interface{}{map[interface{}]interface{}{"xyz": "xyz"}} + + res, err := FindOp{Path: MustNewPointerFromString("/name=val?/efg/name=val")}.Apply(doc) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal(map[interface{}]interface{}{"name": "val"})) + }) + + It("returns an error if it's not an array is being accessed", func() { + _, err := FindOp{Path: MustNewPointerFromString("/key=val")}.Apply(map[interface{}]interface{}{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected to find an array at path '/key=val' but found 'map[interface {}]interface {}'")) + + _, err = FindOp{Path: MustNewPointerFromString("/key=val/items/key=val")}.Apply(map[interface{}]interface{}{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected to find an array at path '/key=val' but found 'map[interface {}]interface {}'")) + }) + }) + + Describe("map key", func() { + It("finds map key", func() { + doc := map[interface{}]interface{}{ + "abc": "abc", + "xyz": "xyz", + } + + res, err := FindOp{Path: MustNewPointerFromString("/abc")}.Apply(doc) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal("abc")) + }) + + It("finds nested map key", func() { + doc := map[interface{}]interface{}{ + "abc": map[interface{}]interface{}{ + "efg": "efg", + "opr": "opr", + }, + "xyz": "xyz", + } + + res, err := FindOp{Path: MustNewPointerFromString("/abc/efg")}.Apply(doc) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(Equal("efg")) + }) + + It("finds nested map key that does not exist", func() { + doc := map[interface{}]interface{}{ + "abc": map[interface{}]interface{}{"opr": "opr"}, + "xyz": "xyz", + } + + res, err := FindOp{Path: MustNewPointerFromString("/abc/efg?")}.Apply(doc) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(BeNil()) + }) + + It("finds super nested map key that does not exist", func() { + doc := map[interface{}]interface{}{ + "abc": map[interface{}]interface{}{ + "efg": map[interface{}]interface{}{}, // wrong level + }, + } + + res, err := FindOp{Path: MustNewPointerFromString("/abc/opr?/efg")}.Apply(doc) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(BeNil()) + }) + + It("returns an error if parent key does not exist", func() { + doc := map[interface{}]interface{}{"xyz": "xyz"} + + _, err := FindOp{Path: MustNewPointerFromString("/abc/efg")}.Apply(doc) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected to find a map key 'abc' for path '/abc'")) + }) + + It("returns an error if key does not exist", func() { + doc := map[interface{}]interface{}{"xyz": "xyz"} + + _, err := FindOp{Path: MustNewPointerFromString("/abc")}.Apply(doc) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected to find a map key 'abc' for path '/abc'")) + }) + + It("returns nil for missing key if key is not expected to exist", func() { + doc := map[interface{}]interface{}{"xyz": "xyz"} + + res, err := FindOp{Path: MustNewPointerFromString("/abc?/efg")}.Apply(doc) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(BeNil()) + }) + + It("returns nil for nested missing keys if key is not expected to exist", func() { + doc := map[interface{}]interface{}{"xyz": "xyz"} + + res, err := FindOp{Path: MustNewPointerFromString("/abc?/other/efg")}.Apply(doc) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(BeNil()) + }) + + It("returns an error if missing key needs to be created but next access does not make sense", func() { + doc := map[interface{}]interface{}{"xyz": "xyz"} + + _, err := FindOp{Path: MustNewPointerFromString("/abc?/0")}.Apply(doc) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected to find key or matching index token at path '/abc?/0'")) + }) + + It("returns an error if it's not a map when key is being accessed", func() { + _, err := FindOp{Path: MustNewPointerFromString("/abc")}.Apply([]interface{}{1, 2, 3}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected to find a map at path '/abc' but found '[]interface {}'")) + + _, err = FindOp{Path: MustNewPointerFromString("/abc/efg")}.Apply([]interface{}{1, 2, 3}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal( + "Expected to find a map at path '/abc' but found '[]interface {}'")) + }) + }) +}) diff --git a/vendor/github.com/cppforlife/go-patch/patch/integration_test.go b/vendor/github.com/cppforlife/go-patch/patch/integration_test.go index c56b99de7..15000fb8b 100644 --- a/vendor/github.com/cppforlife/go-patch/patch/integration_test.go +++ b/vendor/github.com/cppforlife/go-patch/patch/integration_test.go @@ -116,4 +116,47 @@ instance_groups: Expect(res).To(Equal(out)) }) + + It("works with find op", func() { + inStr := ` +releases: +- name: capi + version: 0.1 + +instance_groups: +- name: cloud_controller + instances: 0 + jobs: + - name: cloud_controller + release: capi + +- name: uaa + instances: 0 +` + + var in interface{} + + err := yaml.Unmarshal([]byte(inStr), &in) + Expect(err).ToNot(HaveOccurred()) + + path := MustNewPointerFromString("/instance_groups/name=cloud_controller") + + res, err := FindOp{Path: path}.Apply(in) + Expect(err).ToNot(HaveOccurred()) + + outStr := ` +name: cloud_controller +instances: 0 +jobs: +- name: cloud_controller + release: capi +` + + var out interface{} + + err = yaml.Unmarshal([]byte(outStr), &out) + Expect(err).ToNot(HaveOccurred()) + + Expect(res).To(Equal(out)) + }) }) diff --git a/vendor/github.com/cppforlife/go-patch/patch/op_definition_test.go b/vendor/github.com/cppforlife/go-patch/patch/op_definition_test.go index 60b250875..dba8fc1e4 100644 --- a/vendor/github.com/cppforlife/go-patch/patch/op_definition_test.go +++ b/vendor/github.com/cppforlife/go-patch/patch/op_definition_test.go @@ -35,6 +35,12 @@ var _ = Describe("NewOpsFromDefinitions", func() { Expect(err.Error()).To(Equal("Unknown operation [0] with type 'test'")) }) + It("returns error if operation type is find since it's not useful in list of operations", func() { + _, err := NewOpsFromDefinitions([]OpDefinition{{Type: "find"}}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("Unknown operation [0] with type 'find'")) + }) + Describe("replace", func() { It("requires path", func() { _, err := NewOpsFromDefinitions([]OpDefinition{{Type: "replace"}}) diff --git a/vendor/github.com/cppforlife/go-patch/patch/ops.go b/vendor/github.com/cppforlife/go-patch/patch/ops.go index 733271a2e..5d65b3157 100644 --- a/vendor/github.com/cppforlife/go-patch/patch/ops.go +++ b/vendor/github.com/cppforlife/go-patch/patch/ops.go @@ -10,6 +10,7 @@ type Op interface { var _ Op = Ops{} var _ Op = ReplaceOp{} var _ Op = RemoveOp{} +var _ Op = FindOp{} var _ Op = ErrOp{} func (ops Ops) Apply(doc interface{}) (interface{}, error) { diff --git a/vendor/github.com/cppforlife/go-patch/patch/pointer.go b/vendor/github.com/cppforlife/go-patch/patch/pointer.go index 9a6e8c60c..2690729b5 100644 --- a/vendor/github.com/cppforlife/go-patch/patch/pointer.go +++ b/vendor/github.com/cppforlife/go-patch/patch/pointer.go @@ -151,3 +151,15 @@ func (p Pointer) String() string { return strings.Join(strs, "/") } + +// UnmarshalFlag satisfies go-flags flag interface +func (p *Pointer) UnmarshalFlag(data string) error { + ptr, err := NewPointerFromString(data) + if err != nil { + return err + } + + *p = ptr + + return nil +} diff --git a/vendor/github.com/cppforlife/go-patch/patch/pointer_test.go b/vendor/github.com/cppforlife/go-patch/patch/pointer_test.go index 4e4fd766c..1ca52a719 100644 --- a/vendor/github.com/cppforlife/go-patch/patch/pointer_test.go +++ b/vendor/github.com/cppforlife/go-patch/patch/pointer_test.go @@ -131,3 +131,21 @@ var _ = Describe("Pointer.Tokens", func() { }) } }) + +var _ = Describe("Pointer.UnmarshalFlag", func() { + It("parses pointer if it's valid", func() { + ptr := &Pointer{} + + err := ptr.UnmarshalFlag("/abc") + Expect(err).ToNot(HaveOccurred()) + Expect(*ptr).To(Equal(MustNewPointerFromString("/abc"))) + }) + + It("returns error if string doesn't start with /", func() { + ptr := &Pointer{} + + err := ptr.UnmarshalFlag("abc") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("Expected to start with '/'")) + }) +})