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
.
apiVersion: generic-webhook/v1alpha1
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
accept: false
With this project, you can avoid writing from scratch simple K8S Validating or Mutating webhooks. The logic of the webhook can be written in a simple yaml configuration file. Moreover, this yaml config can be changed dynamically without the need of restarting the app.
Apart from that, it also allows you to maintain a single K8S deployment for all your webhooks instead of having to maintain a separate deployment for each webhook. This is possible because the GenericWebhookConfig
config file accepts multiple webhook configs, each listening to a different path.
You need (at least) the following resources:
- deployment
- service
- configmap
The configmap
should contain the GenericWebhookConfig
.
apiVersion: v1
kind: ConfigMap
metadata:
[...]
data:
generic-webhook-config: |
apiVersion: generic-webhook/v1alpha1
kind: GenericWebhookConfig
webhooks:
- ...
- ...
The pod template within the deployment
should mount the previous config map as a file. Finally, in the same pod template, we should pass --config <path-to-mounted-generic-webhook-config-file>
and --port <port>
as args
for the container.
This file allows the user to configure several webhooks in a single app. In this section, we'll see the structure and syntax that it follows.
The 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.
apiVersion: generic-webhook/v1alpha1
kind: GenericWebhookConfig
webhooks:
- # Configuration for the first webhook
- # Configuration for the second webhook
...
The structure of the configuration of a webhook (an entry in the webhooks
list) is the following:
# Name used to identify this webhook
name: <name>
# Path where this webhook will listen (<hostname>:<port>/<path>)
path: <path>
# The actions (accept and/or patch) this webhook will perform
actions:
- # The condition that must be met to execute this action. The condition
# is evaluated based on the K8S manifest that the K8S control plane sends
# to this app.
# For example, "the manifest defines a pod and this pod defines request.cpu"
condition: {}
# [ACTION] Whether to accept or not the manifest sent by the K8S control plane
# If not specified, it defaults to true
accept: true | false
# [ACTION] The patch to apply to the manifest. If set, then the webhook behaves
# as a Mutating webhook
patch: []
- ...
The syntax of the condition
can be found in Defining a condition. The syntax of the patch can be found in Defining a patch.
It can be frustrating to deploy a change in the configmap
that contains the GenericWebhookConfig
just to see that it's not working as expected. For this reason, it's advisable to test new configurations in advance. That's why this app can be invoked as a cli tool.
Assuming you've cloned the repo and you have poetry installed.
poetry run python3 generic_k8s_webhook/main.py --config <path-to-GenericWebhookConfig-file> cli --k8s-manifest <k8s-manifest-to-analise> --wh-name <webhook-to-use>
The value of the --config
argument is the path of the GenericWebhookConfig
config file. The value of the --k8s-manifest
argument is a K8S manifest file that will be processed by the webhook. The value of the --wh-name
is just the name of the webhook that we'll use to process the manifest. Remember that we can have several webhooks in the same app.
When defining a condition, we can use any of the following operators.
Defines a constant value. It is normally used with the 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).
const: <constant value>
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 operator, by default it will take as a base context the item that the 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 or not.
# Refers to the latest context
getValue: .metadata.name
# Refers always to the root context
getValue: $.metadata.name
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 operator.
and:
- <elem1>
- <elem2>
- ...
and:
forEach:
...
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 operator.
or:
- <elem1>
- <elem2>
- ...
or:
forEach:
...
It negates the value of its argument.
not:
<operator>
not:
and:
- ...
Compares two elements and returns true if they are equal.
equal:
- const: default
- getValue: .metadata.namespace
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 operator.
sum:
- const: 1
- const: 4
sum:
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 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.
forEach:
elements: {getValue: .spec.containers}
op:
equal:
- const: my-side-car
- getValue: .name
The patch is defined almost in the same as a standard jsonpatch. The only difference is that, when defining a path, instead of using /
to separate its components, we use .
.
patch:
- op: add
path: .metadata.labels
value: <any value>
See the contributor guide