From e594407ab53ab78a94e85fd43885f666784eb718 Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Wed, 9 Dec 2015 16:35:35 -0800 Subject: [PATCH] Add dynamic APIs to the list of discoverable APIs --- pkg/apiserver/apiserver.go | 8 ++++---- pkg/master/master.go | 36 ++++++++++++++++++++++++++++-------- pkg/master/master_test.go | 33 +++++++++++++++++++++++++++++++-- 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 1a2042fbfc8..6a3faa0ff6d 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -254,8 +254,8 @@ func AddApiWebService(container *restful.Container, apiPrefix string, versions [ } // Adds a service to return the supported api versions at /apis. -func AddApisWebService(container *restful.Container, apiPrefix string, groups []unversioned.APIGroup) { - rootAPIHandler := RootAPIHandler(groups) +func AddApisWebService(container *restful.Container, apiPrefix string, f func() []unversioned.APIGroup) { + rootAPIHandler := RootAPIHandler(f) ws := new(restful.WebService) ws.Path(apiPrefix) ws.Doc("get available API versions") @@ -308,10 +308,10 @@ func APIVersionHandler(versions ...string) restful.RouteFunction { } // RootAPIHandler returns a handler which will list the provided groups and versions as available. -func RootAPIHandler(groups []unversioned.APIGroup) restful.RouteFunction { +func RootAPIHandler(f func() []unversioned.APIGroup) restful.RouteFunction { return func(req *restful.Request, resp *restful.Response) { // TODO: use restful's Response methods - writeJSON(http.StatusOK, api.Codec, &unversioned.APIGroupList{Groups: groups}, resp.ResponseWriter, true) + writeJSON(http.StatusOK, api.Codec, &unversioned.APIGroupList{Groups: f()}, resp.ResponseWriter, true) } } diff --git a/pkg/master/master.go b/pkg/master/master.go index 3433ef569e6..ba112972baf 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -111,8 +111,8 @@ type Master struct { // storage for third party objects thirdPartyStorage storage.Interface - // map from api path to storage for those objects - thirdPartyResources map[string]*thirdpartyresourcedataetcd.REST + // map from api path to a tuple of (storage for the objects, APIGroup) + thirdPartyResources map[string]thirdPartyEntry // protects the map thirdPartyResourcesLock sync.RWMutex @@ -120,6 +120,13 @@ type Master struct { tunneler Tunneler } +// thirdPartyEntry combines objects storage and API group into one struct +// for easy lookup. +type thirdPartyEntry struct { + storage *thirdpartyresourcedataetcd.REST + group unversioned.APIGroup +} + // New returns a new instance of Master from the given config. // Certain config fields will be set to a default value if unset. // Certain config fields must be specified, including: @@ -184,7 +191,7 @@ func (m *Master) InstallAPIs(c *Config) { // Install extensions unless disabled. if !m.ApiGroupVersionOverrides["extensions/v1beta1"].Disable { m.thirdPartyStorage = c.StorageDestinations.APIGroups[extensions.GroupName].Default - m.thirdPartyResources = map[string]*thirdpartyresourcedataetcd.REST{} + m.thirdPartyResources = map[string]thirdPartyEntry{} expVersion := m.experimental(c) @@ -217,7 +224,20 @@ func (m *Master) InstallAPIs(c *Config) { // This should be done after all groups are registered // TODO: replace the hardcoded "apis". - apiserver.AddApisWebService(m.HandlerContainer, "/apis", allGroups) + apiserver.AddApisWebService(m.HandlerContainer, "/apis", func() []unversioned.APIGroup { + groups := []unversioned.APIGroup{} + for ix := range allGroups { + groups = append(groups, allGroups[ix]) + } + m.thirdPartyResourcesLock.Lock() + defer m.thirdPartyResourcesLock.Unlock() + if m.thirdPartyResources != nil { + for key := range m.thirdPartyResources { + groups = append(groups, m.thirdPartyResources[key].group) + } + } + return groups + }) } func (m *Master) initV1ResourcesStorage(c *Config) { @@ -432,7 +452,7 @@ func (m *Master) removeThirdPartyStorage(path string) error { defer m.thirdPartyResourcesLock.Unlock() storage, found := m.thirdPartyResources[path] if found { - if err := m.removeAllThirdPartyResources(storage); err != nil { + if err := m.removeAllThirdPartyResources(storage.storage); err != nil { return err } delete(m.thirdPartyResources, path) @@ -486,10 +506,10 @@ func (m *Master) ListThirdPartyResources() []string { return result } -func (m *Master) addThirdPartyResourceStorage(path string, storage *thirdpartyresourcedataetcd.REST) { +func (m *Master) addThirdPartyResourceStorage(path string, storage *thirdpartyresourcedataetcd.REST, apiGroup unversioned.APIGroup) { m.thirdPartyResourcesLock.Lock() defer m.thirdPartyResourcesLock.Unlock() - m.thirdPartyResources[path] = storage + m.thirdPartyResources[path] = thirdPartyEntry{storage, apiGroup} } // InstallThirdPartyResource installs a third party resource specified by 'rsrc'. When a resource is @@ -518,7 +538,7 @@ func (m *Master) InstallThirdPartyResource(rsrc *extensions.ThirdPartyResource) Versions: []unversioned.GroupVersionForDiscovery{groupVersion}, } apiserver.AddGroupWebService(m.HandlerContainer, path, apiGroup) - m.addThirdPartyResourceStorage(path, thirdparty.Storage[strings.ToLower(kind)+"s"].(*thirdpartyresourcedataetcd.REST)) + m.addThirdPartyResourceStorage(path, thirdparty.Storage[strings.ToLower(kind)+"s"].(*thirdpartyresourcedataetcd.REST), apiGroup) apiserver.InstallServiceErrorHandler(m.HandlerContainer, m.NewRequestInfoResolver(), []string{thirdparty.GroupVersion.String()}) return nil } diff --git a/pkg/master/master_test.go b/pkg/master/master_test.go index ae9a235e99f..3abc983b4a9 100644 --- a/pkg/master/master_test.go +++ b/pkg/master/master_test.go @@ -41,7 +41,6 @@ import ( "k8s.io/kubernetes/pkg/registry/endpoint" "k8s.io/kubernetes/pkg/registry/namespace" "k8s.io/kubernetes/pkg/registry/registrytest" - thirdpartyresourcedatastorage "k8s.io/kubernetes/pkg/registry/thirdpartyresourcedata/etcd" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/storage" etcdstorage "k8s.io/kubernetes/pkg/storage/etcd" @@ -343,6 +342,36 @@ func TestDiscoveryAtAPIS(t *testing.T) { assert.Equal(expectGroupName, groupList.Groups[0].Name) assert.Equal(expectVersions, groupList.Groups[0].Versions) assert.Equal(expectPreferredVersion, groupList.Groups[0].PreferredVersion) + + thirdPartyGV := unversioned.GroupVersionForDiscovery{GroupVersion: "company.com/v1", Version: "v1"} + master.thirdPartyResources["/apis/company.com/v1"] = thirdPartyEntry{ + nil, + unversioned.APIGroup{ + Name: "company.com", + Versions: []unversioned.GroupVersionForDiscovery{thirdPartyGV}, + PreferredVersion: thirdPartyGV, + }, + } + + resp, err = http.Get(server.URL + "/apis") + if !assert.NoError(err) { + t.Errorf("unexpected error: %v", err) + } + + assert.Equal(http.StatusOK, resp.StatusCode) + + assert.NoError(decodeResponse(resp, &groupList)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + thirdPartyGroupName := "company.com" + thirdPartyExpectVersions := []unversioned.GroupVersionForDiscovery{thirdPartyGV} + + assert.Equal(thirdPartyGroupName, groupList.Groups[1].Name) + assert.Equal(thirdPartyExpectVersions, groupList.Groups[1].Versions) + assert.Equal(thirdPartyGV, groupList.Groups[1].PreferredVersion) + } var versionsToTest = []string{"v1", "v3"} @@ -365,7 +394,7 @@ type FooList struct { func initThirdParty(t *testing.T, version string) (*Master, *etcdtesting.EtcdTestServer, *httptest.Server, *assert.Assertions) { master, etcdserver, _, assert := setUp(t) - master.thirdPartyResources = map[string]*thirdpartyresourcedatastorage.REST{} + master.thirdPartyResources = map[string]thirdPartyEntry{} api := &extensions.ThirdPartyResource{ ObjectMeta: api.ObjectMeta{ Name: "foo.company.com",