Skip to content

File tree

5 files changed

+192
-5
lines changed

5 files changed

+192
-5
lines changed

pkg/crd/markers/validation.go

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

9494
must(markers.MakeAnyTypeDefinition("kubebuilder:default", markers.DescribesField, Default{})).
9595
WithHelp(Default{}.Help()),
96+
must(markers.MakeDefinition("default", markers.DescribesField, KubernetesDefault{})).
97+
WithHelp(KubernetesDefault{}.Help()),
9698

9799
must(markers.MakeAnyTypeDefinition("kubebuilder:example", markers.DescribesField, Example{})).
98100
WithHelp(Example{}.Help()),
@@ -241,6 +243,20 @@ type Default struct {
241243
Value interface{}
242244
}
243245

246+
// +controllertools:marker:generateHelp:category="CRD validation"
247+
// Default sets the default value for this field.
248+
//
249+
// A default value will be accepted as any value valid for the field.
250+
// Only JSON-formatted values are accepted. `ref(...)` values are ignored.
251+
// Formatting for common types include: boolean: `true`, string:
252+
// `"Cluster"`, numerical: `1.24`, array: `[1,2]`, object: `{"policy":
253+
// "delete"}`). Defaults should be defined in pruned form, and only best-effort
254+
// validation will be performed. Full validation of a default requires
255+
// submission of the containing CRD to an apiserver.
256+
type KubernetesDefault struct {
257+
Value interface{}
258+
}
259+
244260
// +controllertools:marker:generateHelp:category="CRD validation"
245261
// Example sets the example value for this field.
246262
//
@@ -505,6 +521,39 @@ func (m Default) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
505521
return nil
506522
}
507523

524+
func (m Default) ApplyPriority() ApplyPriority {
525+
// explicitly go after +default markers, so kubebuilder-specific defaults get applied last and stomp
526+
return 10
527+
}
528+
529+
func (m *KubernetesDefault) ParseMarker(_ string, _ string, restFields string) error {
530+
if strings.HasPrefix(strings.TrimSpace(restFields), "ref(") {
531+
// Skip +default=ref(...) values for now, since we don't have a good way to evaluate go constant values via AST.
532+
// See https://github.com/kubernetes-sigs/controller-tools/pull/938#issuecomment-2096790018
533+
return nil
534+
}
535+
return json.Unmarshal([]byte(restFields), &m.Value)
536+
}
537+
538+
// Defaults are only valid CRDs created with the v1 API
539+
func (m KubernetesDefault) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
540+
if m.Value == nil {
541+
// only apply to the schema if we have a non-nil default value
542+
return nil
543+
}
544+
marshalledDefault, err := json.Marshal(m.Value)
545+
if err != nil {
546+
return err
547+
}
548+
schema.Default = &apiext.JSON{Raw: marshalledDefault}
549+
return nil
550+
}
551+
552+
func (m KubernetesDefault) ApplyPriority() ApplyPriority {
553+
// explicitly go before +kubebuilder:default markers, so kubebuilder-specific defaults get applied last and stomp
554+
return 9
555+
}
556+
508557
func (m Example) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
509558
marshalledExample, err := json.Marshal(m.Value)
510559
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: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import (
3838
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
3939
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
4040

41+
const DefaultRefValue = "defaultRefValue"
42+
4143
// CronJobSpec defines the desired state of CronJob
4244
// +kubebuilder:validation:XValidation:rule="has(oldSelf.forbiddenInt) || !has(self.forbiddenInt)",message="forbiddenInt is not allowed",fieldPath=".forbiddenInt",reason="FieldValueForbidden"
4345
type CronJobSpec struct {
@@ -116,9 +118,9 @@ type CronJobSpec struct {
116118
// +kubebuilder:example={a,b}
117119
DefaultedSlice []string `json:"defaultedSlice"`
118120

119-
// This tests that object defaulting can be performed.
120-
// +kubebuilder:default={{nested: {foo: "baz", bar: true}},{nested: {bar: false}}}
121-
// +kubebuilder:example={{nested: {foo: "baz", bar: true}},{nested: {bar: false}}}
121+
// This tests that slice and object defaulting can be performed.
122+
// +kubebuilder:default={{nested: {foo: "baz", bar: true}},{nested: {foo: "qux", bar: false}}}
123+
// +kubebuilder:example={{nested: {foo: "baz", bar: true}},{nested: {foo: "qux", bar: false}}}
122124
DefaultedObject []RootObject `json:"defaultedObject"`
123125

124126
// This tests that empty slice defaulting can be performed.
@@ -133,6 +135,39 @@ type CronJobSpec struct {
133135
// +kubebuilder:default={}
134136
DefaultedEmptyObject EmpiableObject `json:"defaultedEmptyObject"`
135137

138+
// This tests that kubebuilder defaulting takes precedence.
139+
// +kubebuilder:default="kubebuilder-default"
140+
// +default="kubernetes-default"
141+
DoubleDefaultedString string `json:"doubleDefaultedString"`
142+
143+
// This tests that primitive defaulting can be performed.
144+
// +default="forty-two"
145+
KubernetesDefaultedString string `json:"kubernetesDefaultedString"`
146+
147+
// This tests that slice defaulting can be performed.
148+
// +default=["a","b"]
149+
KubernetesDefaultedSlice []string `json:"kubernetesDefaultedSlice"`
150+
151+
// This tests that slice and object defaulting can be performed.
152+
// +default=[{"nested": {"foo": "baz", "bar": true}},{"nested": {"foo": "qux", "bar": false}}]
153+
KubernetesDefaultedObject []RootObject `json:"kubernetesDefaultedObject"`
154+
155+
// This tests that empty slice defaulting can be performed.
156+
// +default=[]
157+
KubernetesDefaultedEmptySlice []string `json:"kubernetesDefaultedEmptySlice"`
158+
159+
// This tests that an empty object defaulting can be performed on a map.
160+
// +default={}
161+
KubernetesDefaultedEmptyMap map[string]string `json:"kubernetesDefaultedEmptyMap"`
162+
163+
// This tests that an empty object defaulting can be performed on an object.
164+
// +default={}
165+
KubernetesDefaultedEmptyObject EmpiableObject `json:"kubernetesDefaultedEmptyObject"`
166+
167+
// This tests that use of +default=ref(...) doesn't break generation
168+
// +default=ref(DefaultRefValue)
169+
KubernetesDefaultedRef string `json:"kubernetesDefaultedRef,omitempty"`
170+
136171
// This tests that pattern validator is properly applied.
137172
// +kubebuilder:validation:Pattern=`^$|^((https):\/\/?)[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|\/?))$`
138173
PatternObject string `json:"patternObject"`

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

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,15 @@ spec:
133133
foo: baz
134134
- nested:
135135
bar: false
136-
description: This tests that object defaulting can be performed.
136+
foo: qux
137+
description: This tests that slice and object defaulting can be performed.
137138
example:
138139
- nested:
139140
bar: true
140141
foo: baz
141142
- nested:
142143
bar: false
144+
foo: qux
143145
items:
144146
properties:
145147
nested:
@@ -184,6 +186,74 @@ spec:
184186
explicitlyRequiredKubernetes:
185187
description: This tests explicitly required kubernetes fields
186188
type: string
189+
doubleDefaultedString:
190+
default: kubebuilder-default
191+
description: This tests that kubebuilder defaulting takes precedence.
192+
type: string
193+
kubernetesDefaultedEmptyMap:
194+
additionalProperties:
195+
type: string
196+
default: {}
197+
description: This tests that an empty object defaulting can be performed
198+
on a map.
199+
type: object
200+
kubernetesDefaultedEmptyObject:
201+
default: {}
202+
description: This tests that an empty object defaulting can be performed
203+
on an object.
204+
properties:
205+
bar:
206+
type: string
207+
foo:
208+
default: forty-two
209+
type: string
210+
type: object
211+
kubernetesDefaultedEmptySlice:
212+
default: []
213+
description: This tests that empty slice defaulting can be performed.
214+
items:
215+
type: string
216+
type: array
217+
kubernetesDefaultedObject:
218+
default:
219+
- nested:
220+
bar: true
221+
foo: baz
222+
- nested:
223+
bar: false
224+
foo: qux
225+
description: This tests that slice and object defaulting can be performed.
226+
items:
227+
properties:
228+
nested:
229+
properties:
230+
bar:
231+
type: boolean
232+
foo:
233+
type: string
234+
required:
235+
- bar
236+
- foo
237+
type: object
238+
required:
239+
- nested
240+
type: object
241+
type: array
242+
kubernetesDefaultedSlice:
243+
default:
244+
- a
245+
- b
246+
description: This tests that slice defaulting can be performed.
247+
items:
248+
type: string
249+
type: array
250+
kubernetesDefaultedString:
251+
default: forty-two
252+
description: This tests that primitive defaulting can be performed.
253+
type: string
254+
kubernetesDefaultedRef:
255+
description: This tests that use of +default=ref(...) doesn't break generation
256+
type: string
187257
embeddedResource:
188258
type: object
189259
x-kubernetes-embedded-resource: true
@@ -6898,6 +6968,7 @@ spec:
68986968
- defaultedObject
68996969
- defaultedSlice
69006970
- defaultedString
6971+
- doubleDefaultedString
69016972
- embeddedResource
69026973
- explicitlyRequiredKubebuilder
69036974
- explicitlyRequiredKubernetes
@@ -6907,6 +6978,12 @@ spec:
69076978
- int32WithValidations
69086979
- intWithValidations
69096980
- jobTemplate
6981+
- kubernetesDefaultedEmptyMap
6982+
- kubernetesDefaultedEmptyObject
6983+
- kubernetesDefaultedEmptySlice
6984+
- kubernetesDefaultedObject
6985+
- kubernetesDefaultedSlice
6986+
- kubernetesDefaultedString
69106987
- mapOfInfo
69116988
- nestedMapOfInfo
69126989
- nestedStructWithSeveralFields

pkg/markers/parse.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -820,13 +820,23 @@ func parserScanner(raw string, err func(*sc.Scanner, string)) *sc.Scanner {
820820
return scanner
821821
}
822822

823+
type markerParser interface {
824+
ParseMarker(name string, anonymousName string, restFields string) error
825+
}
826+
823827
// Parse uses the type information in this Definition to parse the given
824828
// raw marker in the form `+a:b:c=arg,d=arg` into an output object of the
825829
// type specified in the definition.
826830
func (d *Definition) Parse(rawMarker string) (interface{}, error) {
827831
name, anonName, fields := splitMarker(rawMarker)
828832

829-
out := reflect.Indirect(reflect.New(d.Output))
833+
outPointer := reflect.New(d.Output)
834+
out := reflect.Indirect(outPointer)
835+
836+
if parser, ok := outPointer.Interface().(markerParser); ok {
837+
err := parser.ParseMarker(name, anonName, fields)
838+
return out.Interface(), err
839+
}
830840

831841
// if we're a not a struct or have no arguments, treat the full `a:b:c` as the name,
832842
// otherwise, treat `c` as a field name, and `a:b` as the marker name.

0 commit comments

Comments
 (0)