Skip to content

Commit

Permalink
Implementing TriggerGroups as inline EventListener resource
Browse files Browse the repository at this point in the history
This feature allows an operator to specify a set of interceptors that will be executed
before a group of triggers are selected and executed. This allows common data
to be passed from interceptor execution down to multiple triggers to solve
a set of common use cases across multiple Triggers.

This feature is enabled for now inline in the EventListener spec, but in the future
may be enabled only in alpha once the feature gates proposal is implemented within
this project.

Addresses tektoncd#945
  • Loading branch information
jmcshane authored and tekton-robot committed Oct 12, 2021
1 parent ae9b7eb commit 9898086
Show file tree
Hide file tree
Showing 19 changed files with 525 additions and 184 deletions.
2 changes: 1 addition & 1 deletion cmd/triggerrun/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ func processTriggerSpec(kubeClient kubernetes.Interface, client triggersclientse

log := eventLog.With(zap.String(triggers.TriggerLabelKey, r.EventListenerName))

finalPayload, header, iresp, err := r.ExecuteInterceptors(*tri, request, body, log, eventID)
finalPayload, header, iresp, err := r.ExecuteTriggerInterceptors(*tri, request, body, log, eventID, map[string]interface{}{})
if err != nil {
log.Error(err)
return nil, err
Expand Down
85 changes: 85 additions & 0 deletions docs/eventlisteners.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ or more [`Interceptors`](./interceptors.md).
- [Structure of an `EventListener`](#structure-of-an-eventlistener)
- [Specifying the Kubernetes service account](#specifiying-the-kubernetes-service-account)
- [Specifying `Triggers`](#specifying-triggers)
- [Specifying `TriggerGroups`](#specifying-trigger-groups)
- [Specifying `Resources`](#specifying-resources)
- [Specifying a `kubernetesResource` object](#specifying-a-kubernetesresource-object)
- [Specifying `Replicas`](#specifying-replicas)
Expand Down Expand Up @@ -166,6 +167,90 @@ rules:
verbs: ["impersonate"]
```

## Specifying `TriggerGroups`

`TriggerGroups` is a feature that allows you to specify a set of interceptors that will process before a set of
`Trigger` resources are processed by the eventlistener. The goal of this feature is described in
[TEP-0053](https://github.com/tektoncd/community/blob/main/teps/0053-nested-triggers.md).TriggerGroups` allow for
a common set of interceptors to be defined inline in the `EventListenerSpec` before `Triggers` are invoked.

`TriggerGroups` is currently an `alpha` feature. To use it, you use use the v1beta1 API version with the
`enable-api-fields` [feature flag set to `alpha`](./install.md#Customizing-the-Triggers-Controller-behavior).

You can optionally specify one or more `Triggers` that define the actions to take when the `EventListener` detects a qualifying event. You can specify *either* a reference to an
external `Trigger` object *or* reference/define the `TriggerBindings`, `TriggerTemplates`, and `Interceptors` in the `Trigger` definition. A `TriggerGroup` definition specifies the following fields:

- `name` - (optional) a valid [Kubernetes name](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set) that uniquely identifies the `TriggerGroup`
- `interceptors` - a list of [`Interceptors`](#specifying-interceptors) that will process event payload data before passing it to the downstream `Triggers`
- `triggerSelector` - a combination of a Kubernetes `labelSelector` and a `namespaceSelector` as defined later [in this document](#constraining-eventlisteners-to-specific-namespaces). These two fields work together to define the `Triggers` that will be processed once `Interceptors` processing completes.

Below is an example EventListener that defines an inline `triggerGroup`:

```yaml
apiVersion: triggers.tekton.dev/v1beta1
kind: EventListener
metadata:
name: eventlistener
spec:
triggerGroups:
- name: github-pr-group
interceptors:
- name: "validate GitHub payload and filter on eventType"
ref:
name: "github"
params:
- name: "secretRef"
value:
secretName: github-secret
secretKey: secretToken
- name: "eventTypes"
value: ["pull_request"]
triggerSelector:
labelSelector:
matchLabels:
type: github-pr
```

This configuration would first process any event that is sent to the `EventListener` and determine if it matches
the outlined conditions. If it passes these conditions, it will use the `triggerSelector` matching criteria to determine
the target `Trigger` resources to continue processing.

Any `extensions` fields added during `triggerGroup` processing are passed to the downstream `Trigger` execution. This allows
for shared data across all Triggers that are processed after group execution completes. As an example, `extensions.myfield` would
be available to all `Trigger` resources matched by this group:

```yaml
apiVersion: triggers.tekton.dev/v1beta1
kind: EventListener
metadata:
name: eventlistener
spec:
triggerGroups:
- name: cel-filter-group
interceptors:
- name: "validate body and add field"
ref:
name: "cel"
params:
- name: "filter"
value: "body.action in ['opened', 'reopened']"
- name: "overlays"
value:
- key: myfield
expression: "body.pull_request.head.sha.truncate(7)"
triggerSelector:
namespaceSelector:
matchNames:
- foo
labelSelector:
matchLabels:
type: cel-preprocessed
```

At this time, each `TriggerGroup` determines its own downstream Triggers, so if two separate groups select the same
downstream `Trigger` resources, it may be executed multiple times. If you use this feature, ensure that `Trigger` resources
are labeled to be queried by the appropriate set of `TriggerGroups`.

## Specifying `Resources`

You can optionally customize the sink deployment for your `EventListener` using the `resources` field. It accepts the following types of objects:
Expand Down
22 changes: 22 additions & 0 deletions examples/v1beta1/triggergroups/eventlistener-triggergroup.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
apiVersion: triggers.tekton.dev/v1beta1
kind: EventListener
metadata:
name: listener-triggergroup
spec:
serviceAccountName: tekton-triggers-example-sa
triggerGroups:
- name: github-pr
interceptors:
- ref:
name: "cel"
params:
- name: "filter"
value: "header.match('X-GitHub-Event', 'pull_request')"
- name: "overlays"
value:
- key: truncated_sha
expression: "body.pull_request.head.sha.truncate(7)"
triggerSelector:
labelSelector:
matchLabels:
type: github-pr
1 change: 1 addition & 0 deletions examples/v1beta1/triggergroups/rbac.yaml
55 changes: 55 additions & 0 deletions examples/v1beta1/triggergroups/trigger.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
apiVersion: triggers.tekton.dev/v1beta1
kind: Trigger
metadata:
name: trigger
labels:
type: github-pr
spec:
bindings:
- name: gitrevision
value: $(extensions.truncated_sha)
- name: gitrepositoryurl
value: $(body.repository.url)
- name: contenttype
value: $(header.Content-Type)
template:
ref: pipeline-template
---
apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerTemplate
metadata:
name: pipeline-template
spec:
params:
- name: gitrevision
description: The git revision
default: main
- name: gitrepositoryurl
description: The git repository url
- name: message
description: The message to print
default: This is the default message
- name: contenttype
description: The Content-Type of the event
resourcetemplates:
- apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
generateName: simple-pipeline-run-
spec:
pipelineRef:
name: simple-pipeline
params:
- name: message
value: $(tt.params.message)
- name: contenttype
value: $(tt.params.contenttype)
resources:
- name: git-source
resourceSpec:
type: git
params:
- name: revision
value: $(tt.params.gitrevision)
- name: url
value: $(tt.params.gitrepositoryurl)
3 changes: 3 additions & 0 deletions pkg/apis/triggers/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ const (

// TriggerLabelKey is used as the label identifier for a Trigger
TriggerLabelKey = "/trigger"

// TriggerGroupLabelKey is used as a label identifier for a TriggerGroup
TriggerGroupLabelKey = "/triggergroup"
)
23 changes: 19 additions & 4 deletions pkg/apis/triggers/v1beta1/event_listener_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ var _ kmeta.OwnerRefable = (*EventListener)(nil)
type EventListenerSpec struct {
ServiceAccountName string `json:"serviceAccountName,omitempty"`
Triggers []EventListenerTrigger `json:"triggers"`
NamespaceSelector NamespaceSelector `json:"namespaceSelector,omitempty"`
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`
Resources Resources `json:"resources,omitempty"`
// Trigger groups allow for centralized processing of an interceptor chain
TriggerGroups []EventListenerTriggerGroup `json:"triggerGroups"`
NamespaceSelector NamespaceSelector `json:"namespaceSelector,omitempty"`
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`
Resources Resources `json:"resources,omitempty"`
}

type Resources struct {
Expand Down Expand Up @@ -112,6 +114,19 @@ type EventListenerTrigger struct {
ServiceAccountName string `json:"serviceAccountName,omitempty"`
}

// EventListenerTriggerGroup defines a group of Triggers that share a common set of interceptors
type EventListenerTriggerGroup struct {
Name string `json:"name"`
Interceptors []*TriggerInterceptor `json:"interceptors"`
TriggerSelector EventListenerTriggerSelector `json:"triggerSelector"`
}

// EventListenerTriggerSelector defines ways to select a group of triggers using their metadata
type EventListenerTriggerSelector struct {
NamespaceSelector NamespaceSelector `json:"namespaceSelector,omitempty"`
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`
}

// EventInterceptor provides a hook to intercept and pre-process events
type EventInterceptor = TriggerInterceptor

Expand All @@ -123,7 +138,7 @@ type SecretRef struct {
SecretName string `json:"secretName,omitempty"`
}

// EventListenerBinding refers to a particular TriggerBinding or ClusterTriggerBindingresource.
// EventListenerBinding refers to a particular TriggerBinding or ClusterTriggerBinding resource.
type EventListenerBinding = TriggerSpecBinding

// EventListenerTemplate refers to a particular TriggerTemplate resource.
Expand Down
14 changes: 14 additions & 0 deletions pkg/apis/triggers/v1beta1/event_listener_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,20 @@ func (s *EventListenerSpec) validate(ctx context.Context) (errs *apis.FieldError
if s.Resources.CustomResource != nil {
errs = errs.Also(validateCustomObject(s.Resources.CustomResource).ViaField("spec.resources.customResource"))
}

for i, group := range s.TriggerGroups {
errs = errs.Also(group.validate(ctx).ViaField(fmt.Sprintf("spec.triggerGroups[%d]", i)))
}
return errs
}

func (g *EventListenerTriggerGroup) validate(ctx context.Context) (errs *apis.FieldError) {
if g.TriggerSelector.LabelSelector == nil && len(g.TriggerSelector.NamespaceSelector.MatchNames) == 0 {
errs = errs.Also(apis.ErrMissingOneOf("triggerSelector.labelSelector", "triggerSelector.namespaceSelector"))
}
if len(g.Interceptors) == 0 {
errs = errs.Also(apis.ErrMissingField("interceptors"))
}
return errs
}

Expand Down
74 changes: 73 additions & 1 deletion pkg/apis/triggers/v1beta1/event_listener_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,35 @@ func Test_EventListenerValidate(t *testing.T) {
}},
},
},
}}
}, {
name: "Valid event listener with TriggerGroup and namespaceSelector",
el: &triggersv1beta1.EventListener{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
Spec: triggersv1beta1.EventListenerSpec{
TriggerGroups: []triggersv1beta1.EventListenerTriggerGroup{{
Name: "my-group",
Interceptors: []*triggersv1beta1.TriggerInterceptor{{
Ref: triggersv1beta1.InterceptorRef{
Name: "cel",
},
Params: []triggersv1beta1.InterceptorParams{{
Name: "filter",
Value: test.ToV1JSON(t, "has(body.repository)"),
}},
}},
TriggerSelector: triggersv1beta1.EventListenerTriggerSelector{
NamespaceSelector: triggersv1beta1.NamespaceSelector{
MatchNames: []string{
"foobar",
},
},
},
}},
},
}}}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
Expand Down Expand Up @@ -1160,6 +1188,50 @@ func TestEventListenerValidate_error(t *testing.T) {
},
},
wantErr: apis.ErrMultipleOneOf("spec.triggers[0].template or bindings or interceptors", "spec.triggers[0].triggerRef"),
}, {
name: "missing label and namespace selector",
el: &triggersv1beta1.EventListener{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
Spec: triggersv1beta1.EventListenerSpec{
TriggerGroups: []triggersv1beta1.EventListenerTriggerGroup{{
Name: "my-group",
Interceptors: []*triggersv1beta1.TriggerInterceptor{{
Ref: triggersv1beta1.InterceptorRef{
Name: "cel",
},
Params: []triggersv1beta1.InterceptorParams{{
Name: "filter",
Value: test.ToV1JSON(t, "has(body.repository)"),
}},
}},
}},
},
},
wantErr: apis.ErrMissingOneOf("spec.triggerGroups[0].triggerSelector.labelSelector", "spec.triggerGroups[0].triggerSelector.namespaceSelector"),
}, {
name: "triggerGroup requires interceptor",
el: &triggersv1beta1.EventListener{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
Spec: triggersv1beta1.EventListenerSpec{
TriggerGroups: []triggersv1beta1.EventListenerTriggerGroup{{
Name: "my-group",
TriggerSelector: triggersv1beta1.EventListenerTriggerSelector{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"foo": "bar",
},
},
},
}},
},
},
wantErr: apis.ErrMissingField("spec.triggerGroups[0].interceptors"),
}}

for _, tc := range tests {
Expand Down
Loading

0 comments on commit 9898086

Please sign in to comment.