From 0253ed4a4313ce7129924d3ba5bc667d2fd6d33a Mon Sep 17 00:00:00 2001 From: jennybuckley Date: Tue, 13 Nov 2018 12:42:36 -0800 Subject: [PATCH] convert ManagedFields to internal type --- Godeps/Godeps.json | 8 + .../pkg/endpoints/handlers/apply/decode.go | 173 ++++++++++++ .../endpoints/handlers/apply/decode_test.go | 64 +++++ .../pkg/endpoints/handlers/apply/encode.go | 188 +++++++++++++ .../handlers/apply/roundtrip_test.go | 265 ++++++++++++++++++ 5 files changed, 698 insertions(+) create mode 100644 staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/decode.go create mode 100644 staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/decode_test.go create mode 100644 staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/encode.go create mode 100644 staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/roundtrip_test.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 1f9068a8399..c52b6303d1c 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -4008,6 +4008,14 @@ "ImportPath": "k8s.io/utils/pointer", "Rev": "66066c83e385e385ccc3c964b44fd7dcd413d0ed" }, + { + "ImportPath": "sigs.k8s.io/structured-merge-diff/fieldpath", + "Rev": "261cd9791e59cf384903eff0e7281c6522f5cec7" + }, + { + "ImportPath": "sigs.k8s.io/structured-merge-diff/value", + "Rev": "261cd9791e59cf384903eff0e7281c6522f5cec7" + }, { "ImportPath": "vbom.ml/util/sortorder", "Rev": "db5cfe13f5cc80a4990d98e2e1b0707a4d1a5394" diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/decode.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/decode.go new file mode 100644 index 00000000000..73af5d40326 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/decode.go @@ -0,0 +1,173 @@ +/* +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 ( + "fmt" + "reflect" + "strconv" + + "sigs.k8s.io/structured-merge-diff/fieldpath" + "sigs.k8s.io/structured-merge-diff/value" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// DecodeManagedFields converts ManagedFields from the wire format (api format) +// to the format used by sigs.k8s.io/structured-merge-diff +func DecodeManagedFields(encodedManagedFields map[string]metav1.VersionedFieldSet) (managedFields fieldpath.ManagedFields, err error) { + managedFields = make(map[string]*fieldpath.VersionedSet, len(encodedManagedFields)) + for manager, encodedVersionedSet := range encodedManagedFields { + managedFields[manager], err = decodeVersionedSet(&encodedVersionedSet) + if err != nil { + return nil, fmt.Errorf("error decoding versioned set for %v: %v", manager, err) + } + } + return managedFields, nil +} + +func decodeVersionedSet(encodedVersionedSet *metav1.VersionedFieldSet) (versionedSet *fieldpath.VersionedSet, err error) { + versionedSet = &fieldpath.VersionedSet{} + versionedSet.APIVersion = fieldpath.APIVersion(encodedVersionedSet.APIVersion) + versionedSet.Set = &fieldpath.Set{} + err = decodeSetInto(&encodedVersionedSet.Fields, versionedSet.Set) + if err != nil { + return nil, fmt.Errorf("error decoding set: %v", err) + } + return versionedSet, nil +} + +func decodeSetInto(encodedSet *metav1.FieldSet, set *fieldpath.Set) (err error) { + m, err := decodePathElementSet(encodedSet.Members) + if err != nil { + return fmt.Errorf("error decoding path element set: %v", err) + } + set.Members = *m + c, err := decodeSetNodeMap(encodedSet.Children) + if err != nil { + return fmt.Errorf("error decoding children: %v", err) + } + set.Children = *c + return nil +} + +func decodePathElementSet(encodedPathElementSet []metav1.FieldPathElement) (pathElementSet *fieldpath.PathElementSet, err error) { + pathElementSet = &fieldpath.PathElementSet{} + for _, encodedPathElement := range encodedPathElementSet { + pathElement, err := decodePathElement(&encodedPathElement) + if err != nil { + return nil, fmt.Errorf("error decoding path element: %v", err) + } + pathElementSet.Insert(*pathElement) + } + return pathElementSet, nil +} + +func decodeSetNodeMap(encodedSetNodeMap []metav1.FieldSetNode) (setNodeMap *fieldpath.SetNodeMap, err error) { + setNodeMap = &fieldpath.SetNodeMap{} + for _, encodedSetNode := range encodedSetNodeMap { + pathElement, err := decodePathElement(&encodedSetNode.PathElement) + if err != nil { + return nil, fmt.Errorf("error decoding path element: %v", err) + } + set := setNodeMap.Descend(*pathElement) + decodeSetInto(encodedSetNode.Set, set) + } + return setNodeMap, nil +} + +func decodePathElement(encodedPathElement *metav1.FieldPathElement) (pathElement *fieldpath.PathElement, err error) { + if encodedPathElement == nil { + return nil, nil + } + if !validateOneOf(encodedPathElement.FieldName, encodedPathElement.Key, encodedPathElement.Value, encodedPathElement.Index) { + return nil, fmt.Errorf("too many fields set: %v", encodedPathElement) + } + + pathElement = &fieldpath.PathElement{} + pathElement.FieldName = encodedPathElement.FieldName + if encodedPathElement.Key != nil { + pathElement.Key = make([]value.Field, len(encodedPathElement.Key)) + for i, encodedField := range encodedPathElement.Key { + pathElement.Key[i], err = decodeField(&encodedField) + if err != nil { + return nil, fmt.Errorf("error decoding field: %v", err) + } + } + } + pathElement.Value, err = decodeValue(encodedPathElement.Value) + if err != nil { + return nil, fmt.Errorf("error decoding value: %v", err) + } + if encodedPathElement.Index != nil { + v := int(*encodedPathElement.Index) + pathElement.Index = &v + } + return pathElement, nil +} + +func decodeField(encodedField *metav1.FieldNameValuePair) (field value.Field, err error) { + field = value.Field{} + if encodedField == nil { + return field, fmt.Errorf("unexpected nil pointer") + } + field.Name = encodedField.Name + v, err := decodeValue(&encodedField.Value) + if err != nil { + return field, fmt.Errorf("error decoding value: %v", err) + } + if v != nil { + field.Value = *v + } + return field, nil +} + +func decodeValue(encodedValue *metav1.FieldValue) (v *value.Value, err error) { + if encodedValue == nil { + return nil, nil + } + if !validateOneOf(encodedValue.FloatValue, encodedValue.IntValue, encodedValue.StringValue, encodedValue.BooleanValue) { + return nil, fmt.Errorf("too many fields set: %v", encodedValue) + } + + v = &value.Value{} + if encodedValue.FloatValue != nil { + f, err := strconv.ParseFloat(*encodedValue.FloatValue, 64) + if err != nil { + return nil, fmt.Errorf("error parsing float: %v", err) + } + v.FloatValue = (*value.Float)(&f) + } + if encodedValue.IntValue != nil { + i := int64(*encodedValue.IntValue) + v.IntValue = (*value.Int)(&i) + } + v.StringValue = (*value.String)(encodedValue.StringValue) + v.BooleanValue = (*value.Boolean)(encodedValue.BooleanValue) + v.Null = encodedValue.Null + return v, nil +} + +func validateOneOf(fields ...interface{}) bool { + nonNilCount := 0 + for _, field := range fields { + if !reflect.ValueOf(field).IsNil() { + nonNilCount++ + } + } + return nonNilCount <= 1 +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/decode_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/decode_test.go new file mode 100644 index 00000000000..3b069c59ac1 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/decode_test.go @@ -0,0 +1,64 @@ +/* +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" +) + +// TestValidateOneOf tests that validateOneOf returns true if <= 1 +// non-nil fields are passed to it are and false otherwise +func TestValidateOneOf(t *testing.T) { + var nilValue *string + nonNilValue := strPtr("test") + + tests := []struct { + fields []interface{} + expected bool + }{ + { + fields: []interface{}{}, + expected: true, + }, { + fields: []interface{}{nilValue}, + expected: true, + }, { + fields: []interface{}{nonNilValue}, + expected: true, + }, { + fields: []interface{}{nilValue, nonNilValue, nilValue}, + expected: true, + }, { + fields: []interface{}{nonNilValue, nonNilValue}, + expected: false, + }, { + fields: []interface{}{nilValue, nonNilValue, nonNilValue, nilValue}, + expected: false, + }, + } + + for i, tc := range tests { + actual := validateOneOf(tc.fields...) + if actual != tc.expected { + t.Errorf("[%v]expected validateOneOf(%v) to return:\n\t%+v\nbut got:\n\t%+v", i, tc.fields, tc.expected, actual) + } + } +} + +func strPtr(s string) *string { + return &s +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/encode.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/encode.go new file mode 100644 index 00000000000..3d82b47ce30 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/encode.go @@ -0,0 +1,188 @@ +/* +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 ( + "fmt" + "sort" + "strconv" + + "sigs.k8s.io/structured-merge-diff/fieldpath" + "sigs.k8s.io/structured-merge-diff/value" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EncodeManagedFields converts ManagedFields from the the format used by +// sigs.k8s.io/structured-merge-diff to the the wire format (api format) +func EncodeManagedFields(managedFields fieldpath.ManagedFields) (encodedManagedFields map[string]metav1.VersionedFieldSet, err error) { + encodedManagedFields = make(map[string]metav1.VersionedFieldSet, len(managedFields)) + for manager, versionedSet := range managedFields { + v, err := encodeVersionedSet(versionedSet) + if err != nil { + return nil, fmt.Errorf("error encoding versioned set for %v: %v", manager, err) + } + encodedManagedFields[manager] = *v + } + return encodedManagedFields, nil +} + +func encodeVersionedSet(versionedSet *fieldpath.VersionedSet) (encodedVersionedSet *metav1.VersionedFieldSet, err error) { + encodedVersionedSet = &metav1.VersionedFieldSet{} + encodedVersionedSet.APIVersion = string(versionedSet.APIVersion) + f, err := encodeSet(versionedSet.Set) + if err != nil { + return nil, fmt.Errorf("error encoding set: %v", err) + } + encodedVersionedSet.Fields = *f + return encodedVersionedSet, nil +} + +func encodeSet(set *fieldpath.Set) (encodedSet *metav1.FieldSet, err error) { + encodedSet = &metav1.FieldSet{} + encodedSet.Members, err = encodePathElementSet(&set.Members) + if err != nil { + return nil, fmt.Errorf("error encoding path element set: %v", err) + } + encodedSet.Children, err = encodeSetNodeMap(&set.Children) + if err != nil { + return nil, fmt.Errorf("error encoding children: %v", err) + } + return encodedSet, nil +} + +func encodePathElementSet(pathElementSet *fieldpath.PathElementSet) (encodedPathElementSet []metav1.FieldPathElement, err error) { + // Get a list of the elements in the pathElementSet and sort it + pathElementList := make(sortablePathElementList, 0) + pathElementSet.Iterate(func(pathElement fieldpath.PathElement) { + pathElementList = append(pathElementList, &pathElement) + }) + sort.Sort(pathElementList) + if len(pathElementList) == 0 { + return nil, nil + } + + encodedPathElementSet = make([]metav1.FieldPathElement, len(pathElementList)) + for i, pathElement := range pathElementList { + encodedPathElement, err := encodePathElement(pathElement) + if err != nil { + return nil, fmt.Errorf("error encoding path element: %v", err) + } + encodedPathElementSet[i] = encodedPathElement + } + return encodedPathElementSet, nil +} + +func encodeSetNodeMap(setNodeMap *fieldpath.SetNodeMap) (encodedSetNodeMap []metav1.FieldSetNode, err error) { + // Get a list of the keys in the setNodeMap and sort it + pathElementList := make(sortablePathElementList, 0) + setNodeMap.Iterate(func(pathElement fieldpath.PathElement) { + pathElementList = append(pathElementList, &pathElement) + }) + sort.Sort(pathElementList) + if len(pathElementList) == 0 { + return nil, nil + } + + encodedSetNodeMap = make([]metav1.FieldSetNode, len(pathElementList)) + for i, pathElement := range pathElementList { + encodedPathElement, err := encodePathElement(pathElement) + if err != nil { + return nil, fmt.Errorf("error encoding path element %v: %v", pathElement.String(), err) + } + set, ok := setNodeMap.Get(*pathElement) + if !ok { + return nil, fmt.Errorf("error looking up path element %v", pathElement.String()) + } + encodedSet, err := encodeSet(set) + if err != nil { + return nil, fmt.Errorf("error encoding set for %v: %v", pathElement.String(), err) + } + encodedSetNodeMap[i] = metav1.FieldSetNode{ + PathElement: encodedPathElement, + Set: encodedSet, + } + } + return encodedSetNodeMap, nil +} + +func encodePathElement(pathElement *fieldpath.PathElement) (encodedPathElement metav1.FieldPathElement, err error) { + encodedPathElement = metav1.FieldPathElement{} + if pathElement == nil { + return encodedPathElement, nil + } + encodedPathElement.FieldName = pathElement.FieldName + if pathElement.Key != nil { + encodedPathElement.Key = make([]metav1.FieldNameValuePair, len(pathElement.Key)) + for i, field := range pathElement.Key { + encodedPathElement.Key[i], err = encodeField(&field) + if err != nil { + return encodedPathElement, fmt.Errorf("error encoding field: %v", err) + } + } + } + encodedPathElement.Value, err = encodeValue(pathElement.Value) + if err != nil { + return encodedPathElement, fmt.Errorf("error encoding value: %v", err) + } + if pathElement.Index != nil { + v := int32(*pathElement.Index) + encodedPathElement.Index = &v + } + return encodedPathElement, nil +} + +func encodeField(field *value.Field) (encodedField metav1.FieldNameValuePair, err error) { + encodedField = metav1.FieldNameValuePair{} + if field == nil { + return encodedField, fmt.Errorf("unexpected nil pointer") + } + encodedField.Name = field.Name + v, err := encodeValue(&field.Value) + if err != nil { + return encodedField, fmt.Errorf("error encoding value: %v", err) + } + if v != nil { + encodedField.Value = *v + } + return encodedField, nil +} + +func encodeValue(v *value.Value) (encodedValue *metav1.FieldValue, err error) { + if v == nil { + return nil, nil + } + encodedValue = &metav1.FieldValue{} + if v.FloatValue != nil { + f := strconv.FormatFloat(float64(*v.FloatValue), 'f', -1, 64) + encodedValue.FloatValue = &f + } + if v.IntValue != nil { + i := int32(int64(*v.IntValue)) + encodedValue.IntValue = &i + } + encodedValue.StringValue = (*string)(v.StringValue) + encodedValue.BooleanValue = (*bool)(v.BooleanValue) + encodedValue.Null = v.Null + return encodedValue, nil +} + +type sortablePathElementList []*fieldpath.PathElement + +func (p sortablePathElementList) Len() int { return len(p) } +func (p sortablePathElementList) Less(i, j int) bool { return p[i].String() < p[j].String() } +func (p sortablePathElementList) Swap(i, j int) { p[i], p[j] = p[j], p[i] } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/roundtrip_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/roundtrip_test.go new file mode 100644 index 00000000000..a8c6dcd7292 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/apply/roundtrip_test.go @@ -0,0 +1,265 @@ +/* +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 ( + "reflect" + "strings" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/ghodss/yaml" +) + +// TestRoundTripManagedFields will roundtrip ManagedFields from the format used by +// sigs.k8s.io/structured-merge-diff to the wire format (api format) and back +func TestRoundTripManagedFields(t *testing.T) { + tests := []struct { + yaml []byte + errString string + }{ + { + yaml: []byte(`foo: + apiVersion: v1 + fields: + children: + - pathElement: + index: 5 + set: + members: + - fieldName: i + - pathElement: + value: + floatValue: 3.1415 + set: + members: + - fieldName: pi + - pathElement: + value: + intValue: 3 + set: + members: + - fieldName: alsoPi + - pathElement: + value: + booleanValue: false + set: + members: + - fieldName: notTrue`), + }, { + yaml: []byte(`foo: + apiVersion: v1 + fields: + children: + - pathElement: + fieldName: spec + set: + children: + - pathElement: + fieldName: containers + set: + children: + - pathElement: + key: + - name: name + value: + stringValue: c + 'null': false + set: + members: + - fieldName: image + - fieldName: name`), + }, { + yaml: []byte(`foo: + apiVersion: v1 + fields: + members: + - fieldName: apiVersion + - fieldName: kind + children: + - pathElement: + fieldName: metadata + set: + members: + - fieldName: name + children: + - pathElement: + fieldName: labels + set: + members: + - fieldName: app + - pathElement: + fieldName: spec + set: + members: + - fieldName: replicas + children: + - pathElement: + fieldName: selector + set: + children: + - pathElement: + fieldName: matchLabels + set: + members: + - fieldName: app + - pathElement: + fieldName: template + set: + children: + - pathElement: + fieldName: metadata + set: + children: + - pathElement: + fieldName: labels + set: + members: + - fieldName: app + - pathElement: + fieldName: spec + set: + children: + - pathElement: + fieldName: containers + set: + children: + - pathElement: + key: + - name: name + value: + stringValue: nginx + 'null': false + set: + members: + - fieldName: image + - fieldName: name + children: + - pathElement: + fieldName: ports + set: + children: + - pathElement: + index: 0 + set: + members: + - fieldName: containerPort`), + }, { + yaml: []byte(`foo: + apiVersion: v1 + fields: + members: + - fieldName: allowVolumeExpansion + - fieldName: apiVersion + - fieldName: kind + - fieldName: provisioner + children: + - pathElement: + fieldName: metadata + set: + members: + - fieldName: name + - pathElement: + fieldName: parameters + set: + members: + - fieldName: resturl + - fieldName: restuser + - fieldName: secretName + - fieldName: secretNamespace`), + }, { + yaml: []byte(`foo: + apiVersion: v1 + fields: + members: + - fieldName: apiVersion + - fieldName: kind + children: + - pathElement: + fieldName: metadata + set: + members: + - fieldName: name + - pathElement: + fieldName: spec + set: + members: + - fieldName: group + - fieldName: scope + children: + - pathElement: + fieldName: names + set: + members: + - fieldName: kind + - fieldName: plural + - fieldName: singular + children: + - pathElement: + fieldName: shortNames + set: + members: + - index: 0 + - pathElement: + fieldName: versions + set: + children: + - pathElement: + key: + - name: name + value: + stringValue: v1 + 'null': false + set: + members: + - fieldName: name + - fieldName: served + - fieldName: storage`), + }, + } + + for i, tc := range tests { + var original map[string]metav1.VersionedFieldSet + if err := yaml.Unmarshal(tc.yaml, &original); err != nil { + t.Errorf("[%v]did not expect yaml unmarshalling error but got: %v", i, err) + continue + } + + decoded, err := DecodeManagedFields(original) + if err == nil && len(tc.errString) > 0 { + t.Errorf("[%v]expected error but got none.", i) + continue + } + if err != nil && len(tc.errString) == 0 { + t.Errorf("[%v]did not expect error but got: %v", i, err) + continue + } + if err != nil && len(tc.errString) > 0 && !strings.Contains(err.Error(), tc.errString) { + t.Errorf("[%v]expected error with %q but got: %v", i, tc.errString, err) + continue + } + encoded, err := EncodeManagedFields(decoded) + if err != nil { + t.Errorf("[%v]did not expect round trip error but got: %v", i, err) + continue + } + if !reflect.DeepEqual(encoded, original) { + t.Errorf("[%v]expected:\n\t%+v\nbut got:\n\t%+v", i, original, encoded) + } + } +}