Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update docs to explain how the new pseudolanguage works #38

Merged
merged 1 commit into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading