diff --git a/pkg/apis/meta/v1/types.go b/pkg/apis/meta/v1/types.go index 996b9ad9589..b7fb2cd37bf 100644 --- a/pkg/apis/meta/v1/types.go +++ b/pkg/apis/meta/v1/types.go @@ -25,7 +25,11 @@ limitations under the License. // separate packages. package v1 -import "strings" +import ( + "fmt" + + "strings" +) // TypeMeta describes an individual object in an API response or request // with strings representing the type of the object and its API schema version. @@ -403,6 +407,19 @@ type APIResource struct { Namespaced bool `json:"namespaced" protobuf:"varint,2,opt,name=namespaced"` // kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo') Kind string `json:"kind" protobuf:"bytes,3,opt,name=kind"` + // verbs is a list of supported kube verbs (this includes get, list, watch, create, + // update, patch, delete, deletecollection, and proxy) + Verbs Verbs `json:"verbs" protobuf:"bytes,4,opt,name=verbs"` +} + +// Verbs masks the value so protobuf can generate +// +// +protobuf.nullable=true +// +protobuf.options.(gogoproto.goproto_stringer)=false +type Verbs []string + +func (vs Verbs) String() string { + return fmt.Sprintf("%v", []string(vs)) } // APIResourceList is a list of APIResource, it is used to expose the name of the diff --git a/pkg/apis/meta/v1/types_test.go b/pkg/apis/meta/v1/types_test.go new file mode 100644 index 00000000000..540ba7131e5 --- /dev/null +++ b/pkg/apis/meta/v1/types_test.go @@ -0,0 +1,112 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/ugorji/go/codec" +) + +func TestVerbsMarshalJSON(t *testing.T) { + cases := []struct { + input APIResource + result string + }{ + {APIResource{}, `{"name":"","namespaced":false,"kind":"","verbs":null}`}, + {APIResource{Verbs: Verbs([]string{})}, `{"name":"","namespaced":false,"kind":"","verbs":[]}`}, + {APIResource{Verbs: Verbs([]string{"delete"})}, `{"name":"","namespaced":false,"kind":"","verbs":["delete"]}`}, + } + + for i, c := range cases { + result, err := json.Marshal(&c.input) + if err != nil { + t.Errorf("[%d] Failed to marshal input: '%v': %v", i, c.input, err) + } + if string(result) != c.result { + t.Errorf("[%d] Failed to marshal input: '%v': expected %+v, got %q", i, c.input, c.result, string(result)) + } + } +} + +func TestVerbsUnmarshalJSON(t *testing.T) { + cases := []struct { + input string + result APIResource + }{ + {`{}`, APIResource{}}, + {`{"verbs":null}`, APIResource{}}, + {`{"verbs":[]}`, APIResource{Verbs: Verbs([]string{})}}, + {`{"verbs":["delete"]}`, APIResource{Verbs: Verbs([]string{"delete"})}}, + } + + for i, c := range cases { + var result APIResource + if err := codec.NewDecoderBytes([]byte(c.input), new(codec.JsonHandle)).Decode(&result); err != nil { + t.Errorf("[%d] Failed to unmarshal input '%v': %v", i, c.input, err) + } + if !reflect.DeepEqual(result, c.result) { + t.Errorf("[%d] Failed to unmarshal input '%v': expected %+v, got %+v", i, c.input, c.result, result) + } + } +} + +func TestVerbsUgorjiUnmarshalJSON(t *testing.T) { + cases := []struct { + input string + result APIResource + }{ + {`{}`, APIResource{}}, + {`{"verbs":null}`, APIResource{}}, + {`{"verbs":[]}`, APIResource{Verbs: Verbs([]string{})}}, + {`{"verbs":["delete"]}`, APIResource{Verbs: Verbs([]string{"delete"})}}, + } + + for i, c := range cases { + var result APIResource + if err := json.Unmarshal([]byte(c.input), &result); err != nil { + t.Errorf("[%d] Failed to unmarshal input '%v': %v", i, c.input, err) + } + if !reflect.DeepEqual(result, c.result) { + t.Errorf("[%d] Failed to unmarshal input '%v': expected %+v, got %+v", i, c.input, c.result, result) + } + } +} + +func TestVerbsProto(t *testing.T) { + cases := []APIResource{ + {}, + {Verbs: Verbs([]string{})}, + {Verbs: Verbs([]string{"delete"})}, + } + + for _, input := range cases { + data, err := input.Marshal() + if err != nil { + t.Fatalf("Failed to marshal input: '%v': %v", input, err) + } + resource := APIResource{} + if err := resource.Unmarshal(data); err != nil { + t.Fatalf("Failed to unmarshal output: '%v': %v", input, err) + } + if !reflect.DeepEqual(input, resource) { + t.Errorf("Marshal->Unmarshal is not idempotent: '%v' vs '%v'", input, resource) + } + } +} diff --git a/pkg/apiserver/api_installer.go b/pkg/apiserver/api_installer.go index 9db4e80eb62..28a8442a6ae 100644 --- a/pkg/apiserver/api_installer.go +++ b/pkg/apiserver/api_installer.go @@ -62,6 +62,21 @@ type documentable interface { SwaggerDoc() map[string]string } +// toDiscoveryKubeVerb maps an action.Verb to the logical kube verb, used for discovery +var toDiscoveryKubeVerb = map[string]string{ + "CONNECT": "", // do not list in discovery. + "DELETE": "delete", + "DELETECOLLECTION": "deletecollection", + "GET": "get", + "LIST": "list", + "PATCH": "patch", + "POST": "create", + "PROXY": "proxy", + "PUT": "update", + "WATCH": "watch", + "WATCHLIST": "watch", +} + // errEmptyName is returned when API requests do not fill the name section of the path. var errEmptyName = errors.NewBadRequest("name must be provided") @@ -490,6 +505,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag allMediaTypes := append(mediaTypes, streamMediaTypes...) ws.Produces(allMediaTypes...) + kubeVerbs := map[string]struct{}{} reqScope := RequestScope{ ContextFunc: ctxFn, Serializer: a.group.Serializer, @@ -518,6 +534,14 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag namespaced = "" } + if kubeVerb, found := toDiscoveryKubeVerb[action.Verb]; found { + if len(kubeVerb) != 0 { + kubeVerbs[kubeVerb] = struct{}{} + } + } else { + return nil, fmt.Errorf("unknown action verb for discovery: %s", action.Verb) + } + switch action.Verb { case "GET": // Get a resource. var handler restful.RouteFunction @@ -754,6 +778,13 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag } // Note: update GetAuthorizerAttributes() when adding a custom handler. } + + apiResource.Verbs = make([]string, 0, len(kubeVerbs)) + for kubeVerb := range kubeVerbs { + apiResource.Verbs = append(apiResource.Verbs, kubeVerb) + } + sort.Strings(apiResource.Verbs) + return &apiResource, nil } diff --git a/pkg/auth/authorizer/interfaces.go b/pkg/auth/authorizer/interfaces.go index e23ea45960b..7255a5040d5 100644 --- a/pkg/auth/authorizer/interfaces.go +++ b/pkg/auth/authorizer/interfaces.go @@ -28,7 +28,7 @@ type Attributes interface { // GetUser returns the user.Info object to authorize GetUser() user.Info - // GetVerb returns the kube verb associated with API requests (this includes get, list, watch, create, update, patch, delete, and proxy), + // GetVerb returns the kube verb associated with API requests (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy), // or the lowercased HTTP verb associated with non-API requests (this includes get, put, post, patch, and delete) GetVerb() string