Skip to content

Commit

Permalink
Update docs to explain how the new pseudolanguage works
Browse files Browse the repository at this point in the history
The v1beta1 schema version supports defining conditions with a
pseudolanguage that makes them more compact with respect to the old
structured operators. This commit updates the current docs to reflect
this change.
  • Loading branch information
jordipiqueselles committed Jun 2, 2024
1 parent c43633e commit 8e87555
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 156 deletions.
124 changes: 12 additions & 112 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,18 @@

Configurable K8S webhook that can implement multiple validators and mutators using a simple yaml config file.

For example, this file configures it to validate that no `serviceaccount` is uses the `kube-system` namespace. This validator can be accessed on `<hostname>:<port>/check-namespace-sa`.
For example, this is the config to validate that no `serviceaccount` uses the `kube-system` namespace. This validator can be accessed on `<hostname>:<port>/check-namespace-sa`.

```yaml
apiVersion: generic-webhook/v1alpha1
apiVersion: generic-webhook/v1beta1
kind: GenericWebhookConfig
webhooks:
- name: check-namespace-sa
path: /check-namespace-sa
actions:
# Refuse the request if it's a ServiceAccount that
# is placed on the "kube-system" namespace
- condition:
and:
- equal:
- getValue: .metadata.namespace
- const: kube-system
- equal:
- getValue: .kind
- const: ServiceAccount
- condition: .metadata.namespace == "kube-system" && .kind == "ServiceAccount"
accept: false
```
Expand All @@ -47,7 +40,7 @@ metadata:
[...]
data:
generic-webhook-config: |
apiVersion: generic-webhook/v1alpha1
apiVersion: generic-webhook/v1beta1
kind: GenericWebhookConfig
webhooks:
- ...
Expand All @@ -63,7 +56,7 @@ This file allows the user to configure several webhooks in a single app. In this
The [examples](./examples/) directory contains a fair amount of examples to help the user better understand how they can leverage the `GenericWebhookConfig` to write their own Validating or Mutating webhooks.

```yaml
apiVersion: generic-webhook/v1alpha1
apiVersion: generic-webhook/v1beta1
kind: GenericWebhookConfig
webhooks:
- # Configuration for the first webhook
Expand Down Expand Up @@ -114,116 +107,23 @@ The value of the `--config` argument is the path of the `GenericWebhookConfig` c

### Defining a condition

When defining a condition, we can use any of the following operators.

- [const](#const)
- [getValue](#getvalue)
- [and](#and)
- [or](#or)
- [not](#not)
- [equal](#equal)
- [sum](#sum)
- [forEach](#foreach)

#### const

Defines a constant value. It is normally used with the [equal](#equal) operator, in case we want to check that a field in the manifest equals to a value that we define (using the `const` operator).

```yaml
const: <constant value>
```

#### getValue

Retrieves a value defined in the manifest. The `<path>` is a `.` (dot) separated path that references a field in the manifest. For example, `.metadata.name`. If the `getValue` is used nested within the [forEach](#foreach) operator, by default it will take as a base context the item that the [forEach](#foreach) is iterating. If you want to use the root of the manifest as the context, then you must add a `$` at the beginning of the path. For example, `$.metadata.name` will always resolve to the metadata defined at the root level independently if the `getValue` is nested in a [forEach](#foreach) or not.

```yaml
# Refers to the latest context
getValue: .metadata.name
# Refers always to the root context
getValue: $.metadata.name
```

#### and

It performs an `and` operation on a list of elements. The list of elements can be explicitly defined (an actual yaml list) or implicitly defined. This last case is exemplified when the `and` consumes the result generated by the [forEach](#foreach) operator.
The conditions can be defined using structured operators and/or a simple pseudolanguage. For example, the following condition combines both a structured operator (an `and`) and a couple of lines of this pseudolanguage.

```yaml
and:
- <elem1>
- <elem2>
- ...
and:
forEach:
...
```

#### or

It performs an `or` operation on a list of elements. The list of elements can be explicitly defined (an actual yaml list) or implicitly defined. This last case is exemplified when the `or` consumes the result generated by the [forEach](#foreach) operator.

```yaml
or:
- <elem1>
- <elem2>
- ...
or:
forEach:
...
```

#### not

It negates the value of its argument.

```yaml
not:
<operator>
not:
and:
- ...
- .kind == "Pod"
- .metadata.labels.latencyCritical == true && .metadata.labels.app == "backend"
```

#### equal

Compares two elements and returns true if they are equal.

```yaml
equal:
- const: default
- getValue: .metadata.namespace
```

#### sum

Sums the values of a list of elements. The list of elements can be explicitly defined (an actual yaml list) or implicitly defined. This last case is exemplified when the `sum` consumes the result generated by the [forEach](#foreach) operator.
We can also iterate over lists and nested lists. In the following example, we check that a pod has at least one container called "main".

```yaml
sum:
- const: 1
- const: 4
sum:
forEach:
...
any: .spec.containers.* -> .name == "main"
```

#### forEach

It's like a `map` operation. If executes the operation `op` for each element defined in `elements`. It returns the transformed list of elements. The [getValue](#getvalue) operator that lives within a `forEach` receives as context the current element that the `forEach` is iterating. In this example, `.name` resolves to the name of a container.
The `*` is used to iterate over a list, in that case the list of containers. The `->` operator is like a map. So, assuming the pod has two containers, one named "main" and the other named "foo", the `.spec.containers.* -> .name == "main"` returns `[true, false]`.

```yaml
forEach:
elements: {getValue: .spec.containers}
op:
equal:
- const: my-side-car
- getValue: .name
```
You can check [operators-reference](./docs/operators-reference.md) to see all the available structured operators.

### Defining a patch

Expand Down
110 changes: 110 additions & 0 deletions docs/operators-reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
When defining a condition, we can use any of the following operators.

- [const](#const)
- [getValue](#getvalue)
- [and](#and)
- [or](#or)
- [not](#not)
- [equal](#equal)
- [sum](#sum)
- [forEach](#foreach)

#### const

Defines a constant value. It is normally used with the [equal](#equal) operator, in case we want to check that a field in the manifest equals to a value that we define (using the `const` operator).

```yaml
const: <constant value>
```
#### getValue
Retrieves a value defined in the manifest. The `<path>` is a `.` (dot) separated path that references a field in the manifest. For example, `.metadata.name`. If the `getValue` is used nested within the [forEach](#foreach) operator, by default it will take as a base context the item that the [forEach](#foreach) is iterating. If you want to use the root of the manifest as the context, then you must add a `$` at the beginning of the path. For example, `$.metadata.name` will always resolve to the metadata defined at the root level independently if the `getValue` is nested in a [forEach](#foreach) or not.

```yaml
# Refers to the latest context
getValue: .metadata.name
# Refers always to the root context
getValue: $.metadata.name
```

#### and

It performs an `and` operation on a list of elements. The list of elements can be explicitly defined (an actual yaml list) or implicitly defined. This last case is exemplified when the `and` consumes the result generated by the [forEach](#foreach) operator.

```yaml
and:
- <elem1>
- <elem2>
- ...
and:
forEach:
...
```

#### or

It performs an `or` operation on a list of elements. The list of elements can be explicitly defined (an actual yaml list) or implicitly defined. This last case is exemplified when the `or` consumes the result generated by the [forEach](#foreach) operator.

```yaml
or:
- <elem1>
- <elem2>
- ...
or:
forEach:
...
```

#### not

It negates the value of its argument.

```yaml
not:
<operator>
not:
and:
- ...
```

#### equal

Compares two elements and returns true if they are equal.

```yaml
equal:
- const: default
- getValue: .metadata.namespace
```

#### sum

Sums the values of a list of elements. The list of elements can be explicitly defined (an actual yaml list) or implicitly defined. This last case is exemplified when the `sum` consumes the result generated by the [forEach](#foreach) operator.

```yaml
sum:
- const: 1
- const: 4
sum:
forEach:
...
```

#### forEach

It's like a `map` operation. If executes the operation `op` for each element defined in `elements`. It returns the transformed list of elements. The [getValue](#getvalue) operator that lives within a `forEach` receives as context the current element that the `forEach` is iterating. In this example, `.name` resolves to the name of a container.

```yaml
forEach:
elements: {getValue: .spec.containers}
op:
equal:
- const: my-side-car
- getValue: .name
```
11 changes: 2 additions & 9 deletions examples/check-namespace-sa.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
apiVersion: generic-webhook/v1alpha1
apiVersion: generic-webhook/v1beta1
kind: GenericWebhookConfig
webhooks:
- name: check-namespace-sa
path: /check-namespace-sa
actions:
# Refuse the request if it's a ServiceAccount that
# is placed on the "kube-system" namespace
- condition:
and:
- equal:
- getValue: .metadata.namespace
- const: kube-system
- equal:
- getValue: .kind
- const: ServiceAccount
- condition: .metadata.namespace == "kube-system" && .kind == "ServiceAccount"
accept: false
40 changes: 5 additions & 35 deletions examples/inject-node-affinity.yaml
Original file line number Diff line number Diff line change
@@ -1,50 +1,20 @@
apiVersion: generic-webhook/v1alpha1
apiVersion: generic-webhook/v1beta1
kind: GenericWebhookConfig
webhooks:
- name: patch-node-affinity
path: /patch-node-affinity
actions:
# If the pod doesn't have any node affinity that involves the `myorg.io/instance-cost`
# label, then we inject to it a preferred node affinity for the nodes that have
# label, then we inject a preferred node affinity for the nodes that have
# the value `cheap` for the `myorg.io/instance-cost` label
- condition:
and:
# We are analysing a Pod
- equal:
- getValue: .kind
- const: Pod

- .kind == "Pod"
# {key: myorg.io/instance-cost} doesn't appear in preferredDuringSchedulingIgnoredDuringExecution
- and:
forEach:
elements:
getValue: .spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution
op:
and:
forEach:
elements:
getValue: .preference.matchExpressions
op:
not:
equal:
- getValue: .key
- const: myorg.io/instance-cost

- all: .spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution.*.preference.matchExpressions.* -> .key != "myorg.io/instance-cost"
# {key: myorg.io/instance-cost} doesn't appear in requiredDuringSchedulingIgnoredDuringExecution
- and:
forEach:
elements:
getValue: .spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms
op:
and:
forEach:
elements:
getValue: .matchExpressions
op:
not:
equal:
- getValue: .key
- const: myorg.io/instance-cost
- all: .spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.*.matchExpressions.* -> .key != "myorg.io/instance-cost"

patch:
# Inject a preferred node affinity for the nodes that have the label
Expand Down

0 comments on commit 8e87555

Please sign in to comment.