Add support for installing custom object APIs

This commit is contained in:
Brendan Burns 2015-08-19 22:08:26 -07:00
parent 7bfc8b5f37
commit b196d0f84b
16 changed files with 603 additions and 158 deletions

View File

@ -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()

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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
}

View File

@ -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))

View File

@ -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
}

View File

@ -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,

View File

@ -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)
}

View File

@ -101,6 +101,7 @@ func TestUpdate(t *testing.T) {
)
}
<<<<<<< HEAD
func TestDelete(t *testing.T) {
ctx := api.NewDefaultContext()
storage, fakeClient := newStorage(t)

View File

@ -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)
}
}

View File

@ -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{} },

View File

@ -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])
}
}

View File

@ -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{}
}

View File

@ -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 <kind>.<domain>.<tld>", rsrc.Name)
}
return convertToCamelCase(parts[0]), strings.Join(parts[1:], "."), nil
}

View File

@ -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)
}
}
}