From 81858d755ec1b468f807b90458ff23ae444cf3da Mon Sep 17 00:00:00 2001 From: Chao Xu Date: Mon, 25 Jul 2016 22:03:39 -0700 Subject: [PATCH] add validateListType to pkg/api/meta/schema_test.go --- .../unversioned/testgroup_test.go | 6 +- docs/devel/api-conventions.md | 5 +- pkg/api/meta/scheme_test.go | 121 ++++++++++++++++++ pkg/api/testapi/testapi.go | 51 ++++++-- 4 files changed, 167 insertions(+), 16 deletions(-) create mode 100644 pkg/api/meta/scheme_test.go diff --git a/cmd/libs/go2idl/client-gen/testoutput/clientset_generated/test_internalclientset/typed/testgroup.k8s.io/unversioned/testgroup_test.go b/cmd/libs/go2idl/client-gen/testoutput/clientset_generated/test_internalclientset/typed/testgroup.k8s.io/unversioned/testgroup_test.go index ec009e0446f..530f3c5b5b6 100644 --- a/cmd/libs/go2idl/client-gen/testoutput/clientset_generated/test_internalclientset/typed/testgroup.k8s.io/unversioned/testgroup_test.go +++ b/cmd/libs/go2idl/client-gen/testoutput/clientset_generated/test_internalclientset/typed/testgroup.k8s.io/unversioned/testgroup_test.go @@ -39,10 +39,12 @@ func init() { if _, found := testapi.Groups[testgroup.SchemeGroupVersion.Group]; found { return } + externalGroupVersion := registered.GroupOrDie(testgroup.SchemeGroupVersion.Group).GroupVersion testapi.Groups[testgroup.SchemeGroupVersion.Group] = testapi.NewTestGroup( - registered.GroupOrDie(testgroup.SchemeGroupVersion.Group).GroupVersion, + externalGroupVersion, testgroup.SchemeGroupVersion, - api.Scheme.KnownTypes(testgroup.SchemeGroupVersion)) + api.Scheme.KnownTypes(testgroup.SchemeGroupVersion), + api.Scheme.KnownTypes(externalGroupVersion)) testHelper = testapi.Groups[testgroup.SchemeGroupVersion.Group] } diff --git a/docs/devel/api-conventions.md b/docs/devel/api-conventions.md index 8247c7260ca..5bc731be7a0 100644 --- a/docs/devel/api-conventions.md +++ b/docs/devel/api-conventions.md @@ -134,8 +134,9 @@ specific actions that create, update, delete, or get. 2. **Lists** are collections of **resources** of one (usually) or more (occasionally) kinds. - Lists have a limited set of common metadata. All lists use the "items" field -to contain the array of objects they return. + The name of a list kind must end with "List". Lists have a limited set of +common metadata. All lists use the required "items" field to contain the array +of objects they return. Any kind that has the "items" field must be a list kind. Most objects defined in the system should have an endpoint that returns the full set of resources, as well as zero or more endpoints that return subsets of diff --git a/pkg/api/meta/scheme_test.go b/pkg/api/meta/scheme_test.go new file mode 100644 index 00000000000..5c0425622b7 --- /dev/null +++ b/pkg/api/meta/scheme_test.go @@ -0,0 +1,121 @@ +/* +Copyright 2016 The Kubernetes Authors. + +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 meta_test + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "k8s.io/kubernetes/pkg/api/testapi" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/util/sets" +) + +// These types do not follow the list convention as documented in +// docs/devel/api-convention.md +var listTypeExceptions = sets.NewString("APIGroupList", "APIResourceList") + +func validateListType(target reflect.Type) error { + // exceptions + if listTypeExceptions.Has(target.Name()) { + return nil + } + hasListSuffix := strings.HasSuffix(target.Name(), "List") + hasMetadata := false + hasItems := false + for i := 0; i < target.NumField(); i++ { + field := target.Field(i) + tag := field.Tag.Get("json") + switch { + case strings.HasPrefix(tag, "metadata"): + hasMetadata = true + case tag == "items": + hasItems = true + if field.Type.Kind() != reflect.Slice { + return fmt.Errorf("Expected items to be slice, got %s", field.Type.Kind()) + } + } + } + if hasListSuffix && !hasMetadata { + return fmt.Errorf("Expected type %s to contain \"metadata\"", target.Name()) + } + if hasListSuffix && !hasItems { + return fmt.Errorf("Expected type %s to contain \"items\"", target.Name()) + } + // if a type contains field Items with JSON tag "items", its name should end with List. + if !hasListSuffix && hasItems { + return fmt.Errorf("Type %s has Items, its name is expected to end with \"List\"", target.Name()) + } + return nil +} + +// TestListTypes verifies that no external type violates the api convention of +// list types. +func TestListTypes(t *testing.T) { + for groupKey, group := range testapi.Groups { + for kind, target := range group.ExternalTypes() { + t.Logf("working on %v in %v", kind, groupKey) + err := validateListType(target) + if err != nil { + t.Error(err) + } + } + } +} + +type WithoutMetaDataList struct { + unversioned.TypeMeta `json:",inline"` + unversioned.ListMeta + Items []interface{} `json:"items"` +} + +type WithoutItemsList struct { + unversioned.TypeMeta `json:",inline"` + unversioned.ListMeta `json:"metadata,omitempty"` +} + +type WrongItemsJSONTagList struct { + unversioned.TypeMeta `json:",inline"` + unversioned.ListMeta `json:"metadata,omitempty"` + Items []interface{} `json:"items,omitempty"` +} + +// If a type has Items, its name should end with "List" +type ListWithWrongName struct { + unversioned.TypeMeta `json:",inline"` + unversioned.ListMeta `json:"metadata,omitempty"` + Items []interface{} `json:"items"` +} + +// TestValidateListType verifies the validateListType function reports error on +// types that violate the api convention. +func TestValidateListType(t *testing.T) { + var testTypes = []interface{}{ + WithoutMetaDataList{}, + WithoutItemsList{}, + WrongItemsJSONTagList{}, + ListWithWrongName{}, + } + for _, testType := range testTypes { + err := validateListType(reflect.TypeOf(testType)) + if err == nil { + t.Errorf("Expected error") + } + } +} diff --git a/pkg/api/testapi/testapi.go b/pkg/api/testapi/testapi.go index f58e1c61ff2..e4557b39871 100644 --- a/pkg/api/testapi/testapi.go +++ b/pkg/api/testapi/testapi.go @@ -71,6 +71,7 @@ type TestGroup struct { externalGroupVersion unversioned.GroupVersion internalGroupVersion unversioned.GroupVersion internalTypes map[string]reflect.Type + externalTypes map[string]reflect.Type } func init() { @@ -112,22 +113,27 @@ func init() { externalGroupVersion: groupVersion, internalGroupVersion: internalGroupVersion, internalTypes: api.Scheme.KnownTypes(internalGroupVersion), + externalTypes: api.Scheme.KnownTypes(groupVersion), } } } if _, ok := Groups[api.GroupName]; !ok { + externalGroupVersion := unversioned.GroupVersion{Group: api.GroupName, Version: registered.GroupOrDie(api.GroupName).GroupVersion.Version} Groups[api.GroupName] = TestGroup{ - externalGroupVersion: unversioned.GroupVersion{Group: api.GroupName, Version: registered.GroupOrDie(api.GroupName).GroupVersion.Version}, + externalGroupVersion: externalGroupVersion, internalGroupVersion: api.SchemeGroupVersion, internalTypes: api.Scheme.KnownTypes(api.SchemeGroupVersion), + externalTypes: api.Scheme.KnownTypes(externalGroupVersion), } } if _, ok := Groups[extensions.GroupName]; !ok { + externalGroupVersion := unversioned.GroupVersion{Group: extensions.GroupName, Version: registered.GroupOrDie(extensions.GroupName).GroupVersion.Version} Groups[extensions.GroupName] = TestGroup{ - externalGroupVersion: unversioned.GroupVersion{Group: extensions.GroupName, Version: registered.GroupOrDie(extensions.GroupName).GroupVersion.Version}, + externalGroupVersion: externalGroupVersion, internalGroupVersion: extensions.SchemeGroupVersion, internalTypes: api.Scheme.KnownTypes(extensions.SchemeGroupVersion), + externalTypes: api.Scheme.KnownTypes(externalGroupVersion), } } if _, ok := Groups[autoscaling.GroupName]; !ok { @@ -138,10 +144,12 @@ func init() { } internalTypes[k] = t } + externalGroupVersion := unversioned.GroupVersion{Group: autoscaling.GroupName, Version: registered.GroupOrDie(autoscaling.GroupName).GroupVersion.Version} Groups[autoscaling.GroupName] = TestGroup{ - externalGroupVersion: unversioned.GroupVersion{Group: autoscaling.GroupName, Version: registered.GroupOrDie(autoscaling.GroupName).GroupVersion.Version}, + externalGroupVersion: externalGroupVersion, internalGroupVersion: extensions.SchemeGroupVersion, internalTypes: internalTypes, + externalTypes: api.Scheme.KnownTypes(externalGroupVersion), } } if _, ok := Groups[autoscaling.GroupName+"IntraGroup"]; !ok { @@ -152,52 +160,66 @@ func init() { break } } + externalGroupVersion := unversioned.GroupVersion{Group: autoscaling.GroupName, Version: registered.GroupOrDie(autoscaling.GroupName).GroupVersion.Version} Groups[autoscaling.GroupName] = TestGroup{ - externalGroupVersion: unversioned.GroupVersion{Group: autoscaling.GroupName, Version: registered.GroupOrDie(autoscaling.GroupName).GroupVersion.Version}, + externalGroupVersion: externalGroupVersion, internalGroupVersion: autoscaling.SchemeGroupVersion, internalTypes: internalTypes, + externalTypes: api.Scheme.KnownTypes(externalGroupVersion), } } if _, ok := Groups[batch.GroupName]; !ok { + externalGroupVersion := unversioned.GroupVersion{Group: batch.GroupName, Version: registered.GroupOrDie(batch.GroupName).GroupVersion.Version} Groups[batch.GroupName] = TestGroup{ - externalGroupVersion: unversioned.GroupVersion{Group: batch.GroupName, Version: registered.GroupOrDie(batch.GroupName).GroupVersion.Version}, + externalGroupVersion: externalGroupVersion, internalGroupVersion: batch.SchemeGroupVersion, internalTypes: api.Scheme.KnownTypes(batch.SchemeGroupVersion), + externalTypes: api.Scheme.KnownTypes(externalGroupVersion), } } if _, ok := Groups[apps.GroupName]; !ok { + externalGroupVersion := unversioned.GroupVersion{Group: apps.GroupName, Version: registered.GroupOrDie(apps.GroupName).GroupVersion.Version} Groups[apps.GroupName] = TestGroup{ - externalGroupVersion: unversioned.GroupVersion{Group: apps.GroupName, Version: registered.GroupOrDie(apps.GroupName).GroupVersion.Version}, + externalGroupVersion: externalGroupVersion, internalGroupVersion: extensions.SchemeGroupVersion, internalTypes: api.Scheme.KnownTypes(extensions.SchemeGroupVersion), + externalTypes: api.Scheme.KnownTypes(externalGroupVersion), } } if _, ok := Groups[policy.GroupName]; !ok { + externalGroupVersion := unversioned.GroupVersion{Group: policy.GroupName, Version: registered.GroupOrDie(policy.GroupName).GroupVersion.Version} Groups[policy.GroupName] = TestGroup{ - externalGroupVersion: unversioned.GroupVersion{Group: policy.GroupName, Version: registered.GroupOrDie(policy.GroupName).GroupVersion.Version}, + externalGroupVersion: externalGroupVersion, internalGroupVersion: policy.SchemeGroupVersion, internalTypes: api.Scheme.KnownTypes(policy.SchemeGroupVersion), + externalTypes: api.Scheme.KnownTypes(externalGroupVersion), } } if _, ok := Groups[federation.GroupName]; !ok { + externalGroupVersion := unversioned.GroupVersion{Group: federation.GroupName, Version: registered.GroupOrDie(federation.GroupName).GroupVersion.Version} Groups[federation.GroupName] = TestGroup{ - externalGroupVersion: unversioned.GroupVersion{Group: federation.GroupName, Version: registered.GroupOrDie(federation.GroupName).GroupVersion.Version}, + externalGroupVersion: externalGroupVersion, internalGroupVersion: federation.SchemeGroupVersion, internalTypes: api.Scheme.KnownTypes(federation.SchemeGroupVersion), + externalTypes: api.Scheme.KnownTypes(externalGroupVersion), } } if _, ok := Groups[rbac.GroupName]; !ok { + externalGroupVersion := unversioned.GroupVersion{Group: rbac.GroupName, Version: registered.GroupOrDie(rbac.GroupName).GroupVersion.Version} Groups[rbac.GroupName] = TestGroup{ - externalGroupVersion: unversioned.GroupVersion{Group: rbac.GroupName, Version: registered.GroupOrDie(rbac.GroupName).GroupVersion.Version}, + externalGroupVersion: externalGroupVersion, internalGroupVersion: rbac.SchemeGroupVersion, internalTypes: api.Scheme.KnownTypes(rbac.SchemeGroupVersion), + externalTypes: api.Scheme.KnownTypes(externalGroupVersion), } } if _, ok := Groups[certificates.GroupName]; !ok { + externalGroupVersion := unversioned.GroupVersion{Group: certificates.GroupName, Version: registered.GroupOrDie(certificates.GroupName).GroupVersion.Version} Groups[certificates.GroupName] = TestGroup{ - externalGroupVersion: unversioned.GroupVersion{Group: certificates.GroupName, Version: registered.GroupOrDie(certificates.GroupName).GroupVersion.Version}, + externalGroupVersion: externalGroupVersion, internalGroupVersion: certificates.SchemeGroupVersion, internalTypes: api.Scheme.KnownTypes(certificates.SchemeGroupVersion), + externalTypes: api.Scheme.KnownTypes(externalGroupVersion), } } @@ -232,6 +254,11 @@ func (g TestGroup) InternalTypes() map[string]reflect.Type { return g.internalTypes } +// ExternalTypes returns a map of external API types' kind names to their Go types. +func (g TestGroup) ExternalTypes() map[string]reflect.Type { + return g.externalTypes +} + // Codec returns the codec for the API version to test against, as set by the // KUBE_TEST_API_TYPE env var. func (g TestGroup) Codec() runtime.Codec { @@ -387,6 +414,6 @@ func GetCodecForObject(obj runtime.Object) (runtime.Codec, error) { return nil, fmt.Errorf("unexpected kind: %v", kind) } -func NewTestGroup(external, internal unversioned.GroupVersion, internalTypes map[string]reflect.Type) TestGroup { - return TestGroup{external, internal, internalTypes} +func NewTestGroup(external, internal unversioned.GroupVersion, internalTypes map[string]reflect.Type, externalTypes map[string]reflect.Type) TestGroup { + return TestGroup{external, internal, internalTypes, externalTypes} }