From df76269a3028ea882039ff463fb0cd2815f87b4c Mon Sep 17 00:00:00 2001 From: Antoine Pelisse Date: Wed, 28 Nov 2018 08:48:56 -0800 Subject: [PATCH] Encode/Decode PathElement to/from strings --- Godeps/Godeps.json | 8 +- .../endpoints/handlers/apply/pathelement.go | 122 ++++++++++++++++++ .../handlers/apply/pathelement_test.go | 81 ++++++++++++ .../handlers/fieldmanager/internal/BUILD | 2 + .../value/unstructured.go | 28 +++- 5 files changed, 231 insertions(+), 10 deletions(-) create mode 100644 staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/pathelement.go create mode 100644 staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/pathelement_test.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index e1cb82d05cb..6925ccd8577 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -4069,7 +4069,7 @@ }, { "ImportPath": "sigs.k8s.io/structured-merge-diff/fieldpath", - "Rev": "01332b709372d8d83b4d7653b30a1961680a47d5" + "Rev": "d45b5c336b0e2bdd0b58ae4181caba85f24c6257" }, { "ImportPath": "sigs.k8s.io/structured-merge-diff/merge", @@ -4077,15 +4077,15 @@ }, { "ImportPath": "sigs.k8s.io/structured-merge-diff/schema", - "Rev": "01332b709372d8d83b4d7653b30a1961680a47d5" + "Rev": "d45b5c336b0e2bdd0b58ae4181caba85f24c6257" }, { "ImportPath": "sigs.k8s.io/structured-merge-diff/typed", - "Rev": "01332b709372d8d83b4d7653b30a1961680a47d5" + "Rev": "d45b5c336b0e2bdd0b58ae4181caba85f24c6257" }, { "ImportPath": "sigs.k8s.io/structured-merge-diff/value", - "Rev": "01332b709372d8d83b4d7653b30a1961680a47d5" + "Rev": "d45b5c336b0e2bdd0b58ae4181caba85f24c6257" }, { "ImportPath": "sigs.k8s.io/yaml", diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/pathelement.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/pathelement.go new file mode 100644 index 00000000000..05a857a8e9a --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/pathelement.go @@ -0,0 +1,122 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package apply + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + + "sigs.k8s.io/structured-merge-diff/fieldpath" + "sigs.k8s.io/structured-merge-diff/value" +) + +const ( + Field = "f" + Value = "v" + Index = "i" + Key = "k" + + Separator = ":" +) + +func NewPathElement(s string) (fieldpath.PathElement, error) { + split := strings.SplitN(s, Separator, 2) + if len(split) < 2 { + return fieldpath.PathElement{}, fmt.Errorf("missing colon: %v", s) + } + switch split[0] { + case Field: + return fieldpath.PathElement{ + FieldName: &split[1], + }, nil + case Value: + val, err := value.FromJSON([]byte(split[1])) + if err != nil { + return fieldpath.PathElement{}, err + } + return fieldpath.PathElement{ + Value: &val, + }, nil + case Index: + i, err := strconv.Atoi(split[1]) + if err != nil { + return fieldpath.PathElement{}, err + } + return fieldpath.PathElement{ + Index: &i, + }, nil + case Key: + kv := map[string]string{} + err := json.Unmarshal([]byte(split[1]), &kv) + if err != nil { + return fieldpath.PathElement{}, err + } + fields := []value.Field{} + for k, v := range kv { + fmt.Println(v) + val, err := value.FromJSON([]byte(v)) + if err != nil { + return fieldpath.PathElement{}, err + } + + fields = append(fields, value.Field{ + Name: k, + Value: val, + }) + } + return fieldpath.PathElement{ + Key: fields, + }, nil + default: + // Ignore unknown key types + return fieldpath.PathElement{}, nil + } +} + +func PathElementString(pe fieldpath.PathElement) (string, error) { + switch { + case pe.FieldName != nil: + return Field + Separator + *pe.FieldName, nil + case len(pe.Key) > 0: + kv := map[string]string{} + for _, k := range pe.Key { + b, err := k.Value.ToJSON() + if err != nil { + return "", err + } + kv[k.Name] = string(b) + } + b, err := json.Marshal(kv) + if err != nil { + return "", err + } + return Key + ":" + string(b), nil + case pe.Value != nil: + b, err := pe.Value.ToJSON() + if err != nil { + return "", err + } + return Value + ":" + string(b), nil + case pe.Index != nil: + return Index + ":" + strconv.Itoa(*pe.Index), nil + default: + return "", errors.New("Invalid type of path element") + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/pathelement_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/pathelement_test.go new file mode 100644 index 00000000000..08339477386 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/pathelement_test.go @@ -0,0 +1,81 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package apply + +import "testing" + +func TestPathElementRoundTrip(t *testing.T) { + tests := []string{ + "i:0", + "i:1234", + "f:", + "f:spec", + "f:more-complicated-string", + "k:{\"name\":\"\\\"my-container\\\"\"}", + "k:{\"port\":\"\\\"8080\\\"\",\"protocol\":\"\\\"TCP\\\"\"}", + "v:null", + "v:\"some-string\"", + "v:1234", + "v:{\"some\":\"json\"}", + } + + for _, test := range tests { + t.Run(test, func(t *testing.T) { + pe, err := NewPathElement(test) + if err != nil { + t.Fatalf("Failed to create path element: %v", err) + } + output, err := PathElementString(pe) + if err != nil { + t.Fatalf("Failed to create string from path element: %v", err) + } + if test != output { + t.Fatalf("Expected round-trip:\ninput: %v\noutput: %v", test, output) + } + }) + } +} + +func TestPathElementIgnoreUnknown(t *testing.T) { + _, err := NewPathElement("r:Hello") + if err != nil { + t.Fatalf("Unknown qualifiers should be ignored") + } +} + +func TestNewPathElementError(t *testing.T) { + tests := []string{ + "", + "no-colon", + "i:index is not a number", + "i:1.23", + "i:", + "v:invalid json", + "v:", + "k:invalid json", + "k:{\"name\":\"invalid json\"}", + } + + for _, test := range tests { + t.Run(test, func(t *testing.T) { + _, err := NewPathElement(test) + if err == nil { + t.Fatalf("Expected error, no error found") + } + }) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/BUILD index 5052fafa876..a4cd640b7b9 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/BUILD @@ -5,6 +5,7 @@ go_library( srcs = [ "gvkparser.go", "managedfields.go", + "pathelement.go", "typeconverter.go", "versionconverter.go", ], @@ -31,6 +32,7 @@ go_test( name = "go_default_test", srcs = [ "managedfields_test.go", + "pathelement_test.go", "typeconverter_test.go", "versionconverter_test.go", ], diff --git a/vendor/sigs.k8s.io/structured-merge-diff/value/unstructured.go b/vendor/sigs.k8s.io/structured-merge-diff/value/unstructured.go index b3670118ec8..62660f2fca4 100644 --- a/vendor/sigs.k8s.io/structured-merge-diff/value/unstructured.go +++ b/vendor/sigs.k8s.io/structured-merge-diff/value/unstructured.go @@ -17,6 +17,7 @@ limitations under the License. package value import ( + "encoding/json" "fmt" "gopkg.in/yaml.v2" @@ -53,6 +54,21 @@ func FromYAML(input []byte) (Value, error) { return v, nil } +// FromJSON is a helper function for reading a JSON document +func FromJSON(input []byte) (Value, error) { + var decoded interface{} + + if err := json.Unmarshal(input, &decoded); err != nil { + return Value{}, err + } + + v, err := FromUnstructured(decoded) + if err != nil { + return Value{}, fmt.Errorf("failed to interpret (%v):\n%s", err, input) + } + return v, nil +} + // FromUnstructured will convert a go interface to a Value. // It's most commonly expected to be used with map[string]interface{} as the // input. `in` must not have any structures with cycles in them. @@ -156,12 +172,12 @@ func FromUnstructured(in interface{}) (Value, error) { // preserve order of keys within maps/structs. This is as a convenience to // humans keeping YAML documents, not because there is a behavior difference. func (v *Value) ToYAML() ([]byte, error) { - out := v.ToUnstructured(true) - b, err := yaml.Marshal(out) - if err != nil { - return nil, err - } - return b, nil + return yaml.Marshal(v.ToUnstructured(true)) +} + +// ToJSON is a helper function for producing a JSon document. +func (v *Value) ToJSON() ([]byte, error) { + return json.Marshal(v.ToUnstructured(false)) } // ToUnstructured will convert the Value into a go-typed object.