From 1278771b340acad9906bb6b3de18365cfc368db5 Mon Sep 17 00:00:00 2001 From: Chao Xu Date: Mon, 14 Sep 2015 20:55:18 -0700 Subject: [PATCH] let apiserver support api discovery --- api/swagger-spec/resourceListing.json | 4 ++ api/swagger-spec/v1.json | 19 ++++++ cmd/integration/integration.go | 4 ++ cmd/kube-apiserver/app/server.go | 31 ++++++--- pkg/api/unversioned.go | 56 ++++++++++++++-- pkg/apis/experimental/types.go | 1 + pkg/apiserver/api_installer.go | 63 ++++++++++-------- pkg/apiserver/apiserver.go | 77 ++++++++++++++++++++-- pkg/master/master.go | 46 +++++++++++-- pkg/master/master_test.go | 5 +- test/integration/auth_test.go | 8 +++ test/integration/framework/master_utils.go | 8 +++ test/integration/scheduler_test.go | 1 + test/integration/secret_test.go | 1 + test/integration/service_account_test.go | 1 + test/integration/utils.go | 1 + 16 files changed, 277 insertions(+), 49 deletions(-) diff --git a/api/swagger-spec/resourceListing.json b/api/swagger-spec/resourceListing.json index 0d748108587..055ea9c2011 100644 --- a/api/swagger-spec/resourceListing.json +++ b/api/swagger-spec/resourceListing.json @@ -9,6 +9,10 @@ "path": "/api", "description": "get available API versions" }, + { + "path": "/apis", + "description": "get available API versions" + }, { "path": "/version", "description": "git code version from which this is built" diff --git a/api/swagger-spec/v1.json b/api/swagger-spec/v1.json index 146bfb2eaf8..f38619e15b2 100644 --- a/api/swagger-spec/v1.json +++ b/api/swagger-spec/v1.json @@ -10950,6 +10950,25 @@ ] } ] + }, + { + "path": "/api/v1", + "description": "API at /api/v1 version v1", + "operations": [ + { + "type": "void", + "method": "GET", + "summary": "get available resources", + "nickname": "getAPIResources", + "parameters": [], + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ] + } + ] } ], "models": { diff --git a/cmd/integration/integration.go b/cmd/integration/integration.go index c3b9a4dd523..ddee019774f 100644 --- a/cmd/integration/integration.go +++ b/cmd/integration/integration.go @@ -132,11 +132,14 @@ func startComponents(firstManifestURL, secondManifestURL string) (string, string // We will fix this by supporting multiple group versions in Config cl.ExperimentalClient = client.NewExperimentalOrDie(&client.Config{Host: apiServer.URL, Version: testapi.Experimental.Version()}) + storageVersions := make(map[string]string) etcdStorage, err := master.NewEtcdStorage(etcdClient, latest.GroupOrDie("").InterfacesFor, testapi.Default.Version(), etcdtest.PathPrefix()) + storageVersions[""] = testapi.Default.Version() if err != nil { glog.Fatalf("Unable to get etcd storage: %v", err) } expEtcdStorage, err := master.NewEtcdStorage(etcdClient, latest.GroupOrDie("experimental").InterfacesFor, testapi.Experimental.Version(), etcdtest.PathPrefix()) + storageVersions["experimental"] = testapi.Experimental.Version() if err != nil { glog.Fatalf("Unable to get etcd storage for experimental: %v", err) } @@ -171,6 +174,7 @@ func startComponents(firstManifestURL, secondManifestURL string) (string, string ReadWritePort: portNumber, PublicAddress: publicAddress, CacheTimeout: 2 * time.Second, + StorageVersions: storageVersions, }) handler.delegate = m.Handler diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index 4f8c80330d3..759a3143d58 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -21,6 +21,7 @@ package app import ( "crypto/tls" + "fmt" "net" "net/http" "os" @@ -245,7 +246,10 @@ func (s *APIServer) verifyClusterIPFlags() { } } -func newEtcd(etcdConfigFile string, etcdServerList []string, interfacesFunc meta.VersionInterfacesFunc, defaultVersion, storageVersion, pathPrefix string) (etcdStorage storage.Interface, err error) { +func newEtcd(etcdConfigFile string, etcdServerList []string, interfacesFunc meta.VersionInterfacesFunc, storageVersion, pathPrefix string) (etcdStorage storage.Interface, err error) { + if storageVersion == "" { + return etcdStorage, fmt.Errorf("storageVersion is required to create a etcd storage") + } var client tools.EtcdClient if etcdConfigFile != "" { client, err = etcd.NewClientFromFile(etcdConfigFile) @@ -264,11 +268,8 @@ func newEtcd(etcdConfigFile string, etcdServerList []string, interfacesFunc meta etcdClient.SetTransport(transport) client = etcdClient } - - if storageVersion == "" { - storageVersion = defaultVersion - } - return master.NewEtcdStorage(client, interfacesFunc, storageVersion, pathPrefix) + etcdStorage, err = master.NewEtcdStorage(client, interfacesFunc, storageVersion, pathPrefix) + return etcdStorage, err } // Run runs the specified APIServer. This should never exit. @@ -340,7 +341,16 @@ func (s *APIServer) Run(_ []string) error { glog.Fatalf("Invalid server address: %v", err) } - etcdStorage, err := newEtcd(s.EtcdConfigFile, s.EtcdServerList, latest.GroupOrDie("").InterfacesFor, latest.GroupOrDie("").Version, s.StorageVersion, s.EtcdPathPrefix) + g, err := latest.Group("") + if err != nil { + return err + } + storageVersions := make(map[string]string) + if s.StorageVersion == "" { + s.StorageVersion = g.Version + } + etcdStorage, err := newEtcd(s.EtcdConfigFile, s.EtcdServerList, g.InterfacesFor, s.StorageVersion, s.EtcdPathPrefix) + storageVersions[""] = s.StorageVersion if err != nil { glog.Fatalf("Invalid storage version or misconfigured etcd: %v", err) } @@ -351,10 +361,14 @@ func (s *APIServer) Run(_ []string) error { if err != nil { glog.Fatalf("experimental API is enabled in runtime config, but not enabled in the environment variable KUBE_API_VERSIONS. Error: %v", err) } - expEtcdStorage, err = newEtcd(s.EtcdConfigFile, s.EtcdServerList, g.InterfacesFor, g.Version, s.ExpStorageVersion, s.EtcdPathPrefix) + if s.ExpStorageVersion == "" { + s.ExpStorageVersion = g.Version + } + expEtcdStorage, err = newEtcd(s.EtcdConfigFile, s.EtcdServerList, g.InterfacesFor, s.ExpStorageVersion, s.EtcdPathPrefix) if err != nil { glog.Fatalf("Invalid experimental storage version or misconfigured etcd: %v", err) } + storageVersions["experimental"] = s.StorageVersion } n := s.ServiceClusterIPRange @@ -426,6 +440,7 @@ func (s *APIServer) Run(_ []string) error { config := &master.Config{ DatabaseStorage: etcdStorage, ExpDatabaseStorage: expEtcdStorage, + StorageVersions: storageVersions, EventTTL: s.EventTTL, KubeletClient: kubeletClient, diff --git a/pkg/api/unversioned.go b/pkg/api/unversioned.go index cf0d809d99e..5e5d5844d3b 100644 --- a/pkg/api/unversioned.go +++ b/pkg/api/unversioned.go @@ -22,16 +22,64 @@ import ( // This file contains API types that are unversioned. -// APIVersions lists the api versions that are available, to allow -// version negotiation. APIVersions isn't just an unnamed array of -// strings in order to allow for future evolution, though unversioned +// APIVersions lists the versions that are available, to allow clients to +// discover the API at /api, which is the root path of the legacy v1 API. type APIVersions struct { + // versions are the api versions that are available. Versions []string `json:"versions"` } +// APIGroupList is a list of APIGroup, to allow clients to discover the API at +// /apis. +type APIGroupList struct { + // groups is a list of APIGroup. + Groups []APIGroup `json:"groups"` +} + +// APIGroup contains the name, the supported versions, and the preferred version +// of a group. +type APIGroup struct { + // name is the name of the group. + Name string `json:"name"` + // versions are the versions supported in this group. + Versions []GroupVersion `json:"versions"` + // preferredVersion is the version preferred by the API server, which + // probably is the storage version. + PreferredVersion GroupVersion `json:"preferredVersion,omitempty"` +} + +// GroupVersion contains the "group/version" and "version" string of a version. +// It is made a struct to keep extensiblity. +type GroupVersion struct { + // groupVersion specifies the API group and version in the form "group/version" + GroupVersion string `json:"groupVersion"` + // version specifies the version in the form of "version". This is to save + // the clients the trouble of splitting the GroupVersion. + Version string `json:"version"` +} + +// APIResource specifies the name of a resource and whether it is namespaced. +type APIResource struct { + // name is the name of the resource. + Name string `json:"name"` + // namespaced indicates if a resource is namespaced or not. + Namespaced bool `json:"namespaced"` +} + +// APIResourceList is a list of APIResource, it is used to expose the name of the +// resources supported in a specific group and version, and if the resource +// is namespaced. +type APIResourceList struct { + // groupVersion is the group and version this APIResourceList is for. + GroupVersion string `json:"groupVersion"` + // resources contains the name of the resources and if they are namespaced. + APIResources []APIResource `json:"resources"` +} + // RootPaths lists the paths available at root. -// For example: "/healthz", "/api". +// For example: "/healthz", "/apis". type RootPaths struct { + // paths are the paths available at root. Paths []string `json:"paths"` } diff --git a/pkg/apis/experimental/types.go b/pkg/apis/experimental/types.go index c73871fc522..760d0798bc9 100644 --- a/pkg/apis/experimental/types.go +++ b/pkg/apis/experimental/types.go @@ -168,6 +168,7 @@ type ThirdPartyResourceList struct { } // An APIVersion represents a single concrete version of an object model. +// TODO: we should consider merge this struct with GroupVersion in unversioned.go type APIVersion struct { // Name of this version (e.g. 'v1'). Name string `json:"name,omitempty"` diff --git a/pkg/apiserver/api_installer.go b/pkg/apiserver/api_installer.go index f45c830f93f..7b4a123d990 100644 --- a/pkg/apiserver/api_installer.go +++ b/pkg/apiserver/api_installer.go @@ -61,8 +61,8 @@ type documentable interface { var errEmptyName = errors.NewBadRequest("name must be provided") // Installs handlers for API resources. -func (a *APIInstaller) Install(ws *restful.WebService) []error { - errors := make([]error, 0) +func (a *APIInstaller) Install(ws *restful.WebService) (apiResources []api.APIResource, errors []error) { + errors = make([]error, 0) proxyHandler := (&ProxyHandler{a.prefix + "/proxy/", a.group.Storage, a.group.Codec, a.group.Context, a.info, a.proxyDialerFn}) @@ -75,11 +75,15 @@ func (a *APIInstaller) Install(ws *restful.WebService) []error { } sort.Strings(paths) for _, path := range paths { - if err := a.registerResourceHandlers(path, a.group.Storage[path], ws, proxyHandler); err != nil { + apiResource, err := a.registerResourceHandlers(path, a.group.Storage[path], ws, proxyHandler) + if err != nil { errors = append(errors, err) } + if apiResource != nil { + apiResources = append(apiResources, *apiResource) + } } - return errors + return apiResources, errors } // NewWebService creates a new restful webservice with the api installer's prefix and version. @@ -95,7 +99,7 @@ func (a *APIInstaller) NewWebService() *restful.WebService { return ws } -func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService, proxyHandler http.Handler) error { +func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService, proxyHandler http.Handler) (*api.APIResource, error) { admit := a.group.Admit context := a.group.Context @@ -112,40 +116,40 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag resource = parts[0] default: // TODO: support deeper paths - return fmt.Errorf("api_installer allows only one or two segment paths (resource or resource/subresource)") + return nil, fmt.Errorf("api_installer allows only one or two segment paths (resource or resource/subresource)") } hasSubresource := len(subresource) > 0 object := storage.New() _, kind, err := a.group.Typer.ObjectVersionAndKind(object) if err != nil { - return err + return nil, err } versionedPtr, err := a.group.Creater.New(a.group.Version, kind) if err != nil { - return err + return nil, err } versionedObject := indirectArbitraryPointer(versionedPtr) mapping, err := a.group.Mapper.RESTMapping(kind, a.group.Version) if err != nil { - return err + return nil, err } // subresources must have parent resources, and follow the namespacing rules of their parent if hasSubresource { parentStorage, ok := a.group.Storage[resource] if !ok { - return fmt.Errorf("subresources can only be declared when the parent is also registered: %s needs %s", path, resource) + return nil, fmt.Errorf("subresources can only be declared when the parent is also registered: %s needs %s", path, resource) } parentObject := parentStorage.New() _, parentKind, err := a.group.Typer.ObjectVersionAndKind(parentObject) if err != nil { - return err + return nil, err } parentMapping, err := a.group.Mapper.RESTMapping(parentKind, a.group.Version) if err != nil { - return err + return nil, err } mapping.Scope = parentMapping.Scope } @@ -178,14 +182,14 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag _, listKind, err := a.group.Typer.ObjectVersionAndKind(list) versionedListPtr, err := a.group.Creater.New(a.group.Version, listKind) if err != nil { - return err + return nil, err } versionedList = indirectArbitraryPointer(versionedListPtr) } versionedListOptions, err := a.group.Creater.New(serverVersion, "ListOptions") if err != nil { - return err + return nil, err } var versionedDeleterObject interface{} @@ -193,7 +197,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag case isGracefulDeleter: objectPtr, err := a.group.Creater.New(serverVersion, "DeleteOptions") if err != nil { - return err + return nil, err } versionedDeleterObject = indirectArbitraryPointer(objectPtr) isDeleter = true @@ -203,7 +207,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag versionedStatusPtr, err := a.group.Creater.New(serverVersion, "Status") if err != nil { - return err + return nil, err } versionedStatus := indirectArbitraryPointer(versionedStatusPtr) var ( @@ -217,11 +221,11 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag getOptions, getSubpath, getSubpathKey = getterWithOptions.NewGetOptions() _, getOptionsKind, err = a.group.Typer.ObjectVersionAndKind(getOptions) if err != nil { - return err + return nil, err } versionedGetOptions, err = a.group.Creater.New(serverVersion, getOptionsKind) if err != nil { - return err + return nil, err } isGetter = true } @@ -238,7 +242,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag if connectOptions != nil { _, connectOptionsKind, err = a.group.Typer.ObjectVersionAndKind(connectOptions) if err != nil { - return err + return nil, err } versionedConnectOptions, err = a.group.Creater.New(serverVersion, connectOptionsKind) } @@ -262,6 +266,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag params := []*restful.Parameter{} actions := []action{} + var apiResource api.APIResource // Get the list of actions for the given scope. switch scope.Name() { case meta.RESTScopeNameRoot: @@ -276,6 +281,8 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag resourcePath = itemPath resourceParams = nameParams } + apiResource.Name = path + apiResource.Namespaced = false namer := rootScopeNaming{scope, a.group.Linker, gpath.Join(a.prefix, itemPath)} // Handler for standard REST verbs (GET, PUT, POST and DELETE). @@ -314,6 +321,8 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag resourcePath = itemPath resourceParams = nameParams } + apiResource.Name = path + apiResource.Namespaced = true namer := scopeNaming{scope, a.group.Linker, gpath.Join(a.prefix, itemPath), false} actions = appendIf(actions, action{"LIST", resourcePath, resourceParams, namer}, isLister) @@ -344,7 +353,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag } break default: - return fmt.Errorf("unsupported restscope: %s", scope.Name()) + return nil, fmt.Errorf("unsupported restscope: %s", scope.Name()) } // Create Routes for the actions. @@ -404,7 +413,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Writes(versionedObject) if isGetterWithOptions { if err := addObjectParams(ws, route, versionedGetOptions); err != nil { - return err + return nil, err } } addParams(route, action.Params) @@ -423,7 +432,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Returns(http.StatusOK, "OK", versionedList). Writes(versionedList) if err := addObjectParams(ws, route, versionedListOptions); err != nil { - return err + return nil, err } switch { case isLister && isWatcher: @@ -529,7 +538,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Returns(http.StatusOK, "OK", watchjson.WatchEvent{}). Writes(watchjson.WatchEvent{}) if err := addObjectParams(ws, route, versionedListOptions); err != nil { - return err + return nil, err } addParams(route, action.Params) ws.Route(route) @@ -548,7 +557,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Returns(http.StatusOK, "OK", watchjson.WatchEvent{}). Writes(watchjson.WatchEvent{}) if err := addObjectParams(ws, route, versionedListOptions); err != nil { - return err + return nil, err } addParams(route, action.Params) ws.Route(route) @@ -576,18 +585,18 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Writes("string") if versionedConnectOptions != nil { if err := addObjectParams(ws, route, versionedConnectOptions); err != nil { - return err + return nil, err } } addParams(route, action.Params) ws.Route(route) } default: - return fmt.Errorf("unrecognized action verb: %s", action.Verb) + return nil, fmt.Errorf("unrecognized action verb: %s", action.Verb) } // Note: update GetAttribs() when adding a custom handler. } - return nil + return &apiResource, nil } // rootScopeNaming reads only names from a request and ignores namespaces. It implements ScopeNamer diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index f24eb28793d..6d3f92ebe4b 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -118,7 +118,9 @@ const ( func (g *APIGroupVersion) InstallREST(container *restful.Container) error { installer := g.newInstaller() ws := installer.NewWebService() - registrationErrors := installer.Install(ws) + apiResources, registrationErrors := installer.Install(ws) + // TODO: g.Version only contains "version" now, it will contain "group/version" in the near future. + AddSupportedResourcesWebService(ws, g.Version, apiResources) container.Add(ws) return errors.NewAggregate(registrationErrors) } @@ -141,8 +143,10 @@ func (g *APIGroupVersion) UpdateREST(container *restful.Container) error { if ws == nil { return apierrors.NewInternalError(fmt.Errorf("unable to find an existing webservice for prefix %s", installer.prefix)) } - - return errors.NewAggregate(installer.Install(ws)) + apiResources, registrationErrors := installer.Install(ws) + // TODO: g.Version only contains "version" now, it will contain "group/version" in the near future. + AddSupportedResourcesWebService(ws, g.Version, apiResources) + return errors.NewAggregate(registrationErrors) } // newInstaller is a helper to create the installer. Used by InstallREST and UpdateREST. @@ -232,7 +236,7 @@ func serviceErrorHandler(requestResolver *APIRequestInfoResolver, apiVersions [] errorJSON(apierrors.NewGenericServerResponse(serviceErr.Code, "", "", "", "", 0, false), codec, response.ResponseWriter) } -// Adds a service to return the supported api versions. +// Adds a service to return the supported api versions at the legacy /api. func AddApiWebService(container *restful.Container, apiPrefix string, versions []string) { // TODO: InstallREST should register each version automatically @@ -248,6 +252,46 @@ func AddApiWebService(container *restful.Container, apiPrefix string, versions [ container.Add(ws) } +// Adds a service to return the supported api versions at /apis. +func AddApisWebService(container *restful.Container, apiPrefix string, groups []api.APIGroup) { + rootAPIHandler := RootAPIHandler(groups) + ws := new(restful.WebService) + ws.Path(apiPrefix) + ws.Doc("get available API versions") + ws.Route(ws.GET("/").To(rootAPIHandler). + Doc("get available API versions"). + Operation("getAPIVersions"). + Produces(restful.MIME_JSON). + Consumes(restful.MIME_JSON)) + container.Add(ws) +} + +// Adds a service to return the supported versions, preferred version, and name +// of a group. E.g., a such web service will be registered at /apis/experimental. +func AddGroupWebService(container *restful.Container, path string, group api.APIGroup) { + groupHandler := GroupHandler(group) + ws := new(restful.WebService) + ws.Path(path) + ws.Doc("get information of a group") + ws.Route(ws.GET("/").To(groupHandler). + Doc("get information of a group"). + Operation("getAPIGroup"). + Produces(restful.MIME_JSON). + Consumes(restful.MIME_JSON)) + container.Add(ws) +} + +// Adds a service to return the supported resources, E.g., a such web service +// will be registered at /apis/experimental/v1. +func AddSupportedResourcesWebService(ws *restful.WebService, groupVersion string, apiResources []api.APIResource) { + resourceHandler := SupportedResourcesHandler(groupVersion, apiResources) + ws.Route(ws.GET("/").To(resourceHandler). + Doc("get available resources"). + Operation("getAPIResources"). + Produces(restful.MIME_JSON). + Consumes(restful.MIME_JSON)) +} + // handleVersion writes the server's version information. func handleVersion(req *restful.Request, resp *restful.Response) { // TODO: use restful's Response methods @@ -262,6 +306,31 @@ func APIVersionHandler(versions ...string) restful.RouteFunction { } } +// RootAPIHandler returns a handler which will list the provided groups and versions as available. +func RootAPIHandler(groups []api.APIGroup) restful.RouteFunction { + return func(req *restful.Request, resp *restful.Response) { + // TODO: use restful's Response methods + writeRawJSON(http.StatusOK, api.APIGroupList{Groups: groups}, resp.ResponseWriter) + } +} + +// GroupHandler returns a handler which will return the api.GroupAndVersion of +// the group. +func GroupHandler(group api.APIGroup) restful.RouteFunction { + return func(req *restful.Request, resp *restful.Response) { + // TODO: use restful's Response methods + writeRawJSON(http.StatusOK, group, resp.ResponseWriter) + } +} + +// SupportedResourcesHandler returns a handler which will list the provided resources as available. +func SupportedResourcesHandler(groupVersion string, apiResources []api.APIResource) restful.RouteFunction { + return func(req *restful.Request, resp *restful.Response) { + // TODO: use restful's Response methods + writeRawJSON(http.StatusOK, api.APIResourceList{GroupVersion: groupVersion, APIResources: apiResources}, resp.ResponseWriter) + } +} + // write renders a returned runtime.Object to the response as a stream or an encoded object. If the object // returned by the response implements rest.ResourceStreamer that interface will be used to render the // response. The Accept header and current API version will be passed in, and the output will be copied diff --git a/pkg/master/master.go b/pkg/master/master.go index e076014dd47..b133de797a5 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -100,9 +100,11 @@ const ( type Config struct { DatabaseStorage storage.Interface ExpDatabaseStorage storage.Interface - EventTTL time.Duration - NodeRegexp string - KubeletClient client.KubeletClient + // StorageVersions is a map between groups and their storage versions + StorageVersions map[string]string + EventTTL time.Duration + NodeRegexp string + KubeletClient client.KubeletClient // allow downstream consumers to disable the core controller loops EnableCoreControllers bool EnableLogsSupport bool @@ -570,16 +572,42 @@ func (m *Master) init(c *Config) { requestInfoResolver := &apiserver.APIRequestInfoResolver{APIPrefixes: sets.NewString(strings.TrimPrefix(defaultVersion.Root, "/")), RestMapper: defaultVersion.Mapper} apiserver.InstallServiceErrorHandler(m.handlerContainer, requestInfoResolver, apiVersions) + // allGroups records all supported groups at /apis + allGroups := []api.APIGroup{} if m.exp { expVersion := m.experimental(c) if err := expVersion.InstallREST(m.handlerContainer); err != nil { glog.Fatalf("Unable to setup experimental api: %v", err) } - apiserver.AddApiWebService(m.handlerContainer, c.APIGroupPrefix+"/"+latest.GroupOrDie("experimental").Group+"/", []string{expVersion.Version}) + g, err := latest.Group("experimental") + if err != nil { + glog.Fatalf("Unable to setup experimental api: %v", err) + } + expAPIVersions := []api.GroupVersion{ + { + GroupVersion: g.Group + "/" + expVersion.Version, + Version: expVersion.Version, + }, + } + storageVersion, found := c.StorageVersions[g.Group] + if !found { + glog.Fatalf("Couldn't find storage version of group %v", g.Group) + } + group := api.APIGroup{ + Name: g.Group, + Versions: expAPIVersions, + PreferredVersion: api.GroupVersion{GroupVersion: g.Group + "/" + storageVersion, Version: storageVersion}, + } + apiserver.AddGroupWebService(m.handlerContainer, c.APIGroupPrefix+"/"+latest.GroupOrDie("experimental").Group+"/", group) + allGroups = append(allGroups, group) expRequestInfoResolver := &apiserver.APIRequestInfoResolver{APIPrefixes: sets.NewString(strings.TrimPrefix(expVersion.Root, "/")), RestMapper: expVersion.Mapper} apiserver.InstallServiceErrorHandler(m.handlerContainer, expRequestInfoResolver, []string{expVersion.Version}) } + // This should be done after all groups are registered + // TODO: replace the hardcoded "apis". + apiserver.AddApisWebService(m.handlerContainer, "/apis", allGroups) + // Register root handler. // We do not register this using restful Webservice since we do not want to surface this in api docs. // Allow master to be embedded in contexts which already have something registered at the root @@ -784,7 +812,15 @@ func (m *Master) InstallThirdPartyAPI(rsrc *experimental.ThirdPartyResource) err glog.Fatalf("Unable to setup thirdparty api: %v", err) } thirdPartyAPIPrefix := makeThirdPartyPath(group) + "/" - apiserver.AddApiWebService(m.handlerContainer, thirdPartyAPIPrefix, []string{rsrc.Versions[0].Name}) + groupVersion := api.GroupVersion{ + GroupVersion: group + "/" + rsrc.Versions[0].Name, + Version: rsrc.Versions[0].Name, + } + apiGroup := api.APIGroup{ + Name: group, + Versions: []api.GroupVersion{groupVersion}, + } + apiserver.AddGroupWebService(m.handlerContainer, thirdPartyAPIPrefix, apiGroup) thirdPartyRequestInfoResolver := &apiserver.APIRequestInfoResolver{APIPrefixes: sets.NewString(strings.TrimPrefix(group, "/")), RestMapper: thirdparty.Mapper} apiserver.InstallServiceErrorHandler(m.handlerContainer, thirdPartyRequestInfoResolver, []string{thirdparty.Version}) return nil diff --git a/pkg/master/master_test.go b/pkg/master/master_test.go index 99390fcd55a..36c59ec7672 100644 --- a/pkg/master/master_test.go +++ b/pkg/master/master_test.go @@ -59,9 +59,12 @@ func setUp(t *testing.T) (Master, Config, *assert.Assertions) { config := Config{} fakeClient := tools.NewFakeEtcdClient(t) fakeClient.Machines = []string{"http://machine1:4001", "http://machine2", "http://machine3:4003"} + storageVersions := make(map[string]string) config.DatabaseStorage = etcdstorage.NewEtcdStorage(fakeClient, testapi.Default.Codec(), etcdtest.PathPrefix()) + storageVersions[""] = testapi.Default.Version() config.ExpDatabaseStorage = etcdstorage.NewEtcdStorage(fakeClient, testapi.Experimental.Codec(), etcdtest.PathPrefix()) - + storageVersions["experimental"] = testapi.Experimental.Version() + config.StorageVersions = storageVersions master.nodeRegistry = registrytest.NewNodeRegistry([]string{"node1", "node2"}, api.NodeResources{}) return master, config, assert.New(t) diff --git a/test/integration/auth_test.go b/test/integration/auth_test.go index 77898471c66..c6cc30b3ebf 100644 --- a/test/integration/auth_test.go +++ b/test/integration/auth_test.go @@ -406,6 +406,7 @@ func TestAuthModeAlwaysAllow(t *testing.T) { APIPrefix: "/api", Authorizer: apiserver.NewAlwaysAllowAuthorizer(), AdmissionControl: admit.NewAlwaysAdmit(), + StorageVersions: map[string]string{"": testapi.Default.Version()}, }) transport := http.DefaultTransport @@ -522,6 +523,7 @@ func TestAuthModeAlwaysDeny(t *testing.T) { APIPrefix: "/api", Authorizer: apiserver.NewAlwaysDenyAuthorizer(), AdmissionControl: admit.NewAlwaysAdmit(), + StorageVersions: map[string]string{"": testapi.Default.Version()}, }) transport := http.DefaultTransport @@ -590,6 +592,7 @@ func TestAliceNotForbiddenOrUnauthorized(t *testing.T) { Authenticator: getTestTokenAuth(), Authorizer: allowAliceAuthorizer{}, AdmissionControl: admit.NewAlwaysAdmit(), + StorageVersions: map[string]string{"": testapi.Default.Version()}, }) previousResourceVersion := make(map[string]float64) @@ -677,6 +680,7 @@ func TestBobIsForbidden(t *testing.T) { Authenticator: getTestTokenAuth(), Authorizer: allowAliceAuthorizer{}, AdmissionControl: admit.NewAlwaysAdmit(), + StorageVersions: map[string]string{"": testapi.Default.Version()}, }) transport := http.DefaultTransport @@ -738,6 +742,7 @@ func TestUnknownUserIsUnauthorized(t *testing.T) { Authenticator: getTestTokenAuth(), Authorizer: allowAliceAuthorizer{}, AdmissionControl: admit.NewAlwaysAdmit(), + StorageVersions: map[string]string{"": testapi.Default.Version()}, }) transport := http.DefaultTransport @@ -818,6 +823,7 @@ func TestNamespaceAuthorization(t *testing.T) { Authenticator: getTestTokenAuth(), Authorizer: a, AdmissionControl: admit.NewAlwaysAdmit(), + StorageVersions: map[string]string{"": testapi.Default.Version()}, }) previousResourceVersion := make(map[string]float64) @@ -933,6 +939,7 @@ func TestKindAuthorization(t *testing.T) { Authenticator: getTestTokenAuth(), Authorizer: a, AdmissionControl: admit.NewAlwaysAdmit(), + StorageVersions: map[string]string{"": testapi.Default.Version()}, }) previousResourceVersion := make(map[string]float64) @@ -1035,6 +1042,7 @@ func TestReadOnlyAuthorization(t *testing.T) { Authenticator: getTestTokenAuth(), Authorizer: a, AdmissionControl: admit.NewAlwaysAdmit(), + StorageVersions: map[string]string{"": testapi.Default.Version()}, }) transport := http.DefaultTransport diff --git a/test/integration/framework/master_utils.go b/test/integration/framework/master_utils.go index ad07615307f..604186c31e1 100644 --- a/test/integration/framework/master_utils.go +++ b/test/integration/framework/master_utils.go @@ -130,11 +130,14 @@ func startMasterOrDie(masterConfig *master.Config) (*master.Master, *httptest.Se var err error if masterConfig == nil { etcdClient := NewEtcdClient() + storageVersions := make(map[string]string) etcdStorage, err = master.NewEtcdStorage(etcdClient, latest.GroupOrDie("").InterfacesFor, latest.GroupOrDie("").Version, etcdtest.PathPrefix()) + storageVersions[""] = latest.GroupOrDie("").Version if err != nil { glog.Fatalf("Failed to create etcd storage for master %v", err) } expEtcdStorage, err := master.NewEtcdStorage(etcdClient, latest.GroupOrDie("experimental").InterfacesFor, latest.GroupOrDie("experimental").Version, etcdtest.PathPrefix()) + storageVersions["experimental"] = latest.GroupOrDie("experimental").Version if err != nil { glog.Fatalf("Failed to create etcd storage for master %v", err) } @@ -142,6 +145,7 @@ func startMasterOrDie(masterConfig *master.Config) (*master.Master, *httptest.Se masterConfig = &master.Config{ DatabaseStorage: etcdStorage, ExpDatabaseStorage: expEtcdStorage, + StorageVersions: storageVersions, KubeletClient: client.FakeKubeletClient{}, EnableExp: true, EnableLogsSupport: false, @@ -270,11 +274,14 @@ func StartPods(numPods int, host string, restClient *client.Client) error { // TODO: Merge this into startMasterOrDie. func RunAMaster(t *testing.T) (*master.Master, *httptest.Server) { etcdClient := NewEtcdClient() + storageVersions := make(map[string]string) etcdStorage, err := master.NewEtcdStorage(etcdClient, latest.GroupOrDie("").InterfacesFor, testapi.Default.Version(), etcdtest.PathPrefix()) + storageVersions[""] = testapi.Default.Version() if err != nil { t.Fatalf("unexpected error: %v", err) } expEtcdStorage, err := master.NewEtcdStorage(etcdClient, latest.GroupOrDie("experimental").InterfacesFor, latest.GroupOrDie("experimental").Version, etcdtest.PathPrefix()) + storageVersions["experimental"] = testapi.Experimental.Version() if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -291,6 +298,7 @@ func RunAMaster(t *testing.T) (*master.Master, *httptest.Server) { EnableExp: true, Authorizer: apiserver.NewAlwaysAllowAuthorizer(), AdmissionControl: admit.NewAlwaysAdmit(), + StorageVersions: storageVersions, }) s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { diff --git a/test/integration/scheduler_test.go b/test/integration/scheduler_test.go index a897a402547..48b24847695 100644 --- a/test/integration/scheduler_test.go +++ b/test/integration/scheduler_test.go @@ -75,6 +75,7 @@ func TestUnschedulableNodes(t *testing.T) { APIPrefix: "/api", Authorizer: apiserver.NewAlwaysAllowAuthorizer(), AdmissionControl: admit.NewAlwaysAdmit(), + StorageVersions: map[string]string{"": testapi.Default.Version()}, }) restClient := client.NewOrDie(&client.Config{Host: s.URL, Version: testapi.Default.Version()}) diff --git a/test/integration/secret_test.go b/test/integration/secret_test.go index 77191c61634..680a73e0c68 100644 --- a/test/integration/secret_test.go +++ b/test/integration/secret_test.go @@ -68,6 +68,7 @@ func TestSecrets(t *testing.T) { APIPrefix: "/api", Authorizer: apiserver.NewAlwaysAllowAuthorizer(), AdmissionControl: admit.NewAlwaysAdmit(), + StorageVersions: map[string]string{"": testapi.Default.Version()}, }) framework.DeleteAllEtcdKeys() diff --git a/test/integration/service_account_test.go b/test/integration/service_account_test.go index 05886332a8d..43602aea2d1 100644 --- a/test/integration/service_account_test.go +++ b/test/integration/service_account_test.go @@ -420,6 +420,7 @@ func startServiceAccountTestServer(t *testing.T) (*client.Client, client.Config, Authenticator: authenticator, Authorizer: authorizer, AdmissionControl: serviceAccountAdmission, + StorageVersions: map[string]string{"": testapi.Default.Version()}, }) // Start the service account and service account token controllers diff --git a/test/integration/utils.go b/test/integration/utils.go index e2e86721171..874cff321c1 100644 --- a/test/integration/utils.go +++ b/test/integration/utils.go @@ -81,6 +81,7 @@ func runAMaster(t *testing.T) (*master.Master, *httptest.Server) { APIPrefix: "/api", Authorizer: apiserver.NewAlwaysAllowAuthorizer(), AdmissionControl: admit.NewAlwaysAdmit(), + StorageVersions: map[string]string{"": testapi.Default.Version()}, }) s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {