From 828e4d35a72d8e879695cc43b594b345124f582e Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Fri, 14 Aug 2015 22:10:15 -0700 Subject: [PATCH 1/6] Add a storage implementation for thirdpartyresources. --- pkg/api/v1/defaults.go | 5 +++++ pkg/kubectl/resource_printer.go | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/api/v1/defaults.go b/pkg/api/v1/defaults.go index 5abf1ab2cd6..a29563c232c 100644 --- a/pkg/api/v1/defaults.go +++ b/pkg/api/v1/defaults.go @@ -25,6 +25,11 @@ import ( func addDefaultingFuncs() { api.Scheme.AddDefaultingFuncs( + func(obj *APIVersion) { + if len(obj.APIGroup) == 0 { + obj.APIGroup = "experimental" + } + }, func(obj *ReplicationController) { var labels map[string]string if obj.Spec.Template != nil { diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index 3d649a6efc1..548f2753167 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -1088,7 +1088,7 @@ func printThirdPartyResource(rsrc *expapi.ThirdPartyResource, w io.Writer, withN func printThirdPartyResourceList(list *expapi.ThirdPartyResourceList, w io.Writer, withNamespace bool, wide bool, showAll bool, columnLabels []string) error { for _, item := range list.Items { - if err := printThirdPartyResource(&item, w, withNamespace, wide, showAll, columnLabels); err != nil { + if err := printThirdPartyResource(&item, w, withNamespace, wide, columnLabels); err != nil { return err } } From 7bfc8b5f37c381e35faccacfe9c3ba3b97a1129d Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Wed, 19 Aug 2015 11:02:01 -0700 Subject: [PATCH 2/6] Add dynamic APIs --- pkg/expapi/deep_copy_generated.go | 40 ++++ pkg/expapi/register.go | 4 + pkg/expapi/types.go | 9 + pkg/expapi/v1/conversion_generated.go | 82 +++++++ pkg/expapi/v1/deep_copy_generated.go | 40 ++++ pkg/expapi/v1/register.go | 4 + pkg/expapi/v1/types.go | 9 + pkg/master/master.go | 49 +++++ pkg/master/master_test.go | 47 ++++ pkg/registry/thirdpartyresourcedata/doc.go | 19 ++ .../thirdpartyresourcedata/etcd/etcd.go | 63 ++++++ .../thirdpartyresourcedata/etcd/etcd_test.go | 205 ++++++++++++++++++ .../thirdpartyresourcedata/registry.go | 88 ++++++++ .../thirdpartyresourcedata/strategy.go | 88 ++++++++ 14 files changed, 747 insertions(+) create mode 100644 pkg/registry/thirdpartyresourcedata/doc.go create mode 100644 pkg/registry/thirdpartyresourcedata/etcd/etcd.go create mode 100644 pkg/registry/thirdpartyresourcedata/etcd/etcd_test.go create mode 100644 pkg/registry/thirdpartyresourcedata/registry.go create mode 100644 pkg/registry/thirdpartyresourcedata/strategy.go diff --git a/pkg/expapi/deep_copy_generated.go b/pkg/expapi/deep_copy_generated.go index 92cc7532ed5..1095995538e 100644 --- a/pkg/expapi/deep_copy_generated.go +++ b/pkg/expapi/deep_copy_generated.go @@ -1005,6 +1005,44 @@ func deepCopy_expapi_ThirdPartyResource(in ThirdPartyResource, out *ThirdPartyRe return nil } +func deepCopy_expapi_ThirdPartyResourceData(in ThirdPartyResourceData, out *ThirdPartyResourceData, c *conversion.Cloner) error { + if err := deepCopy_api_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + if err := deepCopy_api_ObjectMeta(in.ObjectMeta, &out.ObjectMeta, c); err != nil { + return err + } + if in.Data != nil { + out.Data = make([]uint8, len(in.Data)) + for i := range in.Data { + out.Data[i] = in.Data[i] + } + } else { + out.Data = nil + } + return nil +} + +func deepCopy_expapi_ThirdPartyResourceDataList(in ThirdPartyResourceDataList, out *ThirdPartyResourceDataList, c *conversion.Cloner) error { + if err := deepCopy_api_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + if err := deepCopy_api_ListMeta(in.ListMeta, &out.ListMeta, c); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]ThirdPartyResourceData, len(in.Items)) + for i := range in.Items { + if err := deepCopy_expapi_ThirdPartyResourceData(in.Items[i], &out.Items[i], c); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + func deepCopy_expapi_ThirdPartyResourceList(in ThirdPartyResourceList, out *ThirdPartyResourceList, c *conversion.Cloner) error { if err := deepCopy_api_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { return err @@ -1101,6 +1139,8 @@ func init() { deepCopy_expapi_ScaleStatus, deepCopy_expapi_SubresourceReference, deepCopy_expapi_ThirdPartyResource, + deepCopy_expapi_ThirdPartyResourceData, + deepCopy_expapi_ThirdPartyResourceDataList, deepCopy_expapi_ThirdPartyResourceList, deepCopy_util_IntOrString, deepCopy_util_Time, diff --git a/pkg/expapi/register.go b/pkg/expapi/register.go index 7c0997e40ad..35d9246e43e 100644 --- a/pkg/expapi/register.go +++ b/pkg/expapi/register.go @@ -38,6 +38,8 @@ func addKnownTypes() { &ThirdPartyResourceList{}, &DaemonList{}, &Daemon{}, + &ThirdPartyResourceData{}, + &ThirdPartyResourceDataList{}, ) } @@ -51,3 +53,5 @@ func (*ThirdPartyResource) IsAnAPIObject() {} func (*ThirdPartyResourceList) IsAnAPIObject() {} func (*Daemon) IsAnAPIObject() {} func (*DaemonList) IsAnAPIObject() {} +func (*ThirdPartyResourceData) IsAnAPIObject() {} +func (*ThirdPartyResourceDataList) IsAnAPIObject() {} diff --git a/pkg/expapi/types.go b/pkg/expapi/types.go index 2cb481d8a0a..68ec8aadb22 100644 --- a/pkg/expapi/types.go +++ b/pkg/expapi/types.go @@ -167,6 +167,7 @@ type ThirdPartyResourceData struct { Data []byte `json:"name,omitempty" description:"the raw JSON data for this data"` } +<<<<<<< HEAD type Deployment struct { api.TypeMeta `json:",inline"` api.ObjectMeta `json:"metadata,omitempty"` @@ -271,6 +272,7 @@ type DeploymentList struct { Items []Deployment `json:"items"` } +<<<<<<< HEAD // DaemonSpec is the specification of a daemon. type DaemonSpec struct { // Selector is a label query over pods that are managed by the daemon. @@ -318,3 +320,10 @@ type DaemonList struct { Items []Daemon `json:"items"` } + +type ThirdPartyResourceDataList struct { + api.TypeMeta `json:",inline"` + api.ListMeta `json:"metadata,omitempty" description:"standard list metadata; see http://docs.k8s.io/api-conventions.md#metadata"` + + Items []ThirdPartyResourceData `json:"items" description:"items is a list of third party objects"` +} diff --git a/pkg/expapi/v1/conversion_generated.go b/pkg/expapi/v1/conversion_generated.go index fe8a0a19b27..7063f17a7bc 100644 --- a/pkg/expapi/v1/conversion_generated.go +++ b/pkg/expapi/v1/conversion_generated.go @@ -1742,6 +1742,45 @@ func convert_expapi_ThirdPartyResource_To_v1_ThirdPartyResource(in *expapi.Third return nil } +func convert_expapi_ThirdPartyResourceData_To_v1_ThirdPartyResourceData(in *expapi.ThirdPartyResourceData, out *ThirdPartyResourceData, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*expapi.ThirdPartyResourceData))(in) + } + if err := convert_api_TypeMeta_To_v1_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_api_ObjectMeta_To_v1_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil { + return err + } + if err := s.Convert(&in.Data, &out.Data, 0); err != nil { + return err + } + return nil +} + +func convert_expapi_ThirdPartyResourceDataList_To_v1_ThirdPartyResourceDataList(in *expapi.ThirdPartyResourceDataList, out *ThirdPartyResourceDataList, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*expapi.ThirdPartyResourceDataList))(in) + } + if err := convert_api_TypeMeta_To_v1_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_api_ListMeta_To_v1_ListMeta(&in.ListMeta, &out.ListMeta, s); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]ThirdPartyResourceData, len(in.Items)) + for i := range in.Items { + if err := convert_expapi_ThirdPartyResourceData_To_v1_ThirdPartyResourceData(&in.Items[i], &out.Items[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + func convert_expapi_ThirdPartyResourceList_To_v1_ThirdPartyResourceList(in *expapi.ThirdPartyResourceList, out *ThirdPartyResourceList, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*expapi.ThirdPartyResourceList))(in) @@ -2105,6 +2144,45 @@ func convert_v1_ThirdPartyResource_To_expapi_ThirdPartyResource(in *ThirdPartyRe return nil } +func convert_v1_ThirdPartyResourceData_To_expapi_ThirdPartyResourceData(in *ThirdPartyResourceData, out *expapi.ThirdPartyResourceData, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*ThirdPartyResourceData))(in) + } + if err := convert_v1_TypeMeta_To_api_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_v1_ObjectMeta_To_api_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil { + return err + } + if err := s.Convert(&in.Data, &out.Data, 0); err != nil { + return err + } + return nil +} + +func convert_v1_ThirdPartyResourceDataList_To_expapi_ThirdPartyResourceDataList(in *ThirdPartyResourceDataList, out *expapi.ThirdPartyResourceDataList, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*ThirdPartyResourceDataList))(in) + } + if err := convert_v1_TypeMeta_To_api_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_v1_ListMeta_To_api_ListMeta(&in.ListMeta, &out.ListMeta, s); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]expapi.ThirdPartyResourceData, len(in.Items)) + for i := range in.Items { + if err := convert_v1_ThirdPartyResourceData_To_expapi_ThirdPartyResourceData(&in.Items[i], &out.Items[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + func convert_v1_ThirdPartyResourceList_To_expapi_ThirdPartyResourceList(in *ThirdPartyResourceList, out *expapi.ThirdPartyResourceList, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*ThirdPartyResourceList))(in) @@ -2184,6 +2262,8 @@ func init() { convert_expapi_ScaleStatus_To_v1_ScaleStatus, convert_expapi_Scale_To_v1_Scale, convert_expapi_SubresourceReference_To_v1_SubresourceReference, + convert_expapi_ThirdPartyResourceDataList_To_v1_ThirdPartyResourceDataList, + convert_expapi_ThirdPartyResourceData_To_v1_ThirdPartyResourceData, convert_expapi_ThirdPartyResourceList_To_v1_ThirdPartyResourceList, convert_expapi_ThirdPartyResource_To_v1_ThirdPartyResource, convert_v1_APIVersion_To_expapi_APIVersion, @@ -2235,6 +2315,8 @@ func init() { convert_v1_SecurityContext_To_api_SecurityContext, convert_v1_SubresourceReference_To_expapi_SubresourceReference, convert_v1_TCPSocketAction_To_api_TCPSocketAction, + convert_v1_ThirdPartyResourceDataList_To_expapi_ThirdPartyResourceDataList, + convert_v1_ThirdPartyResourceData_To_expapi_ThirdPartyResourceData, convert_v1_ThirdPartyResourceList_To_expapi_ThirdPartyResourceList, convert_v1_ThirdPartyResource_To_expapi_ThirdPartyResource, convert_v1_TypeMeta_To_api_TypeMeta, diff --git a/pkg/expapi/v1/deep_copy_generated.go b/pkg/expapi/v1/deep_copy_generated.go index 6b9e827097a..24bc642d757 100644 --- a/pkg/expapi/v1/deep_copy_generated.go +++ b/pkg/expapi/v1/deep_copy_generated.go @@ -1012,6 +1012,44 @@ func deepCopy_v1_ThirdPartyResource(in ThirdPartyResource, out *ThirdPartyResour return nil } +func deepCopy_v1_ThirdPartyResourceData(in ThirdPartyResourceData, out *ThirdPartyResourceData, c *conversion.Cloner) error { + if err := deepCopy_v1_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + if err := deepCopy_v1_ObjectMeta(in.ObjectMeta, &out.ObjectMeta, c); err != nil { + return err + } + if in.Data != nil { + out.Data = make([]uint8, len(in.Data)) + for i := range in.Data { + out.Data[i] = in.Data[i] + } + } else { + out.Data = nil + } + return nil +} + +func deepCopy_v1_ThirdPartyResourceDataList(in ThirdPartyResourceDataList, out *ThirdPartyResourceDataList, c *conversion.Cloner) error { + if err := deepCopy_v1_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + if err := deepCopy_v1_ListMeta(in.ListMeta, &out.ListMeta, c); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]ThirdPartyResourceData, len(in.Items)) + for i := range in.Items { + if err := deepCopy_v1_ThirdPartyResourceData(in.Items[i], &out.Items[i], c); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + func deepCopy_v1_ThirdPartyResourceList(in ThirdPartyResourceList, out *ThirdPartyResourceList, c *conversion.Cloner) error { if err := deepCopy_v1_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { return err @@ -1108,6 +1146,8 @@ func init() { deepCopy_v1_ScaleStatus, deepCopy_v1_SubresourceReference, deepCopy_v1_ThirdPartyResource, + deepCopy_v1_ThirdPartyResourceData, + deepCopy_v1_ThirdPartyResourceDataList, deepCopy_v1_ThirdPartyResourceList, deepCopy_util_IntOrString, deepCopy_util_Time, diff --git a/pkg/expapi/v1/register.go b/pkg/expapi/v1/register.go index 2beed0adc0c..e6fab8c212e 100644 --- a/pkg/expapi/v1/register.go +++ b/pkg/expapi/v1/register.go @@ -42,6 +42,8 @@ func addKnownTypes() { &ThirdPartyResourceList{}, &DaemonList{}, &Daemon{}, + &ThirdPartyResourceData{}, + &ThirdPartyResourceDataList{}, ) } @@ -55,3 +57,5 @@ func (*ThirdPartyResource) IsAnAPIObject() {} func (*ThirdPartyResourceList) IsAnAPIObject() {} func (*Daemon) IsAnAPIObject() {} func (*DaemonList) IsAnAPIObject() {} +func (*ThirdPartyResourceData) IsAnAPIObject() {} +func (*ThirdPartyResourceDataList) IsAnAPIObject() {} diff --git a/pkg/expapi/v1/types.go b/pkg/expapi/v1/types.go index 2a19aa868d5..65d55c6748b 100644 --- a/pkg/expapi/v1/types.go +++ b/pkg/expapi/v1/types.go @@ -153,6 +153,7 @@ type ThirdPartyResourceData struct { Data []byte `json:"name,omitempty" description:"the raw JSON data for this data"` } +<<<<<<< HEAD type Deployment struct { v1.TypeMeta `json:",inline"` v1.ObjectMeta `json:"metadata,omitempty"` @@ -258,6 +259,7 @@ type DeploymentList struct { Items []Deployment `json:"items" description:"list of deployments"` } +<<<<<<< HEAD // DaemonSpec is the specification of a daemon. type DaemonSpec struct { // Selector is a label query over pods that are managed by the daemon. @@ -318,3 +320,10 @@ type DaemonList struct { // Items is a list of daemons. Items []Daemon `json:"items"` } + +type ThirdPartyResourceDataList struct { + v1.TypeMeta `json:",inline"` + v1.ListMeta `json:"metadata,omitempty" description:"standard list metadata; see http://docs.k8s.io/api-conventions.md#metadata"` + + Items []ThirdPartyResourceData `json:"items" description:"items is a list of third party objects"` +} diff --git a/pkg/master/master.go b/pkg/master/master.go index f0a40edf022..0ddf096c791 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -42,6 +42,7 @@ import ( "k8s.io/kubernetes/pkg/auth/authorizer" "k8s.io/kubernetes/pkg/auth/handlers" client "k8s.io/kubernetes/pkg/client/unversioned" + "k8s.io/kubernetes/pkg/expapi" explatest "k8s.io/kubernetes/pkg/expapi/latest" "k8s.io/kubernetes/pkg/fields" "k8s.io/kubernetes/pkg/healthz" @@ -71,6 +72,7 @@ import ( ipallocator "k8s.io/kubernetes/pkg/registry/service/ipallocator" serviceaccountetcd "k8s.io/kubernetes/pkg/registry/serviceaccount/etcd" thirdpartyresourceetcd "k8s.io/kubernetes/pkg/registry/thirdpartyresource/etcd" + thirdpartyresourcedataetcd "k8s.io/kubernetes/pkg/registry/thirdpartyresourcedata/etcd" "k8s.io/kubernetes/pkg/storage" etcdstorage "k8s.io/kubernetes/pkg/storage/etcd" "k8s.io/kubernetes/pkg/tools" @@ -233,6 +235,9 @@ type Master struct { lastSync int64 // Seconds since Epoch lastSyncMetric prometheus.GaugeFunc clock util.Clock + + // storage for third party objects + thirdPartyStorage storage.Interface } // NewEtcdStorage returns a storage.Interface for the provided arguments or an error if the version @@ -765,6 +770,50 @@ func (m *Master) api_v1() *apiserver.APIGroupVersion { return version } +func (m *Master) InstallThirdPartyAPI(rsrc *expapi.ThirdPartyResource) error { + thirdparty := m.thirdpartyapi(rsrc) + if err := thirdparty.InstallREST(m.handlerContainer); err != nil { + glog.Fatalf("Unable to setup thirdparty api: %v", err) + } + thirdPartyPrefix := "/thirdparty/" + rsrc.Name + "/" + apiRoot := rsrc.Name + apiserver.AddApiWebService(m.handlerContainer, thirdPartyPrefix, []string{rsrc.Versions[0].Name}) + thirdPartyRequestInfoResolver := &apiserver.APIRequestInfoResolver{APIPrefixes: util.NewStringSet(strings.TrimPrefix(apiRoot, "/")), RestMapper: thirdparty.Mapper} + apiserver.InstallServiceErrorHandler(m.handlerContainer, thirdPartyRequestInfoResolver, []string{thirdparty.Version}) + return nil +} + +func (m *Master) thirdpartyapi(rsrc *expapi.ThirdPartyResource) *apiserver.APIGroupVersion { + resourceStorage := thirdpartyresourcedataetcd.NewREST(m.thirdPartyStorage) + + apiGroup := rsrc.Name + apiRoot := "/thirdparty/" + rsrc.Name + "/" + rsrc.Versions[0].Name + + storage := map[string]rest.Storage{ + apiGroup: resourceStorage, + } + + return &apiserver.APIGroupVersion{ + Root: apiRoot, + + Creater: api.Scheme, + Convertor: api.Scheme, + Typer: api.Scheme, + + Mapper: explatest.RESTMapper, + Codec: explatest.Codec, + Linker: explatest.SelfLinker, + Storage: storage, + Version: rsrc.Versions[0].Name, + + Admit: m.admissionControl, + Context: m.requestContextMapper, + + ProxyDialerFn: m.dialer, + MinRequestTimeout: m.minRequestTimeout, + } +} + // expapi returns the resources and codec for the experimental api func (m *Master) expapi(c *Config) *apiserver.APIGroupVersion { controllerStorage := expcontrolleretcd.NewStorage(c.ExpDatabaseStorage) diff --git a/pkg/master/master_test.go b/pkg/master/master_test.go index 779f7ffcf01..23b9a21fb52 100644 --- a/pkg/master/master_test.go +++ b/pkg/master/master_test.go @@ -17,15 +17,21 @@ limitations under the License. package master import ( + "io/ioutil" + "net/http" + "net/http/httptest" "testing" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/latest" + "k8s.io/kubernetes/pkg/expapi" explatest "k8s.io/kubernetes/pkg/expapi/latest" "k8s.io/kubernetes/pkg/registry/registrytest" etcdstorage "k8s.io/kubernetes/pkg/storage/etcd" "k8s.io/kubernetes/pkg/tools" "k8s.io/kubernetes/pkg/tools/etcdtest" + + "github.com/emicklei/go-restful" ) func TestGetServersToValidate(t *testing.T) { @@ -73,3 +79,44 @@ func TestFindExternalAddress(t *testing.T) { t.Errorf("expected findExternalAddress to fail on a node with missing ip information") } } + +func TestInstallThirdPartyAPI(t *testing.T) { + master := &Master{} + api := &expapi.ThirdPartyResource{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + }, + Versions: []expapi.APIVersion{ + expapi.APIVersion{ + APIGroup: "group", + Name: "v1", + }, + }, + } + master.handlerContainer = restful.NewContainer() + if err := master.InstallThirdPartyAPI(api); err != nil { + t.Errorf("unexpected error: %v", err) + } + + server := httptest.NewServer(master.handlerContainer.ServeMux) + defer server.Close() + + resp, err := http.Get(server.URL + "/thirdparty/foo/v1") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("unexpected status: %v", resp) + } + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if string(data) != "foo" { + t.Errorf("unexpected response: %s", string(data)) + } +} diff --git a/pkg/registry/thirdpartyresourcedata/doc.go b/pkg/registry/thirdpartyresourcedata/doc.go new file mode 100644 index 00000000000..62e2dc1e3eb --- /dev/null +++ b/pkg/registry/thirdpartyresourcedata/doc.go @@ -0,0 +1,19 @@ +/* +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 thirdpartyresourcedata provides Registry interface and its REST +// implementation for storing ThirdPartyResourceData api objects. +package thirdpartyresourcedata diff --git a/pkg/registry/thirdpartyresourcedata/etcd/etcd.go b/pkg/registry/thirdpartyresourcedata/etcd/etcd.go new file mode 100644 index 00000000000..e273d9b93f4 --- /dev/null +++ b/pkg/registry/thirdpartyresourcedata/etcd/etcd.go @@ -0,0 +1,63 @@ +/* +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 etcd + +import ( + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/expapi" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/registry/generic" + etcdgeneric "k8s.io/kubernetes/pkg/registry/generic/etcd" + "k8s.io/kubernetes/pkg/registry/thirdpartyresourcedata" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/storage" +) + +// REST implements a RESTStorage for ThirdPartyResourceDatas against etcd +type REST struct { + *etcdgeneric.Etcd +} + +// NewREST returns a registry which will store ThirdPartyResourceData in the given helper +func NewREST(s storage.Interface) *REST { + prefix := "/ThirdPartyResourceData" + + store := &etcdgeneric.Etcd{ + NewFunc: func() runtime.Object { return &expapi.ThirdPartyResourceData{} }, + NewListFunc: func() runtime.Object { return &expapi.ThirdPartyResourceDataList{} }, + KeyRootFunc: func(ctx api.Context) string { + return etcdgeneric.NamespaceKeyRootFunc(ctx, prefix) + }, + KeyFunc: func(ctx api.Context, id string) (string, error) { + return etcdgeneric.NamespaceKeyFunc(ctx, prefix, id) + }, + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*expapi.ThirdPartyResourceData).Name, nil + }, + PredicateFunc: func(label labels.Selector, field fields.Selector) generic.Matcher { + return thirdpartyresourcedata.Matcher(label, field) + }, + EndpointName: "thirdpartyresourcedata", + CreateStrategy: thirdpartyresourcedata.Strategy, + UpdateStrategy: thirdpartyresourcedata.Strategy, + + Storage: s, + } + + return &REST{store} +} diff --git a/pkg/registry/thirdpartyresourcedata/etcd/etcd_test.go b/pkg/registry/thirdpartyresourcedata/etcd/etcd_test.go new file mode 100644 index 00000000000..97581297ac2 --- /dev/null +++ b/pkg/registry/thirdpartyresourcedata/etcd/etcd_test.go @@ -0,0 +1,205 @@ +/* +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 etcd + +import ( + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/rest/resttest" + "k8s.io/kubernetes/pkg/api/testapi" + "k8s.io/kubernetes/pkg/expapi" + "k8s.io/kubernetes/pkg/expapi/v1" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/storage" + etcdstorage "k8s.io/kubernetes/pkg/storage/etcd" + "k8s.io/kubernetes/pkg/tools" + "k8s.io/kubernetes/pkg/tools/etcdtest" + + "github.com/coreos/go-etcd/etcd" +) + +var scheme *runtime.Scheme +var codec runtime.Codec + +func init() { + // Ensure that expapi/v1 packege is used, so that it will get initialized and register HorizontalPodAutoscaler object. + _ = v1.ThirdPartyResource{} +} + +func newStorage(t *testing.T) (*REST, *tools.FakeEtcdClient, storage.Interface) { + fakeEtcdClient := tools.NewFakeEtcdClient(t) + fakeEtcdClient.TestIndex = true + etcdStorage := etcdstorage.NewEtcdStorage(fakeEtcdClient, testapi.Codec(), etcdtest.PathPrefix()) + storage := NewREST(etcdStorage) + return storage, fakeEtcdClient, etcdStorage +} + +func validNewThirdPartyResource(name string) *expapi.ThirdPartyResource { + return &expapi.ThirdPartyResource{ + ObjectMeta: api.ObjectMeta{ + Name: name, + Namespace: api.NamespaceDefault, + }, + Versions: []expapi.APIVersion{ + { + Name: "stable/v1", + }, + }, + } +} + +func TestCreate(t *testing.T) { + storage, fakeEtcdClient, _ := newStorage(t) + test := resttest.New(t, storage, fakeEtcdClient.SetError) + rsrc := validNewThirdPartyResource("foo") + rsrc.ObjectMeta = api.ObjectMeta{} + test.TestCreate( + // valid + rsrc, + // invalid + &expapi.ThirdPartyResource{}, + ) +} + +func TestUpdate(t *testing.T) { + storage, fakeEtcdClient, _ := newStorage(t) + test := resttest.New(t, storage, fakeEtcdClient.SetError) + key, err := storage.KeyFunc(test.TestContext(), "foo") + if err != nil { + t.Fatal(err) + } + key = etcdtest.AddPrefix(key) + fakeEtcdClient.ExpectNotFoundGet(key) + fakeEtcdClient.ChangeIndex = 2 + rsrc := validNewThirdPartyResource("foo") + existing := validNewThirdPartyResource("exists") + existing.Namespace = test.TestNamespace() + obj, err := storage.Create(test.TestContext(), existing) + if err != nil { + t.Fatalf("unable to create object: %v", err) + } + older := obj.(*expapi.ThirdPartyResource) + older.ResourceVersion = "1" + test.TestUpdate( + rsrc, + existing, + older, + ) +} + +func TestDelete(t *testing.T) { + ctx := api.NewDefaultContext() + storage, fakeEtcdClient, _ := newStorage(t) + test := resttest.New(t, storage, fakeEtcdClient.SetError) + rsrc := validNewThirdPartyResource("foo2") + key, _ := storage.KeyFunc(ctx, "foo2") + key = etcdtest.AddPrefix(key) + createFn := func() runtime.Object { + fakeEtcdClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: runtime.EncodeOrDie(testapi.Codec(), rsrc), + ModifiedIndex: 1, + }, + }, + } + return rsrc + } + gracefulSetFn := func() bool { + if fakeEtcdClient.Data[key].R.Node == nil { + return false + } + return fakeEtcdClient.Data[key].R.Node.TTL == 30 + } + test.TestDeleteNoGraceful(createFn, gracefulSetFn) +} + +func TestGet(t *testing.T) { + storage, fakeEtcdClient, _ := newStorage(t) + test := resttest.New(t, storage, fakeEtcdClient.SetError) + rsrc := validNewThirdPartyResource("foo") + test.TestGet(rsrc) +} + +func TestEmptyList(t *testing.T) { + ctx := api.NewDefaultContext() + registry, fakeClient, _ := newStorage(t) + fakeClient.ChangeIndex = 1 + key := registry.KeyRootFunc(ctx) + key = etcdtest.AddPrefix(key) + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{}, + E: fakeClient.NewError(tools.EtcdErrorCodeNotFound), + } + rsrcList, err := registry.List(ctx, labels.Everything(), fields.Everything()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(rsrcList.(*expapi.ThirdPartyResourceList).Items) != 0 { + t.Errorf("Unexpected non-zero autoscaler list: %#v", rsrcList) + } + if rsrcList.(*expapi.ThirdPartyResourceList).ResourceVersion != "1" { + t.Errorf("Unexpected resource version: %#v", rsrcList) + } +} + +func TestList(t *testing.T) { + ctx := api.NewDefaultContext() + registry, fakeClient, _ := newStorage(t) + fakeClient.ChangeIndex = 1 + key := registry.KeyRootFunc(ctx) + key = etcdtest.AddPrefix(key) + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Nodes: []*etcd.Node{ + { + Value: runtime.EncodeOrDie(testapi.Codec(), &expapi.ThirdPartyResource{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + }), + }, + { + Value: runtime.EncodeOrDie(testapi.Codec(), &expapi.ThirdPartyResource{ + ObjectMeta: api.ObjectMeta{Name: "bar"}, + }), + }, + }, + }, + }, + } + obj, err := registry.List(ctx, labels.Everything(), fields.Everything()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + rsrcList := obj.(*expapi.ThirdPartyResourceList) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(rsrcList.Items) != 2 { + t.Errorf("Unexpected ThirdPartyResource list: %#v", rsrcList) + } + if rsrcList.Items[0].Name != "foo" { + t.Errorf("Unexpected ThirdPartyResource: %#v", rsrcList.Items[0]) + } + if rsrcList.Items[1].Name != "bar" { + t.Errorf("Unexpected ThirdPartyResource: %#v", rsrcList.Items[1]) + } +} diff --git a/pkg/registry/thirdpartyresourcedata/registry.go b/pkg/registry/thirdpartyresourcedata/registry.go new file mode 100644 index 00000000000..b1be42354bb --- /dev/null +++ b/pkg/registry/thirdpartyresourcedata/registry.go @@ -0,0 +1,88 @@ +/* +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 thirdpartyresourcedata + +import ( + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/rest" + "k8s.io/kubernetes/pkg/expapi" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/watch" +) + +// Registry is an interface implemented by things that know how to store ThirdPartyResourceData objects. +type Registry interface { + // ListThirdPartyResourceData obtains a list of ThirdPartyResourceData having labels which match selector. + ListThirdPartyResourceData(ctx api.Context, selector labels.Selector) (*expapi.ThirdPartyResourceDataList, error) + // Watch for new/changed/deleted ThirdPartyResourceData + WatchThirdPartyResourceData(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) + // Get a specific ThirdPartyResourceData + GetThirdPartyResourceData(ctx api.Context, name string) (*expapi.ThirdPartyResourceData, error) + // Create a ThirdPartyResourceData based on a specification. + CreateThirdPartyResourceData(ctx api.Context, resource *expapi.ThirdPartyResourceData) (*expapi.ThirdPartyResourceData, error) + // Update an existing ThirdPartyResourceData + UpdateThirdPartyResourceData(ctx api.Context, resource *expapi.ThirdPartyResourceData) (*expapi.ThirdPartyResourceData, error) + // Delete an existing ThirdPartyResourceData + DeleteThirdPartyResourceData(ctx api.Context, name string) error +} + +// storage puts strong typing around storage calls +type storage struct { + rest.StandardStorage +} + +// NewRegistry returns a new Registry interface for the given Storage. Any mismatched +// types will panic. +func NewRegistry(s rest.StandardStorage) Registry { + return &storage{s} +} + +func (s *storage) ListThirdPartyResourceData(ctx api.Context, label labels.Selector) (*expapi.ThirdPartyResourceDataList, error) { + obj, err := s.List(ctx, label, fields.Everything()) + if err != nil { + return nil, err + } + return obj.(*expapi.ThirdPartyResourceDataList), nil +} + +func (s *storage) WatchThirdPartyResourceData(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + return s.Watch(ctx, label, field, resourceVersion) +} + +func (s *storage) GetThirdPartyResourceData(ctx api.Context, name string) (*expapi.ThirdPartyResourceData, error) { + obj, err := s.Get(ctx, name) + if err != nil { + return nil, err + } + return obj.(*expapi.ThirdPartyResourceData), nil +} + +func (s *storage) CreateThirdPartyResourceData(ctx api.Context, ThirdPartyResourceData *expapi.ThirdPartyResourceData) (*expapi.ThirdPartyResourceData, error) { + obj, err := s.Create(ctx, ThirdPartyResourceData) + return obj.(*expapi.ThirdPartyResourceData), err +} + +func (s *storage) UpdateThirdPartyResourceData(ctx api.Context, ThirdPartyResourceData *expapi.ThirdPartyResourceData) (*expapi.ThirdPartyResourceData, error) { + obj, _, err := s.Update(ctx, ThirdPartyResourceData) + return obj.(*expapi.ThirdPartyResourceData), err +} + +func (s *storage) DeleteThirdPartyResourceData(ctx api.Context, name string) error { + _, err := s.Delete(ctx, name, nil) + return err +} diff --git a/pkg/registry/thirdpartyresourcedata/strategy.go b/pkg/registry/thirdpartyresourcedata/strategy.go new file mode 100644 index 00000000000..fd72660a087 --- /dev/null +++ b/pkg/registry/thirdpartyresourcedata/strategy.go @@ -0,0 +1,88 @@ +/* +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 thirdpartyresourcedata + +import ( + "fmt" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/rest" + "k8s.io/kubernetes/pkg/expapi" + "k8s.io/kubernetes/pkg/expapi/validation" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/registry/generic" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/fielderrors" +) + +// strategy implements behavior for ThirdPartyResource objects +type strategy struct { + runtime.ObjectTyper + api.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating ThirdPartyResource +// objects via the REST API. +var Strategy = strategy{api.Scheme, api.SimpleNameGenerator} + +var _ = rest.RESTCreateStrategy(Strategy) + +var _ = rest.RESTUpdateStrategy(Strategy) + +func (strategy) NamespaceScoped() bool { + return true +} + +func (strategy) PrepareForCreate(obj runtime.Object) { +} + +func (strategy) Validate(ctx api.Context, obj runtime.Object) fielderrors.ValidationErrorList { + return validation.ValidateThirdPartyResource(obj.(*expapi.ThirdPartyResource)) +} + +func (strategy) AllowCreateOnUpdate() bool { + return false +} + +func (strategy) PrepareForUpdate(obj, old runtime.Object) { +} + +func (strategy) ValidateUpdate(ctx api.Context, obj, old runtime.Object) fielderrors.ValidationErrorList { + return validation.ValidateThirdPartyResourceUpdate(old.(*expapi.ThirdPartyResource), obj.(*expapi.ThirdPartyResource)) +} + +func (strategy) AllowUnconditionalUpdate() bool { + return true +} + +// Matcher returns a generic matcher for a given label and field selector. +func Matcher(label labels.Selector, field fields.Selector) generic.Matcher { + return generic.MatcherFunc(func(obj runtime.Object) (bool, error) { + sa, ok := obj.(*expapi.ThirdPartyResource) + if !ok { + return false, fmt.Errorf("not a ThirdPartyResource") + } + fields := SelectableFields(sa) + return label.Matches(labels.Set(sa.Labels)) && field.Matches(fields), nil + }) +} + +// SelectableFields returns a label set that can be used for filter selection +func SelectableFields(obj *expapi.ThirdPartyResource) labels.Set { + return labels.Set{} +} From b196d0f84b5b370e94ece35358a13a9bb29eb3da Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Wed, 19 Aug 2015 22:08:26 -0700 Subject: [PATCH 3/6] Add support for installing custom object APIs --- contrib/completions/bash/kubectl | 2 - pkg/api/deep_copy_generated.go | 19 -- pkg/api/v1/conversion_generated.go | 34 --- pkg/api/v1/deep_copy_generated.go | 19 -- pkg/apiserver/api_installer.go | 3 + pkg/apiserver/resthandler.go | 6 +- pkg/expapi/validation/validation.go | 12 + pkg/master/master.go | 25 +- pkg/master/master_test.go | 267 +++++++++++++++++- .../thirdpartyresource/etcd/etcd_test.go | 1 + pkg/registry/thirdpartyresourcedata/codec.go | 175 ++++++++++++ .../thirdpartyresourcedata/etcd/etcd.go | 4 +- .../thirdpartyresourcedata/etcd/etcd_test.go | 69 ++--- .../thirdpartyresourcedata/strategy.go | 10 +- pkg/registry/thirdpartyresourcedata/util.go | 49 ++++ .../thirdpartyresourcedata/util_test.go | 66 +++++ 16 files changed, 603 insertions(+), 158 deletions(-) create mode 100644 pkg/registry/thirdpartyresourcedata/codec.go create mode 100644 pkg/registry/thirdpartyresourcedata/util.go create mode 100644 pkg/registry/thirdpartyresourcedata/util_test.go diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index 08f18d5eb03..21fcb1ed349 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -287,7 +287,6 @@ _kubectl_get() must_have_one_noun+=("secret") must_have_one_noun+=("service") must_have_one_noun+=("serviceaccount") - must_have_one_noun+=("thirdpartyresource") } _kubectl_describe() @@ -461,7 +460,6 @@ _kubectl_delete() must_have_one_noun+=("secret") must_have_one_noun+=("service") must_have_one_noun+=("serviceaccount") - must_have_one_noun+=("thirdpartyresource") } _kubectl_namespace() diff --git a/pkg/api/deep_copy_generated.go b/pkg/api/deep_copy_generated.go index 5bb46937ee7..3263b3abf0c 100644 --- a/pkg/api/deep_copy_generated.go +++ b/pkg/api/deep_copy_generated.go @@ -1982,24 +1982,6 @@ func deepCopy_api_TCPSocketAction(in TCPSocketAction, out *TCPSocketAction, c *c return nil } -func deepCopy_api_ThirdPartyResourceData(in ThirdPartyResourceData, out *ThirdPartyResourceData, c *conversion.Cloner) error { - if err := deepCopy_api_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { - return err - } - if err := deepCopy_api_ObjectMeta(in.ObjectMeta, &out.ObjectMeta, c); err != nil { - return err - } - if in.Data != nil { - out.Data = make([]uint8, len(in.Data)) - for i := range in.Data { - out.Data[i] = in.Data[i] - } - } else { - out.Data = nil - } - return nil -} - func deepCopy_api_TypeMeta(in TypeMeta, out *TypeMeta, c *conversion.Cloner) error { out.Kind = in.Kind out.APIVersion = in.APIVersion @@ -2258,7 +2240,6 @@ func init() { deepCopy_api_StatusCause, deepCopy_api_StatusDetails, deepCopy_api_TCPSocketAction, - deepCopy_api_ThirdPartyResourceData, deepCopy_api_TypeMeta, deepCopy_api_Volume, deepCopy_api_VolumeMount, diff --git a/pkg/api/v1/conversion_generated.go b/pkg/api/v1/conversion_generated.go index 6a75a1a278c..7a1e4f82df1 100644 --- a/pkg/api/v1/conversion_generated.go +++ b/pkg/api/v1/conversion_generated.go @@ -2199,22 +2199,6 @@ func convert_api_TCPSocketAction_To_v1_TCPSocketAction(in *api.TCPSocketAction, return nil } -func convert_api_ThirdPartyResourceData_To_v1_ThirdPartyResourceData(in *api.ThirdPartyResourceData, out *ThirdPartyResourceData, s conversion.Scope) error { - if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { - defaulting.(func(*api.ThirdPartyResourceData))(in) - } - if err := convert_api_TypeMeta_To_v1_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { - return err - } - if err := convert_api_ObjectMeta_To_v1_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil { - return err - } - if err := s.Convert(&in.Data, &out.Data, 0); err != nil { - return err - } - return nil -} - func convert_api_TypeMeta_To_v1_TypeMeta(in *api.TypeMeta, out *TypeMeta, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*api.TypeMeta))(in) @@ -4513,22 +4497,6 @@ func convert_v1_TCPSocketAction_To_api_TCPSocketAction(in *TCPSocketAction, out return nil } -func convert_v1_ThirdPartyResourceData_To_api_ThirdPartyResourceData(in *ThirdPartyResourceData, out *api.ThirdPartyResourceData, s conversion.Scope) error { - if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { - defaulting.(func(*ThirdPartyResourceData))(in) - } - if err := convert_v1_TypeMeta_To_api_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { - return err - } - if err := convert_v1_ObjectMeta_To_api_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil { - return err - } - if err := s.Convert(&in.Data, &out.Data, 0); err != nil { - return err - } - return nil -} - func convert_v1_TypeMeta_To_api_TypeMeta(in *TypeMeta, out *api.TypeMeta, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*TypeMeta))(in) @@ -4765,7 +4733,6 @@ func init() { convert_api_StatusDetails_To_v1_StatusDetails, convert_api_Status_To_v1_Status, convert_api_TCPSocketAction_To_v1_TCPSocketAction, - convert_api_ThirdPartyResourceData_To_v1_ThirdPartyResourceData, convert_api_TypeMeta_To_v1_TypeMeta, convert_api_VolumeMount_To_v1_VolumeMount, convert_api_VolumeSource_To_v1_VolumeSource, @@ -4879,7 +4846,6 @@ func init() { convert_v1_StatusDetails_To_api_StatusDetails, convert_v1_Status_To_api_Status, convert_v1_TCPSocketAction_To_api_TCPSocketAction, - convert_v1_ThirdPartyResourceData_To_api_ThirdPartyResourceData, convert_v1_TypeMeta_To_api_TypeMeta, convert_v1_VolumeMount_To_api_VolumeMount, convert_v1_VolumeSource_To_api_VolumeSource, diff --git a/pkg/api/v1/deep_copy_generated.go b/pkg/api/v1/deep_copy_generated.go index e806c6654a8..7750cda7634 100644 --- a/pkg/api/v1/deep_copy_generated.go +++ b/pkg/api/v1/deep_copy_generated.go @@ -1987,24 +1987,6 @@ func deepCopy_v1_TCPSocketAction(in TCPSocketAction, out *TCPSocketAction, c *co return nil } -func deepCopy_v1_ThirdPartyResourceData(in ThirdPartyResourceData, out *ThirdPartyResourceData, c *conversion.Cloner) error { - if err := deepCopy_v1_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { - return err - } - if err := deepCopy_v1_ObjectMeta(in.ObjectMeta, &out.ObjectMeta, c); err != nil { - return err - } - if in.Data != nil { - out.Data = make([]uint8, len(in.Data)) - for i := range in.Data { - out.Data[i] = in.Data[i] - } - } else { - out.Data = nil - } - return nil -} - func deepCopy_v1_TypeMeta(in TypeMeta, out *TypeMeta, c *conversion.Cloner) error { out.Kind = in.Kind out.APIVersion = in.APIVersion @@ -2260,7 +2242,6 @@ func init() { deepCopy_v1_StatusCause, deepCopy_v1_StatusDetails, deepCopy_v1_TCPSocketAction, - deepCopy_v1_ThirdPartyResourceData, deepCopy_v1_TypeMeta, deepCopy_v1_Volume, deepCopy_v1_VolumeMount, diff --git a/pkg/apiserver/api_installer.go b/pkg/apiserver/api_installer.go index 73a5c4ca068..c278cd44d43 100644 --- a/pkg/apiserver/api_installer.go +++ b/pkg/apiserver/api_installer.go @@ -248,6 +248,9 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag var ctxFn ContextFunc ctxFn = func(req *restful.Request) api.Context { + if context == nil { + return api.NewContext() + } if ctx, ok := context.Get(req.Request); ok { return ctx } diff --git a/pkg/apiserver/resthandler.go b/pkg/apiserver/resthandler.go index 87f6c6daa23..eff8ab9f354 100644 --- a/pkg/apiserver/resthandler.go +++ b/pkg/apiserver/resthandler.go @@ -309,7 +309,7 @@ func createHandler(r rest.NamedCreater, scope RequestScope, typer runtime.Object return } - if admit.Handles(admission.Create) { + if admit != nil && admit.Handles(admission.Create) { userInfo, _ := api.UserFrom(ctx) err = admit.Admit(admission.NewAttributesRecord(obj, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Create, userInfo)) @@ -481,7 +481,7 @@ func UpdateResource(r rest.Updater, scope RequestScope, typer runtime.ObjectType return } - if admit.Handles(admission.Update) { + if admit != nil && admit.Handles(admission.Update) { userInfo, _ := api.UserFrom(ctx) err = admit.Admit(admission.NewAttributesRecord(obj, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Update, userInfo)) @@ -546,7 +546,7 @@ func DeleteResource(r rest.GracefulDeleter, checkBody bool, scope RequestScope, } } - if admit.Handles(admission.Delete) { + if admit != nil && admit.Handles(admission.Delete) { userInfo, _ := api.UserFrom(ctx) err = admit.Admit(admission.NewAttributesRecord(nil, scope.Kind, namespace, name, scope.Resource, scope.Subresource, admission.Delete, userInfo)) diff --git a/pkg/expapi/validation/validation.go b/pkg/expapi/validation/validation.go index 2bc02d80e5a..284450d7e7d 100644 --- a/pkg/expapi/validation/validation.go +++ b/pkg/expapi/validation/validation.go @@ -257,3 +257,15 @@ func ValidateDeployment(obj *expapi.Deployment) errs.ValidationErrorList { allErrs = append(allErrs, ValidateDeploymentSpec(&obj.Spec).Prefix("spec")...) return allErrs } + +func ValidateThirdPartyResourceDataUpdate(old, update *expapi.ThirdPartyResourceData) errs.ValidationErrorList { + return ValidateThirdPartyResourceData(update) +} + +func ValidateThirdPartyResourceData(obj *expapi.ThirdPartyResourceData) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + if len(obj.Name) == 0 { + allErrs = append(allErrs, errs.NewFieldInvalid("name", obj.Name, "name must be non-empty")) + } + return allErrs +} diff --git a/pkg/master/master.go b/pkg/master/master.go index 0ddf096c791..30d6d4d55e5 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -72,6 +72,7 @@ import ( ipallocator "k8s.io/kubernetes/pkg/registry/service/ipallocator" serviceaccountetcd "k8s.io/kubernetes/pkg/registry/serviceaccount/etcd" thirdpartyresourceetcd "k8s.io/kubernetes/pkg/registry/thirdpartyresource/etcd" + "k8s.io/kubernetes/pkg/registry/thirdpartyresourcedata" thirdpartyresourcedataetcd "k8s.io/kubernetes/pkg/registry/thirdpartyresourcedata/etcd" "k8s.io/kubernetes/pkg/storage" etcdstorage "k8s.io/kubernetes/pkg/storage/etcd" @@ -771,26 +772,28 @@ func (m *Master) api_v1() *apiserver.APIGroupVersion { } func (m *Master) InstallThirdPartyAPI(rsrc *expapi.ThirdPartyResource) error { - thirdparty := m.thirdpartyapi(rsrc) + kind, group, err := thirdpartyresourcedata.ExtractApiGroupAndKind(rsrc) + if err != nil { + return err + } + thirdparty := m.thirdpartyapi(group, strings.ToLower(kind)+"s", rsrc.Versions[0].Name) if err := thirdparty.InstallREST(m.handlerContainer); err != nil { glog.Fatalf("Unable to setup thirdparty api: %v", err) } - thirdPartyPrefix := "/thirdparty/" + rsrc.Name + "/" - apiRoot := rsrc.Name + thirdPartyPrefix := "/thirdparty/" + group + "/" apiserver.AddApiWebService(m.handlerContainer, thirdPartyPrefix, []string{rsrc.Versions[0].Name}) - thirdPartyRequestInfoResolver := &apiserver.APIRequestInfoResolver{APIPrefixes: util.NewStringSet(strings.TrimPrefix(apiRoot, "/")), RestMapper: thirdparty.Mapper} + thirdPartyRequestInfoResolver := &apiserver.APIRequestInfoResolver{APIPrefixes: util.NewStringSet(strings.TrimPrefix(group, "/")), RestMapper: thirdparty.Mapper} apiserver.InstallServiceErrorHandler(m.handlerContainer, thirdPartyRequestInfoResolver, []string{thirdparty.Version}) return nil } -func (m *Master) thirdpartyapi(rsrc *expapi.ThirdPartyResource) *apiserver.APIGroupVersion { - resourceStorage := thirdpartyresourcedataetcd.NewREST(m.thirdPartyStorage) +func (m *Master) thirdpartyapi(group, kind, version string) *apiserver.APIGroupVersion { + resourceStorage := thirdpartyresourcedataetcd.NewREST(m.thirdPartyStorage, group, kind) - apiGroup := rsrc.Name - apiRoot := "/thirdparty/" + rsrc.Name + "/" + rsrc.Versions[0].Name + apiRoot := "/thirdparty/" + group + "/" storage := map[string]rest.Storage{ - apiGroup: resourceStorage, + kind: resourceStorage, } return &apiserver.APIGroupVersion{ @@ -800,11 +803,11 @@ func (m *Master) thirdpartyapi(rsrc *expapi.ThirdPartyResource) *apiserver.APIGr Convertor: api.Scheme, Typer: api.Scheme, - Mapper: explatest.RESTMapper, + Mapper: thirdpartyresourcedata.NewMapper(explatest.RESTMapper, kind), Codec: explatest.Codec, Linker: explatest.SelfLinker, Storage: storage, - Version: rsrc.Versions[0].Name, + Version: version, Admit: m.admissionControl, Context: m.requestContextMapper, diff --git a/pkg/master/master_test.go b/pkg/master/master_test.go index 23b9a21fb52..88137175dec 100644 --- a/pkg/master/master_test.go +++ b/pkg/master/master_test.go @@ -17,9 +17,12 @@ limitations under the License. package master import ( + "bytes" + "encoding/json" "io/ioutil" "net/http" "net/http/httptest" + "reflect" "testing" "k8s.io/kubernetes/pkg/api" @@ -30,7 +33,7 @@ import ( etcdstorage "k8s.io/kubernetes/pkg/storage/etcd" "k8s.io/kubernetes/pkg/tools" "k8s.io/kubernetes/pkg/tools/etcdtest" - + "github.com/emicklei/go-restful" ) @@ -80,43 +83,281 @@ func TestFindExternalAddress(t *testing.T) { } } -func TestInstallThirdPartyAPI(t *testing.T) { +type Foo struct { + api.TypeMeta `json:",inline"` + api.ObjectMeta `json:"metadata,omitempty" description:"standard object metadata"` + + SomeField string `json:"someField"` + OtherField int `json:"otherField"` +} + +type FooList struct { + api.TypeMeta `json:",inline"` + api.ListMeta `json:"metadata,omitempty" description:"standard list metadata; see http://docs.k8s.io/api-conventions.md#metadata"` + + items []Foo `json:"items"` +} + +func initThirdParty(t *testing.T) (*tools.FakeEtcdClient, *httptest.Server) { master := &Master{} api := &expapi.ThirdPartyResource{ ObjectMeta: api.ObjectMeta{ - Name: "foo", + Name: "foo.company.com", }, Versions: []expapi.APIVersion{ - expapi.APIVersion{ + { APIGroup: "group", - Name: "v1", + Name: "v1", }, }, } master.handlerContainer = restful.NewContainer() + + fakeClient := tools.NewFakeEtcdClient(t) + fakeClient.Machines = []string{"http://machine1:4001", "http://machine2", "http://machine3:4003"} + master.thirdPartyStorage = etcdstorage.NewEtcdStorage(fakeClient, explatest.Codec, etcdtest.PathPrefix()) + if err := master.InstallThirdPartyAPI(api); err != nil { t.Errorf("unexpected error: %v", err) + t.FailNow() } - + server := httptest.NewServer(master.handlerContainer.ServeMux) + return fakeClient, server +} + +func TestInstallThirdPartyAPIList(t *testing.T) { + fakeClient, server := initThirdParty(t) defer server.Close() - - resp, err := http.Get(server.URL + "/thirdparty/foo/v1") + + fakeClient.ExpectNotFoundGet(etcdtest.PathPrefix() + "/ThirdPartyResourceData/company.com/foos/default") + + resp, err := http.Get(server.URL + "/thirdparty/company.com/v1/namespaces/default/foos") if err != nil { t.Errorf("unexpected error: %v", err) + return } defer resp.Body.Close() - + if resp.StatusCode != http.StatusOK { t.Errorf("unexpected status: %v", resp) } - + data, err := ioutil.ReadAll(resp.Body) if err != nil { t.Errorf("unexpected error: %v", err) } - - if string(data) != "foo" { - t.Errorf("unexpected response: %s", string(data)) + + list := FooList{} + if err := json.Unmarshal(data, &list); err != nil { + t.Errorf("unexpected error: %v", err) } } + +func encodeToThirdParty(name string, obj interface{}) ([]byte, error) { + serial, err := json.Marshal(obj) + if err != nil { + return nil, err + } + thirdPartyData := expapi.ThirdPartyResourceData{ + ObjectMeta: api.ObjectMeta{Name: name}, + Data: serial, + } + return latest.Codec.Encode(&thirdPartyData) +} + +func storeToEtcd(fakeClient *tools.FakeEtcdClient, path, name string, obj interface{}) error { + data, err := encodeToThirdParty(name, obj) + if err != nil { + return err + } + _, err = fakeClient.Set(etcdtest.PathPrefix()+path, string(data), 0) + return err +} + +func decodeResponse(resp *http.Response, obj interface{}) error { + defer resp.Body.Close() + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + if err := json.Unmarshal(data, obj); err != nil { + return err + } + return nil +} + +func TestInstallThirdPartyAPIGet(t *testing.T) { + fakeClient, server := initThirdParty(t) + defer server.Close() + + expectedObj := Foo{ + ObjectMeta: api.ObjectMeta{ + Name: "test", + }, + TypeMeta: api.TypeMeta{ + Kind: "foo", + }, + SomeField: "test field", + OtherField: 10, + } + if err := storeToEtcd(fakeClient, "/ThirdPartyResourceData/company.com/foos/default/test", "test", expectedObj); err != nil { + t.Errorf("unexpected error: %v", err) + t.FailNow() + return + } + + resp, err := http.Get(server.URL + "/thirdparty/company.com/v1/namespaces/default/foos/test") + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + if resp.StatusCode != http.StatusOK { + t.Errorf("unexpected status: %v", resp) + } + item := Foo{} + if err := decodeResponse(resp, &item); err != nil { + t.Errorf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(item, expectedObj) { + t.Errorf("expected:\n%v\nsaw:\n%v\n", expectedObj, item) + } +} + +func TestInstallThirdPartyAPIPost(t *testing.T) { + fakeClient, server := initThirdParty(t) + defer server.Close() + + inputObj := Foo{ + ObjectMeta: api.ObjectMeta{ + Name: "test", + }, + TypeMeta: api.TypeMeta{ + Kind: "foo", + }, + SomeField: "test field", + OtherField: 10, + } + data, err := encodeToThirdParty("test", inputObj) + if err != nil { + t.Errorf("unexpected error: %v") + return + } + + resp, err := http.Post(server.URL+"/thirdparty/company.com/v1/namespaces/default/foos", "application/json", bytes.NewBuffer(data)) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + if resp.StatusCode != http.StatusCreated { + t.Errorf("unexpected status: %v", resp) + } + + item := Foo{} + if err := decodeResponse(resp, &item); err != nil { + t.Errorf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(item, inputObj) { + t.Errorf("expected:\n%v\nsaw:\n%v\n", inputObj, item) + } + + etcdResp, err := fakeClient.Get(etcdtest.PathPrefix()+"/ThirdPartyResourceData/company.com/foos/default/test", false, false) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + obj, err := explatest.Codec.Decode([]byte(etcdResp.Node.Value)) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + thirdPartyObj, ok := obj.(*expapi.ThirdPartyResourceData) + if !ok { + t.Errorf("unexpected object: %v", obj) + } + item = Foo{} + if err := json.Unmarshal(thirdPartyObj.Data, &item); err != nil { + t.Errorf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(item, inputObj) { + t.Errorf("expected:\n%v\nsaw:\n%v\n", inputObj, item) + } +} + +func TestInstallThirdPartyAPIDelete(t *testing.T) { + fakeClient, server := initThirdParty(t) + defer server.Close() + + expectedObj := Foo{ + ObjectMeta: api.ObjectMeta{ + Name: "test", + }, + TypeMeta: api.TypeMeta{ + Kind: "foo", + }, + SomeField: "test field", + OtherField: 10, + } + if err := storeToEtcd(fakeClient, "/ThirdPartyResourceData/company.com/foos/default/test", "test", expectedObj); err != nil { + t.Errorf("unexpected error: %v", err) + t.FailNow() + return + } + + resp, err := http.Get(server.URL + "/thirdparty/company.com/v1/namespaces/default/foos/test") + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + if resp.StatusCode != http.StatusOK { + t.Errorf("unexpected status: %v", resp) + } + + item := Foo{} + if err := decodeResponse(resp, &item); err != nil { + t.Errorf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(item, expectedObj) { + t.Errorf("expected:\n%v\nsaw:\n%v\n", expectedObj, item) + } + + resp, err = httpDelete(server.URL + "/thirdparty/company.com/v1/namespaces/default/foos/test") + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + if resp.StatusCode != http.StatusOK { + t.Errorf("unexpected status: %v", resp) + } + + resp, err = http.Get(server.URL + "/thirdparty/company.com/v1/namespaces/default/foos/test") + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + if resp.StatusCode != http.StatusNotFound { + t.Errorf("unexpected status: %v", resp) + } + expectDeletedKeys := []string{etcdtest.PathPrefix() + "/ThirdPartyResourceData/company.com/foos/default/test"} + if !reflect.DeepEqual(fakeClient.DeletedKeys, expectDeletedKeys) { + t.Errorf("unexpected deleted keys: %v", fakeClient.DeletedKeys) + } +} + +func httpDelete(url string) (*http.Response, error) { + req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + return nil, err + } + client := &http.Client{} + return client.Do(req) +} diff --git a/pkg/registry/thirdpartyresource/etcd/etcd_test.go b/pkg/registry/thirdpartyresource/etcd/etcd_test.go index b60bc020fe0..3406b62bcd6 100644 --- a/pkg/registry/thirdpartyresource/etcd/etcd_test.go +++ b/pkg/registry/thirdpartyresource/etcd/etcd_test.go @@ -101,6 +101,7 @@ func TestUpdate(t *testing.T) { ) } +<<<<<<< HEAD func TestDelete(t *testing.T) { ctx := api.NewDefaultContext() storage, fakeClient := newStorage(t) diff --git a/pkg/registry/thirdpartyresourcedata/codec.go b/pkg/registry/thirdpartyresourcedata/codec.go new file mode 100644 index 00000000000..120a6caa306 --- /dev/null +++ b/pkg/registry/thirdpartyresourcedata/codec.go @@ -0,0 +1,175 @@ +/* +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 thirdpartyresourcedata + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/meta" + "k8s.io/kubernetes/pkg/expapi" + "k8s.io/kubernetes/pkg/runtime" +) + +type thirdPartyResourceDataMapper struct { + mapper meta.RESTMapper + kind string +} + +func (t *thirdPartyResourceDataMapper) GroupForResource(resource string) (string, error) { + return t.mapper.GroupForResource(resource) +} + +func (t *thirdPartyResourceDataMapper) RESTMapping(kind string, versions ...string) (*meta.RESTMapping, error) { + mapping, err := t.mapper.RESTMapping(kind, versions...) + if err != nil { + return nil, err + } + mapping.Codec = NewCodec(mapping.Codec, t.kind) + return mapping, nil +} + +func (t *thirdPartyResourceDataMapper) AliasesForResource(resource string) ([]string, bool) { + return t.mapper.AliasesForResource(resource) +} + +func (t *thirdPartyResourceDataMapper) ResourceSingularizer(resource string) (singular string, err error) { + return t.mapper.ResourceSingularizer(resource) +} + +func (t *thirdPartyResourceDataMapper) VersionAndKindForResource(resource string) (defaultVersion, kind string, err error) { + return t.mapper.VersionAndKindForResource(resource) +} + +func NewMapper(mapper meta.RESTMapper, kind string) meta.RESTMapper { + return &thirdPartyResourceDataMapper{mapper, kind} +} + +type thirdPartyResourceDataCodec struct { + delegate runtime.Codec + kind string +} + +func NewCodec(codec runtime.Codec, kind string) runtime.Codec { + return &thirdPartyResourceDataCodec{codec, kind} +} + +func (t *thirdPartyResourceDataCodec) populate(objIn *expapi.ThirdPartyResourceData, data []byte) error { + var obj interface{} + if err := json.Unmarshal(data, &obj); err != nil { + fmt.Printf("Invalid JSON:\n%s\n", string(data)) + return err + } + mapObj, ok := obj.(map[string]interface{}) + if !ok { + return fmt.Errorf("unexpected object: %#v", obj) + } + kind, ok := mapObj["kind"].(string) + if !ok { + return fmt.Errorf("unexpected object: %#v", obj) + } + if kind == "ThirdPartyResourceData" { + return t.delegate.DecodeInto(data, objIn) + } + if kind != t.kind { + return fmt.Errorf("unexpected kind: %s, expected: %s", kind, t.kind) + } + + metadata, ok := mapObj["metadata"].(map[string]interface{}) + if !ok { + return fmt.Errorf("unexpected object: %#v", obj) + } + + name, ok := metadata["name"].(string) + if !ok { + return fmt.Errorf("unexpected object: %#v", obj) + } + + objIn.Name = name + objIn.Data = data + return nil +} + +func (t *thirdPartyResourceDataCodec) Decode(data []byte) (runtime.Object, error) { + result := &expapi.ThirdPartyResourceData{} + if err := t.populate(result, data); err != nil { + return nil, err + } + return result, nil +} + +func (t *thirdPartyResourceDataCodec) DecodeToVersion(data []byte, version string) (runtime.Object, error) { + // TODO: this is hacky, there must be a better way... + obj, err := t.Decode(data) + if err != nil { + return nil, err + } + objData, err := t.delegate.Encode(obj) + if err != nil { + return nil, err + } + return t.delegate.DecodeToVersion(objData, version) +} + +func (t *thirdPartyResourceDataCodec) DecodeInto(data []byte, obj runtime.Object) error { + thirdParty, ok := obj.(*expapi.ThirdPartyResourceData) + if !ok { + return fmt.Errorf("unexpected object: %#v", obj) + } + return t.populate(thirdParty, data) +} + +func (t *thirdPartyResourceDataCodec) DecodeIntoWithSpecifiedVersionKind(data []byte, obj runtime.Object, kind, version string) error { + thirdParty, ok := obj.(*expapi.ThirdPartyResourceData) + if !ok { + return fmt.Errorf("unexpected object: %#v", obj) + } + if err := t.populate(thirdParty, data); err != nil { + return err + } + thirdParty.Kind = kind + thirdParty.APIVersion = version + return nil +} + +const template = `{ + "kind": "%s", + "items": [ %s ] +}` + +func (t *thirdPartyResourceDataCodec) Encode(obj runtime.Object) (data []byte, err error) { + switch obj := obj.(type) { + case *expapi.ThirdPartyResourceData: + return obj.Data, nil + case *expapi.ThirdPartyResourceDataList: + // TODO: There must be a better way to do this... + buff := &bytes.Buffer{} + dataStrings := make([]string, len(obj.Items)) + for ix := range obj.Items { + dataStrings[ix] = string(obj.Items[ix].Data) + } + fmt.Fprintf(buff, template, t.kind+"List", strings.Join(dataStrings, ",")) + return buff.Bytes(), nil + case *api.Status: + return t.delegate.Encode(obj) + default: + return nil, fmt.Errorf("unexpected object to encode: %#v", obj) + } +} diff --git a/pkg/registry/thirdpartyresourcedata/etcd/etcd.go b/pkg/registry/thirdpartyresourcedata/etcd/etcd.go index e273d9b93f4..ea9495bb232 100644 --- a/pkg/registry/thirdpartyresourcedata/etcd/etcd.go +++ b/pkg/registry/thirdpartyresourcedata/etcd/etcd.go @@ -34,8 +34,8 @@ type REST struct { } // NewREST returns a registry which will store ThirdPartyResourceData in the given helper -func NewREST(s storage.Interface) *REST { - prefix := "/ThirdPartyResourceData" +func NewREST(s storage.Interface, group, kind string) *REST { + prefix := "/ThirdPartyResourceData/" + group + "/" + kind store := &etcdgeneric.Etcd{ NewFunc: func() runtime.Object { return &expapi.ThirdPartyResourceData{} }, diff --git a/pkg/registry/thirdpartyresourcedata/etcd/etcd_test.go b/pkg/registry/thirdpartyresourcedata/etcd/etcd_test.go index 97581297ac2..af89eefcb28 100644 --- a/pkg/registry/thirdpartyresourcedata/etcd/etcd_test.go +++ b/pkg/registry/thirdpartyresourcedata/etcd/etcd_test.go @@ -40,41 +40,37 @@ var codec runtime.Codec func init() { // Ensure that expapi/v1 packege is used, so that it will get initialized and register HorizontalPodAutoscaler object. - _ = v1.ThirdPartyResource{} + _ = v1.ThirdPartyResourceData{} } func newStorage(t *testing.T) (*REST, *tools.FakeEtcdClient, storage.Interface) { fakeEtcdClient := tools.NewFakeEtcdClient(t) fakeEtcdClient.TestIndex = true etcdStorage := etcdstorage.NewEtcdStorage(fakeEtcdClient, testapi.Codec(), etcdtest.PathPrefix()) - storage := NewREST(etcdStorage) + storage := NewREST(etcdStorage, "foo", "bar") return storage, fakeEtcdClient, etcdStorage } -func validNewThirdPartyResource(name string) *expapi.ThirdPartyResource { - return &expapi.ThirdPartyResource{ +func validNewThirdPartyResourceData(name string) *expapi.ThirdPartyResourceData { + return &expapi.ThirdPartyResourceData{ ObjectMeta: api.ObjectMeta{ Name: name, Namespace: api.NamespaceDefault, }, - Versions: []expapi.APIVersion{ - { - Name: "stable/v1", - }, - }, + Data: []byte("foobarbaz"), } } func TestCreate(t *testing.T) { storage, fakeEtcdClient, _ := newStorage(t) test := resttest.New(t, storage, fakeEtcdClient.SetError) - rsrc := validNewThirdPartyResource("foo") + rsrc := validNewThirdPartyResourceData("foo") rsrc.ObjectMeta = api.ObjectMeta{} test.TestCreate( // valid rsrc, // invalid - &expapi.ThirdPartyResource{}, + &expapi.ThirdPartyResourceData{}, ) } @@ -88,14 +84,14 @@ func TestUpdate(t *testing.T) { key = etcdtest.AddPrefix(key) fakeEtcdClient.ExpectNotFoundGet(key) fakeEtcdClient.ChangeIndex = 2 - rsrc := validNewThirdPartyResource("foo") - existing := validNewThirdPartyResource("exists") + rsrc := validNewThirdPartyResourceData("foo") + existing := validNewThirdPartyResourceData("exists") existing.Namespace = test.TestNamespace() obj, err := storage.Create(test.TestContext(), existing) if err != nil { t.Fatalf("unable to create object: %v", err) } - older := obj.(*expapi.ThirdPartyResource) + older := obj.(*expapi.ThirdPartyResourceData) older.ResourceVersion = "1" test.TestUpdate( rsrc, @@ -104,37 +100,10 @@ func TestUpdate(t *testing.T) { ) } -func TestDelete(t *testing.T) { - ctx := api.NewDefaultContext() - storage, fakeEtcdClient, _ := newStorage(t) - test := resttest.New(t, storage, fakeEtcdClient.SetError) - rsrc := validNewThirdPartyResource("foo2") - key, _ := storage.KeyFunc(ctx, "foo2") - key = etcdtest.AddPrefix(key) - createFn := func() runtime.Object { - fakeEtcdClient.Data[key] = tools.EtcdResponseWithError{ - R: &etcd.Response{ - Node: &etcd.Node{ - Value: runtime.EncodeOrDie(testapi.Codec(), rsrc), - ModifiedIndex: 1, - }, - }, - } - return rsrc - } - gracefulSetFn := func() bool { - if fakeEtcdClient.Data[key].R.Node == nil { - return false - } - return fakeEtcdClient.Data[key].R.Node.TTL == 30 - } - test.TestDeleteNoGraceful(createFn, gracefulSetFn) -} - func TestGet(t *testing.T) { storage, fakeEtcdClient, _ := newStorage(t) test := resttest.New(t, storage, fakeEtcdClient.SetError) - rsrc := validNewThirdPartyResource("foo") + rsrc := validNewThirdPartyResourceData("foo") test.TestGet(rsrc) } @@ -152,10 +121,10 @@ func TestEmptyList(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - if len(rsrcList.(*expapi.ThirdPartyResourceList).Items) != 0 { + if len(rsrcList.(*expapi.ThirdPartyResourceDataList).Items) != 0 { t.Errorf("Unexpected non-zero autoscaler list: %#v", rsrcList) } - if rsrcList.(*expapi.ThirdPartyResourceList).ResourceVersion != "1" { + if rsrcList.(*expapi.ThirdPartyResourceDataList).ResourceVersion != "1" { t.Errorf("Unexpected resource version: %#v", rsrcList) } } @@ -171,12 +140,12 @@ func TestList(t *testing.T) { Node: &etcd.Node{ Nodes: []*etcd.Node{ { - Value: runtime.EncodeOrDie(testapi.Codec(), &expapi.ThirdPartyResource{ + Value: runtime.EncodeOrDie(testapi.Codec(), &expapi.ThirdPartyResourceData{ ObjectMeta: api.ObjectMeta{Name: "foo"}, }), }, { - Value: runtime.EncodeOrDie(testapi.Codec(), &expapi.ThirdPartyResource{ + Value: runtime.EncodeOrDie(testapi.Codec(), &expapi.ThirdPartyResourceData{ ObjectMeta: api.ObjectMeta{Name: "bar"}, }), }, @@ -188,18 +157,18 @@ func TestList(t *testing.T) { if err != nil { t.Fatalf("Unexpected error %v", err) } - rsrcList := obj.(*expapi.ThirdPartyResourceList) + rsrcList := obj.(*expapi.ThirdPartyResourceDataList) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(rsrcList.Items) != 2 { - t.Errorf("Unexpected ThirdPartyResource list: %#v", rsrcList) + t.Errorf("Unexpected ThirdPartyResourceData list: %#v", rsrcList) } if rsrcList.Items[0].Name != "foo" { - t.Errorf("Unexpected ThirdPartyResource: %#v", rsrcList.Items[0]) + t.Errorf("Unexpected ThirdPartyResourceData: %#v", rsrcList.Items[0]) } if rsrcList.Items[1].Name != "bar" { - t.Errorf("Unexpected ThirdPartyResource: %#v", rsrcList.Items[1]) + t.Errorf("Unexpected ThirdPartyResourceData: %#v", rsrcList.Items[1]) } } diff --git a/pkg/registry/thirdpartyresourcedata/strategy.go b/pkg/registry/thirdpartyresourcedata/strategy.go index fd72660a087..fc97d47a3fb 100644 --- a/pkg/registry/thirdpartyresourcedata/strategy.go +++ b/pkg/registry/thirdpartyresourcedata/strategy.go @@ -52,7 +52,7 @@ func (strategy) PrepareForCreate(obj runtime.Object) { } func (strategy) Validate(ctx api.Context, obj runtime.Object) fielderrors.ValidationErrorList { - return validation.ValidateThirdPartyResource(obj.(*expapi.ThirdPartyResource)) + return validation.ValidateThirdPartyResourceData(obj.(*expapi.ThirdPartyResourceData)) } func (strategy) AllowCreateOnUpdate() bool { @@ -63,7 +63,7 @@ func (strategy) PrepareForUpdate(obj, old runtime.Object) { } func (strategy) ValidateUpdate(ctx api.Context, obj, old runtime.Object) fielderrors.ValidationErrorList { - return validation.ValidateThirdPartyResourceUpdate(old.(*expapi.ThirdPartyResource), obj.(*expapi.ThirdPartyResource)) + return validation.ValidateThirdPartyResourceDataUpdate(old.(*expapi.ThirdPartyResourceData), obj.(*expapi.ThirdPartyResourceData)) } func (strategy) AllowUnconditionalUpdate() bool { @@ -73,9 +73,9 @@ func (strategy) AllowUnconditionalUpdate() bool { // Matcher returns a generic matcher for a given label and field selector. func Matcher(label labels.Selector, field fields.Selector) generic.Matcher { return generic.MatcherFunc(func(obj runtime.Object) (bool, error) { - sa, ok := obj.(*expapi.ThirdPartyResource) + sa, ok := obj.(*expapi.ThirdPartyResourceData) if !ok { - return false, fmt.Errorf("not a ThirdPartyResource") + return false, fmt.Errorf("not a ThirdPartyResourceData") } fields := SelectableFields(sa) return label.Matches(labels.Set(sa.Labels)) && field.Matches(fields), nil @@ -83,6 +83,6 @@ func Matcher(label labels.Selector, field fields.Selector) generic.Matcher { } // SelectableFields returns a label set that can be used for filter selection -func SelectableFields(obj *expapi.ThirdPartyResource) labels.Set { +func SelectableFields(obj *expapi.ThirdPartyResourceData) labels.Set { return labels.Set{} } diff --git a/pkg/registry/thirdpartyresourcedata/util.go b/pkg/registry/thirdpartyresourcedata/util.go new file mode 100644 index 00000000000..e31c54cd54f --- /dev/null +++ b/pkg/registry/thirdpartyresourcedata/util.go @@ -0,0 +1,49 @@ +/* +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 thirdpartyresourcedata + +import ( + "fmt" + "strings" + + "k8s.io/kubernetes/pkg/expapi" +) + +func convertToCamelCase(input string) string { + result := "" + toUpper := true + for ix := range input { + char := input[ix] + if toUpper { + result = result + string([]byte{(char - 32)}) + toUpper = false + } else if char == '-' { + toUpper = true + } else { + result = result + string([]byte{char}) + } + } + return result +} + +func ExtractApiGroupAndKind(rsrc *expapi.ThirdPartyResource) (kind string, group string, err error) { + parts := strings.Split(rsrc.Name, ".") + if len(parts) < 3 { + return "", "", fmt.Errorf("unexpectedly short resource name: %s, expected at least ..", rsrc.Name) + } + return convertToCamelCase(parts[0]), strings.Join(parts[1:], "."), nil +} diff --git a/pkg/registry/thirdpartyresourcedata/util_test.go b/pkg/registry/thirdpartyresourcedata/util_test.go new file mode 100644 index 00000000000..43db4464c94 --- /dev/null +++ b/pkg/registry/thirdpartyresourcedata/util_test.go @@ -0,0 +1,66 @@ +/* +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 thirdpartyresourcedata + +import ( + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/expapi" +) + +func TestExtractAPIGroupAndKind(t *testing.T) { + tests := []struct { + input string + expectedKind string + expectedGroup string + expectErr bool + }{ + { + input: "foo.company.com", + expectedKind: "Foo", + expectedGroup: "company.com", + }, + { + input: "cron-tab.company.com", + expectedKind: "CronTab", + expectedGroup: "company.com", + }, + { + input: "foo", + expectErr: true, + }, + } + + for _, test := range tests { + kind, group, err := ExtractApiGroupAndKind(&expapi.ThirdPartyResource{ObjectMeta: api.ObjectMeta{Name: test.input}}) + if err != nil && !test.expectErr { + t.Errorf("unexpected error: %v", err) + continue + } + if err == nil && test.expectErr { + t.Errorf("unexpected non-error") + continue + } + if kind != test.expectedKind { + t.Errorf("expected: %s, saw: %s", test.expectedKind, kind) + } + if group != test.expectedGroup { + t.Errorf("expected: %s, saw: %s", test.expectedGroup, group) + } + } +} From 855c7cedccb635867811ab016147f0b0c3da5a3d Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Fri, 21 Aug 2015 14:24:16 -0700 Subject: [PATCH 4/6] Address comments. --- contrib/completions/bash/kubectl | 2 ++ pkg/expapi/types.go | 1 - pkg/expapi/v1/types.go | 1 - pkg/kubectl/resource_printer.go | 2 +- pkg/master/master.go | 4 ++-- pkg/master/master_test.go | 8 ++++---- pkg/registry/thirdpartyresource/etcd/etcd_test.go | 1 - pkg/registry/thirdpartyresourcedata/codec.go | 3 --- pkg/registry/thirdpartyresourcedata/etcd/etcd.go | 4 +++- 9 files changed, 12 insertions(+), 14 deletions(-) diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index 21fcb1ed349..08f18d5eb03 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -287,6 +287,7 @@ _kubectl_get() must_have_one_noun+=("secret") must_have_one_noun+=("service") must_have_one_noun+=("serviceaccount") + must_have_one_noun+=("thirdpartyresource") } _kubectl_describe() @@ -460,6 +461,7 @@ _kubectl_delete() must_have_one_noun+=("secret") must_have_one_noun+=("service") must_have_one_noun+=("serviceaccount") + must_have_one_noun+=("thirdpartyresource") } _kubectl_namespace() diff --git a/pkg/expapi/types.go b/pkg/expapi/types.go index 68ec8aadb22..d826c60e04e 100644 --- a/pkg/expapi/types.go +++ b/pkg/expapi/types.go @@ -167,7 +167,6 @@ type ThirdPartyResourceData struct { Data []byte `json:"name,omitempty" description:"the raw JSON data for this data"` } -<<<<<<< HEAD type Deployment struct { api.TypeMeta `json:",inline"` api.ObjectMeta `json:"metadata,omitempty"` diff --git a/pkg/expapi/v1/types.go b/pkg/expapi/v1/types.go index 65d55c6748b..956e7334466 100644 --- a/pkg/expapi/v1/types.go +++ b/pkg/expapi/v1/types.go @@ -153,7 +153,6 @@ type ThirdPartyResourceData struct { Data []byte `json:"name,omitempty" description:"the raw JSON data for this data"` } -<<<<<<< HEAD type Deployment struct { v1.TypeMeta `json:",inline"` v1.ObjectMeta `json:"metadata,omitempty"` diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index 548f2753167..3d649a6efc1 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -1088,7 +1088,7 @@ func printThirdPartyResource(rsrc *expapi.ThirdPartyResource, w io.Writer, withN func printThirdPartyResourceList(list *expapi.ThirdPartyResourceList, w io.Writer, withNamespace bool, wide bool, showAll bool, columnLabels []string) error { for _, item := range list.Items { - if err := printThirdPartyResource(&item, w, withNamespace, wide, columnLabels); err != nil { + if err := printThirdPartyResource(&item, w, withNamespace, wide, showAll, columnLabels); err != nil { return err } } diff --git a/pkg/master/master.go b/pkg/master/master.go index 30d6d4d55e5..84fc0d7b84c 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -776,7 +776,7 @@ func (m *Master) InstallThirdPartyAPI(rsrc *expapi.ThirdPartyResource) error { if err != nil { return err } - thirdparty := m.thirdpartyapi(group, strings.ToLower(kind)+"s", rsrc.Versions[0].Name) + thirdparty := m.thirdpartyapi(group, kind, rsrc.Versions[0].Name) if err := thirdparty.InstallREST(m.handlerContainer); err != nil { glog.Fatalf("Unable to setup thirdparty api: %v", err) } @@ -793,7 +793,7 @@ func (m *Master) thirdpartyapi(group, kind, version string) *apiserver.APIGroupV apiRoot := "/thirdparty/" + group + "/" storage := map[string]rest.Storage{ - kind: resourceStorage, + strings.ToLower(kind) + "s": resourceStorage, } return &apiserver.APIGroupVersion{ diff --git a/pkg/master/master_test.go b/pkg/master/master_test.go index 88137175dec..d3712417ebc 100644 --- a/pkg/master/master_test.go +++ b/pkg/master/master_test.go @@ -198,7 +198,7 @@ func TestInstallThirdPartyAPIGet(t *testing.T) { Name: "test", }, TypeMeta: api.TypeMeta{ - Kind: "foo", + Kind: "Foo", }, SomeField: "test field", OtherField: 10, @@ -237,12 +237,12 @@ func TestInstallThirdPartyAPIPost(t *testing.T) { Name: "test", }, TypeMeta: api.TypeMeta{ - Kind: "foo", + Kind: "Foo", }, SomeField: "test field", OtherField: 10, } - data, err := encodeToThirdParty("test", inputObj) + data, err := json.Marshal(inputObj) if err != nil { t.Errorf("unexpected error: %v") return @@ -298,7 +298,7 @@ func TestInstallThirdPartyAPIDelete(t *testing.T) { Name: "test", }, TypeMeta: api.TypeMeta{ - Kind: "foo", + Kind: "Foo", }, SomeField: "test field", OtherField: 10, diff --git a/pkg/registry/thirdpartyresource/etcd/etcd_test.go b/pkg/registry/thirdpartyresource/etcd/etcd_test.go index 3406b62bcd6..b60bc020fe0 100644 --- a/pkg/registry/thirdpartyresource/etcd/etcd_test.go +++ b/pkg/registry/thirdpartyresource/etcd/etcd_test.go @@ -101,7 +101,6 @@ func TestUpdate(t *testing.T) { ) } -<<<<<<< HEAD func TestDelete(t *testing.T) { ctx := api.NewDefaultContext() storage, fakeClient := newStorage(t) diff --git a/pkg/registry/thirdpartyresourcedata/codec.go b/pkg/registry/thirdpartyresourcedata/codec.go index 120a6caa306..7d2c75bb0be 100644 --- a/pkg/registry/thirdpartyresourcedata/codec.go +++ b/pkg/registry/thirdpartyresourcedata/codec.go @@ -85,9 +85,6 @@ func (t *thirdPartyResourceDataCodec) populate(objIn *expapi.ThirdPartyResourceD if !ok { return fmt.Errorf("unexpected object: %#v", obj) } - if kind == "ThirdPartyResourceData" { - return t.delegate.DecodeInto(data, objIn) - } if kind != t.kind { return fmt.Errorf("unexpected kind: %s, expected: %s", kind, t.kind) } diff --git a/pkg/registry/thirdpartyresourcedata/etcd/etcd.go b/pkg/registry/thirdpartyresourcedata/etcd/etcd.go index ea9495bb232..497f730ec5c 100644 --- a/pkg/registry/thirdpartyresourcedata/etcd/etcd.go +++ b/pkg/registry/thirdpartyresourcedata/etcd/etcd.go @@ -17,6 +17,8 @@ limitations under the License. package etcd import ( + "strings" + "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/expapi" "k8s.io/kubernetes/pkg/fields" @@ -35,7 +37,7 @@ type REST struct { // NewREST returns a registry which will store ThirdPartyResourceData in the given helper func NewREST(s storage.Interface, group, kind string) *REST { - prefix := "/ThirdPartyResourceData/" + group + "/" + kind + prefix := "/ThirdPartyResourceData/" + group + "/" + strings.ToLower(kind) + "s" store := &etcdgeneric.Etcd{ NewFunc: func() runtime.Object { return &expapi.ThirdPartyResourceData{} }, From d993b8891d68077d440066ce878ff0512a757c36 Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Mon, 31 Aug 2015 21:11:34 -0700 Subject: [PATCH 5/6] Addressed changes --- pkg/master/master_test.go | 2 +- pkg/registry/thirdpartyresourcedata/codec.go | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/master/master_test.go b/pkg/master/master_test.go index d3712417ebc..dcc58bbd195 100644 --- a/pkg/master/master_test.go +++ b/pkg/master/master_test.go @@ -107,7 +107,7 @@ func initThirdParty(t *testing.T) (*tools.FakeEtcdClient, *httptest.Server) { Versions: []expapi.APIVersion{ { APIGroup: "group", - Name: "v1", + Name: "v3", }, }, } diff --git a/pkg/registry/thirdpartyresourcedata/codec.go b/pkg/registry/thirdpartyresourcedata/codec.go index 7d2c75bb0be..1d9dbc1b6d2 100644 --- a/pkg/registry/thirdpartyresourcedata/codec.go +++ b/pkg/registry/thirdpartyresourcedata/codec.go @@ -98,7 +98,15 @@ func (t *thirdPartyResourceDataCodec) populate(objIn *expapi.ThirdPartyResourceD if !ok { return fmt.Errorf("unexpected object: %#v", obj) } - + + if resourceVersion, ok := metadata["resourceVersion"]; ok { + resourceVersionStr, ok := resourceVersion.(string) + if !ok { + return fmt.Errorf("unexpected object: %v", metadata["resourceVersion"]) + } + objIn.ResourceVersion = strconv.Atoi(resourceVersionStr) + } + objIn.Name = name objIn.Data = data return nil From 6aa7ce268993bf1fdc8bba502d554431840d7ed0 Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Mon, 31 Aug 2015 22:28:08 -0700 Subject: [PATCH 6/6] addressed comments. --- pkg/expapi/types.go | 1 - pkg/expapi/v1/types.go | 1 - pkg/master/master.go | 4 +- pkg/master/master_test.go | 57 ++++++-- pkg/registry/thirdpartyresourcedata/codec.go | 128 +++++++++++++++--- .../thirdpartyresourcedata/codec_test.go | 123 +++++++++++++++++ 6 files changed, 279 insertions(+), 35 deletions(-) create mode 100644 pkg/registry/thirdpartyresourcedata/codec_test.go diff --git a/pkg/expapi/types.go b/pkg/expapi/types.go index d826c60e04e..a9d84216443 100644 --- a/pkg/expapi/types.go +++ b/pkg/expapi/types.go @@ -271,7 +271,6 @@ type DeploymentList struct { Items []Deployment `json:"items"` } -<<<<<<< HEAD // DaemonSpec is the specification of a daemon. type DaemonSpec struct { // Selector is a label query over pods that are managed by the daemon. diff --git a/pkg/expapi/v1/types.go b/pkg/expapi/v1/types.go index 956e7334466..d2c53765e03 100644 --- a/pkg/expapi/v1/types.go +++ b/pkg/expapi/v1/types.go @@ -258,7 +258,6 @@ type DeploymentList struct { Items []Deployment `json:"items" description:"list of deployments"` } -<<<<<<< HEAD // DaemonSpec is the specification of a daemon. type DaemonSpec struct { // Selector is a label query over pods that are managed by the daemon. diff --git a/pkg/master/master.go b/pkg/master/master.go index 84fc0d7b84c..515c76873c4 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -799,11 +799,11 @@ func (m *Master) thirdpartyapi(group, kind, version string) *apiserver.APIGroupV return &apiserver.APIGroupVersion{ Root: apiRoot, - Creater: api.Scheme, + Creater: thirdpartyresourcedata.NewObjectCreator(version, api.Scheme), Convertor: api.Scheme, Typer: api.Scheme, - Mapper: thirdpartyresourcedata.NewMapper(explatest.RESTMapper, kind), + Mapper: thirdpartyresourcedata.NewMapper(explatest.RESTMapper, kind, version), Codec: explatest.Codec, Linker: explatest.SelfLinker, Storage: storage, diff --git a/pkg/master/master_test.go b/pkg/master/master_test.go index dcc58bbd195..5934983c15f 100644 --- a/pkg/master/master_test.go +++ b/pkg/master/master_test.go @@ -83,6 +83,8 @@ func TestFindExternalAddress(t *testing.T) { } } +var versionsToTest = []string{"v1", "v3"} + type Foo struct { api.TypeMeta `json:",inline"` api.ObjectMeta `json:"metadata,omitempty" description:"standard object metadata"` @@ -98,7 +100,7 @@ type FooList struct { items []Foo `json:"items"` } -func initThirdParty(t *testing.T) (*tools.FakeEtcdClient, *httptest.Server) { +func initThirdParty(t *testing.T, version string) (*tools.FakeEtcdClient, *httptest.Server) { master := &Master{} api := &expapi.ThirdPartyResource{ ObjectMeta: api.ObjectMeta{ @@ -107,7 +109,7 @@ func initThirdParty(t *testing.T) (*tools.FakeEtcdClient, *httptest.Server) { Versions: []expapi.APIVersion{ { APIGroup: "group", - Name: "v3", + Name: version, }, }, } @@ -127,12 +129,18 @@ func initThirdParty(t *testing.T) (*tools.FakeEtcdClient, *httptest.Server) { } func TestInstallThirdPartyAPIList(t *testing.T) { - fakeClient, server := initThirdParty(t) + for _, version := range versionsToTest { + testInstallThirdPartyAPIListVersion(t, version) + } +} + +func testInstallThirdPartyAPIListVersion(t *testing.T, version string) { + fakeClient, server := initThirdParty(t, version) defer server.Close() fakeClient.ExpectNotFoundGet(etcdtest.PathPrefix() + "/ThirdPartyResourceData/company.com/foos/default") - resp, err := http.Get(server.URL + "/thirdparty/company.com/v1/namespaces/default/foos") + resp, err := http.Get(server.URL + "/thirdparty/company.com/" + version + "/namespaces/default/foos") if err != nil { t.Errorf("unexpected error: %v", err) return @@ -190,7 +198,13 @@ func decodeResponse(resp *http.Response, obj interface{}) error { } func TestInstallThirdPartyAPIGet(t *testing.T) { - fakeClient, server := initThirdParty(t) + for _, version := range versionsToTest { + testInstallThirdPartyAPIGetVersion(t, version) + } +} + +func testInstallThirdPartyAPIGetVersion(t *testing.T, version string) { + fakeClient, server := initThirdParty(t, version) defer server.Close() expectedObj := Foo{ @@ -198,7 +212,8 @@ func TestInstallThirdPartyAPIGet(t *testing.T) { Name: "test", }, TypeMeta: api.TypeMeta{ - Kind: "Foo", + Kind: "Foo", + APIVersion: version, }, SomeField: "test field", OtherField: 10, @@ -209,7 +224,7 @@ func TestInstallThirdPartyAPIGet(t *testing.T) { return } - resp, err := http.Get(server.URL + "/thirdparty/company.com/v1/namespaces/default/foos/test") + resp, err := http.Get(server.URL + "/thirdparty/company.com/" + version + "/namespaces/default/foos/test") if err != nil { t.Errorf("unexpected error: %v", err) return @@ -229,7 +244,13 @@ func TestInstallThirdPartyAPIGet(t *testing.T) { } func TestInstallThirdPartyAPIPost(t *testing.T) { - fakeClient, server := initThirdParty(t) + for _, version := range versionsToTest { + testInstallThirdPartyAPIPostForVersion(t, version) + } +} + +func testInstallThirdPartyAPIPostForVersion(t *testing.T, version string) { + fakeClient, server := initThirdParty(t, version) defer server.Close() inputObj := Foo{ @@ -237,7 +258,8 @@ func TestInstallThirdPartyAPIPost(t *testing.T) { Name: "test", }, TypeMeta: api.TypeMeta{ - Kind: "Foo", + Kind: "Foo", + APIVersion: version, }, SomeField: "test field", OtherField: 10, @@ -248,7 +270,7 @@ func TestInstallThirdPartyAPIPost(t *testing.T) { return } - resp, err := http.Post(server.URL+"/thirdparty/company.com/v1/namespaces/default/foos", "application/json", bytes.NewBuffer(data)) + resp, err := http.Post(server.URL+"/thirdparty/company.com/"+version+"/namespaces/default/foos", "application/json", bytes.NewBuffer(data)) if err != nil { t.Errorf("unexpected error: %v", err) return @@ -270,6 +292,7 @@ func TestInstallThirdPartyAPIPost(t *testing.T) { etcdResp, err := fakeClient.Get(etcdtest.PathPrefix()+"/ThirdPartyResourceData/company.com/foos/default/test", false, false) if err != nil { t.Errorf("unexpected error: %v", err) + t.FailNow() } obj, err := explatest.Codec.Decode([]byte(etcdResp.Node.Value)) if err != nil { @@ -290,7 +313,13 @@ func TestInstallThirdPartyAPIPost(t *testing.T) { } func TestInstallThirdPartyAPIDelete(t *testing.T) { - fakeClient, server := initThirdParty(t) + for _, version := range versionsToTest { + testInstallThirdPartyAPIDeleteVersion(t, version) + } +} + +func testInstallThirdPartyAPIDeleteVersion(t *testing.T, version string) { + fakeClient, server := initThirdParty(t, version) defer server.Close() expectedObj := Foo{ @@ -309,7 +338,7 @@ func TestInstallThirdPartyAPIDelete(t *testing.T) { return } - resp, err := http.Get(server.URL + "/thirdparty/company.com/v1/namespaces/default/foos/test") + resp, err := http.Get(server.URL + "/thirdparty/company.com/" + version + "/namespaces/default/foos/test") if err != nil { t.Errorf("unexpected error: %v", err) return @@ -328,7 +357,7 @@ func TestInstallThirdPartyAPIDelete(t *testing.T) { t.Errorf("expected:\n%v\nsaw:\n%v\n", expectedObj, item) } - resp, err = httpDelete(server.URL + "/thirdparty/company.com/v1/namespaces/default/foos/test") + resp, err = httpDelete(server.URL + "/thirdparty/company.com/" + version + "/namespaces/default/foos/test") if err != nil { t.Errorf("unexpected error: %v", err) return @@ -338,7 +367,7 @@ func TestInstallThirdPartyAPIDelete(t *testing.T) { t.Errorf("unexpected status: %v", resp) } - resp, err = http.Get(server.URL + "/thirdparty/company.com/v1/namespaces/default/foos/test") + resp, err = http.Get(server.URL + "/thirdparty/company.com/" + version + "/namespaces/default/foos/test") if err != nil { t.Errorf("unexpected error: %v", err) return diff --git a/pkg/registry/thirdpartyresourcedata/codec.go b/pkg/registry/thirdpartyresourcedata/codec.go index 1d9dbc1b6d2..bc190a1b9fd 100644 --- a/pkg/registry/thirdpartyresourcedata/codec.go +++ b/pkg/registry/thirdpartyresourcedata/codec.go @@ -25,12 +25,14 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/meta" "k8s.io/kubernetes/pkg/expapi" + "k8s.io/kubernetes/pkg/expapi/latest" "k8s.io/kubernetes/pkg/runtime" ) type thirdPartyResourceDataMapper struct { - mapper meta.RESTMapper - kind string + mapper meta.RESTMapper + kind string + version string } func (t *thirdPartyResourceDataMapper) GroupForResource(resource string) (string, error) { @@ -38,7 +40,16 @@ func (t *thirdPartyResourceDataMapper) GroupForResource(resource string) (string } func (t *thirdPartyResourceDataMapper) RESTMapping(kind string, versions ...string) (*meta.RESTMapping, error) { - mapping, err := t.mapper.RESTMapping(kind, versions...) + if len(versions) != 1 { + return nil, fmt.Errorf("unexpected set of versions: %v", versions) + } + if versions[0] != t.version { + return nil, fmt.Errorf("unknown version %s expected %s", versions[0], t.version) + } + if kind != "ThirdPartyResourceData" { + return nil, fmt.Errorf("unknown kind %s expected %s", kind, t.kind) + } + mapping, err := t.mapper.RESTMapping("ThirdPartyResourceData", latest.Version) if err != nil { return nil, err } @@ -58,8 +69,12 @@ func (t *thirdPartyResourceDataMapper) VersionAndKindForResource(resource string return t.mapper.VersionAndKindForResource(resource) } -func NewMapper(mapper meta.RESTMapper, kind string) meta.RESTMapper { - return &thirdPartyResourceDataMapper{mapper, kind} +func NewMapper(mapper meta.RESTMapper, kind, version string) meta.RESTMapper { + return &thirdPartyResourceDataMapper{ + mapper: mapper, + kind: kind, + version: version, + } } type thirdPartyResourceDataCodec struct { @@ -81,9 +96,13 @@ func (t *thirdPartyResourceDataCodec) populate(objIn *expapi.ThirdPartyResourceD if !ok { return fmt.Errorf("unexpected object: %#v", obj) } + return t.populateFromObject(objIn, mapObj, data) +} + +func (t *thirdPartyResourceDataCodec) populateFromObject(objIn *expapi.ThirdPartyResourceData, mapObj map[string]interface{}, data []byte) error { kind, ok := mapObj["kind"].(string) if !ok { - return fmt.Errorf("unexpected object: %#v", obj) + return fmt.Errorf("unexpected object for kind: %#v", mapObj["kind"]) } if kind != t.kind { return fmt.Errorf("unexpected kind: %s, expected: %s", kind, t.kind) @@ -91,22 +110,40 @@ func (t *thirdPartyResourceDataCodec) populate(objIn *expapi.ThirdPartyResourceD metadata, ok := mapObj["metadata"].(map[string]interface{}) if !ok { - return fmt.Errorf("unexpected object: %#v", obj) + return fmt.Errorf("unexpected object for metadata: %#v", mapObj["metadata"]) + } + + if resourceVersion, ok := metadata["resourceVersion"]; ok { + resourceVersionStr, ok := resourceVersion.(string) + if !ok { + return fmt.Errorf("unexpected object for resourceVersion: %v", resourceVersion) + } + + objIn.ResourceVersion = resourceVersionStr } name, ok := metadata["name"].(string) if !ok { - return fmt.Errorf("unexpected object: %#v", obj) + return fmt.Errorf("unexpected object for name: %#v", metadata) } - - if resourceVersion, ok := metadata["resourceVersion"]; ok { - resourceVersionStr, ok := resourceVersion.(string) + + if labels, ok := metadata["labels"]; ok { + labelMap, ok := labels.(map[string]interface{}) if !ok { - return fmt.Errorf("unexpected object: %v", metadata["resourceVersion"]) + return fmt.Errorf("unexpected object for labels: %v", labelMap) + } + for key, value := range labelMap { + valueStr, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected label: %v", value) + } + if objIn.Labels == nil { + objIn.Labels = map[string]string{} + } + objIn.Labels[key] = valueStr } - objIn.ResourceVersion = strconv.Atoi(resourceVersionStr) } - + objIn.Name = name objIn.Data = data return nil @@ -141,16 +178,50 @@ func (t *thirdPartyResourceDataCodec) DecodeInto(data []byte, obj runtime.Object return t.populate(thirdParty, data) } -func (t *thirdPartyResourceDataCodec) DecodeIntoWithSpecifiedVersionKind(data []byte, obj runtime.Object, kind, version string) error { +func (t *thirdPartyResourceDataCodec) DecodeIntoWithSpecifiedVersionKind(data []byte, obj runtime.Object, version, kind string) error { thirdParty, ok := obj.(*expapi.ThirdPartyResourceData) if !ok { return fmt.Errorf("unexpected object: %#v", obj) } + + if kind != "ThirdPartyResourceData" { + return fmt.Errorf("unexpeceted kind: %s", kind) + } + + var dataObj interface{} + if err := json.Unmarshal(data, &dataObj); err != nil { + return err + } + mapObj, ok := dataObj.(map[string]interface{}) + if !ok { + return fmt.Errorf("unexpcted object: %#v", dataObj) + } + if kindObj, found := mapObj["kind"]; !found { + mapObj["kind"] = kind + } else { + kindStr, ok := kindObj.(string) + if !ok { + return fmt.Errorf("unexpected object for 'kind': %v", kindObj) + } + if kindStr != t.kind { + return fmt.Errorf("kind doesn't match, expecting: %s, got %s", kind, kindStr) + } + } + if versionObj, found := mapObj["apiVersion"]; !found { + mapObj["apiVersion"] = version + } else { + versionStr, ok := versionObj.(string) + if !ok { + return fmt.Errorf("unexpected object for 'apiVersion': %v", versionObj) + } + if versionStr != version { + return fmt.Errorf("version doesn't match, expecting: %s, got %s", version, versionStr) + } + } + if err := t.populate(thirdParty, data); err != nil { return err } - thirdParty.Kind = kind - thirdParty.APIVersion = version return nil } @@ -178,3 +249,26 @@ func (t *thirdPartyResourceDataCodec) Encode(obj runtime.Object) (data []byte, e return nil, fmt.Errorf("unexpected object to encode: %#v", obj) } } + +func NewObjectCreator(version string, delegate runtime.ObjectCreater) runtime.ObjectCreater { + return &thirdPartyResourceDataCreator{version, delegate} +} + +type thirdPartyResourceDataCreator struct { + version string + delegate runtime.ObjectCreater +} + +func (t *thirdPartyResourceDataCreator) New(version, kind string) (out runtime.Object, err error) { + if t.version != version { + return nil, fmt.Errorf("unknown version %s for kind %s", version, kind) + } + switch kind { + case "ThirdPartyResourceData": + return &expapi.ThirdPartyResourceData{}, nil + case "ThirdPartyResourceDataList": + return &expapi.ThirdPartyResourceDataList{}, nil + default: + return t.delegate.New(latest.Version, kind) + } +} diff --git a/pkg/registry/thirdpartyresourcedata/codec_test.go b/pkg/registry/thirdpartyresourcedata/codec_test.go new file mode 100644 index 00000000000..30665cfd7c5 --- /dev/null +++ b/pkg/registry/thirdpartyresourcedata/codec_test.go @@ -0,0 +1,123 @@ +/* +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 thirdpartyresourcedata + +import ( + "encoding/json" + "reflect" + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/expapi" +) + +type Foo struct { + api.TypeMeta `json:",inline"` + api.ObjectMeta `json:"metadata,omitempty" description:"standard object metadata"` + + SomeField string `json:"someField"` + OtherField int `json:"otherField"` +} + +type FooList struct { + api.TypeMeta `json:",inline"` + api.ListMeta `json:"metadata,omitempty" description:"standard list metadata; see http://docs.k8s.io/api-conventions.md#metadata"` + + items []Foo `json:"items"` +} + +func TestCodec(t *testing.T) { + tests := []struct { + obj *Foo + expectErr bool + name string + }{ + { + obj: &Foo{ObjectMeta: api.ObjectMeta{Name: "bar"}}, + expectErr: true, + name: "missing kind", + }, + { + obj: &Foo{ObjectMeta: api.ObjectMeta{Name: "bar"}, TypeMeta: api.TypeMeta{Kind: "Foo"}}, + name: "basic", + }, + { + obj: &Foo{ObjectMeta: api.ObjectMeta{Name: "bar", ResourceVersion: "baz"}, TypeMeta: api.TypeMeta{Kind: "Foo"}}, + name: "resource version", + }, + { + obj: &Foo{ + ObjectMeta: api.ObjectMeta{ + Name: "bar", + ResourceVersion: "baz", + Labels: map[string]string{"foo": "bar", "baz": "blah"}, + }, + TypeMeta: api.TypeMeta{Kind: "Foo"}, + }, + name: "labels", + }, + } + for _, test := range tests { + codec := thirdPartyResourceDataCodec{kind: "Foo"} + data, err := json.Marshal(test.obj) + if err != nil { + t.Errorf("[%s] unexpected error: %v", test.name, err) + continue + } + obj, err := codec.Decode(data) + if err != nil && !test.expectErr { + t.Errorf("[%s] unexpected error: %v", test.name, err) + continue + } + if test.expectErr { + if err == nil { + t.Errorf("[%s] unexpected non-error", test.name) + } + continue + } + rsrcObj, ok := obj.(*expapi.ThirdPartyResourceData) + if !ok { + t.Errorf("[%s] unexpected object: %v", test.name, obj) + continue + } + if !reflect.DeepEqual(rsrcObj.ObjectMeta, test.obj.ObjectMeta) { + t.Errorf("[%s]\nexpected\n%v\nsaw\n%v\n", test.name, rsrcObj.ObjectMeta, test.obj.ObjectMeta) + } + var output Foo + if err := json.Unmarshal(rsrcObj.Data, &output); err != nil { + t.Errorf("[%s] unexpected error: %v", test.name, err) + continue + } + if !reflect.DeepEqual(&output, test.obj) { + t.Errorf("[%s]\nexpected\n%v\nsaw\n%v\n", test.name, test.obj, &output) + } + + data, err = codec.Encode(rsrcObj) + if err != nil { + t.Errorf("[%s] unexpected error: %v", test.name, err) + } + + var output2 Foo + if err := json.Unmarshal(data, &output2); err != nil { + t.Errorf("[%s] unexpected error: %v", test.name, err) + continue + } + if !reflect.DeepEqual(&output2, test.obj) { + t.Errorf("[%s]\nexpected\n%v\nsaw\n%v\n", test.name, test.obj, &output2) + } + } +}