From 74356e8d08c0ee87ca2045e1d8c795317bf62aca Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Thu, 3 Aug 2017 17:02:55 +0200 Subject: [PATCH] Escape '/' in JSON Patch path correctly --- kubernetes/patch_operations.go | 16 ++++++++--- kubernetes/patch_operations_test.go | 42 +++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/kubernetes/patch_operations.go b/kubernetes/patch_operations.go index e794a1324e..ea552105fe 100644 --- a/kubernetes/patch_operations.go +++ b/kubernetes/patch_operations.go @@ -21,7 +21,9 @@ func diffStringMap(pathPrefix string, oldV, newV map[string]interface{}) PatchOp if _, ok := newV[k]; ok { continue } - ops = append(ops, &RemoveOperation{Path: pathPrefix + "/" + k}) + ops = append(ops, &RemoveOperation{ + Path: pathPrefix + "/" + escapeJsonPointer(k), + }) } for k, v := range newV { @@ -33,14 +35,14 @@ func diffStringMap(pathPrefix string, oldV, newV map[string]interface{}) PatchOp } ops = append(ops, &ReplaceOperation{ - Path: pathPrefix + "/" + k, + Path: pathPrefix + "/" + escapeJsonPointer(k), Value: newValue, }) continue } ops = append(ops, &AddOperation{ - Path: pathPrefix + "/" + k, + Path: pathPrefix + "/" + escapeJsonPointer(k), Value: newValue, }) } @@ -48,6 +50,14 @@ func diffStringMap(pathPrefix string, oldV, newV map[string]interface{}) PatchOp return ops } +// escapeJsonPointer escapes string per RFC 6901 +// so it can be used as path in JSON patch operations +func escapeJsonPointer(path string) string { + path = strings.Replace(path, "~", "~0", -1) + path = strings.Replace(path, "/", "~1", -1) + return path +} + type PatchOperations []PatchOperation func (po PatchOperations) MarshalJSON() ([]byte, error) { diff --git a/kubernetes/patch_operations_test.go b/kubernetes/patch_operations_test.go index c60a5e628c..523579ac01 100644 --- a/kubernetes/patch_operations_test.go +++ b/kubernetes/patch_operations_test.go @@ -112,6 +112,30 @@ func TestDiffStringMap(t *testing.T) { }, }, }, + { + Path: "/parent/", + Old: map[string]interface{}{ + "two~with-tilde": "220", + "three/with/three/slashes": "330", + }, + New: map[string]interface{}{ + "one/with-slash": "111", + "three/with/three/slashes": "333", + }, + ExpectedOps: []PatchOperation{ + &AddOperation{ + Path: "/parent/one~1with-slash", + Value: "111", + }, + &RemoveOperation{ + Path: "/parent/two~0with-tilde", + }, + &ReplaceOperation{ + Path: "/parent/three~1with~1three~1slashes", + Value: "333", + }, + }, + }, } for i, tc := range testCases { @@ -122,5 +146,23 @@ func TestDiffStringMap(t *testing.T) { } }) } +} +func TestEscapeJsonPointer(t *testing.T) { + testCases := []struct { + Input string + ExpectedOutput string + }{ + {"simple", "simple"}, + {"special-chars,but no escaping", "special-chars,but no escaping"}, + {"escape-this/forward-slash", "escape-this~1forward-slash"}, + {"escape-this~tilde", "escape-this~0tilde"}, + } + for _, tc := range testCases { + output := escapeJsonPointer(tc.Input) + if output != tc.ExpectedOutput { + t.Fatalf("Expected %q as after escaping %q, given: %q", + tc.ExpectedOutput, tc.Input, output) + } + } }