Skip to content

Commit f1c7919

Browse files
authored
Merge pull request #1270 from JoelSpeed/better-handling-local-types
⚠️ Start from local type declaration when applying schema
2 parents 36dffbf + 5d60153 commit f1c7919

File tree

5 files changed

+96
-27
lines changed

5 files changed

+96
-27
lines changed

pkg/crd/parser.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,12 @@ func (p *Parser) NeedSchemaFor(typ TypeIdent) {
172172
// avoid tripping recursive schemata, like ManagedFields, by adding an empty WIP schema
173173
p.Schemata[typ] = apiext.JSONSchemaProps{}
174174

175-
schemaCtx := newSchemaContext(typ.Package, p, p.AllowDangerousTypes, p.IgnoreUnexportedFields)
175+
schemaCtx := newSchemaContext(typ.Package, p, func(typ TypeIdent) *apiext.JSONSchemaProps {
176+
p.NeedSchemaFor(typ)
177+
178+
props := p.Schemata[typ]
179+
return &props
180+
}, p.AllowDangerousTypes, p.IgnoreUnexportedFields)
176181
ctxForInfo := schemaCtx.ForInfo(info)
177182

178183
pkgMarkers, err := markers.PackageMarkers(p.Collector, typ.Package)

pkg/crd/schema.go

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ type applyFirstMarker interface {
5757
ApplyFirst()
5858
}
5959

60+
// schemaFetcher is a function that fetches a schema for a given type.
61+
type schemaFetcher func(TypeIdent) *apiext.JSONSchemaProps
62+
6063
// schemaRequester knows how to marker that another schema (e.g. via an external reference) is necessary.
6164
type schemaRequester interface {
6265
NeedSchemaFor(typ TypeIdent)
@@ -68,6 +71,7 @@ type schemaContext struct {
6871
info *markers.TypeInfo
6972

7073
schemaRequester schemaRequester
74+
schemaFetcher schemaFetcher
7175
PackageMarkers markers.MarkerValues
7276

7377
allowDangerousTypes bool
@@ -76,11 +80,12 @@ type schemaContext struct {
7680

7781
// newSchemaContext constructs a new schemaContext for the given package and schema requester.
7882
// It must have type info added before use via ForInfo.
79-
func newSchemaContext(pkg *loader.Package, req schemaRequester, allowDangerousTypes, ignoreUnexportedFields bool) *schemaContext {
83+
func newSchemaContext(pkg *loader.Package, req schemaRequester, fetcher schemaFetcher, allowDangerousTypes, ignoreUnexportedFields bool) *schemaContext {
8084
pkg.NeedTypesInfo()
8185
return &schemaContext{
8286
pkg: pkg,
8387
schemaRequester: req,
88+
schemaFetcher: fetcher,
8489
allowDangerousTypes: allowDangerousTypes,
8590
ignoreUnexportedFields: ignoreUnexportedFields,
8691
}
@@ -93,6 +98,7 @@ func (c *schemaContext) ForInfo(info *markers.TypeInfo) *schemaContext {
9398
pkg: c.pkg,
9499
info: info,
95100
schemaRequester: c.schemaRequester,
101+
schemaFetcher: c.schemaFetcher,
96102
allowDangerousTypes: c.allowDangerousTypes,
97103
ignoreUnexportedFields: c.ignoreUnexportedFields,
98104
}
@@ -234,7 +240,9 @@ func typeToSchema(ctx *schemaContext, rawType ast.Expr) *apiext.JSONSchemaProps
234240
return &apiext.JSONSchemaProps{}
235241
}
236242

237-
props.Description = ctx.info.Doc
243+
if ctx.info.Doc != "" {
244+
props.Description = ctx.info.Doc
245+
}
238246

239247
applyMarkers(ctx, ctx.info.Markers, props, rawType)
240248

@@ -270,6 +278,7 @@ func localNamedToSchema(ctx *schemaContext, ident *ast.Ident) *apiext.JSONSchema
270278
if aliasInfo, isAlias := typeInfo.(*types.Alias); isAlias {
271279
typeInfo = aliasInfo.Rhs()
272280
}
281+
273282
if basicInfo, isBasic := typeInfo.(*types.Basic); isBasic {
274283
typ, fmt, err := builtinToType(basicInfo, ctx.allowDangerousTypes)
275284
if err != nil {
@@ -281,32 +290,21 @@ func localNamedToSchema(ctx *schemaContext, ident *ast.Ident) *apiext.JSONSchema
281290
// > Otherwise, the alias information is only in the type name, which
282291
// > points directly to the actual (aliased) type.
283292
if basicInfo.Name() != ident.Name {
284-
ctx.requestSchema("", ident.Name)
285-
link := TypeRefLink("", ident.Name)
286-
return &apiext.JSONSchemaProps{
287-
Type: typ,
288-
Format: fmt,
289-
Ref: &link,
290-
}
293+
return ctx.schemaFetcher(TypeIdent{
294+
Package: ctx.pkg,
295+
Name: ident.Name,
296+
})
291297
}
292298
return &apiext.JSONSchemaProps{
293299
Type: typ,
294300
Format: fmt,
295301
}
296302
}
297-
// NB(directxman12): if there are dot imports, this might be an external reference,
298-
// so use typechecking info to get the actual object
299-
typeNameInfo := typeInfo.(interface{ Obj() *types.TypeName }).Obj()
300-
pkg := typeNameInfo.Pkg()
301-
pkgPath := loader.NonVendorPath(pkg.Path())
302-
if pkg == ctx.pkg.Types {
303-
pkgPath = ""
304-
}
305-
ctx.requestSchema(pkgPath, typeNameInfo.Name())
306-
link := TypeRefLink(pkgPath, typeNameInfo.Name())
307-
return &apiext.JSONSchemaProps{
308-
Ref: &link,
309-
}
303+
304+
return ctx.schemaFetcher(TypeIdent{
305+
Package: ctx.pkg,
306+
Name: ident.Name,
307+
})
310308
}
311309

312310
// namedSchema creates a schema (ref) for an explicitly external type reference.
@@ -501,7 +499,9 @@ func structToSchema(ctx *schemaContext, structType *ast.StructType) *apiext.JSON
501499
} else {
502500
propSchema = typeToSchema(ctx.ForInfo(&markers.TypeInfo{}), field.RawField.Type)
503501
}
504-
propSchema.Description = field.Doc
502+
if field.Doc != "" {
503+
propSchema.Description = field.Doc
504+
}
505505

506506
applyMarkers(ctx, field.Markers, propSchema, field.RawField)
507507

@@ -513,6 +513,9 @@ func structToSchema(ctx *schemaContext, structType *ast.StructType) *apiext.JSON
513513
props.Properties[fieldName] = *propSchema
514514
}
515515

516+
// Ensure the required fields are always listed alphabetically.
517+
sort.Strings(props.Required)
518+
516519
return props
517520
}
518521

pkg/crd/schema_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func transform(t *testing.T, expr string) *apiext.JSONSchemaProps {
6464
pkg.NeedTypesInfo()
6565
failIfErrors(t, pkg.Errors)
6666

67-
schemaContext := newSchemaContext(pkg, nil, true, false).ForInfo(&markers.TypeInfo{})
67+
schemaContext := newSchemaContext(pkg, nil, nil, true, false).ForInfo(&markers.TypeInfo{})
6868
// yick: grab the only type definition
6969
definedType := pkg.Syntax[0].Decls[0].(*ast.GenDecl).Specs[0].(*ast.TypeSpec).Type
7070
result := typeToSchema(schemaContext, definedType)

pkg/crd/testdata/cronjob_types.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,30 @@ type CronJobSpec struct {
389389
// Test that we can add a field that can only be set to a non-default value on updates using XValidation OptionalOldSelf.
390390
// +kubebuilder:validation:XValidation:rule="oldSelf.hasValue() || self == 0",message="must be set to 0 on creation. can be set to any value on an update.",optionalOldSelf=true
391391
OnlyAllowSettingOnUpdate int32 `json:"onlyAllowSettingOnUpdate,omitempty"`
392+
393+
// This tests that unmarkered types are handled correctly.
394+
// When markers are applied at the field level instead of the type level.
395+
// +kubebuilder:validation:Minimum=1
396+
// +kubebuilder:validation:Maximum=10
397+
LocallyBoundedInteger UnmarkeredInteger `json:"locallyBoundedInteger,omitempty"`
398+
399+
// This tests that unmarkered types are handled correctly.
400+
// When markers are applied at the field level instead of the type level.
401+
// +kubebuilder:validation:MinLength=1
402+
// +kubebuilder:validation:MaxLength=10
403+
LocallyBoundedString UnmarkeredString `json:"locallyBoundedString,omitempty"`
404+
405+
// This tests that field-level overrides are handled correctly
406+
// for local type aliases.
407+
// +kubebuilder:validation:MinLength=10
408+
// +kubebuilder:validation:MaxLength=10
409+
FieldLevelAliasOverride StringAliasWithValidation `json:"fieldLevelAliasOverride,omitempty"`
410+
411+
// This tests that field-level overrides are handled correctly
412+
// for local type declarations.
413+
// +kubebuilder:validation:MinLength=10
414+
// +kubebuilder:validation:MaxLength=10
415+
FieldLevelLocalDeclarationOverride LongerString `json:"fieldLevelLocalDeclarationOverride,omitempty"`
392416
}
393417

394418
type InlineAlias = EmbeddedStruct
@@ -518,6 +542,14 @@ type TotallyABool bool
518542
// +listType=set
519543
type Hosts []string
520544

545+
// This tests that unmarkered types are handled correctly.
546+
// When markers are applied at the field level instead of the type level.
547+
type UnmarkeredInteger int32
548+
549+
// This tests that unmarkered types are handled correctly.
550+
// When markers are applied at the field level instead of the type level.
551+
type UnmarkeredString string
552+
521553
func (t TotallyABool) MarshalJSON() ([]byte, error) {
522554
if t {
523555
return []byte(`"true"`), nil

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

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,20 @@ spec:
219219
This is a pointer to distinguish between explicit zero and not specified.
220220
format: int32
221221
type: integer
222+
fieldLevelAliasOverride:
223+
description: |-
224+
This tests that field-level overrides are handled correctly
225+
for local type aliases.
226+
maxLength: 10
227+
minLength: 10
228+
type: string
229+
fieldLevelLocalDeclarationOverride:
230+
description: |-
231+
This tests that field-level overrides are handled correctly
232+
for local type declarations.
233+
maxLength: 10
234+
minLength: 10
235+
type: string
222236
float64WithValidations:
223237
maximum: 1.5
224238
minimum: -0.5
@@ -8849,6 +8863,21 @@ spec:
88498863
default: forty-two
88508864
description: This tests that primitive defaulting can be performed.
88518865
type: string
8866+
locallyBoundedInteger:
8867+
description: |-
8868+
This tests that unmarkered types are handled correctly.
8869+
When markers are applied at the field level instead of the type level.
8870+
format: int32
8871+
maximum: 10
8872+
minimum: 1
8873+
type: integer
8874+
locallyBoundedString:
8875+
description: |-
8876+
This tests that unmarkered types are handled correctly.
8877+
When markers are applied at the field level instead of the type level.
8878+
maxLength: 10
8879+
minLength: 1
8880+
type: string
88528881
longerStringArray:
88538882
description: This tests string alias slice item validation.
88548883
items:
@@ -9055,10 +9084,10 @@ spec:
90559084
and type.
90569085
type: string
90579086
x-kubernetes-validations:
9058-
- message: must have good prefix
9059-
rule: self.startsWith('good-')
90609087
- message: must have even length
90619088
rule: self.size() % 2 == 0
9089+
- message: must have good prefix
9090+
rule: self.startsWith('good-')
90629091
stringWithEvenLengthAndMessageExpression:
90639092
description: Test of the expression-based validation with messageExpression
90649093
marker.

0 commit comments

Comments
 (0)