diff --git a/pkg/conversion/converter.go b/pkg/conversion/converter.go new file mode 100644 index 00000000000..025d7c8cc88 --- /dev/null +++ b/pkg/conversion/converter.go @@ -0,0 +1,235 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 conversion + +import ( + "fmt" + "reflect" +) + +type typePair struct { + source reflect.Type + dest reflect.Type +} + +// DebugLogger allows you to get debugging messages if necessary. +type DebugLogger interface { + Logf(format string, args ...interface{}) +} + +// Converter knows how to convert one type to another. +type Converter struct { + // Map from the conversion pair to a function which can + // do the conversion. + funcs map[typePair]reflect.Value + + // If true, print helpful debugging info. Quite verbose. + Debug DebugLogger +} + +// NewConverter makes a new Converter object. +func NewConverter() *Converter { + return &Converter{ + funcs: map[typePair]reflect.Value{}, + } +} + +// Register registers a conversion func with the Converter. conversionFunc must take +// two parameters, the input and output type. It must take a pointer to each. It must +// return an error. +// +// Example: +// c.Register(func(in *Pod, out *v1beta1.Pod) error { ... return nil }) +func (c *Converter) Register(conversionFunc interface{}) error { + fv := reflect.ValueOf(conversionFunc) + ft := fv.Type() + if ft.Kind() != reflect.Func { + return fmt.Errorf("expected func, got: %v", ft) + } + if ft.NumIn() != 2 { + return fmt.Errorf("expected two in params, got: %v", ft) + } + if ft.NumOut() != 1 { + return fmt.Errorf("expected one out param, got: %v", ft) + } + if ft.In(0).Kind() != reflect.Ptr { + return fmt.Errorf("expected pointer arg for in param 0, got: %v", ft) + } + if ft.In(1).Kind() != reflect.Ptr { + return fmt.Errorf("expected pointer arg for in param 1, got: %v", ft) + } + var forErrorType error + // This convolution is necessary, otherwise TypeOf picks up on the fact + // that forErrorType is nil. + errorType := reflect.TypeOf(&forErrorType).Elem() + if ft.Out(0) != errorType { + return fmt.Errorf("expected error return, got: %v", ft) + } + c.funcs[typePair{ft.In(0).Elem(), ft.In(1).Elem()}] = fv + return nil +} + +// FieldMatchingType contains a list of ways in which struct fields could be +// copied. These constants may be | combined. +type FieldMatchingFlags int + +const ( + // Loop through source fields, search for matching dest field + // to copy it into. Destination fields with no corresponding + // source field will be ignored. + SourceToDest FieldMatchingFlags = 1 << iota + // Loop through destiation fields, search for matching source + // field to copy it from. Source fields with no corresponding + // destination field will be ignored. If SourceToDest is + // specified, this flag is ignored. If niether is specified, + // this flag is the default. + DestFromSource + // Don't treat it as an error if the corresponding source or + // dest field can't be found. + IgnoreMissingFields + // Don't require type names to match. + AllowDifferentFieldNames +) + +// Returns true if the given flag or combination of flags is set. +func (f FieldMatchingFlags) IsSet(flag FieldMatchingFlags) bool { + return f&flag == flag +} + +// Convert will translate src to dest if it knows how. Both must be pointers. +// If no conversion func is registered and the default copying mechanism +// doesn't work on this type pair, an error will be returned. +// Not safe for objects with cyclic references! +func (c *Converter) Convert(src, dest interface{}, flags FieldMatchingFlags) error { + dv, sv := reflect.ValueOf(dest), reflect.ValueOf(src) + if dv.Kind() != reflect.Ptr { + return fmt.Errorf("Need pointer, but got %#v", dest) + } + if sv.Kind() != reflect.Ptr { + return fmt.Errorf("Need pointer, but got %#v", src) + } + dv = dv.Elem() + sv = sv.Elem() + if !dv.CanAddr() { + return fmt.Errorf("Can't write to dest") + } + return c.convert(sv, dv, flags) +} + +// convert recursively copies sv into dv, calling an appropriate conversion function if +// one is registered. +func (c *Converter) convert(sv, dv reflect.Value, flags FieldMatchingFlags) error { + dt, st := dv.Type(), sv.Type() + if fv, ok := c.funcs[typePair{st, dt}]; ok { + if c.Debug != nil { + c.Debug.Logf("Calling custom conversion of '%v' to '%v'", st, dt) + } + ret := fv.Call([]reflect.Value{sv.Addr(), dv.Addr()})[0].Interface() + // This convolution is necssary because nil interfaces won't convert + // to errors. + if ret == nil { + return nil + } + return ret.(error) + } + + if !flags.IsSet(AllowDifferentFieldNames) && dt.Name() != st.Name() { + return fmt.Errorf("Can't convert %v to %v because type names don't match.", st, dt) + } + + // This should handle all simple types. + if st.AssignableTo(dt) { + dv.Set(sv) + return nil + } + if st.ConvertibleTo(dt) { + dv.Set(sv.Convert(dt)) + return nil + } + + if c.Debug != nil { + c.Debug.Logf("Trying to convert '%v' to '%v'", st, dt) + } + + switch dv.Kind() { + case reflect.Struct: + listType := dt + if flags.IsSet(SourceToDest) { + listType = st + } + for i := 0; i < listType.NumField(); i++ { + f := listType.Field(i) + df := dv.FieldByName(f.Name) + sf := sv.FieldByName(f.Name) + if !df.IsValid() || !sf.IsValid() { + switch { + case flags.IsSet(IgnoreMissingFields): + // No error. + case flags.IsSet(SourceToDest): + return fmt.Errorf("%v not present in dest (%v to %v)", f.Name, st, dt) + default: + return fmt.Errorf("%v not present in src (%v to %v)", f.Name, st, dt) + } + continue + } + if err := c.convert(sf, df, flags); err != nil { + return err + } + } + case reflect.Slice: + if sv.IsNil() { + // Don't make a zero-length slice. + 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 := c.convert(sv.Index(i), dv.Index(i), flags); err != nil { + return err + } + } + case reflect.Ptr: + if sv.IsNil() { + // Don't copy a nil ptr! + dv.Set(reflect.Zero(dt)) + return nil + } + dv.Set(reflect.New(dt.Elem())) + return c.convert(sv.Elem(), dv.Elem(), flags) + case reflect.Map: + if sv.IsNil() { + // Don't copy a nil ptr! + dv.Set(reflect.Zero(dt)) + return nil + } + dv.Set(reflect.MakeMap(dt)) + for _, sk := range sv.MapKeys() { + dk := reflect.New(dt.Key()).Elem() + if err := c.convert(sk, dk, flags); err != nil { + return err + } + dkv := reflect.New(dt.Elem()).Elem() + if err := c.convert(sv.MapIndex(sk), dkv, flags); err != nil { + return err + } + dv.SetMapIndex(dk, dkv) + } + default: + return fmt.Errorf("Couldn't copy '%v' into '%v'", st, dt) + } + return nil +} diff --git a/pkg/conversion/converter_test.go b/pkg/conversion/converter_test.go new file mode 100644 index 00000000000..c79e2455d81 --- /dev/null +++ b/pkg/conversion/converter_test.go @@ -0,0 +1,81 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 conversion + +import ( + "fmt" + "testing" +) + +func TestConverter(t *testing.T) { + type A struct { + Foo string + } + type B struct { + Bar string + } + type C struct{} + c := NewConverter() + err := c.Register(func(in *A, out *B) error { + out.Bar = in.Foo + return nil + }) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + err = c.Register(func(in *B, out *A) error { + out.Foo = in.Bar + return nil + }) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + + x := A{"hello, intrepid test reader!"} + y := B{} + + err = c.Convert(&x, &y, 0) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + if e, a := x.Foo, y.Bar; e != a { + t.Errorf("expected %v, got %v", e, a) + } + + z := B{"all your test are belong to us"} + w := A{} + + err = c.Convert(&z, &w, 0) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + if e, a := z.Bar, w.Foo; e != a { + t.Errorf("expected %v, got %v", e, a) + } + + err = c.Register(func(in *A, out *C) error { + return fmt.Errorf("C can't store an A, silly") + }) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + + err = c.Convert(&A{}, &C{}, 0) + if err == nil { + t.Errorf("unexpected non-error") + } +} diff --git a/pkg/conversion/decode.go b/pkg/conversion/decode.go new file mode 100644 index 00000000000..7c0da976f3b --- /dev/null +++ b/pkg/conversion/decode.go @@ -0,0 +1,129 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 conversion + +import ( + "fmt" + + "gopkg.in/v1/yaml" +) + +// Decode converts a YAML or JSON string back into a pointer to an api object. +// Deduces the type based upon the fields added by the MetaInsertionFactory +// technique. The object will be converted, if necessary, into the +// s.InternalVersion type before being returned. Decode will refuse to decode +// objects without a version, because that's probably an error. +func (s *Scheme) Decode(data []byte) (interface{}, error) { + version, kind, err := s.DataAPIVersionAndKind(data) + if err != nil { + return nil, err + } + if version == "" { + return nil, fmt.Errorf("API Version not set in '%s'", string(data)) + } + obj, err := s.NewObject(version, kind) + if err != nil { + return nil, err + } + // yaml is a superset of json, so we use it to decode here. That way, + // we understand both. + err = yaml.Unmarshal(data, obj) + if err != nil { + return nil, err + } + + // Version and Kind should be blank in memory. + blankVersionAndKind := s.MetaInsertionFactory.Create("", "") + err = s.converter.Convert(blankVersionAndKind, obj, SourceToDest|IgnoreMissingFields|AllowDifferentFieldNames) + if err != nil { + return nil, err + } + + // Convert if needed. + if s.InternalVersion != version { + objOut, err := s.NewObject(s.InternalVersion, kind) + if err != nil { + return nil, err + } + err = s.converter.Convert(obj, objOut, 0) + if err != nil { + return nil, err + } + obj = objOut + } + return obj, nil +} + +// DecodeInto parses a YAML or JSON string and stores it in obj. Returns an error +// if data.Kind is set and doesn't match the type of obj. Obj should be a +// pointer to an api type. +// If obj's APIVersion doesn't match that in data, an attempt will be made to convert +// data into obj's version. +func (s *Scheme) DecodeInto(data []byte, obj interface{}) error { + dataVersion, dataKind, err := s.DataAPIVersionAndKind(data) + if err != nil { + return err + } + objVersion, objKind, err := s.ObjectAPIVersionAndKind(obj) + if err != nil { + return err + } + if dataKind == "" { + // Assume objects with unset Kind fields are being unmarshalled into the + // correct type. + dataKind = objKind + } + if dataKind != objKind { + return fmt.Errorf("data of kind '%v', obj of type '%v'", dataKind, objKind) + } + if dataVersion == "" { + // Assume objects with unset Version fields are being unmarshalled into the + // correct type. + dataVersion = objVersion + } + + if objVersion == dataVersion { + // Easy case! + err = yaml.Unmarshal(data, obj) + if err != nil { + return err + } + } else { + external, err := s.NewObject(dataVersion, dataKind) + if err != nil { + return fmt.Errorf("Unable to create new object of type ('%s', '%s')", dataVersion, dataKind) + } + // yaml is a superset of json, so we use it to decode here. That way, + // we understand both. + err = yaml.Unmarshal(data, external) + if err != nil { + return err + } + err = s.converter.Convert(external, obj, 0) + if err != nil { + return err + } + } + + // Version and Kind should be blank in memory. + blankVersionAndKind := s.MetaInsertionFactory.Create("", "") + err = s.converter.Convert(blankVersionAndKind, obj, SourceToDest|IgnoreMissingFields|AllowDifferentFieldNames) + if err != nil { + return err + } + return nil +} diff --git a/pkg/conversion/encode.go b/pkg/conversion/encode.go new file mode 100644 index 00000000000..29b0ab5a0f3 --- /dev/null +++ b/pkg/conversion/encode.go @@ -0,0 +1,136 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 conversion + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// EncodeOrDie is a version of Encode which will panic instead of returning an error. For tests. +func (s *Scheme) EncodeOrDie(obj interface{}) string { + bytes, err := s.Encode(obj) + if err != nil { + panic(err) + } + return string(bytes) +} + +// Encode turns the given api object into an appropriate JSON string. +// Obj may be a pointer to a struct, or a struct. If a struct, a copy +// will be made, therefore it's recommended to pass a pointer to a +// struct. The type must have been registered. +// +// Memory/wire format differences: +// * Having to keep track of the Kind and APIVersion fields makes tests +// very annoying, so the rule is that they are set only in wire format +// (json), not when in native (memory) format. This is possible because +// both pieces of information are implicit in the go typed object. +// * An exception: note that, if there are embedded API objects of known +// type, for example, PodList{... Items []Pod ...}, these embedded +// objects must be of the same version of the object they are embedded +// within, and their APIVersion and Kind must both be empty. +// * Note that the exception does not apply to the APIObject type, which +// recursively does Encode()/Decode(), and is capable of expressing any +// API object. +// * Only versioned objects should be encoded. This means that, if you pass +// a native object, Encode will convert it to a versioned object. For +// example, an api.Pod will get converted to a v1beta1.Pod. However, if +// you pass in an object that's already versioned (v1beta1.Pod), Encode +// will not modify it. +// +// The purpose of the above complex conversion behavior is to allow us to +// change the memory format yet not break compatibility with any stored +// objects, whether they be in our storage layer (e.g., etcd), or in user's +// config files. +// +func (s *Scheme) Encode(obj interface{}) (data []byte, err error) { + return s.EncodeToVersion(obj, s.ExternalVersion) +} + +// EncodeToVersion is like Encode, but you may choose the version. +func (s *Scheme) EncodeToVersion(obj interface{}, destVersion string) (data []byte, err error) { + obj = maybeCopy(obj) + v, _ := enforcePtr(obj) // maybeCopy guarantees a pointer + if _, registered := s.typeToVersion[v.Type()]; !registered { + return nil, fmt.Errorf("type %v is not registered and it will be impossible to Decode it, therefore Encode will refuse to encode it.", v.Type()) + } + + objVersion, objKind, err := s.ObjectAPIVersionAndKind(obj) + if err != nil { + return nil, err + } + + // Perform a conversion if necessary. + if objVersion != destVersion { + objOut, err := s.NewObject(destVersion, objKind) + if err != nil { + return nil, err + } + err = s.converter.Convert(obj, objOut, 0) + if err != nil { + return nil, err + } + obj = objOut + } + + // Version and Kind should be set on the wire. + setVersionAndKind := s.MetaInsertionFactory.Create(destVersion, objKind) + err = s.converter.Convert(setVersionAndKind, obj, SourceToDest|IgnoreMissingFields|AllowDifferentFieldNames) + if err != nil { + return nil, err + } + + // To add metadata, do some simple surgery on the JSON. + data, err = json.Marshal(obj) + if err != nil { + return nil, err + } + + // Version and Kind should be blank in memory. + blankVersionAndKind := s.MetaInsertionFactory.Create("", "") + err = s.converter.Convert(blankVersionAndKind, obj, SourceToDest|IgnoreMissingFields|AllowDifferentFieldNames) + if err != nil { + return nil, err + } + + return data, nil + + meta, err := json.Marshal(s.MetaInsertionFactory.Create(destVersion, objKind)) + if err != nil { + return nil, err + } + // Stick these together, omitting the last } from meta and the first { from + // data. Add a comma to meta if necessary. + metaN := len(meta) + if len(data) > 2 { + meta[metaN-1] = ',' // Add comma + } else { + meta = meta[:metaN-1] // Just remove } + } + together := append(meta, data[1:]...) + if s.Indent { + var out bytes.Buffer + err := json.Indent(&out, together, "", " ") + if err != nil { + return nil, err + } + return out.Bytes(), nil + } + return together, nil +} diff --git a/pkg/conversion/scheme.go b/pkg/conversion/scheme.go new file mode 100644 index 00000000000..7f49b0cee47 --- /dev/null +++ b/pkg/conversion/scheme.go @@ -0,0 +1,223 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 conversion + +import ( + "fmt" + "reflect" + + "gopkg.in/v1/yaml" +) + +// MetaInsertionFactory is used to create an object to store and retrieve +// the version and kind information for all objects. The default uses the +// keys "apiVersion" and "kind" respectively. The object produced by this +// factory is used to clear the version and kind fields in memory, so it +// must match the layout of your actual api structs. (E.g., if you have your +// version and kind field inside an inlined struct, this must produce an +// inlined struct with the same field name.) +type MetaInsertionFactory interface { + // Create should make a new object with two fields. + // This object will be used to encode this metadata along with your + // API objects, so the tags on the fields you use shouldn't conflict. + Create(apiVersion, kind string) interface{} + // Interpret should take the same type of object that Create creates. + // It should return the version and kind information from this object. + Interpret(interface{}) (apiVersion, kind string) +} + +// Default is a global scheme. +var Default = NewScheme() + +// Scheme defines an entire encoding and decoding scheme. +type Scheme struct { + // versionMap allows one to figure out the go type of an object with + // the given version and name. + versionMap map[string]map[string]reflect.Type + + // typeToVersion allows one to figure out the version for a given go object. + // The reflect.Type we index by should *not* be a pointer. If the same type + // is registered for multiple versions, the last one wins. + typeToVersion map[reflect.Type]string + + // converter stores all registered conversion functions. It also has + // default coverting behavior. + converter *Converter + + // Indent will cause the JSON output from Encode to be indented, iff it is true. + Indent bool + + // InternalVersion is the default internal version. It is recommended that + // you use "" for the internal version. + InternalVersion string + + // ExternalVersion is the default external version. + ExternalVersion string + + // MetaInsertionFactory is used to create an object to store and retrieve + // the version and kind information for all objects. The default uses the + // keys "apiVersion" and "kind" respectively. + MetaInsertionFactory MetaInsertionFactory +} + +// NewScheme manufactures a new scheme. +func NewScheme() *Scheme { + return &Scheme{ + versionMap: map[string]map[string]reflect.Type{}, + typeToVersion: map[reflect.Type]string{}, + converter: NewConverter(), + InternalVersion: "", + ExternalVersion: "v1", + MetaInsertionFactory: metaInsertion{}, + } +} + +// AddKnownTypes registers the types of the arguments to the marshaller of the package api. +// Encode() refuses the object unless its type is registered with AddKnownTypes. +func (s *Scheme) AddKnownTypes(version string, types ...interface{}) { + knownTypes, found := s.versionMap[version] + if !found { + knownTypes = map[string]reflect.Type{} + s.versionMap[version] = knownTypes + } + for _, obj := range types { + t := reflect.TypeOf(obj) + if t.Kind() != reflect.Struct { + panic("All types must be structs.") + } + knownTypes[t.Name()] = t + s.typeToVersion[t] = version + } +} + +// NewObject returns a new object of the given version and name, +// or an error if it hasn't been registered. +func (s *Scheme) NewObject(versionName, typeName string) (interface{}, error) { + if types, ok := s.versionMap[versionName]; ok { + if t, ok := types[typeName]; ok { + return reflect.New(t).Interface(), nil + } + return nil, fmt.Errorf("No type '%v' for version '%v'", typeName, versionName) + } + return nil, fmt.Errorf("No version '%v'", versionName) +} + +// AddConversionFuncs adds functions to the list of conversion functions. The given +// functions should know how to convert between two API objects. We deduce how to call +// it from the types of its two parameters; see the comment for Converter.Register. +// +// Note that, if you need to copy sub-objects that didn't change, it's safe to call +// Convert() inside your conversionFuncs, as long as you don't start a conversion +// chain that's infinitely recursive. +// +// Also note that the default behavior, if you don't add a conversion function, is to +// sanely copy fields that have the same names. It's OK if the destination type has +// extra fields, but it must not remove any. So you only need to add a conversion +// function for things with changed/removed fields. +func (s *Scheme) AddConversionFuncs(conversionFuncs ...interface{}) error { + for _, f := range conversionFuncs { + err := s.converter.Register(f) + if err != nil { + return err + } + } + return nil +} + +// Convert will attempt to convert in into out. Both must be pointers. +// For easy testing of conversion functions. Returns an error if the conversion isn't +// possible. +func (s *Scheme) Convert(in, out interface{}) error { + return s.converter.Convert(in, out, 0) +} + +// metaInsertion provides a default implementation of MetaInsertionFactory. +type metaInsertion struct { + JSONBase struct { + APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` + Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` + } `json:",inline" yaml:",inline"` +} + +// Create should make a new object with two fields. +// This object will be used to encode this metadata along with your +// API objects, so the tags on the fields you use shouldn't conflict. +func (metaInsertion) Create(apiVersion, kind string) interface{} { + m := metaInsertion{} + m.JSONBase.APIVersion = apiVersion + m.JSONBase.Kind = kind + return &m +} + +// Interpret should take the same type of object that Create creates. +// It should return the version and kind information from this object. +func (metaInsertion) Interpret(in interface{}) (apiVersion, kind string) { + m := in.(*metaInsertion) + return m.JSONBase.APIVersion, m.JSONBase.Kind +} + +// DataAPIVersionAndKind will return the APIVersion and Kind of the given wire-format +// enconding of an API Object, or an error. +func (s *Scheme) DataAPIVersionAndKind(data []byte) (apiVersion, kind string, err error) { + findKind := s.MetaInsertionFactory.Create("", "") + // yaml is a superset of json, so we use it to decode here. That way, + // we understand both. + err = yaml.Unmarshal(data, findKind) + if err != nil { + return "", "", fmt.Errorf("couldn't get version/kind: %v", err) + } + apiVersion, kind = s.MetaInsertionFactory.Interpret(findKind) + return apiVersion, kind, nil +} + +// ObjectVersionAndKind returns the API version and kind of the go object, +// or an error if it's not a pointer or is unregistered. +func (s *Scheme) ObjectAPIVersionAndKind(obj interface{}) (apiVersion, kind string, err error) { + v, err := enforcePtr(obj) + if err != nil { + return "", "", err + } + t := v.Type() + if version, ok := s.typeToVersion[t]; !ok { + return "", "", fmt.Errorf("Unregistered type: %v", t) + } else { + return version, t.Name(), nil + } +} + +// maybeCopy copies obj if it is not a pointer, to get a settable/addressable +// object. Guaranteed to return a pointer. +func maybeCopy(obj interface{}) interface{} { + v := reflect.ValueOf(obj) + if v.Kind() == reflect.Ptr { + return obj + } + v2 := reflect.New(v.Type()) + v2.Elem().Set(v) + return v2.Interface() +} + +// Ensures that obj is a pointer of some sort. Returns a reflect.Value of the +// dereferenced pointer, ensuring that it is settable/addressable. +// Returns an error if this is not possible. +func enforcePtr(obj interface{}) (reflect.Value, error) { + v := reflect.ValueOf(obj) + if v.Kind() != reflect.Ptr { + return reflect.Value{}, fmt.Errorf("expected pointer, but got %v", v.Type().Name()) + } + return v.Elem(), nil +} diff --git a/pkg/conversion/scheme_test.go b/pkg/conversion/scheme_test.go new file mode 100644 index 00000000000..ad5aa610bd6 --- /dev/null +++ b/pkg/conversion/scheme_test.go @@ -0,0 +1,248 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 conversion + +import ( + "encoding/json" + "flag" + "fmt" + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +var fuzzIters = flag.Int("fuzz_iters", 10, "How many fuzzing iterations to do.") + +// Intended to be compatible with the default implementation of MetaInsertionFactory +type JSONBase struct { + ID string `yaml:"ID,omitempty" json:"ID,omitempty"` + APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` + Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` +} + +type TestType1 struct { + JSONBase `json:",inline" yaml:",inline"` + A string `yaml:"A,omitempty" json:"A,omitempty"` + B int `yaml:"B,omitempty" json:"B,omitempty"` + C int8 `yaml:"C,omitempty" json:"C,omitempty"` + D int16 `yaml:"D,omitempty" json:"D,omitempty"` + E int32 `yaml:"E,omitempty" json:"E,omitempty"` + F int64 `yaml:"F,omitempty" json:"F,omitempty"` + G uint `yaml:"G,omitempty" json:"G,omitempty"` + H uint8 `yaml:"H,omitempty" json:"H,omitempty"` + I uint16 `yaml:"I,omitempty" json:"I,omitempty"` + J uint32 `yaml:"J,omitempty" json:"J,omitempty"` + K uint64 `yaml:"K,omitempty" json:"K,omitempty"` + L bool `yaml:"L,omitempty" json:"L,omitempty"` + M map[string]int `yaml:"M,omitempty" json:"M,omitempty"` + N map[string]TestType2 `yaml:"N,omitempty" json:"N,omitempty"` + O *TestType2 `yaml:"O,omitempty" json:"O,omitempty"` + P []TestType2 `yaml:"Q,omitempty" json:"Q,omitempty"` +} + +type TestType2 struct { + A string `yaml:"A,omitempty" json:"A,omitempty"` + B int `yaml:"B,omitempty" json:"B,omitempty"` +} + +// We depend on the name of the external and internal types matching. Ordinarily, +// we'd accomplish this with an additional package, but since this is a test, we +// can just enclose stuff in a function to simulate that. +func externalTypeReturn() interface{} { + type TestType2 struct { + A string `yaml:"A,omitempty" json:"A,omitempty"` + B int `yaml:"B,omitempty" json:"B,omitempty"` + } + type TestType1 struct { + JSONBase `json:",inline" yaml:",inline"` + A string `yaml:"A,omitempty" json:"A,omitempty"` + B int `yaml:"B,omitempty" json:"B,omitempty"` + C int8 `yaml:"C,omitempty" json:"C,omitempty"` + D int16 `yaml:"D,omitempty" json:"D,omitempty"` + E int32 `yaml:"E,omitempty" json:"E,omitempty"` + F int64 `yaml:"F,omitempty" json:"F,omitempty"` + G uint `yaml:"G,omitempty" json:"G,omitempty"` + H uint8 `yaml:"H,omitempty" json:"H,omitempty"` + I uint16 `yaml:"I,omitempty" json:"I,omitempty"` + J uint32 `yaml:"J,omitempty" json:"J,omitempty"` + K uint64 `yaml:"K,omitempty" json:"K,omitempty"` + L bool `yaml:"L,omitempty" json:"L,omitempty"` + M map[string]int `yaml:"M,omitempty" json:"M,omitempty"` + N map[string]TestType2 `yaml:"N,omitempty" json:"N,omitempty"` + O *TestType2 `yaml:"O,omitempty" json:"O,omitempty"` + P []TestType2 `yaml:"Q,omitempty" json:"Q,omitempty"` + } + return TestType1{} +} + +type ExternalInternalSame struct { + JSONBase `json:",inline" yaml:",inline"` + A TestType2 `yaml:"A,omitempty" json:"A,omitempty"` +} + +// TestObjectFuzzer can randomly populate all the above objects. +var TestObjectFuzzer = util.NewFuzzer( + func(j *JSONBase) { + // We have to customize the randomization of JSONBases because their + // APIVersion and Kind must remain blank in memory. + j.APIVersion = "" + j.Kind = "" + j.ID = util.RandString() + }, + func(u *uint64) { + // TODO: Fix JSON/YAML packages and/or write custom encoding + // for uint64's. Somehow the LS *byte* of this is lost, but + // only when all 8 bytes are set. + *u = util.RandUint64() >> 8 + }, + func(u *uint) { + // TODO: Fix JSON/YAML packages and/or write custom encoding + // for uint64's. Somehow the LS *byte* of this is lost, but + // only when all 8 bytes are set. + *u = uint(util.RandUint64() >> 8) + }, +) + +// Returns a new Scheme set up with the test objects. +func GetTestScheme() *Scheme { + s := NewScheme() + s.AddKnownTypes("", TestType1{}, ExternalInternalSame{}) + s.AddKnownTypes("v1", externalTypeReturn(), ExternalInternalSame{}) + s.ExternalVersion = "v1" + s.InternalVersion = "" + return s +} + +func objDiff(a, b interface{}) string { + ab, err := json.Marshal(a) + if err != nil { + panic("a") + } + bb, err := json.Marshal(b) + if err != nil { + panic("b") + } + return util.StringDiff(string(ab), string(bb)) + + // An alternate diff attempt, in case json isn't showing you + // the difference. (reflect.DeepEqual makes a distinction between + // nil and empty slices, for example.) + return util.StringDiff( + fmt.Sprintf("%#v", a), + fmt.Sprintf("%#v", b), + ) +} + +func runTest(t *testing.T, source interface{}) { + name := reflect.TypeOf(source).Elem().Name() + TestObjectFuzzer.Fuzz(source) + + s := GetTestScheme() + data, err := s.Encode(source) + if err != nil { + t.Errorf("%v: %v (%#v)", name, err, source) + return + } + obj2, err := s.Decode(data) + if err != nil { + t.Errorf("%v: %v (%v)", name, err, string(data)) + return + } else { + if !reflect.DeepEqual(source, obj2) { + t.Errorf("1: %v: diff: %v", name, objDiff(source, obj2)) + return + } + } + obj3 := reflect.New(reflect.TypeOf(source).Elem()).Interface() + err = s.DecodeInto(data, obj3) + if err != nil { + t.Errorf("2: %v: %v", name, err) + return + } else { + if !reflect.DeepEqual(source, obj3) { + t.Errorf("3: %v: diff: %v", name, objDiff(source, obj3)) + return + } + } +} + +func TestTypes(t *testing.T) { + table := []interface{}{ + &TestType1{}, + &ExternalInternalSame{}, + } + for _, item := range table { + // Try a few times, since runTest uses random values. + for i := 0; i < *fuzzIters; i++ { + runTest(t, item) + } + } +} + +func TestEncode_NonPtr(t *testing.T) { + s := GetTestScheme() + tt := TestType1{A: "I'm not a pointer object"} + obj := interface{}(tt) + data, err := s.Encode(obj) + obj2, err2 := s.Decode(data) + if err != nil || err2 != nil { + t.Fatalf("Failure: '%v' '%v'", err, err2) + } + if _, ok := obj2.(*TestType1); !ok { + t.Fatalf("Got wrong type") + } + if !reflect.DeepEqual(obj2, &tt) { + t.Errorf("Expected:\n %#v,\n Got:\n %#v", &tt, obj2) + } +} + +func TestEncode_Ptr(t *testing.T) { + s := GetTestScheme() + tt := &TestType1{A: "I am a pointer object"} + obj := interface{}(tt) + data, err := s.Encode(obj) + obj2, err2 := s.Decode(data) + if err != nil || err2 != nil { + t.Fatalf("Failure: '%v' '%v'", err, err2) + } + if _, ok := obj2.(*TestType1); !ok { + t.Fatalf("Got wrong type") + } + if !reflect.DeepEqual(obj2, tt) { + t.Errorf("Expected:\n %#v,\n Got:\n %#v", &tt, obj2) + } +} + +func TestBadJSONRejection(t *testing.T) { + s := GetTestScheme() + badJSONs := [][]byte{ + []byte(`{"apiVersion":"v1"}`), // Missing kind + []byte(`{"kind":"TestType1"}`), // Missing version + []byte(`{"apiVersion":"v1","kind":"bar"}`), // Unknown kind + []byte(`{"apiVersion":"bar","kind":"TestType1"}`), // Unknown version + } + for _, b := range badJSONs { + if _, err := s.Decode(b); err == nil { + t.Errorf("Did not reject bad json: %s", string(b)) + } + } + badJSONKindMismatch := []byte(`{"apiVersion":"v1","kind": "ExternalInternalSame"}`) + if err := s.DecodeInto(badJSONKindMismatch, &TestType1{}); err == nil { + t.Errorf("Kind is set but doesn't match the object type: %s", badJSONKindMismatch) + } +}