JMESPath
JMESPath (pronounced “James path”) is a JSON query language created by James Saryerwinnie and is the language that Kyverno supports to perform more complex selections of fields and values and also manipulation thereof by using one or more filters. If you’re familiar with kubectl
and Kubernetes already, this might ring a bell in that it’s similar to JSONPath. JMESPath can be used almost anywhere in Kyverno although is an optional component depending on the type and complexity of a Kyverno policy or rule that is being written. While many policies can be written with simple overlay patterns, others require more detailed selection and transformation. The latter is where JMESPath is useful.
While the complete specifications of JMESPath can be read on the official site’s specifications page, much of the specifics may not apply to Kubernetes use cases and further can be rather thick reading. This page serves as an easier guide and tutorial on how to learn and harness JMESPath for Kubernetes resources for use in crafting Kyverno policies. It should not be a replacement for the official JMESPath documentation but simply a use case specific guide to augment the already comprehensive literature.
Getting Set Up
In order to position yourself for success with JMESPath expressions inside Kyverno policies, a few tools are recommended.
kubectl
, the Kubernetes CLI here. While havingkubectl
is a given, it comes in handy especially when building a JMESPath expression around performing API lookups.kyverno
, the Kyverno CLI here or via krew. Kyverno acts as a webhook (when run in-cluster) but also as a standalone CLI when run outside giving you the ability to test policies and, more recently, to test custom JMESPath filters which are endemic to only Kyverno. With thejp
subcommand, it contains the functionality present in the upstreamjp
CLI tool and also newer capabilities. It effectively allows you to test out JMESPath expressions live in a command line interface by passing in a JSON document and seeing the results without having to repeatedly test Kyverno policies.yq
, the YAML processor here.yq
allows reading from a Kubernetes manifest and converting to JSON, which is helpful in order to be piped tojp
in order to test expressions. The Kyverno CLIjp
subcommand also accepts YAML files in addition to JSON.jq
, the JSON processor here.jq
is an extremely popular tool for working with JSON documents and has its own filter ability, but it’s also useful in order to format JSON on the terminal for better visuals.
Basics
JMESPath is used when you need fine-grained selection of a document and need to perform some type of query logic against the result. For example, if in a given field you need to refer to the value of another field either in the same resource or in a different one, you’ll need to use JMESPath. This sample policy performs a simple mutation on a Pod to add a new label named appns
and set the value of it based on the value of the Namespace in which that Pod is created.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: add-labels
5spec:
6 rules:
7 - name: add-labels
8 match:
9 any:
10 - resources:
11 kinds:
12 - Pod
13 mutate:
14 patchStrategicMerge:
15 metadata:
16 labels:
17 appns: "{{request.namespace}}"
JMESPath expressions in most places in Kyverno must be enclosed in double curly braces like {{request.namespace}}
. If an expression is used as the value of a field and contains nothing else, the expression needs to be wrapped in quotes: appns: "{{request.namespace}}"
. If the value field contains other text outside of the expression, then it can be unquoted and treated as a string but this isn’t strictly required: message: The namespace name is {{request.namespace}}
.
When building a JMESPath expression, a dot (.
) character is called a “sub-expression” and used to descend into nested structures. In the {{request.namespace}}
example, this expression is looking for the top-most object the key of which is called request
and then looking for a child object the key of which is called namespace
. Whatever the value of the namespace
key is will be inserted where the expression is written. Given the below AdmissionReview snippet, which will be explained in a moment, the value that would result from the {{request.namespace}}
expression is foo
.
1{
2 "apiVersion": "admission.k8s.io/v1",
3 "kind": "AdmissionReview",
4 "request": {
5 "namespace": "foo"
6 }
7}
When submitting a Pod which matches the policy above, the result which gets created after Kyverno has mutated it would then look something like this.
Incoming Pod
1apiVersion: v1
2kind: Pod
3metadata:
4 name: mypod
5spec:
6 containers:
7 - name: busybox
8 image: busybox
Outgoing Pod
1apiVersion: v1
2kind: Pod
3metadata:
4 name: mypod
5 labels:
6 appns: foo
7spec:
8 containers:
9 - name: busybox
10 image: busybox
Notice in the highlighted lines that the new label appns
has been added to the Pod and the value set equal to the expression {{request.namespace}}
which, in this instance, happened to be foo
because it was created in the foo
Namespace. Should this Pod be created in another Namespace called bar
, the label would be appns: bar
.
Remember
JMESPath, like JSONPath, is a query language for JSON and, as such, it only works when fed with a JSON-encoded document. Although most work with Kubernetes resources using YAML, the API server will convert this to JSON internally and use that format when storing and sending objects to webhooks like Kyverno. This is why the programyq
will be invaluable when building the correct expression based upon Kubernetes manifests written in YAML.AdmissionReview
Kyverno is an example, although there are many others, of an admission controller. As the name implies, these are pieces of software which have some stake in whether a given resource is admitted into the cluster or not. They may be either validating, mutating, or both. The latter applies to Kyverno as it has both capabilities. For a graphical representation of the order in which these requests make their way into Kyverno, see the introduction page.
Note
As the name “admission” implies, this process only takes place when an object does not already exist. Pre-existing objects have already been admitted successfully in the past and therefore do not apply here. Certain other operations on pre-existing objects, however, are subject to the admissions process including examples like executing (exec
) commands inside Pods and deleting objects but, importantly, not when reading back objects. The act of reading, getting, or listing resources does not result in an admission review process.When a resource that matches the criteria of a selection statement gets sent to the Kubernetes API server, after the API server performs some basic modifications to it, it then gets sent to webhooks which have told the API server via a MutatingWebhookConfiguration or ValidatingWebhookConfiguration resource–which Kyverno creates for you based upon the policies you write–that it wishes to be informed. The API server will “wrap” the matching resource in another resource called an AdmissionReview which contains a number of other descriptive data about that request, for example what type or request this is (like a creation or deletion), the user who submitted the request, and, most importantly, the contents of the resource itself. Given the simple Pod example above that a user wishes to be created in the foo
Namespace, the AdmissionReview request that hits Kyverno might look like following.
1{
2 "kind": "AdmissionReview",
3 "apiVersion": "admission.k8s.io/v1",
4 "request": {
5 "uid": "3d4fc6c1-7906-47d9-b7da-fc2b22353643",
6 "kind": {
7 "group": "",
8 "version": "v1",
9 "kind": "Pod"
10 },
11 "resource": {
12 "group": "",
13 "version": "v1",
14 "resource": "pods"
15 },
16 "requestKind": {
17 "group": "",
18 "version": "v1",
19 "kind": "Pod"
20 },
21 "requestResource": {
22 "group": "",
23 "version": "v1",
24 "resource": "pods"
25 },
26 "name": "mypod",
27 "namespace": "foo",
28 "operation": "CREATE",
29 "userInfo": {
30 "username": "thomas",
31 "uid": "404d34c4-47ff-4d40-b25b-4ec4197cdf63"
32 },
33 "object": {
34 "kind": "Pod",
35 "apiVersion": "v1",
36 "metadata": {
37 "name": "mypod",
38 "creationTimestamp": null
39 },
40 "spec": {
41 "containers": [
42 {
43 "name": "busybox",
44 "image": "busybox",
45 "resources": {}
46 }
47 ]
48 },
49 "status": {}
50 },
51 "oldObject": null,
52 "dryRun": false,
53 "options": {
54 "kind": "CreateOptions",
55 "apiVersion": "meta.k8s.io/v1"
56 }
57 }
58}
As can be seen, the full Pod is represented along with other metadata surrounding its creation.
These AdmissionReview resources serve as the most common source of data when building JMESPath expressions, specifically request.object
. For the other data properties which can be consumed via an AdmissionReview resource, refer back to the variables page.
Formatting
Because there are various types of values in differing fields, there are differing ways values must be supplied to JMESPath expressions as inputs in order to generate not only a valid expression but produce the output desired. Specifying values in the correct format is key to this success. Values which are supported but need to be differentiated in formatting are numbers (i.e., an integer like 6
or a floating point like 6.7
), a quantity (i.e., a number with a unit of measure like 6Mi
), a duration (i.e., a number with a unit of time like 6h
), a semver (i.e., a version number like 1.2.3
), and others. Because Kyverno (and therefore most custom JMESPath filters built for Kyverno) is designed for Kubernetes, it is Kubernetes aware. Therefore, specifying `6` as an input to a filter is not the same as specifying '6' where the former is interpreted as “the number six” and latter as “six bytes”. The types which map to the possible values are either JSON or string. In JMESPath, these are literal expression and raw string literals. Use the table below to find how to format the type of value which should be supplied.
Value Type | Input Type | JMESPath Type | Formatting |
---|---|---|---|
Number | Integer | Literal | backticks |
Quantity | String | Raw | quotes |
Duration | String | Raw | quotes |
Labels (map) | Object | Literal | backticks |
Paths in a JMESPath expression may also need escaping or literal quoting depending on the contents. For example, in a ResourceQuota the following schema elements may be present:
1spec:
2 hard:
3 limits.memory: 3750Mi
4 requests.cpu: "5"
To represent the limits.memory
field in a JMESPath expression requires literal quoting of the key in order to avoid being interpreted as child nodes limits
and memory
. The expression would then be {{ spec.hard.\"limits.memory\" }}
. A similar approach is needed when individual keys contain special characters, for example a dash (-
). Quoting and then escaping is similarly needed there, ex., {{ images.containers.\"my-container\".tag }}
.
Quoting of an overall JMESPath expression can also impact how it is evaluated. For fields which only contain a JMESPath expression (ex., key: "{{ request.object.spec.template.spec.containers[].image | contains(@, 'nginx') }}"
) it is important to use double quotes on the outer expression (as shown) and single quotes for input fields of type string. Even if no JMESPath filters are used, any expression should be wrapped in double quotes to avoid unintended evaluation.
Useful Patterns
When developing policies for Kubernetes resources, there are several patterns which are common where JMESPath can be useful. This section attempts to capture example patterns that have been observed through real world use cases and how to write JMESPath for them.
Flattening Arrays
In many Kubernetes resources, arrays of both objects and strings are very common. For example, in Pod resources, spec.containers[]
is an array of objects where each object in the array may optionally specify args[]
which is an array of strings. Policy very often must be able to peer into these arrays and match a given pattern with enough flexibility to implement a sufficiently advanced level of control. The JMESPath flatten operator can help to simplify these checks so writing Kyverno policy becomes less verbose and require fewer rules.
Pods may contain multiple containers and in different locations in the Pod spec, for example ephemeralContainers[]
, initContainers[]
, and containers[]
. Regardless of where the container occurs, a container is still a container. And although it’s possible to name each location in the spec individually, this produces rule or expression sprawl. It is often more efficient to collect all the containers together in a single query for processing. Consider the example Pod below.
1apiVersion: v1
2kind: Pod
3metadata:
4 name: mypod
5spec:
6 initContainers:
7 - name: redis
8 image: redis
9 containers:
10 - name: busybox
11 image: busybox
12 - name: nginx
13 image: nginx
Assume this Pod is saved as pod.yaml
locally, its containers[]
may be queried using a simple JMESPath expression with help from the Kyverno CLI.
1$ kyverno jp query -i pod.yaml "spec.containers[]"
2[
3 {
4 "image": "busybox",
5 "name": "busybox"
6 },
7 {
8 "image": "nginx",
9 "name": "nginx"
10 }
11]
The above output shows the return of an array of objects as expected where each object is the container. But by using a multi-select list, the initContainer[]
array may also be parsed.
1$ kyverno jp query -i pod.yaml "spec.[initContainers, containers]"
2[
3 [
4 {
5 "image": "redis",
6 "name": "redis"
7 }
8 ],
9 [
10 {
11 "image": "busybox",
12 "name": "busybox"
13 },
14 {
15 "image": "nginx",
16 "name": "nginx"
17 }
18 ]
19]
In the above, a multi-select list spec.[initContainers, containers]
“wraps” the results of both initContainers[]
and containers[]
in parent array thereby producing an array consisting of multiple arrays. By using the flatten operator, these results can be collapsed into just a single array.
1$ kyverno jp query -i pod.yaml "spec.[initContainers, containers][]"
2[
3 {
4 "image": "redis",
5 "name": "redis"
6 },
7 {
8 "image": "busybox",
9 "name": "busybox"
10 },
11 {
12 "image": "nginx",
13 "name": "nginx"
14 }
15]
With just a single array in which all containers, regardless of where they are, occur in a single hierarchy, it becomes easier to process the data for relevant fields and take action. For example, if you wished to write a policy which forbid using the image named busybox
in a Pod, by flattening all containers it becomes easier to isolate just the image
field. Because it does not matter where busybox
may be found, if found the entire Pod must be rejected. Therefore, while loops or other methods may work, a more efficient method is to simply gather all containers across the Pod and flatten them.
1$ kyverno jp query -i pod.yaml "spec.[initContainers, containers][].image"
2[
3 "redis",
4 "busybox",
5 "nginx"
6]
With all of the images stored in a simple array, the values can be parsed much easier and just one expression written to contain the necessary logic.
1deny:
2 conditions:
3 any:
4 - key: busybox
5 operator: AnyIn
6 value: "{{request.object.spec.[initContainers, containers][].image}}"
Non-Existence Checks
It is common for a JMESPath expression to name a specific field so that its value may be acted upon. For example, in the basics section above, the label appns
is written to a Pod via a mutate rule which does not contain it or is set to a different value. A Kyverno validate rule which exists to check the value of that label or any other field is commonplace. Because the schema for many Kubernetes resources is flexible in that many fields are optional, policy rules must contend with the scenario in which a matching resource does not contain the field being checked. When using JMESPath to check the value of such a field, a simple expression might be written {{request.object.metadata.labels.appns}}
. If a resource is submitted which either does not contain any labels at all or does not contain a label with the specified key then the expression cannot be evaluated. An error is likely to result similar to JMESPath query failed: Unknown key "labels" in path
. In these types of cases, the JMESPath expression should use a non-existence check in the form of the OR expression followed by a “default” value if the field does not exist. The resulting full expression which will correctly evaluate is {{request.object.metadata.labels.appns || ''}}
. This expression reads, “take the value of the key request.object.metadata.labels.appns or, if it does not exist, set it to an empty string”. Note that the value on the right side may need to be customized given the ultimate use of the value expected to be produced. This non-existence pattern can be used in almost any JMESPath expression to mitigate scenarios in which the initial query may be invalid.
Matching Special Characters
Kyverno reserves special behavior for wildcard characters such as *
and ?
. However, certain Kubernetes resources permit wildcards as values in various fields which are treated literally. It may be necessary to construct a policy which validates literal usage of such wildcards. Using the JMESPath contains()
filter it is possible to do so. The below policy shows how to use contains()
to match on wildcards as literal characters.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: restrict-ingress-wildcard
5spec:
6 validationFailureAction: Enforce
7 rules:
8 - name: block-ingress-wildcard
9 match:
10 any:
11 - resources:
12 kinds:
13 - Ingress
14 validate:
15 message: "Wildcards are not permitted as hosts."
16 foreach:
17 - list: "request.object.spec.rules"
18 deny:
19 conditions:
20 any:
21 - key: "{{ contains(element.host, '*') }}"
22 operator: Equals
23 value: true
Custom Filters
In addition to the filters available in the upstream JMESPath library which Kyverno uses, there are also many new and custom filters developed for Kyverno’s use found nowhere else. These filters augment the already robust capabilities of JMESPath to bring new functionality and capabilities which help solve common use cases in running Kubernetes. The filters endemic to Kyverno can be used in addition to any of those found in the upstream JMESPath library used by Kyverno and do not represent replaced or removed functionality.
For instructions on how to test these filters in a standalone method (i.e., outside of Kyverno policy), see the documentation on the kyverno jp
subcommand.
Information on each subcommand, its inputs and output, and specific usage instructions can be found below along with helpful and common use cases that have been identified.
Add
Expand
The add()
filter very simply adds two values and produces a sum. The official JMESPath library does not include most basic arithmetic operators such as add, subtract, multiply, and divide, the exception being sum()
as documented here. While sum()
is useful in that it accepts an array of integers as an input, add()
is useful as a simplified filter when only two individual values need to be summed. Note that add()
here is different from the length()
filter which is used to obtain a count of a certain number of items. Use add()
instead when you have values of two fields you wish to add together.
add()
is also value-aware (based on the formatting used for the inputs) and is capable of adding numbers, quantities, and durations without any form of unit conversion.
Arithmetic filters like add()
currently accept inputs in the following formats.
- Number (ex., `10`)
- Quantity (ex., ‘10Mi’)
- Duration (ex., ‘10h’)
Note that how the inputs are enclosed determines how Kyverno interprets their type. Numbers enclosed in back ticks are scalar values while quantities and durations are enclosed in single quotes thus treating them as strings. Using the correct enclosing character is important because, in Kubernetes “regular” numbers are treated implicitly as units of measure. The number written `10` is interpreted as an integer or “the number ten” whereas ‘10’ is interpreted as a string or “ten bytes”. See the Formatting section above for more details.
Input 1 | Input 2 | Output |
---|---|---|
Number | Number | Number |
Quantity or Number | Quantity or Number | Quantity |
Duration or Number | Duration or Number | Duration |
Some specific behaviors to note:
- If a duration (‘1h’) and a number (`5`) are the inputs, the number will be interpreted as seconds resulting in a sum of
1h0m5s
. - Because of durations being a string just like resource quantities, and the minutes unit of “m” also present in quantities interpreted as the “milli” prefix, there is no support for minutes.
Example: This policy denies a Pod if any of its containers specify memory requests and limits in excess of 200Mi.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: add-demo
5spec:
6 validationFailureAction: Enforce
7 background: false
8 rules:
9 - name: add-demo
10 match:
11 any:
12 - resources:
13 kinds:
14 - Pod
15 operations:
16 - CREATE
17 - UPDATE
18 validate:
19 message: "The total memory defined in requests and limits must not exceed 200Mi."
20 foreach:
21 - list: "request.object.spec.containers"
22 deny:
23 conditions:
24 any:
25 - key: "{{ add('{{ element.resources.requests.memory || `0` }}', '{{ element.resources.limits.memory || `0` }}') }}"
26 operator: GreaterThan
27 value: 200Mi
Base64_decode
Expand
The base64_decode()
filter takes in a base64-encoded string and produces the decoded output similar to the tool and command base64 --decode
. This can be useful when working with Kubernetes Secrets and deciphering their values in order to take action on them in a policy.
Input 1 | Output |
---|---|
String | String |
Some specific behaviors to note:
- Base64-encoded strings with newline characters will be printed back with them inline.
Example: This policy checks every container, initContainer, and ephemeralContainer in a Pod and decodes a Secret having the path data.license
to ensure it does not refer to a prohibited license key.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: base64-decode-demo
5spec:
6 background: false
7 validationFailureAction: Enforce
8 rules:
9 - name: base64-decode-demo
10 match:
11 any:
12 - resources:
13 kinds:
14 - Pod
15 preconditions:
16 all:
17 - key: "{{ request.object.spec.[containers, initContainers, ephemeralContainers][].env[].valueFrom.secretKeyRef || '' | length(@) }}"
18 operator: GreaterThanOrEquals
19 value: 1
20 - key: "{{request.operation}}"
21 operator: NotEquals
22 value: DELETE
23 validate:
24 message: This license key may not be consumed by a Secret.
25 foreach:
26 - list: "request.object.spec.[containers, initContainers, ephemeralContainers][].env[].valueFrom.secretKeyRef"
27 context:
28 - name: status
29 apiCall:
30 jmesPath: "data.license"
31 urlPath: "/api/v1/namespaces/{{request.namespace}}/secrets/{{element.name}}"
32 deny:
33 conditions:
34 any:
35 - key: "{{ status | base64_decode(@) }}"
36 operator: Equals
37 value: W0247-4RXD3-6TW0F-0FD63-64EFD-38180
Base64_encode
Expand
The base64_encode()
filter is the inverse of the base64_decode()
filter and takes in a regular, plaintext and unencoded string and produces a base64-encoded output similar to the tool and command base64
. This can be useful when working with Kubernetes Secrets by encoding data into the base64 format which is the only acceptable format for Kubernetes Secrets.
Input 1 | Output |
---|---|
String | String |
Example: This policy generates a Secret when a new Namespace is created the contents of which is the value of an annotation named corpkey
.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: base64-encode-demo
5spec:
6 rules:
7 - name: gen-supkey
8 match:
9 any:
10 - resources:
11 kinds:
12 - Namespace
13 generate:
14 apiVersion: v1
15 kind: Secret
16 name: sup-key
17 namespace: "{{request.object.metadata.name}}"
18 synchronize: false
19 data:
20 data:
21 token: "{{ base64_encode('{{ request.object.metadata.annotations.corpkey }}') }}"
Compare
Expand
The compare()
filter is provided as an analog to the inbuilt function to Golang of the same name. It compares two strings lexicographically where the first string is compared against the second. If both strings are equal, the result is 0
(ex., “a” compared to “a”). If the first is in lower lexical order than the second, the result is -1
(ex., “a” compared to “b”). And if the first is in higher order than the second, the result is 1
(ex., “b” compared to “a”). Kyverno also has built-in operators for string comparison where Equals
is usually the most common, and in most use cases it is more practical to use the Equals
operator in expressions such as preconditions and deny.conditions
blocks.
Input 1 | Input 2 | Output |
---|---|---|
String | String | Number |
Example: This policy will write a new label called dictionary
into a Service putting into order the values of two annotations if the order of the first comes before the second.
1apiVersion : kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: compare-demo
5spec:
6 background: false
7 rules:
8 - name: write-dictionary
9 match:
10 any:
11 - resources:
12 kinds:
13 - Service
14 preconditions:
15 any:
16 - key: "{{ compare('{{request.object.metadata.annotations.foo}}', '{{request.object.metadata.annotations.bar}}') }}"
17 operator: LessThan
18 value: 0
19 mutate:
20 patchStrategicMerge:
21 metadata:
22 labels:
23 dictionary: "{{request.object.metadata.annotations.foo}}-{{request.object.metadata.annotations.bar}}"
Divide
Expand
The divide()
filter performs arithmetic divide capabilities between two input fields and produces an output quotient. Like other arithmetic custom filters, it is input aware based on the type passed and, for quantities, allows auto conversion between units of measure. For example, dividing 10Mi (ten mebibytes) by 5Ki (five kibibytes) results in the value 5120 as units are first normalized and then canceled through division. The divide()
filter is currently under development to better account for all permutations of input types, however the below table captures the most common and practical use cases.
Arithmetic filters like divide()
currently accept inputs in the following formats.
- Number (ex., `10`)
- Quantity (ex., ‘10Mi’)
- Duration (ex., ‘10h’)
Note that how the inputs are enclosed determines how Kyverno interprets their type. Numbers enclosed in back ticks are scalar values while quantities and durations are enclosed in single quotes thus treating them as strings. Using the correct enclosing character is important because, in Kubernetes “regular” numbers are treated implicitly as units of measure. The number written `10` is interpreted as an integer or “the number ten” whereas ‘10’ is interpreted as a string or “ten bytes”. See the Formatting section above for more details.
Input 1 | Input 2 | Output |
---|---|---|
Number | Number | Number |
Quantity | Number | Quantity |
Quantity | Quantity | Number |
Duration | Number | Duration |
Duration | Duration | Number |
Example: This policy will check every container in a Pod and ensure that memory limits are no more than 2.5x its requests.
1apiVersion : kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: enforce-resources-as-ratio
5spec:
6 validationFailureAction: Audit
7 rules:
8 - name: check-memory-requests-limits
9 match:
10 any:
11 - resources:
12 kinds:
13 - Pod
14 operations:
15 - CREATE
16 - UPDATE
17 validate:
18 message: Limits may not exceed 2.5x the requests.
19 foreach:
20 - list: "request.object.spec.containers"
21 deny:
22 conditions:
23 any:
24 # Set resources.limits.memory equal to zero if not present and resources.requests.memory equal to 1m rather than zero
25 # to avoid undefined division error. No memory request in this case is basically the same as 1m. Kubernetes API server
26 # will automatically set requests=limits if only limits is defined.
27 - key: "{{ divide('{{ element.resources.limits.memory || '0' }}', '{{ element.resources.requests.memory || '1m' }}') }}"
28 operator: GreaterThan
29 value: 2.5
Equal_fold
Expand
The equal_fold()
filter is designed to provide text case folding for two sets of strings as inputs. Case folding allows comparing two strings for equivalency where the only differences are letter cases. The return is a boolean (either true
or false
). For example, comparing “pizza” to “Pizza” results in true
because other than title case on “Pizza” the strings are equivalent. Likewise with “pizza” and “pIzZa”. Comparing “pizza” to “APPLE” results in false
because even once normalized to the same case, the strings are different.
Input 1 | Input 2 | Output |
---|---|---|
String | String | Boolean |
Related filters to equal_fold()
are to_upper()
and to_lower()
which can also be used to normalize text for comparison.
Example: This policy will validate that a ConfigMap with a label named dept
and the value of a key under data
by the same name have the same case-insensitive value.
1apiVersion : kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: equal-fold-demo
5spec:
6 validationFailureAction: Enforce
7 background: false
8 rules:
9 - name: validate-dept-label-data
10 match:
11 any:
12 - resources:
13 kinds:
14 - ConfigMap
15 validate:
16 message: The dept label must equal the data.dept value aside from case.
17 deny:
18 conditions:
19 any:
20 - key: "{{ equal_fold('{{request.object.metadata.labels.dept}}', '{{request.object.data.dept}}') }}"
21 operator: NotEquals
22 value: true
Image_normalize
Expand
The image_normalize()
filter is used to output the canonical image string as it is known internally to Kyverno. This filter will render the internal values for the default image registry and tag (latest
) if they apply to a given image. It is useful particularly in mutate rules where the full image value is needed to decide whether to mutate it. This filter cannot be tested with the Kyverno CLI because the runtime context is only available in webhook mode.
For example, assuming the value of the default registry is left at its defaults of docker.io
and a Pod is sent to Kyverno which has a single container the value of its image
field being only nginx
, when run through the image_normalize()
filter will come out docker.io/nginx:latest
.
Input 1 | Output |
---|---|
String | String |
Example: This policy will mutate the value of the image
field in a Pod’s containers[]
array if, after normalization, it begins with docker.io
. The registry will be replaced with harbor.corp.org
.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: image-normalize-demo
5spec:
6 rules:
7 - name: replace-image-registry-pod-containers
8 match:
9 any:
10 - resources:
11 kinds:
12 - Pod
13 mutate:
14 foreach:
15 - list: "request.object.spec.containers"
16 patchStrategicMerge:
17 spec:
18 containers:
19 - name: "{{ element.name }}"
20 image: "{{ regex_replace_all('^docker.io/(.*)$', image_normalize('{{element.image}}'), 'harbor.corp.org/$1' )}}"
Items
Expand
The items()
filter iterates on map keys (ex., annotations or labels) or arrays and converts them to an array of objects with key/value attributes with custom names.
For example, given the following map below
1{
2 "team": "apple",
3 "organization": "banana"
4}
the items()
filter can transform this into an array of objects which assigns a key and value of arbitrary name to each of the entries in the map.
1$ echo '{"team" : "apple" , "organization" : "banana" }' | kyverno jp query "items(@, 'key', 'value')"
2[
3 {
4 "key": "organization",
5 "value": "banana"
6 },
7 {
8 "key": "team",
9 "value": "apple"
10 }
11]
It can also work on an input source given as an array of objects.
1$ echo '[{"team" : "apple"} , {"organization" : "banana"}]' | kyverno jp query "items(@, 'key', 'value')"
2Reading from terminal input.
3Enter input object and hit Ctrl+D.
4# items(@, 'key', 'value')
5[
6 {
7 "key": 0,
8 "value": {
9 "team": "apple"
10 }
11 },
12 {
13 "key": 1,
14 "value": {
15 "organization": "banana"
16 }
17 }
18]
Input 1 | Input 2 | Input 3 | Output |
---|---|---|---|
Map (Object) or Array | String | String | Array/Object |
Related filter to items()
is its inverse, object_from_list()
.
Example: This policy will take the labels on a Namespace foobar
where a Bucket is deployed and add them as key/value elements to the spec.forProvider.tagging.tagSet[]
array.
1apiVersion : kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: test-policy
5spec:
6 background: false
7 rules:
8 - name: test-rule
9 match:
10 any:
11 - resources:
12 kinds:
13 - Bucket
14 context:
15 - name: nslabels
16 apiCall:
17 urlPath: /api/v1/namespaces/foobar
18 jmesPath: items(metadata.labels,'key','value')
19 mutate:
20 foreach:
21 - list: "nslabels"
22 patchesJson6902: |-
23 - path: "/spec/forProvider/tagging/tagSet/-1"
24 op: add
25 value:
26 key: "{{element.key}}"
27 value": "{{element.value}}"
Given a Namespace which looks like the following
1apiVersion: v1
2kind: Namespace
3metadata:
4 name: foobar
5 labels:
6 team: apple
7 organization: banana
and a Bucket which looks like the below
1apiVersion: s3.aws.crossplane.io/v1beta1
2kind: Bucket
3metadata:
4 name: lambda-bucket
5spec:
6 forProvider:
7 acl: private
8 locationConstraint: eu-central-1
9 accelerateConfiguration:
10 status: Enabled
11 versioningConfiguration:
12 status: Enabled
13 notificationConfiguration:
14 lambdaFunctionConfigurations:
15 - events: ["s3:ObjectCreated:*"]
16 lambdaFunctionArn: arn:aws:lambda:eu-central-1:255932642927:function:lambda
17 paymentConfiguration:
18 payer: BucketOwner
19 tagging:
20 tagSet:
21 - key: s3-bucket
22 value: lambda-bucket
23 objectLockEnabledForBucket: false
24 providerConfigRef:
25 name: default
the final spec.forProvider.tagging.tagSet[]
will appear as below. Note that as of Kubernetes 1.21, the immutable label with key kubernetes.io/metadata.name
and value equal to that of the Namespace name is automatically added to all Namespaces, hence the discrepancy when comparing Namespace with Bucket resource manifests above.
1$ kubectl get bucket lambda-bucket -o json | kyverno jp query "spec.forProvider.tagging.tagSet[]"
2[
3 {
4 "key": "s3-bucket",
5 "value": "lambda-bucket"
6 },
7 {
8 "key": "kubernetes.io/metadata.name",
9 "value": "foobar"
10 },
11 {
12 "key": "organization",
13 "value": "banana"
14 },
15 {
16 "key": "team",
17 "value": "apple"
18 }
19]
Label_match
Expand
The label_match()
filter compares two sets of Kubernetes labels (both key and value) and outputs a boolean response if they are equivalent. This custom filter is useful in that it functions similarly to how the Kubernetes API server associates one resource with another through label selectors. There may be one or multiple labels in each set. Labels may occur in any order. A response of true
indicates all the labels (key and value) in the first input are accounted for in the second input. The second input, to which the first is compared, may have additional labels but it must have at minimum all those listed in the first input.
For example, the first collection compared to the second below results in true
despite the ordering.
1{
2 "dog": "lab",
3 "color": "tan"
4}
1{
2 "color": "tan",
3 "dog": "lab"
4}
Likewise, these two below collections also result in true
when compared because the entirety of the first is found within the second.
1{
2 "dog": "lab",
3 "color": "tan"
4}
1{
2 "color": "tan",
3 "weight":"chonky",
4 "dog": "lab"
5}
These last two collections when compared are false
because one of the values of one of the labels does not match what is in the first input.
1{
2 "dog": "lab",
3 "color": "tan"
4}
1{
2 "color": "black",
3 "dog": "lab"
4}
Input 1 | Input 2 | Output |
---|---|---|
Map (Object) | Map (Object) | Boolean |
Example: This policy checks all incoming Deployments to ensure they have a matching, preexisting PodDisruptionBudget in the same Namespace. The label_match()
filter is used in a query to count how many PDBs have a label set matching that of the incoming Deployment.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: require-pdb
5spec:
6 validationFailureAction: Audit
7 background: false
8 rules:
9 - name: require-pdb
10 match:
11 any:
12 - resources:
13 kinds:
14 - Deployment
15 operations:
16 - CREATE
17 context:
18 - name: pdb_count
19 apiCall:
20 urlPath: "/apis/policy/v1beta1/namespaces/{{request.namespace}}/poddisruptionbudgets"
21 jmesPath: "items[?label_match(spec.selector.matchLabels, `{{request.object.spec.template.metadata.labels}}`)] | length(@)"
22 validate:
23 message: "There is no corresponding PodDisruptionBudget found for this Deployment."
24 deny:
25 conditions:
26 any:
27 - key: "{{pdb_count}}"
28 operator: LessThan
29 value: 1
Modulo
Expand
The modulo()
filter returns the modulo or remainder between a division of two numbers. For example, the modulo of a division between 10
and 3
would be 1
since 3
can be divided into 10
only 3
times (equaling 9
) while producing 1
as a remainder.
Arithmetic filters like modulo()
currently accept inputs in the following formats.
- Number (ex., `10`)
- Quantity (ex., ‘10Mi’)
- Duration (ex., ‘10h’)
Note that how the inputs are enclosed determines how Kyverno interprets their type. Numbers enclosed in back ticks are scalar values while quantities and durations are enclosed in single quotes thus treating them as strings. Using the correct enclosing character is important because, in Kubernetes “regular” numbers are treated implicitly as units of measure. The number written `10` is interpreted as an integer or “the number ten” whereas ‘10’ is interpreted as a string or “ten bytes”. See the Formatting section above for more details.
Input 1 | Input 2 | Output |
---|---|---|
Number | Number | Number |
The inputs list is currently under construction.
Example: This policy checks every container and ensures that memory limits are evenly divisible by its requests.
1apiVersion : kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: modulo-demo
5spec:
6 validationFailureAction: Audit
7 rules:
8 - name: check-memory-requests-limits
9 match:
10 any:
11 - resources:
12 kinds:
13 - Pod
14 operations:
15 - CREATE
16 - UPDATE
17 validate:
18 message: Limits must be evenly divisible by the requests.
19 foreach:
20 - list: "request.object.spec.containers"
21 deny:
22 conditions:
23 any:
24 # Set resources.limits.memory equal to zero if not present and resources.requests.memory equal to 1m rather than zero
25 # to avoid undefined division error. No memory request in this case is basically the same as 1m. Kubernetes API server
26 # will automatically set requests=limits if only limits is defined.
27 - key: "{{ modulo('{{ element.resources.limits.memory || '0' }}', '{{ element.resources.requests.memory || '1m' }}') }}"
28 operator: GreaterThan
29 value: 0
Multiply
Expand
The multiply()
filter performs standard multiplication on two inputs producing an output product. Like other arithmetic filters, it is input aware and will produce output with appropriate units attached.
Arithmetic filters like multiply()
currently accept inputs in the following formats.
- Number (ex., `10`)
- Quantity (ex., ‘10Mi’)
- Duration (ex., ‘10h’)
Note that how the inputs are enclosed determines how Kyverno interprets their type. Numbers enclosed in back ticks are scalar values while quantities and durations are enclosed in single quotes thus treating them as strings. Using the correct enclosing character is important because, in Kubernetes “regular” numbers are treated implicitly as units of measure. The number written `10` is interpreted as an integer or “the number ten” whereas ‘10’ is interpreted as a string or “ten bytes”. See the Formatting section above for more details.
Input 1 | Input 2 | Output |
---|---|---|
Number | Number | Number |
Quantity | Number | Quantity |
Duration | Number | Duration |
Due to the commutative property of multiplication, the ordering of inputs (unlike with divide()
) is irrelevant.
The inputs list is currently under construction.
Example: This policy sets the replica count for a Deployment to a value of two times the current number of Nodes in a cluster.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: multiply-demo
5spec:
6 background: false
7 rules:
8 - name: multiply-replicas
9 match:
10 any:
11 - resources:
12 kinds:
13 - Deployment
14 context:
15 - name: nodecount
16 apiCall:
17 urlPath: "/api/v1/nodes"
18 jmesPath: "items[] | length(@)"
19 mutate:
20 patchStrategicMerge:
21 spec:
22 replicas: "{{ multiply( `{{nodecount}}`,`2`) }}"
Object_from_list
Expand
The object_from_list()
filter takes an array of objects and, based on the selected keys, produces a map. This is essentially the inverse of the items()
filter.
For example, given a Pod definition that looks like the following
1apiVersion: v1
2kind: Pod
3metadata:
4 name: object-from-list-demo
5 labels:
6 foo: bar
7spec:
8 containers:
9 - name: containername01
10 image: containerimage:01
11 env:
12 - name: KEY
13 value: "123-456-789"
14 - name: endpoint
15 value: "licensing.corp.org"
you may want to convert the spec.containers[].env[]
array of objects into a map where each entry in the map sets the key to the name
and the value to the value
fields. Running this through the object_from_list()
filter will produce a map containing those entries.
1$ kyverno jp query -i pod.yaml "object_from_lists(spec.containers[].env[].name,spec.containers[].env[].value)"
2{
3 "KEY": "123-456-789",
4 "endpoint": "licensing.corp.org"
5}
Input 1 | Input 2 | Output |
---|---|---|
Array/string | Array/string | Map (Object) |
Related filter to object_from_list()
is its inverse, items()
.
Example: This policy converts all the environment variables across all containers in a Pod to labels and adds them to that same Pod. Any existing labels will not be replaced but rather augmented with the converted list.
1apiVersion : kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: object-from-list-demo
5 annotations:
6 pod-policies.kyverno.io/autogen-controllers: none
7spec:
8 background: false
9 rules:
10 - name: object-from-list-rule
11 match:
12 any:
13 - resources:
14 kinds:
15 - Pod
16 context:
17 - name: envs
18 variable:
19 jmesPath: request.object.spec.containers[].env[]
20 - name: envs_to_labels
21 variable:
22 jmesPath: object_from_lists(envs[].name, envs[].value)
23 mutate:
24 patchStrategicMerge:
25 metadata:
26 labels:
27 "{{envs_to_labels}}"
Given an incoming Pod that looks like the following
1apiVersion: v1
2kind: Pod
3metadata:
4 name: object-from-list-demo
5 labels:
6 foo: bar
7spec:
8 containers:
9 - name: containername01
10 image: containerimage:01
11 env:
12 - name: KEY
13 value: "123-456-789"
14 - name: ENDPOINT
15 value: "licensing.corp.org"
16 - name: containername02
17 image: containerimage:02
18 env:
19 - name: ZONE
20 value: "fl-west-03"
after applying the policy the resulting label set on the Pod appears as shown below.
1$ kubectl get pod/object-from-list-demo -o json | kyverno jp query "metadata.labels"
2{
3 "ENDPOINT": "licensing.corp.org",
4 "KEY": "123-456-789",
5 "ZONE": "fl-west-03",
6 "foo": "bar"
7}
Parse_json
Expand
The parse_json()
filter takes in a string of any valid encoded JSON and parses it into a fully-formed JSON object. This is useful because it allows Kyverno to access and work with string data that is stored anywhere which accepts strings as if it were “native” JSON data. Primary use cases for this filter include adding anything from snippets to whole documents as the values of labels, annotations, or ConfigMaps which should then be consumed by policy.
Input 1 | Output |
---|---|
String | Any |
Example: This policy uses the parse_json()
filter to read a ConfigMap where a specified key contains JSON-encoded data (an array of strings in this case) and sets the supplementalGroups field of a Pod, if not already supplied, to that list.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: parse-json-demo
5spec:
6 rules:
7 - name: parse-supplementalgroups-from-json
8 match:
9 any:
10 - resources:
11 kinds:
12 - Pod
13 context:
14 - name: gidsMap
15 configMap:
16 name: user-gids-map
17 namespace: default
18 mutate:
19 patchStrategicMerge:
20 spec:
21 securityContext:
22 +(supplementalGroups): "{{ gidsMap.data.\"{{ request.object.metadata.labels.\"corp.com/service-account\" }}\" | parse_json(@)[*].to_number(@) }}"
The referenced ConfigMap may look similar to the below.
1apiVersion: v1
2kind: ConfigMap
3metadata:
4 name: user-gids-map
5 namespace: default
6data:
7 finance: '["1001","1002"]'
Parse_yaml
Expand
The parse_yaml()
filter is the YAML equivalent of the parse_json()
filter and takes in a string of any valid YAML document, serializes it into JSON, and parses it so it may be processed by JMESPath. Like parse_json()
, this is useful because it allows Kyverno to access and work with string data that is stored anywhere which accepts strings as if it were “native” YAML data. Primary use cases for this filter include adding anything from snippets to whole documents as the values of labels, annotations, or ConfigMaps which should then be consumed by policy.
Input 1 | Output |
---|---|
String | Any |
Example: This policy parses a YAML document as the value of an annotation and uses the filtered value from a JMESPath expression in a variable substitution.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: parse-yaml-demo
5spec:
6 validationFailureAction: Enforce
7 background: false
8 rules:
9 - name: check-goodbois
10 match:
11 any:
12 - resources:
13 kinds:
14 - Pod
15 validate:
16 message: "Only good bois allowed."
17 deny:
18 conditions:
19 - key: "{{request.object.metadata.annotations.pets | parse_yaml(@).species.isGoodBoi }}"
20 operator: NotEquals
21 value: true
The referenced Pod may look similar to the below.
1apiVersion: v1
2kind: Pod
3metadata:
4 name: mypod
5 labels:
6 app: busybox
7 annotations:
8 pets: |-
9 species:
10 dog: lab
11 name: dory
12 color: black
13 height: 15
14 isGoodBoi: false
15 snacks:
16 - chimken
17 - fries
18 - pizza
19spec:
20 containers:
21 - name: busybox
22 image: busybox:1.28
Path_canonicalize
Expand
The path_canonicalize()
filter is used to normalize or canonicalize a given path by removing excess slashes. For example, a path supplied to the filter may be /var//lib///kubelet
which will be canonicalized into /var/lib/kubelet
which is how an operating system would interpret the former. This filter is primarily used as a circumvention protection for what would otherwise be strict string matches for paths.
Input 1 | Output |
---|---|
String | String |
Example: This policy uses the path_canonicalize()
filter to check the value of each hostPath.path
field in a volume block in a Pod to ensure it does not attempt to mount the Containerd host socket.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: path-canonicalize-demo
5spec:
6 validationFailureAction: Enforce
7 background: false
8 rules:
9 - name: disallow-mount-containerd-sock
10 match:
11 any:
12 - resources:
13 kinds:
14 - Pod
15 validate:
16 foreach:
17 - list: "request.object.spec.volumes[]"
18 deny:
19 conditions:
20 any:
21 - key: "{{ path_canonicalize(element.hostPath.path) }}"
22 operator: Equals
23 value: "/var/run/containerd/containerd.sock"
Pattern_match
Expand
The pattern_match()
filter is used to perform a simple, non-regex match by specifying an input pattern and the string or number to which it should be compared. The output is always a boolean response. This filter can be useful when wishing to make simpler comparisons, typically with strings or numbers involved. It avoids many of the complexities of regex while still support wildcards such as *
(zero or more characters) and ?
(any one character). Note that since Kyverno supports overlay-style patterns and wildcards, use of pattern_match()
is typically not needed in these scenarios. This filter is more valuable in dynamic lookup scenarios by using JMESPath variables for one or both inputs as exemplified below.
Input 1 | Input 2 | Output |
---|---|---|
String | String | Boolean |
String | Number | Boolean |
Example: This policy uses pattern_match()
with dynamic inputs by fetching a pattern stored in a ConfigMap against an incoming Namespace label value.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: pattern-match-demo
5spec:
6 validationFailureAction: Enforce
7 background: false
8 rules:
9 - match:
10 any:
11 - resources:
12 kinds:
13 - Namespace
14 name: dept-billing-check
15 context:
16 - name: deptbillingcodes
17 configMap:
18 name: deptbillingcodes
19 namespace: default
20 validate:
21 message: The department {{request.object.metadata.labels.dept}} must supply a matching billing code.
22 deny:
23 conditions:
24 any:
25 - key: "{{pattern_match('{{deptbillingcodes.data.{{request.object.metadata.labels.dept}}}}', '{{ request.object.metadata.labels.segbill}}') }}"
26 operator: Equals
27 value: false
The ConfigMap used as the source of the patterns may look like below.
1apiVersion: v1
2kind: ConfigMap
3metadata:
4 name: deptbillingcodes
5data:
6 eng_china: 158-6?-3*
7 eng_india: 158-7?-4*
8 busops: 145-0?-9*
9 finops: 145-1?-5*
And a Namespace upon which the above ClusterPolicy may act can look like below.
1apiVersion: v1
2kind: Namespace
3metadata:
4 name: ind-go
5 labels:
6 dept: eng_india
7 segbill: 158-73-417
Random
Expand
The random()
filter is used to generate a random sequence of string data based upon the input pattern, expressed as regex. The input it takes is a combination of the composition of the pattern and the length of each pattern. This filter is useful in a variety of ways including generating unique resource names. Some other use cases include creating Pod hashes, auth tokens, license keys, GUIDs, and more.
For example, random('[0-9a-z]{5}')
will produce a string output of exactly 5 characters long composed of numbers in the collection 0-9
and lower-case letters in the collection a-z
. The output might be "91t6f"
. More complex random output can be created by chaining multiple pattern and length combinations together. For example, to create a faux license key you could use the expression random('[A-Z0-9]{8}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{12}')
which may generate the output "K284DW7Y-7LMT-XHR3-ZZ53-36366O8JVDG9"
.
Input 1 | Output |
---|---|
String | String |
Example: This policy uses random()
to mutate a new Secret to add a label with key randomoutput
and the value of which is random-
followed by 6 random characters composed of lower-case letters a-z
and numbers 0-9
.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: ver-test
5spec:
6 rules:
7 - name: test-ver-ver
8 match:
9 any:
10 - resources:
11 kinds:
12 - Secret
13 operations:
14 - CREATE
15 context:
16 - name: randomtest
17 variable:
18 jmesPath: random('[a-z0-9]{6}')
19 mutate:
20 patchStrategicMerge:
21 metadata:
22 labels:
23 randomoutput: random-{{randomtest}}
Regex_match
Expand
The regex_match()
filter is similar to the pattern_match()
filter except it accepts standard regular expressions as the comparison format. The first input is the pattern, specified in regex format, while the second is the string compared to the pattern which accepts either string or number. The output is always a boolean response. For example, the following two expressions, which check to ensure a number is in the range of one to seven, both evaluate to true
.
regex_match('^[1-7]$',`1`)
regex_match('^[1-7]$','1')
Input 1 | Input 2 | Output |
---|---|---|
String | String | Boolean |
String | Number | Boolean |
Example: This policy checks that a PersistentVolumeClaim resource contains an annotation named backup-schedule
and its value conforms to a standard Cron expression string. Note that the regular expression in the first input has had an additional backslash added to each backslash to be valid YAML. To use this sample regex in other applications, remove one of each double backslash pair.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: regex-match-demo
5spec:
6 background: true
7 validationFailureAction: Enforce
8 rules:
9 - name: validate-backup-schedule-annotation-cron
10 match:
11 any:
12 - resources:
13 kinds:
14 - PersistentVolumeClaim
15 validate:
16 message: The annotation `backup-schedule` must be present and in cron format.
17 deny:
18 conditions:
19 any:
20 - key: "{{ regex_match('^((?:\\*|[0-5]?[0-9](?:(?:-[0-5]?[0-9])|(?:,[0-5]?[0-9])+)?)(?:\\/[0-9]+)?)\\s+((?:\\*|(?:1?[0-9]|2[0-3])(?:(?:-(?:1?[0-9]|2[0-3]))|(?:,(?:1?[0-9]|2[0-3]))+)?)(?:\\/[0-9]+)?)\\s+((?:\\*|(?:[1-9]|[1-2][0-9]|3[0-1])(?:(?:-(?:[1-9]|[1-2][0-9]|3[0-1]))|(?:,(?:[1-9]|[1-2][0-9]|3[0-1]))+)?)(?:\\/[0-9]+)?)\\s+((?:\\*|(?:[1-9]|1[0-2])(?:(?:-(?:[1-9]|1[0-2]))|(?:,(?:[1-9]|1[0-2]))+)?)(?:\\/[0-9]+)?)\\s+((?:\\*|[0-7](?:-[0-7]|(?:,[0-7])+)?)(?:\\/[0-9]+)?)$', '{{request.object.metadata.annotations.\"backup-schedule\" || ''}}') }}"
21 operator: Equals
22 value: false
Regex_replace_all
Expand
The regex_replace_all()
filter is similar to the replace_all()
filter only differing by the first and third inputs being a valid regular expression rather than a static string. For literal replacement, see regex_replace_all_literal()
. If numbers are supplied for the second and third inputs, they will internally be converted to string. The output is always a string. For example, the expression regex_replace_all('([0-9])([0-9])', 'hello im 42 months old', '${1}1')
results in the output hello im 41 months old
. The first input provides the regex which should be used to match against the second input, and the third serves as the replacement which, in this case, replaces the first capture group from the end with the number 1
.
Input 1 | Input 2 | Input 3 | Output |
---|---|---|---|
Regex (String) | String | Regex (String) | String |
Regex (String) | Number | Number | String |
Example: This policy mutates a Deployment having label named retention
to set the last number to 0
. For example, an incoming Deployment with the label value of days_37
would result in the value days_30
after mutation.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: regex-replace-all-demo
5spec:
6 background: false
7 rules:
8 - name: retention-adjust
9 match:
10 any:
11 - resources:
12 kinds:
13 - Deployment
14 mutate:
15 patchStrategicMerge:
16 metadata:
17 labels:
18 retention: "{{ regex_replace_all('([0-9])([0-9])', '{{ @ }}', '${1}0') }}"
Regex_replace_all_literal
Expand
The regex_replace_all_literal()
filter is similar to the regex_replace_all()
filter with the third input being a static string used for literal replacement. If numbers are supplied for the second and third inputs, they will internally be converted to string. The output is always a string. For example, the expression regex_replace_all_literal('^(\d{3}-?\d{2}-?\d{4})$', '123-45-6789', 'redacted')
would return redacted
as the regex filter matches the faux social security number of 123-45-6789
.
Input 1 | Input 2 | Input 3 | Output |
---|---|---|---|
Regex (String) | String | String | String |
Regex (String) | Number | Number | String |
Example: This policy replaces the image registry for each image in every container so it comes from myregistry.corp.com
. Note that, for images without an explicit registry such as nginx:latest
, Kyverno will internally replace this to be docker.io/nginx:latest
and thereby ensuring the regex pattern below matches.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: regex-replace-all-literal-demo
5spec:
6 background: false
7 rules:
8 - name: replace-image-registry
9 match:
10 any:
11 - resources:
12 kinds:
13 - Pod
14 mutate:
15 foreach:
16 - list: "request.object.spec.containers"
17 patchStrategicMerge:
18 spec:
19 containers:
20 - name: "{{ element.name }}"
21 image: "{{ regex_replace_all_literal('^[^/]+', '{{element.image}}', 'myregistry.corp.com' )}}"
Replace
Expand
The replace()
filter is similar to the replace_all()
filter except it takes a fourth input (a number) to specify how many instances of the source string should be replaced with the replacement string in a parent. For example, the expression shown below results in the value Lorem muspi dolor sit amet foo muspi bar ipsum
because only two instances of the string ipsum
were requested to be replaced. String replacement begins at the left and proceeds to the right halting once the desired count has been reached. If -1
is specified for the four input, it results in all instances of the source string being replaced (effectively the same behavior as replace_all()
).
replace('Lorem ipsum dolor sit amet foo ipsum bar ipsum', 'ipsum', 'muspi', `2`)
Input 1 | Input 2 | Input 3 | Input 4 | Output |
---|---|---|---|---|
String | String | String | Number | String |
Example: This policy replaces the rule on an Ingress resource so that the path field will replace the first instance of /cart
with /shoppingcart
.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: replace-demo
5spec:
6 background: false
7 rules:
8 - name: replace-path
9 match:
10 any:
11 - resources:
12 kinds:
13 - Ingress
14 mutate:
15 foreach:
16 - list: "request.object.spec.rules[].http.paths[]"
17 patchStrategicMerge:
18 spec:
19 rules:
20 - http:
21 paths:
22 - backend:
23 service:
24 name: kuard
25 port:
26 number: 8080
27 path: "{{ replace('{{element.path}}', '/cart', '/shoppingcart', `1`) }}"
28 pathType: ImplementationSpecific
Replace_all
Expand
The replace_all()
filter is used to find and replace all instances of one string with another in an overall parent string. Input strings are assumed to be literal and do not support wildcards. For example, the expression replace_all('Lorem ipsum dolor sit amet', 'ipsum', 'muspi')
results in the value Lorem muspi dolor sit amet
as the string ipsum
has been replaced with muspi
. If there were multiple instances of ipsum
in the parent string, they would all be replaced with muspi
.
Input 1 | Input 2 | Input 3 | Output |
---|---|---|---|
String | String | String | String |
Example: This policy uses replace_all()
to replace the string release-name---
with the contents of the annotation meta.helm.sh/release-name
in the workingDir
field under a container entry within a Deployment.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: replace-all-demo
5spec:
6 background: false
7 rules:
8 - name: replace-workingdir
9 match:
10 any:
11 - resources:
12 kinds:
13 - Deployment
14 mutate:
15 patchStrategicMerge:
16 spec:
17 template:
18 spec:
19 containers:
20 - (name): "*"
21 workingDir: "{{ replace_all('{{@}}', 'release-name---', '{{request.object.metadata.annotations.\"meta.helm.sh/release-name\"}}') }}"
Semver_compare
Expand
The semver_compare()
filter compares two strings which comply with the semantic versioning schema and outputs a boolean response as to the position of the second relative to the first. The first input is the “base” semver string for comparison while the second is the version is compared against the first. The second string accepts an operator prefix and supports AND and OR logic. It also supports the special placeholder variable “x” in any position. For some examples, semver_compare('1.2.3','1.2.4')
results in the output false
because version 1.2.4 is not equal to version 1.2.3. semver_compare('4.1.3','>=4.1.x')
results in the output true
because 4.1.3 is greater than or equal to 4.1.x. semver_compare('4.1.3','!4.x.x')
returns false
because 4.1.3 is equal to 4.x.x. semver_compare('1.8.6','>1.0.0 <2.0.0')
returns true
because the second input is an AND expression and 1.8.6 is both greater than 1.0.0 and less than 2.0.0. And semver_compare('2.1.5','<2.0.0 || >=3.0.0')
returns false
because 2.1.5 is neither less than 2.0.0 nor greater than or equal to 3.0.0.
Input 1 | Input 2 | Output |
---|---|---|
String | String | Boolean |
Example: This policy uses semver_compare()
to check the attestations on a container image and denies it has been built with httpclient greater than version 4.5.0.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: semver-compare-demo
5spec:
6 validationFailureAction: Enforce
7 background: false
8 rules:
9 - name: check-sbom
10 match:
11 any:
12 - resources:
13 kinds:
14 - Pod
15 verifyImages:
16 - image: "ghcr.io/kyverno/test-verify-image*"
17 key: |-
18 -----BEGIN PUBLIC KEY-----
19 MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHMmDjK65krAyDaGaeyWNzgvIu155
20 JI50B2vezCw8+3CVeE0lJTL5dbL3OP98Za0oAEBJcOxky8Riy/XcmfKZbw==
21 -----END PUBLIC KEY-----
22 attestations:
23 - predicateType: https://example.com/CycloneDX/v1
24 conditions:
25 - all:
26 - key: "{{ components[?name == 'commons-logging'].version | [0] }}"
27 operator: GreaterThanOrEquals
28 value: "1.2.0"
29 - key: "{{ semver_compare( {{ components[?name == 'httpclient'].version | [0] }}, '>4.5.0') }}"
30 operator: Equals
31 value: true
Split
Expand
The split()
filter is used to take in an input string, a character or sequence found within that string, and split the source into an array of strings. For example, the string cat,dog,horse
can be split on the comma (,
) character resulting in three separate strings in the collection ["cat","dog","horse"]
. This filter is often most useful when looping over a number of different strings within a single value and performing some comparison or expression.
Input 1 | Input 2 | Output |
---|---|---|
String | String | Array/string |
Example: This policy checks an incoming Ingress to ensure its root path does not conflict with another root path in a different Namespace. It requires that incoming Ingress resources have a single rule with a single path only and assumes the root path is specified explicitly in an existing Ingress rule (ex., when blocking /foo/bar /foo must exist by itself and not part of /foo/baz).
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: split-demo
5spec:
6 validationFailureAction: Audit
7 background: false
8 rules:
9 - name: check-path
10 match:
11 any:
12 - resources:
13 kinds:
14 - Ingress
15 operations:
16 - CREATE
17 context:
18 # Looks up the Ingress paths across the whole cluster.
19 - name: allpaths
20 apiCall:
21 urlPath: "/apis/networking.k8s.io/v1/ingresses"
22 jmesPath: "items[].spec.rules[].http.paths[].path"
23 # Looks up the Ingress paths in the same Namespace where the incoming request is targeted.
24 - name: nspath
25 apiCall:
26 urlPath: "/apis/networking.k8s.io/v1/namespaces/{{request.object.metadata.namespace}}/ingresses"
27 jmesPath: "items[].spec.rules[].http.paths[].path"
28 validate:
29 message: >-
30 The root path /{{request.object.spec.rules[].http.paths[].path | to_string(@) | split(@, '/') | [1]}}/ exists
31 in another Ingress rule elsewhere in the cluster.
32 deny:
33 conditions:
34 all:
35 # Deny if the root path of the request exists somewhere else in the cluster other than the same Namespace.
36 - key: /{{request.object.spec.rules[].http.paths[].path | to_string(@) | split(@, '/') | [1]}}/
37 operator: In
38 value: "{{allpaths}}"
39 - key: /{{request.object.spec.rules[].http.paths[].path | to_string(@) | split(@, '/') | [1]}}/
40 operator: NotIn
41 value: "{{nspath}}"
Subtract
Expand
The subtract()
filter performs arithmetic subtraction capabilities between two input fields (terms) and produces an output difference. Like other arithmetic custom filters, it is input aware based on the type passed and, for quantities, allows auto conversion between units of measure. For example, subtracting 10Mi (ten mebibytes) minus 5Ki (five kibibytes) results in the value 10235Ki. The subtract()
filter is currently under development to better account for all permutations of input types, however the below table captures the most common and practical use cases.
Arithmetic filters like subtract()
currently accept inputs in the following formats.
- Number (ex., `10`)
- Quantity (ex., ‘10Mi’)
- Duration (ex., ‘10h’)
Note that how the inputs are enclosed determines how Kyverno interprets their type. Numbers enclosed in back ticks are scalar values while quantities and durations are enclosed in single quotes thus treating them as strings. Using the correct enclosing character is important because, in Kubernetes “regular” numbers are treated implicitly as units of measure. The number written `10` is interpreted as an integer or “the number ten” whereas ‘10’ is interpreted as a string or “ten bytes”. See the Formatting section above for more details.
Input 1 | Input 2 | Output |
---|---|---|
Number | Number | Number |
Quantity | Quantity | Number |
Duration | Duration | Duration |
Example: This policy sets the value of a new label called lessreplicas
to the value of the current number of replicas in a Deployment minus two so long as there are more than two replicas to start with.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: subtract-demo
5spec:
6 background: false
7 rules:
8 - name: subtract-demo
9 match:
10 any:
11 - resources:
12 kinds:
13 - Deployment
14 preconditions:
15 any:
16 - key: "{{ request.object.spec.replicas }}"
17 operator: GreaterThan
18 value: 2
19 mutate:
20 patchStrategicMerge:
21 metadata:
22 labels:
23 lessreplicas: "{{ subtract('{{ request.object.spec.replicas }}',`2`) }}"
Sum
Expand
The sum()
filter takes an array of numbers, durations, or quantities and sums them together. This is a customized version of sum()
found in the upstream JMESPath specification but augmented to support inputs common to Kubernetes workloads, specifically durations and quantities. sum()
is similar to add()
with the difference that sum()
accepts an array as an input while add()
does not. Inputs must be of a homogenous type. For example, the query echo '{"input":['2Ki','5Gi','8Mi']}' | kyverno jp query "sum(input)"
results in the value "5251074Ki"
. The query echo '{"input":['2h','50s','90s']}' | kyverno jp query "sum(input)"
results in the value "2h2m20s"
. And the query echo '{"input":[6,3,8]}' | kyverno jp query "sum(input)"
results in the value of 17
.
Arithmetic filters like sum()
currently accept inputs in the following formats.
- Number (ex., `10`)
- Quantity (ex., ‘10Mi’)
- Duration (ex., ‘10h’)
See the Formatting section above for more details on how inputs are expected to be supplied. Durations cannot be expressed as minutes (i.e., "5m"
) as the “m” unit is interpreted to be millicores.
Input 1 | Output |
---|---|
Array/Number | Number |
Array/Quantity | Quantity |
Array/Duration | Duration |
Example: This policy sums the memory requests of all containers in a Pod and denies it if it exceeds 1 gibibyte (1Gi).
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: sum-demo
5spec:
6 validationFailureAction: Enforce
7 rules:
8 - name: memory-requests-check
9 match:
10 any:
11 - resources:
12 kinds:
13 - Pod
14 validate:
15 message: The sum of all memory requests in a Pod cannot exceed 1 gibibyte.
16 deny:
17 conditions:
18 all:
19 - key: "{{ sum(request.object.spec.containers[].resources.requests.memory) }}"
20 operator: GreaterThan
21 value: 1Gi
Time_add
Expand
The time_add()
filter is used to take a starting, absolute time in RFC 3339 format, and add some duration to it. Duration can be specified in terms of seconds, minutes, and hours. For times not given in RFC 3339, use the time_parse()
function to convert the source time into RFC 3339.
The expression time_add('2023-01-12T12:37:56-05:00','6h')
results in the value "2023-01-12T18:37:56-05:00"
.
Input 1 | Input 2 | Output |
---|---|---|
Time start (String) | Time duration (String) | Time end (String) |
Example: This policy uses time_add()
in addition to time_now_utc()
and time_to_cron()
to get the current time and add four hours to it in order to write out the new schedule, in Cron format, necessary for a ClusterCleanupPolicy.
1apiVersion: kyverno.io/v2beta1
2kind: ClusterPolicy
3metadata:
4 name: automate-cleanup
5spec:
6 validationFailureAction: Enforce
7 background: false
8 rules:
9 - name: cleanup
10 match:
11 any:
12 - resources:
13 kinds:
14 - PolicyException
15 namespaces:
16 - foo
17 generate:
18 apiVersion: kyverno.io/v2alpha1
19 kind: ClusterCleanupPolicy
20 name: polex-{{ request.namespace }}-{{ request.object.metadata.name }}-{{ random('[0-9a-z]{8}') }}
21 synchronize: false
22 data:
23 metadata:
24 labels:
25 kyverno.io/automated: "true"
26 spec:
27 schedule: "{{ time_add('{{ time_now_utc() }}','4h') | time_to_cron(@) }}"
28 match:
29 any:
30 - resources:
31 kinds:
32 - PolicyException
33 namespaces:
34 - "{{ request.namespace }}"
35 names:
36 - "{{ request.object.metadata.name }}"
Time_after
Expand
The time_after()
filter is used to determine whether one absolute time is after another absolute time where both times are in RFC 3339 format. The output is a boolean response where true
if the end time is after the begin time and false
if it is not.
The expression time_after('2023-01-12T14:07:55-05:00','2023-01-12T19:05:59Z')
results in the value true
. The expression time_after('2023-01-12T19:05:59Z','2023-01-13T19:05:59Z')
results in the value false
.
Input 1 | Input 2 | Output |
---|---|---|
Time end (String) | Time begin (String) | Boolean |
Example: This policy uses time_after()
in addition to time_now_utc()
to deny ConfigMap creation if the current time is after the deadline for cluster decommissioning.
1apiVersion: kyverno.io/v2beta1
2kind: ClusterPolicy
3metadata:
4 name: decommission-policy
5spec:
6 background: false
7 validationFailureAction: Enforce
8 rules:
9 - name: decomm-jan-12
10 match:
11 any:
12 - resources:
13 kinds:
14 - ConfigMap
15 validate:
16 message: "This cluster is being decommissioned and no further resources may be created after January 12th."
17 deny:
18 conditions:
19 all:
20 - key: "{{ time_after('{{time_now_utc() }}','2023-01-12T00:00:00Z') }}"
21 operator: Equals
22 value: true
Time_before
Expand
The time_before()
filter is used to determine whether one absolute time is before another absolute time where both times are in RFC 3339 format. The output is a boolean response where true
if the end time is before the begin time and false
if it is not.
The expression time_before('2023-01-12T19:05:59Z','2023-01-13T19:05:59Z')
results in the value true
. The expression time_before('2023-01-12T19:05:59Z','2023-01-11T19:05:59Z')
results in the value false
.
Input 1 | Input 2 | Output |
---|---|---|
Time end (String) | Time begin (String) | Boolean |
Example: This policy uses time_before()
in addition to time_now_utc()
to effectively set an expiration date for a policy. Up until the UTC time of 2023-01-31T00:00:00Z, the label foo
must be present on a ConfigMap.
1apiVersion: kyverno.io/v2beta1
2kind: ClusterPolicy
3metadata:
4 name: expiration
5spec:
6 background: false
7 validationFailureAction: Enforce
8 rules:
9 - name: expire-jan-31
10 match:
11 any:
12 - resources:
13 kinds:
14 - ConfigMap
15 preconditions:
16 all:
17 - key: "{{ time_before('{{ time_now_utc() }}','2023-01-31T00:00:00Z') }}"
18 operator: Equals
19 value: true
20 validate:
21 message: "The foo label must be set."
22 pattern:
23 metadata:
24 labels:
25 foo: "?*"
Time_between
Expand
The time_between()
filter is used to check if a given time is between a range of two other times where all time is expected in RFC 3339 format.
The expression time_between('2023-01-12T19:05:59Z','2023-01-01T19:05:59Z','2023-01-15T19:05:59Z')
results in the value true
. The expression time_between('2023-01-12T19:05:59Z','2023-01-01T19:05:59Z','2023-01-11T19:05:59Z')
results in the value false
.
Input 1 | Input 2 | Input 3 | Output |
---|---|---|---|
Time to check (String) | Time start (String) | Time end (String) | Boolean |
Example: This policy uses time_between()
in addition to time_now_utc()
to establish a boundary of a policy’s function. Between 1 January 2023 and 31 January 2023, the label foo
must be present on a ConfigMap.
1apiVersion: kyverno.io/v2beta1
2kind: ClusterPolicy
3metadata:
4 name: expiration
5spec:
6 background: false
7 validationFailureAction: Enforce
8 rules:
9 - name: expire-jan-31
10 match:
11 any:
12 - resources:
13 kinds:
14 - ConfigMap
15 preconditions:
16 all:
17 - key: "{{ time_between('{{ time_now_utc() }}','2023-01-01T00:00:00Z','2023-01-31T23:59:59Z') }}"
18 operator: Equals
19 value: true
20 validate:
21 message: "The foo label must be set."
22 pattern:
23 metadata:
24 labels:
25 foo: "?*"
Time_diff
Expand
The time_diff()
filter calculates the amount of time between a start and end time where start and end are given in RFC 3339 format. The output, a string, is the duration and may be a negative duration.
The expression time_diff('2023-01-10T00:00:00Z','2023-01-11T00:00:00Z')
results in the value "24h0m0s"
. The expression time_diff('2023-01-12T00:00:00Z','2023-01-11T00:00:00Z')
results in the value "-24h0m0s"
.
Input 1 | Input 2 | Output |
---|---|---|
Time start (String) | Time duration (String) | Duration (String) |
Example: This policy uses the time_diff()
filter in addition to time_now_utc()
to ensure that a vulnerability scan for a given container image is no more than 24 hours old.
1apiVersion: kyverno.io/v2beta1
2kind: ClusterPolicy
3metadata:
4 name: require-vulnerability-scan
5spec:
6 validationFailureAction: Enforce
7 webhookTimeoutSeconds: 20
8 failurePolicy: Fail
9 rules:
10 - name: scan-not-older-than-one-day
11 match:
12 any:
13 - resources:
14 kinds:
15 - Pod
16 verifyImages:
17 - imageReferences:
18 - "ghcr.io/myorg/myrepo:*"
19 attestations:
20 - predicateType: cosign.sigstore.dev/attestation/vuln/v1
21 attestors:
22 - entries:
23 - keyless:
24 subject: "https://github.com/myorg/myrepo/.github/workflows/*"
25 issuer: "https://token.actions.githubusercontent.com"
26 rekor:
27 url: https://rekor.sigstore.dev
28 conditions:
29 - all:
30 - key: "{{ time_diff('{{metadata.scanFinishedOn}}','{{ time_now_utc() }}') }}"
31 operator: LessThanOrEquals
32 value: "24h"
Time_now
Expand
The time_now()
filter returns the current time in RFC 3339 format. The returned time will be presented as it is known to the Kubernetes Node itself and is not guaranteed to be in a specific time zone. There are no required inputs and the output is always an absolute time (string) in RFC 3339 format.
Input 1 | Output |
---|---|
None | Current time (String) |
Also note that in addition to calling time_now()
, for new resources the then-current time can often be retrieved via the metadata.creationTimestamp
field.
Example: This policy uses the time_now()
filter in addition to time_add()
and time_to_cron()
to generate a ClusterCleanupPolicy from 4 hours after the triggering PolicyException is created, converting it into cron format for use by the ClusterCleanupPolicy.
1apiVersion: kyverno.io/v2beta1
2kind: ClusterPolicy
3metadata:
4 name: automate-cleanup
5spec:
6 validationFailureAction: Enforce
7 background: false
8 rules:
9 - name: cleanup
10 match:
11 any:
12 - resources:
13 kinds:
14 - PolicyException
15 namespaces:
16 - foo
17 generate:
18 apiVersion: kyverno.io/v2alpha1
19 kind: ClusterCleanupPolicy
20 name: polex-{{ request.namespace }}-{{ request.object.metadata.name }}-{{ random('[0-9a-z]{8}') }}
21 synchronize: false
22 data:
23 metadata:
24 labels:
25 kyverno.io/automated: "true"
26 spec:
27 schedule: "{{ time_add('{{ time_now() }}','4h') | time_to_cron(@) }}"
28 match:
29 any:
30 - resources:
31 kinds:
32 - PolicyException
33 namespaces:
34 - "{{ request.namespace }}"
35 names:
36 - "{{ request.object.metadata.name }}"
Time_now_utc
Expand
The time_now_utc()
filter returns the current UTC time in RFC 3339 format. The returned time will be presented in UTC regardless of the time zone returned. There are no required inputs and the output is always an absolute time (string) in RFC 3339 format.
Input 1 | Output |
---|---|
None | Current time (String) |
Also note that in addition to calling time_now_utc()
, for new resources the then-current time can often be retrieved via the metadata.creationTimestamp
field.
Example: This policy uses the time_now_utc()
filter in addition to time_add()
and time_to_cron()
to generate a ClusterCleanupPolicy from 4 hours after the triggering PolicyException is created, converting it into cron format for use by the ClusterCleanupPolicy.
1apiVersion: kyverno.io/v2beta1
2kind: ClusterPolicy
3metadata:
4 name: automate-cleanup
5spec:
6 validationFailureAction: Enforce
7 background: false
8 rules:
9 - name: cleanup
10 match:
11 any:
12 - resources:
13 kinds:
14 - PolicyException
15 namespaces:
16 - foo
17 generate:
18 apiVersion: kyverno.io/v2alpha1
19 kind: ClusterCleanupPolicy
20 name: polex-{{ request.namespace }}-{{ request.object.metadata.name }}-{{ random('[0-9a-z]{8}') }}
21 synchronize: false
22 data:
23 metadata:
24 labels:
25 kyverno.io/automated: "true"
26 spec:
27 schedule: "{{ time_add('{{ time_now_utc() }}','4h') | time_to_cron(@) }}"
28 match:
29 any:
30 - resources:
31 kinds:
32 - PolicyException
33 namespaces:
34 - "{{ request.namespace }}"
35 names:
36 - "{{ request.object.metadata.name }}"
Time_parse
Expand
The time_parse()
filter converts an input time, given some other format, to RFC 3339 format. The first input is any time in the source format, the second input is the actual time to convert which is expected to be in the format specified by the first input. The output is always the second input converted to RFC 3339.
The expression time_parse('Mon Jan 02 2006 15:04:05 -0700', 'Fri Jun 22 2022 17:45:00 +0100')
results in the output of "2022-06-22T17:45:00+01:00"
. The expression time_parse('2006-01-02T15:04:05Z07:00', '2021-01-02T15:04:05-07:00')
results in the output of "2021-01-02T15:04:05-07:00"
.
Input 1 | Input 2 | Output |
---|---|---|
Time format (String) | Time to convert (String) | Time in RFC 3339 (String) |
Example: This policy uses time_parse()
to convert the value of the thistime
annotation, expected to be in a different format, to RFC 3339 and rewriting that value.
1apiVersion: kyverno.io/v2beta1
2kind: ClusterPolicy
3metadata:
4 name: time
5spec:
6 rules:
7 - name: set-time
8 match:
9 any:
10 - resources:
11 kinds:
12 - Service
13 mutate:
14 patchStrategicMerge:
15 metadata:
16 annotations:
17 thistime: "{{ time_parse('Mon Jan 02 2006 15:04:05 -0700','{{ @ }}') }}"
Time_since
Expand
The time_since()
filter is used to calculate the difference between a start and end period of time where the end may either be a static definition or the then-current time. The time formats currently supported are RFC3339 (the same as used by Kubernetes) or a user-definable time format as supported by the time.Parse()
Go function as documented here. The output time difference is always given in hours, minutes, and seconds where seconds may either be an integer or floating point. For example, the expression time_since('','2022-04-10T03:14:05-07:00','2022-04-11T03:14:05-07:00')
will result in the output of "24h0m0s"
. The first input for time format defaults to RFC3339. The expression time_since('Mon Jan _2 15:04:05 MST 2006', 'Mon Jan 02 15:04:05 MST 2021', 'Mon Jan 10 03:14:16 MST 2021')
uses Unix date format for the inputs (the same as when running the date
program) and will result in the output "180h10m11s"
. Helm time format is also parsable, for example time_since('2006-Jan-02','2020-Jan-14','2020-Jan-17')
resulting in "72h0m0s"
. And the expression time_since('','2022-04-10T03:14:05-07:00','')
will result in the difference between the current time and the second input. The output will be given in which seconds is a floating point value, for example "28h0m33.8257394s"
.
The time format (layout) parameter is optional and will be defaulted to RFC3339 if left empty (i.e., ‘’). It may not be set explicitly to an RFC3339 format. The time end (third input) may be set to an empty string indicating the current time (i.e., now) when the expression is evaluated.
Input 1 | Input 2 | Input 3 | Output |
---|---|---|---|
Time format (String) | Time start (String) | Time end (String) | Time difference (String) |
Example: This policy uses time_since()
to compare the time a container image was created to the present time, blocking if that difference is greater than six months.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: time-since-demo
5spec:
6 validationFailureAction: Audit
7 rules:
8 - name: block-stale-images
9 match:
10 any:
11 - resources:
12 kinds:
13 - Pod
14 validate:
15 message: "Images built more than 6 months ago are prohibited."
16 foreach:
17 - list: "request.object.spec.containers"
18 context:
19 - name: imageData
20 imageRegistry:
21 reference: "{{ element.image }}"
22 deny:
23 conditions:
24 all:
25 - key: "{{ time_since('', '{{ imageData.configData.created }}', '') }}"
26 operator: GreaterThan
27 value: 4380h
Time_to_cron
Expand
The time_to_cron()
filter takes in a time in RFC 3339 format and outputs the equivalent Cron-style expression.
The expression time_to_cron('2022-04-11T03:14:05-07:00')
results in the output "14 3 11 4 1"
.
Input 1 | Output |
---|---|
Time (string) | Cron expression (String) |
Example: This policy uses the time_to_cron()
filter in addition to time_add()
and time_now_utc()
to generate a ClusterCleanupPolicy from 4 hours after the triggering PolicyException is created, converting it into cron format for use by the ClusterCleanupPolicy.
1apiVersion: kyverno.io/v2beta1
2kind: ClusterPolicy
3metadata:
4 name: automate-cleanup
5spec:
6 validationFailureAction: Enforce
7 background: false
8 rules:
9 - name: cleanup
10 match:
11 any:
12 - resources:
13 kinds:
14 - PolicyException
15 namespaces:
16 - foo
17 generate:
18 apiVersion: kyverno.io/v2alpha1
19 kind: ClusterCleanupPolicy
20 name: polex-{{ request.namespace }}-{{ request.object.metadata.name }}-{{ random('[0-9a-z]{8}') }}
21 synchronize: false
22 data:
23 metadata:
24 labels:
25 kyverno.io/automated: "true"
26 spec:
27 schedule: "{{ time_add('{{ time_now_utc() }}','4h') | time_to_cron(@) }}"
28 match:
29 any:
30 - resources:
31 kinds:
32 - PolicyException
33 namespaces:
34 - "{{ request.namespace }}"
35 names:
36 - "{{ request.object.metadata.name }}"
Time_truncate
Expand
The time_truncate()
filter takes in a time in RFC 3339 format and a duration and outputs the nearest rounded down time that is a multiple of that duration.
The expression time_truncate('2023-01-12T17:37:00Z','1h')
results in the output "2023-01-12T17:00:00Z"
. The expression time_truncate('2023-01-12T17:37:00Z','2h')
results in the output "2023-01-12T16:00:00Z"
.
Input 1 | Input 2 | Output |
---|---|---|
Time in RFC 3339 (String) | Duration (String) | Time in RFC 3339 (String) |
Example: This policy uses time_truncate()
to get the current value of the thistime
annotation and round it down to the nearest multiple of 2 hours which, when thistime
is set to a value of "2021-01-02T23:04:05Z"
should result in the Service being mutated with the value "2021-01-02T22:00:00Z"
.
1apiVersion: kyverno.io/v2beta1
2kind: ClusterPolicy
3metadata:
4 name: time
5spec:
6 rules:
7 - name: set-time
8 match:
9 any:
10 - resources:
11 kinds:
12 - Service
13 mutate:
14 patchStrategicMerge:
15 metadata:
16 annotations:
17 thistime: "{{ time_truncate('{{ @ }}','2h') }}"
Time_utc
Expand
The time_utc()
filter takes in a time in RFC 3339 format with a time offset and presents the same time in UTC/Zulu.
The expression time_utc('2021-01-02T18:04:05-05:00')
results in the output "2021-01-02T23:04:05Z"
. The expression time_utc('2021-01-02T02:04:05+08:30')
results in the output "2021-01-01T17:34:05Z"
.
Input 1 | Output |
---|---|
Time in RFC 3339 (string) | Time in RFC 3339 (String) |
Example: This policy takes the time of the thistime
annotation and rewrites it in UTC.
1apiVersion: kyverno.io/v2beta1
2kind: ClusterPolicy
3metadata:
4 name: time
5spec:
6 rules:
7 - name: set-time
8 match:
9 any:
10 - resources:
11 kinds:
12 - Service
13 mutate:
14 patchStrategicMerge:
15 metadata:
16 annotations:
17 thistime: "{{ time_utc('{{ @ }}') }}"
To_boolean
Expand
The to_boolean()
filter converts a string to its boolean equivalent. Any strings which spell out “true” or “false” regardless of character case will be returned in boolean format. For example, the query to_boolean('true')
will result in the output true
. The query to_boolean('FalsE')
will result in the output false
.
This filter can be helpful when needing to produce output for a field which only accepts boolean without requiring more complex string manipulation.
Input 1 | Output |
---|---|
String | Boolean |
Example: This policy sets the hostIPC
field of a Pod spec appropriately based on the value of a label (a string). Note that use of this filter may require setting the policy option spec.schemaValidation
to false
since there may be a type checking mismatch.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: to-boolean-demo
5spec:
6 schemaValidation: false
7 rules:
8 - name: canuseIPC
9 match:
10 any:
11 - resources:
12 kinds:
13 - Pod
14 selector:
15 matchLabels:
16 canuseIPC: "true"
17 mutate:
18 patchStrategicMerge:
19 spec:
20 hostIPC: "{{ to_boolean (request.object.metadata.labels.canuseIPC) }}"
To_lower
Expand
The to_lower()
filter takes in a string and outputs the same string with all lower-case letters. It is the opposite of to_upper()
.
Input 1 | Output |
---|---|
String | String |
Example: This policy sets the value of a label named zonekey
to all caps.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: to-lower-demo
5spec:
6 rules:
7 - name: format-deploy-zone
8 match:
9 any:
10 - resources:
11 kinds:
12 - Service
13 mutate:
14 patchStrategicMerge:
15 metadata:
16 labels:
17 zonekey: "{{ to_lower('{{@}}') }}"
To_upper
Expand
The to_upper()
filter takes in a string and outputs the same string with all upper-case letters. It is the opposite of to_lower()
.
Input 1 | Output |
---|---|
String | String |
Example: This policy sets the value of a label named deployzone
to all caps.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: to-upper-demo
5spec:
6 rules:
7 - name: format-deploy-zone
8 match:
9 any:
10 - resources:
11 kinds:
12 - Service
13 mutate:
14 patchStrategicMerge:
15 metadata:
16 labels:
17 deployzone: "{{ to_upper('{{@}}') }}"
Trim
Expand
The trim()
filter takes a string containing a “source” string, a second string representing a collection of discrete characters, and outputs the remainder of the source when both ends of the source string are trimmed by characters appearing in the collection. For example, inputs of ¡¡¡Hello, Gophers!!!
and !¡
will result in the output Hello, Gophers
since the characters ¡
and !
are found at the beginning and end of the input string and will be trimmed. In the case of trim('foocorpcom','mo')
the output returned is foocorpc
since letters m
and o
are found at the end of foocorpcom
. Notice that ordering of the letters in the second input is irrelevant. Interior characters will not be stripped unless exterior characters have also been removed. For example, trim('foocorpcom','o')
will return the input of foocorpcom
because o
does not occur at the beginning or end of the input string. Characters named in the second input will be deduplicated from the source string so long as outside characters have been trimmed first. For example, trim('foocorpcom','mcof')
will result in the output of rp
since the other four characters in the second input collection can be stripped from the beginning and end of the input string.
This filter is similar to truncate()
. The trim()
filter can be useful to remove exact portions of a string when they are known literally.
Input 1 | Input 2 | Output |
---|---|---|
String | String | String |
Example: This policy uses the trim()
filter to remove the domain from an email value set in an annotation.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: trim-demo
5spec:
6 rules:
7 - name: trim-extnameemail
8 match:
9 any:
10 - resources:
11 kinds:
12 - Service
13 mutate:
14 patchStrategicMerge:
15 metadata:
16 annotations:
17 extnameemail: "{{ trim('{{@}}','@corp.com') }}"
Trim_prefix
Expand
The trim_prefix()
filter takes an input string and from it trims the beginning by the second string. For the trim to occur, the input string must begin with the trimmed string. This filter differs from trim()
in that it only removes a string from the beginning of another. For example, the query trim_prefix('docker://kubevirt/fedora-cloud-registry-disk-demo','docker://')
will result in the output of kubevirt/fedora-cloud-registry-disk-demo
.
The trim_prefix()
filter can be useful to remove URIs found in container image values referenced by some custom resources, or anywhere else where a more strategic removal of a substring within a parent is required.
Input 1 | Input 2 | Output |
---|---|---|
String | String | String |
Example: This policy uses the trim_prefix()
filter to remove docker://
from the name of an image in a KubeVirt DataVolume
custom resource through use of an image extractor.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: verify-data-volume-image
5spec:
6 background: false
7 validationFailureAction: Enforce
8 rules:
9 - name: verify-data-volume-image
10 match:
11 any:
12 - resources:
13 kinds:
14 - DataVolume
15 imageExtractors:
16 DataVolume:
17 - path: /spec/source/registry/url
18 jmesPath: "trim_prefix(@, 'docker://')"
19 verifyImages:
20 - imageReferences:
21 - "*"
22 mutateDigest: true
23 verifyDigest: true
24 attestors:
25 - entries:
26 - keys:
27 publicKeys: |
28 -----BEGIN PUBLIC KEY-----
29 ...
30 -----END PUBLIC KEY-----
Truncate
Expand
The truncate()
filter takes a string, a number, and shortens (truncates) that string from the beginning to only include the desired number of characters. For example, calling truncate()
on the string foobar
by the number 3
would result in the output of foo
because only three character positions were requested. This can be a useful filter when formulating values of names, labels, annotations, or other pieces of metadata to conform to a given length and to avoid overruns.
Input 1 | Input 2 | Output |
---|---|---|
String | Number | String |
Example: This policy truncates the value of a label called buildhash
to only take the first twelve characters.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: truncate-demo
5spec:
6 rules:
7 - name: truncate-buildhash
8 match:
9 any:
10 - resources:
11 kinds:
12 - Namespace
13 mutate:
14 patchStrategicMerge:
15 metadata:
16 labels:
17 buildhash: "{{ truncate('{{@}}',`12`) }}"
x509_decode
Expand
The x509_decode()
filter takes in a string which is a PEM-encoded X509 certificate or certificate signing request (CSR), and outputs a JSON object with the decoded details. It may often be required to first decode a base64-encoded string using base64_decode(). This filter can be used to check and validate attributes within a certificate or a CSR such as subject, issuer, SAN fields, and expiration time. An example of a decoded certificate may look like the following:
1{
2 "AuthorityKeyId": null,
3 "BasicConstraintsValid": true,
4 "CRLDistributionPoints": null,
5 "DNSNames": null,
6 "EmailAddresses": null,
7 "ExcludedDNSDomains": null,
8 "ExcludedEmailAddresses": null,
9 "ExcludedIPRanges": null,
10 "ExcludedURIDomains": null,
11 "ExtKeyUsage": null,
12 "Extensions": [
13 {
14 "Critical": true,
15 "Id": [
16 2,
17 5,
18 29,
19 15
20 ],
21 "Value": "AwICpA=="
22 },
23 {
24 "Critical": true,
25 "Id": [
26 2,
27 5,
28 29,
29 19
30 ],
31 "Value": "MAMBAf8="
32 },
33 {
34 "Critical": false,
35 "Id": [
36 2,
37 5,
38 29,
39 14
40 ],
41 "Value": "BBSWivt1n53+61ZGAczAi0mleejTKg=="
42 }
43 ],
44 "ExtraExtensions": null,
45 "IPAddresses": null,
46 "IsCA": true,
47 "Issuer": {
48 "CommonName": "*.kyverno.svc",
49 "Country": null,
50 "ExtraNames": null,
51 "Locality": null,
52 "Names": [
53 {
54 "Type": [
55 2,
56 5,
57 4,
58 3
59 ],
60 "Value": "*.kyverno.svc"
61 }
62 ],
63 "Organization": null,
64 "OrganizationalUnit": null,
65 "PostalCode": null,
66 "Province": null,
67 "SerialNumber": "",
68 "StreetAddress": null
69 },
70 "IssuingCertificateURL": null,
71 "KeyUsage": 37,
72 "MaxPathLen": -1,
73 "MaxPathLenZero": false,
74 "NotAfter": "2023-10-10T12:46:32Z",
75 "NotBefore": "2022-10-10T11:46:32Z",
76 "OCSPServer": null,
77 "PermittedDNSDomains": null,
78 "PermittedDNSDomainsCritical": false,
79 "PermittedEmailAddresses": null,
80 "PermittedIPRanges": null,
81 "PermittedURIDomains": null,
82 "PolicyIdentifiers": null,
83 "PublicKey": {
84 "E": 65537,
85 "N": "28595925905962223424520947352207105451744616797088171943239289907331901888529856098458304611629660120574607501039902142361333982065793213267074854658525100799280158707840279479550961169213763526857247298653141711003931642606662052674943191476488665842309583311097351331994267413776792462637192775240062778036062353517979538994974045127175206597906751521558536719043095219698535279694800624795673809356898452438518041024126624051887044932164506019573725987204208750674129677584956156611454245004918943771571492757639432459688931855526941886354880727024912384140238027697348634609952850513122734230521040730560514233467"
86 },
87 "PublicKeyAlgorithm": 1,
88 "Raw": "MIIC7TCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA0qLmt5dmVybm8uc3ZjMB4XDTIyMTAxMDExNDYzMloXDTIzMTAxMDEyNDYzMlowGDEWMBQGA1UEAwwNKi5reXZlcm5vLnN2YzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOKF+2P0Ufp855hpdsGD4lYkd6oU7HZAOWm1XskAMwrdsqWwTNNAinyHRoPQIbNbGDQ+r6Cggc2mlxHJ90PnC2weHj5otaD17Z+ARZpJZ4HMWkEfFt8sxwo9vuQJRWihqNwFheowjswoSB1DHnPufrZHfztkMoRx278ZfHaIMdlSTg50ektkNDoHA3OJsxxw54X3HR1iq6SZwN8xNT0TI6B6BbfAYWMNmKCiZ2iV6kW//XnTEqGd2WcmhuP0SjwO4tCJbj9oV6+Bj/uhFr7J4foErMaodYDBtQs/ul2tcAwSBHfnC2KcLbiZTZsC0Rs0WPJ4YwF/cOsD7Z/RmLs4FHsCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJaK+3Wfnf7rVkYBzMCLSaV56NMqMA0GCSqGSIb3DQEBCwUAA4IBAQDY7F6b+t9BX7098JyGk6zeT39MoLdSv+8IaKXn+m8GyOKn3CZkruko57ycvPd4taC0gggtmUYynFhwPMQr+boNrrK9rat8Jw3yPPsBq/8D/s6tvwxSNXBfPUI5OvNIB/hA5XpJpdHQaCkYm+FWkcJsolkkbSOfVjUjImW26JHBnnPPtR4Y7dx0SVoPS19IC0T5RmdvgqlXj4XbhTnX3QOujVHn8u+wQ8po7EngHDQs+onfkp8ipe0QpEJL1ZdW2LhyDXGKrZ2y8UPZ9wYNzxHWaj1Thu4B9YFdsPUwWqSxn9e+FygpoktlD8YgT7jwgiVKX7Koz++zyvMIdhvRrtgS",
89 "RawIssuer": "MBgxFjAUBgNVBAMMDSoua3l2ZXJuby5zdmM=",
90 "RawSubject": "MBgxFjAUBgNVBAMMDSoua3l2ZXJuby5zdmM=",
91 "RawSubjectPublicKeyInfo": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4oX7Y/RR+nznmGl2wYPiViR3qhTsdkA5abVeyQAzCt2ypbBM00CKfIdGg9Ahs1sYND6voKCBzaaXEcn3Q+cLbB4ePmi1oPXtn4BFmklngcxaQR8W3yzHCj2+5AlFaKGo3AWF6jCOzChIHUMec+5+tkd/O2QyhHHbvxl8dogx2VJODnR6S2Q0OgcDc4mzHHDnhfcdHWKrpJnA3zE1PRMjoHoFt8BhYw2YoKJnaJXqRb/9edMSoZ3ZZyaG4/RKPA7i0IluP2hXr4GP+6EWvsnh+gSsxqh1gMG1Cz+6Xa1wDBIEd+cLYpwtuJlNmwLRGzRY8nhjAX9w6wPtn9GYuzgUewIDAQAB",
92 "RawTBSCertificate": "MIIB1aADAgECAgEAMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNVBAMMDSoua3l2ZXJuby5zdmMwHhcNMjIxMDEwMTE0NjMyWhcNMjMxMDEwMTI0NjMyWjAYMRYwFAYDVQQDDA0qLmt5dmVybm8uc3ZjMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4oX7Y/RR+nznmGl2wYPiViR3qhTsdkA5abVeyQAzCt2ypbBM00CKfIdGg9Ahs1sYND6voKCBzaaXEcn3Q+cLbB4ePmi1oPXtn4BFmklngcxaQR8W3yzHCj2+5AlFaKGo3AWF6jCOzChIHUMec+5+tkd/O2QyhHHbvxl8dogx2VJODnR6S2Q0OgcDc4mzHHDnhfcdHWKrpJnA3zE1PRMjoHoFt8BhYw2YoKJnaJXqRb/9edMSoZ3ZZyaG4/RKPA7i0IluP2hXr4GP+6EWvsnh+gSsxqh1gMG1Cz+6Xa1wDBIEd+cLYpwtuJlNmwLRGzRY8nhjAX9w6wPtn9GYuzgUewIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAqQwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUlor7dZ+d/utWRgHMwItJpXno0yo=",
93 "SerialNumber": 0,
94 "Signature": "2Oxem/rfQV+9PfCchpOs3k9/TKC3Ur/vCGil5/pvBsjip9wmZK7pKOe8nLz3eLWgtIIILZlGMpxYcDzEK/m6Da6yva2rfCcN8jz7Aav/A/7Orb8MUjVwXz1COTrzSAf4QOV6SaXR0GgpGJvhVpHCbKJZJG0jn1Y1IyJltuiRwZ5zz7UeGO3cdElaD0tfSAtE+UZnb4KpV4+F24U5190Dro1R5/LvsEPKaOxJ4Bw0LPqJ35KfIqXtEKRCS9WXVti4cg1xiq2dsvFD2fcGDc8R1mo9U4buAfWBXbD1MFqksZ/XvhcoKaJLZQ/GIE+48IIlSl+yqM/vs8rzCHYb0a7YEg==",
95 "SignatureAlgorithm": 4,
96 "Subject": {
97 "CommonName": "*.kyverno.svc",
98 "Country": null,
99 "ExtraNames": null,
100 "Locality": null,
101 "Names": [
102 {
103 "Type": [
104 2,
105 5,
106 4,
107 3
108 ],
109 "Value": "*.kyverno.svc"
110 }
111 ],
112 "Organization": null,
113 "OrganizationalUnit": null,
114 "PostalCode": null,
115 "Province": null,
116 "SerialNumber": "",
117 "StreetAddress": null
118 },
119 "SubjectKeyId": "lor7dZ+d/utWRgHMwItJpXno0yo=",
120 "URIs": null,
121 "UnhandledCriticalExtensions": null,
122 "UnknownExtKeyUsage": null,
123 "Version": 3
124}
Input 1 | Output |
---|---|
String | Object |
Example: This policy, designed to operate in background mode only, checks the certificates configured for webhooks and fails if any have an expiration time in the next week.
1apiVersion: kyverno.io/v1
2kind: ClusterPolicy
3metadata:
4 name: test-x509-decode
5spec:
6 validationFailureAction: Audit
7 background: true
8 rules:
9 - name: test-x509-decode
10 match:
11 any:
12 - resources:
13 kinds:
14 - ValidatingWebhookConfiguration
15 - MutatingWebhookConfiguration
16 validate:
17 message: "Certificate will expire in less than a week."
18 deny:
19 conditions:
20 any:
21 - key: "{{ base64_decode('{{ request.object.webhooks[0].clientConfig.caBundle }}').x509_decode(@).time_since('',NotBefore,NotAfter) }}"
22 operator: LessThan
23 value: 168h0m0s