diff --git a/pkg/conversion/encode.go b/pkg/conversion/encode.go index 10c16a2e0ac..41a6561d6c8 100644 --- a/pkg/conversion/encode.go +++ b/pkg/conversion/encode.go @@ -19,6 +19,7 @@ package conversion import ( "encoding/json" "fmt" + "path" ) // EncodeToVersion turns the given api object into an appropriate JSON string. @@ -52,6 +53,14 @@ import ( func (s *Scheme) EncodeToVersion(obj interface{}, destVersion string) (data []byte, err error) { obj = maybeCopy(obj) v, _ := EnforcePtr(obj) // maybeCopy guarantees a pointer + + // Don't encode an object defined in the unversioned package, unless if the + // destVersion is v1, encode it to v1 for backward compatibility. + pkg := path.Base(v.Type().PkgPath()) + if pkg == "unversioned" && destVersion != "v1" { + return s.encodeUnversionedObject(obj) + } + if _, registered := s.typeToVersion[v.Type()]; !registered { return nil, 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(), destVersion) } @@ -102,3 +111,21 @@ func (s *Scheme) EncodeToVersion(obj interface{}, destVersion string) (data []by return data, nil } + +func (s *Scheme) encodeUnversionedObject(obj interface{}) (data []byte, err error) { + _, objKind, err := s.ObjectVersionAndKind(obj) + if err != nil { + return nil, err + } + if err = s.SetVersionAndKind("", objKind, 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/meta.go b/pkg/conversion/meta.go index 44ba29af67f..1488570f30e 100644 --- a/pkg/conversion/meta.go +++ b/pkg/conversion/meta.go @@ -19,6 +19,7 @@ package conversion import ( "encoding/json" "fmt" + "path" "reflect" ) @@ -77,6 +78,7 @@ func UpdateVersionAndKind(baseFields []string, versionField, version, kindField, if err != nil { return err } + pkg := path.Base(v.Type().PkgPath()) t := v.Type() name := t.Name() if v.Kind() != reflect.Struct { @@ -93,6 +95,15 @@ func UpdateVersionAndKind(baseFields []string, versionField, version, kindField, 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) diff --git a/pkg/conversion/meta_test.go b/pkg/conversion/meta_test.go index df001d59f74..452ea148301 100644 --- a/pkg/conversion/meta_test.go +++ b/pkg/conversion/meta_test.go @@ -17,8 +17,11 @@ limitations under the License. package conversion import ( + "fmt" "reflect" "testing" + + "k8s.io/kubernetes/pkg/api/unversioned" ) func TestSimpleMetaFactoryInterpret(t *testing.T) { @@ -72,6 +75,25 @@ func TestSimpleMetaFactoryUpdate(t *testing.T) { } } +// 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"} diff --git a/pkg/conversion/unversioned_test.go b/pkg/conversion/unversioned_test.go new file mode 100644 index 00000000000..3a4a1c22083 --- /dev/null +++ b/pkg/conversion/unversioned_test.go @@ -0,0 +1,92 @@ +/* +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 conversion_test + +import ( + "encoding/json" + "reflect" + "testing" + + // 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/testapi" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/runtime" +) + +var status = &unversioned.Status{ + Status: unversioned.StatusFailure, + Code: 200, + Reason: unversioned.StatusReasonUnknown, + Message: "", +} + +func TestV1EncodeDecodeStatus(t *testing.T) { + + v1Codec := testapi.Default.Codec() + + encoded, err := v1Codec.Encode(status) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + typeMeta := unversioned.TypeMeta{} + if err := json.Unmarshal(encoded, &typeMeta); err != nil { + t.Errorf("unexpected error: %v", err) + } + if typeMeta.Kind != "Status" { + t.Errorf("Kind is not set to \"Status\". Got %v", string(encoded)) + } + if typeMeta.APIVersion != "v1" { + t.Errorf("APIVersion is not set to \"v1\". Got %v", string(encoded)) + } + decoded, err := v1Codec.Decode(encoded) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !reflect.DeepEqual(status, decoded) { + t.Errorf("expected: %v, got: %v", status, decoded) + } +} + +func TestExperimentalEncodeDecodeStatus(t *testing.T) { + // TODO: caesarxuchao: use the testapi.Experimental.Codec() once the PR that + // moves experimental from v1 to v1alpha1 got merged. + // expCodec := testapi.Experimental.Codec() + expCodec := runtime.CodecFor(api.Scheme, "experimental/v1alpha1") + encoded, err := expCodec.Encode(status) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + typeMeta := unversioned.TypeMeta{} + if err := json.Unmarshal(encoded, &typeMeta); err != nil { + t.Errorf("unexpected error: %v", err) + } + if typeMeta.Kind != "Status" { + t.Errorf("Kind is not set to \"Status\". Got %s", encoded) + } + if typeMeta.APIVersion != "" { + t.Errorf("APIVersion is not set to \"\". Got %s", encoded) + } + decoded, err := expCodec.Decode(encoded) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !reflect.DeepEqual(status, decoded) { + t.Errorf("expected: %v, got: %v", status, decoded) + } +}