Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion pkg/crd/markers/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const (
ValidationExactlyOneOfPrefix = validationPrefix + "ExactlyOneOf"
ValidationAtMostOneOfPrefix = validationPrefix + "AtMostOneOf"
ValidationAtLeastOneOfPrefix = validationPrefix + "AtLeastOneOf"
ValidationAllOfPrefix = validationPrefix + "AllOf"

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

// AllOf adds a validation constraint that requires all specified fields to be set.
//
// This marker may be repeated to specify multiple AllOf constraints.
//
// Example:
//
// // +kubebuilder:validation:AllOf=host;port;protocol
// type ServerConfig struct {
// Host *string
// Port *int
// Protocol *string
// }
//
// +controllertools:marker:generateHelp:category="CRD validation"
type AllOf []string

func (m Maximum) ApplyToSchema(ctx *SchemaContext, schema *apiextensionsv1.JSONSchemaProps) error {
if !hasNumericType(schema) {
return fmt.Errorf("must apply maximum to a numeric value, found %s", schema.Type)
Expand Down Expand Up @@ -1004,6 +1023,23 @@ func (AtLeastOneOf) ApplyPriority() ApplyPriority {
return ExactlyOneOf{}.ApplyPriority() + 1
}

func (fields AllOf) ApplyToSchema(ctx *SchemaContext, schema *apiextensionsv1.JSONSchemaProps) error {
if len(fields) == 0 {
return nil
}
rule := fieldsToOneOfCelRuleStr(fields)
xvalidation := XValidation{
Rule: fmt.Sprintf("%s == %d", rule, len(fields)),
Message: fmt.Sprintf("all fields in %v must be set", fields),
}
return xvalidation.ApplyToSchema(ctx, schema)
}

func (AllOf) ApplyPriority() ApplyPriority {
// explicitly go after AtLeastOneOf markers so that the ordering is deterministic
return AtLeastOneOf{}.ApplyPriority() + 1
}

// fieldsToOneOfCelRuleStr converts a slice of field names to a string representation
// [has(self.field1),has(self.field1),...].filter(x, x == true).size()
func fieldsToOneOfCelRuleStr(fields []string) string {
Expand All @@ -1026,7 +1062,7 @@ func fieldsToOneOfCelRuleStr(fields []string) string {
// registration a field-level use would be silently ignored.
type K8sEnumField struct{}

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

Expand Down
11 changes: 11 additions & 0 deletions pkg/crd/markers/zz_generated.markerhelp.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions pkg/crd/testdata/oneof/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ type OneofSpec struct {

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

TypeWithAllOf *TypeWithAllOf `json:"typeWithAllOf,omitempty"`

TypeWithMultipleAllOf *TypeWithMultipleAllOf `json:"typeWithMultipleAllOf,omitempty"`

FirstCustomTypeAlias CustomTypeAlias `json:"firstCustomTypeAlias,omitempty"`

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

// +kubebuilder:validation:AllOf=host;port
type TypeWithAllOf struct {
Host *string `json:"host,omitempty"`
Port *int `json:"port,omitempty"`
Path *string `json:"path,omitempty"`
}

// +kubebuilder:validation:AllOf=a;b
// +kubebuilder:validation:AllOf=c;d
type TypeWithMultipleAllOf struct {
A *string `json:"a,omitempty"`
B *string `json:"b,omitempty"`
C *string `json:"c,omitempty"`
D *string `json:"d,omitempty"`
}

// CustomTypeAlias is a custom alias
// +kubebuilder:validation:XValidation:rule="self >= 100 && self <= 1000",message="invalid CustomTypeAlias value"
type CustomTypeAlias *int32
Expand Down
38 changes: 32 additions & 6 deletions pkg/crd/testdata/testdata.kubebuilder.io_oneofs.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: (devel)
name: oneofs.testdata.kubebuilder.io
spec:
group: testdata.kubebuilder.io
Expand Down Expand Up @@ -107,6 +110,19 @@ spec:
rule: '[has(self.a),has(self.b)].filter(x,x==true).size() <= 1'
- message: at most one of the fields in [c d] may be set
rule: '[has(self.c),has(self.d)].filter(x,x==true).size() <= 1'
typeWithAllOf:
properties:
host:
type: string
path:
type: string
port:
type: integer
type: object
x-kubernetes-validations:
- message: all fields in [host port] must be set
rule: '[has(self.host),has(self.port)].filter(x,x==true).size()
== 2'
typeWithAllOneOf:
properties:
a:
Expand All @@ -129,6 +145,22 @@ spec:
rule: '[has(self.c),has(self.d)].filter(x,x==true).size() == 1'
- message: at least one of the fields in [e f] must be set
rule: '[has(self.e),has(self.f)].filter(x,x==true).size() >= 1'
typeWithMultipleAllOf:
properties:
a:
type: string
b:
type: string
c:
type: string
d:
type: string
type: object
x-kubernetes-validations:
- message: all fields in [a b] must be set
rule: '[has(self.a),has(self.b)].filter(x,x==true).size() == 2'
- message: all fields in [c d] must be set
rule: '[has(self.c),has(self.d)].filter(x,x==true).size() == 2'
typeWithMultipleAtLeastOneOf:
properties:
a:
Expand All @@ -151,9 +183,3 @@ spec:
type: object
served: true
storage: true
status:
acceptedNames:
kind: ""
plural: ""
conditions: null
storedVersions: null
68 changes: 68 additions & 0 deletions pkg/crd/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,74 @@ spec:
`,
wantErr: `spec.typeWithAllOneOf: Invalid value: at least one of the fields in [e f] must be set`,
},
{
name: "AllOf constraint satisfied when all fields are set",
obj: `---
kind: Oneof
apiVersion: testdata.kubebuilder.io/v1beta1
metadata:
name: test
spec:
typeWithAllOf:
host: "localhost"
port: 8080
`,
},
{
name: "AllOf constraint violated when one field is missing",
obj: `---
kind: Oneof
apiVersion: testdata.kubebuilder.io/v1beta1
metadata:
name: test
spec:
typeWithAllOf:
host: "localhost"
`,
wantErr: `spec.typeWithAllOf: Invalid value: all fields in [host port] must be set`,
},
{
name: "AllOf constraint violated when all fields are missing",
obj: `---
kind: Oneof
apiVersion: testdata.kubebuilder.io/v1beta1
metadata:
name: test
spec:
typeWithAllOf: {}
`,
wantErr: `spec.typeWithAllOf: Invalid value: all fields in [host port] must be set`,
},
{
name: "Multiple AllOf constraints satisfied",
obj: `---
kind: Oneof
apiVersion: testdata.kubebuilder.io/v1beta1
metadata:
name: test
spec:
typeWithMultipleAllOf:
a: "a"
b: "b"
c: "c"
d: "d"
`,
},
{
name: "Multiple AllOf constraints violated in first group",
obj: `---
kind: Oneof
apiVersion: testdata.kubebuilder.io/v1beta1
metadata:
name: test
spec:
typeWithMultipleAllOf:
a: "a"
c: "c"
d: "d"
`,
wantErr: `spec.typeWithMultipleAllOf: Invalid value: all fields in [a b] must be set`,
},
}

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