Skip to content

Commit d658d4a

Browse files
committed
Add request.body in CEL notification filtering
This moves the JSON body to request.body from request to allow for future expansion with the headers. Add documentation for the CEL functionality to the receivers doc. Signed-off-by: Kevin McDermott <[email protected]>
1 parent b1c4e7e commit d658d4a

File tree

3 files changed

+116
-4
lines changed

3 files changed

+116
-4
lines changed

docs/spec/v1/receivers.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,117 @@ resources:
700700
**Note:** Cross-namespace references [can be disabled for security
701701
reasons](#disabling-cross-namespace-selectors).
702702

703+
#### Filtering reconciled objects with CEL
704+
705+
To filter the resources that are reconciled you can use [Common Expression Language (CEL)](https://cel.dev/).
706+
707+
For example to trigger `ImageRepositories` on notifications from [Google Artifact Regisry](https://cloud.google.com/artifact-registry/docs/configure-notifications#examples) you can define a receiver.
708+
709+
```yaml
710+
apiVersion: notification.toolkit.fluxcd.io/v1
711+
kind: Receiver
712+
metadata:
713+
name: gar-receiver
714+
namespace: apps
715+
spec:
716+
type: gcr
717+
secretRef:
718+
name: flux-gar-token
719+
resources:
720+
- apiVersion: image.toolkit.fluxcd.io/v1beta2
721+
kind: ImageRepository
722+
name: "*"
723+
matchLabels:
724+
registry: gar
725+
```
726+
727+
This will trigger the reconciliation of all `ImageRepositories` with matching labels `registry: gar`, but if you want to only notify `ImageRepository` resources that are referenced from the incoming hook you can use CEL to filter the resources.
728+
729+
```yaml
730+
apiVersion: notification.toolkit.fluxcd.io/v1
731+
kind: Receiver
732+
metadata:
733+
name: gar-receiver
734+
namespace: apps
735+
spec:
736+
type: gcr
737+
secretRef:
738+
name: flux-gar-token
739+
resources:
740+
- apiVersion: image.toolkit.fluxcd.io/v1beta2
741+
kind: ImageRepository
742+
name: "*"
743+
matchLabels:
744+
registry: gar
745+
resourceFilter: 'request.body.tag.contains(resource.metadata.name)'
746+
```
747+
748+
If the body of the incoming hook looks like this:
749+
750+
```json
751+
{
752+
"action":"INSERT",
753+
"digest":"us-east1-docker.pkg.dev/my-project/my-repo/hello-world@sha256:6ec128e26cd5...",
754+
"tag":"us-east1-docker.pkg.dev/my-project/my-repo/hello-world:1.1"
755+
}
756+
```
757+
758+
This simple example would match `ImageRepositories` with the name `hello-world`.
759+
760+
If you want do do more complex processing:
761+
762+
```yaml
763+
resourceFilter: has(resource.metadata.annotations) && request.body.tag.split('/').last().split(":").first() == resource.metadata.annotations['update-image']
764+
```
765+
766+
This would look for an annotation "update-image" on the resource, and match it to the `hello-world` part of the tag name.
767+
768+
**NOTE**: Currently the `resource` value in the CEL expression only provides the object metadata, this means you can access things like `resource.metadata.labels` and `resource.metadata.annotations`.
769+
770+
There are a number of functions available to the CEL expressions beyond the basic CEL functionality.
771+
772+
The [Strings extension](https://github.com/google/cel-go/tree/master/ext#strings) is available.
773+
774+
In addition the notifications-controller CEL implementation provides the following functions:
775+
776+
#### first
777+
778+
Returns the first element of a CEL array expression.
779+
780+
```
781+
<list<any>>.first() -> <any>
782+
```
783+
784+
This is syntactic sugar for `['hello', 'mellow'][0]`
785+
786+
Examples:
787+
788+
```
789+
['hello', 'mellow'].first() // returns 'hello'
790+
[].first() // returns nil
791+
'this/test'.split('/').first() // returns 'this'
792+
```
793+
794+
#### last
795+
796+
Returns the last element of a CEL array expression.
797+
798+
```
799+
<list<any>>.last() -> <any>
800+
```
801+
802+
Examples:
803+
804+
```
805+
['hello', 'mellow'].last() // returns 'mellow'
806+
[].last() // returns nil
807+
'this/test'.split('/').last() // returns 'test'
808+
```
809+
810+
This is syntactic sugar for `['hello', 'mellow'][size(['hello, 'mellow'])-1]`
811+
812+
For zero-length array values, these will both return `nil`.
813+
703814
### Secret reference
704815
705816
`.spec.secretRef.name` is a required field to specify a name reference to a

internal/server/cel.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ func newCELEvaluator(expr string, req *http.Request) (resourcePredicate, error)
5353

5454
out, _, err := prg.Eval(map[string]any{
5555
"resource": data,
56-
"request": body,
56+
"request": map[string]any{
57+
"body": body,
58+
},
5759
})
5860
if err != nil {
5961
return nil, fmt.Errorf("expression %v failed to evaluate: %w", expr, err)
@@ -74,7 +76,6 @@ func makeCELEnv() (*cel.Env, error) {
7476
mapStrDyn := decls.NewMapType(decls.String, decls.Dyn)
7577
return cel.NewEnv(
7678
celext.Strings(),
77-
celext.Encoders(),
7879
notifications(),
7980
cel.Declarations(
8081
decls.NewVar("resource", mapStrDyn),

internal/server/receiver_handler_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,7 @@ func Test_handlePayload(t *testing.T) {
789789
},
790790
},
791791
},
792-
ResourceFilter: `has(resource.metadata.annotations) && request.tag.split('/').last().split(":").first() == resource.metadata.annotations['update-image']`,
792+
ResourceFilter: `has(resource.metadata.annotations) && request.body.tag.split('/').last().split(":").first() == resource.metadata.annotations['update-image']`,
793793
},
794794
Status: apiv1.ReceiverStatus{
795795
WebhookPath: apiv1.ReceiverWebhookPath,
@@ -878,7 +878,7 @@ func Test_handlePayload(t *testing.T) {
878878
Name: "test-resource",
879879
},
880880
},
881-
ResourceFilter: `has(resource.metadata.annotations) && request.tag.split('/').last().split(":").first() == resource.metadata.annotations['update-image']`,
881+
ResourceFilter: `has(resource.metadata.annotations) && request.body.tag.split('/').last().split(":").first() == resource.metadata.annotations['update-image']`,
882882
},
883883
Status: apiv1.ReceiverStatus{
884884
WebhookPath: apiv1.ReceiverWebhookPath,

0 commit comments

Comments
 (0)