Skip to content

Commit 4d35a95

Browse files
pkg/crd: support validating internal list items on list types
For #342 Signed-off-by: Alexander Yastrebov <alexander.yastrebov@zalando.de>
1 parent 3f5bd8e commit 4d35a95

File tree

5 files changed

+141
-16
lines changed

5 files changed

+141
-16
lines changed

pkg/crd/markers/register.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ func (d *definitionWithHelp) Register(reg *markers.Registry) error {
4242
return nil
4343
}
4444

45+
func (d *definitionWithHelp) clone() *definitionWithHelp {
46+
newDef := *d.Definition
47+
// copy both parts so we don't change the definition
48+
return &definitionWithHelp{
49+
Definition: &newDef,
50+
Help: d.Help,
51+
}
52+
}
53+
4554
func must(def *markers.Definition, err error) *definitionWithHelp {
4655
return &definitionWithHelp{
4756
Definition: markers.Must(def, err),
@@ -60,7 +69,7 @@ type hasHelp interface {
6069
func mustMakeAllWithPrefix(prefix string, target markers.TargetType, objs ...interface{}) []*definitionWithHelp {
6170
defs := make([]*definitionWithHelp, len(objs))
6271
for i, obj := range objs {
63-
name := prefix + ":" + reflect.TypeOf(obj).Name()
72+
name := prefix + reflect.TypeOf(obj).Name()
6473
def, err := markers.MakeDefinition(name, target, obj)
6574
if err != nil {
6675
panic(err)

pkg/crd/markers/validation.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,28 @@ import (
2020
"encoding/json"
2121
"fmt"
2222
"math"
23+
"strings"
2324

2425
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2526

2627
"sigs.k8s.io/controller-tools/pkg/markers"
2728
)
2829

2930
const (
30-
SchemalessName = "kubebuilder:validation:Schemaless"
31+
validationPrefix = "kubebuilder:validation:"
32+
33+
SchemalessName = "kubebuilder:validation:Schemaless"
34+
ValidationItemsPrefix = validationPrefix + "items:"
3135
)
3236

3337
// ValidationMarkers lists all available markers that affect CRD schema generation,
3438
// except for the few that don't make sense as type-level markers (see FieldOnlyMarkers).
3539
// All markers start with `+kubebuilder:validation:`, and continue with their type name.
3640
// A copy is produced of all markers that describes types as well, for making types
3741
// reusable and writing complex validations on slice items.
38-
var ValidationMarkers = mustMakeAllWithPrefix("kubebuilder:validation", markers.DescribesField,
42+
// At last a copy of all markers with the prefix `+kubebuilder:validation:items:` is
43+
// produced for marking slice fields and types.
44+
var ValidationMarkers = mustMakeAllWithPrefix(validationPrefix, markers.DescribesField,
3945

4046
// numeric markers
4147

@@ -110,14 +116,20 @@ func init() {
110116
AllDefinitions = append(AllDefinitions, ValidationMarkers...)
111117

112118
for _, def := range ValidationMarkers {
113-
newDef := *def.Definition
114-
// copy both parts so we don't change the definition
115-
typDef := definitionWithHelp{
116-
Definition: &newDef,
117-
Help: def.Help,
118-
}
119+
typDef := def.clone()
119120
typDef.Target = markers.DescribesType
120-
AllDefinitions = append(AllDefinitions, &typDef)
121+
AllDefinitions = append(AllDefinitions, typDef)
122+
123+
itemsName := ValidationItemsPrefix + strings.TrimPrefix(def.Name, validationPrefix)
124+
125+
itemsFieldDef := def.clone()
126+
itemsFieldDef.Name = itemsName
127+
AllDefinitions = append(AllDefinitions, itemsFieldDef)
128+
129+
itemsTypDef := def.clone()
130+
itemsTypDef.Name = itemsName
131+
itemsTypDef.Target = markers.DescribesType
132+
AllDefinitions = append(AllDefinitions, itemsTypDef)
121133
}
122134

123135
AllDefinitions = append(AllDefinitions, FieldOnlyMarkers...)

pkg/crd/schema.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,16 +128,23 @@ func infoToSchema(ctx *schemaContext) *apiext.JSONSchemaProps {
128128
// applyMarkers applies schema markers given their priority to the given schema
129129
func applyMarkers(ctx *schemaContext, markerSet markers.MarkerValues, props *apiext.JSONSchemaProps, node ast.Node) {
130130
markers := make([]SchemaMarker, 0, len(markerSet))
131+
itemsMarkers := make([]SchemaMarker, 0, len(markerSet))
132+
itemsMarkerNames := make(map[SchemaMarker]string)
131133

132-
for _, markerValues := range markerSet {
134+
for markerName, markerValues := range markerSet {
133135
for _, markerValue := range markerValues {
134136
if schemaMarker, isSchemaMarker := markerValue.(SchemaMarker); isSchemaMarker {
135-
markers = append(markers, schemaMarker)
137+
if strings.HasPrefix(markerName, crdmarkers.ValidationItemsPrefix) {
138+
itemsMarkers = append(itemsMarkers, schemaMarker)
139+
itemsMarkerNames[schemaMarker] = markerName
140+
} else {
141+
markers = append(markers, schemaMarker)
142+
}
136143
}
137144
}
138145
}
139146

140-
sort.Slice(markers, func(i, j int) bool {
147+
cmpPriority := func(markers []SchemaMarker, i, j int) bool {
141148
var iPriority, jPriority crdmarkers.ApplyPriority
142149

143150
switch m := markers[i].(type) {
@@ -159,13 +166,27 @@ func applyMarkers(ctx *schemaContext, markerSet markers.MarkerValues, props *api
159166
}
160167

161168
return iPriority < jPriority
162-
})
169+
}
170+
sort.Slice(markers, func(i, j int) bool { return cmpPriority(markers, i, j) })
171+
sort.Slice(itemsMarkers, func(i, j int) bool { return cmpPriority(itemsMarkers, i, j) })
163172

164173
for _, schemaMarker := range markers {
165174
if err := schemaMarker.ApplyToSchema(props); err != nil {
166175
ctx.pkg.AddError(loader.ErrFromNode(err /* an okay guess */, node))
167176
}
168177
}
178+
179+
for _, schemaMarker := range itemsMarkers {
180+
if props.Type != "array" || props.Items == nil || props.Items.Schema == nil {
181+
err := fmt.Errorf("must apply %s to an array value, found %s", itemsMarkerNames[schemaMarker], props.Type)
182+
ctx.pkg.AddError(loader.ErrFromNode(err, node))
183+
} else {
184+
itemsSchema := props.Items.Schema
185+
if err := schemaMarker.ApplyToSchema(itemsSchema); err != nil {
186+
ctx.pkg.AddError(loader.ErrFromNode(err /* an okay guess */, node))
187+
}
188+
}
189+
}
169190
}
170191

171192
// typeToSchema creates a schema for the given AST type.

pkg/crd/testdata/cronjob_types.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ type CronJobSpec struct {
197197

198198
// This tests that an IntOrString can also have a pattern attached
199199
// to it.
200-
// This can be useful if you want to limit the string to a perecentage or integer.
200+
// This can be useful if you want to limit the string to a percentage or integer.
201201
// The XIntOrString marker is a requirement for having a pattern on this type.
202202
// +kubebuilder:validation:XIntOrString
203203
// +kubebuilder:validation:Pattern="^((100|[0-9]{1,2})%|[0-9]+)$"
@@ -252,6 +252,32 @@ type CronJobSpec struct {
252252

253253
// Checks that arrays work when the type contains a composite literal
254254
ArrayUsingCompositeLiteral [len(struct{ X [3]int }{}.X)]string `json:"arrayUsingCompositeLiteral,omitempty"`
255+
256+
// This tests string slice item validation.
257+
// +kubebuilder:validation:MinItems=1
258+
// +kubebuilder:validation:items:MinLength=1
259+
// +kubebuilder:validation:items:MaxLength=255
260+
// +kubebuilder:validation:items:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
261+
// +listType=set
262+
Hosts []string `json:"hosts,omitempty"`
263+
264+
HostsAlias Hosts `json:"hostsAlias,omitempty"`
265+
266+
// This tests string slice validation.
267+
// +kubebuilder:validation:MinItems=2
268+
// +kubebuilder:validation:MaxItems=2
269+
StringPair []string `json:"stringPair"`
270+
271+
// This tests string alias slice item validation.
272+
// +kubebuilder:validation:MinItems=3
273+
LongerStringArray []LongerString `json:"longerStringArray,omitempty"`
274+
275+
// This tests that a slice of IntOrString can also have a pattern attached to it.
276+
// This can be useful if you want to limit the string to a percentage or integer.
277+
// The XIntOrString marker is a requirement for having a pattern on this type.
278+
// +kubebuilder:validation:items:XIntOrString
279+
// +kubebuilder:validation:items:Pattern="^((100|[0-9]{1,2})%|[0-9]+)$"
280+
IntOrStringArrayWithAPattern []*intstr.IntOrString `json:"intOrStringArrayWithAPattern,omitempty"`
255281
}
256282

257283
type ContainsNestedMap struct {
@@ -360,6 +386,14 @@ type LongerString string
360386
// TotallyABool is a bool that serializes as a string.
361387
type TotallyABool bool
362388

389+
// This tests string slice item validation.
390+
// +kubebuilder:validation:MinItems=1
391+
// +kubebuilder:validation:items:MinLength=1
392+
// +kubebuilder:validation:items:MaxLength=255
393+
// +kubebuilder:validation:items:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
394+
// +listType=set
395+
type Hosts []string
396+
363397
func (t TotallyABool) MarshalJSON() ([]byte, error) {
364398
if t {
365399
return []byte(`"true"`), nil

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

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,20 +196,52 @@ spec:
196196
description: This tests that exported fields are not skipped in the
197197
schema generation
198198
type: string
199+
hosts:
200+
description: This tests string slice item validation.
201+
items:
202+
maxLength: 255
203+
minLength: 1
204+
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
205+
type: string
206+
minItems: 1
207+
type: array
208+
x-kubernetes-list-type: set
209+
hostsAlias:
210+
description: This tests string slice item validation.
211+
items:
212+
maxLength: 255
213+
minLength: 1
214+
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
215+
type: string
216+
minItems: 1
217+
type: array
218+
x-kubernetes-list-type: set
199219
int32WithValidations:
200220
format: int32
201221
maximum: 2
202222
minimum: -2
203223
multipleOf: 2
204224
type: integer
225+
intOrStringArrayWithAPattern:
226+
description: |-
227+
This tests that a slice of IntOrString can also have a pattern attached to it.
228+
This can be useful if you want to limit the string to a percentage or integer.
229+
The XIntOrString marker is a requirement for having a pattern on this type.
230+
items:
231+
anyOf:
232+
- type: integer
233+
- type: string
234+
pattern: ^((100|[0-9]{1,2})%|[0-9]+)$
235+
x-kubernetes-int-or-string: true
236+
type: array
205237
intOrStringWithAPattern:
206238
anyOf:
207239
- type: integer
208240
- type: string
209241
description: |-
210242
This tests that an IntOrString can also have a pattern attached
211243
to it.
212-
This can be useful if you want to limit the string to a perecentage or integer.
244+
This can be useful if you want to limit the string to a percentage or integer.
213245
The XIntOrString marker is a requirement for having a pattern on this type.
214246
pattern: ^((100|[0-9]{1,2})%|[0-9]+)$
215247
x-kubernetes-int-or-string: true
@@ -6609,6 +6641,15 @@ spec:
66096641
- bar
66106642
- foo
66116643
type: object
6644+
longerStringArray:
6645+
description: This tests string alias slice item validation.
6646+
items:
6647+
description: This tests that markers that are allowed on both fields
6648+
and types are applied to types
6649+
minLength: 4
6650+
type: string
6651+
minItems: 3
6652+
type: array
66126653
mapOfArraysOfFloats:
66136654
additionalProperties:
66146655
items:
@@ -6742,6 +6783,13 @@ spec:
67426783
time for any reason. Missed jobs executions will be counted as failed ones.
67436784
format: int64
67446785
type: integer
6786+
stringPair:
6787+
description: This tests string slice validation.
6788+
items:
6789+
type: string
6790+
maxItems: 2
6791+
minItems: 2
6792+
type: array
67456793
stringSliceData:
67466794
additionalProperties:
67476795
items:
@@ -6839,6 +6887,7 @@ spec:
68396887
- nestedassociativeList
68406888
- patternObject
68416889
- schedule
6890+
- stringPair
68426891
- structWithSeveralFields
68436892
- twoOfAKindPart0
68446893
- twoOfAKindPart1

0 commit comments

Comments
 (0)