Skip to content

Commit 5807d2a

Browse files
chore(crd): Add +kubebuilder:validation:AllOf marker to require all specified fields
1 parent b20dcc4 commit 5807d2a

5 files changed

Lines changed: 168 additions & 7 deletions

File tree

pkg/crd/markers/validation.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const (
4040
ValidationExactlyOneOfPrefix = validationPrefix + "ExactlyOneOf"
4141
ValidationAtMostOneOfPrefix = validationPrefix + "AtMostOneOf"
4242
ValidationAtLeastOneOfPrefix = validationPrefix + "AtLeastOneOf"
43+
ValidationAllOfPrefix = validationPrefix + "AllOf"
4344

4445
// K8sEnumTag indicates that the given type is an enum; all const values of this type are considered values in the enum
4546
K8sEnumTag = "k8s:enum"
@@ -98,6 +99,8 @@ var TypeOnlyMarkers = []*definitionWithHelp{
9899
WithHelp(markers.SimpleHelp("CRD validation", "specifies a list of field names that must conform to the ExactlyOneOf constraint.")),
99100
must(markers.MakeDefinition(ValidationAtLeastOneOfPrefix, markers.DescribesType, AtLeastOneOf(nil))).
100101
WithHelp(markers.SimpleHelp("CRD validation", "specifies a list of field names that must conform to the AtLeastOneOf constraint.")),
102+
must(markers.MakeDefinition(ValidationAllOfPrefix, markers.DescribesType, AllOf(nil))).
103+
WithHelp(markers.SimpleHelp("CRD validation", "specifies a list of field names that must all be set.")),
101104
must(markers.MakeDefinition(K8sEnumTag, markers.DescribesType, K8sEnum{})).
102105
WithHelp(markers.SimpleHelp("CRD", "indicates that the given type is an enum; all const values of this type are considered values in the enum")),
103106
must(markers.MakeDefinition(K8sEnumTag, markers.DescribesField, K8sEnumField{})),
@@ -665,6 +668,22 @@ type ExactlyOneOf []string
665668
// +controllertools:marker:generateHelp:category="CRD validation"
666669
type AtLeastOneOf []string
667670

671+
// AllOf adds a validation constraint that requires all specified fields to be set.
672+
//
673+
// This marker may be repeated to specify multiple AllOf constraints.
674+
//
675+
// Example:
676+
//
677+
// // +kubebuilder:validation:AllOf=host;port;protocol
678+
// type ServerConfig struct {
679+
// Host *string
680+
// Port *int
681+
// Protocol *string
682+
// }
683+
//
684+
// +controllertools:marker:generateHelp:category="CRD validation"
685+
type AllOf []string
686+
668687
func (m Maximum) ApplyToSchema(ctx *SchemaContext, schema *apiextensionsv1.JSONSchemaProps) error {
669688
if !hasNumericType(schema) {
670689
return fmt.Errorf("must apply maximum to a numeric value, found %s", schema.Type)
@@ -1004,6 +1023,23 @@ func (AtLeastOneOf) ApplyPriority() ApplyPriority {
10041023
return ExactlyOneOf{}.ApplyPriority() + 1
10051024
}
10061025

1026+
func (fields AllOf) ApplyToSchema(ctx *SchemaContext, schema *apiextensionsv1.JSONSchemaProps) error {
1027+
if len(fields) == 0 {
1028+
return nil
1029+
}
1030+
rule := fieldsToOneOfCelRuleStr(fields)
1031+
xvalidation := XValidation{
1032+
Rule: fmt.Sprintf("%s == %d", rule, len(fields)),
1033+
Message: fmt.Sprintf("all fields in %v must be set", fields),
1034+
}
1035+
return xvalidation.ApplyToSchema(ctx, schema)
1036+
}
1037+
1038+
func (AllOf) ApplyPriority() ApplyPriority {
1039+
// explicitly go after AtLeastOneOf markers so that the ordering is deterministic
1040+
return AtLeastOneOf{}.ApplyPriority() + 1
1041+
}
1042+
10071043
// fieldsToOneOfCelRuleStr converts a slice of field names to a string representation
10081044
// [has(self.field1),has(self.field1),...].filter(x, x == true).size()
10091045
func fieldsToOneOfCelRuleStr(fields []string) string {
@@ -1026,7 +1062,7 @@ func fieldsToOneOfCelRuleStr(fields []string) string {
10261062
// registration a field-level use would be silently ignored.
10271063
type K8sEnumField struct{}
10281064

1029-
func (K8sEnumField) ApplyToSchema(*SchemaContext, *apiextensionsv1.JSONSchemaProps) error {
1065+
func (K8sEnumField) ApplyToSchema(ctx *SchemaContext, schema *apiextensionsv1.JSONSchemaProps) error {
10301066
return fmt.Errorf("k8s:enum must be set on a type, not a field")
10311067
}
10321068

pkg/crd/markers/zz_generated.markerhelp.go

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

pkg/crd/testdata/oneof/types.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ type OneofSpec struct {
3737

3838
TypeWithAllOneOf *TypeWithAllOneofs `json:"typeWithAllOneOf,omitempty"`
3939

40+
TypeWithAllOf *TypeWithAllOf `json:"typeWithAllOf,omitempty"`
41+
42+
TypeWithMultipleAllOf *TypeWithMultipleAllOf `json:"typeWithMultipleAllOf,omitempty"`
43+
4044
FirstCustomTypeAlias CustomTypeAlias `json:"firstCustomTypeAlias,omitempty"`
4145

4246
// This verifies if the custom type alias XValidation is not duplicated.
@@ -100,6 +104,22 @@ type TypeWithAllOneofs struct {
100104
F *string `json:"f,omitempty"`
101105
}
102106

107+
// +kubebuilder:validation:AllOf=host;port
108+
type TypeWithAllOf struct {
109+
Host *string `json:"host,omitempty"`
110+
Port *int `json:"port,omitempty"`
111+
Path *string `json:"path,omitempty"`
112+
}
113+
114+
// +kubebuilder:validation:AllOf=a;b
115+
// +kubebuilder:validation:AllOf=c;d
116+
type TypeWithMultipleAllOf struct {
117+
A *string `json:"a,omitempty"`
118+
B *string `json:"b,omitempty"`
119+
C *string `json:"c,omitempty"`
120+
D *string `json:"d,omitempty"`
121+
}
122+
103123
// CustomTypeAlias is a custom alias
104124
// +kubebuilder:validation:XValidation:rule="self >= 100 && self <= 1000",message="invalid CustomTypeAlias value"
105125
type CustomTypeAlias *int32

pkg/crd/testdata/testdata.kubebuilder.io_oneofs.yaml

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
---
12
apiVersion: apiextensions.k8s.io/v1
23
kind: CustomResourceDefinition
34
metadata:
5+
annotations:
6+
controller-gen.kubebuilder.io/version: (devel)
47
name: oneofs.testdata.kubebuilder.io
58
spec:
69
group: testdata.kubebuilder.io
@@ -107,6 +110,19 @@ spec:
107110
rule: '[has(self.a),has(self.b)].filter(x,x==true).size() <= 1'
108111
- message: at most one of the fields in [c d] may be set
109112
rule: '[has(self.c),has(self.d)].filter(x,x==true).size() <= 1'
113+
typeWithAllOf:
114+
properties:
115+
host:
116+
type: string
117+
path:
118+
type: string
119+
port:
120+
type: integer
121+
type: object
122+
x-kubernetes-validations:
123+
- message: all fields in [host port] must be set
124+
rule: '[has(self.host),has(self.port)].filter(x,x==true).size()
125+
== 2'
110126
typeWithAllOneOf:
111127
properties:
112128
a:
@@ -129,6 +145,22 @@ spec:
129145
rule: '[has(self.c),has(self.d)].filter(x,x==true).size() == 1'
130146
- message: at least one of the fields in [e f] must be set
131147
rule: '[has(self.e),has(self.f)].filter(x,x==true).size() >= 1'
148+
typeWithMultipleAllOf:
149+
properties:
150+
a:
151+
type: string
152+
b:
153+
type: string
154+
c:
155+
type: string
156+
d:
157+
type: string
158+
type: object
159+
x-kubernetes-validations:
160+
- message: all fields in [a b] must be set
161+
rule: '[has(self.a),has(self.b)].filter(x,x==true).size() == 2'
162+
- message: all fields in [c d] must be set
163+
rule: '[has(self.c),has(self.d)].filter(x,x==true).size() == 2'
132164
typeWithMultipleAtLeastOneOf:
133165
properties:
134166
a:
@@ -151,9 +183,3 @@ spec:
151183
type: object
152184
served: true
153185
storage: true
154-
status:
155-
acceptedNames:
156-
kind: ""
157-
plural: ""
158-
conditions: null
159-
storedVersions: null

pkg/crd/validation_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,74 @@ spec:
124124
`,
125125
wantErr: `spec.typeWithAllOneOf: Invalid value: at least one of the fields in [e f] must be set`,
126126
},
127+
{
128+
name: "AllOf constraint satisfied when all fields are set",
129+
obj: `---
130+
kind: Oneof
131+
apiVersion: testdata.kubebuilder.io/v1beta1
132+
metadata:
133+
name: test
134+
spec:
135+
typeWithAllOf:
136+
host: "localhost"
137+
port: 8080
138+
`,
139+
},
140+
{
141+
name: "AllOf constraint violated when one field is missing",
142+
obj: `---
143+
kind: Oneof
144+
apiVersion: testdata.kubebuilder.io/v1beta1
145+
metadata:
146+
name: test
147+
spec:
148+
typeWithAllOf:
149+
host: "localhost"
150+
`,
151+
wantErr: `spec.typeWithAllOf: Invalid value: all fields in [host port] must be set`,
152+
},
153+
{
154+
name: "AllOf constraint violated when all fields are missing",
155+
obj: `---
156+
kind: Oneof
157+
apiVersion: testdata.kubebuilder.io/v1beta1
158+
metadata:
159+
name: test
160+
spec:
161+
typeWithAllOf: {}
162+
`,
163+
wantErr: `spec.typeWithAllOf: Invalid value: all fields in [host port] must be set`,
164+
},
165+
{
166+
name: "Multiple AllOf constraints satisfied",
167+
obj: `---
168+
kind: Oneof
169+
apiVersion: testdata.kubebuilder.io/v1beta1
170+
metadata:
171+
name: test
172+
spec:
173+
typeWithMultipleAllOf:
174+
a: "a"
175+
b: "b"
176+
c: "c"
177+
d: "d"
178+
`,
179+
},
180+
{
181+
name: "Multiple AllOf constraints violated in first group",
182+
obj: `---
183+
kind: Oneof
184+
apiVersion: testdata.kubebuilder.io/v1beta1
185+
metadata:
186+
name: test
187+
spec:
188+
typeWithMultipleAllOf:
189+
a: "a"
190+
c: "c"
191+
d: "d"
192+
`,
193+
wantErr: `spec.typeWithMultipleAllOf: Invalid value: all fields in [a b] must be set`,
194+
},
127195
}
128196

129197
validator, err := newValidator(t.Context(), "./testdata/testdata.kubebuilder.io_oneofs.yaml")

0 commit comments

Comments
 (0)