11
11
- [ Goals] ( #goals )
12
12
- [ Non-Goals] ( #non-goals )
13
13
- [ Proposal] ( #proposal )
14
+ - [ Transition Rules] ( #transition-rules )
15
+ - [ Use Cases] ( #use-cases )
16
+ - [ Considerations] ( #considerations )
17
+ - [ Alternatives Evaluated] ( #alternatives-evaluated )
14
18
- [ Expression lifecycle] ( #expression-lifecycle )
15
19
- [ Function library] ( #function-library )
16
20
- [ User Stories] ( #user-stories )
@@ -292,18 +296,9 @@ is scoped to.
292
296
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
293
297
like the `all` macro, e.g. `self.all(listItem, <predicate>)` or `self.all(mapKey,
294
298
<predicate>)`.
295
-
296
- - For immutability use case, validator will have access to the existing version of the object. This
297
- will be accessible to CEL via the `oldSelf` identifier.
298
- - This will only be available on mergable collection types such as objects (unless
299
- ` x-kubernetes-map-type=atomic` ), maps with `x-kubernetes-map-type=granular` and lists
300
- with `x-kubernetes-list-type` set to `set` or `map`. See [Merge
301
- Strategy](https://kubernetes.io/docs/reference/using-api/server-side-apply/#merge-strategy) for
302
- details.
303
- - The use of "old" is congruent with how `AdmissionReview` identifies the existing object as
304
- ` oldObject` .
305
- - xref [analysis of possible interactions with immutability and
306
- validation](https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/1101-immutable-fields#openapi-extension-x-kubernetes-immutable).
299
+
300
+ - Rule expressions may reference existing object state using the identifier `oldSelf`. Such
301
+ " [transition rules](#transition-rules)" apply only under limited circumstances.
307
302
308
303
- Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible and are escaped
309
304
according to the following rules when accessed in the expression :
@@ -327,17 +322,132 @@ like the `all` macro, e.g. `self.all(listItem, <predicate>)` or `self.all(mapKey
327
322
all validation mechanisms (e.g. the OpenAPIV3 `maxItems` restriction), not just CEL validator
328
323
rules. xref rule 4 in [specifying a structural schema](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#specifying-a-structural-schema).
329
324
330
- - We plan to allow access to the current state of the object to allow validation rules to check the
331
- new value against the current value, e.g. for immutability checks (for validation racheting we would
332
- prefer an approach like described in https://github.com/kubernetes/kubernetes/issues/94060 be pursued).
333
-
334
325
- If the CEL evaluation exceeds the bounds we set (details below), the server will return a 408
335
326
(Request Timeout) HTTP status code. The timeout will be a backstop we expect to rarely be used
336
327
since CEL evaluations are multiple orders of magnitude faster that typical webhook invocations,
337
328
and we can use CEL expression complexity estimations
338
329
([xref](https://github.com/jinmmin/cel-go/blob/a661c99f8e27676c70fc00f4f328476ca4dcdb7f/cel/program.go#L265))
339
330
during CRD update to bound complexity.
340
331
332
+ # ### Transition Rules
333
+
334
+ A rule that contains an expression referencing the identifier `oldSelf` is implicitly considered a
335
+ " transition rule" . Transition rules allow schema authors to prevent certain transitions between two
336
+ otherwise valid states. For example :
337
+
338
+ ` ` ` yaml
339
+ type: string
340
+ enum: ["low", "medium", "high"]
341
+ x-kubernetes-validations:
342
+ - rule: "!(self == 'high' && oldSelf == 'low') && !(self == 'low' && oldSelf == 'high')"
343
+ message: cannot transition directly between 'low' and 'high'
344
+ ` ` `
345
+
346
+ Unlike other rules, transition rules apply only to operations meeting the following criteria :
347
+
348
+ - The operation updates an existing object. Transition rules never apply to create operations.
349
+
350
+ - Both an old and a new value exist. It remains possible to check if a value has been added or
351
+ removed by placing a transition rule on the parent node. Transition rules are never applied to
352
+ custom resource creation. When placed on an optional field, a transition rule will not apply to
353
+ update operations that set or unset the field.
354
+
355
+ - The path to the schema node being validated by a transition rule must resolve to a node that is
356
+ comparable between the old object and the new object. For example, list items and their
357
+ descendants (`spec.foo[10].bar`) can't necessarily be correlated between an existing object and a
358
+ later update to the same object.
359
+
360
+ - Semantics from [server-side
361
+ apply](https://kubernetes.io/docs/reference/using-api/server-side-apply/#merge-strategy) will be
362
+ honored. In particular, updates to descendants of collection types that are mergeable according
363
+ to server-side apply may be validated by transition rules. This includes the elements of maps
364
+ marked `x-kubernetes-map-type=granular` and lists marked
365
+ ` x-kubernetes-list-type=map` . Transition rules apply to elements of `map`-type lists only when
366
+ an element exists in both the old and new object having identical values for all key
367
+ fields. Elements of lists marked `x-kubernetes-list-type=set` and their descendants do not
368
+ support transition rules, however, set membership changes are accessible to transition rules
369
+ defined on the parent (i.e. array) node.
370
+
371
+ If all of the above criteria are satisfied for a given operation, the transition rule will be
372
+ enforced. The identifier `oldSelf` is guaranteed to be bound to a non-null value during expression
373
+ evaluation.
374
+
375
+ Errors will be generated on CRD writes if a schema node contains a transition rule that can never be
376
+ applied, e.g. "*path* : update rule *rule* cannot be set on schema because the schema or its parent
377
+ schema is not mergeable".
378
+
379
+ # #### Use Cases
380
+
381
+ | Use Case | Rule
382
+ | -------- | --------
383
+ | Immutability | `self.foo == oldSelf.foo`
384
+ | Prevent modification/removal once assigned | `oldSelf != 'bar' \|\| self == 'bar'` or `!has(oldSelf.field) \|\| has(self.field)`
385
+ | Append-only set | `self.all(element, element in oldSelf)`
386
+ | If previous value was X, new value can only be A or B, not Y or Z | `oldSelf != 'X' \|\| self in ['A', 'B']`
387
+ | Nondecreasing counters | `self >= oldSelf`
388
+
389
+ # #### Considerations
390
+
391
+ - The use of the prefix "old" in the identifier `oldSelf` is congruent with how `AdmissionReview`
392
+ identifies the existing object as `oldObject`.
393
+
394
+ - CEL doesn't support checking if a root variable is bound. The macro `has` can test for the
395
+ presence of fields, as in `has(self.child)`, but `has(oldSelf)` is not legal.
396
+
397
+ - It is possible for an attempt to update a custom resource to be rejected solely due to a
398
+ disallowed transition from existing state. This [complicates declarative reconciliation
399
+ tools](https://docs.google.com/document/d/1ZpCHE4yrOXoawai8Ldz49A4t9ni-QsHSuoy71VhBTLw/edit) that
400
+ need to decide whether or not to delete-and-recreate an existing object. This problem exists today
401
+ with fields that are immutable or immutable-once-assigned. Once a mechanism exists to communicate
402
+ to clients that an operation was rejected due to transition errors only, that mechanism will be
403
+ adopted for errors produced by the transition rules described in this document (see
404
+ https://github.com/kubernetes/kubernetes/issues/107919).
405
+
406
+ # #### Alternatives Evaluated
407
+
408
+ 1. A boolean field is added to the extension indicating when true that the rule applies only to
409
+ updates. The expression in "rule" may not reference the identifier "oldSelf" unless "updateRule"
410
+ is present and true. If "oldSelf" is referenced by the expression, and updateRule is absent or
411
+ false, an error is generated at CRD update time. The additional boolean field serves as an
412
+ explicit user acknowledgement that the rule should be treated as a transition rule, but doesn't
413
+ serve a technical purpose.
414
+
415
+ ` ` ` yaml
416
+ x-kubernetes-validations:
417
+ - rule: "oldSelf.xyz"
418
+ updateRule: true
419
+ ` ` `
420
+
421
+ 2. A validation context object (`context`) is exposed to expressions. For comparable update
422
+ operations, the context object will provide access to the old value via a field. Schema authors
423
+ take responsibility for avoiding runtime errors in more complex expressions.
424
+
425
+ ` ` ` yaml
426
+ x-kubernetes-validations:
427
+ - rule: "self.size() <= 10 || (has(context.oldSelf) && self.size() <= context.oldSelf.size())"
428
+ ` ` `
429
+
430
+ 3. A new string field named "updateRule" permits CEL expressions that reference `oldSelf`. If "rule"
431
+ and "updateRule" are mutually exclusive, this is effectively equivalent to alternative 1.
432
+
433
+ ` ` ` yaml
434
+ x-kubernetes-validations:
435
+ - updateRule: "oldSelf.xyz"
436
+ ` ` `
437
+
438
+ 4. An "updateRule" field is added, as in alternative 3, but it is not mutually exclusive with the
439
+ " rule" field. The "updateRule" applies when the transition rule criteria are satisfied, otherwise
440
+ " rule" applies. This alternative allows validations to accept an update while rejecting the
441
+ creation of an identical new object, but may be in conflict with support for
442
+ auto-ratcheting. Additionally, requiring two expressions instead of one makes validation rules
443
+ more difficult for users to understand.
444
+
445
+ ` ` ` yaml
446
+ x-kubernetes-validations:
447
+ - rule: "self in [2, 3]"
448
+ updateRule: "self in [2, 3] || (self == 1 && self == oldSelf)"
449
+ ` ` `
450
+
341
451
# ### Expression lifecycle
342
452
343
453
When CRDs are written to the kube-apiserver, all expressions will be [parsed and
0 commit comments