From 7bfc8b5f37c381e35faccacfe9c3ba3b97a1129d Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Wed, 19 Aug 2015 11:02:01 -0700 Subject: [PATCH] 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{} +}