Skip to content

Commit 96dc74a

Browse files
authored
Merge pull request kubernetes#80074 from yue9944882/bugfix/publish-path-parameter
Fixes missing path parameter to CRD restful container
2 parents e861a8b + 18c27b7 commit 96dc74a

File tree

3 files changed

+147
-31
lines changed

3 files changed

+147
-31
lines changed

staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi/BUILD

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,14 @@ go_test(
5454
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
5555
"//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library",
5656
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
57+
"//staging/src/k8s.io/apiserver/pkg/endpoints:go_default_library",
5758
"//vendor/github.com/go-openapi/spec:go_default_library",
5859
"//vendor/github.com/google/go-cmp/cmp:go_default_library",
5960
"//vendor/github.com/google/gofuzz:go_default_library",
6061
"//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library",
6162
"//vendor/github.com/googleapis/gnostic/compiler:go_default_library",
63+
"//vendor/github.com/stretchr/testify/assert:go_default_library",
64+
"//vendor/github.com/stretchr/testify/require:go_default_library",
6265
"//vendor/gopkg.in/yaml.v2:go_default_library",
6366
"//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library",
6467
],

staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi/builder.go

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ const (
5252

5353
var (
5454
swaggerPartialObjectMetadataDescriptions = metav1beta1.PartialObjectMetadata{}.SwaggerDoc()
55+
56+
nameToken = "{name}"
57+
namespaceToken = "{namespace}"
5558
)
5659

5760
var definitions map[string]common.OpenAPIDefinition
@@ -96,33 +99,33 @@ func BuildSwagger(crd *apiextensions.CustomResourceDefinition, version string) (
9699

97100
routes := make([]*restful.RouteBuilder, 0)
98101
root := fmt.Sprintf("/apis/%s/%s/%s", b.group, b.version, b.plural)
102+
99103
if b.namespaced {
100-
routes = append(routes, b.buildRoute(root, "", "GET", "list", sampleList).
101-
Operation("list"+b.kind+"ForAllNamespaces"))
104+
routes = append(routes, b.buildRoute(root, "", "GET", "list", "list", sampleList).Operation("list"+b.kind+"ForAllNamespaces"))
102105
root = fmt.Sprintf("/apis/%s/%s/namespaces/{namespace}/%s", b.group, b.version, b.plural)
103106
}
104-
routes = append(routes, b.buildRoute(root, "", "GET", "list", sampleList))
105-
routes = append(routes, b.buildRoute(root, "", "POST", "create", sample).Reads(sample))
106-
routes = append(routes, b.buildRoute(root, "", "DELETE", "deletecollection", status))
107+
routes = append(routes, b.buildRoute(root, "", "GET", "list", "list", sampleList))
108+
routes = append(routes, b.buildRoute(root, "", "POST", "post", "create", sample).Reads(sample))
109+
routes = append(routes, b.buildRoute(root, "", "DELETE", "deletecollection", "deletecollection", status))
107110

108-
routes = append(routes, b.buildRoute(root, "/{name}", "GET", "read", sample))
109-
routes = append(routes, b.buildRoute(root, "/{name}", "PUT", "replace", sample).Reads(sample))
110-
routes = append(routes, b.buildRoute(root, "/{name}", "DELETE", "delete", status))
111-
routes = append(routes, b.buildRoute(root, "/{name}", "PATCH", "patch", sample).Reads(patch))
111+
routes = append(routes, b.buildRoute(root, "/{name}", "GET", "get", "read", sample))
112+
routes = append(routes, b.buildRoute(root, "/{name}", "PUT", "put", "replace", sample).Reads(sample))
113+
routes = append(routes, b.buildRoute(root, "/{name}", "DELETE", "delete", "delete", status))
114+
routes = append(routes, b.buildRoute(root, "/{name}", "PATCH", "patch", "patch", sample).Reads(patch))
112115

113116
subresources, err := apiextensions.GetSubresourcesForVersion(crd, version)
114117
if err != nil {
115118
return nil, err
116119
}
117120
if subresources != nil && subresources.Status != nil {
118-
routes = append(routes, b.buildRoute(root, "/{name}/status", "GET", "read", sample))
119-
routes = append(routes, b.buildRoute(root, "/{name}/status", "PUT", "replace", sample).Reads(sample))
120-
routes = append(routes, b.buildRoute(root, "/{name}/status", "PATCH", "patch", sample).Reads(patch))
121+
routes = append(routes, b.buildRoute(root, "/{name}/status", "GET", "get", "read", sample))
122+
routes = append(routes, b.buildRoute(root, "/{name}/status", "PUT", "put", "replace", sample).Reads(sample))
123+
routes = append(routes, b.buildRoute(root, "/{name}/status", "PATCH", "patch", "patch", sample).Reads(patch))
121124
}
122125
if subresources != nil && subresources.Scale != nil {
123-
routes = append(routes, b.buildRoute(root, "/{name}/scale", "GET", "read", scale))
124-
routes = append(routes, b.buildRoute(root, "/{name}/scale", "PUT", "replace", scale).Reads(scale))
125-
routes = append(routes, b.buildRoute(root, "/{name}/scale", "PATCH", "patch", scale).Reads(patch))
126+
routes = append(routes, b.buildRoute(root, "/{name}/scale", "GET", "get", "read", scale))
127+
routes = append(routes, b.buildRoute(root, "/{name}/scale", "PUT", "put", "replace", scale).Reads(scale))
128+
routes = append(routes, b.buildRoute(root, "/{name}/scale", "PATCH", "patch", "patch", scale).Reads(patch))
126129
}
127130

128131
for _, route := range routes {
@@ -189,9 +192,9 @@ func subresource(path string) string {
189192
panic("failed to parse subresource; invalid path")
190193
}
191194

192-
func (b *builder) descriptionFor(path, verb string) string {
195+
func (b *builder) descriptionFor(path, operationVerb string) string {
193196
var article string
194-
switch verb {
197+
switch operationVerb {
195198
case "list":
196199
article = " objects of kind "
197200
case "read", "replace":
@@ -209,7 +212,7 @@ func (b *builder) descriptionFor(path, verb string) string {
209212
if len(sub) > 0 {
210213
sub = " " + sub + " of"
211214
}
212-
switch verb {
215+
switch operationVerb {
213216
case "patch":
214217
description = "partially update" + sub + article + b.kind
215218
case "deletecollection":
@@ -219,7 +222,7 @@ func (b *builder) descriptionFor(path, verb string) string {
219222
}
220223
description = "delete collection of" + sub + " " + b.kind
221224
default:
222-
description = verb + sub + article + b.kind
225+
description = operationVerb + sub + article + b.kind
223226
}
224227

225228
return description
@@ -229,29 +232,35 @@ func (b *builder) descriptionFor(path, verb string) string {
229232
// action can be one of: GET, PUT, PATCH, POST, DELETE;
230233
// verb can be one of: list, read, replace, patch, create, delete, deletecollection;
231234
// sample is the sample Go type for response type.
232-
func (b *builder) buildRoute(root, path, action, verb string, sample interface{}) *restful.RouteBuilder {
235+
func (b *builder) buildRoute(root, path, httpMethod, actionVerb, operationVerb string, sample interface{}) *restful.RouteBuilder {
233236
var namespaced string
234237
if b.namespaced {
235238
namespaced = "Namespaced"
236239
}
237-
route := b.ws.Method(action).
240+
route := b.ws.Method(httpMethod).
238241
Path(root+path).
239242
To(func(req *restful.Request, res *restful.Response) {}).
240-
Doc(b.descriptionFor(path, verb)).
243+
Doc(b.descriptionFor(path, operationVerb)).
241244
Param(b.ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
242-
Operation(verb+namespaced+b.kind+strings.Title(subresource(path))).
245+
Operation(operationVerb+namespaced+b.kind+strings.Title(subresource(path))).
243246
Metadata(endpoints.ROUTE_META_GVK, metav1.GroupVersionKind{
244247
Group: b.group,
245248
Version: b.version,
246249
Kind: b.kind,
247250
}).
248-
Metadata(endpoints.ROUTE_META_ACTION, strings.ToLower(action)).
251+
Metadata(endpoints.ROUTE_META_ACTION, actionVerb).
249252
Produces("application/json", "application/yaml").
250253
Returns(http.StatusOK, "OK", sample).
251254
Writes(sample)
255+
if strings.Contains(root, namespaceToken) || strings.Contains(path, namespaceToken) {
256+
route.Param(b.ws.PathParameter("namespace", "object name and auth scope, such as for teams and projects").DataType("string"))
257+
}
258+
if strings.Contains(root, nameToken) || strings.Contains(path, nameToken) {
259+
route.Param(b.ws.PathParameter("name", "name of the "+b.kind).DataType("string"))
260+
}
252261

253262
// Build consume media types
254-
if action == "PATCH" {
263+
if httpMethod == "PATCH" {
255264
route.Consumes("application/json-patch+json",
256265
"application/merge-patch+json",
257266
"application/strategic-merge-patch+json")
@@ -260,30 +269,30 @@ func (b *builder) buildRoute(root, path, action, verb string, sample interface{}
260269
}
261270

262271
// Build option parameters
263-
switch verb {
272+
switch actionVerb {
264273
case "get":
265274
// TODO: CRD support for export is still under consideration
266275
endpoints.AddObjectParams(b.ws, route, &metav1.GetOptions{})
267276
case "list", "deletecollection":
268277
endpoints.AddObjectParams(b.ws, route, &metav1.ListOptions{})
269-
case "replace", "patch":
278+
case "put", "patch":
270279
// TODO: PatchOption added in feature branch but not in master yet
271280
endpoints.AddObjectParams(b.ws, route, &metav1.UpdateOptions{})
272-
case "create":
281+
case "post":
273282
endpoints.AddObjectParams(b.ws, route, &metav1.CreateOptions{})
274283
case "delete":
275284
endpoints.AddObjectParams(b.ws, route, &metav1.DeleteOptions{})
276285
route.Reads(&metav1.DeleteOptions{}).ParameterNamed("body").Required(false)
277286
}
278287

279288
// Build responses
280-
switch verb {
281-
case "create":
289+
switch actionVerb {
290+
case "post":
282291
route.Returns(http.StatusAccepted, "Accepted", sample)
283292
route.Returns(http.StatusCreated, "Created", sample)
284293
case "delete":
285294
route.Returns(http.StatusAccepted, "Accepted", sample)
286-
case "replace":
295+
case "put":
287296
route.Returns(http.StatusCreated, "Created", sample)
288297
}
289298

staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi/builder_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ import (
2121
"testing"
2222

2323
"github.com/go-openapi/spec"
24+
"github.com/stretchr/testify/assert"
25+
"github.com/stretchr/testify/require"
2426

2527
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
2628
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
2729
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
2830
"k8s.io/apimachinery/pkg/util/diff"
2931
"k8s.io/apimachinery/pkg/util/json"
3032
"k8s.io/apimachinery/pkg/util/sets"
33+
"k8s.io/apiserver/pkg/endpoints"
3134
)
3235

3336
func TestNewBuilder(t *testing.T) {
@@ -417,6 +420,107 @@ func TestNewBuilder(t *testing.T) {
417420
}
418421
}
419422

423+
func TestCRDRouteParameterBuilder(t *testing.T) {
424+
testCRDKind := "Foo"
425+
testCRDGroup := "foo-group"
426+
testCRDVersion := "foo-version"
427+
testCRDResourceName := "foos"
428+
429+
testCases := []struct {
430+
scope apiextensions.ResourceScope
431+
paths map[string]struct {
432+
expectNamespaceParam bool
433+
expectNameParam bool
434+
expectedActions sets.String
435+
}
436+
}{
437+
{
438+
scope: apiextensions.NamespaceScoped,
439+
paths: map[string]struct {
440+
expectNamespaceParam bool
441+
expectNameParam bool
442+
expectedActions sets.String
443+
}{
444+
"/apis/foo-group/foo-version/foos": {expectNamespaceParam: false, expectNameParam: false, expectedActions: sets.NewString("list")},
445+
"/apis/foo-group/foo-version/namespaces/{namespace}/foos": {expectNamespaceParam: true, expectNameParam: false, expectedActions: sets.NewString("post", "list", "deletecollection")},
446+
"/apis/foo-group/foo-version/namespaces/{namespace}/foos/{name}": {expectNamespaceParam: true, expectNameParam: true, expectedActions: sets.NewString("get", "put", "patch", "delete")},
447+
"/apis/foo-group/foo-version/namespaces/{namespace}/foos/{name}/scale": {expectNamespaceParam: true, expectNameParam: true, expectedActions: sets.NewString("get", "patch", "put")},
448+
"/apis/foo-group/foo-version/namespaces/{namespace}/foos/{name}/status": {expectNamespaceParam: true, expectNameParam: true, expectedActions: sets.NewString("get", "patch", "put")},
449+
},
450+
},
451+
{
452+
scope: apiextensions.ClusterScoped,
453+
paths: map[string]struct {
454+
expectNamespaceParam bool
455+
expectNameParam bool
456+
expectedActions sets.String
457+
}{
458+
"/apis/foo-group/foo-version/foos": {expectNamespaceParam: false, expectNameParam: false, expectedActions: sets.NewString("post", "list", "deletecollection")},
459+
"/apis/foo-group/foo-version/foos/{name}": {expectNamespaceParam: false, expectNameParam: true, expectedActions: sets.NewString("get", "put", "patch", "delete")},
460+
"/apis/foo-group/foo-version/foos/{name}/scale": {expectNamespaceParam: false, expectNameParam: true, expectedActions: sets.NewString("get", "patch", "put")},
461+
"/apis/foo-group/foo-version/foos/{name}/status": {expectNamespaceParam: false, expectNameParam: true, expectedActions: sets.NewString("get", "patch", "put")},
462+
},
463+
},
464+
}
465+
466+
for _, testCase := range testCases {
467+
testNamespacedCRD := &apiextensions.CustomResourceDefinition{
468+
Spec: apiextensions.CustomResourceDefinitionSpec{
469+
Scope: testCase.scope,
470+
Group: testCRDGroup,
471+
Names: apiextensions.CustomResourceDefinitionNames{
472+
Kind: testCRDKind,
473+
Plural: testCRDResourceName,
474+
},
475+
Versions: []apiextensions.CustomResourceDefinitionVersion{
476+
{
477+
Name: testCRDVersion,
478+
},
479+
},
480+
Subresources: &apiextensions.CustomResourceSubresources{
481+
Status: &apiextensions.CustomResourceSubresourceStatus{},
482+
Scale: &apiextensions.CustomResourceSubresourceScale{},
483+
},
484+
},
485+
}
486+
swagger, err := BuildSwagger(testNamespacedCRD, testCRDVersion)
487+
require.NoError(t, err)
488+
require.Equal(t, len(testCase.paths), len(swagger.Paths.Paths), testCase.scope)
489+
for path, expected := range testCase.paths {
490+
t.Run(path, func(t *testing.T) {
491+
path, ok := swagger.Paths.Paths[path]
492+
if !ok {
493+
t.Errorf("unexpected path %v", path)
494+
}
495+
496+
hasNamespaceParam := false
497+
hasNameParam := false
498+
for _, param := range path.Parameters {
499+
if param.In == "path" && param.Name == "namespace" {
500+
hasNamespaceParam = true
501+
}
502+
if param.In == "path" && param.Name == "name" {
503+
hasNameParam = true
504+
}
505+
}
506+
assert.Equal(t, expected.expectNamespaceParam, hasNamespaceParam)
507+
assert.Equal(t, expected.expectNameParam, hasNameParam)
508+
509+
actions := sets.NewString()
510+
for _, operation := range []*spec.Operation{path.Get, path.Post, path.Put, path.Patch, path.Delete} {
511+
if operation != nil {
512+
action, ok := operation.VendorExtensible.Extensions.GetString(endpoints.ROUTE_META_ACTION)
513+
if ok {
514+
actions.Insert(action)
515+
}
516+
}
517+
}
518+
assert.Equal(t, expected.expectedActions, actions)
519+
})
520+
}
521+
}
522+
}
523+
420524
func properties(p map[string]spec.Schema) sets.String {
421525
ret := sets.NewString()
422526
for k := range p {

0 commit comments

Comments
 (0)