diff --git a/pkg/api/BUILD b/pkg/api/BUILD index 396aa330330..251f2a447f3 100644 --- a/pkg/api/BUILD +++ b/pkg/api/BUILD @@ -93,6 +93,7 @@ go_test( "//vendor:k8s.io/apimachinery/pkg/api/testing", "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", "//vendor:k8s.io/apimachinery/pkg/conversion", + "//vendor:k8s.io/apimachinery/pkg/conversion/unstructured", "//vendor:k8s.io/apimachinery/pkg/runtime", "//vendor:k8s.io/apimachinery/pkg/runtime/schema", "//vendor:k8s.io/apimachinery/pkg/runtime/serializer/protobuf", diff --git a/pkg/api/unstructured_test.go b/pkg/api/unstructured_test.go index 4bab09e7ace..deacd9f4e4b 100644 --- a/pkg/api/unstructured_test.go +++ b/pkg/api/unstructured_test.go @@ -28,6 +28,8 @@ import ( kapitesting "k8s.io/kubernetes/pkg/api/testing" "k8s.io/kubernetes/pkg/api/v1" + "k8s.io/apimachinery/pkg/conversion/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/diff" "k8s.io/apimachinery/pkg/util/json" @@ -95,27 +97,23 @@ func doRoundTrip(t *testing.T, group testapi.TestGroup, kind string) { return } - // TODO; Enable the following part of test once to/from unstructured - // format conversions are implemented. - /* - newUnstr := make(map[string]interface{}) - err = unstructured.NewConverter().ToUnstructured(item, &newUnstr) - if err != nil { - t.Errorf("ToUnstructured failed: %v", err) - return - } + newUnstr := make(map[string]interface{}) + err = unstructured.NewConverter().ToUnstructured(item, &newUnstr) + if err != nil { + t.Errorf("ToUnstructured failed: %v", err) + return + } - newObj := reflect.New(reflect.TypeOf(item).Elem()).Interface().(runtime.Object) - err = unstructured.NewConverter().FromUnstructured(newUnstr, newObj) - if err != nil { - t.Errorf("FromUnstructured failed: %v", err) - return - } + newObj := reflect.New(reflect.TypeOf(item).Elem()).Interface().(runtime.Object) + err = unstructured.NewConverter().FromUnstructured(newUnstr, newObj) + if err != nil { + t.Errorf("FromUnstructured failed: %v", err) + return + } - if !apiequality.Semantic.DeepEqual(item, newObj) { - t.Errorf("Object changed, diff: %v", diff.ObjectReflectDiff(item, newObj)) - } - */ + if !apiequality.Semantic.DeepEqual(item, newObj) { + t.Errorf("Object changed, diff: %v", diff.ObjectReflectDiff(item, newObj)) + } } func TestRoundTrip(t *testing.T) { @@ -135,11 +133,8 @@ func TestRoundTrip(t *testing.T) { } } -// TODO; Enable the following benchmark once to/from unstructured -// format conversions are implemented. -/* func BenchmarkToFromUnstructured(b *testing.B) { - items := benchmarkItems() + items := benchmarkItems(b) size := len(items) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -154,7 +149,6 @@ func BenchmarkToFromUnstructured(b *testing.B) { } b.StopTimer() } -*/ func BenchmarkToFromUnstructuredViaJSON(b *testing.B) { items := benchmarkItems(b) diff --git a/staging/src/k8s.io/apimachinery/pkg/conversion/unstructured/converter.go b/staging/src/k8s.io/apimachinery/pkg/conversion/unstructured/converter.go new file mode 100644 index 00000000000..75b5c56506f --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/conversion/unstructured/converter.go @@ -0,0 +1,517 @@ +/* +Copyright 2017 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 unstructured + +import ( + "bytes" + encodingjson "encoding/json" + "fmt" + "reflect" + "strings" + "sync" + "sync/atomic" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/json" +) + +type structField struct { + structType reflect.Type + field int +} + +type fieldInfo struct { + name string + nameValue reflect.Value +} + +type fieldsCacheMap map[structField]*fieldInfo + +type fieldsCache struct { + sync.Mutex + value atomic.Value +} + +func newFieldsCache() *fieldsCache { + cache := &fieldsCache{} + cache.value.Store(make(fieldsCacheMap)) + return cache +} + +var ( + marshalerType = reflect.TypeOf(new(encodingjson.Marshaler)).Elem() + unmarshalerType = reflect.TypeOf(new(encodingjson.Unmarshaler)).Elem() + mapStringInterfaceType = reflect.TypeOf(map[string]interface{}{}) + fieldCache = newFieldsCache() +) + +// Converter knows how to convert betweek runtime.Object and +// Unstructured in both ways. +type Converter struct { +} + +func NewConverter() *Converter { + return &Converter{} +} + +func (c *Converter) FromUnstructured(u map[string]interface{}, obj runtime.Object) error { + return fromUnstructured(reflect.ValueOf(u), reflect.ValueOf(obj).Elem()) +} + +func fromUnstructured(sv, dv reflect.Value) error { + sv = unwrapInterface(sv) + if !sv.IsValid() { + dv.Set(reflect.Zero(dv.Type())) + return nil + } + st, dt := sv.Type(), dv.Type() + + switch dt.Kind() { + case reflect.Map, reflect.Slice, reflect.Ptr, reflect.Struct, reflect.Interface: + // Those require non-trivial conversion. + default: + // This should handle all simple types. + if st.AssignableTo(dt) { + dv.Set(sv) + return nil + } + // We cannot simply use "ConvertibleTo", as JSON doesn't support conversions + // between those four groups: bools, integers, floats and string. We need to + // do the same. + if st.ConvertibleTo(dt) { + switch st.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + switch dt.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + dv.Set(sv.Convert(dt)) + return nil + } + case reflect.Float32, reflect.Float64: + switch dt.Kind() { + case reflect.Float32, reflect.Float64: + dv.Set(sv.Convert(dt)) + return nil + } + } + return fmt.Errorf("cannot convert %s to %d", st.String(), dt.String()) + } + } + + // Check if the object has a custom JSON marshaller/unmarshaller. + if reflect.PtrTo(dt).Implements(unmarshalerType) { + data, err := json.Marshal(sv.Interface()) + if err != nil { + return fmt.Errorf("error encoding %s to json: %v", st.String(), err) + } + unmarshaler := dv.Addr().Interface().(encodingjson.Unmarshaler) + return unmarshaler.UnmarshalJSON(data) + } + + switch dt.Kind() { + case reflect.Map: + return mapFromUnstructured(sv, dv) + case reflect.Slice: + return sliceFromUnstructured(sv, dv) + case reflect.Ptr: + return pointerFromUnstructured(sv, dv) + case reflect.Struct: + return structFromUnstructured(sv, dv) + case reflect.Interface: + return interfaceFromUnstructured(sv, dv) + default: + return fmt.Errorf("unrecognized type: %v", dt.Kind()) + } +} + +func fieldInfoFromField(structType reflect.Type, field int) *fieldInfo { + fieldCacheMap := fieldCache.value.Load().(fieldsCacheMap) + if info, ok := fieldCacheMap[structField{structType, field}]; ok { + return info + } + + // Cache miss - we need to compute the field name. + info := &fieldInfo{} + typeField := structType.Field(field) + jsonTag := typeField.Tag.Get("json") + if len(jsonTag) == 0 { + // Make the first character lowercase. + if typeField.Name == "" { + info.name = typeField.Name + } else { + info.name = strings.ToLower(typeField.Name[:1]) + typeField.Name[1:] + } + } else { + info.name = strings.Split(jsonTag, ",")[0] + } + info.nameValue = reflect.ValueOf(info.name) + + fieldCache.Lock() + defer fieldCache.Unlock() + fieldCacheMap = fieldCache.value.Load().(fieldsCacheMap) + newFieldCacheMap := make(fieldsCacheMap) + for k, v := range fieldCacheMap { + newFieldCacheMap[k] = v + } + newFieldCacheMap[structField{structType, field}] = info + fieldCache.value.Store(newFieldCacheMap) + return info +} + +func unwrapInterface(v reflect.Value) reflect.Value { + for v.Kind() == reflect.Interface { + v = v.Elem() + } + return v +} + +func mapFromUnstructured(sv, dv reflect.Value) error { + st, dt := sv.Type(), dv.Type() + if st.Kind() != reflect.Map { + return fmt.Errorf("cannot restore map from %v", st.Kind()) + } + + if !st.Key().AssignableTo(dt.Key()) && !st.Key().ConvertibleTo(dt.Key()) { + return fmt.Errorf("cannot copy map with non-assignable keys: %v %v", st.Key(), dt.Key()) + } + + if sv.IsNil() { + dv.Set(reflect.Zero(dt)) + return nil + } + dv.Set(reflect.MakeMap(dt)) + for _, key := range sv.MapKeys() { + value := reflect.New(dt.Elem()).Elem() + if val := unwrapInterface(sv.MapIndex(key)); val.IsValid() { + if err := fromUnstructured(val, value); err != nil { + return err + } + } else { + value.Set(reflect.Zero(dt.Elem())) + } + if st.Key().AssignableTo(dt.Key()) { + dv.SetMapIndex(key, value) + } else { + dv.SetMapIndex(key.Convert(dt.Key()), value) + } + } + return nil +} + +func sliceFromUnstructured(sv, dv reflect.Value) error { + st, dt := sv.Type(), dv.Type() + if st.Kind() == reflect.String && dt.Elem().Kind() == reflect.Uint8 { + // We store original []byte representation as string. + // This conversion is allowed, but we need to be careful about + // marshaling data appropriately. + if len(sv.Interface().(string)) > 0 { + marshalled, err := json.Marshal(sv.Interface()) + if err != nil { + return fmt.Errorf("error encoding %s to json: %v", st, err) + } + // TODO: Is this Unmarshal needed? + var data []byte + err = json.Unmarshal(marshalled, &data) + if err != nil { + return fmt.Errorf("error decoding from json: %v", err) + } + dv.SetBytes(data) + } else { + dv.Set(reflect.Zero(dt)) + } + return nil + } + if st.Kind() != reflect.Slice { + return fmt.Errorf("cannot restore slice from %v", st.Kind()) + } + + if sv.IsNil() { + dv.Set(reflect.Zero(dt)) + return nil + } + dv.Set(reflect.MakeSlice(dt, sv.Len(), sv.Cap())) + for i := 0; i < sv.Len(); i++ { + if err := fromUnstructured(sv.Index(i), dv.Index(i)); err != nil { + return err + } + } + return nil +} + +func pointerFromUnstructured(sv, dv reflect.Value) error { + st, dt := sv.Type(), dv.Type() + + if st.Kind() == reflect.Ptr && sv.IsNil() { + dv.Set(reflect.Zero(dt)) + return nil + } + dv.Set(reflect.New(dt.Elem())) + switch st.Kind() { + case reflect.Ptr, reflect.Interface: + return fromUnstructured(sv.Elem(), dv.Elem()) + default: + return fromUnstructured(sv, dv.Elem()) + } +} + +func structFromUnstructured(sv, dv reflect.Value) error { + st, dt := sv.Type(), dv.Type() + if st.Kind() != reflect.Map { + return fmt.Errorf("cannot restore struct from: %v", st.Kind()) + } + + for i := 0; i < dt.NumField(); i++ { + fieldInfo := fieldInfoFromField(dt, i) + fv := dv.Field(i) + + if len(fieldInfo.name) == 0 { + // This field is inlined. + if err := fromUnstructured(sv, fv); err != nil { + return err + } + } else { + value := unwrapInterface(sv.MapIndex(fieldInfo.nameValue)) + if value.IsValid() { + if err := fromUnstructured(value, fv); err != nil { + return err + } + } else { + fv.Set(reflect.Zero(fv.Type())) + } + } + } + return nil +} + +func interfaceFromUnstructured(sv, dv reflect.Value) error { + // TODO: Is this conversion safe? + dv.Set(sv) + return nil +} + +func (c *Converter) ToUnstructured(obj runtime.Object, u *map[string]interface{}) error { + return toUnstructured(reflect.ValueOf(obj).Elem(), reflect.ValueOf(u).Elem()) +} + +func toUnstructured(sv, dv reflect.Value) error { + st, dt := sv.Type(), dv.Type() + + // Check if the object has a custom JSON marshaller/unmarshaller. + if st.Implements(marshalerType) { + if sv.Kind() == reflect.Ptr && sv.IsNil() { + // We're done - we don't need to store anything. + return nil + } + + marshaler := sv.Interface().(encodingjson.Marshaler) + data, err := marshaler.MarshalJSON() + if err != nil { + return err + } + if bytes.Equal(data, []byte("null")) { + // We're done - we don't need to store anything. + } else { + switch { + case len(data) > 0 && data[0] == '"': + var result string + err := json.Unmarshal(data, &result) + if err != nil { + return fmt.Errorf("error decoding from json: %v", err) + } + dv.Set(reflect.ValueOf(result)) + case len(data) > 0 && data[0] == '{': + result := make(map[string]interface{}) + err := json.Unmarshal(data, &result) + if err != nil { + return fmt.Errorf("error decoding from json: %v", err) + } + dv.Set(reflect.ValueOf(result)) + default: + var result int64 + err := json.Unmarshal(data, &result) + if err != nil { + return fmt.Errorf("error decoding from json: %v", err) + } + dv.Set(reflect.ValueOf(result)) + } + } + return nil + } + + switch st.Kind() { + case reflect.String, reflect.Bool, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + if dt.Kind() == reflect.Interface && dv.NumMethod() == 0 { + dv.Set(reflect.New(st)) + } + dv.Set(sv) + return nil + case reflect.Map: + return mapToUnstructured(sv, dv) + case reflect.Slice: + return sliceToUnstructured(sv, dv) + case reflect.Ptr: + return pointerToUnstructured(sv, dv) + case reflect.Struct: + return structToUnstructured(sv, dv) + case reflect.Interface: + return interfaceToUnstructured(sv, dv) + default: + return fmt.Errorf("unrecognized type: %v", st.Kind()) + } +} + +func mapToUnstructured(sv, dv reflect.Value) error { + st, dt := sv.Type(), dv.Type() + if sv.IsNil() { + dv.Set(reflect.Zero(dt)) + return nil + } + if dt.Kind() == reflect.Interface && dv.NumMethod() == 0 { + if st.Key().Kind() == reflect.String { + switch st.Elem().Kind() { + // TODO It should be possible to reuse the slice for primitive types. + // However, it is panicing in the following form. + // case reflect.String, reflect.Bool, + // reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + // reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + // sv.Set(sv) + // return nil + default: + // We need to do a proper conversion. + } + } + dv.Set(reflect.MakeMap(mapStringInterfaceType)) + dv = dv.Elem() + dt = dv.Type() + } + if dt.Kind() != reflect.Map { + return fmt.Errorf("cannot convert struct to: %v", dt.Kind()) + } + + if !st.Key().AssignableTo(dt.Key()) && !st.Key().ConvertibleTo(dt.Key()) { + return fmt.Errorf("cannot copy map with non-assignable keys: %v %v", st.Key(), dt.Key()) + } + + for _, key := range sv.MapKeys() { + value := reflect.New(dt.Elem()).Elem() + if err := toUnstructured(sv.MapIndex(key), value); err != nil { + return err + } + if st.Key().AssignableTo(dt.Key()) { + dv.SetMapIndex(key, value) + } else { + dv.SetMapIndex(key.Convert(dt.Key()), value) + } + } + return nil +} + +func sliceToUnstructured(sv, dv reflect.Value) error { + st, dt := sv.Type(), dv.Type() + if sv.IsNil() { + dv.Set(reflect.Zero(dt)) + return nil + } + if dt.Kind() == reflect.Interface && dv.NumMethod() == 0 { + switch st.Elem().Kind() { + // TODO It should be possible to reuse the slice for primitive types. + // However, it is panicing in the following form. + // case reflect.String, reflect.Bool, + // reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + // reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + // sv.Set(sv) + // return nil + default: + // We need to do a proper conversion. + dv.Set(reflect.MakeSlice(reflect.SliceOf(dt), sv.Len(), sv.Cap())) + dv = dv.Elem() + dt = dv.Type() + } + } + if dt.Kind() != reflect.Slice { + return fmt.Errorf("cannot convert slice to: %v", dt.Kind()) + } + for i := 0; i < sv.Len(); i++ { + if err := toUnstructured(sv.Index(i), dv.Index(i)); err != nil { + return err + } + } + return nil +} + +func pointerToUnstructured(sv, dv reflect.Value) error { + if sv.IsNil() { + // We're done - we don't need to store anything. + return nil + } + return toUnstructured(sv.Elem(), dv) +} + +func structToUnstructured(sv, dv reflect.Value) error { + st, dt := sv.Type(), dv.Type() + if dt.Kind() == reflect.Interface && dv.NumMethod() == 0 { + dv.Set(reflect.MakeMap(mapStringInterfaceType)) + dv = dv.Elem() + dt = dv.Type() + } + if dt.Kind() != reflect.Map { + return fmt.Errorf("cannot convert struct to: %v", dt.Kind()) + } + realMap := dv.Interface().(map[string]interface{}) + + for i := 0; i < st.NumField(); i++ { + fieldInfo := fieldInfoFromField(st, i) + fv := sv.Field(i) + + if len(fieldInfo.name) == 0 { + // This field is inlined. + if err := toUnstructured(fv, dv); err != nil { + return err + } + continue + } + if !fv.IsValid() { + // No fource field, skip. + continue + } + switch fv.Type().Kind() { + case reflect.String, reflect.Bool, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + realMap[fieldInfo.name] = fv.Interface() + default: + subv := reflect.New(dt.Elem()).Elem() + if err := toUnstructured(fv, subv); err != nil { + return err + } + dv.SetMapIndex(fieldInfo.nameValue, subv) + } + } + return nil +} + +func interfaceToUnstructured(sv, dv reflect.Value) error { + if !sv.IsValid() || sv.IsNil() { + dv.Set(reflect.Zero(dv.Type())) + return nil + } + return toUnstructured(sv.Elem(), dv) +} diff --git a/staging/src/k8s.io/apimachinery/pkg/conversion/unstructured/converter_test.go b/staging/src/k8s.io/apimachinery/pkg/conversion/unstructured/converter_test.go new file mode 100644 index 00000000000..e51c08f3b90 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/conversion/unstructured/converter_test.go @@ -0,0 +1,440 @@ +/* +Copyright 2015 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 unstructured + +import ( + "fmt" + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/diff" + "k8s.io/apimachinery/pkg/util/json" +) + +// Definte a number of test types. +type A struct { + A int `json:"aa,omitempty"` + B string `json:"ab,omitempty"` + C bool `json:"ac,omitempty"` +} + +type B struct { + A A `json:"ba"` + B string `json:"bb"` + C map[string]string `json:"bc"` + D []string `json:"bd"` +} + +type C struct { + A []A `json:"ca"` + B B `json:",inline"` + C string `json:"cc"` + D *int64 `json:"cd"` + E map[string]int `json:"ce"` + F []bool `json:"cf"` + G []int `json"cg"` + H float32 `json:ch"` + I []interface{} `json:"ci"` +} + +type D struct { + A []interface{} `json:"da"` +} + +type E struct { + A interface{} `json:"ea"` +} + +type F struct { + A string `json:"fa"` + B map[string]string `json:"fb"` + C []A `json:"fc"` + D int `json:"fd"` + E float32 `json:"fe"` + F []string `json:"ff"` + G []int `json:"fg"` + H []bool `json:"fh"` + I []float32 `json:"fi"` +} + +// Implement runtime.Object to make types usable for tests. + +func (c *C) GetObjectKind() schema.ObjectKind { + return schema.EmptyObjectKind +} + +func (d *D) GetObjectKind() schema.ObjectKind { + return schema.EmptyObjectKind +} + +func (e *E) GetObjectKind() schema.ObjectKind { + return schema.EmptyObjectKind +} + +func (f *F) GetObjectKind() schema.ObjectKind { + return schema.EmptyObjectKind +} + +func doRoundTrip(t *testing.T, item runtime.Object) { + data, err := json.Marshal(item) + if err != nil { + t.Errorf("Error when marshaling object: %v", err) + return + } + + unstr := make(map[string]interface{}) + err = json.Unmarshal(data, &unstr) + if err != nil { + t.Errorf("Error when unmarshaling to unstructured: %v", err) + return + } + + data, err = json.Marshal(unstr) + if err != nil { + t.Errorf("Error when marshaling unstructured: %v", err) + return + } + unmarshalledObj := reflect.New(reflect.TypeOf(item).Elem()).Interface() + err = json.Unmarshal(data, &unmarshalledObj) + if err != nil { + t.Errorf("Error when unmarshaling to object: %v", err) + return + } + if !reflect.DeepEqual(item, unmarshalledObj) { + t.Errorf("Object changed during JSON operations, diff: %v", diff.ObjectReflectDiff(item, unmarshalledObj)) + return + } + + newUnstr := make(map[string]interface{}) + err = NewConverter().ToUnstructured(item, &newUnstr) + if err != nil { + t.Errorf("ToUnstructured failed: %v", err) + return + } + + newObj := reflect.New(reflect.TypeOf(item).Elem()).Interface().(runtime.Object) + err = NewConverter().FromUnstructured(newUnstr, newObj) + if err != nil { + t.Errorf("FromUnstructured failed: %v", err) + return + } + + if !reflect.DeepEqual(item, newObj) { + t.Errorf("Object changed, diff: %v", diff.ObjectReflectDiff(item, newObj)) + } +} + +func TestRoundTrip(t *testing.T) { + intVal := int64(42) + testCases := []struct{ + obj runtime.Object + }{ + { + // This (among others) tests nil map, slice and pointer. + obj: &C{ + C: "ccc", + }, + }, + { + // This (among others) tests empty map and slice. + obj: &C{ + A: []A{}, + C: "ccc", + E: map[string]int{}, + I: []interface{}{}, + }, + }, + { + obj: &C{ + A: []A{ + { + A: 1, + B: "11", + C: true, + }, + { + A: 2, + B: "22", + C: false, + }, + }, + B: B{ + A: A{ + A: 3, + B: "33", + }, + B: "bbb", + C: map[string]string{ + "k1": "v1", + "k2": "v2", + }, + D: []string{"s1", "s2"}, + }, + C: "ccc", + D: &intVal, + E: map[string]int{ + "k1": 1, + "k2": 2, + }, + F: []bool{true, false, false}, + G: []int{1, 2, 5}, + H: 3.3, + I: []interface{}{nil, nil, nil}, + }, + }, + { + // Test slice of interface{} with empty slices. + obj: &D{ + A: []interface{}{[]interface{}{}, []interface{}{}}, + }, + }, + { + // Test slice of interface{} with different values. + obj: &D{ + A: []interface{}{3.0, "3.0", nil}, + }, + }, + } + + for i := range testCases { + doRoundTrip(t, testCases[i].obj) + if t.Failed() { + break + } + } +} + +// Verifies that: +// 1) serialized json -> object +// 2) serialized json -> map[string]interface{} -> object +// produces the same object. +func doUnrecognized(t *testing.T, jsonData string, item runtime.Object, expectedErr error) { + unmarshalledObj := reflect.New(reflect.TypeOf(item).Elem()).Interface() + err := json.Unmarshal([]byte(jsonData), &unmarshalledObj) + if (err != nil) != (expectedErr != nil) { + t.Errorf("Unexpected error when unmarshaling to object: %v, expected: %v", err, expectedErr) + return + } + + unstr := make(map[string]interface{}) + err = json.Unmarshal([]byte(jsonData), &unstr) + if err != nil { + t.Errorf("Error when unmarshaling to unstructured: %v", err) + return + } + newObj := reflect.New(reflect.TypeOf(item).Elem()).Interface().(runtime.Object) + err = NewConverter().FromUnstructured(unstr, newObj) + if (err != nil) != (expectedErr != nil) { + t.Errorf("Unexpected error in FromUnstructured: %v, expected: %v", err, expectedErr) + } + + if expectedErr == nil && !reflect.DeepEqual(unmarshalledObj, newObj) { + t.Errorf("Object changed, diff: %v", diff.ObjectReflectDiff(unmarshalledObj, newObj)) + } +} + +func TestUnrecognized(t *testing.T) { + testCases := []struct{ + data string + obj runtime.Object + err error + }{ + { + data: "{\"da\":[3.0,\"3.0\",null]}", + obj: &D{}, + }, + { + data: "{\"ea\":[3.0,\"3.0\",null]}", + obj: &E{}, + }, + { + data: "{\"ea\":[null,null,null]}", + obj: &E{}, + }, + { + data: "{\"ea\":[[],[null]]}", + obj: &E{}, + }, + { + data: "{\"ea\":{\"a\":[],\"b\":null}}", + obj: &E{}, + }, + { + data: "{\"fa\":\"fa\",\"fb\":{\"a\":\"a\"}}", + obj: &F{}, + }, + { + data: "{\"fa\":\"fa\",\"fb\":{\"a\":null}}", + obj: &F{}, + }, + { + data: "{\"fc\":[null]}", + obj: &F{}, + }, + { + data: "{\"fc\":[{\"aa\":123,\"ab\":\"bbb\"}]}", + obj: &F{}, + }, + { + // Only unknown fields + data: "{\"fx\":[{\"aa\":123,\"ab\":\"bbb\"}],\"fz\":123}", + obj: &F{}, + }, + { + data: "{\"fc\":[{\"aa\":\"aaa\",\"ab\":\"bbb\"}]}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal string into Go value of type int"), + }, + { + data: "{\"fd\":123,\"fe\":3.5}", + obj: &F{}, + }, + { + data: "{\"ff\":[\"abc\"],\"fg\":[123],\"fh\":[true,false]}", + obj: &F{}, + }, + { + // Invalid string data + data: "{\"fa\":123}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal number into Go value of type string"), + }, + { + // Invalid string data + data: "{\"fa\":13.5}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal number into Go value of type string"), + }, + { + // Invalid string data + data: "{\"fa\":true}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal bool into Go value of type string"), + }, + { + // Invalid []string data + data: "{\"ff\":123}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal number into Go value of type []string"), + }, + { + // Invalid []string data + data: "{\"ff\":3.5}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal number into Go value of type []string"), + }, + { + // Invalid []string data + data: "{\"ff\":[123,345]}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal number into Go value of type string"), + }, + { + // Invalid []int data + data: "{\"fg\":123}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal number into Go value of type []int"), + }, + { + // Invalid []int data + data: "{\"fg\":\"abc\"}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal string into Go value of type []int"), + }, + { + // Invalid []int data + data: "{\"fg\":[\"abc\"]}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal string into Go value of type int"), + }, + { + // Invalid []int data + data: "{\"fg\":[3.5]}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal number 3.5 into Go value of type int"), + }, + { + // Invalid []int data + data: "{\"fg\":[true,false]}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal number 3.5 into Go value of type int"), + }, + { + // Invalid []bool data + data: "{\"fh\":123}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal number into Go value of type []bool"), + }, + { + // Invalid []bool data + data: "{\"fh\":\"abc\"}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal string into Go value of type []bool"), + }, + { + // Invalid []bool data + data: "{\"fh\":[\"abc\"]}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal string into Go value of type bool"), + }, + { + // Invalid []bool data + data: "{\"fh\":[3.5]}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal number into Go value of type bool"), + }, + { + // Invalid []bool data + data: "{\"fh\":[123]}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal number into Go value of type bool"), + }, + { + // Invalid []float data + data: "{\"fi\":123}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal number into Go value of type []float32"), + }, + { + // Invalid []float data + data: "{\"fi\":\"abc\"}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal string into Go value of type []float32"), + }, + { + // Invalid []float data + data: "{\"fi\":[\"abc\"]}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal string into Go value of type float32"), + }, + { + // Invalid []float data + data: "{\"fi\":[true]}", + obj: &F{}, + err: fmt.Errorf("json: cannot unmarshal bool into Go value of type float32"), + }, + } + + for i := range testCases { + doUnrecognized(t, testCases[i].data, testCases[i].obj, testCases[i].err) + if t.Failed() { + break + } + } +} diff --git a/staging/src/k8s.io/apimachinery/pkg/conversion/unstructured/doc.go b/staging/src/k8s.io/apimachinery/pkg/conversion/unstructured/doc.go new file mode 100644 index 00000000000..cd40e74bed3 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/conversion/unstructured/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2017 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 unstructured provides conversion from runtime objects +// to map[string]interface{} representation. +package unstructured // import "k8s.io/apimachinery/pkg/conversion/unstructured" diff --git a/vendor/BUILD b/vendor/BUILD index 59e183e654b..f7786212295 100644 --- a/vendor/BUILD +++ b/vendor/BUILD @@ -15414,3 +15414,29 @@ go_library( "//vendor:k8s.io/sample-apiserver/pkg/apis/wardle", ], ) + +go_test( + name = "k8s.io/apimachinery/pkg/conversion/unstructured_test", + srcs = ["k8s.io/apimachinery/pkg/conversion/unstructured/converter_test.go"], + library = ":k8s.io/apimachinery/pkg/conversion/unstructured", + tags = ["automanaged"], + deps = [ + "//vendor:k8s.io/apimachinery/pkg/runtime", + "//vendor:k8s.io/apimachinery/pkg/runtime/schema", + "//vendor:k8s.io/apimachinery/pkg/util/diff", + "//vendor:k8s.io/apimachinery/pkg/util/json", + ], +) + +go_library( + name = "k8s.io/apimachinery/pkg/conversion/unstructured", + srcs = [ + "k8s.io/apimachinery/pkg/conversion/unstructured/converter.go", + "k8s.io/apimachinery/pkg/conversion/unstructured/doc.go", + ], + tags = ["automanaged"], + deps = [ + "//vendor:k8s.io/apimachinery/pkg/runtime", + "//vendor:k8s.io/apimachinery/pkg/util/json", + ], +)