diff --git a/api/swagger-spec/resourceListing.json b/api/swagger-spec/resourceListing.json index 2e802cfa360..cb3854c2b03 100644 --- a/api/swagger-spec/resourceListing.json +++ b/api/swagger-spec/resourceListing.json @@ -1,6 +1,10 @@ { "swaggerVersion": "1.2", "apis": [ + { + "path": "/apis", + "description": "get available API versions" + }, { "path": "/version", "description": "git code version from which this is built" @@ -20,10 +24,6 @@ { "path": "/apis/extensions", "description": "get information of a group" - }, - { - "path": "/apis", - "description": "get available API versions" } ], "apiVersion": "", diff --git a/pkg/genericapiserver/genericapiserver.go b/pkg/genericapiserver/genericapiserver.go index ac1f125aa57..2aad6382940 100644 --- a/pkg/genericapiserver/genericapiserver.go +++ b/pkg/genericapiserver/genericapiserver.go @@ -24,6 +24,7 @@ import ( "net/http/pprof" "path" "regexp" + "sort" "strconv" "strings" "time" @@ -301,6 +302,10 @@ type GenericAPIServer struct { ProxyTransport http.RoundTripper KubernetesServiceNodePort int + + // Map storing information about all groups to be exposed in discovery response. + // The map is from name to the group. + apiGroupsForDiscovery map[string]unversioned.APIGroup } func (s *GenericAPIServer) StorageDecorator() generic.StorageDecorator { @@ -415,6 +420,7 @@ func New(c *Config) *GenericAPIServer { ExtraEndpointPorts: c.ExtraEndpointPorts, KubernetesServiceNodePort: c.KubernetesServiceNodePort, + apiGroupsForDiscovery: map[string]unversioned.APIGroup{}, } var handlerContainer *restful.Container @@ -543,6 +549,8 @@ func (s *GenericAPIServer) init(c *Config) { } else { s.InsecureHandler = handler } + + s.installGroupsDiscoveryHandler() } // Exposes the given group versions in API. @@ -555,6 +563,25 @@ func (s *GenericAPIServer) InstallAPIGroups(groupsInfo []APIGroupInfo) error { return nil } +// Installs handler at /apis to list all group versions for discovery +func (s *GenericAPIServer) installGroupsDiscoveryHandler() { + apiserver.AddApisWebService(s.Serializer, s.HandlerContainer, s.APIGroupPrefix, func() []unversioned.APIGroup { + // Return the list of supported groups in sorted order (to have a deterministic order). + groups := []unversioned.APIGroup{} + groupNames := make([]string, len(s.apiGroupsForDiscovery)) + var i int = 0 + for groupName := range s.apiGroupsForDiscovery { + groupNames[i] = groupName + i++ + } + sort.Strings(groupNames) + for _, groupName := range groupNames { + groups = append(groups, s.apiGroupsForDiscovery[groupName]) + } + return groups + }) +} + func (s *GenericAPIServer) Run(options *ServerRunOptions) { // We serve on 2 ports. See docs/accessing_the_api.md secureLocation := "" @@ -692,12 +719,21 @@ func (s *GenericAPIServer) installAPIGroup(apiGroupInfo *APIGroupInfo) error { Versions: apiVersionsForDiscovery, PreferredVersion: preferedVersionForDiscovery, } + s.AddAPIGroupForDiscovery(apiGroup) apiserver.AddGroupWebService(s.Serializer, s.HandlerContainer, apiPrefix+"/"+apiGroup.Name, apiGroup) } apiserver.InstallServiceErrorHandler(s.Serializer, s.HandlerContainer, s.NewRequestInfoResolver(), apiVersions) return nil } +func (s *GenericAPIServer) AddAPIGroupForDiscovery(apiGroup unversioned.APIGroup) { + s.apiGroupsForDiscovery[apiGroup.Name] = apiGroup +} + +func (s *GenericAPIServer) RemoveAPIGroupForDiscovery(groupName string) { + delete(s.apiGroupsForDiscovery, groupName) +} + func (s *GenericAPIServer) getAPIGroupVersion(apiGroupInfo *APIGroupInfo, groupVersion unversioned.GroupVersion, apiPrefix string) (*apiserver.APIGroupVersion, error) { storage := make(map[string]rest.Storage) for k, v := range apiGroupInfo.VersionedResourcesStorageMap[groupVersion.Version] { diff --git a/pkg/genericapiserver/genericapiserver_test.go b/pkg/genericapiserver/genericapiserver_test.go index be22c99e342..e21ddcc95a4 100644 --- a/pkg/genericapiserver/genericapiserver_test.go +++ b/pkg/genericapiserver/genericapiserver_test.go @@ -54,6 +54,7 @@ func TestNew(t *testing.T) { config.ProxyDialer = func(network, addr string) (net.Conn, error) { return nil, nil } config.ProxyTLSClientConfig = &tls.Config{} + config.Serializer = api.Codecs s := New(&config) diff --git a/pkg/master/master.go b/pkg/master/master.go index df7db628f53..9abb127d4f1 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -241,23 +241,6 @@ func (m *Master) InstallAPIs(c *Config) { if err := m.InstallAPIGroups(apiGroupsInfo); err != nil { glog.Fatalf("Error in registering group versions: %v", err) } - - // This should be done after all groups are registered - // TODO: replace the hardcoded "apis". - apiserver.AddApisWebService(m.Serializer, 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) { @@ -446,6 +429,7 @@ func (m *Master) removeThirdPartyStorage(path string) error { return err } delete(m.thirdPartyResources, path) + m.RemoveAPIGroupForDiscovery(getThirdPartyGroupName(path)) } return nil } @@ -500,6 +484,7 @@ func (m *Master) addThirdPartyResourceStorage(path string, storage *thirdpartyre m.thirdPartyResourcesLock.Lock() defer m.thirdPartyResourcesLock.Unlock() m.thirdPartyResources[path] = thirdPartyEntry{storage, apiGroup} + m.AddAPIGroupForDiscovery(apiGroup) } // InstallThirdPartyResource installs a third party resource specified by 'rsrc'. When a resource is diff --git a/pkg/master/master_test.go b/pkg/master/master_test.go index adb5fe1c971..af799f26f13 100644 --- a/pkg/master/master_test.go +++ b/pkg/master/master_test.go @@ -48,7 +48,6 @@ import ( etcdtesting "k8s.io/kubernetes/pkg/storage/etcd/testing" "k8s.io/kubernetes/pkg/util/intstr" - "github.com/emicklei/go-restful" "github.com/stretchr/testify/assert" "golang.org/x/net/context" ) @@ -85,6 +84,8 @@ func newMaster(t *testing.T) (*Master, *etcdtesting.EtcdTestServer, Config, *ass config.Serializer = api.Codecs config.KubeletClient = client.FakeKubeletClient{} + config.APIPrefix = "/api" + config.APIGroupPrefix = "/apis" config.ProxyDialer = func(network, addr string) (net.Conn, error) { return nil, nil } config.ProxyTLSClientConfig = &tls.Config{} @@ -269,30 +270,28 @@ func TestDiscoveryAtAPIS(t *testing.T) { t.Fatalf("unexpected error: %v", err) } - expectGroupName := extensions.GroupName - expectVersions := []unversioned.GroupVersionForDiscovery{ + extensionsGroupName := extensions.GroupName + extensionsVersions := []unversioned.GroupVersionForDiscovery{ { GroupVersion: testapi.Extensions.GroupVersion().String(), Version: testapi.Extensions.GroupVersion().Version, }, } - expectPreferredVersion := unversioned.GroupVersionForDiscovery{ + extensionsPreferredVersion := unversioned.GroupVersionForDiscovery{ GroupVersion: config.StorageVersions[extensions.GroupName], Version: apiutil.GetVersion(config.StorageVersions[extensions.GroupName]), } - assert.Equal(expectGroupName, groupList.Groups[0].Name) - assert.Equal(expectVersions, groupList.Groups[0].Versions) - assert.Equal(expectPreferredVersion, groupList.Groups[0].PreferredVersion) + assert.Equal(extensionsGroupName, groupList.Groups[0].Name) + assert.Equal(extensionsVersions, groupList.Groups[0].Versions) + assert.Equal(extensionsPreferredVersion, groupList.Groups[0].PreferredVersion) thirdPartyGV := unversioned.GroupVersionForDiscovery{GroupVersion: "company.com/v1", Version: "v1"} - master.thirdPartyResources["/apis/company.com/v1"] = thirdPartyEntry{ - nil, + master.addThirdPartyResourceStorage("/apis/company.com/v1", nil, unversioned.APIGroup{ Name: "company.com", Versions: []unversioned.GroupVersionForDiscovery{thirdPartyGV}, PreferredVersion: thirdPartyGV, - }, - } + }) resp, err = http.Get(server.URL + "/apis") if !assert.NoError(err) { @@ -309,10 +308,13 @@ func TestDiscoveryAtAPIS(t *testing.T) { 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) - + assert.Equal(2, len(groupList.Groups)) + assert.Equal(thirdPartyGroupName, groupList.Groups[0].Name) + assert.Equal(thirdPartyExpectVersions, groupList.Groups[0].Versions) + assert.Equal(thirdPartyGV, groupList.Groups[0].PreferredVersion) + assert.Equal(extensionsGroupName, groupList.Groups[1].Name) + assert.Equal(extensionsVersions, groupList.Groups[1].Versions) + assert.Equal(extensionsPreferredVersion, groupList.Groups[1].PreferredVersion) } var versionsToTest = []string{"v1", "v3"} @@ -333,9 +335,8 @@ type FooList struct { } func initThirdParty(t *testing.T, version string) (*Master, *etcdtesting.EtcdTestServer, *httptest.Server, *assert.Assertions) { - master, etcdserver, _, assert := setUp(t) + master, etcdserver, _, assert := newMaster(t) - master.thirdPartyResources = map[string]thirdPartyEntry{} api := &extensions.ThirdPartyResource{ ObjectMeta: api.ObjectMeta{ Name: "foo.company.com", @@ -347,7 +348,6 @@ func initThirdParty(t *testing.T, version string) (*Master, *etcdtesting.EtcdTes }, }, } - master.HandlerContainer = restful.NewContainer() master.thirdPartyStorage = etcdstorage.NewEtcdStorage(etcdserver.Client, testapi.Extensions.Codec(), etcdtest.PathPrefix(), false) if !assert.NoError(master.InstallThirdPartyResource(api)) { @@ -355,7 +355,7 @@ func initThirdParty(t *testing.T, version string) (*Master, *etcdtesting.EtcdTes } server := httptest.NewServer(master.HandlerContainer.ServeMux) - return &master, etcdserver, server, assert + return master, etcdserver, server, assert } func TestInstallThirdPartyAPIList(t *testing.T) { diff --git a/pkg/master/thirdparty_controller.go b/pkg/master/thirdparty_controller.go index 177f9467eba..15b44dfc452 100644 --- a/pkg/master/thirdparty_controller.go +++ b/pkg/master/thirdparty_controller.go @@ -37,6 +37,10 @@ func makeThirdPartyPath(group string) string { return thirdpartyprefix + "/" + group } +func getThirdPartyGroupName(path string) string { + return strings.TrimPrefix(strings.TrimPrefix(path, thirdpartyprefix), "/") +} + // resourceInterface is the interface for the parts of the master that know how to add/remove // third party resources. Extracted into an interface for injection for testing. type resourceInterface interface {