From e59b3e8c5a702f2396d77549f5e2840856ea2281 Mon Sep 17 00:00:00 2001 From: Arthur Befumo <34725560+Arthur-Befumo@users.noreply.github.com> Date: Tue, 27 Aug 2024 23:47:12 -0700 Subject: [PATCH] Add ability to patch yaml with comments (#349) --- yamlpatch/container_test.go | 30 +++++++++++++++--------------- yamlpatch/patch.go | 20 +++++++++++--------- yamlpatch/patch_test.go | 32 ++++++++++++++++++++++++++++++++ yamlpatch/types.go | 3 +++ 4 files changed, 61 insertions(+), 24 deletions(-) diff --git a/yamlpatch/container_test.go b/yamlpatch/container_test.go index d6170769..5c1ae18d 100644 --- a/yamlpatch/container_test.go +++ b/yamlpatch/container_test.go @@ -84,7 +84,7 @@ func TestContainers(t *testing.T) { Patch: func(t *testing.T, node *yaml.Node) { c, err := newContainer(node) require.NoError(t, err) - valNode, err := valueToYAMLNode("newvalue") + valNode, err := valueToYAMLNode("newvalue", "") require.NoError(t, err) err = c.Add("newkey", valNode) require.NoError(t, err) @@ -100,7 +100,7 @@ newkey: newvalue Patch: func(t *testing.T, node *yaml.Node) { c, err := newContainer(node) require.NoError(t, err) - valNode, err := valueToYAMLNode(map[string]interface{}{"bar": "val"}) + valNode, err := valueToYAMLNode(map[string]interface{}{"bar": "val"}, "") require.NoError(t, err) err = c.Add("foo", valNode) require.NoError(t, err) @@ -116,7 +116,7 @@ foo: Patch: func(t *testing.T, node *yaml.Node) { c, err := newContainer(node) require.NoError(t, err) - valNode, err := valueToYAMLNode("newvalue") + valNode, err := valueToYAMLNode("newvalue", "") require.NoError(t, err) err = c.Add("foo", valNode) require.EqualError(t, err, "key foo already exists and can not be added") @@ -131,7 +131,7 @@ foo: Patch: func(t *testing.T, node *yaml.Node) { c, err := newContainer(node) require.NoError(t, err) - valNode, err := valueToYAMLNode("newvalue") + valNode, err := valueToYAMLNode("newvalue", "") require.NoError(t, err) err = c.Set("foo", valNode) require.NoError(t, err) @@ -144,7 +144,7 @@ foo: Patch: func(t *testing.T, node *yaml.Node) { c, err := newContainer(node) require.NoError(t, err) - valNode, err := valueToYAMLNode(map[string]interface{}{"bar": "update", "baz": 2}) + valNode, err := valueToYAMLNode(map[string]interface{}{"bar": "update", "baz": 2}, "") require.NoError(t, err) err = c.Set("foo", valNode) require.NoError(t, err) @@ -160,7 +160,7 @@ foo: Patch: func(t *testing.T, node *yaml.Node) { c, err := newContainer(node) require.NoError(t, err) - valNode, err := valueToYAMLNode("newvalue") + valNode, err := valueToYAMLNode("newvalue", "") require.NoError(t, err) err = c.Set("notfound", valNode) require.EqualError(t, err, "key notfound does not exist and can not be replaced") @@ -291,7 +291,7 @@ foo: Patch: func(t *testing.T, node *yaml.Node) { c, err := newContainer(node) require.NoError(t, err) - valNode, err := valueToYAMLNode("newvalue") + valNode, err := valueToYAMLNode("newvalue", "") require.NoError(t, err) err = c.Add("-", valNode) require.NoError(t, err) @@ -311,7 +311,7 @@ foo: Patch: func(t *testing.T, node *yaml.Node) { c, err := newContainer(node) require.NoError(t, err) - valNode, err := valueToYAMLNode("newvalue") + valNode, err := valueToYAMLNode("newvalue", "") require.NoError(t, err) err = c.Add("3", valNode) require.NoError(t, err) @@ -331,7 +331,7 @@ foo: Patch: func(t *testing.T, node *yaml.Node) { c, err := newContainer(node) require.NoError(t, err) - valNode, err := valueToYAMLNode("newvalue") + valNode, err := valueToYAMLNode("newvalue", "") require.NoError(t, err) err = c.Add("0", valNode) require.NoError(t, err) @@ -351,7 +351,7 @@ foo: Patch: func(t *testing.T, node *yaml.Node) { c, err := newContainer(node) require.NoError(t, err) - valNode, err := valueToYAMLNode("newvalue") + valNode, err := valueToYAMLNode("newvalue", "") require.NoError(t, err) err = c.Add("1", valNode) require.NoError(t, err) @@ -368,7 +368,7 @@ foo: Patch: func(t *testing.T, node *yaml.Node) { c, err := newContainer(node) require.NoError(t, err) - valNode, err := valueToYAMLNode("newvalue") + valNode, err := valueToYAMLNode("newvalue", "") require.NoError(t, err) err = c.Add("-", valNode) require.NoError(t, err) @@ -385,7 +385,7 @@ foo: Patch: func(t *testing.T, node *yaml.Node) { c, err := newContainer(node) require.NoError(t, err) - valNode, err := valueToYAMLNode("newvalue") + valNode, err := valueToYAMLNode("newvalue", "") require.NoError(t, err) err = c.Add("4", valNode) require.EqualError(t, err, "add index key out of bounds (idx 4, len 3)") @@ -404,7 +404,7 @@ foo: Patch: func(t *testing.T, node *yaml.Node) { c, err := newContainer(node) require.NoError(t, err) - valNode, err := valueToYAMLNode("newvalue") + valNode, err := valueToYAMLNode("newvalue", "") require.NoError(t, err) err = c.Set("1", valNode) require.NoError(t, err) @@ -423,7 +423,7 @@ foo: Patch: func(t *testing.T, node *yaml.Node) { c, err := newContainer(node) require.NoError(t, err) - valNode, err := valueToYAMLNode(map[string]interface{}{"bar": "update", "baz": 2}) + valNode, err := valueToYAMLNode(map[string]interface{}{"bar": "update", "baz": 2}, "") require.NoError(t, err) err = c.Set("1", valNode) require.NoError(t, err) @@ -442,7 +442,7 @@ foo: Patch: func(t *testing.T, node *yaml.Node) { c, err := newContainer(node) require.NoError(t, err) - valNode, err := valueToYAMLNode("newvalue") + valNode, err := valueToYAMLNode("newvalue", "") require.NoError(t, err) err = c.Set("2", valNode) require.EqualError(t, err, "set index key out of bounds (idx 2, len 2)") diff --git a/yamlpatch/patch.go b/yamlpatch/patch.go index 128a48be..d9d17c44 100644 --- a/yamlpatch/patch.go +++ b/yamlpatch/patch.go @@ -34,11 +34,11 @@ func Apply(originalBytes []byte, patch Patch) ([]byte, error) { var err error switch op.Type { case OperationAdd: - err = patchAdd(node, op.Path, op.Value) + err = patchAdd(node, op.Path, op.Value, op.Comment) case OperationRemove: err = patchRemove(node, op.Path) case OperationReplace: - err = patchReplace(node, op.Path, op.Value) + err = patchReplace(node, op.Path, op.Value, op.Comment) case OperationMove: err = patchMove(node, op.Path, op.From) case OperationCopy: @@ -64,8 +64,8 @@ func Apply(originalBytes []byte, patch Patch) ([]byte, error) { return buf.Bytes(), nil } -func patchAdd(node *yaml.Node, path Path, value interface{}) error { - valueNode, err := valueToYAMLNode(value) +func patchAdd(node *yaml.Node, path Path, value interface{}, comment string) error { + valueNode, err := valueToYAMLNode(value, comment) if err != nil { return err } @@ -100,12 +100,12 @@ func patchRemove(node *yaml.Node, path Path) error { return parent.Remove(path.Key()) } -func patchReplace(node *yaml.Node, path Path, value interface{}) error { +func patchReplace(node *yaml.Node, path Path, value interface{}, comment string) error { parent, _, err := getParentAndLeaf(node, path) if err != nil { return err } - valueNode, err := valueToYAMLNode(value) + valueNode, err := valueToYAMLNode(value, comment) if err != nil { return err } @@ -169,8 +169,9 @@ func patchTest(node *yaml.Node, path Path, testValue interface{}) error { if err != nil { return err } - // roundtrip test value to use standard type - testValueNode, err := valueToYAMLNode(testValue) + // roundtrip test value to use standard type, comment is unset since this operation only + // looks at the existing value and compares it against the test value + testValueNode, err := valueToYAMLNode(testValue, "") if err != nil { return err } @@ -222,7 +223,7 @@ func unmarshalNode(text []byte) (*yaml.Node, error) { return node.Content[0], nil } -func valueToYAMLNode(value interface{}) (*yaml.Node, error) { +func valueToYAMLNode(value interface{}, comment string) (*yaml.Node, error) { yamlBytes, err := yaml.Marshal(value) if err != nil { return nil, err @@ -232,6 +233,7 @@ func valueToYAMLNode(value interface{}) (*yaml.Node, error) { return nil, err } clearYAMLStyle(node) + node.HeadComment = comment return node, nil } diff --git a/yamlpatch/patch_test.go b/yamlpatch/patch_test.go index 89e2c57b..49db41ad 100644 --- a/yamlpatch/patch_test.go +++ b/yamlpatch/patch_test.go @@ -94,6 +94,38 @@ foo: foo: arr: [0, 1, 2, 3] bar: 1 # my bar`, + }, + { + Name: "add to array with comment", + Patch: []string{`{"op":"add","path":"/foo/arr/-","value":4,"comment":"the number 4"}`}, + Body: `# my foo +foo: + arr: + # numbers 1 through 3 + - 1 + - 2 + - 3`, + Expected: `# my foo +foo: + arr: + # numbers 1 through 3 + - 1 + - 2 + - 3 + # the number 4 + - 4`, + }, + { + Name: "replace object with comment", + Patch: []string{`{"op":"replace","path":"/foo/bar","value":{"key":"value"},"comment":"key value pair"}`}, + Body: `# my foo +foo: + bar: hello-world # previous comment`, + Expected: `# my foo +foo: + bar: + # key value pair + key: value`, }, // Test cases from json-patch: https://github.com/evanphx/json-patch/blob/master/patch_test.go to verify JSON Patch correctness. { diff --git a/yamlpatch/types.go b/yamlpatch/types.go index fdfd3e61..ec5e3bd6 100644 --- a/yamlpatch/types.go +++ b/yamlpatch/types.go @@ -28,6 +28,9 @@ type Operation struct { Path Path `json:"path" yaml:"path"` From Path `json:"from,omitempty" yaml:"from,omitempty"` Value interface{} `json:"value,omitempty" yaml:"value,omitempty"` + + // If not empty, will be inserted as a head comment above this node + Comment string `json:"comment,omitempty" yaml:"comment,omitempty"` } func (op Operation) String() string {