2023-06-14 21:39:57 +00:00
---
layout: post
title: Sending Apprise Notifications from Concourse CI
date: 2023-06-14 23:39:00 Europe/Amsterdam
categories: concourse apprise
---
Recently, I deployed [Concourse CI ](https://concourse-ci.org/ ) 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.
2023-07-05 21:39:07 +00:00
This took some time getting right, but the result works like a charm ([source code](https://git.kun.is/pim/static)).
2023-06-14 21:39:57 +00:00
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 ](https://github.com/caronc/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 ](https://github.com/mockersf/concourse-slack-notifier ) 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 resource
- `in` : retrieve a version of the resource
- `out` : 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
2023-07-05 21:39:07 +00:00
The whole script can be found [here ](https://git.kun.is/pim/concourse-apprise-notifier/src/branch/master/out ), but I will explain the most important bits of it.
2023-06-14 21:39:57 +00:00
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:
```bash
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:
```bash
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.
```bash
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.
```bash
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:
```bash
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.
```bash
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` :
```bash
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.
```bash
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.
```bash
echo "${timestamp}" >& 3
```
# Building the Container
As said earlier, to actually use this script, we need to add it to a image.
2023-07-05 21:39:07 +00:00
I won't be explaining this whole process, but the source can be found [here ](https://git.kun.is/pim/concourse-apprise-notifier/src/branch/master/pipeline.yml ).
2023-06-14 21:39:57 +00:00
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.
2023-07-05 21:39:07 +00:00
I use it for the blog you are reading right now and the pipeline definition can be found [here ](https://git.kun.is/pim/static/src/branch/main/pipeline.yml ).
2023-06-14 21:39:57 +00:00
Here we specify the resource type in a Concourse pipeline:
```yaml
resource_types:
- name: apprise
type: registry-image
source:
2023-07-05 21:39:07 +00:00
repository: git.kun.is/pim/concourse-apprise-notifier
2023-06-14 21:39:57 +00:00
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:
```yaml
resources:
- name: apprise-notification
type: apprise
source:
2023-07-12 17:59:07 +00:00
host: https://apprise.kun.is:444
2023-06-14 21:39:57 +00:00
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:
```yaml
- 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:
![picture showing my Ntfy app with the Apprise notification ](ntfy.png )
And to finish this off, here is what it looks like in the Concourse web UI:
![the concourse web gui showing the pipeline of my static website including the the apprise notification resources ](pipeline.png )
# 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.