Skip to content

Commit b2a6684

Browse files
committed
Address feedback, clarify beta usage and adoption expectations
1 parent 4adef38 commit b2a6684

File tree

1 file changed

+34
-24
lines changed
  • keps/sig-api-machinery/2876-crd-validation-expression-language

1 file changed

+34
-24
lines changed

keps/sig-api-machinery/2876-crd-validation-expression-language/README.md

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -227,20 +227,20 @@ kind: CustomResourceDefinition
227227
228228
Example Validation Rules:
229229
230-
| Rule | Purpose |
231-
| ---------------- | ------------ |
232-
| `self.minReplicas <= self.replicas && self.replicas <= self.maxReplicas` | Validate that the three fields defining replicas are ordered appropriately |
233-
| `'Available' in self.stateCounts` | Validate that an entry with the 'Available' key exists in a map |
234-
| `(size(self.list1) == 0) != (size(self.list2) == 0)` | Validate that one of two lists is non-empty, but not both |
235-
| `!('MY_KEY' in self.map1) || self['MY_KEY].matches('^[a-zA-Z]*$')` | Validate the value of a map for a specific key, if it is in the map |
236-
| `self.envars.filter(e, e.name = 'MY_ENV').all(e, e.value.matches('^[a-zA-Z]*$')` | Validate the 'value' field of a listMap entry where key field 'name' is 'MY_ENV' |
237-
| `has(self.expired) && self.created + self.ttl < self.expired` | Validate that 'expired' date is after a 'create' date plus a 'ttl' duration |
238-
| `self.health.startsWith('ok')` | Validate a 'health' string field has the prefix 'ok' |
239-
| `self.widgets.exists(w, w.key == 'x' && w.foo < 10)` | Validate that the 'foo' property of a listMap item with a key 'x' is less than 10 |
240-
| `type(self) == string ? self == '100%' : self == 1000` | Validate an int-or-string field for both the the int and string cases |
241-
| `self.metadata.name == 'singleton'` | Validate that an object's name matches a specific value (making it a singleton) |
242-
| `self.set1.all(e, !(e in self.set2))` | Validate that two listSets are disjoint |
243-
| `size(self.names) == size(self.details) && self.names.all(n, n in self.details)` | Validate the 'details' map is keyed by the items in the 'names' listSet |
230+
| Rule | Purpose |
231+
| ---------------- | ------------ |
232+
| `self.minReplicas <= self.replicas && self.replicas <= self.maxReplicas` | Validate that the three fields defining replicas are ordered appropriately |
233+
| `'Available' in self.stateCounts` | Validate that an entry with the 'Available' key exists in a map |
234+
| `(self.list1.size() == 0) != self.list2.size() == 0)` | Validate that one of two lists is non-empty, but not both |
235+
| `!('MY_KEY' in self.map1) \|\| self['MY_KEY'].matches('^[a-zA-Z]*$')` | Validate the value of a map for a specific key, if it is in the map |
236+
| `self.envars.filter(e, e.name = 'MY_ENV').all(e, e.value.matches('^[a-zA-Z]*$')` | Validate the 'value' field of a listMap entry where key field 'name' is 'MY_ENV' |
237+
| `has(self.expired) && self.created + self.ttl < self.expired` | Validate that 'expired' date is after a 'create' date plus a 'ttl' duration |
238+
| `self.health.startsWith('ok')` | Validate a 'health' string field has the prefix 'ok' |
239+
| `self.widgets.exists(w, w.key == 'x' && w.foo < 10)` | Validate that the 'foo' property of a listMap item with a key 'x' is less than 10 |
240+
| `type(self) == string ? self == '100%' : self == 1000` | Validate an int-or-string field for both the the int and string cases |
241+
| `self.metadata.name == 'singleton'` | Validate that an object's name matches a specific value (making it a singleton) |
242+
| `self.set1.all(e, !(e in self.set2))` | Validate that two listSets are disjoint |
243+
| `self.names.size() == self.details.size() && self.names.all(n, n in self.details)` | Validate the 'details' map is keyed by the items in the 'names' listSet |
244244

245245

246246
- Each validator may have multiple validation rules.
@@ -286,7 +286,7 @@ is scoped to.
286286

287287
- For OpenAPIv3 scalar types (integer, string & boolean), the expression will have access to the
288288
scalar data element the validator is scoped to. The data element will be accessible to CEL
289-
expressions via `self`, e.g. `len(self) > 10`.
289+
expressions via `self`, e.g. `self.size() > 10`.
290290

291291
- For OpenAPIv3 list and map types, the expression will have access to the data element of the list
292292
or map. These will be accessible to CEL via `self`. The elements of a map or list can be validated using the CEL support for collections
@@ -321,7 +321,7 @@ like the `all` macro, e.g. `self.all(listItem, <predicate>)` or `self.all(mapKey
321321
same expression, e.g. `self.status.quantity <= self.spec.maxQuantity`. Because CRDs only allow the
322322
`name` and `generateName` to be declared in the `metadata` of an object, these are the only
323323
metadata fields that may be validated using CEL validator rules. For example,
324-
`self.metadata.name.endsWith('mySuffix')` is allowed, but `size(self.metadata.labels) < 3` it not
324+
`self.metadata.name.endsWith('mySuffix')` is allowed, but `self.metadata.labels.size() < 3` it not
325325
allowed. The limit on which `metadata` fields may be validated is an intentional design choice
326326
(that aims to allow for generic access to labels and annotations across all kinds) and applies to
327327
all validation mechanisms (e.g. the OpenAPIV3 `maxItems` restriction), not just CEL validator
@@ -502,7 +502,7 @@ Types:
502502

503503
| OpenAPIv3 type | CEL type |
504504
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
505-
| 'object' with Properties | object / "message type" |
505+
| 'object' with Properties | object / "message type" (`type(<object>)` evaluates to `selfType<uniqueNumber>.path.to.object.from.self` |
506506
| 'object' with AdditionalProperties | map |
507507
| 'object' with x-kubernetes-embedded-type | object / "message type", 'apiVersion', 'kind', 'metadata.name' and 'metadata.generateName' are implicitly included in schema |
508508
| 'object' with x-kubernetes-preserve-unknown-fields | object / "message type", unknown fields are NOT accessible in CEL expression |
@@ -513,6 +513,7 @@ Types:
513513
| 'boolean' | boolean |
514514
| 'number' (all formats) | double |
515515
| 'integer' (all formats) | int (64) |
516+
| <no equivalent> | uint (64) |
516517
| 'null' | null_type |
517518
| 'string' | string |
518519
| 'string' with format=byte (base64 encoded) | bytes |
@@ -567,17 +568,25 @@ The initial changes made in the type integration will be:
567568

568569
So instead of treating "associative lists" as maps in CEL, we will continue to treat them as lists, but override equality to ignore object order (i.e. use map equality semantics).
569570

570-
Looking up entiries by keys is available primarily via the `exists_one` and `filter` macros. Examples:
571+
Looking up and iterating over entiries is available via the `exists`, `exists_one` and `all` macros:
571572

572573
```
573-
// exists_one() and exists() behave similarly if all map keys are checked, but exists_one() has slightly stricter
574-
// semantics, which make it preferable
574+
// Since there is already a guarantee that "associative lists" only one entry
575+
// exists in the list for each key, `exists` can be used to check if a map contains a particular key instead of `exists_one`.
576+
// Note that `exists_one` can also be used, but handles any errors encountered more strictly.
577+
// See the CEL language spec how errors are handled by `exists_one` and `exists` for more details.
575578

576-
// To check if the map contains a entry with a particular key:
577-
associativeList.exists_one(e, e.key1 == 'a' && e.key2 == 'b')
579+
// Check if an "associative list" contains an entry for a key:
580+
associativeList.exists(e, e.key1 == 'a' && e.key2 == 'b')
578581

579-
// To lookup a map entry by key and check if some condition is met on the other fields of the entry:
580-
associativeList.exists_one(e, e.key1 == 'a' && e.key2 == 'b' && e.val == 100)
582+
// To validate a map contains an entry with a particular key and that some condition is met on the other fields of the entry:
583+
associativeList.exists(e, e.key1 == 'a' && e.key2 == 'b' && e.val == 100)
584+
585+
// To check the value for a particular key meets some condition (but also allow the entry to be absent):
586+
associativeList.all(e, e.key1 == 'a' && e.key2 == 'b' && e.val == 100)
587+
588+
// To check some condition on all entries of an "associative list":
589+
associativeList.all(e, e.val == 100)
581590
```
582591

583592
### Test Plan
@@ -603,6 +612,7 @@ developers to test their validation rules.
603612
- CEL numeric comparison issue is resolved (e.g. ability to compare ints to doubles)
604613
- [Reduce noise of invalid data messages reported from cel.UnstructuredToVal](https://github.com/kubernetes/kubernetes/issues/106440)
605614
- [Benchmark cel.UnstructuredToVal and optimize away repeated wrapper object construction](https://github.com/kubernetes/kubernetes/issues/106438)
615+
- Demonstrate adoption and successful feature usage in the community
606616

607617
## Production Readiness Review Questionnaire
608618

0 commit comments

Comments
 (0)