diff --git a/pkg/api/serialization_test.go b/pkg/api/serialization_test.go index b1c89045b5f..d77d0c9d567 100644 --- a/pkg/api/serialization_test.go +++ b/pkg/api/serialization_test.go @@ -35,6 +35,26 @@ var fuzzIters = flag.Int("fuzz_iters", 50, "How many fuzzing iterations to do.") // apiObjectFuzzer can randomly populate api objects. var apiObjectFuzzer = fuzz.New().NilChance(.5).NumElements(1, 1).Funcs( + func(j *runtime.PluginBase, c fuzz.Continue) { + // Do nothing; this struct has only a Kind field and it must stay blank in memory. + }, + func(j *runtime.JSONBase, c fuzz.Continue) { + // We have to customize the randomization of JSONBases because their + // APIVersion and Kind must remain blank in memory. + j.APIVersion = "" + j.Kind = "" + j.ID = c.RandString() + // 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. + j.ResourceVersion = c.RandUint64() >> 8 + j.SelfLink = c.RandString() + + var sec, nsec int64 + c.Fuzz(&sec) + c.Fuzz(&nsec) + j.CreationTimestamp = util.Unix(sec, nsec).Rfc3339Copy() + }, func(j *api.JSONBase, c fuzz.Continue) { // We have to customize the randomization of JSONBases because their // APIVersion and Kind must remain blank in memory. diff --git a/pkg/conversion/converter.go b/pkg/conversion/converter.go index b5f7f93c9d5..f61433910a3 100644 --- a/pkg/conversion/converter.go +++ b/pkg/conversion/converter.go @@ -39,12 +39,18 @@ type Converter struct { // If non-nil, will be called to print helpful debugging info. Quite verbose. Debug DebugLogger + + // NameFunc is called to retrieve the name of a type; this name is used for the + // purpose of deciding whether two types match or not (i.e., will we attempt to + // do a conversion). The default returns the go type name. + NameFunc func(t reflect.Type) string } // NewConverter creates a new Converter object. func NewConverter() *Converter { return &Converter{ - funcs: map[typePair]reflect.Value{}, + funcs: map[typePair]reflect.Value{}, + NameFunc: func(t reflect.Type) string { return t.Name() }, } } @@ -55,6 +61,80 @@ type Scope interface { // Call Convert to convert sub-objects. Note that if you call it with your own exact // parameters, you'll run out of stack space before anything useful happens. Convert(src, dest interface{}, flags FieldMatchingFlags) error + + // SrcTags and DestTags contain the struct tags that src and dest had, respectively. + // If the enclosing object was not a struct, then these will contain no tags, of course. + SrcTag() reflect.StructTag + DestTag() reflect.StructTag + + // Flags returns the flags with which the conversion was started. + Flags() FieldMatchingFlags + + // Meta returns any information originally passed to Convert. + Meta() *Meta +} + +// Meta is supplied by Scheme, when it calls Convert. +type Meta struct { + SrcVersion string + DestVersion string + + // TODO: If needed, add a user data field here. +} + +// scope contains information about an ongoing conversion. +type scope struct { + converter *Converter + meta *Meta + flags FieldMatchingFlags + srcTagStack []reflect.StructTag + destTagStack []reflect.StructTag +} + +// push adds a level to the src/dest tag stacks. +func (s *scope) push() { + s.srcTagStack = append(s.srcTagStack, "") + s.destTagStack = append(s.destTagStack, "") +} + +// pop removes a level to the src/dest tag stacks. +func (s *scope) pop() { + n := len(s.srcTagStack) + s.srcTagStack = s.srcTagStack[:n-1] + s.destTagStack = s.destTagStack[:n-1] +} + +func (s *scope) setSrcTag(tag reflect.StructTag) { + s.srcTagStack[len(s.srcTagStack)-1] = tag +} + +func (s *scope) setDestTag(tag reflect.StructTag) { + s.destTagStack[len(s.destTagStack)-1] = tag +} + +// Convert continues a conversion. +func (s *scope) Convert(src, dest interface{}, flags FieldMatchingFlags) error { + return s.converter.Convert(src, dest, flags, s.meta) +} + +// SrcTag returns the tag of the struct containing the current source item, if any. +func (s *scope) SrcTag() reflect.StructTag { + return s.srcTagStack[len(s.srcTagStack)-1] +} + +// DestTag returns the tag of the struct containing the current dest item, if any. +func (s *scope) DestTag() reflect.StructTag { + return s.destTagStack[len(s.destTagStack)-1] +} + +// Flags returns the flags with which the current conversion was started. +func (s *scope) Flags() FieldMatchingFlags { + return s.flags +} + +// Meta returns the meta object that was originally passed to Convert. +func (s *scope) Meta() *Meta { + return s.meta } // Register registers a conversion func with the Converter. conversionFunc must take @@ -82,7 +162,7 @@ func (c *Converter) Register(conversionFunc interface{}) error { if ft.In(1).Kind() != reflect.Ptr { return fmt.Errorf("expected pointer arg for 'in' param 1, got: %v", ft) } - scopeType := Scope(c) + scopeType := Scope(nil) if e, a := reflect.TypeOf(&scopeType).Elem(), ft.In(2); e != a { return fmt.Errorf("expected '%v' arg for 'in' param 2, got '%v' (%v)", e, a, ft) } @@ -127,8 +207,12 @@ func (f FieldMatchingFlags) IsSet(flag FieldMatchingFlags) bool { // 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. +// Read the comments on the various FieldMatchingFlags constants to understand +// what the 'flags' parameter does. +// 'meta' is given to allow you to pass information to conversion functions, +// it is not used by Convert() other than storing it in the scope. // Not safe for objects with cyclic references! -func (c *Converter) Convert(src, dest interface{}, flags FieldMatchingFlags) error { +func (c *Converter) Convert(src, dest interface{}, flags FieldMatchingFlags, meta *Meta) error { dv, sv := reflect.ValueOf(dest), reflect.ValueOf(src) if dv.Kind() != reflect.Ptr { return fmt.Errorf("Need pointer, but got %#v", dest) @@ -141,18 +225,24 @@ func (c *Converter) Convert(src, dest interface{}, flags FieldMatchingFlags) err if !dv.CanAddr() { return fmt.Errorf("Can't write to dest") } - return c.convert(sv, dv, flags) + s := &scope{ + converter: c, + flags: flags, + meta: meta, + } + s.push() // Easy way to make SrcTag and DestTag never fail + return c.convert(sv, dv, s) } // 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 { +func (c *Converter) convert(sv, dv reflect.Value, scope *scope) 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) } - args := []reflect.Value{sv.Addr(), dv.Addr(), reflect.ValueOf(Scope(c))} + args := []reflect.Value{sv.Addr(), dv.Addr(), reflect.ValueOf(scope)} ret := fv.Call(args)[0].Interface() // This convolution is necssary because nil interfaces won't convert // to errors. @@ -162,7 +252,7 @@ func (c *Converter) convert(sv, dv reflect.Value, flags FieldMatchingFlags) erro return ret.(error) } - if !flags.IsSet(AllowDifferentFieldTypeNames) && dt.Name() != st.Name() { + if !scope.flags.IsSet(AllowDifferentFieldTypeNames) && c.NameFunc(dt) != c.NameFunc(st) { return fmt.Errorf("Can't convert %v to %v because type names don't match.", st, dt) } @@ -180,28 +270,41 @@ func (c *Converter) convert(sv, dv reflect.Value, flags FieldMatchingFlags) erro c.Debug.Logf("Trying to convert '%v' to '%v'", st, dt) } + scope.push() + defer scope.pop() + switch dv.Kind() { case reflect.Struct: listType := dt - if flags.IsSet(SourceToDest) { + if scope.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 sf.IsValid() { + // No need to check error, since we know it's valid. + field, _ := st.FieldByName(f.Name) + scope.setSrcTag(field.Tag) + } + if df.IsValid() { + field, _ := dt.FieldByName(f.Name) + scope.setDestTag(field.Tag) + } + // TODO: set top level of scope.src/destTagStack with these field tags here. if !df.IsValid() || !sf.IsValid() { switch { - case flags.IsSet(IgnoreMissingFields): + case scope.flags.IsSet(IgnoreMissingFields): // No error. - case flags.IsSet(SourceToDest): + case scope.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 { + if err := c.convert(sf, df, scope); err != nil { return err } } @@ -213,7 +316,7 @@ func (c *Converter) convert(sv, dv reflect.Value, flags FieldMatchingFlags) erro } 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 { + if err := c.convert(sv.Index(i), dv.Index(i), scope); err != nil { return err } } @@ -224,7 +327,7 @@ func (c *Converter) convert(sv, dv reflect.Value, flags FieldMatchingFlags) erro return nil } dv.Set(reflect.New(dt.Elem())) - return c.convert(sv.Elem(), dv.Elem(), flags) + return c.convert(sv.Elem(), dv.Elem(), scope) case reflect.Map: if sv.IsNil() { // Don't copy a nil ptr! @@ -234,11 +337,11 @@ func (c *Converter) convert(sv, dv reflect.Value, flags FieldMatchingFlags) erro 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 { + if err := c.convert(sk, dk, scope); err != nil { return err } dkv := reflect.New(dt.Elem()).Elem() - if err := c.convert(sv.MapIndex(sk), dkv, flags); err != nil { + if err := c.convert(sv.MapIndex(sk), dkv, scope); err != nil { return err } dv.SetMapIndex(dk, dkv) diff --git a/pkg/conversion/converter_test.go b/pkg/conversion/converter_test.go index 96c569cd6cd..573c9791ded 100644 --- a/pkg/conversion/converter_test.go +++ b/pkg/conversion/converter_test.go @@ -35,6 +35,7 @@ func TestConverter_CallsRegisteredFunctions(t *testing.T) { } type C struct{} c := NewConverter() + c.Debug = t err := c.Register(func(in *A, out *B, s Scope) error { out.Bar = in.Foo return s.Convert(&in.Baz, &out.Baz, 0) @@ -53,7 +54,7 @@ func TestConverter_CallsRegisteredFunctions(t *testing.T) { x := A{"hello, intrepid test reader!", 3} y := B{} - err = c.Convert(&x, &y, 0) + err = c.Convert(&x, &y, 0, nil) if err != nil { t.Fatalf("unexpected error %v", err) } @@ -67,7 +68,7 @@ func TestConverter_CallsRegisteredFunctions(t *testing.T) { z := B{"all your test are belong to us", 42} w := A{} - err = c.Convert(&z, &w, 0) + err = c.Convert(&z, &w, 0, nil) if err != nil { t.Fatalf("unexpected error %v", err) } @@ -85,36 +86,43 @@ func TestConverter_CallsRegisteredFunctions(t *testing.T) { t.Fatalf("unexpected error %v", err) } - err = c.Convert(&A{}, &C{}, 0) + err = c.Convert(&A{}, &C{}, 0, nil) if err == nil { t.Errorf("unexpected non-error") } } func TestConverter_fuzz(t *testing.T) { - newAnonType := func() interface{} { - return reflect.New(reflect.TypeOf(externalTypeReturn()).Elem()).Interface() - } // Use the same types from the scheme test. table := []struct { from, to, check interface{} }{ - {&TestType1{}, newAnonType(), &TestType1{}}, - {newAnonType(), &TestType1{}, newAnonType()}, + {&TestType1{}, &ExternalTestType1{}, &TestType1{}}, + {&ExternalTestType1{}, &TestType1{}, &ExternalTestType1{}}, } f := fuzz.New().NilChance(.5).NumElements(0, 100) c := NewConverter() + c.NameFunc = func(t reflect.Type) string { + // Hide the fact that we don't have separate packages for these things. + return map[reflect.Type]string{ + reflect.TypeOf(TestType1{}): "TestType1", + reflect.TypeOf(ExternalTestType1{}): "TestType1", + reflect.TypeOf(TestType2{}): "TestType2", + reflect.TypeOf(ExternalTestType2{}): "TestType2", + }[t] + } + c.Debug = t for i, item := range table { for j := 0; j < *fuzzIters; j++ { f.Fuzz(item.from) - err := c.Convert(item.from, item.to, 0) + err := c.Convert(item.from, item.to, 0, nil) if err != nil { t.Errorf("(%v, %v): unexpected error: %v", i, j, err) continue } - err = c.Convert(item.to, item.check, 0) + err = c.Convert(item.to, item.check, 0, nil) if err != nil { t.Errorf("(%v, %v): unexpected error: %v", i, j, err) continue @@ -126,6 +134,72 @@ func TestConverter_fuzz(t *testing.T) { } } +func TestConverter_tags(t *testing.T) { + type Foo struct { + A string `test:"foo"` + } + type Bar struct { + A string `test:"bar"` + } + c := NewConverter() + c.Debug = t + err := c.Register( + func(in *string, out *string, s Scope) error { + if e, a := "foo", s.SrcTag().Get("test"); e != a { + t.Errorf("expected %v, got %v", e, a) + } + if e, a := "bar", s.DestTag().Get("test"); e != a { + t.Errorf("expected %v, got %v", e, a) + } + return nil + }, + ) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + c.Convert(&Foo{}, &Bar{}, 0, nil) +} + +func TestConverter_meta(t *testing.T) { + type Foo struct{ A string } + type Bar struct{ A string } + c := NewConverter() + c.Debug = t + checks := 0 + err := c.Register( + func(in *Foo, out *Bar, s Scope) error { + if s.Meta() == nil || s.Meta().SrcVersion != "test" || s.Meta().DestVersion != "passes" { + t.Errorf("Meta did not get passed!") + } + checks++ + s.Convert(&in.A, &out.A, 0) + return nil + }, + ) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + err = c.Register( + func(in *string, out *string, s Scope) error { + if s.Meta() == nil || s.Meta().SrcVersion != "test" || s.Meta().DestVersion != "passes" { + t.Errorf("Meta did not get passed a second time!") + } + checks++ + return nil + }, + ) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + err = c.Convert(&Foo{}, &Bar{}, 0, &Meta{SrcVersion: "test", DestVersion: "passes"}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if checks != 2 { + t.Errorf("Registered functions did not get called.") + } +} + func TestConverter_flags(t *testing.T) { type Foo struct{ A string } type Bar struct{ A string } @@ -199,11 +273,12 @@ func TestConverter_flags(t *testing.T) { } f := fuzz.New().NilChance(.5).NumElements(0, 100) c := NewConverter() + c.Debug = t for i, item := range table { for j := 0; j < *fuzzIters; j++ { f.Fuzz(item.from) - err := c.Convert(item.from, item.to, item.flags) + err := c.Convert(item.from, item.to, item.flags, nil) if item.shouldSucceed && err != nil { t.Errorf("(%v, %v): unexpected error: %v", i, j, err) continue diff --git a/pkg/conversion/decode.go b/pkg/conversion/decode.go index 530f74ab571..c2c67e72092 100644 --- a/pkg/conversion/decode.go +++ b/pkg/conversion/decode.go @@ -58,7 +58,7 @@ func (s *Scheme) Decode(data []byte) (interface{}, error) { if err != nil { return nil, err } - err = s.converter.Convert(obj, objOut, 0) + err = s.converter.Convert(obj, objOut, 0, s.generateConvertMeta(version, s.InternalVersion)) if err != nil { return nil, err } @@ -112,7 +112,7 @@ func (s *Scheme) DecodeInto(data []byte, obj interface{}) error { if err != nil { return err } - err = s.converter.Convert(external, obj, 0) + err = s.converter.Convert(external, obj, 0, s.generateConvertMeta(dataVersion, objVersion)) if err != nil { return err } diff --git a/pkg/conversion/encode.go b/pkg/conversion/encode.go index 554a6dc4437..e82ddce9236 100644 --- a/pkg/conversion/encode.go +++ b/pkg/conversion/encode.go @@ -81,7 +81,7 @@ func (s *Scheme) EncodeToVersion(obj interface{}, destVersion string) (data []by if err != nil { return nil, err } - err = s.converter.Convert(obj, objOut, 0) + err = s.converter.Convert(obj, objOut, 0, s.generateConvertMeta(objVersion, destVersion)) if err != nil { return nil, err } diff --git a/pkg/conversion/scheme.go b/pkg/conversion/scheme.go index af88ba4e63e..c77b80e4703 100644 --- a/pkg/conversion/scheme.go +++ b/pkg/conversion/scheme.go @@ -51,6 +51,10 @@ type Scheme struct { // is registered for multiple versions, the last one wins. typeToVersion map[reflect.Type]string + // typeToKind allows one to figure out the desired "kind" field for a given + // go object. Requirements and caveats are the same as typeToVersion. + typeToKind map[reflect.Type]string + // converter stores all registered conversion functions. It also has // default coverting behavior. converter *Converter @@ -73,14 +77,26 @@ type Scheme struct { // NewScheme manufactures a new scheme. func NewScheme() *Scheme { - return &Scheme{ + s := &Scheme{ versionMap: map[string]map[string]reflect.Type{}, typeToVersion: map[reflect.Type]string{}, + typeToKind: map[reflect.Type]string{}, converter: NewConverter(), InternalVersion: "", ExternalVersion: "v1", MetaInsertionFactory: metaInsertion{}, } + s.converter.NameFunc = s.nameFunc + return s +} + +// nameFunc returns the name of the type that we wish to use for encoding. Defaults to +// the go name of the type if the type is not registered. +func (s *Scheme) nameFunc(t reflect.Type) string { + if kind, ok := s.typeToKind[t]; ok { + return kind + } + return t.Name() } // AddKnownTypes registers all types passed in 'types' as being members of version 'version. @@ -104,9 +120,32 @@ func (s *Scheme) AddKnownTypes(version string, types ...interface{}) { } knownTypes[t.Name()] = t s.typeToVersion[t] = version + s.typeToKind[t] = t.Name() } } +// AddKnownTypeWithName is like AddKnownTypes, but it lets you specify what this type should +// be encoded as. Useful for testing when you don't want to make multiple packages to define +// your structs. +func (s *Scheme) AddKnownTypeWithName(version, kind string, obj interface{}) { + knownTypes, found := s.versionMap[version] + if !found { + knownTypes = map[string]reflect.Type{} + s.versionMap[version] = knownTypes + } + t := reflect.TypeOf(obj) + if t.Kind() != reflect.Ptr { + panic("All types must be pointers to structs.") + } + t = t.Elem() + if t.Kind() != reflect.Struct { + panic("All types must be pointers to structs.") + } + knownTypes[kind] = t + s.typeToVersion[t] = version + s.typeToKind[t] = kind +} + // 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) { @@ -124,9 +163,26 @@ func (s *Scheme) NewObject(versionName, typeName string) (interface{}, error) { // sub-objects. We deduce how to call these functions from the types of their 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 -// s.Convert() inside your conversionFuncs, as long as you don't start a conversion -// chain that's infinitely recursive. +// Note that, if you need to copy sub-objects that didn't change, you can use the +// conversion.Scope object that will be passed to your conversion function. +// Additionally, all conversions started by Scheme will set the SrcVersion and +// DestVersion fields on the Meta object. Example: +// +// s.AddConversionFuncs( +// func(in *InternalObject, out *ExternalObject, scope conversion.Scope) error { +// // You can depend on Meta() being non-nil, and this being set to +// // the source version, e.g., "" +// s.Meta().SrcVersion +// // You can depend on this being set to the destination version, +// // e.g., "v1beta1". +// s.Meta().DestVersion +// // Call scope.Convert to copy sub-fields. +// s.Convert(&in.SubFieldThatMoved, &out.NewLocation.NewName, 0) +// return nil +// }, +// ) +// +// (For more detail about conversion functions, see Converter.Register's comment.) // // Also note that the default behavior, if you don't add a conversion function, is to // sanely copy fields that have the same names and same type names. It's OK if the @@ -144,9 +200,28 @@ func (s *Scheme) AddConversionFuncs(conversionFuncs ...interface{}) error { // 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. +// possible. You can call this with types that haven't been registered (for example, +// a to test conversion of types that are nested within registered types), but in +// that case, the conversion.Scope object passed to your conversion functions won't +// have SrcVersion or DestVersion fields set correctly in Meta(). func (s *Scheme) Convert(in, out interface{}) error { - return s.converter.Convert(in, out, 0) + inVersion := "unknown" + outVersion := "unknown" + if v, _, err := s.ObjectVersionAndKind(in); err == nil { + inVersion = v + } + if v, _, err := s.ObjectVersionAndKind(out); err == nil { + outVersion = v + } + return s.converter.Convert(in, out, 0, s.generateConvertMeta(inVersion, outVersion)) +} + +// generateConvertMeta constructs the meta value we pass to Convert. +func (s *Scheme) generateConvertMeta(srcVersion, destVersion string) *Meta { + return &Meta{ + SrcVersion: srcVersion, + DestVersion: destVersion, + } } // metaInsertion provides a default implementation of MetaInsertionFactory. @@ -194,11 +269,12 @@ func (s *Scheme) ObjectVersionAndKind(obj interface{}) (apiVersion, kind string, return "", "", err } t := v.Type() - if version, ok := s.typeToVersion[t]; !ok { + version, vOK := s.typeToVersion[t] + kind, kOK := s.typeToKind[t] + if !vOK || !kOK { return "", "", fmt.Errorf("Unregistered type: %v", t) - } else { - return version, t.Name(), nil } + return version, kind, nil } // SetVersionAndKind sets the version and kind fields (with help from @@ -206,7 +282,7 @@ func (s *Scheme) ObjectVersionAndKind(obj interface{}) (apiVersion, kind string, // must be a pointer. func (s *Scheme) SetVersionAndKind(version, kind string, obj interface{}) error { versionAndKind := s.MetaInsertionFactory.Create(version, kind) - return s.converter.Convert(versionAndKind, obj, SourceToDest|IgnoreMissingFields|AllowDifferentFieldTypeNames) + return s.converter.Convert(versionAndKind, obj, SourceToDest|IgnoreMissingFields|AllowDifferentFieldTypeNames, nil) } // maybeCopy copies obj if it is not a pointer, to get a settable/addressable diff --git a/pkg/conversion/scheme_test.go b/pkg/conversion/scheme_test.go index c8232ff4ea6..d59f6dde450 100644 --- a/pkg/conversion/scheme_test.go +++ b/pkg/conversion/scheme_test.go @@ -63,34 +63,28 @@ type TestType2 struct { 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 { - MyWeirdCustomEmbeddedVersionKindField `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 ExternalTestType2 struct { + A string `yaml:"A,omitempty" json:"A,omitempty"` + B int `yaml:"B,omitempty" json:"B,omitempty"` +} +type ExternalTestType1 struct { + MyWeirdCustomEmbeddedVersionKindField `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]ExternalTestType2 `yaml:"N,omitempty" json:"N,omitempty"` + O *ExternalTestType2 `yaml:"O,omitempty" json:"O,omitempty"` + P []ExternalTestType2 `yaml:"Q,omitempty" json:"Q,omitempty"` } type ExternalInternalSame struct { @@ -124,8 +118,13 @@ var TestObjectFuzzer = fuzz.New().NilChance(.5).NumElements(1, 100).Funcs( // Returns a new Scheme set up with the test objects. func GetTestScheme() *Scheme { s := NewScheme() - s.AddKnownTypes("", &TestType1{}, &ExternalInternalSame{}) - s.AddKnownTypes("v1", externalTypeReturn(), &ExternalInternalSame{}) + // Ordinarily, we wouldn't add TestType2, but because this is a test and + // both types are from the same package, we need to get it into the system + // so that converter will match it with ExternalType2. + s.AddKnownTypes("", &TestType1{}, &TestType2{}, &ExternalInternalSame{}) + s.AddKnownTypes("v1", &ExternalInternalSame{}) + s.AddKnownTypeWithName("v1", "TestType1", &ExternalTestType1{}) + s.AddKnownTypeWithName("v1", "TestType2", &ExternalTestType2{}) s.ExternalVersion = "v1" s.InternalVersion = "" s.MetaInsertionFactory = testMetaInsertionFactory{} @@ -270,3 +269,145 @@ func TestBadJSONRejection(t *testing.T) { t.Errorf("Kind is set but doesn't match the object type: %s", badJSONKindMismatch) } } + +func TestMetaValues(t *testing.T) { + type InternalSimple struct { + Version string `json:"version,omitempty" yaml:"version,omitempty"` + Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` + TestString string `json:"testString" yaml:"testString"` + } + type ExternalSimple struct { + Version string `json:"version,omitempty" yaml:"version,omitempty"` + Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` + TestString string `json:"testString" yaml:"testString"` + } + s := NewScheme() + s.InternalVersion = "" + s.ExternalVersion = "externalVersion" + s.AddKnownTypeWithName("", "Simple", &InternalSimple{}) + s.AddKnownTypeWithName("externalVersion", "Simple", &ExternalSimple{}) + + internalToExternalCalls := 0 + externalToInternalCalls := 0 + + // Register functions to verify that scope.Meta() gets set correctly. + err := s.AddConversionFuncs( + func(in *InternalSimple, out *ExternalSimple, scope Scope) error { + if e, a := "", scope.Meta().SrcVersion; e != a { + t.Errorf("Expected '%v', got '%v'", e, a) + } + if e, a := "externalVersion", scope.Meta().DestVersion; e != a { + t.Errorf("Expected '%v', got '%v'", e, a) + } + scope.Convert(&in.TestString, &out.TestString, 0) + internalToExternalCalls++ + return nil + }, + func(in *ExternalSimple, out *InternalSimple, scope Scope) error { + if e, a := "externalVersion", scope.Meta().SrcVersion; e != a { + t.Errorf("Expected '%v', got '%v'", e, a) + } + if e, a := "", scope.Meta().DestVersion; e != a { + t.Errorf("Expected '%v', got '%v'", e, a) + } + scope.Convert(&in.TestString, &out.TestString, 0) + externalToInternalCalls++ + return nil + }, + ) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + simple := &InternalSimple{ + TestString: "foo", + } + + // Test Encode, Decode, and DecodeInto + data, err := s.EncodeToVersion(simple, "externalVersion") + obj2, err2 := s.Decode(data) + obj3 := &InternalSimple{} + err3 := s.DecodeInto(data, obj3) + if err != nil || err2 != nil { + t.Fatalf("Failure: '%v' '%v' '%v'", err, err2, err3) + } + if _, ok := obj2.(*InternalSimple); !ok { + t.Fatalf("Got wrong type") + } + if e, a := simple, obj2; !reflect.DeepEqual(e, a) { + t.Errorf("Expected:\n %#v,\n Got:\n %#v", e, a) + } + if e, a := simple, obj3; !reflect.DeepEqual(e, a) { + t.Errorf("Expected:\n %#v,\n Got:\n %#v", e, a) + } + + // Test Convert + external := &ExternalSimple{} + err = s.Convert(simple, external) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if e, a := simple.TestString, external.TestString; e != a { + t.Errorf("Expected %v, got %v", e, a) + } + + // Encode and Convert should each have caused an increment. + if e, a := 2, internalToExternalCalls; e != a { + t.Errorf("Expected %v, got %v", e, a) + } + // Decode and DecodeInto should each have caused an increment. + if e, a := 2, externalToInternalCalls; e != a { + t.Errorf("Expected %v, got %v", e, a) + } +} + +func TestMetaValuesUnregisteredConvert(t *testing.T) { + type InternalSimple struct { + Version string `json:"version,omitempty" yaml:"version,omitempty"` + Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` + TestString string `json:"testString" yaml:"testString"` + } + type ExternalSimple struct { + Version string `json:"version,omitempty" yaml:"version,omitempty"` + Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` + TestString string `json:"testString" yaml:"testString"` + } + s := NewScheme() + s.InternalVersion = "" + s.ExternalVersion = "externalVersion" + // We deliberately don't register the types. + + internalToExternalCalls := 0 + + // Register functions to verify that scope.Meta() gets set correctly. + err := s.AddConversionFuncs( + func(in *InternalSimple, out *ExternalSimple, scope Scope) error { + if e, a := "unknown", scope.Meta().SrcVersion; e != a { + t.Errorf("Expected '%v', got '%v'", e, a) + } + if e, a := "unknown", scope.Meta().DestVersion; e != a { + t.Errorf("Expected '%v', got '%v'", e, a) + } + scope.Convert(&in.TestString, &out.TestString, 0) + internalToExternalCalls++ + return nil + }, + ) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + simple := &InternalSimple{TestString: "foo"} + external := &ExternalSimple{} + err = s.Convert(simple, external) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if e, a := simple.TestString, external.TestString; e != a { + t.Errorf("Expected %v, got %v", e, a) + } + + // Verify that our conversion handler got called. + if e, a := 1, internalToExternalCalls; e != a { + t.Errorf("Expected %v, got %v", e, a) + } +} diff --git a/pkg/kubecfg/parse_test.go b/pkg/kubecfg/parse_test.go index f09b47fe9b5..5681bfd7610 100644 --- a/pkg/kubecfg/parse_test.go +++ b/pkg/kubecfg/parse_test.go @@ -17,6 +17,7 @@ limitations under the License. package kubecfg import ( + "encoding/json" "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" @@ -34,7 +35,9 @@ func TestParseBadStorage(t *testing.T) { func DoParseTest(t *testing.T, storage string, obj runtime.Object, p *Parser) { jsonData, _ := runtime.DefaultCodec.Encode(obj) - yamlData, _ := yaml.Marshal(obj) + var tmp map[string]interface{} + json.Unmarshal(jsonData, &tmp) + yamlData, _ := yaml.Marshal(tmp) t.Logf("Intermediate yaml:\n%v\n", string(yamlData)) t.Logf("Intermediate json:\n%v\n", string(jsonData)) jsonGot, jsonErr := p.ToWireFormat(jsonData, storage, runtime.DefaultCodec) diff --git a/pkg/runtime/extension.go b/pkg/runtime/extension.go new file mode 100644 index 00000000000..4d731cb4838 --- /dev/null +++ b/pkg/runtime/extension.go @@ -0,0 +1,55 @@ +/* +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 runtime + +import ( + "gopkg.in/v1/yaml" +) + +func (re *RawExtension) UnmarshalJSON(in []byte) error { + re.RawJSON = in + return nil +} + +func (re *RawExtension) MarshalJSON() ([]byte, error) { + return re.RawJSON, nil +} + +// SetYAML implements the yaml.Setter interface. +func (re *RawExtension) SetYAML(tag string, value interface{}) bool { + if value == nil { + re.RawJSON = []byte("null") + return true + } + // Why does the yaml package send value as a map[interface{}]interface{}? + // It's especially frustrating because encoding/json does the right thing + // by giving a []byte. So here we do the embarrasing thing of re-encode and + // de-encode the right way. + // TODO: Write a version of Decode that uses reflect to turn this value + // into an API object. + b, err := yaml.Marshal(value) + if err != nil { + panic("yaml can't reverse its own object") + } + re.RawJSON = b + return true +} + +// GetYAML implements the yaml.Getter interface. +func (re *RawExtension) GetYAML() (tag string, value interface{}) { + return tag, re.RawJSON +} diff --git a/pkg/runtime/helper_test.go b/pkg/runtime/helper_test.go deleted file mode 100644 index b68178a3227..00000000000 --- a/pkg/runtime/helper_test.go +++ /dev/null @@ -1,81 +0,0 @@ -/* -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 runtime_test - -import ( - "reflect" - "testing" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - _ "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" - "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" -) - -func TestEncode(t *testing.T) { - pod := &api.Pod{ - Labels: map[string]string{"name": "foo"}, - } - obj := runtime.Object(pod) - data, err := runtime.DefaultScheme.Encode(obj) - obj2, err2 := runtime.DefaultScheme.Decode(data) - if err != nil || err2 != nil { - t.Fatalf("Failure: '%v' '%v'", err, err2) - } - if _, ok := obj2.(*api.Pod); !ok { - t.Fatalf("Got wrong type") - } - if !reflect.DeepEqual(obj2, pod) { - t.Errorf("Expected:\n %#v,\n Got:\n %#v", &pod, obj2) - } -} - -func TestBadJSONRejection(t *testing.T) { - badJSONMissingKind := []byte(`{ }`) - if _, err := runtime.DefaultScheme.Decode(badJSONMissingKind); err == nil { - t.Errorf("Did not reject despite lack of kind field: %s", badJSONMissingKind) - } - badJSONUnknownType := []byte(`{"kind": "bar"}`) - if _, err1 := runtime.DefaultScheme.Decode(badJSONUnknownType); err1 == nil { - t.Errorf("Did not reject despite use of unknown type: %s", badJSONUnknownType) - } - /*badJSONKindMismatch := []byte(`{"kind": "Pod"}`) - if err2 := DecodeInto(badJSONKindMismatch, &Minion{}); err2 == nil { - t.Errorf("Kind is set but doesn't match the object type: %s", badJSONKindMismatch) - }*/ -} - -func TestExtractList(t *testing.T) { - pl := &api.PodList{ - Items: []api.Pod{ - {JSONBase: api.JSONBase{ID: "1"}}, - {JSONBase: api.JSONBase{ID: "2"}}, - {JSONBase: api.JSONBase{ID: "3"}}, - }, - } - list, err := runtime.ExtractList(pl) - if err != nil { - t.Fatalf("Unexpected error %v", err) - } - if e, a := len(list), len(pl.Items); e != a { - t.Fatalf("Expected %v, got %v", e, a) - } - for i := range list { - if e, a := list[i].(*api.Pod).ID, pl.Items[i].ID; e != a { - t.Fatalf("Expected %v, got %v", e, a) - } - } -} diff --git a/pkg/runtime/helper.go b/pkg/runtime/scheme.go similarity index 71% rename from pkg/runtime/helper.go rename to pkg/runtime/scheme.go index 840a9fddcca..1e22560a6d3 100644 --- a/pkg/runtime/helper.go +++ b/pkg/runtime/scheme.go @@ -34,6 +34,125 @@ type Scheme struct { raw *conversion.Scheme } +// fromScope gets the input version, desired output version, and desired Scheme +// from a conversion.Scope. +func fromScope(s conversion.Scope) (inVersion, outVersion string, scheme *Scheme) { + scheme = DefaultScheme + inVersion = s.Meta().SrcVersion + outVersion = s.Meta().DestVersion + return inVersion, outVersion, scheme +} + +func init() { + // Set up a generic mapping between RawExtension and EmbeddedObject. + DefaultScheme.AddConversionFuncs( + embeddedObjectToRawExtension, + rawExtensionToEmbeddedObject, + ) +} + +// emptyPlugin is used to copy the Kind field to and from plugin objects. +type emptyPlugin struct { + PluginBase `json:",inline" yaml:",inline"` +} + +// embeddedObjectToRawExtension does the conversion you would expect from the name, using the information +// given in conversion.Scope. It's placed in the DefaultScheme as a ConversionFunc to enable plugins; +// see the comment for RawExtension. +func embeddedObjectToRawExtension(in *EmbeddedObject, out *RawExtension, s conversion.Scope) error { + if in.Object == nil { + out.RawJSON = []byte("null") + return nil + } + + // Figure out the type and kind of the output object. + _, outVersion, scheme := fromScope(s) + _, kind, err := scheme.raw.ObjectVersionAndKind(in.Object) + if err != nil { + return err + } + + // Manufacture an object of this type and kind. + outObj, err := scheme.New(outVersion, kind) + if err != nil { + return err + } + + // Manually do the conversion. + err = s.Convert(in.Object, outObj, 0) + if err != nil { + return err + } + + // Copy the kind field into the ouput object. + err = s.Convert( + &emptyPlugin{PluginBase: PluginBase{Kind: kind}}, + outObj, + conversion.SourceToDest|conversion.IgnoreMissingFields|conversion.AllowDifferentFieldTypeNames, + ) + if err != nil { + return err + } + // Because we provide the correct version, EncodeToVersion will not attempt a conversion. + raw, err := scheme.EncodeToVersion(outObj, outVersion) + if err != nil { + // TODO: if this fails, create an Unknown-- maybe some other + // component will understand it. + return err + } + out.RawJSON = raw + return nil +} + +// rawExtensionToEmbeddedObject does the conversion you would expect from the name, using the information +// given in conversion.Scope. It's placed in the DefaultScheme as a ConversionFunc to enable plugins; +// see the comment for RawExtension. +func rawExtensionToEmbeddedObject(in *RawExtension, out *EmbeddedObject, s conversion.Scope) error { + if len(in.RawJSON) == 4 && string(in.RawJSON) == "null" { + out.Object = nil + return nil + } + // Figure out the type and kind of the output object. + inVersion, outVersion, scheme := fromScope(s) + _, kind, err := scheme.raw.DataVersionAndKind(in.RawJSON) + if err != nil { + return err + } + + // We have to make this object ourselves because we don't store the version field for + // plugin objects. + inObj, err := scheme.New(inVersion, kind) + if err != nil { + return err + } + + err = scheme.DecodeInto(in.RawJSON, inObj) + if err != nil { + return err + } + + // Make the desired internal version, and do the conversion. + outObj, err := scheme.New(outVersion, kind) + if err != nil { + return err + } + err = scheme.Convert(inObj, outObj) + if err != nil { + return err + } + // Last step, clear the Kind field; that should always be blank in memory. + err = s.Convert( + &emptyPlugin{PluginBase: PluginBase{Kind: ""}}, + outObj, + conversion.SourceToDest|conversion.IgnoreMissingFields|conversion.AllowDifferentFieldTypeNames, + ) + if err != nil { + return err + } + out.Object = outObj + return nil +} + // NewScheme creates a new Scheme. A default scheme is provided and accessible // as the "DefaultScheme" variable. func NewScheme(internalVersion, externalVersion string) *Scheme { @@ -54,6 +173,13 @@ func (s *Scheme) AddKnownTypes(version string, types ...Object) { s.raw.AddKnownTypes(version, interfaces...) } +// AddKnownTypeWithName is like AddKnownTypes, but it lets you specify what this type should +// be encoded as. Useful for testing when you don't want to make multiple packages to define +// your structs. +func (s *Scheme) AddKnownTypeWithName(version, kind string, obj Object) { + s.raw.AddKnownTypeWithName(version, kind, obj) +} + // New returns a new API object of the given version ("" for internal // representation) and name, or an error if it hasn't been registered. func (s *Scheme) New(versionName, typeName string) (Object, error) { @@ -153,6 +279,11 @@ func (s *Scheme) Encode(obj Object) (data []byte, err error) { return s.raw.Encode(obj) } +// EncodeToVersion is like Encode, but lets you specify the destination version. +func (s *Scheme) EncodeToVersion(obj Object, destVersion string) (data []byte, err error) { + return s.raw.EncodeToVersion(obj, destVersion) +} + // enforcePtr 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. diff --git a/pkg/runtime/scheme_test.go b/pkg/runtime/scheme_test.go new file mode 100644 index 00000000000..c317c6f7efa --- /dev/null +++ b/pkg/runtime/scheme_test.go @@ -0,0 +1,211 @@ +/* +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 runtime_test + +import ( + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +type JSONBase struct { + Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` + APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` +} + +type InternalSimple struct { + JSONBase `json:",inline" yaml:",inline"` + TestString string `json:"testString" yaml:"testString"` +} + +type ExternalSimple struct { + JSONBase `json:",inline" yaml:",inline"` + TestString string `json:"testString" yaml:"testString"` +} + +func (*InternalSimple) IsAnAPIObject() {} +func (*ExternalSimple) IsAnAPIObject() {} + +func TestScheme(t *testing.T) { + runtime.DefaultScheme.AddKnownTypeWithName("", "Simple", &InternalSimple{}) + runtime.DefaultScheme.AddKnownTypeWithName("externalVersion", "Simple", &ExternalSimple{}) + + internalToExternalCalls := 0 + externalToInternalCalls := 0 + + // Register functions to verify that scope.Meta() gets set correctly. + err := runtime.DefaultScheme.AddConversionFuncs( + func(in *InternalSimple, out *ExternalSimple, scope conversion.Scope) error { + if e, a := "", scope.Meta().SrcVersion; e != a { + t.Errorf("Expected '%v', got '%v'", e, a) + } + if e, a := "externalVersion", scope.Meta().DestVersion; e != a { + t.Errorf("Expected '%v', got '%v'", e, a) + } + scope.Convert(&in.JSONBase, &out.JSONBase, 0) + scope.Convert(&in.TestString, &out.TestString, 0) + internalToExternalCalls++ + return nil + }, + func(in *ExternalSimple, out *InternalSimple, scope conversion.Scope) error { + if e, a := "externalVersion", scope.Meta().SrcVersion; e != a { + t.Errorf("Expected '%v', got '%v'", e, a) + } + if e, a := "", scope.Meta().DestVersion; e != a { + t.Errorf("Expected '%v', got '%v'", e, a) + } + scope.Convert(&in.JSONBase, &out.JSONBase, 0) + scope.Convert(&in.TestString, &out.TestString, 0) + externalToInternalCalls++ + return nil + }, + ) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + simple := &InternalSimple{ + TestString: "foo", + } + + // Test Encode, Decode, and DecodeInto + obj := runtime.Object(simple) + data, err := runtime.DefaultScheme.EncodeToVersion(obj, "externalVersion") + obj2, err2 := runtime.DefaultScheme.Decode(data) + obj3 := &InternalSimple{} + err3 := runtime.DefaultScheme.DecodeInto(data, obj3) + if err != nil || err2 != nil { + t.Fatalf("Failure: '%v' '%v' '%v'", err, err2, err3) + } + if _, ok := obj2.(*InternalSimple); !ok { + t.Fatalf("Got wrong type") + } + if e, a := simple, obj2; !reflect.DeepEqual(e, a) { + t.Errorf("Expected:\n %#v,\n Got:\n %#v", e, a) + } + if e, a := simple, obj3; !reflect.DeepEqual(e, a) { + t.Errorf("Expected:\n %#v,\n Got:\n %#v", e, a) + } + + // Test Convert + external := &ExternalSimple{} + err = runtime.DefaultScheme.Convert(simple, external) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if e, a := simple.TestString, external.TestString; e != a { + t.Errorf("Expected %v, got %v", e, a) + } + + // Encode and Convert should each have caused an increment. + if e, a := 2, internalToExternalCalls; e != a { + t.Errorf("Expected %v, got %v", e, a) + } + // Decode and DecodeInto should each have caused an increment. + if e, a := 2, externalToInternalCalls; e != a { + t.Errorf("Expected %v, got %v", e, a) + } +} + +func TestBadJSONRejection(t *testing.T) { + badJSONMissingKind := []byte(`{ }`) + if _, err := runtime.DefaultScheme.Decode(badJSONMissingKind); err == nil { + t.Errorf("Did not reject despite lack of kind field: %s", badJSONMissingKind) + } + badJSONUnknownType := []byte(`{"kind": "bar"}`) + if _, err1 := runtime.DefaultScheme.Decode(badJSONUnknownType); err1 == nil { + t.Errorf("Did not reject despite use of unknown type: %s", badJSONUnknownType) + } + /*badJSONKindMismatch := []byte(`{"kind": "Pod"}`) + if err2 := DecodeInto(badJSONKindMismatch, &Minion{}); err2 == nil { + t.Errorf("Kind is set but doesn't match the object type: %s", badJSONKindMismatch) + }*/ +} + +type ExtensionA struct { + runtime.PluginBase `json:",inline" yaml:",inline"` + TestString string `json:"testString" yaml:"testString"` +} + +type ExtensionB struct { + runtime.PluginBase `json:",inline" yaml:",inline"` + TestString string `json:"testString" yaml:"testString"` +} + +type ExternalExtensionType struct { + JSONBase `json:",inline" yaml:",inline"` + Extension runtime.RawExtension `json:"extension" yaml:"extension"` +} + +type InternalExtensionType struct { + JSONBase `json:",inline" yaml:",inline"` + Extension runtime.EmbeddedObject `json:"extension" yaml:"extension"` +} + +func (*ExtensionA) IsAnAPIObject() {} +func (*ExtensionB) IsAnAPIObject() {} +func (*ExternalExtensionType) IsAnAPIObject() {} +func (*InternalExtensionType) IsAnAPIObject() {} + +func TestExtensionMapping(t *testing.T) { + runtime.DefaultScheme.AddKnownTypeWithName("", "ExtensionType", &InternalExtensionType{}) + runtime.DefaultScheme.AddKnownTypeWithName("", "A", &ExtensionA{}) + runtime.DefaultScheme.AddKnownTypeWithName("", "B", &ExtensionB{}) + runtime.DefaultScheme.AddKnownTypeWithName("testExternal", "ExtensionType", &ExternalExtensionType{}) + runtime.DefaultScheme.AddKnownTypeWithName("testExternal", "A", &ExtensionA{}) + runtime.DefaultScheme.AddKnownTypeWithName("testExternal", "B", &ExtensionB{}) + + table := []struct { + obj runtime.Object + encoded string + }{ + { + &InternalExtensionType{Extension: runtime.EmbeddedObject{&ExtensionA{TestString: "foo"}}}, + `{"kind":"ExtensionType","apiVersion":"testExternal","extension":{"kind":"A","testString":"foo"}}`, + }, { + &InternalExtensionType{Extension: runtime.EmbeddedObject{&ExtensionB{TestString: "bar"}}}, + `{"kind":"ExtensionType","apiVersion":"testExternal","extension":{"kind":"B","testString":"bar"}}`, + }, { + &InternalExtensionType{Extension: runtime.EmbeddedObject{nil}}, + `{"kind":"ExtensionType","apiVersion":"testExternal","extension":null}`, + }, + } + + for _, item := range table { + gotEncoded, err := runtime.DefaultScheme.EncodeToVersion(item.obj, "testExternal") + if err != nil { + t.Errorf("unexpected error '%v' (%#v)", err, item.obj) + } else if e, a := item.encoded, string(gotEncoded); e != a { + t.Errorf("expected %v, got %v", e, a) + } + + gotDecoded, err := runtime.DefaultScheme.Decode([]byte(item.encoded)) + if err != nil { + t.Errorf("unexpected error '%v' (%v)", err, item.encoded) + } else if e, a := item.obj, gotDecoded; !reflect.DeepEqual(e, a) { + var eEx, aEx runtime.Object + if obj, ok := e.(*InternalExtensionType); ok { + eEx = obj.Extension.Object + } + if obj, ok := a.(*InternalExtensionType); ok { + aEx = obj.Extension.Object + } + t.Errorf("expected %#v, got %#v (%#v, %#v)", e, a, eEx, aEx) + } + } +} diff --git a/pkg/runtime/types.go b/pkg/runtime/types.go index 17148733950..b7ec6a0e169 100644 --- a/pkg/runtime/types.go +++ b/pkg/runtime/types.go @@ -43,6 +43,12 @@ type JSONBase struct { APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` } +// PluginBase is like JSONBase, but it's intended for plugin objects that won't ever be encoded +// except while embedded in other objects. +type PluginBase struct { + Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` +} + // EmbeddedObject has appropriate encoder and decoder functions, such that on the wire, it's // stored as a []byte, but in memory, the contained object is accessable as an Object // via the Get() function. Only valid API objects may be stored via EmbeddedObject. @@ -51,18 +57,69 @@ type JSONBase struct { // // Note that object assumes that you've registered all of your api types with the api package. // -// TODO(dbsmith): Stop using runtime.Codec, use the codec appropriate for the conversion (I have a plan). +// EmbeddedObject and RawExtension can be used together to allow for API object extensions: +// see the comment for RawExtension. type EmbeddedObject struct { Object } -// Extension allows api objects with unknown types to be passed-through. This can be used -// to deal with the API objects from a plug-in. Extension objects still have functioning -// JSONBase features-- kind, version, resourceVersion, etc. -// TODO: Not implemented yet -type Extension struct { - JSONBase `yaml:",inline" json:",inline"` - // RawJSON to go here. +// RawExtension is used with EmbeddedObject to do a two-phase encoding of extension objects. +// +// To use this, make a field which has RawExtension as its type in your external, versioned +// struct, and EmbeddedObject in your internal struct. You also need to register your +// various plugin types. +// +// // Internal package: +// type MyAPIObject struct { +// runtime.JSONBase `yaml:",inline" json:",inline"` +// MyPlugin runtime.EmbeddedObject `json:"myPlugin" yaml:"myPlugin"` +// } +// type PluginA struct { +// runtime.PluginBase `yaml:",inline" json:",inline"` +// AOption string `yaml:"aOption" json:"aOption"` +// } +// +// // External package: +// type MyAPIObject struct { +// runtime.JSONBase `yaml:",inline" json:",inline"` +// MyPlugin runtime.RawExtension `json:"myPlugin" yaml:"myPlugin"` +// } +// type PluginA struct { +// runtime.PluginBase `yaml:",inline" json:",inline"` +// AOption string `yaml:"aOption" json:"aOption"` +// } +// +// // On the wire, the JSON will look something like this: +// { +// "kind":"MyAPIObject", +// "apiVersion":"v1beta1", +// "myPlugin": { +// "kind":"PluginA", +// "aOption":"foo", +// }, +// } +// +// So what happens? Decode first uses json or yaml to unmarshal the serialized data into +// your external MyAPIObject. That causes the raw JSON to be stored, but not unpacked. +// The next step is to copy (using pkg/conversion) into the internal struct. The runtime +// package's DefaultScheme has conversion functions installed which will unpack the +// JSON stored in RawExtension, turning it into the correct object type, and storing it +// in the EmbeddedObject. (TODO: In the case where the object is of an unknown type, a +// runtime.Unknown object will be created and stored.) +type RawExtension struct { + RawJSON []byte } -func (*Extension) IsAnAPIObject() {} +// Unknown allows api objects with unknown types to be passed-through. This can be used +// to deal with the API objects from a plug-in. Unknown objects still have functioning +// JSONBase features-- kind, version, resourceVersion, etc. +// TODO: Not implemented yet! +type Unknown struct { + JSONBase `yaml:",inline" json:",inline"` + // RawJSON will hold the complete JSON of the object which couldn't be matched + // with a registered type. Most likely, nothing should be done with this + // except for passing it through the system. + RawJSON []byte +} + +func (*Unknown) IsAnAPIObject() {}