mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-30 15:05:27 +00:00
Merge pull request #1239 from lavalamp/fixApi
Proposal for solving api plugability
This commit is contained in:
commit
575c9cde4f
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
55
pkg/runtime/extension.go
Normal file
55
pkg/runtime/extension.go
Normal file
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
211
pkg/runtime/scheme_test.go
Normal file
211
pkg/runtime/scheme_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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() {}
|
||||
|
Loading…
Reference in New Issue
Block a user