Skip to content

Commit 4d23c3a

Browse files
committed
Fix +listType marker not working on type alias fields
Include underlying type info (array/object) in schema for slice/map type aliases, allowing field-level markers like +listType to work. Fixes #988 Signed-off-by: sivchari <[email protected]>
1 parent d6c2788 commit 4d23c3a

3 files changed

Lines changed: 64 additions & 15 deletions

File tree

pkg/crd/schema.go

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ import (
3838
const (
3939
// defPrefix is the prefix used to link to definitions in the OpenAPI schema.
4040
defPrefix = "#/definitions/"
41+
42+
// arrayType is the JSON schema type for arrays.
43+
arrayType = "array"
4144
)
4245

4346
// byteType is the types.Type for byte (see the types documention
@@ -201,7 +204,7 @@ func applyMarkers(ctx *schemaContext, markerSet markers.MarkerValues, props *api
201204
}
202205

203206
for _, schemaMarker := range itemsMarkers {
204-
if props.Type != "array" || props.Items == nil || props.Items.Schema == nil {
207+
if props.Type != arrayType || props.Items == nil || props.Items.Schema == nil {
205208
err := fmt.Errorf("must apply %s to an array value, found %s", schemaMarker.Name, props.Type)
206209
ctx.pkg.AddError(loader.ErrFromNode(err, node))
207210
} else {
@@ -312,26 +315,31 @@ func localNamedToSchema(ctx *schemaContext, ident *ast.Ident) *apiextensionsv1.J
312315
ctx.requestSchema(pkgPath, typeNameInfo.Name())
313316
link := TypeRefLink(pkgPath, typeNameInfo.Name())
314317

318+
// For type aliases to slice/map types, include the underlying type information
319+
// so that markers like +listType can be applied at the field level.
320+
props := &apiextensionsv1.JSONSchemaProps{
321+
Ref: &link,
322+
}
323+
315324
// In cases where we have a named type, apply the type and format from the named schema
316325
// to allow markers that need this information to apply correctly.
317-
var typ, fmt string
318326
if namedInfo, isNamed := typeInfo.(*types.Named); isNamed {
319-
// We don't want/need to do this for structs, maps, or arrays.
320-
// These are already handled in infoToSchema if they have custom marshalling.
321-
if _, isBasic := namedInfo.Underlying().(*types.Basic); isBasic {
327+
switch namedInfo.Underlying().(type) {
328+
case *types.Slice:
329+
props.Type = arrayType
330+
case *types.Map:
331+
props.Type = "object"
332+
case *types.Basic:
333+
// We don't want/need to do this for structs, maps, or arrays.
334+
// These are already handled in infoToSchema if they have custom marshalling.
322335
namedTypeInfo := ctx.schemaRequester.LookupType(ctx.pkg, namedInfo.Obj().Name())
323-
324336
namedSchema := infoToSchema(ctx.ForInfo(namedTypeInfo))
325-
typ = namedSchema.Type
326-
fmt = namedSchema.Format
337+
props.Type = namedSchema.Type
338+
props.Format = namedSchema.Format
327339
}
328340
}
329341

330-
return &apiextensionsv1.JSONSchemaProps{
331-
Type: typ,
332-
Format: fmt,
333-
Ref: &link,
334-
}
342+
return props
335343
default:
336344
ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("unsupported type %T for identifier %s", typeInfo, ident.Name), ident))
337345
return &apiextensionsv1.JSONSchemaProps{}
@@ -354,10 +362,19 @@ func namedToSchema(ctx *schemaContext, named *ast.SelectorExpr) *apiextensionsv1
354362
nonVendorPath := loader.NonVendorPath(typeNameInfo.Pkg().Path())
355363
ctx.requestSchema(nonVendorPath, typeNameInfo.Name())
356364
link := TypeRefLink(nonVendorPath, typeNameInfo.Name())
357-
return &apiextensionsv1.JSONSchemaProps{
365+
// For type aliases to slice/map types, include the underlying type information
366+
// so that markers like +listType can be applied at the field level.
367+
props := &apiextensionsv1.JSONSchemaProps{
358368
Ref: &link,
359369
}
370+
switch typeInfoRaw.Underlying().(type) {
371+
case *types.Slice:
372+
props.Type = arrayType
373+
case *types.Map:
374+
props.Type = "object"
375+
}
360376
// NB(directxman12): we special-case things like resource.Quantity during the "collapse" phase.
377+
return props
361378
}
362379

363380
// arrayToSchema creates a schema for the items of the given array, dealing appropriately
@@ -376,7 +393,7 @@ func arrayToSchema(ctx *schemaContext, array *ast.ArrayType) *apiextensionsv1.JS
376393
items := typeToSchema(ctx.ForInfo(&markers.TypeInfo{}), array.Elt)
377394

378395
return &apiextensionsv1.JSONSchemaProps{
379-
Type: "array",
396+
Type: arrayType,
380397
Items: &apiextensionsv1.JSONSchemaPropsOrArray{Schema: items},
381398
}
382399
}

pkg/crd/testdata/cronjob_types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,12 @@ type CronJobSpec struct {
207207
// This tests that associative lists work via a nested type.
208208
NestedAssociativeList NestedAssociativeList `json:"nestedassociativeList"`
209209

210+
// This tests that +listType can be applied at the field level for type aliases (issue #988).
211+
// +listType=map
212+
// +listMapKey=name
213+
// +listMapKey=secondary
214+
FieldLevelListType ConditionsWithoutMarker `json:"fieldLevelListType"`
215+
210216
// A map that allows different actors to manage different fields
211217
// +mapType=granular
212218
MapOfInfo map[string][]byte `json:"mapOfInfo"`
@@ -544,6 +550,10 @@ type NestedAssociativeList []AssociativeType
544550
// +mapType=granular
545551
type NestedMapOfInfo map[string][]byte
546552

553+
// ConditionsWithoutMarker is a type alias to a slice without type-level markers.
554+
// This tests that +listType can be applied at the field level (issue #988).
555+
type ConditionsWithoutMarker []AssociativeType
556+
547557
// +kubebuilder:validation:MinLength=4
548558
// This tests that markers that are allowed on both fields and types are applied to types
549559
type LongerString string

pkg/crd/testdata/testdata.kubebuilder.io_cronjobs.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,27 @@ spec:
235235
This tests that field-level overrides are handled correctly
236236
for local type aliases.
237237
type: string
238+
fieldLevelListType:
239+
description: 'This tests that +listType can be applied at the field
240+
level for type aliases (issue #988).'
241+
items:
242+
properties:
243+
foo:
244+
type: string
245+
name:
246+
type: string
247+
secondary:
248+
type: integer
249+
required:
250+
- foo
251+
- name
252+
- secondary
253+
type: object
254+
type: array
255+
x-kubernetes-list-map-keys:
256+
- name
257+
- secondary
258+
x-kubernetes-list-type: map
238259
fieldLevelLocalDeclarationOverride:
239260
allOf:
240261
- minLength: 4
@@ -9721,6 +9742,7 @@ spec:
97219742
- explicitlyRequiredK8s
97229743
- explicitlyRequiredKubebuilder
97239744
- explicitlyRequiredKubernetes
9745+
- fieldLevelListType
97249746
- float64WithValidations
97259747
- floatWithValidations
97269748
- foo

0 commit comments

Comments
 (0)