Skip to content

Commit 8bce013

Browse files
committed
remove access to object fields as root bound variables
1 parent 71308bb commit 8bce013

File tree

1 file changed

+32
-44
lines changed
  • keps/sig-api-machinery/2876-crd-validation-expression-language

1 file changed

+32
-44
lines changed

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

Lines changed: 32 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
- [Goals](#goals)
1212
- [Non-Goals](#non-goals)
1313
- [Proposal](#proposal)
14-
- [Field paths and field patterns](#field-paths-and-field-patterns)
1514
- [Expression lifecycle](#expression-lifecycle)
1615
- [Function library](#function-library)
1716
- [User Stories](#user-stories)
@@ -140,7 +139,7 @@ suited or insufficient.
140139
These improvements are largely complementary to expression support and either
141140
are (or should be) addressed by in separate KEPs.
142141

143-
For use cases cannot be covered by build-in validation support:
142+
For use cases that cannot be covered by build-in validation support:
144143

145144
- Admission Webhooks: have validating admission webhook for further validation
146145
- Custom validators: write custom checks in several languages such as Rego
@@ -216,7 +215,7 @@ kind: CustomResourceDefinition
216215
properties:
217216
spec:
218217
x-kubernetes-validations:
219-
- rule: "minReplicas <= maxReplicas"
218+
- rule: "self.minReplicas <= self.maxReplicas"
220219
message: "minReplicas cannot be larger than maxReplicas"
221220
type: object
222221
properties:
@@ -228,18 +227,18 @@ kind: CustomResourceDefinition
228227
229228
Example Validation Rules:
230229
231-
| Rule | Purpose |
232-
| ---------------- | ------------ |
233-
| `minReplicas <= replicas <= maxReplicas` | Validate that the three fields defining replicas are ordered appropriately |
234-
| `'Available' in stateCounts` | Validate the 'Available' key exists in a map |
235-
| `(size(list1) == 0) != (size(list2) == 0)` | Validate that one of two lists is non-empty, but not both |
236-
| `created + ttl < expiry` | Validate that 'expiry' date is after a 'create' date plus a 'ttl' duration |
237-
| `health.startsWith('ok')` | Validate a 'health' string field has the prefix 'ok' |
238-
| `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 |
239-
| `type(limit) == string ? limit == '100%' : limit == 1000` | Validate an int-or-string field for both the the int and string cases |
240-
| `metadata.name == 'singleton` | Validate that an object's name matches a specific value (making it a singleton) |
241-
| `set1.all(e, !(e in set2))` | Validate that two listSets are disjoint |
242-
| `size(names) == size(details) && names.all(n, n in details)` | Validate the 'details' map is keyed by the items in the names listSet |
230+
| Rule | Purpose |
231+
| ---------------- | ------------ |
232+
| `self.minReplicas <= 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+
| `has(self.expired) && self.created + self.ttl < self.expired` | Validate that 'expired' date is after a 'create' date plus a 'ttl' duration |
236+
| `self.health.startsWith('ok')` | Validate a 'health' string field has the prefix 'ok' |
237+
| `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 |
238+
| `type(self.limit) == string ? self.limit == '100%' : self.limit == 1000` | Validate an int-or-string field for both the the int and string cases |
239+
| `self.metadata.name == 'singleton` | Validate that an object's name matches a specific value (making it a singleton) |
240+
| `self.set1.all(e, !(e in self.set2))` | Validate that two listSets are disjoint |
241+
| `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 |
243242

244243

245244
- Each validator may have multiple validation rules.
@@ -279,39 +278,34 @@ is scoped to.
279278
280279
It will cause a lot of keywords to be reserved and users have to memorize those variable when writing rules.
281280
- Using other names like `this`, `me`, `value`, `_`. The name should be self-explanatory, less chance of conflict and easy to be picked up.
282-
- For OpenAPIv3 object types, the expression will have direct access to all the
283-
fields of the object the validator is scoped to.
281+
282+
- For OpenAPIv3 object types, the expression may use field selection to access all the
283+
properties of the object the validator is scoped to, e.g. `self.field == 10`.
284284

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

289289
- For OpenAPIv3 list and map types, the expression will have access to the data element of the list
290290
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
291291
like the `all` macro, e.g. `self.all(listItem, <predicate>)` or `self.all(mapKey,
292292
<predicate>)`.
293293

294294
- For immutability use case, validator will have access to the existing version of the object. This
295-
will be accessible to CEL via the `old<propertyName>` identifier.
295+
will be accessible to CEL via the `oldSelf` identifier.
296296
- This will only be available on mergable collection types such as objects (unless
297297
`x-kubernetes-map-type=atomic`), maps with `x-kubernetes-map-type=granular` and lists
298298
with `x-kubernetes-list-type` set to `set` or `map`. See [Merge
299299
Strategy](https://kubernetes.io/docs/reference/using-api/server-side-apply/#merge-strategy) for
300300
details.
301301
- The use of "old" is congruent with how `AdmissionReview` identifies the existing object as
302-
`oldObject`. To avoid name collisions `old<propertyName>` will be treated the same as a CEL
303-
keyword for escaping purposes (see below).
302+
`oldObject`.
304303
- xref [analysis of possible interactions with immutability and
305304
validation](https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/1101-immutable-fields#openapi-extension-x-kubernetes-immutable).
306305

307306
- If a object property name is a CEL keyword (see RESERVED in [CEL Syntax](https://github.com/google/cel-spec/blob/master/doc/langdef.md#syntax)),
308307
it will be escaped by prepending a _ prefix. To prevent this from causing a subsequent collision, properties named with a CEL keyword and a `_` prefix will be
309308
prefixed by `__` (generally, N+1 the existing number of `_`s).
310-
311-
- If a object property name is a CEL language identifier (`int`, `uint`, `double`, `bool`, `string`,
312-
`bytes`, `list`, `map`, `null_type`, `type`, see [CEL language
313-
identifiers](https://github.com/google/cel-spec/blob/master/doc/langdef.md#values)) it is not
314-
accessible as a root variable and must be accessed via `self`, .e.g. `self.int`.
315309

316310
- Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible.
317311
If a property name is "self" or matches with a [reserved language identifier](https://github.com/google/cel-spec/blob/v0.6.0/doc/langdef.md#values)
@@ -330,10 +324,10 @@ like the `all` macro, e.g. `self.all(listItem, <predicate>)` or `self.all(mapKey
330324
- Rules may be written at the root of an object, and may make field selection into any fields
331325
declared in the OpenAPIv3 schema of the CRD as well as `apiVersion`, `kind`, `metadata.name` and
332326
`metadata.generateName`. This includes selection of fields in both the `spec` and `status` in the
333-
same expression, e.g. `status.quantity <= spec.maxQuantity`. Because CRDs only allow the `name`
327+
same expression, e.g. `self.status.quantity <= self.spec.maxQuantity`. Because CRDs only allow the `name`
334328
and `generateName` to be declared in the `metadata` of an object, these are the only metadata
335329
fields that may be validated using CEL validator rules. For example,
336-
`metadata.name.endsWith('mySuffix')` is allowed, but `size(metadata.labels) < 3` it not
330+
`self.metadata.name.endsWith('mySuffix')` is allowed, but `size(self.metadata.labels) < 3` it not
337331
allowed. The limit on which `metadata` fields may be validated is an intentional design choice
338332
(that aims to keep metadata behavior uniform across types) and applies to all validation
339333
mechanisms (e.g. the OpenAPIV3 `maxItems` restriction), not just CEL validator rules.
@@ -349,14 +343,6 @@ like the `all` macro, e.g. `self.all(listItem, <predicate>)` or `self.all(mapKey
349343
([xref](https://github.com/jinmmin/cel-go/blob/a661c99f8e27676c70fc00f4f328476ca4dcdb7f/cel/program.go#L265))
350344
during CRD update to bound complexity.
351345

352-
#### Field paths and field patterns
353-
354-
A field path is a patch to a single node in the data tree. I.e. it specifies the
355-
exact indices of the list items and the keys of map entries it traverses.
356-
357-
A field *pattern* is a path to all nodes in the data tree that match the pattern. I.e.
358-
it may wildcard list item and map keys.
359-
360346
#### Expression lifecycle
361347

362348
When CRDs are written to the kube-apiserver, all expressions will be [parsed and
@@ -584,18 +570,20 @@ The initial changes made in the type integration will be:
584570
- We couldn't have both: (a) equality of string keys, which implies a canonical ordering of key fields, and (b) ability for developers to successfully lookup a map entry regardless of how they order the keys in the string representation
585571

586572

587-
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) and introduce utility functions to make the list representation easy to use in CEL. E.g. instead of:
573+
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).
574+
575+
Looking up entiries by keys is available primarily via the `exists_one` and `filter` macros. Examples:
588576

589577
```
590-
associativeList.filter(e, e.key1 == 'a' && e.key2 == 'b').all(e, e.val == 100)
591-
```
578+
// exists_one() and exists() behave similarly if all map keys are checked, but exsists_one() has slightly stricter
579+
// semantics, which make it preferable
592580
593-
We plan to add a `get()` builtin function (exact name and semantics TBD) that allows for lookup of a single map entry:
581+
// To check if the map contains a entry with a particular key:
582+
associativeList.exists_one(e, e.key1 == 'a' && e.key2 == 'b')
594583
584+
// To lookup a map entry by key and check if some condition is met on the other fields of the entry:
585+
associativeList.exists_one(e, e.key1 == 'a' && e.key2 == 'b' && e.val == 100)
595586
```
596-
associativeList.get(e, e.key1 == 'a' && e.key2 == 'b').val == 100
597-
```
598-
599587

600588
### Test Plan
601589

0 commit comments

Comments
 (0)