Merge pull request #80074 from yue9944882/bugfix/publish-path-parameter

Fixes missing path parameter to CRD restful container
This commit is contained in:
Kubernetes Prow Robot 2019-08-08 10:03:45 -07:00 committed by GitHub
commit 96dc74a437
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 147 additions and 31 deletions

View File

@ -54,11 +54,14 @@ go_test(
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/endpoints:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library", "//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/github.com/google/go-cmp/cmp:go_default_library", "//vendor/github.com/google/go-cmp/cmp:go_default_library",
"//vendor/github.com/google/gofuzz:go_default_library", "//vendor/github.com/google/gofuzz:go_default_library",
"//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library", "//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library",
"//vendor/github.com/googleapis/gnostic/compiler:go_default_library", "//vendor/github.com/googleapis/gnostic/compiler:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/github.com/stretchr/testify/require:go_default_library",
"//vendor/gopkg.in/yaml.v2:go_default_library", "//vendor/gopkg.in/yaml.v2:go_default_library",
"//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library", "//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library",
], ],

View File

@ -52,6 +52,9 @@ const (
var ( var (
swaggerPartialObjectMetadataDescriptions = metav1beta1.PartialObjectMetadata{}.SwaggerDoc() swaggerPartialObjectMetadataDescriptions = metav1beta1.PartialObjectMetadata{}.SwaggerDoc()
nameToken = "{name}"
namespaceToken = "{namespace}"
) )
var definitions map[string]common.OpenAPIDefinition var definitions map[string]common.OpenAPIDefinition
@ -96,33 +99,33 @@ func BuildSwagger(crd *apiextensions.CustomResourceDefinition, version string) (
routes := make([]*restful.RouteBuilder, 0) routes := make([]*restful.RouteBuilder, 0)
root := fmt.Sprintf("/apis/%s/%s/%s", b.group, b.version, b.plural) root := fmt.Sprintf("/apis/%s/%s/%s", b.group, b.version, b.plural)
if b.namespaced { if b.namespaced {
routes = append(routes, b.buildRoute(root, "", "GET", "list", sampleList). routes = append(routes, b.buildRoute(root, "", "GET", "list", "list", sampleList).Operation("list"+b.kind+"ForAllNamespaces"))
Operation("list"+b.kind+"ForAllNamespaces"))
root = fmt.Sprintf("/apis/%s/%s/namespaces/{namespace}/%s", b.group, b.version, b.plural) root = fmt.Sprintf("/apis/%s/%s/namespaces/{namespace}/%s", b.group, b.version, b.plural)
} }
routes = append(routes, b.buildRoute(root, "", "GET", "list", sampleList)) routes = append(routes, b.buildRoute(root, "", "GET", "list", "list", sampleList))
routes = append(routes, b.buildRoute(root, "", "POST", "create", sample).Reads(sample)) routes = append(routes, b.buildRoute(root, "", "POST", "post", "create", sample).Reads(sample))
routes = append(routes, b.buildRoute(root, "", "DELETE", "deletecollection", status)) routes = append(routes, b.buildRoute(root, "", "DELETE", "deletecollection", "deletecollection", status))
routes = append(routes, b.buildRoute(root, "/{name}", "GET", "read", sample)) routes = append(routes, b.buildRoute(root, "/{name}", "GET", "get", "read", sample))
routes = append(routes, b.buildRoute(root, "/{name}", "PUT", "replace", sample).Reads(sample)) routes = append(routes, b.buildRoute(root, "/{name}", "PUT", "put", "replace", sample).Reads(sample))
routes = append(routes, b.buildRoute(root, "/{name}", "DELETE", "delete", status)) routes = append(routes, b.buildRoute(root, "/{name}", "DELETE", "delete", "delete", status))
routes = append(routes, b.buildRoute(root, "/{name}", "PATCH", "patch", sample).Reads(patch)) routes = append(routes, b.buildRoute(root, "/{name}", "PATCH", "patch", "patch", sample).Reads(patch))
subresources, err := apiextensions.GetSubresourcesForVersion(crd, version) subresources, err := apiextensions.GetSubresourcesForVersion(crd, version)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if subresources != nil && subresources.Status != nil { if subresources != nil && subresources.Status != nil {
routes = append(routes, b.buildRoute(root, "/{name}/status", "GET", "read", sample)) routes = append(routes, b.buildRoute(root, "/{name}/status", "GET", "get", "read", sample))
routes = append(routes, b.buildRoute(root, "/{name}/status", "PUT", "replace", sample).Reads(sample)) routes = append(routes, b.buildRoute(root, "/{name}/status", "PUT", "put", "replace", sample).Reads(sample))
routes = append(routes, b.buildRoute(root, "/{name}/status", "PATCH", "patch", sample).Reads(patch)) routes = append(routes, b.buildRoute(root, "/{name}/status", "PATCH", "patch", "patch", sample).Reads(patch))
} }
if subresources != nil && subresources.Scale != nil { if subresources != nil && subresources.Scale != nil {
routes = append(routes, b.buildRoute(root, "/{name}/scale", "GET", "read", scale)) routes = append(routes, b.buildRoute(root, "/{name}/scale", "GET", "get", "read", scale))
routes = append(routes, b.buildRoute(root, "/{name}/scale", "PUT", "replace", scale).Reads(scale)) routes = append(routes, b.buildRoute(root, "/{name}/scale", "PUT", "put", "replace", scale).Reads(scale))
routes = append(routes, b.buildRoute(root, "/{name}/scale", "PATCH", "patch", scale).Reads(patch)) routes = append(routes, b.buildRoute(root, "/{name}/scale", "PATCH", "patch", "patch", scale).Reads(patch))
} }
for _, route := range routes { for _, route := range routes {
@ -189,9 +192,9 @@ func subresource(path string) string {
panic("failed to parse subresource; invalid path") panic("failed to parse subresource; invalid path")
} }
func (b *builder) descriptionFor(path, verb string) string { func (b *builder) descriptionFor(path, operationVerb string) string {
var article string var article string
switch verb { switch operationVerb {
case "list": case "list":
article = " objects of kind " article = " objects of kind "
case "read", "replace": case "read", "replace":
@ -209,7 +212,7 @@ func (b *builder) descriptionFor(path, verb string) string {
if len(sub) > 0 { if len(sub) > 0 {
sub = " " + sub + " of" sub = " " + sub + " of"
} }
switch verb { switch operationVerb {
case "patch": case "patch":
description = "partially update" + sub + article + b.kind description = "partially update" + sub + article + b.kind
case "deletecollection": case "deletecollection":
@ -219,7 +222,7 @@ func (b *builder) descriptionFor(path, verb string) string {
} }
description = "delete collection of" + sub + " " + b.kind description = "delete collection of" + sub + " " + b.kind
default: default:
description = verb + sub + article + b.kind description = operationVerb + sub + article + b.kind
} }
return description return description
@ -229,29 +232,35 @@ func (b *builder) descriptionFor(path, verb string) string {
// action can be one of: GET, PUT, PATCH, POST, DELETE; // action can be one of: GET, PUT, PATCH, POST, DELETE;
// verb can be one of: list, read, replace, patch, create, delete, deletecollection; // verb can be one of: list, read, replace, patch, create, delete, deletecollection;
// sample is the sample Go type for response type. // sample is the sample Go type for response type.
func (b *builder) buildRoute(root, path, action, verb string, sample interface{}) *restful.RouteBuilder { func (b *builder) buildRoute(root, path, httpMethod, actionVerb, operationVerb string, sample interface{}) *restful.RouteBuilder {
var namespaced string var namespaced string
if b.namespaced { if b.namespaced {
namespaced = "Namespaced" namespaced = "Namespaced"
} }
route := b.ws.Method(action). route := b.ws.Method(httpMethod).
Path(root+path). Path(root+path).
To(func(req *restful.Request, res *restful.Response) {}). To(func(req *restful.Request, res *restful.Response) {}).
Doc(b.descriptionFor(path, verb)). Doc(b.descriptionFor(path, operationVerb)).
Param(b.ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). Param(b.ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
Operation(verb+namespaced+b.kind+strings.Title(subresource(path))). Operation(operationVerb+namespaced+b.kind+strings.Title(subresource(path))).
Metadata(endpoints.ROUTE_META_GVK, metav1.GroupVersionKind{ Metadata(endpoints.ROUTE_META_GVK, metav1.GroupVersionKind{
Group: b.group, Group: b.group,
Version: b.version, Version: b.version,
Kind: b.kind, Kind: b.kind,
}). }).
Metadata(endpoints.ROUTE_META_ACTION, strings.ToLower(action)). Metadata(endpoints.ROUTE_META_ACTION, actionVerb).
Produces("application/json", "application/yaml"). Produces("application/json", "application/yaml").
Returns(http.StatusOK, "OK", sample). Returns(http.StatusOK, "OK", sample).
Writes(sample) Writes(sample)
if strings.Contains(root, namespaceToken) || strings.Contains(path, namespaceToken) {
route.Param(b.ws.PathParameter("namespace", "object name and auth scope, such as for teams and projects").DataType("string"))
}
if strings.Contains(root, nameToken) || strings.Contains(path, nameToken) {
route.Param(b.ws.PathParameter("name", "name of the "+b.kind).DataType("string"))
}
// Build consume media types // Build consume media types
if action == "PATCH" { if httpMethod == "PATCH" {
route.Consumes("application/json-patch+json", route.Consumes("application/json-patch+json",
"application/merge-patch+json", "application/merge-patch+json",
"application/strategic-merge-patch+json") "application/strategic-merge-patch+json")
@ -260,16 +269,16 @@ func (b *builder) buildRoute(root, path, action, verb string, sample interface{}
} }
// Build option parameters // Build option parameters
switch verb { switch actionVerb {
case "get": case "get":
// TODO: CRD support for export is still under consideration // TODO: CRD support for export is still under consideration
endpoints.AddObjectParams(b.ws, route, &metav1.GetOptions{}) endpoints.AddObjectParams(b.ws, route, &metav1.GetOptions{})
case "list", "deletecollection": case "list", "deletecollection":
endpoints.AddObjectParams(b.ws, route, &metav1.ListOptions{}) endpoints.AddObjectParams(b.ws, route, &metav1.ListOptions{})
case "replace", "patch": case "put", "patch":
// TODO: PatchOption added in feature branch but not in master yet // TODO: PatchOption added in feature branch but not in master yet
endpoints.AddObjectParams(b.ws, route, &metav1.UpdateOptions{}) endpoints.AddObjectParams(b.ws, route, &metav1.UpdateOptions{})
case "create": case "post":
endpoints.AddObjectParams(b.ws, route, &metav1.CreateOptions{}) endpoints.AddObjectParams(b.ws, route, &metav1.CreateOptions{})
case "delete": case "delete":
endpoints.AddObjectParams(b.ws, route, &metav1.DeleteOptions{}) endpoints.AddObjectParams(b.ws, route, &metav1.DeleteOptions{})
@ -277,13 +286,13 @@ func (b *builder) buildRoute(root, path, action, verb string, sample interface{}
} }
// Build responses // Build responses
switch verb { switch actionVerb {
case "create": case "post":
route.Returns(http.StatusAccepted, "Accepted", sample) route.Returns(http.StatusAccepted, "Accepted", sample)
route.Returns(http.StatusCreated, "Created", sample) route.Returns(http.StatusCreated, "Created", sample)
case "delete": case "delete":
route.Returns(http.StatusAccepted, "Accepted", sample) route.Returns(http.StatusAccepted, "Accepted", sample)
case "replace": case "put":
route.Returns(http.StatusCreated, "Created", sample) route.Returns(http.StatusCreated, "Created", sample)
} }

View File

@ -21,6 +21,8 @@ import (
"testing" "testing"
"github.com/go-openapi/spec" "github.com/go-openapi/spec"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
@ -28,6 +30,7 @@ import (
"k8s.io/apimachinery/pkg/util/diff" "k8s.io/apimachinery/pkg/util/diff"
"k8s.io/apimachinery/pkg/util/json" "k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/endpoints"
) )
func TestNewBuilder(t *testing.T) { func TestNewBuilder(t *testing.T) {
@ -417,6 +420,107 @@ func TestNewBuilder(t *testing.T) {
} }
} }
func TestCRDRouteParameterBuilder(t *testing.T) {
testCRDKind := "Foo"
testCRDGroup := "foo-group"
testCRDVersion := "foo-version"
testCRDResourceName := "foos"
testCases := []struct {
scope apiextensions.ResourceScope
paths map[string]struct {
expectNamespaceParam bool
expectNameParam bool
expectedActions sets.String
}
}{
{
scope: apiextensions.NamespaceScoped,
paths: map[string]struct {
expectNamespaceParam bool
expectNameParam bool
expectedActions sets.String
}{
"/apis/foo-group/foo-version/foos": {expectNamespaceParam: false, expectNameParam: false, expectedActions: sets.NewString("list")},
"/apis/foo-group/foo-version/namespaces/{namespace}/foos": {expectNamespaceParam: true, expectNameParam: false, expectedActions: sets.NewString("post", "list", "deletecollection")},
"/apis/foo-group/foo-version/namespaces/{namespace}/foos/{name}": {expectNamespaceParam: true, expectNameParam: true, expectedActions: sets.NewString("get", "put", "patch", "delete")},
"/apis/foo-group/foo-version/namespaces/{namespace}/foos/{name}/scale": {expectNamespaceParam: true, expectNameParam: true, expectedActions: sets.NewString("get", "patch", "put")},
"/apis/foo-group/foo-version/namespaces/{namespace}/foos/{name}/status": {expectNamespaceParam: true, expectNameParam: true, expectedActions: sets.NewString("get", "patch", "put")},
},
},
{
scope: apiextensions.ClusterScoped,
paths: map[string]struct {
expectNamespaceParam bool
expectNameParam bool
expectedActions sets.String
}{
"/apis/foo-group/foo-version/foos": {expectNamespaceParam: false, expectNameParam: false, expectedActions: sets.NewString("post", "list", "deletecollection")},
"/apis/foo-group/foo-version/foos/{name}": {expectNamespaceParam: false, expectNameParam: true, expectedActions: sets.NewString("get", "put", "patch", "delete")},
"/apis/foo-group/foo-version/foos/{name}/scale": {expectNamespaceParam: false, expectNameParam: true, expectedActions: sets.NewString("get", "patch", "put")},
"/apis/foo-group/foo-version/foos/{name}/status": {expectNamespaceParam: false, expectNameParam: true, expectedActions: sets.NewString("get", "patch", "put")},
},
},
}
for _, testCase := range testCases {
testNamespacedCRD := &apiextensions.CustomResourceDefinition{
Spec: apiextensions.CustomResourceDefinitionSpec{
Scope: testCase.scope,
Group: testCRDGroup,
Names: apiextensions.CustomResourceDefinitionNames{
Kind: testCRDKind,
Plural: testCRDResourceName,
},
Versions: []apiextensions.CustomResourceDefinitionVersion{
{
Name: testCRDVersion,
},
},
Subresources: &apiextensions.CustomResourceSubresources{
Status: &apiextensions.CustomResourceSubresourceStatus{},
Scale: &apiextensions.CustomResourceSubresourceScale{},
},
},
}
swagger, err := BuildSwagger(testNamespacedCRD, testCRDVersion)
require.NoError(t, err)
require.Equal(t, len(testCase.paths), len(swagger.Paths.Paths), testCase.scope)
for path, expected := range testCase.paths {
t.Run(path, func(t *testing.T) {
path, ok := swagger.Paths.Paths[path]
if !ok {
t.Errorf("unexpected path %v", path)
}
hasNamespaceParam := false
hasNameParam := false
for _, param := range path.Parameters {
if param.In == "path" && param.Name == "namespace" {
hasNamespaceParam = true
}
if param.In == "path" && param.Name == "name" {
hasNameParam = true
}
}
assert.Equal(t, expected.expectNamespaceParam, hasNamespaceParam)
assert.Equal(t, expected.expectNameParam, hasNameParam)
actions := sets.NewString()
for _, operation := range []*spec.Operation{path.Get, path.Post, path.Put, path.Patch, path.Delete} {
if operation != nil {
action, ok := operation.VendorExtensible.Extensions.GetString(endpoints.ROUTE_META_ACTION)
if ok {
actions.Insert(action)
}
}
}
assert.Equal(t, expected.expectedActions, actions)
})
}
}
}
func properties(p map[string]spec.Schema) sets.String { func properties(p map[string]spec.Schema) sets.String {
ret := sets.NewString() ret := sets.NewString()
for k := range p { for k := range p {