Sending Apprise Notifications from Concourse CI
Recently, I deployed Concourse CI because I wanted to get my feet wet with a CI/CD pipeline. However, I had a practical use case lying around for a long time: automatically compiling my static website and deploying it to my docker Swarm. This took some time getting right, but the result works like a charm (source code).
It’s comforting to know I don’t have move a finger and my website is automatically deployed. However, I would still like to receive some indication of what’s happening. And what’s a better way to do that, than using my Apprise service to keep me up to date. There’s a little snag though: I could not find any Concourse resource that does this. That’s when I decided to just create it myself.
The Plagiarism Hunt
As any good computer person, I am lazy. I’d rather just copy someone’s work, so that’s what I did. I found this GitHub repository that does the same thing but for Slack notifications. For some reason it’s archived, but it seemed like it should work. I actually noticed lots of repositories for Concourse resource types are archived, so not sure what’s going on there.
Getting to know Concourse
Let’s first understand what we need to do reach our end goal of sending Apprise notifications from Concourse.
A Concourse pipeline takes some inputs, performs some operations on them which result in some outputs. These inputs and outputs are called resources in Concourse. For example, a Git repository could be a resource. Each resource is an instance of a resource type. A resource type therefore is simply a blueprint that can create multiple resources. To continue the example, a resource type could be “Git repository”.
We therefore need to create our own resource type that can send Apprise notifications. A resource type is simply a container that includes three scripts:
check
: check for a new version of a resourcein
: retrieve a version of the resourceout
: create a version of the resource
As Apprise notifications are basically fire-and-forget, we will only implement the out
script.
Writing the out
script
The whole script can be found here, but I will explain the most important bits of it. Note that I only use Apprise’s persistent storage solution, and not its stateless solution.
Concourse provides us with the working directory, which we cd
to:
cd "${1}"
We create a timestamp, formatted in JSON, which we will use for the resource’s new version later. Concourse requires us to set a version for the resource, but since Apprise notifications don’t have that, we use the timestamp:
timestamp="$(jq -n "{version:{timestamp:\"$(date +%s)\"}}")"
First some black magic Bash to redirect file descriptors. Not sure why this is needed, but I copied it anyways. After that, we create a temporary file holding resource’s parameters.
exec 3>&1
exec 1>&2
payload=$(mktemp /tmp/resource-in.XXXXXX)
cat > "${payload}" <&0
We then extract the individual parameters. The source
key contains values how the resource type was specified, while the params
key specifies parameters for this specific resource.
apprise_host="$(jq -r '.source.host' < "${payload}")"
apprise_key="$(jq -r '.source.key' < "${payload}")"
alert_body="$(jq -r '.params.body' < "${payload}")"
alert_title="$(jq -r '.params.title // null' < "${payload}")"
alert_type="$(jq -r '.params.type // null' < "${payload}")"
alert_tag="$(jq -r '.params.tag // null' < "${payload}")"
alert_format="$(jq -r '.params.format // null' < "${payload}")"
We then format the different parameters using JSON:
alert_body="$(eval "printf \"${alert_body}\"" | jq -R -s .)"
[ "${alert_title}" != "null" ] && alert_title="$(eval "printf \"${alert_title}\"" | jq -R -s .)"
[ "${alert_type}" != "null" ] && alert_type="$(eval "printf \"${alert_type}\"" | jq -R -s .)"
[ "${alert_tag}" != "null" ] && alert_tag="$(eval "printf \"${alert_tag}\"" | jq -R -s .)"
[ "${alert_format}" != "null" ] && alert_format="$(eval "printf \"${alert_format}\"" | jq -R -s .)"
Next, from the individual parameters we construct the final JSON message body we send to the Apprise endpoint.
body="$(cat <<EOF
{
"body": ${alert_body},
"title": ${alert_title},
"type": ${alert_type},
"tag": ${alert_tag},
"format": ${alert_format}
}
EOF
)"
Before sending it just yet, we compact the JSON and remove any values that are null
:
compact_body="$(echo "${body}" | jq -c '.')"
echo "$compact_body" | jq 'del(..|nulls)' > /tmp/compact_body.json
Here is the most important line, where we send the payload to the Apprise endpoint. It’s quite straight-forward.
curl -v -X POST -T /tmp/compact_body.json -H "Content-Type: application/json" "${apprise_host}/notify/${apprise_key}"
Finally, we print the timestamp (fake version) in order to appease the Concourse gods.
echo "${timestamp}" >&3
Building the Container
As said earlier, to actually use this script, we need to add it to a image. I won’t be explaining this whole process, but the source can be found here. The most important take-aways are these:
- Use
concourse/oci-build-task
to build a image from a Dockerfile. - Use
registry-image
to push the image to an image registry.
Using the Resource Type
Using our newly created resource type is surprisingly simple. I use it for the blog you are reading right now and the pipeline definition can be found here. Here we specify the resource type in a Concourse pipeline:
resource_types:
- name: apprise
type: registry-image
source:
repository: git.kun.is/pim/concourse-apprise-notifier
tag: "1.1.1"
We simply have to tell Concourse where to find the image, and which tag we want. Next, we instantiate the resource type to create a resource:
resources:
- name: apprise-notification
type: apprise
source:
host: https://apprise.kun.is:444
key: concourse
icon: bell
We simply specify the host to send Apprise notifications to. Yeah, I even gave it a little bell because it’s cute.
All that’s left to do, is actually send the notification. Let’s see how that is done:
- name: deploy-static-website
plan:
- task: deploy-site
config: ...
on_success:
put: apprise-notification
params:
title: "Static website deployed!"
body: "New version: $(cat version/version)"
no_get: true
As can be seen, the Apprise notification can be triggered when a task is executed successfully. We do this using the put
command, which execute the out
script underwater. We set the notification’s title and body, and send it! The result is seen below in my Ntfy app, which Apprise forwards the message to:
And to finish this off, here is what it looks like in the Concourse web UI:
Conclusion
Concourse’s way of representing everything as an image/container is really interesting in my opinion. A resource type is quite easily implemented as well, although Bash might not be the optimal way to do this. I’ve seen some people implement it in Rust, which might be a good excuse to finally learn that language :)
Apart from Apprise notifications, I’m planning on creating a resource type to deploy to a Docker swarm eventually. This seems like a lot harder than simply sending notifications though.