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

pull more complete information from request
pull/6/head
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.)
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.
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:
- user (the user-string which a user was authenticated as).
- 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
`/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
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`
- `readonly`, type boolean, when true, means that the policy only applies to GET
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.
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
1. Alice can do anything: `{"user":"alice"}`
2. Kubelet can read any pods: `{"user":"kubelet", "kind": "pods", "readonly": true}`
3. Kubelet can read and write events: `{"user":"kubelet", "kind": "events"}`
4. Bob can just read pods in namespace "projectCaribou": `{"user":"bob", "kind": "pods", "readonly": true, "ns": "projectCaribou"}`
2. Kubelet can read any pods: `{"user":"kubelet", "resource": "pods", "readonly": true}`
3. Kubelet can read and write events: `{"user":"kubelet", "resource": "events"}`
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)

View File

@ -22,15 +22,15 @@ import (
type attributesRecord struct {
namespace string
kind string
resource string
operation string
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{
namespace: namespace,
kind: kind,
resource: resource,
operation: operation,
object: object,
}
@ -40,8 +40,8 @@ func (record *attributesRecord) GetNamespace() string {
return record.namespace
}
func (record *attributesRecord) GetKind() string {
return record.kind
func (record *attributesRecord) GetResource() string {
return record.resource
}
func (record *attributesRecord) GetOperation() string {

View File

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

View File

@ -49,13 +49,14 @@ func (a *APIInstaller) Install() (ws *restful.WebService, errors []error) {
// Initialize the custom handlers.
watchHandler := (&WatchHandler{
storage: a.restHandler.storage,
codec: a.restHandler.codec,
canonicalPrefix: a.restHandler.canonicalPrefix,
selfLinker: a.restHandler.selfLinker,
storage: a.restHandler.storage,
codec: a.restHandler.codec,
canonicalPrefix: a.restHandler.canonicalPrefix,
selfLinker: a.restHandler.selfLinker,
apiRequestInfoResolver: a.restHandler.apiRequestInfoResolver,
})
redirectHandler := (&RedirectHandler{a.restHandler.storage, a.restHandler.codec})
proxyHandler := (&ProxyHandler{a.prefix + "/proxy/", 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, a.restHandler.apiRequestInfoResolver})
for path, storage := range a.restHandler.storage {
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/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/healthz"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
@ -57,7 +58,7 @@ type defaultAPIServer struct {
// 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 {
prefix := root + "/" + version
group := NewAPIGroupVersion(storage, codec, prefix, selfLinker, admissionControl, mapper)
group := NewAPIGroupVersion(storage, codec, root, prefix, selfLinker, admissionControl, mapper)
container := restful.NewContainer()
container.Router(restful.CurlyRouter{})
mux := container.ServeMux
@ -85,15 +86,16 @@ type APIGroupVersion struct {
// This is a helper method for registering multiple sets of REST handlers under different
// prefixes onto a server.
// 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{
handler: RESTHandler{
storage: storage,
codec: codec,
canonicalPrefix: canonicalPrefix,
selfLinker: selfLinker,
ops: NewOperations(),
admissionControl: admissionControl,
storage: storage,
codec: codec,
canonicalPrefix: canonicalPrefix,
selfLinker: selfLinker,
ops: NewOperations(),
admissionControl: admissionControl,
apiRequestInfoResolver: &APIRequestInfoResolver{util.NewStringSet(apiRoot), latest.RESTMapper},
},
mapper: mapper,
}

View File

@ -25,6 +25,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer"
authhandlers "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/handlers"
"github.com/GoogleCloudPlatform/kubernetes/pkg/httplog"
@ -153,12 +154,13 @@ type RequestAttributeGetter interface {
}
type requestAttributeGetter struct {
userContexts authhandlers.RequestContext
userContexts authhandlers.RequestContext
apiRequestInfoResolver *APIRequestInfoResolver
}
// NewAttributeGetter returns an object which implements the RequestAttributeGetter interface.
func NewRequestAttributeGetter(userContexts authhandlers.RequestContext) RequestAttributeGetter {
return &requestAttributeGetter{userContexts}
func NewRequestAttributeGetter(userContexts authhandlers.RequestContext, restMapper meta.RESTMapper, apiRoots ...string) RequestAttributeGetter {
return &requestAttributeGetter{userContexts, &APIRequestInfoResolver{util.NewStringSet(apiRoots...), restMapper}}
}
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)
namespace, kind, _, _ := KindAndNamespace(req)
apiRequestInfo, _ := r.apiRequestInfoResolver.GetAPIRequestInfo(req)
// If a path follows the conventions of the REST object store, then
// we can extract the object Kind. Otherwise, not.
attribs.Kind = kind
// we can extract the resource. Otherwise, not.
attribs.Resource = apiRequestInfo.Resource
// If the request specifies a namespace, then the namespace is filled in.
// Assumes there is no empty string namespace. Unspecified results
// in empty (does not understand defaulting rules.)
attribs.Namespace = namespace
attribs.Namespace = apiRequestInfo.Namespace
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:
// Storage paths
// /ns/{namespace}/{kind}
// /ns/{namespace}/{kind}/{resourceName}
// /{kind}
// /{kind}/{resourceName}
// /{kind}/{resourceName}?namespace={namespace}
// /{kind}?namespace={namespace}
// /ns/{namespace}/{resource}
// /ns/{namespace}/{resource}/{resourceName}
// /{resource}
// /{resource}/{resourceName}
// /{resource}/{resourceName}?namespace={namespace}
// /{resource}?namespace={namespace}
//
// Special verbs:
// /proxy/{kind}/{resourceName}
// /proxy/ns/{namespace}/{kind}/{resourceName}
// /redirect/ns/{namespace}/{kind}/{resourceName}
// /redirect/{kind}/{resourceName}
// /watch/{kind}
// /watch/ns/{namespace}/{kind}
// /proxy/{resource}/{resourceName}
// /proxy/ns/{namespace}/{resource}/{resourceName}
// /redirect/ns/{namespace}/{resource}/{resourceName}
// /redirect/{resource}/{resourceName}
// /watch/{resource}
// /watch/ns/{namespace}/{resource}
//
// Fully qualified paths for above:
// /api/{version}/*
// /api/{version}/*
func KindAndNamespace(req *http.Request) (namespace, kind string, parts []string, err error) {
parts = splitPath(req.URL.Path)
if len(parts) < 1 {
err = fmt.Errorf("Unable to determine kind and namespace from an empty URL path")
return
func (r *APIRequestInfoResolver) GetAPIRequestInfo(req *http.Request) (APIRequestInfo, error) {
requestInfo := APIRequestInfo{}
currentParts := splitPath(req.URL.Path)
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
if parts[0] == "api" {
if len(parts) > 2 {
parts = parts[2:]
} else {
err = fmt.Errorf("Unable to determine kind and namespace from url, %v", req.URL)
return
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 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}/*
if _, ok := specialVerbs[parts[0]]; ok {
if len(parts) > 1 {
parts = parts[1:]
if _, ok := specialVerbs[currentParts[0]]; ok {
requestInfo.Verb = currentParts[0]
if len(currentParts) > 1 {
currentParts = currentParts[1:]
} else {
err = fmt.Errorf("Unable to determine kind and namespace from url, %v", req.URL)
return
return requestInfo, fmt.Errorf("Unable to determine kind and namespace from url, %v", req.URL)
}
} 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
if parts[0] == "ns" {
if len(parts) < 3 {
err = fmt.Errorf("ResourceTypeAndNamespace expects a path of form /ns/{namespace}/*")
return
}
namespace = parts[1]
kind = parts[2]
parts = parts[2:]
return
// parsing successful, so we now know the proper value for .Parts
requestInfo.Parts = currentParts
// if there's another part remaining after the kind, then that's the resource name
if len(requestInfo.Parts) >= 2 {
requestInfo.Name = requestInfo.Parts[1]
}
// URL forms: /{kind}/*
// URL forms: POST /{kind} is a legacy API convention to create in "default" namespace
// URL forms: /{kind}/{resourceName} use the "default" namespace if omitted from query param
// 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
}
// if there's no name on the request and we thought it was a get before, then the actual verb is a list
if len(requestInfo.Name) == 0 && requestInfo.Verb == "get" {
requestInfo.Verb = "list"
}
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"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)
type fakeRL bool
@ -63,59 +65,78 @@ func TestReadOnly(t *testing.T) {
}
}
func TestKindAndNamespace(t *testing.T) {
func TestGetAPIRequestInfo(t *testing.T) {
successCases := []struct {
method string
url string
expectedNamespace string
expectedKind string
expectedParts []string
method string
url string
expectedVerb string
expectedAPIVersion string
expectedNamespace string
expectedResource string
expectedKind string
expectedName string
expectedParts []string
}{
// resource paths
{"GET", "/ns/other/pods", "other", "pods", []string{"pods"}},
{"GET", "/ns/other/pods/foo", "other", "pods", []string{"pods", "foo"}},
{"GET", "/pods", api.NamespaceAll, "pods", []string{"pods"}},
{"POST", "/pods", api.NamespaceDefault, "pods", []string{"pods"}},
{"GET", "/pods/foo", api.NamespaceDefault, "pods", []string{"pods", "foo"}},
{"GET", "/pods/foo?namespace=other", "other", "pods", []string{"pods", "foo"}},
{"GET", "/pods?namespace=other", "other", "pods", []string{"pods"}},
{"GET", "/ns/other/pods", "list", "", "other", "pods", "Pod", "", []string{"pods"}},
{"GET", "/ns/other/pods/foo", "get", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/pods", "list", "", api.NamespaceAll, "pods", "Pod", "", []string{"pods"}},
{"POST", "/pods", "create", "", api.NamespaceDefault, "pods", "Pod", "", []string{"pods"}},
{"GET", "/pods/foo", "get", "", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/pods/foo?namespace=other", "get", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/pods?namespace=other", "list", "", "other", "pods", "Pod", "", []string{"pods"}},
// special verbs
{"GET", "/proxy/ns/other/pods/foo", "other", "pods", []string{"pods", "foo"}},
{"GET", "/proxy/pods/foo", api.NamespaceDefault, "pods", []string{"pods", "foo"}},
{"GET", "/redirect/ns/other/pods/foo", "other", "pods", []string{"pods", "foo"}},
{"GET", "/redirect/pods/foo", api.NamespaceDefault, "pods", []string{"pods", "foo"}},
{"GET", "/watch/pods", api.NamespaceAll, "pods", []string{"pods"}},
{"GET", "/watch/ns/other/pods", "other", "pods", []string{"pods"}},
{"GET", "/proxy/ns/other/pods/foo", "proxy", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/proxy/pods/foo", "proxy", "", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/redirect/ns/other/pods/foo", "redirect", "", "other", "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/redirect/pods/foo", "redirect", "", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/watch/pods", "watch", "", api.NamespaceAll, "pods", "Pod", "", []string{"pods"}},
{"GET", "/watch/ns/other/pods", "watch", "", "other", "pods", "Pod", "", []string{"pods"}},
// fully-qualified paths
{"GET", "/api/v1beta1/ns/other/pods", "other", "pods", []string{"pods"}},
{"GET", "/api/v1beta1/ns/other/pods/foo", "other", "pods", []string{"pods", "foo"}},
{"GET", "/api/v1beta1/pods", api.NamespaceAll, "pods", []string{"pods"}},
{"POST", "/api/v1beta1/pods", api.NamespaceDefault, "pods", []string{"pods"}},
{"GET", "/api/v1beta1/pods/foo", api.NamespaceDefault, "pods", []string{"pods", "foo"}},
{"GET", "/api/v1beta1/pods/foo?namespace=other", "other", "pods", []string{"pods", "foo"}},
{"GET", "/api/v1beta1/pods?namespace=other", "other", "pods", []string{"pods"}},
{"GET", "/api/v1beta1/proxy/pods/foo", api.NamespaceDefault, "pods", []string{"pods", "foo"}},
{"GET", "/api/v1beta1/redirect/pods/foo", api.NamespaceDefault, "pods", []string{"pods", "foo"}},
{"GET", "/api/v1beta1/watch/pods", api.NamespaceAll, "pods", []string{"pods"}},
{"GET", "/api/v1beta1/watch/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", "get", "v1beta1", "other", "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/api/v1beta1/pods", "list", "v1beta1", api.NamespaceAll, "pods", "Pod", "", []string{"pods"}},
{"POST", "/api/v1beta1/pods", "create", "v1beta1", api.NamespaceDefault, "pods", "Pod", "", []string{"pods"}},
{"GET", "/api/v1beta1/pods/foo", "get", "v1beta1", api.NamespaceDefault, "pods", "Pod", "foo", []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", "list", "v1beta1", "other", "pods", "Pod", "", []string{"pods"}},
{"GET", "/api/v1beta1/proxy/pods/foo", "proxy", "v1beta1", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/api/v1beta1/redirect/pods/foo", "redirect", "v1beta1", api.NamespaceDefault, "pods", "Pod", "foo", []string{"pods", "foo"}},
{"GET", "/api/v1beta1/watch/pods", "watch", "v1beta1", api.NamespaceAll, "pods", "Pod", "", []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 {
req, _ := http.NewRequest(successCase.method, successCase.url, nil)
namespace, kind, parts, err := KindAndNamespace(req)
apiRequestInfo, err := apiRequestInfoResolver.GetAPIRequestInfo(req)
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 {
t.Errorf("Unexpected namespace for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedNamespace, namespace)
if successCase.expectedVerb != apiRequestInfo.Verb {
t.Errorf("Unexpected verb for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedVerb, apiRequestInfo.Verb)
}
if successCase.expectedKind != kind {
t.Errorf("Unexpected resourceType for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedKind, kind)
if successCase.expectedAPIVersion != apiRequestInfo.APIVersion {
t.Errorf("Unexpected apiVersion for url: %s, expected: %s, actual: %s", successCase.url, successCase.expectedAPIVersion, apiRequestInfo.APIVersion)
}
if !reflect.DeepEqual(successCase.expectedParts, parts) {
t.Errorf("Unexpected parts for url: %s, expected: %v, actual: %v", successCase.url, successCase.expectedParts, parts)
if successCase.expectedNamespace != apiRequestInfo.Namespace {
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 {
t.Errorf("Unexpected error %v", err)
}
_, _, _, err = KindAndNamespace(req)
_, err = apiRequestInfoResolver.GetAPIRequestInfo(req)
if err == nil {
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
// specified by items implementing Redirector.
type ProxyHandler struct {
prefix string
storage map[string]RESTStorage
codec runtime.Codec
prefix string
storage map[string]RESTStorage
codec runtime.Codec
apiRequestInfoResolver *APIRequestInfoResolver
}
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 {
notFound(w, req)
return
}
namespace, resource, parts := requestInfo.Namespace, requestInfo.Resource, requestInfo.Parts
ctx := api.WithNamespace(api.NewContext(), namespace)
if len(parts) < 2 {
notFound(w, req)
@ -103,17 +106,17 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
rest = rest + "/"
}
}
storage, ok := r.storage[kind]
storage, ok := r.storage[resource]
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)
return
}
redirector, ok := storage.(Redirector)
if !ok {
httplog.LogOf(req, w).Addf("'%v' is not a redirector", kind)
errorJSON(errors.NewMethodNotSupported(kind, "proxy"), r.codec, w)
httplog.LogOf(req, w).Addf("'%v' is not a redirector", resource)
errorJSON(errors.NewMethodNotSupported(resource, "proxy"), r.codec, w)
return
}
@ -156,7 +159,7 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
proxy.Transport = &proxyTransport{
proxyScheme: req.URL.Scheme,
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.ServeHTTP(w, newReq)

View File

@ -26,35 +26,37 @@ import (
)
type RedirectHandler struct {
storage map[string]RESTStorage
codec runtime.Codec
storage map[string]RESTStorage
codec runtime.Codec
apiRequestInfoResolver *APIRequestInfoResolver
}
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 {
notFound(w, req)
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" {
notFound(w, req)
return
}
id := parts[1]
storage, ok := r.storage[kind]
storage, ok := r.storage[resource]
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)
return
}
redirector, ok := storage.(Redirector)
if !ok {
httplog.LogOf(req, w).Addf("'%v' is not a redirector", kind)
errorJSON(errors.NewMethodNotSupported(kind, "redirect"), r.codec, w)
httplog.LogOf(req, w).Addf("'%v' is not a redirector", resource)
errorJSON(errors.NewMethodNotSupported(resource, "redirect"), r.codec, w)
return
}

View File

@ -32,28 +32,29 @@ import (
// RESTHandler implements HTTP verbs on a set of RESTful resources identified by name.
type RESTHandler struct {
storage map[string]RESTStorage
codec runtime.Codec
canonicalPrefix string
selfLinker runtime.SelfLinker
ops *Operations
admissionControl admission.Interface
storage map[string]RESTStorage
codec runtime.Codec
canonicalPrefix string
selfLinker runtime.SelfLinker
ops *Operations
admissionControl admission.Interface
apiRequestInfoResolver *APIRequestInfoResolver
}
// ServeHTTP handles requests to all RESTStorage objects.
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 {
notFound(w, req)
return
}
storage, ok := h.storage[kind]
storage, ok := h.storage[requestInfo.Resource]
if !ok {
notFound(w, req)
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.

View File

@ -36,10 +36,11 @@ import (
)
type WatchHandler struct {
storage map[string]RESTStorage
codec runtime.Codec
canonicalPrefix string
selfLinker runtime.SelfLinker
storage map[string]RESTStorage
codec runtime.Codec
canonicalPrefix string
selfLinker runtime.SelfLinker
apiRequestInfoResolver *APIRequestInfoResolver
}
// 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
}
namespace, kind, _, err := KindAndNamespace(req)
requestInfo, err := h.apiRequestInfoResolver.GetAPIRequestInfo(req)
if err != nil {
notFound(w, req)
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 {
notFound(w, req)
return
}
watcher, ok := storage.(ResourceWatcher)
if !ok {
errorJSON(errors.NewMethodNotSupported(kind, "watch"), h.codec, w)
errorJSON(errors.NewMethodNotSupported(requestInfo.Resource, "watch"), h.codec, w)
return
}

View File

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

View File

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

View File

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

View File

@ -40,7 +40,7 @@ type Attributes interface {
GetNamespace() string
// 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
@ -55,7 +55,7 @@ type AttributesRecord struct {
User user.Info
ReadOnly bool
Namespace string
Kind string
Resource string
}
func (a AttributesRecord) GetUserName() string {
@ -74,6 +74,6 @@ func (a AttributesRecord) GetNamespace() string {
return a.Namespace
}
func (a AttributesRecord) GetKind() string {
return a.Kind
func (a AttributesRecord) GetResource() string {
return a.Resource
}

View File

@ -454,7 +454,7 @@ func (m *Master) init(c *Config) {
m.InsecureHandler = handler
attributeGetter := apiserver.NewRequestAttributeGetter(userContexts)
attributeGetter := apiserver.NewRequestAttributeGetter(userContexts, latest.RESTMapper, "api")
handler = apiserver.WithAuthorizationCheck(handler, attributeGetter, m.authorizer)
// 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.
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)
for k, v := range m.storage {
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.
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)
for k, v := range m.storage {
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.
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)
for k, v := range m.storage {
if k == "minions" {
@ -557,5 +557,5 @@ func (m *Master) api_v1beta3() (map[string]apiserver.RESTStorage, runtime.Codec,
}
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{}
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 {

View File

@ -58,7 +58,7 @@ func (l *limitRanger) Admit(a admission.Attributes) (err error) {
// ensure it meets each prescribed min/max
for i := range items.Items {
limitRange := &items.Items[i]
err = l.limitFunc(limitRange, a.GetKind(), a.GetObject())
err = l.limitFunc(limitRange, a.GetResource(), a.GetObject())
if err != nil {
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
func PodLimitFunc(limitRange *api.LimitRange, kind string, obj runtime.Object) error {
if kind != "pods" {
func PodLimitFunc(limitRange *api.LimitRange, resourceName string, obj runtime.Object) error {
if resourceName != "pods" {
return nil
}
@ -161,11 +161,11 @@ func PodLimitFunc(limitRange *api.LimitRange, kind string, obj runtime.Object) e
switch minOrMax {
case "Min":
if observed < enforced {
return apierrors.NewForbidden(kind, pod.Name, err)
return apierrors.NewForbidden(resourceName, pod.Name, err)
}
case "Max":
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
if a.GetKind() != "pods" {
if a.GetResource() != "pods" {
return nil
}

View File

@ -44,7 +44,7 @@ func NewResourceQuota(client client.Interface) admission.Interface {
return &quota{client: client}
}
var kindToResourceName = map[string]api.ResourceName{
var resourceToResourceName = map[string]api.ResourceName{
"pods": api.ResourcePods,
"services": api.ResourceServices,
"replicationControllers": api.ResourceReplicationControllers,
@ -57,7 +57,7 @@ func (q *quota) Admit(a admission.Attributes) (err error) {
}
obj := a.GetObject()
kind := a.GetKind()
resource := a.GetResource()
name := "Unknown"
if obj != nil {
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())
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 {
@ -90,7 +90,7 @@ func (q *quota) Admit(a admission.Attributes) (err error) {
usage.Status = quota.Status
err = q.client.ResourceQuotaUsages(usage.Namespace).Create(&usage)
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
func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, client client.Interface) (bool, error) {
obj := a.GetObject()
kind := a.GetKind()
resourceName := a.GetResource()
name := "Unknown"
if obj != nil {
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.)
if a.GetOperation() == "CREATE" {
resourceName := kindToResourceName[a.GetKind()]
resourceName := resourceToResourceName[a.GetResource()]
hard, hardFound := status.Hard[resourceName]
if hardFound {
used, usedFound := status.Used[resourceName]
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() {
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 {
status.Used[resourceName] = *resource.NewQuantity(used.Value()+int64(1), resource.DecimalSI)
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
if a.GetKind() == "pods" && (set[api.ResourceMemory] || set[api.ResourceCPU]) {
if a.GetResource() == "pods" && (set[api.ResourceMemory] || set[api.ResourceCPU]) {
pod := obj.(*api.Pod)
deltaCPU := resourcequota.PodCPU(pod)
deltaMemory := resourcequota.PodMemory(pod)
@ -138,7 +138,7 @@ func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, cli
if a.GetOperation() == "UPDATE" {
oldPod, err := client.Pods(a.GetNamespace()).Get(pod.Name)
if err != nil {
return false, apierrors.NewForbidden(kind, name, err)
return false, apierrors.NewForbidden(resourceName, name, err)
}
oldCPU := resourcequota.PodCPU(oldPod)
oldMemory := resourcequota.PodMemory(oldPod)
@ -150,10 +150,10 @@ func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, cli
if hardMemFound {
used, usedFound := status.Used[api.ResourceMemory]
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() {
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 {
status.Used[api.ResourceMemory] = *resource.NewQuantity(used.Value()+deltaMemory.Value(), resource.DecimalSI)
dirty = true
@ -163,10 +163,10 @@ func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, cli
if hardCPUFound {
used, usedFound := status.Used[api.ResourceCPU]
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() {
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 {
status.Used[api.ResourceCPU] = *resource.NewMilliQuantity(used.MilliValue()+deltaCPU.MilliValue(), resource.DecimalSI)
dirty = true

View File

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