diff --git a/pkg/conversion/converter.go b/pkg/conversion/converter.go index 8c106d52611..00c9bcb492c 100644 --- a/pkg/conversion/converter.go +++ b/pkg/conversion/converter.go @@ -40,8 +40,11 @@ type DebugLogger interface { type Converter struct { // Map from the conversion pair to a function which can // do the conversion. - conversionFuncs map[typePair]reflect.Value - generatedConversionFuncs map[typePair]reflect.Value + conversionFuncs ConversionFuncs + generatedConversionFuncs ConversionFuncs + + // Set of conversions that should be treated as a no-op + ignoredConversions map[typePair]struct{} // This is a map from a source field type and name, to a list of destination // field type and name. @@ -76,21 +79,30 @@ type Converter struct { // NewConverter creates a new Converter object. func NewConverter() *Converter { c := &Converter{ - conversionFuncs: map[typePair]reflect.Value{}, - generatedConversionFuncs: map[typePair]reflect.Value{}, - defaultingFuncs: map[reflect.Type]reflect.Value{}, - defaultingInterfaces: map[reflect.Type]interface{}{}, + conversionFuncs: NewConversionFuncs(), + generatedConversionFuncs: NewConversionFuncs(), + ignoredConversions: make(map[typePair]struct{}), + defaultingFuncs: make(map[reflect.Type]reflect.Value), + defaultingInterfaces: make(map[reflect.Type]interface{}), nameFunc: func(t reflect.Type) string { return t.Name() }, - structFieldDests: map[typeNamePair][]typeNamePair{}, - structFieldSources: map[typeNamePair][]typeNamePair{}, + structFieldDests: make(map[typeNamePair][]typeNamePair), + structFieldSources: make(map[typeNamePair][]typeNamePair), - inputFieldMappingFuncs: map[reflect.Type]FieldMappingFunc{}, - inputDefaultFlags: map[reflect.Type]FieldMatchingFlags{}, + inputFieldMappingFuncs: make(map[reflect.Type]FieldMappingFunc), + inputDefaultFlags: make(map[reflect.Type]FieldMatchingFlags), } c.RegisterConversionFunc(ByteSliceCopy) return c } +// WithConversions returns a Converter that is a copy of c but with the additional +// fns merged on top. +func (c *Converter) WithConversions(fns ConversionFuncs) *Converter { + copied := *c + copied.conversionFuncs = c.conversionFuncs.Merge(fns) + return &copied +} + // ByteSliceCopy prevents recursing into every byte func ByteSliceCopy(in *[]byte, out *[]byte, s Scope) error { *out = make([]byte, len(*in)) @@ -130,6 +142,42 @@ type Scope interface { // the value of the source or destination struct tags. type FieldMappingFunc func(key string, sourceTag, destTag reflect.StructTag) (source string, dest string) +func NewConversionFuncs() ConversionFuncs { + return ConversionFuncs{fns: make(map[typePair]reflect.Value)} +} + +type ConversionFuncs struct { + fns map[typePair]reflect.Value +} + +// Add adds the provided conversion functions to the lookup table - they must have the signature +// `func(type1, type2, Scope) error`. Functions are added in the order passed and will override +// previously registered pairs. +func (c ConversionFuncs) Add(fns ...interface{}) error { + for _, fn := range fns { + fv := reflect.ValueOf(fn) + ft := fv.Type() + if err := verifyConversionFunctionSignature(ft); err != nil { + return err + } + c.fns[typePair{ft.In(0).Elem(), ft.In(1).Elem()}] = fv + } + return nil +} + +// Merge returns a new ConversionFuncs that contains all conversions from +// both other and c, with other conversions taking precedence. +func (c ConversionFuncs) Merge(other ConversionFuncs) ConversionFuncs { + merged := NewConversionFuncs() + for k, v := range c.fns { + merged.fns[k] = v + } + for k, v := range other.fns { + merged.fns[k] = v + } + return merged +} + // Meta is supplied by Scheme, when it calls Convert. type Meta struct { SrcVersion string @@ -296,34 +344,44 @@ func verifyConversionFunctionSignature(ft reflect.Type) error { // return nil // }) func (c *Converter) RegisterConversionFunc(conversionFunc interface{}) error { - fv := reflect.ValueOf(conversionFunc) - ft := fv.Type() - if err := verifyConversionFunctionSignature(ft); err != nil { - return err - } - c.conversionFuncs[typePair{ft.In(0).Elem(), ft.In(1).Elem()}] = fv - return nil + return c.conversionFuncs.Add(conversionFunc) } // Similar to RegisterConversionFunc, but registers conversion function that were // automatically generated. func (c *Converter) RegisterGeneratedConversionFunc(conversionFunc interface{}) error { - fv := reflect.ValueOf(conversionFunc) - ft := fv.Type() - if err := verifyConversionFunctionSignature(ft); err != nil { - return err + return c.generatedConversionFuncs.Add(conversionFunc) +} + +// RegisterIgnoredConversion registers a "no-op" for conversion, where any requested +// conversion between from and to is ignored. +func (c *Converter) RegisterIgnoredConversion(from, to interface{}) error { + typeFrom := reflect.TypeOf(from) + typeTo := reflect.TypeOf(to) + if reflect.TypeOf(from).Kind() != reflect.Ptr { + return fmt.Errorf("expected pointer arg for 'from' param 0, got: %v", typeFrom) } - c.generatedConversionFuncs[typePair{ft.In(0).Elem(), ft.In(1).Elem()}] = fv + if typeTo.Kind() != reflect.Ptr { + return fmt.Errorf("expected pointer arg for 'to' param 1, got: %v", typeTo) + } + c.ignoredConversions[typePair{typeFrom.Elem(), typeTo.Elem()}] = struct{}{} return nil } +// IsConversionIgnored returns true if the specified objects should be dropped during +// conversion. +func (c *Converter) IsConversionIgnored(inType, outType reflect.Type) bool { + _, found := c.ignoredConversions[typePair{inType, outType}] + return found +} + func (c *Converter) HasConversionFunc(inType, outType reflect.Type) bool { - _, found := c.conversionFuncs[typePair{inType, outType}] + _, found := c.conversionFuncs.fns[typePair{inType, outType}] return found } func (c *Converter) ConversionFuncValue(inType, outType reflect.Type) (reflect.Value, bool) { - value, found := c.conversionFuncs[typePair{inType, outType}] + value, found := c.conversionFuncs.fns[typePair{inType, outType}] return value, found } @@ -509,16 +567,26 @@ func (c *Converter) convert(sv, dv reflect.Value, scope *scope) error { fv.Call(args) } + pair := typePair{st, dt} + + // ignore conversions of this type + if _, ok := c.ignoredConversions[pair]; ok { + if c.Debug != nil { + c.Debug.Logf("Ignoring conversion of '%v' to '%v'", st, dt) + } + return nil + } + // Convert sv to dv. - if fv, ok := c.conversionFuncs[typePair{st, dt}]; ok { + if fv, ok := c.conversionFuncs.fns[pair]; ok { if c.Debug != nil { c.Debug.Logf("Calling custom conversion of '%v' to '%v'", st, dt) } return c.callCustom(sv, dv, fv, scope) } - if fv, ok := c.generatedConversionFuncs[typePair{st, dt}]; ok { + if fv, ok := c.generatedConversionFuncs.fns[pair]; ok { if c.Debug != nil { - c.Debug.Logf("Calling custom conversion of '%v' to '%v'", st, dt) + c.Debug.Logf("Calling generated conversion of '%v' to '%v'", st, dt) } return c.callCustom(sv, dv, fv, scope) } diff --git a/pkg/conversion/converter_test.go b/pkg/conversion/converter_test.go index 6ef60347abc..8787ec37454 100644 --- a/pkg/conversion/converter_test.go +++ b/pkg/conversion/converter_test.go @@ -24,6 +24,8 @@ import ( "testing" "github.com/google/gofuzz" + + "k8s.io/kubernetes/pkg/api/unversioned" ) func testLogger(t *testing.T) DebugLogger { @@ -221,6 +223,55 @@ func TestConverter_CallsRegisteredFunctions(t *testing.T) { } } +func TestConverter_IgnoredConversion(t *testing.T) { + type A struct{} + type B struct{} + + count := 0 + c := NewConverter() + if err := c.RegisterConversionFunc(func(in *A, out *B, s Scope) error { + count++ + return nil + }); err != nil { + t.Fatalf("unexpected error %v", err) + } + if err := c.RegisterIgnoredConversion(&A{}, &B{}); err != nil { + t.Fatal(err) + } + a := A{} + b := B{} + if err := c.Convert(&a, &b, 0, nil); err != nil { + t.Errorf("%v", err) + } + if count != 0 { + t.Errorf("unexpected number of conversion invocations") + } +} + +func TestConverter_IgnoredConversionNested(t *testing.T) { + type C string + type A struct { + C C + } + type B struct { + C C + } + + c := NewConverter() + typed := C("") + if err := c.RegisterIgnoredConversion(&typed, &typed); err != nil { + t.Fatal(err) + } + a := A{C: C("test")} + b := B{C: C("other")} + if err := c.Convert(&a, &b, AllowDifferentFieldTypeNames, nil); err != nil { + t.Errorf("%v", err) + } + if b.C != C("other") { + t.Errorf("expected no conversion of field C: %#v", b) + } +} + func TestConverter_GeneratedConversionOverriden(t *testing.T) { type A struct{} type B struct{} @@ -243,6 +294,37 @@ func TestConverter_GeneratedConversionOverriden(t *testing.T) { } } +func TestConverter_WithConversionOverriden(t *testing.T) { + type A struct{} + type B struct{} + c := NewConverter() + if err := c.RegisterConversionFunc(func(in *A, out *B, s Scope) error { + return fmt.Errorf("conversion function should be overriden") + }); err != nil { + t.Fatalf("unexpected error %v", err) + } + if err := c.RegisterGeneratedConversionFunc(func(in *A, out *B, s Scope) error { + return fmt.Errorf("generated function should be overriden") + }); err != nil { + t.Fatalf("unexpected error %v", err) + } + + ext := NewConversionFuncs() + ext.Add(func(in *A, out *B, s Scope) error { + return nil + }) + newc := c.WithConversions(ext) + + a := A{} + b := B{} + if err := c.Convert(&a, &b, 0, nil); err == nil || err.Error() != "conversion function should be overriden" { + t.Errorf("unexpected error: %v", err) + } + if err := newc.Convert(&a, &b, 0, nil); err != nil { + t.Errorf("%v", err) + } +} + func TestConverter_MapsStringArrays(t *testing.T) { type A struct { Foo string @@ -681,3 +763,132 @@ func TestConverter_FieldRename(t *testing.T) { } } } + +func TestMetaValues(t *testing.T) { + type InternalSimple struct { + APIVersion string `json:"apiVersion,omitempty"` + Kind string `json:"kind,omitempty"` + TestString string `json:"testString"` + } + type ExternalSimple struct { + APIVersion string `json:"apiVersion,omitempty"` + Kind string `json:"kind,omitempty"` + TestString string `json:"testString"` + } + internalGV := unversioned.GroupVersion{Group: "test.group", Version: "__internal"} + externalGV := unversioned.GroupVersion{Group: "test.group", Version: "externalVersion"} + + s := NewScheme() + s.AddKnownTypeWithName(internalGV.WithKind("Simple"), &InternalSimple{}) + s.AddKnownTypeWithName(externalGV.WithKind("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 { + t.Logf("internal -> external") + if e, a := internalGV.String(), scope.Meta().SrcVersion; e != a { + t.Fatalf("Expected '%v', got '%v'", e, a) + } + if e, a := externalGV.String(), scope.Meta().DestVersion; e != a { + t.Fatalf("Expected '%v', got '%v'", e, a) + } + scope.Convert(&in.TestString, &out.TestString, 0) + internalToExternalCalls++ + return nil + }, + func(in *ExternalSimple, out *InternalSimple, scope Scope) error { + t.Logf("external -> internal") + if e, a := externalGV.String(), scope.Meta().SrcVersion; e != a { + t.Errorf("Expected '%v', got '%v'", e, a) + } + if e, a := internalGV.String(), scope.Meta().DestVersion; e != a { + t.Fatalf("Expected '%v', got '%v'", e, a) + } + scope.Convert(&in.TestString, &out.TestString, 0) + externalToInternalCalls++ + return nil + }, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + simple := &InternalSimple{ + TestString: "foo", + } + + s.Log(t) + + out, err := s.ConvertToVersion(simple, externalGV.String()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + internal, err := s.ConvertToVersion(out, internalGV.String()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if e, a := simple, internal; !reflect.DeepEqual(e, a) { + t.Errorf("Expected:\n %#v,\n Got:\n %#v", e, a) + } + + if e, a := 1, internalToExternalCalls; e != a { + t.Errorf("Expected %v, got %v", e, a) + } + if e, a := 1, externalToInternalCalls; e != a { + t.Errorf("Expected %v, got %v", e, a) + } +} + +func TestMetaValuesUnregisteredConvert(t *testing.T) { + type InternalSimple struct { + Version string `json:"apiVersion,omitempty"` + Kind string `json:"kind,omitempty"` + TestString string `json:"testString"` + } + type ExternalSimple struct { + Version string `json:"apiVersion,omitempty"` + Kind string `json:"kind,omitempty"` + TestString string `json:"testString"` + } + s := NewScheme() + // 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/unknown", scope.Meta().SrcVersion; e != a { + t.Fatalf("Expected '%v', got '%v'", e, a) + } + if e, a := "unknown/unknown", scope.Meta().DestVersion; e != a { + t.Fatalf("Expected '%v', got '%v'", e, a) + } + scope.Convert(&in.TestString, &out.TestString, 0) + internalToExternalCalls++ + return nil + }, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + simple := &InternalSimple{TestString: "foo"} + external := &ExternalSimple{} + err = s.Convert(simple, external) + if err != nil { + t.Fatalf("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/conversion/decode.go b/pkg/conversion/decode.go deleted file mode 100644 index 0e5c7a76285..00000000000 --- a/pkg/conversion/decode.go +++ /dev/null @@ -1,194 +0,0 @@ -/* -Copyright 2014 The Kubernetes Authors All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package conversion - -import ( - "errors" - "fmt" - "net/url" - - "github.com/ugorji/go/codec" - - "k8s.io/kubernetes/pkg/api/unversioned" -) - -func (s *Scheme) DecodeToVersionedObject(data []byte) (interface{}, unversioned.GroupVersionKind, error) { - kind, err := s.DataKind(data) - if err != nil { - return nil, unversioned.GroupVersionKind{}, err - } - - internalGV, exists := s.InternalVersions[kind.Group] - if !exists { - return nil, unversioned.GroupVersionKind{}, fmt.Errorf("no internalVersion specified for %v", kind) - } - - if len(kind.Group) == 0 && len(internalGV.Group) != 0 { - return nil, unversioned.GroupVersionKind{}, fmt.Errorf("group not set in '%s'", string(data)) - } - if len(kind.Version) == 0 && len(internalGV.Version) != 0 { - return nil, unversioned.GroupVersionKind{}, fmt.Errorf("version not set in '%s'", string(data)) - } - if kind.Kind == "" { - return nil, unversioned.GroupVersionKind{}, fmt.Errorf("kind not set in '%s'", string(data)) - } - - obj, err := s.NewObject(kind) - if err != nil { - return nil, unversioned.GroupVersionKind{}, err - } - - if err := codec.NewDecoderBytes(data, new(codec.JsonHandle)).Decode(obj); err != nil { - return nil, unversioned.GroupVersionKind{}, err - } - return obj, kind, nil -} - -// Decode converts a JSON string back into a pointer to an api object. -// Deduces the type based upon the fields added by the MetaInsertionFactory -// technique. The object will be converted, if necessary, into the -// s.InternalVersion type before being returned. Decode will not decode -// objects without version set unless InternalVersion is also "". -func (s *Scheme) Decode(data []byte) (interface{}, error) { - return s.DecodeToVersion(data, unversioned.GroupVersion{}) -} - -// DecodeToVersion converts a JSON string back into a pointer to an api object. -// Deduces the type based upon the fields added by the MetaInsertionFactory -// technique. The object will be converted, if necessary, into the versioned -// type before being returned. Decode will not decode objects without version -// set unless version is also "". -// a GroupVersion with .IsEmpty() == true is means "use the internal version for -// the object's group" -func (s *Scheme) DecodeToVersion(data []byte, targetVersion unversioned.GroupVersion) (interface{}, error) { - obj, sourceKind, err := s.DecodeToVersionedObject(data) - if err != nil { - return nil, err - } - // Version and Kind should be blank in memory. - if err := s.SetVersionAndKind("", "", obj); err != nil { - return nil, err - } - - // if the targetVersion is empty, then we want the internal version, but the internal version varies by - // group. We can lookup the group now because we have knowledge of the group - if targetVersion.IsEmpty() { - exists := false - targetVersion, exists = s.InternalVersions[sourceKind.Group] - if !exists { - return nil, fmt.Errorf("no internalVersion specified for %v", targetVersion) - } - } - - // Convert if needed. - if targetVersion != sourceKind.GroupVersion() { - objOut, err := s.NewObject(targetVersion.WithKind(sourceKind.Kind)) - if err != nil { - return nil, err - } - flags, meta := s.generateConvertMeta(sourceKind.GroupVersion(), targetVersion, obj) - if err := s.converter.Convert(obj, objOut, flags, meta); err != nil { - return nil, err - } - obj = objOut - } - return obj, nil -} - -// DecodeInto parses a JSON string and stores it in obj. Returns an error -// if data.Kind is set and doesn't match the type of obj. Obj should be a -// pointer to an api type. -// If obj's version doesn't match that in data, an attempt will be made to convert -// data into obj's version. -func (s *Scheme) DecodeInto(data []byte, obj interface{}) error { - return s.DecodeIntoWithSpecifiedVersionKind(data, obj, unversioned.GroupVersionKind{}) -} - -// DecodeIntoWithSpecifiedVersionKind compares the passed in requestGroupVersionKind -// with data.Version and data.Kind, defaulting data.Version and -// data.Kind to the specified value if they are empty, or generating an error if -// data.Version and data.Kind are not empty and differ from the specified value. -// The function then implements the functionality of DecodeInto. -// If specifiedVersion and specifiedKind are empty, the function degenerates to -// DecodeInto. -func (s *Scheme) DecodeIntoWithSpecifiedVersionKind(data []byte, obj interface{}, requestedGVK unversioned.GroupVersionKind) error { - if len(data) == 0 { - return errors.New("empty input") - } - dataKind, err := s.DataKind(data) - if err != nil { - return err - } - if len(dataKind.Group) == 0 { - dataKind.Group = requestedGVK.Group - } - if len(dataKind.Version) == 0 { - dataKind.Version = requestedGVK.Version - } - if len(dataKind.Kind) == 0 { - dataKind.Kind = requestedGVK.Kind - } - - if len(requestedGVK.Group) > 0 && requestedGVK.Group != dataKind.Group { - return errors.New(fmt.Sprintf("The fully qualified kind in the data (%v) does not match the specified apiVersion(%v)", dataKind, requestedGVK)) - } - if len(requestedGVK.Version) > 0 && requestedGVK.Version != dataKind.Version { - return errors.New(fmt.Sprintf("The fully qualified kind in the data (%v) does not match the specified apiVersion(%v)", dataKind, requestedGVK)) - } - if len(requestedGVK.Kind) > 0 && requestedGVK.Kind != dataKind.Kind { - return errors.New(fmt.Sprintf("The fully qualified kind in the data (%v) does not match the specified apiVersion(%v)", dataKind, requestedGVK)) - } - - objGVK, err := s.ObjectKind(obj) - if err != nil { - return err - } - // Assume objects with unset fields are being unmarshalled into the - // correct type. - if len(dataKind.Group) == 0 { - dataKind.Group = objGVK.Group - } - if len(dataKind.Version) == 0 { - dataKind.Version = objGVK.Version - } - if len(dataKind.Kind) == 0 { - dataKind.Kind = objGVK.Kind - } - - external, err := s.NewObject(dataKind) - if err != nil { - return err - } - if err := codec.NewDecoderBytes(data, new(codec.JsonHandle)).Decode(external); err != nil { - return err - } - flags, meta := s.generateConvertMeta(dataKind.GroupVersion(), objGVK.GroupVersion(), external) - if err := s.converter.Convert(external, obj, flags, meta); err != nil { - return err - } - - // Version and Kind should be blank in memory. - return s.SetVersionAndKind("", "", obj) -} - -func (s *Scheme) DecodeParametersInto(parameters url.Values, obj interface{}) error { - if err := s.Convert(¶meters, obj); err != nil { - return err - } - // TODO: Should we do any convertion here? - return nil -} diff --git a/pkg/conversion/doc.go b/pkg/conversion/doc.go index f0626a569c2..3ef2eaba457 100644 --- a/pkg/conversion/doc.go +++ b/pkg/conversion/doc.go @@ -14,18 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package conversion provides go object versioning and encoding/decoding -// mechanisms. +// Package conversion provides go object versioning. // // Specifically, conversion provides a way for you to define multiple versions // of the same object. You may write functions which implement conversion logic, // but for the fields which did not change, copying is automated. This makes it // easy to modify the structures you use in memory without affecting the format // you store on disk or respond to in your external API calls. -// -// The second offering of this package is automated encoding/decoding. The version -// and type of the object is recorded in the output, so it can be recreated upon -// reading. Currently, conversion writes JSON output, and interprets both JSON -// and YAML input. -// package conversion diff --git a/pkg/conversion/encode.go b/pkg/conversion/encode.go deleted file mode 100644 index 3120583d454..00000000000 --- a/pkg/conversion/encode.go +++ /dev/null @@ -1,155 +0,0 @@ -/* -Copyright 2014 The Kubernetes Authors All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package conversion - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "path" - - "k8s.io/kubernetes/pkg/api/unversioned" -) - -// EncodeToVersion turns the given api object into an appropriate JSON string. -// Obj may be a pointer to a struct, or a struct. If a struct, a copy -// will be made, therefore it's recommended to pass a pointer to a -// struct. The type must have been registered. -// -// Memory/wire format differences: -// * Having to keep track of the Kind and Version fields makes tests -// very annoying, so the rule is that they are set only in wire format -// (json), not when in native (memory) format. This is possible because -// both pieces of information are implicit in the go typed object. -// * An exception: note that, if there are embedded API objects of known -// type, for example, PodList{... Items []Pod ...}, these embedded -// objects must be of the same version of the object they are embedded -// within, and their Version and Kind must both be empty. -// * Note that the exception does not apply to a generic APIObject type -// which recursively does Encode()/Decode(), and is capable of -// expressing any API object. -// * Only versioned objects should be encoded. This means that, if you pass -// a native object, Encode will convert it to a versioned object. For -// example, an api.Pod will get converted to a v1.Pod. However, if -// you pass in an object that's already versioned (v1.Pod), Encode -// will not modify it. -// -// The purpose of the above complex conversion behavior is to allow us to -// change the memory format yet not break compatibility with any stored -// objects, whether they be in our storage layer (e.g., etcd), or in user's -// config files. -// -func (s *Scheme) EncodeToVersion(obj interface{}, destVersion string) (data []byte, err error) { - buff := &bytes.Buffer{} - if err := s.EncodeToVersionStream(obj, destVersion, buff); err != nil { - return nil, err - } - return buff.Bytes(), nil -} - -func (s *Scheme) EncodeToVersionStream(obj interface{}, destGroupVersionString string, stream io.Writer) error { - obj = maybeCopy(obj) - v, _ := EnforcePtr(obj) // maybeCopy guarantees a pointer - - // Don't encode an object defined in the unversioned package, unless if the - // destGroupVersionString is v1, encode it to v1 for backward compatibility. - pkg := path.Base(v.Type().PkgPath()) - if pkg == "unversioned" && destGroupVersionString != "v1" { - // TODO: convert this to streaming too - data, err := s.encodeUnversionedObject(obj) - if err != nil { - return err - } - _, err = stream.Write(data) - return err - } - - if _, registered := s.typeToGVK[v.Type()]; !registered { - return fmt.Errorf("type %v is not registered for %q and it will be impossible to Decode it, therefore Encode will refuse to encode it.", v.Type(), destGroupVersionString) - } - - objKind, err := s.ObjectKind(obj) - if err != nil { - return err - } - - destVersion, err := unversioned.ParseGroupVersion(destGroupVersionString) - if err != nil { - return err - } - - // Perform a conversion if necessary. - if objKind.GroupVersion() != destVersion { - objOut, err := s.NewObject(destVersion.WithKind(objKind.Kind)) - if err != nil { - return err - } - flags, meta := s.generateConvertMeta(objKind.GroupVersion(), destVersion, obj) - err = s.converter.Convert(obj, objOut, flags, meta) - if err != nil { - return err - } - obj = objOut - - // ensure the output object name comes from the destination type - newGroupVersionKind, err := s.ObjectKind(obj) - if err != nil { - return err - } - objKind.Kind = newGroupVersionKind.Kind - } - - // Version and Kind should be set on the wire. - err = s.SetVersionAndKind(destVersion.String(), objKind.Kind, obj) - if err != nil { - return err - } - - // To add metadata, do some simple surgery on the JSON. - encoder := json.NewEncoder(stream) - if err := encoder.Encode(obj); err != nil { - return err - } - - // Version and Kind should be blank in memory. Reset them, since it's - // possible that we modified a user object and not a copy above. - err = s.SetVersionAndKind("", "", obj) - if err != nil { - return err - } - - return nil -} - -func (s *Scheme) encodeUnversionedObject(obj interface{}) (data []byte, err error) { - objGVK, err := s.ObjectKind(obj) - if err != nil { - return nil, err - } - if err = s.SetVersionAndKind("", objGVK.Kind, obj); err != nil { - return nil, err - } - data, err = json.Marshal(obj) - if err != nil { - return nil, err - } - // Version and Kind should be blank in memory. Reset them, since it's - // possible that we modified a user object and not a copy above. - err = s.SetVersionAndKind("", "", obj) - return data, nil -} diff --git a/pkg/conversion/error.go b/pkg/conversion/error.go index 6ddbf9b7a78..94075e8adfb 100644 --- a/pkg/conversion/error.go +++ b/pkg/conversion/error.go @@ -28,6 +28,11 @@ type notRegisteredErr struct { t reflect.Type } +// NewNotRegisteredErr is exposed for testing. +func NewNotRegisteredErr(gvk unversioned.GroupVersionKind, t reflect.Type) error { + return ¬RegisteredErr{gvk: gvk, t: t} +} + func (k *notRegisteredErr) Error() string { if k.t != nil { return fmt.Sprintf("no kind is registered for the type %v", k.t) diff --git a/pkg/conversion/helper.go b/pkg/conversion/helper.go new file mode 100644 index 00000000000..39f78265959 --- /dev/null +++ b/pkg/conversion/helper.go @@ -0,0 +1,39 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package conversion + +import ( + "fmt" + "reflect" +) + +// 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. +func EnforcePtr(obj interface{}) (reflect.Value, error) { + v := reflect.ValueOf(obj) + if v.Kind() != reflect.Ptr { + if v.Kind() == reflect.Invalid { + return reflect.Value{}, fmt.Errorf("expected pointer, but got invalid kind") + } + return reflect.Value{}, fmt.Errorf("expected pointer, but got %v type", v.Type()) + } + if v.IsNil() { + return reflect.Value{}, fmt.Errorf("expected pointer, but got nil") + } + return v.Elem(), nil +} diff --git a/pkg/conversion/helper_test.go b/pkg/conversion/helper_test.go new file mode 100644 index 00000000000..69fef3334b1 --- /dev/null +++ b/pkg/conversion/helper_test.go @@ -0,0 +1,38 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package conversion + +import "testing" + +func TestInvalidPtrValueKind(t *testing.T) { + var simple interface{} + switch obj := simple.(type) { + default: + _, err := EnforcePtr(obj) + if err == nil { + t.Errorf("Expected error on invalid kind") + } + } +} + +func TestEnforceNilPtr(t *testing.T) { + var nilPtr *struct{} + _, err := EnforcePtr(nilPtr) + if err == nil { + t.Errorf("Expected error on nil pointer") + } +} diff --git a/pkg/conversion/meta.go b/pkg/conversion/meta.go deleted file mode 100644 index bb33d12cb56..00000000000 --- a/pkg/conversion/meta.go +++ /dev/null @@ -1,153 +0,0 @@ -/* -Copyright 2014 The Kubernetes Authors All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package conversion - -import ( - "encoding/json" - "fmt" - "path" - "reflect" - - "k8s.io/kubernetes/pkg/api/unversioned" -) - -// MetaFactory is used to store and retrieve the version and kind -// information for all objects in a scheme. -type MetaFactory interface { - // Update sets the given version and kind onto the object. - Update(version, kind string, obj interface{}) error - // Interpret should return the group,version,kind of the wire-format of - // the object. - Interpret(data []byte) (gvk unversioned.GroupVersionKind, err error) -} - -// DefaultMetaFactory is a default factory for versioning objects in JSON. The object -// in memory and in the default JSON serialization will use the "kind" and "apiVersion" -// fields. -var DefaultMetaFactory = SimpleMetaFactory{KindField: "Kind", VersionField: "APIVersion"} - -// SimpleMetaFactory provides default methods for retrieving the type and version of objects -// that are identified with an "apiVersion" and "kind" fields in their JSON -// serialization. It may be parameterized with the names of the fields in memory, or an -// optional list of base structs to search for those fields in memory. -type SimpleMetaFactory struct { - // The name of the API version field in memory of the struct - VersionField string - // The name of the kind field in memory of the struct. - KindField string - // Optional, if set will look in the named inline structs to find the fields to set. - BaseFields []string -} - -// Interpret will return the group,version,kind of the JSON wire-format -// encoding of an object, or an error. -func (SimpleMetaFactory) Interpret(data []byte) (unversioned.GroupVersionKind, error) { - findKind := struct { - APIVersion string `json:"apiVersion,omitempty"` - Kind string `json:"kind,omitempty"` - }{} - err := json.Unmarshal(data, &findKind) - if err != nil { - return unversioned.GroupVersionKind{}, fmt.Errorf("couldn't get version/kind; json parse error: %v", err) - } - gv, err := unversioned.ParseGroupVersion(findKind.APIVersion) - if err != nil { - return unversioned.GroupVersionKind{}, fmt.Errorf("couldn't parse apiVersion: %v", err) - } - - return gv.WithKind(findKind.Kind), nil -} - -func (f SimpleMetaFactory) Update(version, kind string, obj interface{}) error { - return UpdateVersionAndKind(f.BaseFields, f.VersionField, version, f.KindField, kind, obj) -} - -// UpdateVersionAndKind uses reflection to find and set the versionField and kindField fields -// on a pointer to a struct to version and kind. Provided as a convenience for others -// implementing MetaFactory. Pass an array to baseFields to check one or more nested structs -// for the named fields. The version field is treated as optional if it is not present in the struct. -// TODO: this method is on its way out -func UpdateVersionAndKind(baseFields []string, versionField, version, kindField, kind string, obj interface{}) error { - if typed, ok := obj.(unversioned.ObjectKind); ok { - if len(version) == 0 && len(kind) == 0 { - typed.SetGroupVersionKind(nil) - } else { - gv, err := unversioned.ParseGroupVersion(version) - if err != nil { - return err - } - typed.SetGroupVersionKind(&unversioned.GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: kind}) - } - return nil - } - v, err := EnforcePtr(obj) - if err != nil { - return err - } - pkg := path.Base(v.Type().PkgPath()) - t := v.Type() - name := t.Name() - if v.Kind() != reflect.Struct { - return fmt.Errorf("expected struct, but got %v: %v (%#v)", v.Kind(), name, v.Interface()) - } - - for i := range baseFields { - base := v.FieldByName(baseFields[i]) - if !base.IsValid() { - continue - } - v = base - } - - field := v.FieldByName(kindField) - if !field.IsValid() { - // Types defined in the unversioned package are allowed to not have a - // kindField. Clients will have to know what they are based on the - // context. - // TODO: add some type trait here, or some way of indicating whether - // this feature is allowed on a per-type basis. Using package name is - // overly broad and a bit hacky. - if pkg == "unversioned" { - return nil - } - return fmt.Errorf("couldn't find %v field in %#v", kindField, v.Interface()) - } - field.SetString(kind) - - if field := v.FieldByName(versionField); field.IsValid() { - field.SetString(version) - } - - return nil -} - -// 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. -func EnforcePtr(obj interface{}) (reflect.Value, error) { - v := reflect.ValueOf(obj) - if v.Kind() != reflect.Ptr { - if v.Kind() == reflect.Invalid { - return reflect.Value{}, fmt.Errorf("expected pointer, but got invalid kind") - } - return reflect.Value{}, fmt.Errorf("expected pointer, but got %v type", v.Type()) - } - if v.IsNil() { - return reflect.Value{}, fmt.Errorf("expected pointer, but got nil") - } - return v.Elem(), nil -} diff --git a/pkg/conversion/meta_test.go b/pkg/conversion/meta_test.go deleted file mode 100644 index 4cf92fb0688..00000000000 --- a/pkg/conversion/meta_test.go +++ /dev/null @@ -1,289 +0,0 @@ -/* -Copyright 2014 The Kubernetes Authors All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package conversion - -import ( - "fmt" - "reflect" - "testing" - - "k8s.io/kubernetes/pkg/api/unversioned" -) - -func TestSimpleMetaFactoryInterpret(t *testing.T) { - factory := SimpleMetaFactory{} - fqKind, err := factory.Interpret([]byte(`{"apiVersion":"g/1","kind":"object"}`)) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - expectedFQKind := unversioned.GroupVersionKind{Group: "g", Version: "1", Kind: "object"} - if expectedFQKind != fqKind { - t.Errorf("unexpected interpret: %s %s", expectedFQKind, fqKind) - } - - // no kind or version - fqKind, err = factory.Interpret([]byte(`{}`)) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !fqKind.IsEmpty() { - t.Errorf("unexpected interpret: %s %s", fqKind) - } - - // unparsable - fqKind, err = factory.Interpret([]byte(`{`)) - if err == nil { - t.Errorf("unexpected non-error") - } -} - -func TestSimpleMetaFactoryUpdate(t *testing.T) { - factory := SimpleMetaFactory{VersionField: "V", KindField: "K"} - - obj := struct { - V string - K string - }{"1", "2"} - - // must pass a pointer - if err := factory.Update("test", "other", obj); err == nil { - t.Errorf("unexpected non-error") - } - if obj.V != "1" || obj.K != "2" { - t.Errorf("unexpected update: %v", obj) - } - - // updates - if err := factory.Update("test", "other", &obj); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if obj.V != "test" || obj.K != "other" { - t.Errorf("unexpected update: %v", obj) - } -} - -// Test Updating objects that don't have a Kind field. -func TestSimpleMetaFactoryUpdateNoKindField(t *testing.T) { - factory := SimpleMetaFactory{VersionField: "APIVersion", KindField: "Kind"} - // obj does not have a Kind field and is not defined in the unversioned package. - obj := struct { - SomeField string - }{"1"} - expectedError := fmt.Errorf("couldn't find %v field in %#v", factory.KindField, obj) - if err := factory.Update("test", "other", &obj); err == nil || expectedError.Error() != err.Error() { - t.Fatalf("expected error: %v, got: %v", expectedError, err) - } - - // ListMeta does not have a Kind field, but is defined in the unversioned package. - listMeta := unversioned.ListMeta{} - if err := factory.Update("test", "other", &listMeta); err != nil { - t.Fatalf("unexpected error: %v", err) - } -} - -func TestSimpleMetaFactoryUpdateStruct(t *testing.T) { - factory := SimpleMetaFactory{BaseFields: []string{"Test"}, VersionField: "V", KindField: "K"} - - type Inner struct { - V string - K string - } - obj := struct { - Test Inner - }{Test: Inner{"1", "2"}} - - // updates - if err := factory.Update("test", "other", &obj); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if obj.Test.V != "test" || obj.Test.K != "other" { - t.Errorf("unexpected update: %v", obj) - } -} - -func TestMetaValues(t *testing.T) { - type InternalSimple struct { - APIVersion string `json:"apiVersion,omitempty"` - Kind string `json:"kind,omitempty"` - TestString string `json:"testString"` - } - type ExternalSimple struct { - APIVersion string `json:"apiVersion,omitempty"` - Kind string `json:"kind,omitempty"` - TestString string `json:"testString"` - } - internalGV := unversioned.GroupVersion{Group: "test.group", Version: ""} - externalGV := unversioned.GroupVersion{Group: "test.group", Version: "externalVersion"} - - s := NewScheme() - s.InternalVersions[internalGV.Group] = internalGV - s.AddKnownTypeWithName(internalGV.WithKind("Simple"), &InternalSimple{}) - s.AddKnownTypeWithName(externalGV.WithKind("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 { - t.Logf("internal -> external") - if e, a := internalGV.String(), scope.Meta().SrcVersion; e != a { - t.Fatalf("Expected '%v', got '%v'", e, a) - } - if e, a := externalGV.String(), scope.Meta().DestVersion; e != a { - t.Fatalf("Expected '%v', got '%v'", e, a) - } - scope.Convert(&in.TestString, &out.TestString, 0) - internalToExternalCalls++ - return nil - }, - func(in *ExternalSimple, out *InternalSimple, scope Scope) error { - t.Logf("external -> internal") - if e, a := externalGV.String(), scope.Meta().SrcVersion; e != a { - t.Errorf("Expected '%v', got '%v'", e, a) - } - if e, a := internalGV.String(), scope.Meta().DestVersion; e != a { - t.Fatalf("Expected '%v', got '%v'", e, a) - } - scope.Convert(&in.TestString, &out.TestString, 0) - externalToInternalCalls++ - return nil - }, - ) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - simple := &InternalSimple{ - TestString: "foo", - } - - s.Log(t) - - // Test Encode, Decode, and DecodeInto - data, err := s.EncodeToVersion(simple, externalGV.String()) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - t.Logf(string(data)) - obj2, err := s.Decode(data) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - 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) - } - - obj3 := &InternalSimple{} - if err := s.DecodeInto(data, obj3); err != nil { - t.Fatalf("unexpected error: %v", err) - } - 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.Fatalf("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:"apiVersion,omitempty"` - Kind string `json:"kind,omitempty"` - TestString string `json:"testString"` - } - type ExternalSimple struct { - Version string `json:"apiVersion,omitempty"` - Kind string `json:"kind,omitempty"` - TestString string `json:"testString"` - } - s := NewScheme() - // 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/unknown", scope.Meta().SrcVersion; e != a { - t.Fatalf("Expected '%v', got '%v'", e, a) - } - if e, a := "unknown/unknown", scope.Meta().DestVersion; e != a { - t.Fatalf("Expected '%v', got '%v'", e, a) - } - scope.Convert(&in.TestString, &out.TestString, 0) - internalToExternalCalls++ - return nil - }, - ) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - simple := &InternalSimple{TestString: "foo"} - external := &ExternalSimple{} - err = s.Convert(simple, external) - if err != nil { - t.Fatalf("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) - } -} - -func TestInvalidPtrValueKind(t *testing.T) { - var simple interface{} - switch obj := simple.(type) { - default: - _, err := EnforcePtr(obj) - if err == nil { - t.Errorf("Expected error on invalid kind") - } - } -} - -func TestEnforceNilPtr(t *testing.T) { - var nilPtr *struct{} - _, err := EnforcePtr(nilPtr) - if err == nil { - t.Errorf("Expected error on nil pointer") - } -} diff --git a/pkg/conversion/queryparams/convert.go b/pkg/conversion/queryparams/convert.go index 450a43001a2..0f04e7bb4cb 100644 --- a/pkg/conversion/queryparams/convert.go +++ b/pkg/conversion/queryparams/convert.go @@ -21,8 +21,6 @@ import ( "net/url" "reflect" "strings" - - "k8s.io/kubernetes/pkg/runtime" ) func jsonTag(field reflect.StructField) (string, bool) { @@ -93,10 +91,10 @@ func addListOfParams(values url.Values, tag string, omitempty bool, list reflect } } -// Convert takes a versioned runtime.Object and serializes it to a url.Values object -// using JSON tags as parameter names. Only top-level simple values, arrays, and slices -// are serialized. Embedded structs, maps, etc. will not be serialized. -func Convert(obj runtime.Object) (url.Values, error) { +// Convert takes an object and converts it to a url.Values object using JSON tags as +// parameter names. Only top-level simple values, arrays, and slices are serialized. +// Embedded structs, maps, etc. will not be serialized. +func Convert(obj interface{}) (url.Values, error) { result := url.Values{} if obj == nil { return result, nil diff --git a/pkg/conversion/queryparams/convert_test.go b/pkg/conversion/queryparams/convert_test.go index 8b513f076db..405357557fa 100644 --- a/pkg/conversion/queryparams/convert_test.go +++ b/pkg/conversion/queryparams/convert_test.go @@ -23,7 +23,6 @@ import ( "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/conversion/queryparams" - "k8s.io/kubernetes/pkg/runtime" ) type namedString string @@ -85,7 +84,7 @@ func validateResult(t *testing.T, input interface{}, actual, expected url.Values func TestConvert(t *testing.T) { tests := []struct { - input runtime.Object + input interface{} expected url.Values }{ { diff --git a/pkg/conversion/scheme.go b/pkg/conversion/scheme.go index e5ea10b50c1..236ac39f971 100644 --- a/pkg/conversion/scheme.go +++ b/pkg/conversion/scheme.go @@ -33,6 +33,13 @@ type Scheme struct { // The reflect.Type we index by should *not* be a pointer. typeToGVK map[reflect.Type][]unversioned.GroupVersionKind + // unversionedTypes are transformed without conversion in ConvertToVersion. + unversionedTypes map[reflect.Type]unversioned.GroupVersionKind + // unversionedKinds are the names of kinds that can be created in the context of any group + // or version + // TODO: resolve the status of unversioned types. + unversionedKinds map[string]reflect.Type + // converter stores all registered conversion functions. It also has // default coverting behavior. converter *Converter @@ -44,34 +51,17 @@ type Scheme struct { // Indent will cause the JSON output from Encode to be indented, // if and only if it is true. Indent bool - - // InternalVersion is the default internal version. It is recommended that - // you use "" for the internal version. - // TODO logically the InternalVersion is different for every Group, so this structure - // must be map - InternalVersions map[string]unversioned.GroupVersion - - // MetaInsertionFactory is used to create an object to store and retrieve - // the version and kind information for all objects. The default uses the - // keys "apiVersion" and "kind" respectively. - MetaFactory MetaFactory } // NewScheme manufactures a new scheme. func NewScheme() *Scheme { s := &Scheme{ - gvkToType: map[unversioned.GroupVersionKind]reflect.Type{}, - typeToGVK: map[reflect.Type][]unversioned.GroupVersionKind{}, - converter: NewConverter(), - cloner: NewCloner(), - // TODO remove this hard coded list. As step one, hardcode it here so this pull doesn't become even bigger - InternalVersions: map[string]unversioned.GroupVersion{ - "": {}, - "componentconfig": {Group: "componentconfig"}, - "extensions": {Group: "extensions"}, - "metrics": {Group: "metrics"}, - }, - MetaFactory: DefaultMetaFactory, + gvkToType: map[unversioned.GroupVersionKind]reflect.Type{}, + typeToGVK: map[reflect.Type][]unversioned.GroupVersionKind{}, + unversionedTypes: map[reflect.Type]unversioned.GroupVersionKind{}, + unversionedKinds: map[string]reflect.Type{}, + converter: NewConverter(), + cloner: NewCloner(), } s.converter.nameFunc = s.nameFunc return s @@ -92,11 +82,8 @@ func (s *Scheme) nameFunc(t reflect.Type) string { } for _, gvk := range gvks { - internalGV, exists := s.InternalVersions[gvk.Group] - if !exists { - internalGV := gvk.GroupVersion() - internalGV.Version = "" - } + internalGV := gvk.GroupVersion() + internalGV.Version = "__internal" // this is hacky and maybe should be passed in internalGVK := internalGV.WithKind(gvk.Kind) if internalType, exists := s.gvkToType[internalGVK]; exists { @@ -107,11 +94,30 @@ func (s *Scheme) nameFunc(t reflect.Type) string { return gvks[0].Kind } +// AddUnversionedTypes registers all types passed in 'types' as being members of version 'version', +// and marks them as being convertible to all API versions. +// All objects passed to types should be pointers to structs. The name that go reports for +// the struct becomes the "kind" field when encoding. +func (s *Scheme) AddUnversionedTypes(version unversioned.GroupVersion, types ...interface{}) { + s.AddKnownTypes(version, types...) + for _, obj := range types { + t := reflect.TypeOf(obj).Elem() + gvk := version.WithKind(t.Name()) + s.unversionedTypes[t] = gvk + if _, ok := s.unversionedKinds[gvk.Kind]; ok { + panic(fmt.Sprintf("%v has already been registered as unversioned kind %q - kind name must be unique", reflect.TypeOf(t), gvk.Kind)) + } + s.unversionedKinds[gvk.Kind] = t + } +} + // AddKnownTypes registers all types passed in 'types' as being members of version 'version'. -// Encode() will refuse objects unless their type has been registered with AddKnownTypes. // All objects passed to types should be pointers to structs. The name that go reports for // the struct becomes the "kind" field when encoding. func (s *Scheme) AddKnownTypes(gv unversioned.GroupVersion, types ...interface{}) { + if len(gv.Version) == 0 { + panic(fmt.Sprintf("version is required on all types: %s %v", gv, types[0])) + } for _, obj := range types { t := reflect.TypeOf(obj) if t.Kind() != reflect.Ptr { @@ -133,6 +139,9 @@ func (s *Scheme) AddKnownTypes(gv unversioned.GroupVersion, types ...interface{} // your structs. func (s *Scheme) AddKnownTypeWithName(gvk unversioned.GroupVersionKind, obj interface{}) { t := reflect.TypeOf(obj) + if len(gvk.Version) == 0 { + panic(fmt.Sprintf("version is required on all types: %s %v", gvk, t)) + } if t.Kind() != reflect.Ptr { panic("All types must be pointers to structs.") } @@ -168,6 +177,9 @@ func (s *Scheme) NewObject(kind unversioned.GroupVersionKind) (interface{}, erro return reflect.New(t).Interface(), nil } + if t, exists := s.unversionedKinds[kind.Kind]; exists { + return reflect.New(t).Interface(), nil + } return nil, ¬RegisteredErr{gvk: kind} } @@ -221,6 +233,13 @@ func (s *Scheme) AddGeneratedConversionFuncs(conversionFuncs ...interface{}) err return nil } +// AddIgnoredConversionType identifies a pair of types that should be skipped by +// dynamic conversion (because the data inside them is explicitly dropped during +// conversion). +func (s *Scheme) AddIgnoredConversionType(from, to interface{}) error { + return s.converter.RegisterIgnoredConversion(from, to) +} + // AddDeepCopyFuncs adds functions to the list of deep copy functions. // Note that to copy sub-objects, you can use the conversion.Cloner object that // will be passed to your deep-copy function. @@ -282,6 +301,22 @@ func (s *Scheme) Recognizes(gvk unversioned.GroupVersionKind) bool { return exists } +// IsUnversioned returns true if the Go object is registered as an unversioned type, or sets +// ok to false if the provided object is not registered in the scheme. +func (s *Scheme) IsUnversioned(obj interface{}) (unversioned bool, registered bool) { + v, err := EnforcePtr(obj) + if err != nil { + return false, false + } + t := v.Type() + + if _, ok := s.typeToGVK[t]; !ok { + return false, false + } + _, ok := s.unversionedTypes[t] + return ok, true +} + // RegisterInputDefaults sets the provided field mapping function and field matching // as the defaults for the provided input type. The fn may be nil, in which case no // mapping will happen by default. Use this method to register a mechanism for handling @@ -330,15 +365,23 @@ func (s *Scheme) ConvertToVersion(in interface{}, outGroupVersionString string) return nil, fmt.Errorf("only pointers to struct types may be converted: %v", t) } - gvks, ok := s.typeToGVK[t] - if !ok { - return nil, fmt.Errorf("%v cannot be converted into version %q", t, outGroupVersionString) - } outVersion, err := unversioned.ParseGroupVersion(outGroupVersionString) if err != nil { return nil, err } - outKind := outVersion.WithKind(gvks[0].Kind) + + var kind unversioned.GroupVersionKind + if unversionedKind, ok := s.unversionedTypes[t]; ok { + kind = unversionedKind + } else { + kinds, ok := s.typeToGVK[t] + if !ok || len(kinds) == 0 { + return nil, fmt.Errorf("%v is not a registered type and cannot be converted into version %q", t, outGroupVersionString) + } + kind = kinds[0] + } + + outKind := outVersion.WithKind(kind.Kind) inKind, err := s.ObjectKind(in) if err != nil { @@ -355,10 +398,6 @@ func (s *Scheme) ConvertToVersion(in interface{}, outGroupVersionString string) return nil, err } - if err := s.SetVersionAndKind(outVersion.String(), outKind.Kind, out); err != nil { - return nil, err - } - return out, nil } @@ -367,6 +406,14 @@ func (s *Scheme) Converter() *Converter { return s.converter } +// WithConversions returns a scheme with additional conversion functions +func (s *Scheme) WithConversions(fns ConversionFuncs) *Scheme { + c := s.converter.WithConversions(fns) + copied := *s + copied.converter = c + return &copied +} + // generateConvertMeta constructs the meta value we pass to Convert. func (s *Scheme) generateConvertMeta(srcGroupVersion, destGroupVersion unversioned.GroupVersion, in interface{}) (FieldMatchingFlags, *Meta) { t := reflect.TypeOf(in) @@ -377,12 +424,6 @@ func (s *Scheme) generateConvertMeta(srcGroupVersion, destGroupVersion unversion } } -// DataKind will return the group,version,kind of the given wire-format -// encoding of an API Object, or an error. -func (s *Scheme) DataKind(data []byte) (unversioned.GroupVersionKind, error) { - return s.MetaFactory.Interpret(data) -} - // ObjectKind returns the group,version,kind of the go object, // or an error if it's not a pointer or is unregistered. func (s *Scheme) ObjectKind(obj interface{}) (unversioned.GroupVersionKind, error) { @@ -390,7 +431,6 @@ func (s *Scheme) ObjectKind(obj interface{}) (unversioned.GroupVersionKind, erro if err != nil { return unversioned.GroupVersionKind{}, err } - return gvks[0], nil } @@ -399,22 +439,16 @@ func (s *Scheme) ObjectKind(obj interface{}) (unversioned.GroupVersionKind, erro func (s *Scheme) ObjectKinds(obj interface{}) ([]unversioned.GroupVersionKind, error) { v, err := EnforcePtr(obj) if err != nil { - return []unversioned.GroupVersionKind{}, err + return nil, err } t := v.Type() gvks, ok := s.typeToGVK[t] if !ok { - return []unversioned.GroupVersionKind{}, ¬RegisteredErr{t: t} + return nil, ¬RegisteredErr{t: t} } - return gvks, nil -} -// SetVersionAndKind sets the version and kind fields (with help from -// MetaInsertionFactory). Returns an error if this isn't possible. obj -// must be a pointer. -func (s *Scheme) SetVersionAndKind(version, kind string, obj interface{}) error { - return s.MetaFactory.Update(version, kind, obj) + return gvks, 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 9c62f407d28..766aa8b479b 100644 --- a/pkg/conversion/scheme_test.go +++ b/pkg/conversion/scheme_test.go @@ -18,15 +18,11 @@ package conversion import ( "encoding/json" - "fmt" - "reflect" - "strings" "testing" "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/util" - "github.com/ghodss/yaml" "github.com/google/gofuzz" flag "github.com/spf13/pflag" ) @@ -109,7 +105,7 @@ var TestObjectFuzzer = fuzz.New().NilChance(.5).NumElements(1, 100).Funcs( // Returns a new Scheme set up with the test objects. func GetTestScheme() *Scheme { - internalGV := unversioned.GroupVersion{} + internalGV := unversioned.GroupVersion{Version: "__internal"} externalGV := unversioned.GroupVersion{Version: "v1"} s := NewScheme() @@ -122,34 +118,9 @@ func GetTestScheme() *Scheme { s.AddKnownTypeWithName(externalGV.WithKind("TestType2"), &ExternalTestType2{}) s.AddKnownTypeWithName(internalGV.WithKind("TestType3"), &TestType1{}) s.AddKnownTypeWithName(externalGV.WithKind("TestType3"), &ExternalTestType1{}) - s.MetaFactory = testMetaFactory{} return s } -type testMetaFactory struct{} - -func (testMetaFactory) Interpret(data []byte) (unversioned.GroupVersionKind, error) { - findKind := struct { - APIVersion string `json:"myVersionKey,omitempty"` - ObjectKind string `json:"myKindKey,omitempty"` - }{} - // yaml is a superset of json, so we use it to decode here. That way, - // we understand both. - err := yaml.Unmarshal(data, &findKind) - if err != nil { - return unversioned.GroupVersionKind{}, fmt.Errorf("couldn't get version/kind: %v", err) - } - gv, err := unversioned.ParseGroupVersion(findKind.APIVersion) - if err != nil { - return unversioned.GroupVersionKind{}, err - } - return gv.WithKind(findKind.ObjectKind), nil -} - -func (testMetaFactory) Update(version, kind string, obj interface{}) error { - return UpdateVersionAndKind(nil, "APIVersion", version, "ObjectKind", kind, obj) -} - func objDiff(a, b interface{}) string { ab, err := json.Marshal(a) if err != nil { @@ -170,111 +141,6 @@ func objDiff(a, b interface{}) string { //) } -func runTest(t *testing.T, source interface{}) { - name := reflect.TypeOf(source).Elem().Name() - TestObjectFuzzer.Fuzz(source) - - s := GetTestScheme() - data, err := s.EncodeToVersion(source, "v1") - if err != nil { - t.Errorf("%v: %v (%#v)", name, err, source) - return - } - obj2, err := s.Decode(data) - if err != nil { - t.Errorf("%v: %v (%v)", name, err, string(data)) - return - } - if !reflect.DeepEqual(source, obj2) { - t.Errorf("1: %v: diff: %v", name, objDiff(source, obj2)) - return - } - obj3 := reflect.New(reflect.TypeOf(source).Elem()).Interface() - err = s.DecodeInto(data, obj3) - if err != nil { - t.Errorf("2: %v: %v", name, err) - return - } - if !reflect.DeepEqual(source, obj3) { - t.Errorf("3: %v: diff: %v", name, objDiff(source, obj3)) - return - } -} - -func TestTypes(t *testing.T) { - table := []interface{}{ - &TestType1{}, - &ExternalInternalSame{}, - } - for _, item := range table { - // Try a few times, since runTest uses random values. - for i := 0; i < *fuzzIters; i++ { - runTest(t, item) - } - } -} - -func TestMultipleNames(t *testing.T) { - s := GetTestScheme() - - obj, err := s.Decode([]byte(`{"myKindKey":"TestType3","myVersionKey":"v1","A":"value"}`)) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - internal := obj.(*TestType1) - if internal.A != "value" { - t.Fatalf("unexpected decoded object: %#v", internal) - } - - out, err := s.EncodeToVersion(internal, "v1") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !strings.Contains(string(out), `"myKindKey":"TestType1"`) { - t.Errorf("unexpected encoded output: %s", string(out)) - } -} - -func TestConvertTypesWhenDefaultNamesMatch(t *testing.T) { - internalGV := unversioned.GroupVersion{} - externalGV := unversioned.GroupVersion{Version: "v1"} - - s := NewScheme() - // create two names internally, with TestType1 being preferred - s.AddKnownTypeWithName(internalGV.WithKind("TestType1"), &TestType1{}) - s.AddKnownTypeWithName(internalGV.WithKind("OtherType1"), &TestType1{}) - // create two names externally, with TestType1 being preferred - s.AddKnownTypeWithName(externalGV.WithKind("TestType1"), &ExternalTestType1{}) - s.AddKnownTypeWithName(externalGV.WithKind("OtherType1"), &ExternalTestType1{}) - s.MetaFactory = testMetaFactory{} - - ext := &ExternalTestType1{} - ext.APIVersion = "v1" - ext.ObjectKind = "OtherType1" - ext.A = "test" - data, err := json.Marshal(ext) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - expect := &TestType1{A: "test"} - - obj, err := s.Decode(data) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !reflect.DeepEqual(expect, obj) { - t.Errorf("unexpected object: %#v", obj) - } - - into := &TestType1{} - if err := s.DecodeInto(data, into); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !reflect.DeepEqual(expect, obj) { - t.Errorf("unexpected object: %#v", obj) - } -} - func TestKnownTypes(t *testing.T) { s := GetTestScheme() if len(s.KnownTypes(unversioned.GroupVersion{Group: "group", Version: "v2"})) != 0 { @@ -313,75 +179,3 @@ func TestConvertToVersionErr(t *testing.T) { t.Fatalf("unexpected non-error") } } - -func TestEncode_NonPtr(t *testing.T) { - s := GetTestScheme() - tt := TestType1{A: "I'm not a pointer object"} - obj := interface{}(tt) - data, err := s.EncodeToVersion(obj, "v1") - obj2, err2 := s.Decode(data) - if err != nil || err2 != nil { - t.Fatalf("Failure: '%v' '%v'", err, err2) - } - if _, ok := obj2.(*TestType1); !ok { - t.Fatalf("Got wrong type") - } - if !reflect.DeepEqual(obj2, &tt) { - t.Errorf("Expected:\n %#v,\n Got:\n %#v", &tt, obj2) - } -} - -func TestEncode_Ptr(t *testing.T) { - s := GetTestScheme() - tt := &TestType1{A: "I am a pointer object"} - obj := interface{}(tt) - data, err := s.EncodeToVersion(obj, "v1") - obj2, err2 := s.Decode(data) - if err != nil || err2 != nil { - t.Fatalf("Failure: '%v' '%v'", err, err2) - } - if _, ok := obj2.(*TestType1); !ok { - t.Fatalf("Got wrong type") - } - if !reflect.DeepEqual(obj2, tt) { - t.Errorf("Expected:\n %#v,\n Got:\n %#v", &tt, obj2) - } -} - -func TestBadJSONRejection(t *testing.T) { - s := GetTestScheme() - badJSONs := [][]byte{ - []byte(`{"myVersionKey":"v1"}`), // Missing kind - []byte(`{"myVersionKey":"v1","myKindKey":"bar"}`), // Unknown kind - []byte(`{"myVersionKey":"bar","myKindKey":"TestType1"}`), // Unknown version - } - for _, b := range badJSONs { - if _, err := s.Decode(b); err == nil { - t.Errorf("Did not reject bad json: %s", string(b)) - } - } - badJSONKindMismatch := []byte(`{"myVersionKey":"v1","myKindKey":"ExternalInternalSame"}`) - if err := s.DecodeInto(badJSONKindMismatch, &TestType1{}); err == nil { - t.Errorf("Kind is set but doesn't match the object type: %s", badJSONKindMismatch) - } - if err := s.DecodeInto([]byte(``), &TestType1{}); err == nil { - t.Errorf("Did not give error for empty data") - } -} - -func TestBadJSONRejectionForSetInternalVersion(t *testing.T) { - s := GetTestScheme() - s.InternalVersions[""] = unversioned.GroupVersion{Version: "v1"} - badJSONs := [][]byte{ - []byte(`{"myKindKey":"TestType1"}`), // Missing version - } - for _, b := range badJSONs { - if _, err := s.Decode(b); err == nil { - t.Errorf("Did not reject bad json: %s", string(b)) - } - } - badJSONKindMismatch := []byte(`{"myVersionKey":"v1","myKindKey":"ExternalInternalSame"}`) - if err := s.DecodeInto(badJSONKindMismatch, &TestType1{}); err == nil { - t.Errorf("Kind is set but doesn't match the object type: %s", badJSONKindMismatch) - } -} diff --git a/pkg/conversion/unversioned_test.go b/pkg/conversion/unversioned_test.go index e4b0c1ea73c..eee3e10c507 100644 --- a/pkg/conversion/unversioned_test.go +++ b/pkg/conversion/unversioned_test.go @@ -23,10 +23,10 @@ import ( // TODO: Ideally we should create the necessary package structure in e.g., // pkg/conversion/test/... instead of importing pkg/api here. - "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/latest" "k8s.io/kubernetes/pkg/api/testapi" "k8s.io/kubernetes/pkg/api/unversioned" - "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" + "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/runtime" ) @@ -67,8 +67,8 @@ func TestV1EncodeDecodeStatus(t *testing.T) { func TestExperimentalEncodeDecodeStatus(t *testing.T) { // TODO: caesarxuchao: use the testapi.Extensions.Codec() once the PR that // moves experimental from v1 to v1beta1 got merged. - expCodec := runtime.CodecFor(api.Scheme, v1beta1.SchemeGroupVersion) - encoded, err := expCodec.Encode(status) + expCodec := latest.Codecs.LegacyCodec(extensions.SchemeGroupVersion) + encoded, err := runtime.Encode(expCodec, status) if err != nil { t.Errorf("unexpected error: %v", err) } @@ -79,7 +79,7 @@ func TestExperimentalEncodeDecodeStatus(t *testing.T) { if typeMeta.Kind != "Status" { t.Errorf("Kind is not set to \"Status\". Got %s", encoded) } - if typeMeta.APIVersion != "" { + if typeMeta.APIVersion != "v1" { t.Errorf("APIVersion is not set to \"\". Got %s", encoded) } decoded, err := runtime.Decode(expCodec, encoded) diff --git a/pkg/runtime/serializer/protobuf/doc.go b/pkg/runtime/serializer/protobuf/doc.go new file mode 100644 index 00000000000..3fec7197aff --- /dev/null +++ b/pkg/runtime/serializer/protobuf/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2015 The Kubernetes Authors 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 protobuf handles serializing API objects to and from wire formats. +package protobuf