Skip to content

Commit d200945

Browse files
authored
Merge pull request #32425 from cici37/celDoc
Add resource constraints and oldSelf context into validation rule
2 parents ed6ac94 + 40157c8 commit d200945

File tree

1 file changed

+173
-0
lines changed

1 file changed

+173
-0
lines changed

content/en/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions.md

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,8 @@ CustomResourceDefinition schemas using the `x-kubernetes-validations` extension.
716716
The Rule is scoped to the location of the `x-kubernetes-validations` extension in the schema.
717717
And `self` variable in the CEL expression is bound to the scoped value.
718718

719+
All validation rules are scoped to the current object: no cross-object or stateful validation rules are supported.
720+
719721
For example:
720722

721723
```yaml
@@ -994,7 +996,178 @@ Here is the declarations type mapping between OpenAPIv3 and CEL type:
994996
xref: [CEL types](https://github.com/google/cel-spec/blob/v0.6.0/doc/langdef.md#values), [OpenAPI
995997
types](https://swagger.io/specification/#data-types), [Kubernetes Structural Schemas](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#specifying-a-structural-schema).
996998

999+
#### Validation functions {#available-validation-functions}
1000+
1001+
Functions available include:
1002+
- CEL standard functions, defined in the [list of standard definitions](https://github.com/google/cel-spec/blob/v0.7.0/doc/langdef.md#list-of-standard-definitions)
1003+
- CEL standard [macros](https://github.com/google/cel-spec/blob/v0.7.0/doc/langdef.md#macros)
1004+
- CEL [extended string function library](https://pkg.go.dev/github.com/google/[email protected]/ext#Strings)
1005+
- Kubernetes [CEL extension library](https://pkg.go.dev/k8s.io/[email protected]/pkg/apiserver/schema/cel/library#pkg-functions)
1006+
1007+
#### Transition rules
1008+
1009+
A rule that contains an expression referencing the identifier `oldSelf` is implicitly considered a
1010+
_transition rule_. Transition rules allow schema authors to prevent certain transitions between two
1011+
otherwise valid states. For example:
1012+
1013+
```yaml
1014+
type: string
1015+
enum: ["low", "medium", "high"]
1016+
x-kubernetes-validations:
1017+
- rule: "!(self == 'high' && oldSelf == 'low') && !(self == 'low' && oldSelf == 'high')"
1018+
message: cannot transition directly between 'low' and 'high'
1019+
```
1020+
1021+
Unlike other rules, transition rules apply only to operations meeting the following criteria:
1022+
1023+
- The operation updates an existing object. Transition rules never apply to create operations.
1024+
1025+
- Both an old and a new value exist. It remains possible to check if a value has been added or
1026+
removed by placing a transition rule on the parent node. Transition rules are never applied to
1027+
custom resource creation. When placed on an optional field, a transition rule will not apply to
1028+
update operations that set or unset the field.
1029+
1030+
- The path to the schema node being validated by a transition rule must resolve to a node that is
1031+
comparable between the old object and the new object. For example, list items and their
1032+
descendants (`spec.foo[10].bar`) can't necessarily be correlated between an existing object and a
1033+
later update to the same object.
1034+
1035+
Errors will be generated on CRD writes if a schema node contains a transition rule that can never be
1036+
applied, e.g. "*path*: update rule *rule* cannot be set on schema because the schema or its parent
1037+
schema is not mergeable".
1038+
1039+
Transition rules are only allowed on _correlatable portions_ of a schema.
1040+
A portion of the schema is correlatable if all `array` parent schemas are of type `x-kubernetes-list-type=map`; any `set`or `atomic`array parent schemas make it impossible to unambiguously correlate a `self` with `oldSelf`.
1041+
1042+
Here are some examples for transition rules:
1043+
1044+
{{< table caption="Transition rules examples" >}}
1045+
| Use Case | Rule
1046+
| -------- | --------
1047+
| Immutability | `self.foo == oldSelf.foo`
1048+
| Prevent modification/removal once assigned | `oldSelf != 'bar' \|\| self == 'bar'` or `!has(oldSelf.field) \|\| has(self.field)`
1049+
| Append-only set | `self.all(element, element in oldSelf)`
1050+
| If previous value was X, new value can only be A or B, not Y or Z | `oldSelf != 'X' \|\| self in ['A', 'B']`
1051+
| Monotonic (non-decreasing) counters | `self >= oldSelf`
1052+
{{< /table >}}
1053+
1054+
#### Resource use by validation functions
1055+
1056+
When you create or update a CustomResourceDefinition that uses validation rules,
1057+
the API server checks the likely impact of running those validation rules. If a rule is
1058+
estimated to be prohibitively expensive to execute, the API server rejects the create
1059+
or update operation, and returns an error message.
1060+
A similar system is used at runtime that observes the actions the interpreter takes. If the interpreter executes
1061+
too many instructions, execution of the rule will be halted, and an error will result.
1062+
Each CustomResourceDefinition is also allowed a certain amount of resources to finish executing all of
1063+
its validation rules. If the sum total of its rules are estimated at creation time to go over that limit,
1064+
then a validation error will also occur.
1065+
1066+
You are unlikely to encounter issues with the resource budget for validation if you only
1067+
specify rules that always take the same amount of time regardless of how large their input is.
1068+
For example, a rule that asserts that `self.foo == 1` does not by itself have any
1069+
risk of rejection on validation resource budget groups.
1070+
But if `foo` is a string and you define a validation rule `self.foo.contains("someString")`, that rule takes
1071+
longer to execute depending on how long `foo` is.
1072+
Another example would be if `foo` were an array, and you specified a validation rule `self.foo.all(x, x > 5)`. The cost system always assumes the worst-case scenario if
1073+
a limit on the length of `foo` is not given, and this will happen for anything that can be iterated
1074+
over (lists, maps, etc.).
1075+
1076+
Because of this, it is considered best practice to put a limit via `maxItems`, `maxProperties`, and
1077+
`maxLength` for anything that will be processed in a validation rule in order to prevent validation errors during cost estimation. For example, given this schema with one rule:
1078+
1079+
```yaml
1080+
openAPIV3Schema:
1081+
type: object
1082+
properties:
1083+
foo:
1084+
type: array
1085+
items:
1086+
type: string
1087+
x-kubernetes-validations:
1088+
- rule: "self.all(x, x.contains('a string'))"
1089+
```
1090+
1091+
then the API server rejects this rule on validation budget grounds with error:
1092+
```
1093+
spec.validation.openAPIV3Schema.properties[spec].properties[foo].x-kubernetes-validations[0].rule: Forbidden:
1094+
CEL rule exceeded budget by more than 100x (try simplifying the rule, or adding maxItems, maxProperties, and
1095+
maxLength where arrays, maps, and strings are used)
1096+
```
1097+
1098+
The rejection happens because `self.all` implies calling `contains()` on every string in `foo`,
1099+
which in turn will check the given string to see if it contains `'a string'`. Without limits, this is a very
1100+
expensive rule.
1101+
1102+
If you do not specify any validation limit, the estimated cost of this rule will exceed the per-rule cost limit. But if you
1103+
add limits in the appropriate places, the rule will be allowed:
1104+
1105+
```yaml
1106+
openAPIV3Schema:
1107+
type: object
1108+
properties:
1109+
foo:
1110+
type: array
1111+
maxItems: 25
1112+
items:
1113+
type: string
1114+
maxLength: 10
1115+
x-kubernetes-validations:
1116+
- rule: "self.all(x, x.contains('a string'))"
1117+
```
1118+
1119+
The cost estimation system takes into account how many times the rule will be executed in addition to the
1120+
estimated cost of the rule itself. For instance, the following rule will have the same estimated cost as the
1121+
previous example (despite the rule now being defined on the individual array items):
1122+
1123+
```yaml
1124+
openAPIV3Schema:
1125+
type: object
1126+
properties:
1127+
foo:
1128+
type: array
1129+
maxItems: 25
1130+
items:
1131+
type: string
1132+
x-kubernetes-validations:
1133+
- rule: "self.contains('a string'))"
1134+
maxLength: 10
1135+
```
1136+
1137+
If a list inside of a list has a validation rule that uses `self.all`, that is significantly more expensive
1138+
than a non-nested list with the same rule. A rule that would have been allowed on a non-nested list might need lower limits set on both nested lists in order to be allowed. For example, even without having limits set,
1139+
the following rule is allowed:
1140+
1141+
```yaml
1142+
openAPIV3Schema:
1143+
type: object
1144+
properties:
1145+
foo:
1146+
type: array
1147+
items:
1148+
type: integer
1149+
x-kubernetes-validations:
1150+
- rule: "self.all(x, x == 5)"
1151+
```
1152+
1153+
But the same rule on the following schema (with a nested array added) produces a validation error:
1154+
1155+
```yaml
1156+
openAPIV3Schema:
1157+
type: object
1158+
properties:
1159+
foo:
1160+
type: array
1161+
items:
1162+
type: array
1163+
items:
1164+
type: integer
1165+
x-kubernetes-validations:
1166+
- rule: "self.all(x, x == 5)"
1167+
```
9971168

1169+
This is because each item of `foo` is itself an array, and each subarray in turn calls `self.all`. Avoid nested
1170+
lists and maps if possible where validation rules are used.
9981171

9991172
### Defaulting
10001173

0 commit comments

Comments
 (0)