Skip to content

Commit 2fac91e

Browse files
Add support for empty maps or lists
1 parent 881ffb4 commit 2fac91e

File tree

5 files changed

+51
-1
lines changed

5 files changed

+51
-1
lines changed

pkg/crd/markers/validation.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,9 @@ func (m Default) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
477477
if err != nil {
478478
return err
479479
}
480+
if schema.Type == "array" && string(marshalledDefault) == "{}" {
481+
marshalledDefault = []byte("[]")
482+
}
480483
schema.Default = &apiext.JSON{Raw: marshalledDefault}
481484
return nil
482485
}

pkg/crd/testdata/cronjob_types.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,18 @@ type CronJobSpec struct {
120120
// +kubebuilder:example={{nested: {foo: "baz", bar: true}},{nested: {bar: false}}}
121121
DefaultedObject []RootObject `json:"defaultedObject"`
122122

123+
// This tests that empty slice defaulting can be performed.
124+
// +kubebuilder:default={}
125+
DefaultedEmptySlice []string `json:"defaultedEmptySlice"`
126+
127+
// This tests that an empty object defaulting can be performed on a map.
128+
// +kubebuilder:default={}
129+
DefaultedEmptyMap map[string]string `json:"defaultedEmptyMap"`
130+
131+
// This tests that an empty object defaulting can be performed on an object.
132+
// +kubebuilder:default={}
133+
DefaultedEmptyObject EmpiableObject `json:"defaultedEmptyObject"`
134+
123135
// This tests that pattern validator is properly applied.
124136
// +kubebuilder:validation:Pattern=`^$|^((https):\/\/?)[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|\/?))$`
125137
PatternObject string `json:"patternObject"`
@@ -297,6 +309,13 @@ type MinMaxObject struct {
297309
Baz string `json:"baz,omitempty"`
298310
}
299311

312+
type EmpiableObject struct {
313+
314+
// +kubebuilder:default=forty-two
315+
Foo string `json:"foo,omitempty"`
316+
Bar string `json:"bar,omitempty"`
317+
}
318+
300319
type unexportedStruct struct {
301320
// This tests that exported fields are not skipped in the schema generation
302321
Foo string `json:"foo"`

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,28 @@ spec:
9696
- Forbid
9797
- Replace
9898
type: string
99+
defaultedEmptyMap:
100+
default: {}
101+
description: This tests that an empty object defaulting can be performed on a map.
102+
type: object
103+
additionalProperties:
104+
type: string
105+
defaultedEmptyObject:
106+
default: {}
107+
description: This tests that an empty object defaulting can be performed on an object.
108+
properties:
109+
bar:
110+
type: string
111+
foo:
112+
type: string
113+
default: forty-two
114+
type: object
115+
defaultedEmptySlice:
116+
default: []
117+
description: This tests that empty slice defaulting can be performed.
118+
items:
119+
type: string
120+
type: array
99121
defaultedObject:
100122
default:
101123
- nested:
@@ -7434,6 +7456,9 @@ spec:
74347456
- baz
74357457
- binaryName
74367458
- canBeNull
7459+
- defaultedEmptyMap
7460+
- defaultedEmptyObject
7461+
- defaultedEmptySlice
74377462
- defaultedObject
74387463
- defaultedSlice
74397464
- defaultedString

pkg/markers/parse.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,15 +310,17 @@ func guessType(scanner *sc.Scanner, raw string, allowSlice bool) *Argument {
310310
// We'll cross that bridge when we get there.
311311

312312
// look ahead till we can figure out if this is a map or a slice
313+
hint = peekNoSpace(subScanner)
313314
firstElemType := guessType(subScanner, subRaw, false)
314315
if firstElemType.Type == StringType {
315316
// might be a map or slice, parse the string and check for colon
316317
// (blech, basically arbitrary look-ahead due to raw strings).
317318
var keyVal string // just ignore this
318319
(&Argument{Type: StringType}).parseString(subScanner, raw, reflect.Indirect(reflect.ValueOf(&keyVal)))
319320

320-
if subScanner.Scan() == ':' {
321+
if token := subScanner.Scan(); token == ':' || hint == '}' {
321322
// it's got a string followed by a colon -- it's a map
323+
// or an empty map in case of {}
322324
return &Argument{
323325
Type: MapType,
324326
ItemType: &Argument{Type: AnyType},

pkg/markers/parse_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ var _ = Describe("Parsing", func() {
190190
It("should support delimitted slices of delimitted slices", argParseTestCase{arg: sliceOSlice, raw: "{{1,1},{2,3},{5,8}}", output: sliceOSliceOut}.Run)
191191

192192
It("should support maps", argParseTestCase{arg: Argument{Type: MapType, ItemType: &Argument{Type: StringType}}, raw: "{formal: hello, `informal`: `hi!`}", output: map[string]string{"formal": "hello", "informal": "hi!"}}.Run)
193+
It("should work with empty maps (which are equal to empty lists in the output)", argParseTestCase{arg: Argument{Type: MapType, ItemType: &Argument{Type: StringType}}, raw: "{}", output: map[string]string{}}.Run)
193194

194195
Context("with any value", func() {
195196
anyArg := Argument{Type: AnyType}

0 commit comments

Comments
 (0)