Slack slash commands to Tekton
EDIT 2020-07-03 The CEL syntax for splitting strings changed in Triggers 0.5.0, the examples here don’t reflect those changes.
Slack “slash commands” are an easy way to communicate from Slack to services.
Simply, you can add a command to a Slack app, and trigger HTTP POST requests from Slack to an endpoint of your choosing.
These are sent as HTTP POST requests, with URL form-encoding of the data.
Tekton Triggers provide EventListeners which can receive and act on webhooks, to drive TaskRuns and PipelineRuns.
Unfortunately, EventListeners only support JSON bodies, and Slack commands are form-encoded.
The params are documented here
The example looks like this:
token=gIkuvaNzQIHg97ATvDxqgjtO
&team_id=T0001
&team_domain=example
&enterprise_id=E0001
&enterprise_name=Globular%20Construct%20Inc
&channel_id=C2147483705
&channel_name=test
&user_id=U2147483697
&user_name=Steve
&command=/weather
&text=94070
&response_url=https://hooks.slack.com/commands/1234/5678
&trigger_id=13345224609.738474920.8088930838d88f008e0
You can see that there’s a “command” and “text” field that contain the command and the text after the command from the user, along with other fields.
How can you connect a Slack command to a Tekton Pipeline?
Introducing…
slack-webhook-interceptor
I’ve implemented a simple Webhook interceptor for Tekton here.
This is a simple introduction, to getting a Task executing (but, it’d be trivial to adapt for Pipelines).
Simple Task
Here’s a simple script driven task, for now it just echoes the provided params:
slack-webhook-task.yaml
apiVersion: tekton.dev/v1alpha1
kind: Task
metadata:
name: slack-webhook-task
spec:
inputs:
params:
- name: command
description: the command from Slack
- name: repo
description: the repo to source
- name: sha
description: the specific SHA in the repo
steps:
- image: registry.access.redhat.com/ubi8/ubi-minimal
script: |
#!/usr/bin/env bash
echo $(inputs.params.command) $(inputs.params.repo) $(inputs.params.sha)
Trigger Template Binding
We want to bind to the following parameters: command
, repo
and sha
.
task-run-binding.yaml
apiVersion: tekton.dev/v1alpha1
kind: TriggerBinding
metadata:
name: task-run-binding
spec:
params:
- name: command
value: $(body.slack.command)
- name: repo
value: $(body.slack.repo)
- name: sha
value: $(body.slack.sha)
Notice that these are coming in a JSON body that should be formatted like this:
{
"slack": {
"command": "/build",
"repo": "https://github.com/bigkevmcd/slack-webhook-interceptor",
"sha": "ca82a6dff817ec66f44342007202690a93763949"
}
}
Trigger Template
For this example, I just want to execute the Task defined above:
task-run-template.yaml
apiVersion: tekton.dev/v1alpha1
kind: TriggerTemplate
metadata:
name: task-run-template
spec:
params:
- name: command
description: The command from Slack
- name: repo
description: The repo to process.
- name: sha
description: The specific commit (SHA) to process.
resourcetemplates:
- apiVersion: tekton.dev/v1alpha1
kind: TaskRun
metadata:
generateName: slack-task-run-
spec:
taskRef:
name: slack-webhook-task
inputs:
params:
- name: command
value: $(params.command)
- name: repo
value: $(params.repo)
- name: sha
value: $(params.sha)
EventListener
And here comes the Slack specific section:
listener-interceptor.yaml
apiVersion: tekton.dev/v1alpha1
kind: EventListener
metadata:
name: listener-interceptor
spec:
triggers:
- name: foo-trig
interceptors:
- webhook:
header:
- name: Slack-Decodeprefix
value: slack
objectRef:
kind: Service
name: slack-webhook-interceptor
apiVersion: v1
namespace: default
- cel:
filter: body.slack.command == '/build'
overlays:
- key: slack.repo
expression: "split(body.slack.text, ' ')[0]"
- key: slack.sha
expression: "split(body.slack.text, ' ')[1]"
- key: slack.command
expression: "split(body.slack.command, '/')[1]"
bindings:
- name: task-run-binding
template:
name: task-run-template
This is using a Webhook interceptor to convert the incoming form-encoded data to JSON.
The Webhook interceptor is sending an additional header, Slack-Decodeprefix: slack
which configures the top-level object key to store the form parameters in.
Form values can be duplicated in a request, and all values should be retained, this is handled by the Go url.Values as type Values map[string][]string
.
By default, the interceptor flattens these values resulting in squashing the results into non-arrays in the response, this is useful if you know that you’ll only get one value for keys as it shortens access to the values, instead of body.slack.command[0] == '/build'
I can write body.slack.command == '/build'
.
You can disable this with SlackDecodenoflatten: true
if you need multiple values in the response body.
This in combination with the CEL filter results in triggering the Task if the command is /build
, you can point multiple “slash commands” at the same EventListener and match on the correct command, caveat: doing this will trigger multiple requests to the interceptor.
The CEL interceptor uses two new features introduced in Tekton Triggers v0.3.0, overlays and the new split
function.
The Webhook interceptor
Finally, you’ll need a running version of the webhook interceptor.
apiVersion: apps/v1
kind: Deployment
metadata:
name: slack-webhook-interceptor
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: slack-webhook-interceptor
template:
metadata:
labels:
app.kubernetes.io/name: slack-webhook-interceptor
spec:
serviceAccountName: default
containers:
- name: slack-webhook-interceptor
image: PATH_TO_IMAGE
imagePullPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
name: slack-webhook-interceptor
namespace: default
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: slack-webhook-interceptor
ports:
- protocol: TCP
port: 80
targetPort: 8080
Replace PATH_TO_IMAGE
with the Quay.io/Docker Hub/ECR reference for a build of the image.
Bring up your environment
Save the files above into a directory (or use the ones in https://github.com/bigkevmcd/slack-webhook-interceptor/tree/master/example) and apply them to your Kubernetes cluster with Tekton installed
Ensure that the pods are all up and running before continuing.
Creating a Slash command
To create a new App in Slack, you need to go to https://api.slack.com/apps?new_app=1 in a workspace you have permission to create new apps.
For obvious reasons, you might not have permission in all workspaces, so you’ll want to use a workspace you can add apps to.
From here, you want to add a new Slash command:
Click on “Slash commands” on the App page:
And then click to create a new command:
For this example, I’m creating a simple slash command “/build” which I’ll provide a repo and sha for.
You’ll need to put an Internet accessible URL into the “Request URL” field, if you’re just testing this locally, you can port-forward and ngrok:
kubectl port-forward svc/el-listener-interceptor 8080 # run this in one console
ngrok http 8080 # run this in a second console
You can then use the ngrok.io
address provided by ngrok.
Triggering the command from Slack
When the command executes, it will output the result of triggering the execution.
Part of the payload of the Slash command has a URL that can be used to send a response back with better information.
Viewing the results of triggering the command
With the tkn tool, it’s fairly simple to track the TaskRun status:
$ tkn taskrun list
NAME STARTED DURATION STATUS
slack-task-run-skc65 15 seconds ago 13 seconds Succeeded
And simply viewing the logs shows that it’s doing the right thing.
$ tkn taskrun logs slack-task-run-skc65
[unnamed-0] {"level":"info","ts":1583136445.1732686,"logger":"fallback-logger","caller":"logging/config.go:69","msg":"Fetch GitHub commit ID from kodata failed: \"KO_DATA_PATH\" does not exist or is empty"}
[unnamed-0] build https://github.com/bigkevmcd/slack-webhook-interceptor 57112af9d9419f97e46731c9f0768b7988baa3ff
The simple demo task just outputs the command, repo and SHA separated by spaces.
You can see the benefits of the split
function from the CEL interceptor for manipulating the strings from the incoming post, it’s splitting the repo
and sha
apart, and trimming the leading /
from the command too.
Alternatives - Slack apps
This uses Slack “slash-commands”, but Slack has introduced a newer mechanism called Slack Apps, these can also drive Triggers, but slash commands are quick and easy to setup.
Sending responses back to Slack
I’ll cover sending responses back to Slack from a TaskRun in a subsequent post.