Skip to content

File tree

5 files changed

+213
-0
lines changed

5 files changed

+213
-0
lines changed

pkg/crd/markers/validation.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ var FieldOnlyMarkers = []*definitionWithHelp{
9191

9292
must(markers.MakeAnyTypeDefinition("kubebuilder:default", markers.DescribesField, Default{})).
9393
WithHelp(Default{}.Help()),
94+
must(markers.MakeAnyTypeJSONDefinition("default", markers.DescribesField, KubernetesDefault{})).
95+
WithHelp(KubernetesDefault{}.Help()),
9496

9597
must(markers.MakeAnyTypeDefinition("kubebuilder:example", markers.DescribesField, Example{})).
9698
WithHelp(Example{}.Help()),
@@ -239,6 +241,19 @@ type Default struct {
239241
Value interface{}
240242
}
241243

244+
// +controllertools:marker:generateHelp:category="CRD validation"
245+
// Default sets the default value for this field.
246+
//
247+
// A default value will be accepted as any value valid for the
248+
// field. Formatting for common types include: boolean: `true`, string:
249+
// `"Cluster"`, numerical: `1.24`, array: `[1,2]`, object: `{"policy":
250+
// "delete"}`). Defaults should be defined in pruned form, and only best-effort
251+
// validation will be performed. Full validation of a default requires
252+
// submission of the containing CRD to an apiserver.
253+
type KubernetesDefault struct {
254+
Value interface{}
255+
}
256+
242257
// +controllertools:marker:generateHelp:category="CRD validation"
243258
// Example sets the example value for this field.
244259
//
@@ -503,6 +518,26 @@ func (m Default) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
503518
return nil
504519
}
505520

521+
func (m Default) ApplyPriority() ApplyPriority {
522+
// explicitly go after +default markers, so kubebuilder-specific defaults get applied last and stomp
523+
return 10
524+
}
525+
526+
// Defaults are only valid CRDs created with the v1 API
527+
func (m KubernetesDefault) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
528+
marshalledDefault, err := json.Marshal(m.Value)
529+
if err != nil {
530+
return err
531+
}
532+
schema.Default = &apiext.JSON{Raw: marshalledDefault}
533+
return nil
534+
}
535+
536+
func (m KubernetesDefault) ApplyPriority() ApplyPriority {
537+
// explicitly go before +kubebuilder:default markers, so kubebuilder-specific defaults get applied last and stomp
538+
return 9
539+
}
540+
506541
func (m Example) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
507542
marshalledExample, err := json.Marshal(m.Value)
508543
if err != nil {

pkg/crd/markers/zz_generated.markerhelp.go

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/crd/testdata/cronjob_types.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,35 @@ type CronJobSpec struct {
133133
// +kubebuilder:default={}
134134
DefaultedEmptyObject EmpiableObject `json:"defaultedEmptyObject"`
135135

136+
// This tests that kubebuilder defaulting takes precedence.
137+
// +kubebuilder:default="kubebuilder-default"
138+
// +default="kubernetes-default"
139+
DoubleDefaultedString string `json:"doubleDefaultedString"`
140+
141+
// This tests that primitive defaulting can be performed.
142+
// +default="forty-two"
143+
KubernetesDefaultedString string `json:"kubernetesDefaultedString"`
144+
145+
// This tests that slice defaulting can be performed.
146+
// +default=["a","b"]
147+
KubernetesDefaultedSlice []string `json:"kubernetesDefaultedSlice"`
148+
149+
// This tests that object defaulting can be performed.
150+
// +default=[{"nested": {"foo": "baz", "bar": true}},{"nested": {"bar": false}}]
151+
KubernetesDefaultedObject []RootObject `json:"kubernetesDefaultedObject"`
152+
153+
// This tests that empty slice defaulting can be performed.
154+
// +default=[]
155+
KubernetesDefaultedEmptySlice []string `json:"kubernetesDefaultedEmptySlice"`
156+
157+
// This tests that an empty object defaulting can be performed on a map.
158+
// +default={}
159+
KubernetesDefaultedEmptyMap map[string]string `json:"kubernetesDefaultedEmptyMap"`
160+
161+
// This tests that an empty object defaulting can be performed on an object.
162+
// +default={}
163+
KubernetesDefaultedEmptyObject EmpiableObject `json:"kubernetesDefaultedEmptyObject"`
164+
136165
// This tests that pattern validator is properly applied.
137166
// +kubebuilder:validation:Pattern=`^$|^((https):\/\/?)[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|\/?))$`
138167
PatternObject string `json:"patternObject"`

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,70 @@ spec:
172172
description: This tests that primitive defaulting can be performed.
173173
example: forty-two
174174
type: string
175+
doubleDefaultedString:
176+
default: kubebuilder-default
177+
description: This tests that kubebuilder defaulting takes precedence.
178+
type: string
179+
kubernetesDefaultedEmptyMap:
180+
additionalProperties:
181+
type: string
182+
default: {}
183+
description: This tests that an empty object defaulting can be performed
184+
on a map.
185+
type: object
186+
kubernetesDefaultedEmptyObject:
187+
default: {}
188+
description: This tests that an empty object defaulting can be performed
189+
on an object.
190+
properties:
191+
bar:
192+
type: string
193+
foo:
194+
default: forty-two
195+
type: string
196+
type: object
197+
kubernetesDefaultedEmptySlice:
198+
default: []
199+
description: This tests that empty slice defaulting can be performed.
200+
items:
201+
type: string
202+
type: array
203+
kubernetesDefaultedObject:
204+
default:
205+
- nested:
206+
bar: true
207+
foo: baz
208+
- nested:
209+
bar: false
210+
description: This tests that object defaulting can be performed.
211+
items:
212+
properties:
213+
nested:
214+
properties:
215+
bar:
216+
type: boolean
217+
foo:
218+
type: string
219+
required:
220+
- bar
221+
- foo
222+
type: object
223+
required:
224+
- nested
225+
type: object
226+
type: array
227+
kubernetesDefaultedSlice:
228+
default:
229+
- a
230+
- b
231+
description: This tests that slice defaulting can be performed.
232+
items:
233+
type: string
234+
type: array
235+
kubernetesDefaultedString:
236+
default: forty-two
237+
description: This tests that primitive defaulting can be performed.
238+
type: string
175239
embeddedResource:
176240
type: object
177241
x-kubernetes-embedded-resource: true
@@ -6886,13 +6950,20 @@ spec:
68866950
- defaultedObject
68876951
- defaultedSlice
68886952
- defaultedString
6953+
- doubleDefaultedString
68896954
- embeddedResource
68906955
- float64WithValidations
68916956
- floatWithValidations
68926957
- foo
68936958
- int32WithValidations
68946959
- intWithValidations
68956960
- jobTemplate
6961+
- kubernetesDefaultedEmptyMap
6962+
- kubernetesDefaultedEmptyObject
6963+
- kubernetesDefaultedEmptySlice
6964+
- kubernetesDefaultedObject
6965+
- kubernetesDefaultedSlice
6966+
- kubernetesDefaultedString
68966967
- mapOfInfo
68976968
- nestedMapOfInfo
68986969
- nestedStructWithSeveralFields

pkg/markers/parse.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ package markers
1818

1919
import (
2020
"bytes"
21+
"encoding/json"
22+
"errors"
2123
"fmt"
2224
"reflect"
2325
"strconv"
@@ -726,6 +728,11 @@ type Definition struct {
726728
// Strict indicates that this definition should error out when parsing if
727729
// not all non-optional fields were seen.
728730
Strict bool
731+
732+
// parseJSON indicates the value should be parsed using json parsing.
733+
// This means strings must be quoted, arrays must use [] notation, etc.
734+
// This is only allowed when AnonymousField() is true.
735+
parseJSON bool
729736
}
730737

731738
// AnonymousField indicates that the definition has one field,
@@ -824,6 +831,49 @@ func parserScanner(raw string, err func(*sc.Scanner, string)) *sc.Scanner {
824831
// raw marker in the form `+a:b:c=arg,d=arg` into an output object of the
825832
// type specified in the definition.
826833
func (d *Definition) Parse(rawMarker string) (interface{}, error) {
834+
if d.parseJSON {
835+
return d.parseWithJSON(rawMarker)
836+
} else {
837+
return d.parseWithScanner(rawMarker)
838+
}
839+
}
840+
841+
func (d *Definition) parseWithJSON(rawMarker string) (interface{}, error) {
842+
_, _, fields := splitMarker(rawMarker)
843+
844+
out := reflect.Indirect(reflect.New(d.Output))
845+
846+
var errs []error
847+
if !d.AnonymousField() {
848+
errs = append(errs, errors.New("parseJSON requires anonymous definition"))
849+
return out.Interface(), loader.MaybeErrList(errs)
850+
}
851+
852+
// might still be a struct that something fiddled with, so double check
853+
structFieldName := d.FieldNames[""]
854+
outTarget := out
855+
if structFieldName != "" {
856+
// it's a struct field mapped to an anonymous marker
857+
outTarget = out.FieldByName(structFieldName)
858+
if !outTarget.CanSet() {
859+
errs = append(errs, fmt.Errorf("cannot set field %q (might not exist)", structFieldName))
860+
return out.Interface(), loader.MaybeErrList(errs)
861+
}
862+
}
863+
864+
fmt.Println("parse", fields, "into", outTarget)
865+
866+
var i any
867+
if err := json.Unmarshal([]byte(fields), &i); err != nil {
868+
errs = append(errs, err)
869+
} else {
870+
castAndSet(outTarget, reflect.ValueOf(i))
871+
}
872+
873+
return out.Interface(), loader.MaybeErrList(errs)
874+
}
875+
876+
func (d *Definition) parseWithScanner(rawMarker string) (interface{}, error) {
827877
name, anonName, fields := splitMarker(rawMarker)
828878

829879
out := reflect.Indirect(reflect.New(d.Output))
@@ -952,6 +1002,18 @@ func MakeAnyTypeDefinition(name string, target TargetType, output interface{}) (
9521002
return defn, nil
9531003
}
9541004

1005+
// MakeAnyTypeJSONDefinition constructs a definition for an output struct with a
1006+
// field named `Value` of type `interface{}`. The argument to the marker will
1007+
// be parsed as AnyType using json parsing and assigned to the field named `Value`.
1008+
func MakeAnyTypeJSONDefinition(name string, target TargetType, output interface{}) (*Definition, error) {
1009+
defn, err := MakeAnyTypeDefinition(name, target, output)
1010+
if err != nil {
1011+
return nil, err
1012+
}
1013+
defn.parseJSON = true
1014+
return defn, nil
1015+
}
1016+
9551017
// splitMarker takes a marker in the form of `+a:b:c=arg,d=arg` and splits it
9561018
// into the name (`a:b`), the name if it's not a struct (`a:b:c`), and the parts
9571019
// that are definitely fields (`arg,d=arg`).

0 commit comments

Comments
 (0)