diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 7ef887e1dda..50ab1d9ae2b 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -78,6 +78,10 @@ type Mux interface { type APIGroupVersion struct { Storage map[string]rest.Storage + // Root is the APIPrefix under which this is being served. It can also be the APIPrefix/APIGroup that is being served + // Since the APIGroup may not contain a '/', you can get the APIGroup by parsing from the last '/' + // TODO Currently, an APIPrefix with a '/' is not supported in conjunction with an empty APIGroup. This struct should + // be refactored to keep separate information separate to avoid this sort of problem in the future. Root string Version string @@ -112,6 +116,38 @@ const ( MaxTimeoutSecs = 600 ) +func (g *APIGroupVersion) GetAPIPrefix() string { + slashlessRoot := strings.Trim(g.Root, "/") + if lastSlashIndex := strings.LastIndex(slashlessRoot, "/"); lastSlashIndex != -1 { + return slashlessRoot[:lastSlashIndex] + } + + return slashlessRoot +} + +func (g *APIGroupVersion) GetAPIGroup() string { + slashlessRoot := strings.Trim(g.Root, "/") + if lastSlashIndex := strings.LastIndex(slashlessRoot, "/"); lastSlashIndex != -1 { + return slashlessRoot[lastSlashIndex:] + } + + return "" +} + +func (g *APIGroupVersion) GetAPIVersion() string { + return g.Version +} + +func (g *APIGroupVersion) GetAPIRequestInfoResolver() *APIRequestInfoResolver { + apiPrefix := g.GetAPIPrefix() + info := &APIRequestInfoResolver{sets.NewString(apiPrefix), sets.String{}, g.Mapper} + if len(g.GetAPIGroup()) == 0 { + info.GrouplessAPIPrefixes.Insert(apiPrefix) + } + + return info +} + // InstallREST registers the REST handlers (storage, watch, proxy and redirect) into a restful Container. // It is expected that the provided path root prefix will serve all operations. Root MUST NOT end // in a slash. @@ -151,12 +187,10 @@ func (g *APIGroupVersion) UpdateREST(container *restful.Container) error { // newInstaller is a helper to create the installer. Used by InstallREST and UpdateREST. func (g *APIGroupVersion) newInstaller() *APIInstaller { - info := &APIRequestInfoResolver{sets.NewString(strings.TrimPrefix(g.Root, "/")), g.Mapper} - prefix := path.Join(g.Root, g.Version) installer := &APIInstaller{ group: g, - info: info, + info: g.GetAPIRequestInfoResolver(), prefix: prefix, minRequestTimeout: g.MinRequestTimeout, proxyDialerFn: g.ProxyDialerFn, diff --git a/pkg/apiserver/errors.go b/pkg/apiserver/errors.go index 16cc8b1f173..a7c1fbdf328 100644 --- a/pkg/apiserver/errors.go +++ b/pkg/apiserver/errors.go @@ -86,3 +86,22 @@ func forbidden(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusForbidden) fmt.Fprintf(w, "Forbidden: %#v", req.RequestURI) } + +// errAPIPrefixNotFound indicates that a APIRequestInfo resolution failed because the request isn't under +// any known API prefixes +type errAPIPrefixNotFound struct { + SpecifiedPrefix string +} + +func (e *errAPIPrefixNotFound) Error() string { + return fmt.Sprintf("no valid API prefix found matching %v", e.SpecifiedPrefix) +} + +func IsAPIPrefixNotFound(err error) bool { + if err == nil { + return false + } + + _, ok := err.(*errAPIPrefixNotFound) + return ok +} diff --git a/pkg/apiserver/handlers.go b/pkg/apiserver/handlers.go index 8e4cefb124d..788795d2212 100644 --- a/pkg/apiserver/handlers.go +++ b/pkg/apiserver/handlers.go @@ -348,8 +348,8 @@ type requestAttributeGetter struct { } // NewAttributeGetter returns an object which implements the RequestAttributeGetter interface. -func NewRequestAttributeGetter(requestContextMapper api.RequestContextMapper, restMapper meta.RESTMapper, apiRoots ...string) RequestAttributeGetter { - return &requestAttributeGetter{requestContextMapper, &APIRequestInfoResolver{sets.NewString(apiRoots...), restMapper}} +func NewRequestAttributeGetter(requestContextMapper api.RequestContextMapper, restMapper meta.RESTMapper, apiRoots []string, grouplessAPIRoots []string) RequestAttributeGetter { + return &requestAttributeGetter{requestContextMapper, &APIRequestInfoResolver{sets.NewString(apiRoots...), sets.NewString(grouplessAPIRoots...), restMapper}} } func (r *requestAttributeGetter) GetAttribs(req *http.Request) authorizer.Attributes { @@ -395,6 +395,8 @@ func WithAuthorizationCheck(handler http.Handler, getAttribs RequestAttributeGet type APIRequestInfo struct { // Verb is the kube verb associated with the request, not the http verb. This includes things like list and watch. Verb string + APIPrefix string + APIGroup string APIVersion string Namespace string // Resource is the name of the resource being requested. This is not the kind. For example: pods @@ -415,66 +417,68 @@ type APIRequestInfo struct { } type APIRequestInfoResolver struct { - APIPrefixes sets.String - RestMapper meta.RESTMapper + APIPrefixes sets.String + GrouplessAPIPrefixes sets.String + RestMapper meta.RESTMapper } // TODO write an integration test against the swagger doc to test the APIRequestInfo and match up behavior to responses // GetAPIRequestInfo returns the information from the http request. If error is not nil, APIRequestInfo holds the information as best it is known before the failure // Valid Inputs: // Storage paths -// /namespaces -// /namespaces/{namespace} -// /namespaces/{namespace}/{resource} -// /namespaces/{namespace}/{resource}/{resourceName} -// /{resource} -// /{resource}/{resourceName} +// /apis/{api-group}/{version}/namespaces +// /api/{version}/namespaces +// /api/{version}/namespaces/{namespace} +// /api/{version}/namespaces/{namespace}/{resource} +// /api/{version}/namespaces/{namespace}/{resource}/{resourceName} +// /api/{version}/{resource} +// /api/{version}/{resource}/{resourceName} // // Special verbs: -// /proxy/{resource}/{resourceName} -// /proxy/namespaces/{namespace}/{resource}/{resourceName} -// /redirect/namespaces/{namespace}/{resource}/{resourceName} -// /redirect/{resource}/{resourceName} -// /watch/{resource} -// /watch/namespaces/{namespace}/{resource} -// -// Fully qualified paths for above: -// /api/{version}/* -// /api/{version}/* +// /api/{version}/proxy/{resource}/{resourceName} +// /api/{version}/proxy/namespaces/{namespace}/{resource}/{resourceName} +// /api/{version}/redirect/namespaces/{namespace}/{resource}/{resourceName} +// /api/{version}/redirect/{resource}/{resourceName} +// /api/{version}/watch/{resource} +// /api/{version}/watch/namespaces/{namespace}/{resource} func (r *APIRequestInfoResolver) GetAPIRequestInfo(req *http.Request) (APIRequestInfo, error) { requestInfo := APIRequestInfo{ Raw: splitPath(req.URL.Path), } currentParts := requestInfo.Raw - if len(currentParts) < 1 { - return requestInfo, fmt.Errorf("Unable to determine kind and namespace from an empty URL path") + if len(currentParts) < 3 { + return requestInfo, fmt.Errorf("a resource request must have a url with at least three parts, not %v", req.URL) } - for _, currPrefix := range r.APIPrefixes.List() { - // handle input of form /api/{version}/* by adjusting special paths - if currentParts[0] == currPrefix { - if len(currentParts) > 1 { - requestInfo.APIVersion = currentParts[1] - } + if !r.APIPrefixes.Has(currentParts[0]) { + return requestInfo, &errAPIPrefixNotFound{currentParts[0]} + } + requestInfo.APIPrefix = currentParts[0] + currentParts = currentParts[1:] - if len(currentParts) > 2 { - currentParts = currentParts[2:] - } else { - return requestInfo, fmt.Errorf("Unable to determine kind and namespace from url, %v", req.URL) - } + if !r.GrouplessAPIPrefixes.Has(requestInfo.APIPrefix) { + // one part (APIPrefix) has already been consumed, so this is actually "do we have four parts?" + if len(currentParts) < 3 { + return requestInfo, fmt.Errorf("a resource request with an API group must have a url with at least four parts, not %v", req.URL) } + + requestInfo.APIGroup = currentParts[0] + currentParts = currentParts[1:] } + requestInfo.APIVersion = currentParts[0] + currentParts = currentParts[1:] + // handle input of form /{specialVerb}/* if _, ok := specialVerbs[currentParts[0]]; ok { - requestInfo.Verb = currentParts[0] - - if len(currentParts) > 1 { - currentParts = currentParts[1:] - } else { - return requestInfo, fmt.Errorf("Unable to determine kind and namespace from url, %v", req.URL) + if len(currentParts) < 2 { + return requestInfo, fmt.Errorf("unable to determine kind and namespace from url, %v", req.URL) } + + requestInfo.Verb = currentParts[0] + currentParts = currentParts[1:] + } else { switch req.Method { case "POST": diff --git a/pkg/apiserver/handlers_test.go b/pkg/apiserver/handlers_test.go index b54e3cbc8c0..a0927757dd8 100644 --- a/pkg/apiserver/handlers_test.go +++ b/pkg/apiserver/handlers_test.go @@ -199,6 +199,8 @@ func TestGetAPIRequestInfo(t *testing.T) { method string url string expectedVerb string + expectedAPIPrefix string + expectedAPIGroup string expectedAPIVersion string expectedNamespace string expectedResource string @@ -209,42 +211,38 @@ func TestGetAPIRequestInfo(t *testing.T) { }{ // resource paths - {"GET", "/namespaces", "list", "", "", "namespaces", "", "Namespace", "", []string{"namespaces"}}, - {"GET", "/namespaces/other", "get", "", "other", "namespaces", "", "Namespace", "other", []string{"namespaces", "other"}}, + {"GET", "/api/v1/namespaces", "list", "api", "", "v1", "", "namespaces", "", "Namespace", "", []string{"namespaces"}}, + {"GET", "/api/v1/namespaces/other", "get", "api", "", "v1", "other", "namespaces", "", "Namespace", "other", []string{"namespaces", "other"}}, - {"GET", "/namespaces/other/pods", "list", "", "other", "pods", "", "Pod", "", []string{"pods"}}, - {"GET", "/namespaces/other/pods/foo", "get", "", "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}}, - {"GET", "/pods", "list", "", api.NamespaceAll, "pods", "", "Pod", "", []string{"pods"}}, - {"GET", "/namespaces/other/pods/foo", "get", "", "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}}, - {"GET", "/namespaces/other/pods", "list", "", "other", "pods", "", "Pod", "", []string{"pods"}}, + {"GET", "/api/v1/namespaces/other/pods", "list", "api", "", "v1", "other", "pods", "", "Pod", "", []string{"pods"}}, + {"GET", "/api/v1/namespaces/other/pods/foo", "get", "api", "", "v1", "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/api/v1/pods", "list", "api", "", "v1", api.NamespaceAll, "pods", "", "Pod", "", []string{"pods"}}, + {"GET", "/api/v1/namespaces/other/pods/foo", "get", "api", "", "v1", "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/api/v1/namespaces/other/pods", "list", "api", "", "v1", "other", "pods", "", "Pod", "", []string{"pods"}}, // special verbs - {"GET", "/proxy/namespaces/other/pods/foo", "proxy", "", "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}}, - {"GET", "/redirect/namespaces/other/pods/foo", "redirect", "", "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}}, - {"GET", "/watch/pods", "watch", "", api.NamespaceAll, "pods", "", "Pod", "", []string{"pods"}}, - {"GET", "/watch/namespaces/other/pods", "watch", "", "other", "pods", "", "Pod", "", []string{"pods"}}, - - // fully-qualified paths - {"GET", getPath("pods", "other", ""), "list", testapi.Default.Version(), "other", "pods", "", "Pod", "", []string{"pods"}}, - {"GET", getPath("pods", "other", "foo"), "get", testapi.Default.Version(), "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}}, - {"GET", getPath("pods", "", ""), "list", testapi.Default.Version(), api.NamespaceAll, "pods", "", "Pod", "", []string{"pods"}}, - {"POST", getPath("pods", "", ""), "create", testapi.Default.Version(), api.NamespaceAll, "pods", "", "Pod", "", []string{"pods"}}, - {"GET", getPath("pods", "", "foo"), "get", testapi.Default.Version(), api.NamespaceAll, "pods", "", "Pod", "foo", []string{"pods", "foo"}}, - {"GET", pathWithPrefix("proxy", "pods", "", "foo"), "proxy", testapi.Default.Version(), api.NamespaceAll, "pods", "", "Pod", "foo", []string{"pods", "foo"}}, - {"GET", pathWithPrefix("watch", "pods", "", ""), "watch", testapi.Default.Version(), api.NamespaceAll, "pods", "", "Pod", "", []string{"pods"}}, - {"GET", pathWithPrefix("redirect", "pods", "", ""), "redirect", testapi.Default.Version(), api.NamespaceAll, "pods", "", "Pod", "", []string{"pods"}}, - {"GET", pathWithPrefix("watch", "pods", "other", ""), "watch", testapi.Default.Version(), "other", "pods", "", "Pod", "", []string{"pods"}}, + {"GET", "/api/v1/proxy/namespaces/other/pods/foo", "proxy", "api", "", "v1", "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/api/v1/redirect/namespaces/other/pods/foo", "redirect", "api", "", "v1", "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}}, + {"GET", "/api/v1/watch/pods", "watch", "api", "", "v1", api.NamespaceAll, "pods", "", "Pod", "", []string{"pods"}}, + {"GET", "/api/v1/watch/namespaces/other/pods", "watch", "api", "", "v1", "other", "pods", "", "Pod", "", []string{"pods"}}, // subresource identification - {"GET", "/namespaces/other/pods/foo/status", "get", "", "other", "pods", "status", "Pod", "foo", []string{"pods", "foo", "status"}}, - {"PUT", "/namespaces/other/finalize", "update", "", "other", "finalize", "", "", "", []string{"finalize"}}, + {"GET", "/api/v1/namespaces/other/pods/foo/status", "get", "api", "", "v1", "other", "pods", "status", "Pod", "foo", []string{"pods", "foo", "status"}}, + {"PUT", "/api/v1/namespaces/other/finalize", "update", "api", "", "v1", "other", "finalize", "", "", "", []string{"finalize"}}, // verb identification - {"PATCH", "/namespaces/other/pods/foo", "patch", "", "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}}, - {"DELETE", "/namespaces/other/pods/foo", "delete", "", "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}}, + {"PATCH", "/api/v1/namespaces/other/pods/foo", "patch", "api", "", "v1", "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}}, + {"DELETE", "/api/v1/namespaces/other/pods/foo", "delete", "api", "", "v1", "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}}, + {"POST", "/api/v1/namespaces/other/pods", "create", "api", "", "v1", "other", "pods", "", "Pod", "", []string{"pods"}}, + + // api group identification + {"POST", "/apis/experimental/v1/namespaces/other/pods", "create", "api", "experimental", "v1", "other", "pods", "", "Pod", "", []string{"pods"}}, + + // api version identification + {"POST", "/apis/experimental/v1beta3/namespaces/other/pods", "create", "api", "experimental", "v1beta3", "other", "pods", "", "Pod", "", []string{"pods"}}, } - apiRequestInfoResolver := &APIRequestInfoResolver{sets.NewString("api"), testapi.Default.RESTMapper()} + apiRequestInfoResolver := &APIRequestInfoResolver{sets.NewString("api", "apis"), sets.NewString("api"), testapi.Default.RESTMapper()} for _, successCase := range successCases { req, _ := http.NewRequest(successCase.method, successCase.url, nil) @@ -282,7 +280,10 @@ func TestGetAPIRequestInfo(t *testing.T) { errorCases := map[string]string{ "no resource path": "/", "just apiversion": "/api/version/", + "just prefix, group, version": "/apis/group/version/", "apiversion with no resource": "/api/version/", + "bad prefix": "/badprefix/version/resource", + "missing api group": "/apis/version/resource", } for k, v := range errorCases { req, err := http.NewRequest("GET", v, nil) diff --git a/pkg/master/master.go b/pkg/master/master.go index 71d4afbf808..01e9088bf61 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -573,7 +573,7 @@ func (m *Master) init(c *Config) { apiserver.InstallSupport(m.muxHelper, m.rootWebService, c.EnableProfiling, healthzChecks...) apiserver.AddApiWebService(m.handlerContainer, c.APIPrefix, apiVersions) defaultVersion := m.defaultAPIGroupVersion() - requestInfoResolver := &apiserver.APIRequestInfoResolver{APIPrefixes: sets.NewString(strings.TrimPrefix(defaultVersion.Root, "/")), RestMapper: defaultVersion.Mapper} + requestInfoResolver := defaultVersion.GetAPIRequestInfoResolver() apiserver.InstallServiceErrorHandler(m.handlerContainer, requestInfoResolver, apiVersions) // allGroups records all supported groups at /apis @@ -608,7 +608,7 @@ func (m *Master) init(c *Config) { } 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} + expRequestInfoResolver := expVersion.GetAPIRequestInfoResolver() apiserver.InstallServiceErrorHandler(m.handlerContainer, expRequestInfoResolver, []string{expVersion.Version}) } @@ -652,7 +652,10 @@ func (m *Master) init(c *Config) { m.InsecureHandler = handler - attributeGetter := apiserver.NewRequestAttributeGetter(m.requestContextMapper, latest.GroupOrDie("").RESTMapper, "api") + attributeGetter := apiserver.NewRequestAttributeGetter(m.requestContextMapper, latest.GroupOrDie("").RESTMapper, + []string{strings.Trim(c.APIPrefix, "/"), strings.Trim(thirdpartyprefix, "/")}, // all possible API prefixes + []string{strings.Trim(c.APIPrefix, "/")}, // APIPrefixes that won't have groups (legacy) + ) handler = apiserver.WithAuthorizationCheck(handler, attributeGetter, m.authorizer) // Install Authenticator @@ -918,7 +921,7 @@ func (m *Master) InstallThirdPartyResource(rsrc *expapi.ThirdPartyResource) erro } apiserver.AddGroupWebService(m.handlerContainer, path, apiGroup) m.addThirdPartyResourceStorage(path, thirdparty.Storage[strings.ToLower(kind)+"s"].(*thirdpartyresourcedataetcd.REST)) - thirdPartyRequestInfoResolver := &apiserver.APIRequestInfoResolver{APIPrefixes: sets.NewString(strings.TrimPrefix(group, "/")), RestMapper: thirdparty.Mapper} + thirdPartyRequestInfoResolver := &apiserver.APIRequestInfoResolver{APIPrefixes: sets.NewString(strings.Trim(thirdpartyprefix, "/")), RestMapper: thirdparty.Mapper} apiserver.InstallServiceErrorHandler(m.handlerContainer, thirdPartyRequestInfoResolver, []string{thirdparty.Version}) return nil }