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 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

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: picture showing my Ntfy app with the Apprise notification

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

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.