update APIRequestInfo for APIGroup

This commit is contained in:
deads2k 2015-09-22 15:43:29 -04:00
parent 28d71418ca
commit dc8d0de70b
5 changed files with 134 additions and 73 deletions

View File

@ -78,6 +78,10 @@ type Mux interface {
type APIGroupVersion struct { type APIGroupVersion struct {
Storage map[string]rest.Storage 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 Root string
Version string Version string
@ -112,6 +116,38 @@ const (
MaxTimeoutSecs = 600 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. // 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 // It is expected that the provided path root prefix will serve all operations. Root MUST NOT end
// in a slash. // 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. // newInstaller is a helper to create the installer. Used by InstallREST and UpdateREST.
func (g *APIGroupVersion) newInstaller() *APIInstaller { func (g *APIGroupVersion) newInstaller() *APIInstaller {
info := &APIRequestInfoResolver{sets.NewString(strings.TrimPrefix(g.Root, "/")), g.Mapper}
prefix := path.Join(g.Root, g.Version) prefix := path.Join(g.Root, g.Version)
installer := &APIInstaller{ installer := &APIInstaller{
group: g, group: g,
info: info, info: g.GetAPIRequestInfoResolver(),
prefix: prefix, prefix: prefix,
minRequestTimeout: g.MinRequestTimeout, minRequestTimeout: g.MinRequestTimeout,
proxyDialerFn: g.ProxyDialerFn, proxyDialerFn: g.ProxyDialerFn,

View File

@ -86,3 +86,22 @@ func forbidden(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusForbidden) w.WriteHeader(http.StatusForbidden)
fmt.Fprintf(w, "Forbidden: %#v", req.RequestURI) 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
}

View File

@ -348,8 +348,8 @@ type requestAttributeGetter struct {
} }
// NewAttributeGetter returns an object which implements the RequestAttributeGetter interface. // NewAttributeGetter returns an object which implements the RequestAttributeGetter interface.
func NewRequestAttributeGetter(requestContextMapper api.RequestContextMapper, restMapper meta.RESTMapper, apiRoots ...string) RequestAttributeGetter { func NewRequestAttributeGetter(requestContextMapper api.RequestContextMapper, restMapper meta.RESTMapper, apiRoots []string, grouplessAPIRoots []string) RequestAttributeGetter {
return &requestAttributeGetter{requestContextMapper, &APIRequestInfoResolver{sets.NewString(apiRoots...), restMapper}} return &requestAttributeGetter{requestContextMapper, &APIRequestInfoResolver{sets.NewString(apiRoots...), sets.NewString(grouplessAPIRoots...), restMapper}}
} }
func (r *requestAttributeGetter) GetAttribs(req *http.Request) authorizer.Attributes { func (r *requestAttributeGetter) GetAttribs(req *http.Request) authorizer.Attributes {
@ -395,6 +395,8 @@ func WithAuthorizationCheck(handler http.Handler, getAttribs RequestAttributeGet
type APIRequestInfo struct { type APIRequestInfo struct {
// Verb is the kube verb associated with the request, not the http verb. This includes things like list and watch. // Verb is the kube verb associated with the request, not the http verb. This includes things like list and watch.
Verb string Verb string
APIPrefix string
APIGroup string
APIVersion string APIVersion string
Namespace string Namespace string
// Resource is the name of the resource being requested. This is not the kind. For example: pods // 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 { type APIRequestInfoResolver struct {
APIPrefixes sets.String APIPrefixes sets.String
RestMapper meta.RESTMapper 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 // 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 // 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: // Valid Inputs:
// Storage paths // Storage paths
// /namespaces // /apis/{api-group}/{version}/namespaces
// /namespaces/{namespace} // /api/{version}/namespaces
// /namespaces/{namespace}/{resource} // /api/{version}/namespaces/{namespace}
// /namespaces/{namespace}/{resource}/{resourceName} // /api/{version}/namespaces/{namespace}/{resource}
// /{resource} // /api/{version}/namespaces/{namespace}/{resource}/{resourceName}
// /{resource}/{resourceName} // /api/{version}/{resource}
// /api/{version}/{resource}/{resourceName}
// //
// Special verbs: // Special verbs:
// /proxy/{resource}/{resourceName} // /api/{version}/proxy/{resource}/{resourceName}
// /proxy/namespaces/{namespace}/{resource}/{resourceName} // /api/{version}/proxy/namespaces/{namespace}/{resource}/{resourceName}
// /redirect/namespaces/{namespace}/{resource}/{resourceName} // /api/{version}/redirect/namespaces/{namespace}/{resource}/{resourceName}
// /redirect/{resource}/{resourceName} // /api/{version}/redirect/{resource}/{resourceName}
// /watch/{resource} // /api/{version}/watch/{resource}
// /watch/namespaces/{namespace}/{resource} // /api/{version}/watch/namespaces/{namespace}/{resource}
//
// Fully qualified paths for above:
// /api/{version}/*
// /api/{version}/*
func (r *APIRequestInfoResolver) GetAPIRequestInfo(req *http.Request) (APIRequestInfo, error) { func (r *APIRequestInfoResolver) GetAPIRequestInfo(req *http.Request) (APIRequestInfo, error) {
requestInfo := APIRequestInfo{ requestInfo := APIRequestInfo{
Raw: splitPath(req.URL.Path), Raw: splitPath(req.URL.Path),
} }
currentParts := requestInfo.Raw currentParts := requestInfo.Raw
if len(currentParts) < 1 { if len(currentParts) < 3 {
return requestInfo, fmt.Errorf("Unable to determine kind and namespace from an empty URL path") 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() { if !r.APIPrefixes.Has(currentParts[0]) {
// handle input of form /api/{version}/* by adjusting special paths return requestInfo, &errAPIPrefixNotFound{currentParts[0]}
if currentParts[0] == currPrefix { }
if len(currentParts) > 1 { requestInfo.APIPrefix = currentParts[0]
requestInfo.APIVersion = currentParts[1] currentParts = currentParts[1:]
}
if len(currentParts) > 2 { if !r.GrouplessAPIPrefixes.Has(requestInfo.APIPrefix) {
currentParts = currentParts[2:] // one part (APIPrefix) has already been consumed, so this is actually "do we have four parts?"
} else { if len(currentParts) < 3 {
return requestInfo, fmt.Errorf("Unable to determine kind and namespace from url, %v", req.URL) 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}/* // handle input of form /{specialVerb}/*
if _, ok := specialVerbs[currentParts[0]]; ok { if _, ok := specialVerbs[currentParts[0]]; ok {
requestInfo.Verb = currentParts[0] if len(currentParts) < 2 {
return requestInfo, fmt.Errorf("unable to determine kind and namespace from url, %v", req.URL)
if len(currentParts) > 1 {
currentParts = currentParts[1:]
} else {
return requestInfo, fmt.Errorf("Unable to determine kind and namespace from url, %v", req.URL)
} }
requestInfo.Verb = currentParts[0]
currentParts = currentParts[1:]
} else { } else {
switch req.Method { switch req.Method {
case "POST": case "POST":

View File

@ -199,6 +199,8 @@ func TestGetAPIRequestInfo(t *testing.T) {
method string method string
url string url string
expectedVerb string expectedVerb string
expectedAPIPrefix string
expectedAPIGroup string
expectedAPIVersion string expectedAPIVersion string
expectedNamespace string expectedNamespace string
expectedResource string expectedResource string
@ -209,42 +211,38 @@ func TestGetAPIRequestInfo(t *testing.T) {
}{ }{
// resource paths // resource paths
{"GET", "/namespaces", "list", "", "", "namespaces", "", "Namespace", "", []string{"namespaces"}}, {"GET", "/api/v1/namespaces", "list", "api", "", "v1", "", "namespaces", "", "Namespace", "", []string{"namespaces"}},
{"GET", "/namespaces/other", "get", "", "other", "namespaces", "", "Namespace", "other", []string{"namespaces", "other"}}, {"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", "/api/v1/namespaces/other/pods", "list", "api", "", "v1", "other", "pods", "", "Pod", "", []string{"pods"}},
{"GET", "/namespaces/other/pods/foo", "get", "", "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}}, {"GET", "/api/v1/namespaces/other/pods/foo", "get", "api", "", "v1", "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/pods", "list", "", api.NamespaceAll, "pods", "", "Pod", "", []string{"pods"}}, {"GET", "/api/v1/pods", "list", "api", "", "v1", api.NamespaceAll, "pods", "", "Pod", "", []string{"pods"}},
{"GET", "/namespaces/other/pods/foo", "get", "", "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}}, {"GET", "/api/v1/namespaces/other/pods/foo", "get", "api", "", "v1", "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"}},
// special verbs // special verbs
{"GET", "/proxy/namespaces/other/pods/foo", "proxy", "", "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}}, {"GET", "/api/v1/proxy/namespaces/other/pods/foo", "proxy", "api", "", "v1", "other", "pods", "", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/redirect/namespaces/other/pods/foo", "redirect", "", "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", "/watch/pods", "watch", "", api.NamespaceAll, "pods", "", "Pod", "", []string{"pods"}}, {"GET", "/api/v1/watch/pods", "watch", "api", "", "v1", api.NamespaceAll, "pods", "", "Pod", "", []string{"pods"}},
{"GET", "/watch/namespaces/other/pods", "watch", "", "other", "pods", "", "Pod", "", []string{"pods"}}, {"GET", "/api/v1/watch/namespaces/other/pods", "watch", "api", "", "v1", "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"}},
// subresource identification // subresource identification
{"GET", "/namespaces/other/pods/foo/status", "get", "", "other", "pods", "status", "Pod", "foo", []string{"pods", "foo", "status"}}, {"GET", "/api/v1/namespaces/other/pods/foo/status", "get", "api", "", "v1", "other", "pods", "status", "Pod", "foo", []string{"pods", "foo", "status"}},
{"PUT", "/namespaces/other/finalize", "update", "", "other", "finalize", "", "", "", []string{"finalize"}}, {"PUT", "/api/v1/namespaces/other/finalize", "update", "api", "", "v1", "other", "finalize", "", "", "", []string{"finalize"}},
// verb identification // verb identification
{"PATCH", "/namespaces/other/pods/foo", "patch", "", "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", "/namespaces/other/pods/foo", "delete", "", "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 { for _, successCase := range successCases {
req, _ := http.NewRequest(successCase.method, successCase.url, nil) req, _ := http.NewRequest(successCase.method, successCase.url, nil)
@ -282,7 +280,10 @@ func TestGetAPIRequestInfo(t *testing.T) {
errorCases := map[string]string{ errorCases := map[string]string{
"no resource path": "/", "no resource path": "/",
"just apiversion": "/api/version/", "just apiversion": "/api/version/",
"just prefix, group, version": "/apis/group/version/",
"apiversion with no resource": "/api/version/", "apiversion with no resource": "/api/version/",
"bad prefix": "/badprefix/version/resource",
"missing api group": "/apis/version/resource",
} }
for k, v := range errorCases { for k, v := range errorCases {
req, err := http.NewRequest("GET", v, nil) req, err := http.NewRequest("GET", v, nil)

View File

@ -573,7 +573,7 @@ func (m *Master) init(c *Config) {
apiserver.InstallSupport(m.muxHelper, m.rootWebService, c.EnableProfiling, healthzChecks...) apiserver.InstallSupport(m.muxHelper, m.rootWebService, c.EnableProfiling, healthzChecks...)
apiserver.AddApiWebService(m.handlerContainer, c.APIPrefix, apiVersions) apiserver.AddApiWebService(m.handlerContainer, c.APIPrefix, apiVersions)
defaultVersion := m.defaultAPIGroupVersion() 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) apiserver.InstallServiceErrorHandler(m.handlerContainer, requestInfoResolver, apiVersions)
// allGroups records all supported groups at /apis // 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) apiserver.AddGroupWebService(m.handlerContainer, c.APIGroupPrefix+"/"+latest.GroupOrDie("experimental").Group+"/", group)
allGroups = append(allGroups, 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}) apiserver.InstallServiceErrorHandler(m.handlerContainer, expRequestInfoResolver, []string{expVersion.Version})
} }
@ -652,7 +652,10 @@ func (m *Master) init(c *Config) {
m.InsecureHandler = handler 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) handler = apiserver.WithAuthorizationCheck(handler, attributeGetter, m.authorizer)
// Install Authenticator // Install Authenticator
@ -918,7 +921,7 @@ func (m *Master) InstallThirdPartyResource(rsrc *expapi.ThirdPartyResource) erro
} }
apiserver.AddGroupWebService(m.handlerContainer, path, apiGroup) 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))
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}) apiserver.InstallServiceErrorHandler(m.handlerContainer, thirdPartyRequestInfoResolver, []string{thirdparty.Version})
return nil return nil
} }