From b39be9eeea3e59be4a7b1decba0655ca8e1f2707 Mon Sep 17 00:00:00 2001 From: rithwik-01 Date: Fri, 26 Jun 2026 22:00:08 -0700 Subject: [PATCH 1/3] Detect array query parameter types in OpenAPI builders When a restful parameter DataType is prefixed with '[]' (e.g. '[]string', '[]integer'), emit type: array with the correct items sub-schema instead of treating it as a scalar type. This allows upstream callers to pass compound DataType values through restful plumbing without losing the array semantics, fixing OpenAPI spec generation for parameters like PodExecOptions.Command []string. Related: kubernetes/kubernetes#140050 --- pkg/builder/openapi.go | 25 ++++++++++++++++++++++--- pkg/builder3/openapi.go | 35 ++++++++++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/pkg/builder/openapi.go b/pkg/builder/openapi.go index c33c59626..4a1a67cbf 100644 --- a/pkg/builder/openapi.go +++ b/pkg/builder/openapi.go @@ -447,9 +447,28 @@ func (o *openAPI) buildParameter(restParam common.Parameter, bodySample interfac default: return ret, fmt.Errorf("unknown restful operation kind : %v", restParam.Kind()) } - openAPIType, openAPIFormat := common.OpenAPITypeFormat(restParam.DataType()) - if openAPIType == "" { - return ret, fmt.Errorf("non-body Restful parameter type should be a simple type, but got : %v", restParam.DataType()) + dataType := restParam.DataType() + var openAPIType, openAPIFormat, itemsType string + if strings.HasPrefix(dataType, "[]") { + // Array type: element type is encoded as "[]string" or "[]integer" + itemsType = dataType[2:] + itemsAPIType, itemsAPIFormat := common.OpenAPITypeFormat(itemsType) + if itemsAPIType == "" { + return ret, fmt.Errorf("non-body Restful parameter array element type should be a simple type, but got: %v", itemsType) + } + openAPIType = "array" + openAPIFormat = "" + ret.Items = &spec.Items{ + SimpleSchema: spec.SimpleSchema{ + Type: itemsAPIType, + Format: itemsAPIFormat, + }, + } + } else { + openAPIType, openAPIFormat = common.OpenAPITypeFormat(dataType) + if openAPIType == "" { + return ret, fmt.Errorf("non-body Restful parameter type should be a simple type, but got : %v", dataType) + } } ret.Type = openAPIType ret.Format = openAPIFormat diff --git a/pkg/builder3/openapi.go b/pkg/builder3/openapi.go index 94d8706d8..aed7dd376 100644 --- a/pkg/builder3/openapi.go +++ b/pkg/builder3/openapi.go @@ -419,16 +419,41 @@ func (o *openAPI) buildParameter(restParam common.Parameter) (ret *spec3.Paramet default: return ret, fmt.Errorf("unsupported restful parameter kind : %v", restParam.Kind()) } - openAPIType, openAPIFormat := common.OpenAPITypeFormat(restParam.DataType()) - if openAPIType == "" { - return ret, fmt.Errorf("non-body Restful parameter type should be a simple type, but got : %v", restParam.DataType()) + dataType := restParam.DataType() + var schemaType string + var schemaFormat string + var itemsSchema *spec.SchemaOrArray + + if strings.HasPrefix(dataType, "[]") { + // Array type: element type is encoded as "[]string" or "[]integer" + itemsType := dataType[2:] + itemsAPIType, itemsAPIFormat := common.OpenAPITypeFormat(itemsType) + if itemsAPIType == "" { + return ret, fmt.Errorf("non-body Restful parameter array element type should be a simple type, but got: %v", itemsType) + } + schemaType = "array" + schemaFormat = "" + itemsSchema = &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{itemsAPIType}, + Format: itemsAPIFormat, + }, + }, + } + } else { + schemaType, schemaFormat = common.OpenAPITypeFormat(dataType) + if schemaType == "" { + return ret, fmt.Errorf("non-body Restful parameter type should be a simple type, but got : %v", dataType) + } } ret.Schema = &spec.Schema{ SchemaProps: spec.SchemaProps{ - Type: []string{openAPIType}, - Format: openAPIFormat, + Type: []string{schemaType}, + Format: schemaFormat, UniqueItems: !restParam.AllowMultiple(), + Items: itemsSchema, }, } return ret, nil From 382bbc2b8d078dd648a2d68f094b11afb589a768 Mon Sep 17 00:00:00 2001 From: rithwik-01 Date: Fri, 3 Jul 2026 18:15:06 -0700 Subject: [PATCH 2/3] fix: guard []byte scalar in array detection, add collectionFormat/Style/Explode for AllowMultiple - Check OpenAPITypeFormat(dataType) before stripping [] prefix so known scalar types like "[]byte" (base64-encoded string) aren't incorrectly emitted as array[uint8]. - V2: set CollectionFormat="multi" when array parameter has AllowMultiple. - V3: set Style="form", Explode=true when array parameter has AllowMultiple. --- pkg/builder/openapi.go | 17 +++++++++-------- pkg/builder3/openapi.go | 14 ++++++++------ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/pkg/builder/openapi.go b/pkg/builder/openapi.go index 4a1a67cbf..7ab2d024b 100644 --- a/pkg/builder/openapi.go +++ b/pkg/builder/openapi.go @@ -448,10 +448,11 @@ func (o *openAPI) buildParameter(restParam common.Parameter, bodySample interfac return ret, fmt.Errorf("unknown restful operation kind : %v", restParam.Kind()) } dataType := restParam.DataType() - var openAPIType, openAPIFormat, itemsType string - if strings.HasPrefix(dataType, "[]") { + var openAPIType, openAPIFormat string + openAPIType, openAPIFormat = common.OpenAPITypeFormat(dataType) + if openAPIType == "" && strings.HasPrefix(dataType, "[]") { // Array type: element type is encoded as "[]string" or "[]integer" - itemsType = dataType[2:] + itemsType := dataType[2:] itemsAPIType, itemsAPIFormat := common.OpenAPITypeFormat(itemsType) if itemsAPIType == "" { return ret, fmt.Errorf("non-body Restful parameter array element type should be a simple type, but got: %v", itemsType) @@ -464,15 +465,15 @@ func (o *openAPI) buildParameter(restParam common.Parameter, bodySample interfac Format: itemsAPIFormat, }, } - } else { - openAPIType, openAPIFormat = common.OpenAPITypeFormat(dataType) - if openAPIType == "" { - return ret, fmt.Errorf("non-body Restful parameter type should be a simple type, but got : %v", dataType) - } + } else if openAPIType == "" { + return ret, fmt.Errorf("non-body Restful parameter type should be a simple type, but got : %v", dataType) } ret.Type = openAPIType ret.Format = openAPIFormat ret.UniqueItems = !restParam.AllowMultiple() + if openAPIType == "array" && restParam.AllowMultiple() { + ret.CollectionFormat = "multi" + } return ret, nil } diff --git a/pkg/builder3/openapi.go b/pkg/builder3/openapi.go index aed7dd376..2d2541147 100644 --- a/pkg/builder3/openapi.go +++ b/pkg/builder3/openapi.go @@ -424,7 +424,8 @@ func (o *openAPI) buildParameter(restParam common.Parameter) (ret *spec3.Paramet var schemaFormat string var itemsSchema *spec.SchemaOrArray - if strings.HasPrefix(dataType, "[]") { + schemaType, schemaFormat = common.OpenAPITypeFormat(dataType) + if schemaType == "" && strings.HasPrefix(dataType, "[]") { // Array type: element type is encoded as "[]string" or "[]integer" itemsType := dataType[2:] itemsAPIType, itemsAPIFormat := common.OpenAPITypeFormat(itemsType) @@ -441,11 +442,8 @@ func (o *openAPI) buildParameter(restParam common.Parameter) (ret *spec3.Paramet }, }, } - } else { - schemaType, schemaFormat = common.OpenAPITypeFormat(dataType) - if schemaType == "" { - return ret, fmt.Errorf("non-body Restful parameter type should be a simple type, but got : %v", dataType) - } + } else if schemaType == "" { + return ret, fmt.Errorf("non-body Restful parameter type should be a simple type, but got : %v", dataType) } ret.Schema = &spec.Schema{ @@ -456,6 +454,10 @@ func (o *openAPI) buildParameter(restParam common.Parameter) (ret *spec3.Paramet Items: itemsSchema, }, } + if schemaType == "array" && restParam.AllowMultiple() { + ret.Style = "form" + ret.Explode = true + } return ret, nil } From 881c0f08e92c8f3073b16097ef876a11c503e61c Mon Sep 17 00:00:00 2001 From: rithwik-01 Date: Mon, 6 Jul 2026 19:50:53 -0700 Subject: [PATCH 3/3] fix: use AllowMultiple as signal for array query parameters Replace []-prefix-based array detection with AllowMultiple() as the canonical signal for emitting type: array in OpenAPI schemas. The [] prefix is only stripped to derive the element type. V2: Always set CollectionFormat="multi" for AllowMultiple params. V3: Always set Style="form" and Explode=true for AllowMultiple params. []string without AllowMultiple is treated as a misconfiguration. --- pkg/builder/openapi.go | 42 +++++++++--------- pkg/builder/openapi_test.go | 71 +++++++++++++++++++++++++++++++ pkg/builder3/openapi.go | 65 ++++++++++++++-------------- pkg/builder3/openapi_test.go | 82 ++++++++++++++++++++++++++++++++++++ 4 files changed, 208 insertions(+), 52 deletions(-) diff --git a/pkg/builder/openapi.go b/pkg/builder/openapi.go index 7ab2d024b..865aa1e9c 100644 --- a/pkg/builder/openapi.go +++ b/pkg/builder/openapi.go @@ -448,32 +448,34 @@ func (o *openAPI) buildParameter(restParam common.Parameter, bodySample interfac return ret, fmt.Errorf("unknown restful operation kind : %v", restParam.Kind()) } dataType := restParam.DataType() - var openAPIType, openAPIFormat string - openAPIType, openAPIFormat = common.OpenAPITypeFormat(dataType) - if openAPIType == "" && strings.HasPrefix(dataType, "[]") { - // Array type: element type is encoded as "[]string" or "[]integer" - itemsType := dataType[2:] - itemsAPIType, itemsAPIFormat := common.OpenAPITypeFormat(itemsType) - if itemsAPIType == "" { - return ret, fmt.Errorf("non-body Restful parameter array element type should be a simple type, but got: %v", itemsType) - } - openAPIType = "array" - openAPIFormat = "" + + // AllowMultiple indicates an array parameter. The element type is + // derived by stripping the "[]" prefix from DataType. + elementType := dataType + if restParam.AllowMultiple() { + elementType = strings.TrimPrefix(dataType, "[]") + } + + openAPIType, openAPIFormat := common.OpenAPITypeFormat(elementType) + if openAPIType == "" { + return ret, fmt.Errorf("non-body Restful parameter type should be a simple type, but got: %v", dataType) + } + + if restParam.AllowMultiple() { + ret.Type = "array" + ret.Format = "" + ret.CollectionFormat = "multi" ret.Items = &spec.Items{ SimpleSchema: spec.SimpleSchema{ - Type: itemsAPIType, - Format: itemsAPIFormat, + Type: openAPIType, + Format: openAPIFormat, }, } - } else if openAPIType == "" { - return ret, fmt.Errorf("non-body Restful parameter type should be a simple type, but got : %v", dataType) + } else { + ret.Type = openAPIType + ret.Format = openAPIFormat } - ret.Type = openAPIType - ret.Format = openAPIFormat ret.UniqueItems = !restParam.AllowMultiple() - if openAPIType == "array" && restParam.AllowMultiple() { - ret.CollectionFormat = "multi" - } return ret, nil } diff --git a/pkg/builder/openapi_test.go b/pkg/builder/openapi_test.go index 50e7a4b22..3b46b3988 100644 --- a/pkg/builder/openapi_test.go +++ b/pkg/builder/openapi_test.go @@ -27,6 +27,7 @@ import ( "github.com/stretchr/testify/assert" openapi "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/common/restfuladapter" "k8s.io/kube-openapi/pkg/util/jsontesting" "k8s.io/kube-openapi/pkg/validation/spec" ) @@ -653,3 +654,73 @@ func TestEscapeJsonPointerInDefinitionName(t *testing.T) { }) } } + +func TestBuildParameter(t *testing.T) { + o := &openAPI{} + + // buildParameter receives *restfuladapter.ParamAdapter at runtime. + newParam := func(dt string, allowMultiple bool) openapi.Parameter { + return &restfuladapter.ParamAdapter{ + Param: restful.QueryParameter("p", "").DataType(dt).AllowMultiple(allowMultiple), + } + } + + tests := []struct { + name string + param openapi.Parameter + wantType string + wantFormat string + wantItems bool + wantErr bool + }{ + { + name: "scalar string", + param: newParam("string", false), + wantType: "string", + }, + { + name: "scalar []byte stays scalar", + param: newParam("[]byte", false), + wantType: "string", + wantFormat: "byte", + }, + { + name: "AllowMultiple=true emits array", + param: newParam("[]string", true), + wantType: "array", + wantItems: true, + }, + { + name: "AllowMultiple=true with plain DataType emits array", + param: newParam("string", true), + wantType: "array", + wantItems: true, + }, + { + name: "[]string without AllowMultiple is a misconfiguration — errors intentionally", + param: newParam("[]string", false), + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := o.buildParameter(tt.param, nil) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.wantType, got.Type) + assert.Equal(t, tt.wantFormat, got.Format) + if tt.wantItems { + assert.NotNil(t, got.Items) + assert.Equal(t, "multi", got.CollectionFormat) + } else { + assert.Nil(t, got.Items) + assert.Empty(t, got.CollectionFormat) + } + assert.True(t, got.UniqueItems != tt.param.AllowMultiple()) + }) + } +} diff --git a/pkg/builder3/openapi.go b/pkg/builder3/openapi.go index 2d2541147..2fbb3037f 100644 --- a/pkg/builder3/openapi.go +++ b/pkg/builder3/openapi.go @@ -420,43 +420,44 @@ func (o *openAPI) buildParameter(restParam common.Parameter) (ret *spec3.Paramet return ret, fmt.Errorf("unsupported restful parameter kind : %v", restParam.Kind()) } dataType := restParam.DataType() - var schemaType string - var schemaFormat string - var itemsSchema *spec.SchemaOrArray - - schemaType, schemaFormat = common.OpenAPITypeFormat(dataType) - if schemaType == "" && strings.HasPrefix(dataType, "[]") { - // Array type: element type is encoded as "[]string" or "[]integer" - itemsType := dataType[2:] - itemsAPIType, itemsAPIFormat := common.OpenAPITypeFormat(itemsType) - if itemsAPIType == "" { - return ret, fmt.Errorf("non-body Restful parameter array element type should be a simple type, but got: %v", itemsType) - } - schemaType = "array" - schemaFormat = "" - itemsSchema = &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{itemsAPIType}, - Format: itemsAPIFormat, - }, - }, - } - } else if schemaType == "" { - return ret, fmt.Errorf("non-body Restful parameter type should be a simple type, but got : %v", dataType) + + // AllowMultiple indicates an array parameter. The element type is + // derived by stripping the "[]" prefix from DataType. + elementType := dataType + if restParam.AllowMultiple() { + elementType = strings.TrimPrefix(dataType, "[]") } - ret.Schema = &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{schemaType}, - Format: schemaFormat, - UniqueItems: !restParam.AllowMultiple(), - Items: itemsSchema, - }, + elementAPIType, elementAPIFormat := common.OpenAPITypeFormat(elementType) + if elementAPIType == "" { + return ret, fmt.Errorf("non-body Restful parameter type should be a simple type, but got: %v", dataType) } - if schemaType == "array" && restParam.AllowMultiple() { + + if restParam.AllowMultiple() { ret.Style = "form" ret.Explode = true + ret.Schema = &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + UniqueItems: false, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{elementAPIType}, + Format: elementAPIFormat, + }, + }, + }, + }, + } + } else { + ret.Schema = &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{elementAPIType}, + Format: elementAPIFormat, + UniqueItems: true, + }, + } } return ret, nil } diff --git a/pkg/builder3/openapi_test.go b/pkg/builder3/openapi_test.go index cb349cdb5..8974fbd9c 100644 --- a/pkg/builder3/openapi_test.go +++ b/pkg/builder3/openapi_test.go @@ -27,6 +27,7 @@ import ( "github.com/stretchr/testify/assert" openapi "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/common/restfuladapter" "k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/util/jsontesting" "k8s.io/kube-openapi/pkg/validation/spec" @@ -581,3 +582,84 @@ func TestEscapeJsonPointerInSchemaName(t *testing.T) { }) } } + +func TestBuildParameter(t *testing.T) { + o := &openAPI{} + + // buildParameter receives *restfuladapter.ParamAdapter at runtime. + newParam := func(dt string, allowMultiple bool) openapi.Parameter { + return &restfuladapter.ParamAdapter{ + Param: restful.QueryParameter("p", "").DataType(dt).AllowMultiple(allowMultiple), + } + } + + tests := []struct { + name string + param openapi.Parameter + wantType string + wantFormat string + wantItems bool + wantStyle string + wantExplode bool + wantErr bool + }{ + { + name: "scalar string", + param: newParam("string", false), + wantType: "string", + }, + { + name: "scalar []byte stays scalar", + param: newParam("[]byte", false), + wantType: "string", + wantFormat: "byte", + }, + { + name: "AllowMultiple=true emits array", + param: newParam("[]string", true), + wantType: "array", + wantItems: true, + wantStyle: "form", + wantExplode: true, + }, + { + name: "AllowMultiple=true with plain DataType emits array", + param: newParam("string", true), + wantType: "array", + wantItems: true, + wantStyle: "form", + wantExplode: true, + }, + { + name: "[]string without AllowMultiple is a misconfiguration — errors intentionally", + param: newParam("[]string", false), + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := o.buildParameter(tt.param) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.NotNil(t, got.Schema, "schema should not be nil") + assert.Equal(t, spec.StringOrArray{tt.wantType}, got.Schema.Type) + assert.Equal(t, tt.wantFormat, got.Schema.Format) + assert.True(t, got.Schema.UniqueItems != tt.param.AllowMultiple()) + if tt.wantItems { + assert.NotNil(t, got.Schema.Items, "items should not be nil") + assert.NotNil(t, got.Schema.Items.Schema, "items schema should not be nil") + assert.Equal(t, spec.StringOrArray{"string"}, got.Schema.Items.Schema.Type) + assert.Equal(t, tt.wantStyle, got.Style) + assert.Equal(t, tt.wantExplode, got.Explode) + } else { + assert.Nil(t, got.Schema.Items) + assert.Empty(t, got.Style) + assert.False(t, got.Explode) + } + }) + } +}