Skip to content

Commit a684e69

Browse files
authored
Validation: add documentation and use CEL pre-processor (#3333)
* Move to oneof * more oneof * simplify SE one * simplify expressions * add validation readme * lint
1 parent 20632d1 commit a684e69

30 files changed

+158
-93
lines changed

GUIDELINES.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ followed for Istio APIs.
1919
- [Proto Guidelines](#proto-guidelines)
2020
- [Style](#style)
2121
- [Basic Proto Versioning](#basic-proto-ersioning)
22+
- [Validation Guidelines](#validation-guidelines)
2223
- [CRD Guidelines](#crd-guidelines)
2324
- [Style](#crd-style)
2425
- [Basic CRD Versioning](#basic-crd-versioning)
@@ -214,6 +215,75 @@ protos.
214215
215216
- Loosening validation is permitted. As a result, it is recommended to err on the side of stricter validation.
216217
218+
## Validation Guidelines
219+
220+
All types should have as strict validation specified on it as possible to rule out invalid states.
221+
These are ultimately compiled to Kubernetes CustomResourceDefinitions, which use OpenAPI validation with some Kubernetes extras.
222+
This is handled by our own custom [protoc-gen-crd](https://github.com/istio/tools/tree/master/cmd/protoc-gen-crd) which compiles our
223+
protobuf definitions down to CRDs.
224+
225+
There are a few types of validations:
226+
* Automatic ones, based on the protobuf type. For example, a UInt32Value automatically has a validation to check the number between `0` and `MaxUint32`
227+
* Protobuf `field_behavior`. Currently only `[(google.api.field_behavior) = REQUIRED]` is implemented.
228+
* Comment driven validations (see below).
229+
230+
Most validation is driven by comments on fields and messages.
231+
All validations in [KubeBuilder](https://book.kubebuilder.io/reference/markers/crd-validation) are supported, as well as some extras:
232+
233+
- `+protoc-gen-crd:map-value-validation`: apply the validation to each *value* in a map.
234+
Note it's not possible to apply validations to each key. You can, however, validate the entire map together with a CEL rule.
235+
- `+protoc-gen-crd:list-value-validation`: apply the validation to each value in a list.
236+
- `+protoc-gen-crd:duration-validation:none`: exclude the default requirement that a duration field is non-zero.
237+
- `+protoc-gen-crd:validation:XIntOrString`: marks a field as accepting integers or strings.
238+
- `+protoc-gen-crd:validation:IgnoreSubValidation`: if referencing a message in a field, and that message has some validation on it already, exclude the listed validations.
239+
This is uncommon, but can be used when referencing a message in a certain context has different rules than others.
240+
241+
The most common validations are:
242+
- Sizes: `MaxLength` (strings), `MaxItems` (lists), `MaxProperties` (maps)
243+
- Regex: `Pattern`
244+
- CEL: `XValidation`
245+
246+
### CEL
247+
248+
[CEL](https://cel.dev/) is a small language that allows us to write expressions to represent validation logic.
249+
This comes with a lot of quirks!
250+
251+
Useful tools and references:
252+
* [CEL playground](https://playcel.undistro.io/) allows an easy way to run CEL expressions against some types.
253+
* [Kubernetes CEL docs](https://kubernetes.io/docs/reference/using-api/cel/).
254+
* [CEL language definition](https://github.com/google/cel-spec/blob/master/doc/langdef.md).
255+
256+
The biggest challenge with CEL is the complexity limit imposed by Kubernetes.
257+
This estimates the cost to run the function, and rejects it if it is too high.
258+
This takes into account the cost of a function and the cost of *potential* inputs.
259+
This makes it, typically, required to put maximum size bounds on items.
260+
261+
Kubernetes changes version-to-version on how it estimates cost (usually getting more lenient) and what functions are available.
262+
We want to target the oldest version for compatibility purposes.
263+
Our tests do not currently cover this (a prototype of doing so can be found [here](https://github.com/istio/api/pull/3275)).
264+
A list of what features are in which versions can be found [here](https://kubernetes.io/docs/reference/using-api/cel/#cel-options-language-features-and-libraries).
265+
266+
Istio has some custom macros that are expanded at compile time, driven by the [celpp](https://github.com/howardjohn/celpp) package.
267+
This extends CEL with these capabilities:
268+
* **default**. Usage: `default(self.x, 'DEF')`.
269+
* **oneof**. Usage: `oneof(self.x, self.y, self.z)`. This checks that 0 or 1 of these fields is set.
270+
* **index**. Usage: `self.index({}, x, z, b)`. This does `self.x.z.b` and returns `{}` if any of these is not set.
271+
272+
Unlike typical Go usage, CEL does not have a concept of zero values for unset fields.
273+
As a result, an optional field needs special care.
274+
Do not write `self.fruit == 'apple'`, for instance, write `default(self.fruit, '') == 'apple'.
275+
276+
### Testing
277+
278+
As validation logic is really easy to get wrong, it's useful to write tests.
279+
This is done by adding YAML files under `tests/testdata`.
280+
Each type has a `valid` and `invalid` file to do positive and negative cases.
281+
282+
Aside from explicitly testing these, these also form the seed corpus for fuzzing when these are pulled into `istio/istio`.
283+
This fuzz testing verifies the CRD validation has the same result as the webhook (Golang) validation code.
284+
Currently, this mostly serves to ensure we do not make something overly strict.
285+
In the future, it may show us that its safe to disable the webhook entirely, if CRD validation can cover the full validation surface.
286+
217287
## CRD Guidelines
218288
219289
### CRD Style

extensions/v1alpha1/wasm.pb.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/v1alpha1/wasm.proto

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ option go_package="istio.io/api/extensions/v1alpha1";
236236
// +genclient
237237
// +k8s:deepcopy-gen=true
238238
// -->
239-
// +kubebuilder:validation:XValidation:message="only one of targetRefs or selector can be set",rule="(has(self.selector)?1:0)+(has(self.targetRef)?1:0)+(has(self.targetRefs)?1:0)<=1"
239+
// +kubebuilder:validation:XValidation:message="only one of targetRefs or selector can be set",rule="oneof(self.selector, self.targetRef, self.targetRefs)"
240240
message WasmPlugin {
241241
// Criteria used to select the specific set of pods/VMs on which
242242
// this plugin configuration should be applied. If omitted, this
@@ -462,7 +462,7 @@ message VmConfig {
462462
repeated EnvVar env = 1;
463463
}
464464

465-
// +kubebuilder:validation:XValidation:message="value may only be set when valueFrom is INLINE",rule="(has(self.valueFrom) ? self.valueFrom : '') != 'HOST' || !has(self.value)"
465+
// +kubebuilder:validation:XValidation:message="value may only be set when valueFrom is INLINE",rule="default(self.valueFrom, '') != 'HOST' || !has(self.value)"
466466
message EnvVar {
467467
// Name of the environment variable.
468468
// Must be a C_IDENTIFIER.

kubernetes/customresourcedefinitions.gen.yaml

Lines changed: 37 additions & 42 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

networking/v1/destination_rule_alias.gen.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

networking/v1/service_entry_alias.gen.go

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

networking/v1/workload_entry_alias.gen.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)