Writing Concourse resource-types in bash can be fairly fast, especially if you’re relying on external tools to do some of the heavy lifting for you. I think a lot of us in this space are also use to do writing up quick bash scripts to do things, so writing up a quick and dirty resource-type in bash feels natural. Here are some tips for writing resource-types in bash.

I’m not going to explain how resource-types work in Concourse. I’ll instead refer you to the documentation: resource-types

1. Redirecting stdout and stderr

The top of all of my resource-type scripts start with this; no exceptions:

#!/usr/bin/env bash

set -euo pipefail

exec 3>&1 # make stdout available as file descriptor 3 for the result
exec 1>&2 # redirect all output to stderr for logging

The first two lines are standard “this is how all your bash scripts should start” stuff. I won’t explain why you should use them here. The last two lines starting with exec are the Concourse-specific bits. I’ll explain what those do.

Concourse expects the information for a version generated by the resource to come from stdout. We need to lock down output to stdout then. We do this by first mapping file descriptor (fd) 3 to 1. This means when we write to fd 3 we’ll actually write to stdout. Then to stop every other executable from writing to stdout by default within our shell session, we remap fd 1 to 2 which is stderr. Whenever something writes to fd 1 it’ll actually be written to fd 2, the stderr file.

2. Reading Input From Concourse

Concourse can pass information on stdin to your script. Here’s how I capture and parse this information. This part requires jq since Concourse passes in data as JSON.

payload="$(cat <&0)"

my_var="$(jq -r '.source.my_var // ""' <<< "$payload")"
foobar="$(jq -r '.source.foobar // ""' <<< "$payload")"

3. Logging Errors

Logging errors in your script is easy now after redirecting fd 1 to 2. Simple call echo with your error and exit 1. Exiting with exit code 1 is how you tell Concourse that something went wrong. It’s also standard behaviour in Unix in general.

echo "The really bad thing happened!"
exit 1

I commonly error out if some inputs to the script are not set.

if [[ -z "${my_var}" ]]; then
    echo "my_var is not set and is required"
    exit 1
fi

4. Capturing the input/output Paths

Only applies to the /opt/resource/in and /opt/resource/out scripts.

Depending on what your resource-type does, these may be completely irrelevant to you and you can ignore them.

Both the in and out scripts are passed a file path as the first command line argument. For the in script, this is a file path where it can store artifacts that later steps in a job can use.

For the out script, this is where artifacts from previous steps are usually passed in.

For /opt/resource/in I usually write:

output_dir="${1}"

For /opt/resource/out I usually write:

inputs_dir="${1}"

To avoid worrying about if there’s a trailing slash or not I always add a slash after the variable like so:

echo "hello" > "${output_dir}/world"

This works because having two forward slashes // in a path is the same as having one forward slash. So /tmp/world and /tmp//world are equivalent.

5. Outputting Versions with jq

For the following examples, $version would be set to a json object of key-value pairs, such as:

version='{"id": "abc123"}'

The tldr of this tip is that --argjson is your best friend for crafting json responses

To output a new version in a /opt/resource/check script I’ll use the following jq:

jq -n \
--argjson version $version "[$version]" >&3

For the /opt/resource/in script I’ll get the version passed in on stdin and re-use it:

version=$(jq -r '.version // ""' <<< "$payload")

# rest of the script here

jq -n \
--argjson version $version \
'{
version: $version,
metadata: []
}' >&3

It’s the same for /opt/resource/out, except $version will come from a file or the put steps params:

jq -n \
--argjson version $version \
'{
version: $version,
metadata: []
}' >&3

6. Packaging the Resource-type

Not much of a tip, but here’s the Dockerfile that I use for all my bash-based resource-types. I use a custom BASE_IMAGE that has jq and bash installed.

ARG BASE_IMAGE
FROM ${BASE_IMAGE}

RUN mkdir -p /opt/resource
COPY check.sh /opt/resource/check
COPY in.sh /opt/resource/in
COPY out.sh /opt/resource/out