Merge pull request #3931 from deads2k/deads-pull-more-info-from-request

pull more complete information from request
This commit is contained in:
Brendan Burns 2015-02-04 12:23:20 -08:00
commit 550b98ebf4
21 changed files with 316 additions and 225 deletions

View File

@ -10,7 +10,7 @@ readonly port is not currently subject to authorization, but is planned to be
removed soon.) removed soon.)
The authorization check for any request compares attributes of the context of The authorization check for any request compares attributes of the context of
the request, (such as user, resource kind, and namespace) with access the request, (such as user, resource, and namespace) with access
policies. An API call must be allowed by some policy in order to proceed. policies. An API call must be allowed by some policy in order to proceed.
The following implementations are available, and are selected by flag: The following implementations are available, and are selected by flag:
@ -28,10 +28,10 @@ The following implementations are available, and are selected by flag:
A request has 4 attributes that can be considered for authorization: A request has 4 attributes that can be considered for authorization:
- user (the user-string which a user was authenticated as). - user (the user-string which a user was authenticated as).
- whether the request is readonly (GETs are readonly) - whether the request is readonly (GETs are readonly)
- what kind of object is being accessed - what resource is being accessed
- applies only to the API endpoints, such as - applies only to the API endpoints, such as
`/api/v1beta1/pods`. For miscelaneous endpoints, like `/version`, the `/api/v1beta1/pods`. For miscelaneous endpoints, like `/version`, the
kind is the empty string. resource is the empty string.
- the namespace of the object being access, or the empty string if the - the namespace of the object being access, or the empty string if the
endpoint does not support namespaced objects. endpoint does not support namespaced objects.
@ -49,7 +49,7 @@ Each line is a "policy object". A policy object is a map with the following pro
- `user`, type string; the user-string from `--token_auth_file` - `user`, type string; the user-string from `--token_auth_file`
- `readonly`, type boolean, when true, means that the policy only applies to GET - `readonly`, type boolean, when true, means that the policy only applies to GET
operations. operations.
- `kind`, type string; a kind of object, from an URL, such as `pods`. - `resource`, type string; a resource from an URL, such as `pods`.
- `namespace`, type string; a namespace string. - `namespace`, type string; a namespace string.
An unset property is the same as a property set to the zero value for its type (e.g. empty string, 0, false). An unset property is the same as a property set to the zero value for its type (e.g. empty string, 0, false).
@ -76,9 +76,9 @@ To permit an action Policy with an unset namespace applies regardless of namespa
### Examples ### Examples
1. Alice can do anything: `{"user":"alice"}` 1. Alice can do anything: `{"user":"alice"}`
2. Kubelet can read any pods: `{"user":"kubelet", "kind": "pods", "readonly": true}` 2. Kubelet can read any pods: `{"user":"kubelet", "resource": "pods", "readonly": true}`
3. Kubelet can read and write events: `{"user":"kubelet", "kind": "events"}` 3. Kubelet can read and write events: `{"user":"kubelet", "resource": "events"}`
4. Bob can just read pods in namespace "projectCaribou": `{"user":"bob", "kind": "pods", "readonly": true, "ns": "projectCaribou"}` 4. Bob can just read pods in namespace "projectCaribou": `{"user":"bob", "resource": "pods", "readonly": true, "ns": "projectCaribou"}`
[Complete file example](../pkg/auth/authorizer/abac/example_policy_file.jsonl) [Complete file example](../pkg/auth/authorizer/abac/example_policy_file.jsonl)

View File

@ -22,15 +22,15 @@ import (
type attributesRecord struct { type attributesRecord struct {
namespace string namespace string
kind string resource string
operation string operation string
object runtime.Object object runtime.Object
} }
func NewAttributesRecord(object runtime.Object, namespace, kind, operation string) Attributes { func NewAttributesRecord(object runtime.Object, namespace, resource, operation string) Attributes {
return &attributesRecord{ return &attributesRecord{
namespace: namespace, namespace: namespace,
kind: kind, resource: resource,
operation: operation, operation: operation,
object: object, object: object,
} }
@ -40,8 +40,8 @@ func (record *attributesRecord) GetNamespace() string {
return record.namespace return record.namespace
} }
func (record *attributesRecord) GetKind() string { func (record *attributesRecord) GetResource() string {
return record.kind return record.resource
} }
func (record *attributesRecord) GetOperation() string { func (record *attributesRecord) GetOperation() string {

View File

@ -24,7 +24,7 @@ import (
// that is used to make an admission decision. // that is used to make an admission decision.
type Attributes interface { type Attributes interface {
GetNamespace() string GetNamespace() string
GetKind() string GetResource() string
GetOperation() string GetOperation() string
GetObject() runtime.Object GetObject() runtime.Object
} }

View File

@ -49,13 +49,14 @@ func (a *APIInstaller) Install() (ws *restful.WebService, errors []error) {
// Initialize the custom handlers. // Initialize the custom handlers.
watchHandler := (&WatchHandler{ watchHandler := (&WatchHandler{
storage: a.restHandler.storage, storage: a.restHandler.storage,
codec: a.restHandler.codec, codec: a.restHandler.codec,
canonicalPrefix: a.restHandler.canonicalPrefix, canonicalPrefix: a.restHandler.canonicalPrefix,
selfLinker: a.restHandler.selfLinker, selfLinker: a.restHandler.selfLinker,
apiRequestInfoResolver: a.restHandler.apiRequestInfoResolver,
}) })
redirectHandler := (&RedirectHandler{a.restHandler.storage, a.restHandler.codec}) redirectHandler := (&RedirectHandler{a.restHandler.storage, a.restHandler.codec, a.restHandler.apiRequestInfoResolver})
proxyHandler := (&ProxyHandler{a.prefix + "/proxy/", a.restHandler.storage, a.restHandler.codec}) proxyHandler := (&ProxyHandler{a.prefix + "/proxy/", a.restHandler.storage, a.restHandler.codec, a.restHandler.apiRequestInfoResolver})
for path, storage := range a.restHandler.storage { for path, storage := range a.restHandler.storage {
if err := a.registerResourceHandlers(path, storage, ws, watchHandler, redirectHandler, proxyHandler); err != nil { if err := a.registerResourceHandlers(path, storage, ws, watchHandler, redirectHandler, proxyHandler); err != nil {

View File

@ -28,6 +28,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/admission" "github.com/GoogleCloudPlatform/kubernetes/pkg/admission"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/healthz" "github.com/GoogleCloudPlatform/kubernetes/pkg/healthz"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
@ -57,7 +58,7 @@ type defaultAPIServer struct {
// Note: This method is used only in tests. // Note: This method is used only in tests.
func Handle(storage map[string]RESTStorage, codec runtime.Codec, root string, version string, selfLinker runtime.SelfLinker, admissionControl admission.Interface, mapper meta.RESTMapper) http.Handler { func Handle(storage map[string]RESTStorage, codec runtime.Codec, root string, version string, selfLinker runtime.SelfLinker, admissionControl admission.Interface, mapper meta.RESTMapper) http.Handler {
prefix := root + "/" + version prefix := root + "/" + version
group := NewAPIGroupVersion(storage, codec, prefix, selfLinker, admissionControl, mapper) group := NewAPIGroupVersion(storage, codec, root, prefix, selfLinker, admissionControl, mapper)
container := restful.NewContainer() container := restful.NewContainer()
container.Router(restful.CurlyRouter{}) container.Router(restful.CurlyRouter{})
mux := container.ServeMux mux := container.ServeMux
@ -85,15 +86,16 @@ type APIGroupVersion struct {
// This is a helper method for registering multiple sets of REST handlers under different // This is a helper method for registering multiple sets of REST handlers under different
// prefixes onto a server. // prefixes onto a server.
// TODO: add multitype codec serialization // TODO: add multitype codec serialization
func NewAPIGroupVersion(storage map[string]RESTStorage, codec runtime.Codec, canonicalPrefix string, selfLinker runtime.SelfLinker, admissionControl admission.Interface, mapper meta.RESTMapper) *APIGroupVersion { func NewAPIGroupVersion(storage map[string]RESTStorage, codec runtime.Codec, apiRoot, canonicalPrefix string, selfLinker runtime.SelfLinker, admissionControl admission.Interface, mapper meta.RESTMapper) *APIGroupVersion {
return &APIGroupVersion{ return &APIGroupVersion{
handler: RESTHandler{ handler: RESTHandler{
storage: storage, storage: storage,
codec: codec, codec: codec,
canonicalPrefix: canonicalPrefix, canonicalPrefix: canonicalPrefix,
selfLinker: selfLinker, selfLinker: selfLinker,
ops: NewOperations(), ops: NewOperations(),
admissionControl: admissionControl, admissionControl: admissionControl,
apiRequestInfoResolver: &APIRequestInfoResolver{util.NewStringSet(apiRoot), latest.RESTMapper},
}, },
mapper: mapper, mapper: mapper,
} }

View File

@ -25,6 +25,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer" "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer"
authhandlers "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/handlers" authhandlers "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/handlers"
"github.com/GoogleCloudPlatform/kubernetes/pkg/httplog" "github.com/GoogleCloudPlatform/kubernetes/pkg/httplog"
@ -153,12 +154,13 @@ type RequestAttributeGetter interface {
} }
type requestAttributeGetter struct { type requestAttributeGetter struct {
userContexts authhandlers.RequestContext userContexts authhandlers.RequestContext
apiRequestInfoResolver *APIRequestInfoResolver
} }
// NewAttributeGetter returns an object which implements the RequestAttributeGetter interface. // NewAttributeGetter returns an object which implements the RequestAttributeGetter interface.
func NewRequestAttributeGetter(userContexts authhandlers.RequestContext) RequestAttributeGetter { func NewRequestAttributeGetter(userContexts authhandlers.RequestContext, restMapper meta.RESTMapper, apiRoots ...string) RequestAttributeGetter {
return &requestAttributeGetter{userContexts} return &requestAttributeGetter{userContexts, &APIRequestInfoResolver{util.NewStringSet(apiRoots...), restMapper}}
} }
func (r *requestAttributeGetter) GetAttribs(req *http.Request) authorizer.Attributes { func (r *requestAttributeGetter) GetAttribs(req *http.Request) authorizer.Attributes {
@ -171,16 +173,16 @@ func (r *requestAttributeGetter) GetAttribs(req *http.Request) authorizer.Attrib
attribs.ReadOnly = IsReadOnlyReq(*req) attribs.ReadOnly = IsReadOnlyReq(*req)
namespace, kind, _, _ := KindAndNamespace(req) apiRequestInfo, _ := r.apiRequestInfoResolver.GetAPIRequestInfo(req)
// If a path follows the conventions of the REST object store, then // If a path follows the conventions of the REST object store, then
// we can extract the object Kind. Otherwise, not. // we can extract the resource. Otherwise, not.
attribs.Kind = kind attribs.Resource = apiRequestInfo.Resource
// If the request specifies a namespace, then the namespace is filled in. // If the request specifies a namespace, then the namespace is filled in.
// Assumes there is no empty string namespace. Unspecified results // Assumes there is no empty string namespace. Unspecified results
// in empty (does not understand defaulting rules.) // in empty (does not understand defaulting rules.)
attribs.Namespace = namespace attribs.Namespace = apiRequestInfo.Namespace
return &attribs return &attribs
} }
@ -197,78 +199,136 @@ func WithAuthorizationCheck(handler http.Handler, getAttribs RequestAttributeGet
}) })
} }
// KindAndNamespace returns the kind, namespace, and path parts for the request relative to /{kind}/{name} // APIRequestInfo holds information parsed from the http.Request
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
APIVersion string
Namespace string
// Resource is the name of the resource being requested. This is not the kind. For example: pods
Resource string
// Kind is the type of object being manipulated. For example: Pod
Kind string
// Name is empty for some verbs, but if the request directly indicates a name (not in body content) then this field is filled in.
Name string
// Parts are the path parts for the request relative to /{resource}/{name}
Parts []string
}
type APIRequestInfoResolver struct {
apiPrefixes util.StringSet
restMapper meta.RESTMapper
}
// 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
// /ns/{namespace}/{kind} // /ns/{namespace}/{resource}
// /ns/{namespace}/{kind}/{resourceName} // /ns/{namespace}/{resource}/{resourceName}
// /{kind} // /{resource}
// /{kind}/{resourceName} // /{resource}/{resourceName}
// /{kind}/{resourceName}?namespace={namespace} // /{resource}/{resourceName}?namespace={namespace}
// /{kind}?namespace={namespace} // /{resource}?namespace={namespace}
// //
// Special verbs: // Special verbs:
// /proxy/{kind}/{resourceName} // /proxy/{resource}/{resourceName}
// /proxy/ns/{namespace}/{kind}/{resourceName} // /proxy/ns/{namespace}/{resource}/{resourceName}
// /redirect/ns/{namespace}/{kind}/{resourceName} // /redirect/ns/{namespace}/{resource}/{resourceName}
// /redirect/{kind}/{resourceName} // /redirect/{resource}/{resourceName}
// /watch/{kind} // /watch/{resource}
// /watch/ns/{namespace}/{kind} // /watch/ns/{namespace}/{resource}
// //
// Fully qualified paths for above: // Fully qualified paths for above:
// /api/{version}/* // /api/{version}/*
// /api/{version}/* // /api/{version}/*
func KindAndNamespace(req *http.Request) (namespace, kind string, parts []string, err error) { func (r *APIRequestInfoResolver) GetAPIRequestInfo(req *http.Request) (APIRequestInfo, error) {
parts = splitPath(req.URL.Path) requestInfo := APIRequestInfo{}
if len(parts) < 1 {
err = fmt.Errorf("Unable to determine kind and namespace from an empty URL path") currentParts := splitPath(req.URL.Path)
return if len(currentParts) < 1 {
return requestInfo, fmt.Errorf("Unable to determine kind and namespace from an empty URL path")
} }
// handle input of form /api/{version}/* by adjusting special paths for _, currPrefix := range r.apiPrefixes.List() {
if parts[0] == "api" { // handle input of form /api/{version}/* by adjusting special paths
if len(parts) > 2 { if currentParts[0] == currPrefix {
parts = parts[2:] if len(currentParts) > 1 {
} else { requestInfo.APIVersion = currentParts[1]
err = fmt.Errorf("Unable to determine kind and namespace from url, %v", req.URL) }
return
if len(currentParts) > 2 {
currentParts = currentParts[2:]
} else {
return requestInfo, fmt.Errorf("Unable to determine kind and namespace from url, %v", req.URL)
}
} }
} }
// handle input of form /{specialVerb}/* // handle input of form /{specialVerb}/*
if _, ok := specialVerbs[parts[0]]; ok { if _, ok := specialVerbs[currentParts[0]]; ok {
if len(parts) > 1 { requestInfo.Verb = currentParts[0]
parts = parts[1:]
if len(currentParts) > 1 {
currentParts = currentParts[1:]
} else { } else {
err = fmt.Errorf("Unable to determine kind and namespace from url, %v", req.URL) return requestInfo, fmt.Errorf("Unable to determine kind and namespace from url, %v", req.URL)
return }
} else {
switch req.Method {
case "POST":
requestInfo.Verb = "create"
case "GET":
requestInfo.Verb = "get"
case "PUT":
requestInfo.Verb = "update"
case "DELETE":
requestInfo.Verb = "delete"
}
}
// URL forms: /ns/{namespace}/{resource}/*, where parts are adjusted to be relative to kind
if currentParts[0] == "ns" {
if len(currentParts) < 3 {
return requestInfo, fmt.Errorf("ResourceTypeAndNamespace expects a path of form /ns/{namespace}/*")
}
requestInfo.Resource = currentParts[2]
requestInfo.Namespace = currentParts[1]
currentParts = currentParts[2:]
} else {
// URL forms: /{resource}/*
// URL forms: POST /{resource} is a legacy API convention to create in "default" namespace
// URL forms: /{resource}/{resourceName} use the "default" namespace if omitted from query param
// URL forms: /{resource} assume cross-namespace operation if omitted from query param
requestInfo.Resource = currentParts[0]
requestInfo.Namespace = req.URL.Query().Get("namespace")
if len(requestInfo.Namespace) == 0 {
if len(currentParts) > 1 || req.Method == "POST" {
requestInfo.Namespace = api.NamespaceDefault
} else {
requestInfo.Namespace = api.NamespaceAll
}
} }
} }
// URL forms: /ns/{namespace}/{kind}/*, where parts are adjusted to be relative to kind // parsing successful, so we now know the proper value for .Parts
if parts[0] == "ns" { requestInfo.Parts = currentParts
if len(parts) < 3 {
err = fmt.Errorf("ResourceTypeAndNamespace expects a path of form /ns/{namespace}/*") // if there's another part remaining after the kind, then that's the resource name
return if len(requestInfo.Parts) >= 2 {
} requestInfo.Name = requestInfo.Parts[1]
namespace = parts[1]
kind = parts[2]
parts = parts[2:]
return
} }
// URL forms: /{kind}/* // if there's no name on the request and we thought it was a get before, then the actual verb is a list
// URL forms: POST /{kind} is a legacy API convention to create in "default" namespace if len(requestInfo.Name) == 0 && requestInfo.Verb == "get" {
// URL forms: /{kind}/{resourceName} use the "default" namespace if omitted from query param requestInfo.Verb = "list"
// URL forms: /{kind} assume cross-namespace operation if omitted from query param
kind = parts[0]
namespace = req.URL.Query().Get("namespace")
if len(namespace) == 0 {
if len(parts) > 1 || req.Method == "POST" {
namespace = api.NamespaceDefault
} else {
namespace = api.NamespaceAll
}
} }
return
// if we have a resource, we have a good shot at being able to determine kind
if len(requestInfo.Resource) > 0 {
_, requestInfo.Kind, _ = r.restMapper.VersionAndKindForResource(requestInfo.Resource)
}
return requestInfo, nil
} }

View File

@ -23,6 +23,8 @@ import (
"testing" "testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
) )
type fakeRL bool type fakeRL bool
@ -63,59 +65,78 @@ func TestReadOnly(t *testing.T) {
} }
} }
func TestKindAndNamespace(t *testing.T) { func TestGetAPIRequestInfo(t *testing.T) {
successCases := []struct { successCases := []struct {
method string method string
url string url string
expectedNamespace string expectedVerb string
expectedKind string expectedAPIVersion string
expectedParts []string expectedNamespace string
expectedResource string
expectedKind string
expectedName string
expectedParts []string
}{ }{
// resource paths // resource paths
{"GET", "/ns/other/pods", "other", "pods", []string{"pods"}}, {"GET", "/ns/other/pods", "list", "", "other", "pods", "Pod", "", []string{"pods"}},
{"GET", "/ns/other/pods/foo", "other", "pods", []string{"pods", "foo"}}, {"GET", "/ns/other/pods/foo", "get", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/pods", api.NamespaceAll, "pods", []string{"pods"}}, {"GET", "/pods", "list", "", api.NamespaceAll, "pods", "Pod", "", []string{"pods"}},
{"POST", "/pods", api.NamespaceDefault, "pods", []string{"pods"}}, {"POST", "/pods", "create", "", api.NamespaceDefault, "pods", "Pod", "", []string{"pods"}},
{"GET", "/pods/foo", api.NamespaceDefault, "pods", []string{"pods", "foo"}}, {"GET", "/pods/foo", "get", "", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/pods/foo?namespace=other", "other", "pods", []string{"pods", "foo"}}, {"GET", "/pods/foo?namespace=other", "get", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/pods?namespace=other", "other", "pods", []string{"pods"}}, {"GET", "/pods?namespace=other", "list", "", "other", "pods", "Pod", "", []string{"pods"}},
// special verbs // special verbs
{"GET", "/proxy/ns/other/pods/foo", "other", "pods", []string{"pods", "foo"}}, {"GET", "/proxy/ns/other/pods/foo", "proxy", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/proxy/pods/foo", api.NamespaceDefault, "pods", []string{"pods", "foo"}}, {"GET", "/proxy/pods/foo", "proxy", "", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/redirect/ns/other/pods/foo", "other", "pods", []string{"pods", "foo"}}, {"GET", "/redirect/ns/other/pods/foo", "redirect", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/redirect/pods/foo", api.NamespaceDefault, "pods", []string{"pods", "foo"}}, {"GET", "/redirect/pods/foo", "redirect", "", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/watch/pods", api.NamespaceAll, "pods", []string{"pods"}}, {"GET", "/watch/pods", "watch", "", api.NamespaceAll, "pods", "Pod", "", []string{"pods"}},
{"GET", "/watch/ns/other/pods", "other", "pods", []string{"pods"}}, {"GET", "/watch/ns/other/pods", "watch", "", "other", "pods", "Pod", "", []string{"pods"}},
// fully-qualified paths // fully-qualified paths
{"GET", "/api/v1beta1/ns/other/pods", "other", "pods", []string{"pods"}}, {"GET", "/api/v1beta1/ns/other/pods", "list", "v1beta1", "other", "pods", "Pod", "", []string{"pods"}},
{"GET", "/api/v1beta1/ns/other/pods/foo", "other", "pods", []string{"pods", "foo"}}, {"GET", "/api/v1beta1/ns/other/pods/foo", "get", "v1beta1", "other", "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/api/v1beta1/pods", api.NamespaceAll, "pods", []string{"pods"}}, {"GET", "/api/v1beta1/pods", "list", "v1beta1", api.NamespaceAll, "pods", "Pod", "", []string{"pods"}},
{"POST", "/api/v1beta1/pods", api.NamespaceDefault, "pods", []string{"pods"}}, {"POST", "/api/v1beta1/pods", "create", "v1beta1", api.NamespaceDefault, "pods", "Pod", "", []string{"pods"}},
{"GET", "/api/v1beta1/pods/foo", api.NamespaceDefault, "pods", []string{"pods", "foo"}}, {"GET", "/api/v1beta1/pods/foo", "get", "v1beta1", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/api/v1beta1/pods/foo?namespace=other", "other", "pods", []string{"pods", "foo"}}, {"GET", "/api/v1beta1/pods/foo?namespace=other", "get", "v1beta1", "other", "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/api/v1beta1/pods?namespace=other", "other", "pods", []string{"pods"}}, {"GET", "/api/v1beta1/pods?namespace=other", "list", "v1beta1", "other", "pods", "Pod", "", []string{"pods"}},
{"GET", "/api/v1beta1/proxy/pods/foo", api.NamespaceDefault, "pods", []string{"pods", "foo"}}, {"GET", "/api/v1beta1/proxy/pods/foo", "proxy", "v1beta1", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/api/v1beta1/redirect/pods/foo", api.NamespaceDefault, "pods", []string{"pods", "foo"}}, {"GET", "/api/v1beta1/redirect/pods/foo", "redirect", "v1beta1", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/api/v1beta1/watch/pods", api.NamespaceAll, "pods", []string{"pods"}}, {"GET", "/api/v1beta1/watch/pods", "watch", "v1beta1", api.NamespaceAll, "pods", "Pod", "", []string{"pods"}},
{"GET", "/api/v1beta1/watch/ns/other/pods", "other", "pods", []string{"pods"}}, {"GET", "/api/v1beta1/watch/ns/other/pods", "watch", "v1beta1", "other", "pods", "Pod", "", []string{"pods"}},
} }
apiRequestInfoResolver := &APIRequestInfoResolver{util.NewStringSet("api"), latest.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)
namespace, kind, parts, err := KindAndNamespace(req)
apiRequestInfo, err := apiRequestInfoResolver.GetAPIRequestInfo(req)
if err != nil { if err != nil {
t.Errorf("Unexpected error for url: %s", successCase.url) t.Errorf("Unexpected error for url: %s %v", successCase.url, err)
} }
if successCase.expectedNamespace != namespace { if successCase.expectedVerb != apiRequestInfo.Verb {
t.Errorf("Unexpected namespace for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedNamespace, namespace) t.Errorf("Unexpected verb for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedVerb, apiRequestInfo.Verb)
} }
if successCase.expectedKind != kind { if successCase.expectedAPIVersion != apiRequestInfo.APIVersion {
t.Errorf("Unexpected resourceType for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedKind, kind) t.Errorf("Unexpected apiVersion for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedAPIVersion, apiRequestInfo.APIVersion)
} }
if !reflect.DeepEqual(successCase.expectedParts, parts) { if successCase.expectedNamespace != apiRequestInfo.Namespace {
t.Errorf("Unexpected parts for url: %s, expected: %v, actual: %v", successCase.url, successCase.expectedParts, parts) t.Errorf("Unexpected namespace for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedNamespace, apiRequestInfo.Namespace)
}
if successCase.expectedKind != apiRequestInfo.Kind {
t.Errorf("Unexpected kind for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedKind, apiRequestInfo.Kind)
}
if successCase.expectedResource != apiRequestInfo.Resource {
t.Errorf("Unexpected resource for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedResource, apiRequestInfo.Resource)
}
if successCase.expectedName != apiRequestInfo.Name {
t.Errorf("Unexpected name for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedName, apiRequestInfo.Name)
}
if !reflect.DeepEqual(successCase.expectedParts, apiRequestInfo.Parts) {
t.Errorf("Unexpected parts for url: %s, expected: %v, actual: %v", successCase.url, successCase.expectedParts, apiRequestInfo.Parts)
} }
} }
@ -131,7 +152,7 @@ func TestKindAndNamespace(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("Unexpected error %v", err) t.Errorf("Unexpected error %v", err)
} }
_, _, _, err = KindAndNamespace(req) _, err = apiRequestInfoResolver.GetAPIRequestInfo(req)
if err == nil { if err == nil {
t.Errorf("Expected error for key: %s", k) t.Errorf("Expected error for key: %s", k)
} }

View File

@ -75,17 +75,20 @@ var tagsToAttrs = map[string]util.StringSet{
// ProxyHandler provides a http.Handler which will proxy traffic to locations // ProxyHandler provides a http.Handler which will proxy traffic to locations
// specified by items implementing Redirector. // specified by items implementing Redirector.
type ProxyHandler struct { type ProxyHandler struct {
prefix string prefix string
storage map[string]RESTStorage storage map[string]RESTStorage
codec runtime.Codec codec runtime.Codec
apiRequestInfoResolver *APIRequestInfoResolver
} }
func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
namespace, kind, parts, err := KindAndNamespace(req) requestInfo, err := r.apiRequestInfoResolver.GetAPIRequestInfo(req)
if err != nil { if err != nil {
notFound(w, req) notFound(w, req)
return return
} }
namespace, resource, parts := requestInfo.Namespace, requestInfo.Resource, requestInfo.Parts
ctx := api.WithNamespace(api.NewContext(), namespace) ctx := api.WithNamespace(api.NewContext(), namespace)
if len(parts) < 2 { if len(parts) < 2 {
notFound(w, req) notFound(w, req)
@ -103,17 +106,17 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
rest = rest + "/" rest = rest + "/"
} }
} }
storage, ok := r.storage[kind] storage, ok := r.storage[resource]
if !ok { if !ok {
httplog.LogOf(req, w).Addf("'%v' has no storage object", kind) httplog.LogOf(req, w).Addf("'%v' has no storage object", resource)
notFound(w, req) notFound(w, req)
return return
} }
redirector, ok := storage.(Redirector) redirector, ok := storage.(Redirector)
if !ok { if !ok {
httplog.LogOf(req, w).Addf("'%v' is not a redirector", kind) httplog.LogOf(req, w).Addf("'%v' is not a redirector", resource)
errorJSON(errors.NewMethodNotSupported(kind, "proxy"), r.codec, w) errorJSON(errors.NewMethodNotSupported(resource, "proxy"), r.codec, w)
return return
} }
@ -156,7 +159,7 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
proxy.Transport = &proxyTransport{ proxy.Transport = &proxyTransport{
proxyScheme: req.URL.Scheme, proxyScheme: req.URL.Scheme,
proxyHost: req.URL.Host, proxyHost: req.URL.Host,
proxyPathPrepend: path.Join(r.prefix, "ns", namespace, kind, id), proxyPathPrepend: path.Join(r.prefix, "ns", namespace, resource, id),
} }
proxy.FlushInterval = 200 * time.Millisecond proxy.FlushInterval = 200 * time.Millisecond
proxy.ServeHTTP(w, newReq) proxy.ServeHTTP(w, newReq)

View File

@ -26,35 +26,37 @@ import (
) )
type RedirectHandler struct { type RedirectHandler struct {
storage map[string]RESTStorage storage map[string]RESTStorage
codec runtime.Codec codec runtime.Codec
apiRequestInfoResolver *APIRequestInfoResolver
} }
func (r *RedirectHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (r *RedirectHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
namespace, kind, parts, err := KindAndNamespace(req) requestInfo, err := r.apiRequestInfoResolver.GetAPIRequestInfo(req)
if err != nil { if err != nil {
notFound(w, req) notFound(w, req)
return return
} }
ctx := api.WithNamespace(api.NewContext(), namespace) resource, parts := requestInfo.Resource, requestInfo.Parts
ctx := api.WithNamespace(api.NewContext(), requestInfo.Namespace)
// redirection requires /kind/resourceName path parts // redirection requires /resource/resourceName path parts
if len(parts) != 2 || req.Method != "GET" { if len(parts) != 2 || req.Method != "GET" {
notFound(w, req) notFound(w, req)
return return
} }
id := parts[1] id := parts[1]
storage, ok := r.storage[kind] storage, ok := r.storage[resource]
if !ok { if !ok {
httplog.LogOf(req, w).Addf("'%v' has no storage object", kind) httplog.LogOf(req, w).Addf("'%v' has no storage object", resource)
notFound(w, req) notFound(w, req)
return return
} }
redirector, ok := storage.(Redirector) redirector, ok := storage.(Redirector)
if !ok { if !ok {
httplog.LogOf(req, w).Addf("'%v' is not a redirector", kind) httplog.LogOf(req, w).Addf("'%v' is not a redirector", resource)
errorJSON(errors.NewMethodNotSupported(kind, "redirect"), r.codec, w) errorJSON(errors.NewMethodNotSupported(resource, "redirect"), r.codec, w)
return return
} }

View File

@ -32,28 +32,29 @@ import (
// RESTHandler implements HTTP verbs on a set of RESTful resources identified by name. // RESTHandler implements HTTP verbs on a set of RESTful resources identified by name.
type RESTHandler struct { type RESTHandler struct {
storage map[string]RESTStorage storage map[string]RESTStorage
codec runtime.Codec codec runtime.Codec
canonicalPrefix string canonicalPrefix string
selfLinker runtime.SelfLinker selfLinker runtime.SelfLinker
ops *Operations ops *Operations
admissionControl admission.Interface admissionControl admission.Interface
apiRequestInfoResolver *APIRequestInfoResolver
} }
// ServeHTTP handles requests to all RESTStorage objects. // ServeHTTP handles requests to all RESTStorage objects.
func (h *RESTHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (h *RESTHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
namespace, kind, parts, err := KindAndNamespace(req) requestInfo, err := h.apiRequestInfoResolver.GetAPIRequestInfo(req)
if err != nil { if err != nil {
notFound(w, req) notFound(w, req)
return return
} }
storage, ok := h.storage[kind] storage, ok := h.storage[requestInfo.Resource]
if !ok { if !ok {
notFound(w, req) notFound(w, req)
return return
} }
h.handleRESTStorage(parts, req, w, storage, namespace, kind) h.handleRESTStorage(requestInfo.Parts, req, w, storage, requestInfo.Namespace, requestInfo.Resource)
} }
// Sets the SelfLink field of the object. // Sets the SelfLink field of the object.

View File

@ -36,10 +36,11 @@ import (
) )
type WatchHandler struct { type WatchHandler struct {
storage map[string]RESTStorage storage map[string]RESTStorage
codec runtime.Codec codec runtime.Codec
canonicalPrefix string canonicalPrefix string
selfLinker runtime.SelfLinker selfLinker runtime.SelfLinker
apiRequestInfoResolver *APIRequestInfoResolver
} }
// setSelfLinkAddName sets the self link, appending the object's name to the canonical path & type. // setSelfLinkAddName sets the self link, appending the object's name to the canonical path & type.
@ -87,21 +88,21 @@ func (h *WatchHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
namespace, kind, _, err := KindAndNamespace(req) requestInfo, err := h.apiRequestInfoResolver.GetAPIRequestInfo(req)
if err != nil { if err != nil {
notFound(w, req) notFound(w, req)
return return
} }
ctx := api.WithNamespace(api.NewContext(), namespace) ctx := api.WithNamespace(api.NewContext(), requestInfo.Namespace)
storage := h.storage[kind] storage := h.storage[requestInfo.Resource]
if storage == nil { if storage == nil {
notFound(w, req) notFound(w, req)
return return
} }
watcher, ok := storage.(ResourceWatcher) watcher, ok := storage.(ResourceWatcher)
if !ok { if !ok {
errorJSON(errors.NewMethodNotSupported(kind, "watch"), h.codec, w) errorJSON(errors.NewMethodNotSupported(requestInfo.Resource, "watch"), h.codec, w)
return return
} }

View File

@ -52,7 +52,7 @@ type policy struct {
// TODO: make this a proper REST object with its own registry. // TODO: make this a proper REST object with its own registry.
Readonly bool `json:"readonly,omitempty"` Readonly bool `json:"readonly,omitempty"`
Kind string `json:"kind,omitempty"` Resource string `json:"resource,omitempty"`
Namespace string `json:"namespace,omitempty"` Namespace string `json:"namespace,omitempty"`
// TODO: "expires" string in RFC3339 format. // TODO: "expires" string in RFC3339 format.
@ -100,7 +100,7 @@ func NewFromFile(path string) (policyList, error) {
func (p policy) matches(a authorizer.Attributes) bool { func (p policy) matches(a authorizer.Attributes) bool {
if p.subjectMatches(a) { if p.subjectMatches(a) {
if p.Readonly == false || (p.Readonly == a.IsReadOnly()) { if p.Readonly == false || (p.Readonly == a.IsReadOnly()) {
if p.Kind == "" || (p.Kind == a.GetKind()) { if p.Resource == "" || (p.Resource == a.GetResource()) {
if p.Namespace == "" || (p.Namespace == a.GetNamespace()) { if p.Namespace == "" || (p.Namespace == a.GetNamespace()) {
return true return true
} }

View File

@ -76,49 +76,49 @@ func NotTestAuthorize(t *testing.T) {
testCases := []struct { testCases := []struct {
User user.DefaultInfo User user.DefaultInfo
RO bool RO bool
Kind string Resource string
NS string NS string
ExpectAllow bool ExpectAllow bool
}{ }{
// Scheduler can read pods // Scheduler can read pods
{User: uScheduler, RO: true, Kind: "pods", NS: "ns1", ExpectAllow: true}, {User: uScheduler, RO: true, Resource: "pods", NS: "ns1", ExpectAllow: true},
{User: uScheduler, RO: true, Kind: "pods", NS: "", ExpectAllow: true}, {User: uScheduler, RO: true, Resource: "pods", NS: "", ExpectAllow: true},
// Scheduler cannot write pods // Scheduler cannot write pods
{User: uScheduler, RO: false, Kind: "pods", NS: "ns1", ExpectAllow: false}, {User: uScheduler, RO: false, Resource: "pods", NS: "ns1", ExpectAllow: false},
{User: uScheduler, RO: false, Kind: "pods", NS: "", ExpectAllow: false}, {User: uScheduler, RO: false, Resource: "pods", NS: "", ExpectAllow: false},
// Scheduler can write bindings // Scheduler can write bindings
{User: uScheduler, RO: true, Kind: "bindings", NS: "ns1", ExpectAllow: true}, {User: uScheduler, RO: true, Resource: "bindings", NS: "ns1", ExpectAllow: true},
{User: uScheduler, RO: true, Kind: "bindings", NS: "", ExpectAllow: true}, {User: uScheduler, RO: true, Resource: "bindings", NS: "", ExpectAllow: true},
// Alice can read and write anything in the right namespace. // Alice can read and write anything in the right namespace.
{User: uAlice, RO: true, Kind: "pods", NS: "projectCaribou", ExpectAllow: true}, {User: uAlice, RO: true, Resource: "pods", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, RO: true, Kind: "widgets", NS: "projectCaribou", ExpectAllow: true}, {User: uAlice, RO: true, Resource: "widgets", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, RO: true, Kind: "", NS: "projectCaribou", ExpectAllow: true}, {User: uAlice, RO: true, Resource: "", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, RO: false, Kind: "pods", NS: "projectCaribou", ExpectAllow: true}, {User: uAlice, RO: false, Resource: "pods", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, RO: false, Kind: "widgets", NS: "projectCaribou", ExpectAllow: true}, {User: uAlice, RO: false, Resource: "widgets", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, RO: false, Kind: "", NS: "projectCaribou", ExpectAllow: true}, {User: uAlice, RO: false, Resource: "", NS: "projectCaribou", ExpectAllow: true},
// .. but not the wrong namespace. // .. but not the wrong namespace.
{User: uAlice, RO: true, Kind: "pods", NS: "ns1", ExpectAllow: false}, {User: uAlice, RO: true, Resource: "pods", NS: "ns1", ExpectAllow: false},
{User: uAlice, RO: true, Kind: "widgets", NS: "ns1", ExpectAllow: false}, {User: uAlice, RO: true, Resource: "widgets", NS: "ns1", ExpectAllow: false},
{User: uAlice, RO: true, Kind: "", NS: "ns1", ExpectAllow: false}, {User: uAlice, RO: true, Resource: "", NS: "ns1", ExpectAllow: false},
// Chuck can read events, since anyone can. // Chuck can read events, since anyone can.
{User: uChuck, RO: true, Kind: "events", NS: "ns1", ExpectAllow: true}, {User: uChuck, RO: true, Resource: "events", NS: "ns1", ExpectAllow: true},
{User: uChuck, RO: true, Kind: "events", NS: "", ExpectAllow: true}, {User: uChuck, RO: true, Resource: "events", NS: "", ExpectAllow: true},
// Chuck can't do other things. // Chuck can't do other things.
{User: uChuck, RO: false, Kind: "events", NS: "ns1", ExpectAllow: false}, {User: uChuck, RO: false, Resource: "events", NS: "ns1", ExpectAllow: false},
{User: uChuck, RO: true, Kind: "pods", NS: "ns1", ExpectAllow: false}, {User: uChuck, RO: true, Resource: "pods", NS: "ns1", ExpectAllow: false},
{User: uChuck, RO: true, Kind: "floop", NS: "ns1", ExpectAllow: false}, {User: uChuck, RO: true, Resource: "floop", NS: "ns1", ExpectAllow: false},
// Chunk can't access things with no kind or namespace // Chunk can't access things with no kind or namespace
// TODO: find a way to give someone access to miscelaneous endpoints, such as // TODO: find a way to give someone access to miscelaneous endpoints, such as
// /healthz, /version, etc. // /healthz, /version, etc.
{User: uChuck, RO: true, Kind: "", NS: "", ExpectAllow: false}, {User: uChuck, RO: true, Resource: "", NS: "", ExpectAllow: false},
} }
for _, tc := range testCases { for _, tc := range testCases {
attr := authorizer.AttributesRecord{ attr := authorizer.AttributesRecord{
User: &tc.User, User: &tc.User,
ReadOnly: tc.RO, ReadOnly: tc.RO,
Kind: tc.Kind, Resource: tc.Resource,
Namespace: tc.NS, Namespace: tc.NS,
} }
t.Logf("tc: %v -> attr %v", tc, attr) t.Logf("tc: %v -> attr %v", tc, attr)

View File

@ -1,9 +1,9 @@
{"user":"admin"} {"user":"admin"}
{"user":"scheduler", "readonly": true, "kind": "pods"} {"user":"scheduler", "readonly": true, "resource": "pods"}
{"user":"scheduler", "kind": "bindings"} {"user":"scheduler", "resource": "bindings"}
{"user":"kubelet", "readonly": true, "kind": "pods"} {"user":"kubelet", "readonly": true, "resource": "pods"}
{"user":"kubelet", "readonly": true, "kind": "services"} {"user":"kubelet", "readonly": true, "resource": "services"}
{"user":"kubelet", "readonly": true, "kind": "endpoints"} {"user":"kubelet", "readonly": true, "resource": "endpoints"}
{"user":"kubelet", "kind": "events"} {"user":"kubelet", "resource": "events"}
{"user":"alice", "ns": "projectCaribou"} {"user":"alice", "ns": "projectCaribou"}
{"user":"bob", "readonly": true, "ns": "projectCaribou"} {"user":"bob", "readonly": true, "ns": "projectCaribou"}

View File

@ -40,7 +40,7 @@ type Attributes interface {
GetNamespace() string GetNamespace() string
// The kind of object, if a request is for a REST object. // The kind of object, if a request is for a REST object.
GetKind() string GetResource() string
} }
// Authorizer makes an authorization decision based on information gained by making // Authorizer makes an authorization decision based on information gained by making
@ -55,7 +55,7 @@ type AttributesRecord struct {
User user.Info User user.Info
ReadOnly bool ReadOnly bool
Namespace string Namespace string
Kind string Resource string
} }
func (a AttributesRecord) GetUserName() string { func (a AttributesRecord) GetUserName() string {
@ -74,6 +74,6 @@ func (a AttributesRecord) GetNamespace() string {
return a.Namespace return a.Namespace
} }
func (a AttributesRecord) GetKind() string { func (a AttributesRecord) GetResource() string {
return a.Kind return a.Resource
} }

View File

@ -454,7 +454,7 @@ func (m *Master) init(c *Config) {
m.InsecureHandler = handler m.InsecureHandler = handler
attributeGetter := apiserver.NewRequestAttributeGetter(userContexts) attributeGetter := apiserver.NewRequestAttributeGetter(userContexts, latest.RESTMapper, "api")
handler = apiserver.WithAuthorizationCheck(handler, attributeGetter, m.authorizer) handler = apiserver.WithAuthorizationCheck(handler, attributeGetter, m.authorizer)
// Install Authenticator // Install Authenticator
@ -531,25 +531,25 @@ func (m *Master) getServersToValidate(c *Config) map[string]apiserver.Server {
} }
// api_v1beta1 returns the resources and codec for API version v1beta1. // api_v1beta1 returns the resources and codec for API version v1beta1.
func (m *Master) api_v1beta1() (map[string]apiserver.RESTStorage, runtime.Codec, string, runtime.SelfLinker, admission.Interface, meta.RESTMapper) { func (m *Master) api_v1beta1() (map[string]apiserver.RESTStorage, runtime.Codec, string, string, runtime.SelfLinker, admission.Interface, meta.RESTMapper) {
storage := make(map[string]apiserver.RESTStorage) storage := make(map[string]apiserver.RESTStorage)
for k, v := range m.storage { for k, v := range m.storage {
storage[k] = v storage[k] = v
} }
return storage, v1beta1.Codec, "/api/v1beta1", latest.SelfLinker, m.admissionControl, latest.RESTMapper return storage, v1beta1.Codec, "api", "/api/v1beta1", latest.SelfLinker, m.admissionControl, latest.RESTMapper
} }
// api_v1beta2 returns the resources and codec for API version v1beta2. // api_v1beta2 returns the resources and codec for API version v1beta2.
func (m *Master) api_v1beta2() (map[string]apiserver.RESTStorage, runtime.Codec, string, runtime.SelfLinker, admission.Interface, meta.RESTMapper) { func (m *Master) api_v1beta2() (map[string]apiserver.RESTStorage, runtime.Codec, string, string, runtime.SelfLinker, admission.Interface, meta.RESTMapper) {
storage := make(map[string]apiserver.RESTStorage) storage := make(map[string]apiserver.RESTStorage)
for k, v := range m.storage { for k, v := range m.storage {
storage[k] = v storage[k] = v
} }
return storage, v1beta2.Codec, "/api/v1beta2", latest.SelfLinker, m.admissionControl, latest.RESTMapper return storage, v1beta2.Codec, "api", "/api/v1beta2", latest.SelfLinker, m.admissionControl, latest.RESTMapper
} }
// api_v1beta3 returns the resources and codec for API version v1beta3. // api_v1beta3 returns the resources and codec for API version v1beta3.
func (m *Master) api_v1beta3() (map[string]apiserver.RESTStorage, runtime.Codec, string, runtime.SelfLinker, admission.Interface, meta.RESTMapper) { func (m *Master) api_v1beta3() (map[string]apiserver.RESTStorage, runtime.Codec, string, string, runtime.SelfLinker, admission.Interface, meta.RESTMapper) {
storage := make(map[string]apiserver.RESTStorage) storage := make(map[string]apiserver.RESTStorage)
for k, v := range m.storage { for k, v := range m.storage {
if k == "minions" { if k == "minions" {
@ -557,5 +557,5 @@ func (m *Master) api_v1beta3() (map[string]apiserver.RESTStorage, runtime.Codec,
} }
storage[strings.ToLower(k)] = v storage[strings.ToLower(k)] = v
} }
return storage, v1beta3.Codec, "/api/v1beta3", latest.SelfLinker, m.admissionControl, latest.RESTMapper return storage, v1beta3.Codec, "api", "/api/v1beta3", latest.SelfLinker, m.admissionControl, latest.RESTMapper
} }

View File

@ -36,7 +36,7 @@ func init() {
type alwaysDeny struct{} type alwaysDeny struct{}
func (alwaysDeny) Admit(a admission.Attributes) (err error) { func (alwaysDeny) Admit(a admission.Attributes) (err error) {
return apierrors.NewForbidden(a.GetKind(), "", errors.New("Admission control is denying all modifications")) return apierrors.NewForbidden(a.GetResource(), "", errors.New("Admission control is denying all modifications"))
} }
func NewAlwaysDeny() admission.Interface { func NewAlwaysDeny() admission.Interface {

View File

@ -58,7 +58,7 @@ func (l *limitRanger) Admit(a admission.Attributes) (err error) {
// ensure it meets each prescribed min/max // ensure it meets each prescribed min/max
for i := range items.Items { for i := range items.Items {
limitRange := &items.Items[i] limitRange := &items.Items[i]
err = l.limitFunc(limitRange, a.GetKind(), a.GetObject()) err = l.limitFunc(limitRange, a.GetResource(), a.GetObject())
if err != nil { if err != nil {
return err return err
} }
@ -86,8 +86,8 @@ func Max(a int64, b int64) int64 {
} }
// PodLimitFunc enforces that a pod spec does not exceed any limits specified on the supplied limit range // PodLimitFunc enforces that a pod spec does not exceed any limits specified on the supplied limit range
func PodLimitFunc(limitRange *api.LimitRange, kind string, obj runtime.Object) error { func PodLimitFunc(limitRange *api.LimitRange, resourceName string, obj runtime.Object) error {
if kind != "pods" { if resourceName != "pods" {
return nil return nil
} }
@ -161,11 +161,11 @@ func PodLimitFunc(limitRange *api.LimitRange, kind string, obj runtime.Object) e
switch minOrMax { switch minOrMax {
case "Min": case "Min":
if observed < enforced { if observed < enforced {
return apierrors.NewForbidden(kind, pod.Name, err) return apierrors.NewForbidden(resourceName, pod.Name, err)
} }
case "Max": case "Max":
if observed > enforced { if observed > enforced {
return apierrors.NewForbidden(kind, pod.Name, err) return apierrors.NewForbidden(resourceName, pod.Name, err)
} }
} }
} }

View File

@ -47,7 +47,7 @@ func (resourceDefaults) Admit(a admission.Attributes) (err error) {
} }
// we only care about pods // we only care about pods
if a.GetKind() != "pods" { if a.GetResource() != "pods" {
return nil return nil
} }

View File

@ -44,7 +44,7 @@ func NewResourceQuota(client client.Interface) admission.Interface {
return &quota{client: client} return &quota{client: client}
} }
var kindToResourceName = map[string]api.ResourceName{ var resourceToResourceName = map[string]api.ResourceName{
"pods": api.ResourcePods, "pods": api.ResourcePods,
"services": api.ResourceServices, "services": api.ResourceServices,
"replicationControllers": api.ResourceReplicationControllers, "replicationControllers": api.ResourceReplicationControllers,
@ -57,7 +57,7 @@ func (q *quota) Admit(a admission.Attributes) (err error) {
} }
obj := a.GetObject() obj := a.GetObject()
kind := a.GetKind() resource := a.GetResource()
name := "Unknown" name := "Unknown"
if obj != nil { if obj != nil {
name, _ = meta.NewAccessor().Name(obj) name, _ = meta.NewAccessor().Name(obj)
@ -65,7 +65,7 @@ func (q *quota) Admit(a admission.Attributes) (err error) {
list, err := q.client.ResourceQuotas(a.GetNamespace()).List(labels.Everything()) list, err := q.client.ResourceQuotas(a.GetNamespace()).List(labels.Everything())
if err != nil { if err != nil {
return apierrors.NewForbidden(a.GetKind(), name, fmt.Errorf("Unable to %s %s at this time because there was an error enforcing quota", a.GetOperation(), kind)) return apierrors.NewForbidden(a.GetResource(), name, fmt.Errorf("Unable to %s %s at this time because there was an error enforcing quota", a.GetOperation(), resource))
} }
if len(list.Items) == 0 { if len(list.Items) == 0 {
@ -90,7 +90,7 @@ func (q *quota) Admit(a admission.Attributes) (err error) {
usage.Status = quota.Status usage.Status = quota.Status
err = q.client.ResourceQuotaUsages(usage.Namespace).Create(&usage) err = q.client.ResourceQuotaUsages(usage.Namespace).Create(&usage)
if err != nil { if err != nil {
return apierrors.NewForbidden(a.GetKind(), name, fmt.Errorf("Unable to %s %s at this time because there was an error enforcing quota", a.GetOperation(), a.GetKind())) return apierrors.NewForbidden(a.GetResource(), name, fmt.Errorf("Unable to %s %s at this time because there was an error enforcing quota", a.GetOperation(), a.GetResource()))
} }
} }
} }
@ -102,7 +102,7 @@ func (q *quota) Admit(a admission.Attributes) (err error) {
// Return an error if the operation should not pass admission control // Return an error if the operation should not pass admission control
func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, client client.Interface) (bool, error) { func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, client client.Interface) (bool, error) {
obj := a.GetObject() obj := a.GetObject()
kind := a.GetKind() resourceName := a.GetResource()
name := "Unknown" name := "Unknown"
if obj != nil { if obj != nil {
name, _ = meta.NewAccessor().Name(obj) name, _ = meta.NewAccessor().Name(obj)
@ -114,15 +114,15 @@ func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, cli
} }
// handle max counts for each kind of resource (pods, services, replicationControllers, etc.) // handle max counts for each kind of resource (pods, services, replicationControllers, etc.)
if a.GetOperation() == "CREATE" { if a.GetOperation() == "CREATE" {
resourceName := kindToResourceName[a.GetKind()] resourceName := resourceToResourceName[a.GetResource()]
hard, hardFound := status.Hard[resourceName] hard, hardFound := status.Hard[resourceName]
if hardFound { if hardFound {
used, usedFound := status.Used[resourceName] used, usedFound := status.Used[resourceName]
if !usedFound { if !usedFound {
return false, apierrors.NewForbidden(kind, name, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed.")) return false, apierrors.NewForbidden(a.GetResource(), name, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed."))
} }
if used.Value() >= hard.Value() { if used.Value() >= hard.Value() {
return false, apierrors.NewForbidden(kind, name, fmt.Errorf("Limited to %s %s", hard.String(), kind)) return false, apierrors.NewForbidden(a.GetResource(), name, fmt.Errorf("Limited to %s %s", hard.String(), a.GetResource()))
} else { } else {
status.Used[resourceName] = *resource.NewQuantity(used.Value()+int64(1), resource.DecimalSI) status.Used[resourceName] = *resource.NewQuantity(used.Value()+int64(1), resource.DecimalSI)
dirty = true dirty = true
@ -130,7 +130,7 @@ func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, cli
} }
} }
// handle memory/cpu constraints, and any diff of usage based on memory/cpu on updates // handle memory/cpu constraints, and any diff of usage based on memory/cpu on updates
if a.GetKind() == "pods" && (set[api.ResourceMemory] || set[api.ResourceCPU]) { if a.GetResource() == "pods" && (set[api.ResourceMemory] || set[api.ResourceCPU]) {
pod := obj.(*api.Pod) pod := obj.(*api.Pod)
deltaCPU := resourcequota.PodCPU(pod) deltaCPU := resourcequota.PodCPU(pod)
deltaMemory := resourcequota.PodMemory(pod) deltaMemory := resourcequota.PodMemory(pod)
@ -138,7 +138,7 @@ func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, cli
if a.GetOperation() == "UPDATE" { if a.GetOperation() == "UPDATE" {
oldPod, err := client.Pods(a.GetNamespace()).Get(pod.Name) oldPod, err := client.Pods(a.GetNamespace()).Get(pod.Name)
if err != nil { if err != nil {
return false, apierrors.NewForbidden(kind, name, err) return false, apierrors.NewForbidden(resourceName, name, err)
} }
oldCPU := resourcequota.PodCPU(oldPod) oldCPU := resourcequota.PodCPU(oldPod)
oldMemory := resourcequota.PodMemory(oldPod) oldMemory := resourcequota.PodMemory(oldPod)
@ -150,10 +150,10 @@ func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, cli
if hardMemFound { if hardMemFound {
used, usedFound := status.Used[api.ResourceMemory] used, usedFound := status.Used[api.ResourceMemory]
if !usedFound { if !usedFound {
return false, apierrors.NewForbidden(kind, name, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed.")) return false, apierrors.NewForbidden(resourceName, name, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed."))
} }
if used.Value()+deltaMemory.Value() > hardMem.Value() { if used.Value()+deltaMemory.Value() > hardMem.Value() {
return false, apierrors.NewForbidden(kind, name, fmt.Errorf("Limited to %s memory", hardMem.String())) return false, apierrors.NewForbidden(resourceName, name, fmt.Errorf("Limited to %s memory", hardMem.String()))
} else { } else {
status.Used[api.ResourceMemory] = *resource.NewQuantity(used.Value()+deltaMemory.Value(), resource.DecimalSI) status.Used[api.ResourceMemory] = *resource.NewQuantity(used.Value()+deltaMemory.Value(), resource.DecimalSI)
dirty = true dirty = true
@ -163,10 +163,10 @@ func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, cli
if hardCPUFound { if hardCPUFound {
used, usedFound := status.Used[api.ResourceCPU] used, usedFound := status.Used[api.ResourceCPU]
if !usedFound { if !usedFound {
return false, apierrors.NewForbidden(kind, name, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed.")) return false, apierrors.NewForbidden(resourceName, name, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed."))
} }
if used.MilliValue()+deltaCPU.MilliValue() > hardCPU.MilliValue() { if used.MilliValue()+deltaCPU.MilliValue() > hardCPU.MilliValue() {
return false, apierrors.NewForbidden(kind, name, fmt.Errorf("Limited to %s CPU", hardCPU.String())) return false, apierrors.NewForbidden(resourceName, name, fmt.Errorf("Limited to %s CPU", hardCPU.String()))
} else { } else {
status.Used[api.ResourceCPU] = *resource.NewMilliQuantity(used.MilliValue()+deltaCPU.MilliValue(), resource.DecimalSI) status.Used[api.ResourceCPU] = *resource.NewMilliQuantity(used.MilliValue()+deltaCPU.MilliValue(), resource.DecimalSI)
dirty = true dirty = true

View File

@ -691,7 +691,7 @@ func TestKindAuthorization(t *testing.T) {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
a := newAuthorizerWithContents(t, `{"kind": "services"} a := newAuthorizerWithContents(t, `{"resource": "services"}
`) `)
var m *master.Master var m *master.Master