Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 86 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,92 @@ GitHub Action and Docker image used to deploy a Docker stack on a Docker Swarm.
| `prune` | `PRUNE` | Prune services that are not defined in the stack file | | **0** |
| `stack_file` | `STACK_FILE` | Path to the stack file used in the deploy. | ✅ | |
| `stack_name` | `STACK_NAME` | Name of the stack to be deployed. | ✅ | |
| `stack_param` | `STACK_PARAM` | Additional parameter (env var) to be passed to the stack. | | |
| `env_file` | `ENV_FILE` | Additional environment variables to be passed to the stack. | | |
| `stack_param` | `STACK_PARAM` | A single additional value, available to the stack file as `${STACK_PARAM}`. Superseded by `env_file`, see [Passing values into the stack file](#passing-values-into-the-stack-file). | | |
| `env_file` | `ENV_FILE` | Additional environment variables **as content**, one `VAR=VALUE` per line. Mutually exclusive with `env_file_path`. | | |
| `env_file_path` | `ENV_FILE_PATH` | **Path** to a file of additional environment variables, one `VAR=VALUE` per line. Mutually exclusive with `env_file`. | | |
| `debug` | `DEBUG` | Verbose logging | | **0** |
| `scale_after` | `SCALE_AFTER` | Scale a service after a deployment has converged successfully. Example: servicename=1 | | |


## Passing values into the stack file

Your stack file can reference environment variables, and `docker stack deploy`
substitutes them as it reads the file. This action gives you three ways to set
those variables, all of which work through that same substitution.

### `env_file` — the variables themselves

Pass the content directly. Each line is a `VAR=VALUE` pair:

```yaml
- name: Deploy
uses: kitconcept/docker-stack-deploy@v1.4.0
with:
# ...
env_file: |
BACKEND_REPLICAS=2
FRONTEND_REPLICAS=3
SOLR_JAVA_MEM=-Xms1536m -Xmx1536m
```

```yaml
# stacks/plone.yml
services:
backend:
deploy:
replicas: ${BACKEND_REPLICAS}
frontend:
deploy:
replicas: ${FRONTEND_REPLICAS}
```

Values are taken **verbatim**, matching `docker --env-file`: a value may
contain spaces, and quotes around it become part of the value rather than
being stripped. So `GREETING="hello"` sets `GREETING` to `"hello"`, quotes
included.

Lines that are blank or start with `#` are ignored. Anything else that is not
`NAME=VALUE` is an error naming the offending line, rather than a silent
failure to export.

### `env_file_path` — a file to read them from

If the variables already live in a file in your repository, point at it
instead. Relative paths resolve against the workspace, the same as `stack_file`:

```yaml
with:
# ...
env_file_path: "stacks/production.env"
```

The file format and the verbatim semantics are identical to `env_file`. The two
inputs are mutually exclusive — supplying both is an error, because the
precedence between them would otherwise be arbitrary.

### `stack_param` — a single value (discouraged)

`stack_param` sets one variable, always named `STACK_PARAM`:

```yaml
with:
# ...
stack_param: "2"
```

```yaml
# stacks/plone.yml
services:
backend:
deploy:
replicas: ${STACK_PARAM}
```

It works, and it is kept for compatibility, but it is a one-variable special
case of `env_file` with a name you cannot choose. Prefer `env_file` or
`env_file_path` for anything new.


## Using the GitHub Action

Add, or edit an existing, `yaml` file inside `.github/actions` and use the configuration options listed above.
Expand All @@ -66,10 +146,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout codebase
uses: actions/checkout@v2
uses: actions/checkout@v7

- name: Deploy
uses: kitconcept/docker-stack-deploy@v1.0.1
uses: kitconcept/docker-stack-deploy@v1.4.0
with:
remote_host: ${{ secrets.REMOTE_HOST }}
remote_user: ${{ secrets.REMOTE_USER }}
Expand All @@ -96,10 +176,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout codebase
uses: actions/checkout@v2
uses: actions/checkout@v7

- name: Deploy
uses: kitconcept/docker-stack-deploy@v1.0.1
uses: kitconcept/docker-stack-deploy@v1.4.0
with:
registry: "ghcr.io"
username: ${{ secrets.GHCR_USERNAME }}
Expand All @@ -109,7 +189,6 @@ jobs:
remote_private_key: ${{ secrets.REMOTE_PRIVATE_KEY }}
stack_file: "stacks/plone.yml"
stack_name: "plone-live"
stack_param: "foo"
```

## Using the Docker Image
Expand Down
7 changes: 6 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ inputs:
required: false
default: ""
env_file:
description: "Additional environment variables in the format VAR_01=VALUE\nVAR_02=VALUE"
description: "Additional environment variables, as content, in the format VAR_01=VALUE\nVAR_02=VALUE. Mutually exclusive with env_file_path."
required: false
default: ""
env_file_path:
description: "Path to a file containing additional environment variables, one VAR=VALUE per line. Mutually exclusive with env_file."
required: false
default: ""
debug:
Expand Down Expand Up @@ -80,5 +84,6 @@ runs:
STACK_NAME: ${{ inputs.stack_name }}
STACK_PARAM: ${{ inputs.stack_param }}
ENV_FILE: ${{ inputs.env_file }}
ENV_FILE_PATH: ${{ inputs.env_file_path }}
DEBUG: ${{ inputs.debug }}
SCALE_AFTER: ${{ inputs.scale_after }}
1 change: 1 addition & 0 deletions news/2.documentation
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Documented how values reach the stack file, with worked examples for `env_file`, `env_file_path` and `stack_param`, and noted that `stack_param` is a one-variable special case of `env_file`. @ericof
1 change: 1 addition & 0 deletions news/3.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added an `env_file_path` input, which reads the additional environment variables from a file instead of taking them as content. `env_file` keeps its current meaning, so nothing breaks; supplying both is an error. @ericof
48 changes: 40 additions & 8 deletions scripts/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ set -e
SSH_DIR="/root/.ssh"
SSH_KEY="${SSH_DIR}/docker"
KNOWN_HOSTS="${SSH_DIR}/known_hosts"
ENV_FILE_PATH="/root/.env"
# Where the `env_file` input's content is materialised before being parsed.
# Distinct from the ENV_FILE_PATH input, which is a path supplied by the user.
ENV_FILE_DEST="/root/.env"

OPTS=("--with-registry-auth" "--resolve-image=${RESOLVE_IMAGE:-always}")
[ "${PRUNE:-0}" = "1" ] && OPTS+=("--prune")
Expand All @@ -32,9 +34,10 @@ configure_ssh_key() {
ssh-add "${SSH_KEY}"
}

configure_env_file() {
printf '%s' "$ENV_FILE" > "${ENV_FILE_PATH}"
env_file_len=$(grep -cv -e '^#' -e '^$' "${ENV_FILE_PATH}" || true)
# Parse a NAME=VALUE file and export every entry. $1 is the file to read.
load_env_file() {
local source_file="$1" env_file_len line
env_file_len=$(grep -cv -e '^#' -e '^$' "${source_file}" || true)
if [[ ${env_file_len} -gt 0 ]]; then
echo "Environment Variables: Additional values"
if [ "${DEBUG}" != "0" ]; then
Expand All @@ -47,8 +50,9 @@ configure_env_file() {
# Values are taken verbatim, matching `docker --env-file`: quotes in the
# file are part of the value, not delimiters around it.
#
# ENV_FILE has no trailing newline, so the `-n` test is what keeps the
# final line from being dropped by read's non-zero exit.
# The ENV_FILE input has no trailing newline, so the `-n` test is what
# keeps the final line from being dropped by read's non-zero exit. A file
# read through env_file_path usually does end in a newline; both work.
while IFS= read -r line || [ -n "${line}" ]; do
case "${line}" in
''|\#*) continue ;;
Expand All @@ -58,13 +62,32 @@ configure_env_file() {
exit 1
fi
export "${line?}"
done < "${ENV_FILE_PATH}"
done < "${source_file}"
if [ "${DEBUG}" != "0" ]; then
echo "Environment vars after: $(env|wc -l)"
fi
fi
}

# The env_file input carries the variables themselves, so materialise them
# before parsing.
configure_env_file() {
printf '%s' "$ENV_FILE" > "${ENV_FILE_DEST}"
load_env_file "${ENV_FILE_DEST}"
}

# The env_file_path input names a file to read instead. Relative paths resolve
# against the workspace, which is the working directory of a docker action --
# the same way the stack_file input works.
configure_env_file_path() {
if [ ! -f "${ENV_FILE_PATH}" ]; then
echo "${ENV_FILE_PATH} does not exist."
exit 1
fi
echo "Environment Variables: Reading ${ENV_FILE_PATH}"
load_env_file "${ENV_FILE_PATH}"
}

configure_ssh_host() {
ssh-keyscan -p "${REMOTE_PORT}" "${REMOTE_HOST}" > "${KNOWN_HOSTS}"
chmod 600 "${KNOWN_HOSTS}"
Expand Down Expand Up @@ -110,7 +133,16 @@ fi
[ -z ${DEBUG+x} ] && export DEBUG="0"

# ADDITIONAL ENV VARIABLES
if [[ -z "${ENV_FILE}" ]]; then
# env_file carries the variables themselves; env_file_path names a file to read
# them from. Accepting both at once would leave the precedence undefined, so it
# is rejected rather than guessed at.
if [[ -n "${ENV_FILE}" ]] && [[ -n "${ENV_FILE_PATH}" ]]; then
echo "Inputs env_file and env_file_path are mutually exclusive!"
exit 1
fi
if [[ -n "${ENV_FILE_PATH}" ]]; then
configure_env_file_path;
elif [[ -z "${ENV_FILE}" ]]; then
export ENV_FILE=""
else
configure_env_file;
Expand Down
117 changes: 114 additions & 3 deletions tests/entrypoint.bats
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export_from_env_file() {
ENV_FILE="${body}" WANT="${var}" \
bash -c "
source '${ENTRYPOINT}'
ENV_FILE_PATH=\"\${HOME}/.env\"
ENV_FILE_DEST=\"\${HOME}/.env\"
configure_env_file >/dev/null 2>&1
printf '%s' \"\${!WANT}\"
"
Expand Down Expand Up @@ -187,7 +187,7 @@ export_from_env_file() {
run env -i PATH="${PATH}" HOME="${BATS_TEST_TMPDIR}" ENV_FILE=".env" \
bash -c "
source '${ENTRYPOINT}'
ENV_FILE_PATH=\"\${HOME}/.env\"
ENV_FILE_DEST=\"\${HOME}/.env\"
configure_env_file
"
[ "$status" -eq 1 ]
Expand All @@ -198,13 +198,124 @@ export_from_env_file() {
run env -i PATH="${PATH}" HOME="${BATS_TEST_TMPDIR}" ENV_FILE="MY-VAR=1" \
bash -c "
source '${ENTRYPOINT}'
ENV_FILE_PATH=\"\${HOME}/.env\"
ENV_FILE_DEST=\"\${HOME}/.env\"
configure_env_file
"
[ "$status" -eq 1 ]
[[ "$output" == *"is not in NAME=VALUE format"* ]]
}

# --- env_file_path (issue #3) -------------------------------------------------

# Write a file with the given content, run configure_env_file_path against it,
# and print the resulting value of one variable.
#
# The value is bracketed because bats strips trailing newlines from $output,
# which would hide a value that wrongly kept one.
export_from_env_file_path() {
local body="$1" var="$2"
local file="${BATS_TEST_TMPDIR}/vars.env"
printf '%s\n' "${body}" > "${file}"
run env -i PATH="${PATH}" HOME="${BATS_TEST_TMPDIR}" \
ENV_FILE_PATH="${file}" WANT="${var}" \
bash -c "
source '${ENTRYPOINT}'
configure_env_file_path >/dev/null 2>&1
printf '[%s]' \"\${!WANT}\"
"
}

@test "env_file_path exports a value read from a file (issue #3)" {
export_from_env_file_path 'DB_USER=plone' DB_USER
[ "$status" -eq 0 ]
[ "$output" = "[plone]" ]
}

@test "env_file_path exports every entry in the file (issue #3)" {
export_from_env_file_path $'DB_USER=plone\nDB_NAME=site' DB_NAME
[ "$status" -eq 0 ]
[ "$output" = "[site]" ]
}

@test "env_file_path ignores comments and blank lines (issue #3)" {
export_from_env_file_path $'# a comment\n\nDB_USER=plone' DB_USER
[ "$status" -eq 0 ]
[ "$output" = "[plone]" ]
}

@test "env_file_path takes values verbatim, like env_file (issue #3)" {
# The semantics must not diverge between the two inputs: quotes are part of
# the value, matching `docker --env-file`.
export_from_env_file_path 'GREETING="hello world"' GREETING
[ "$status" -eq 0 ]
[ "$output" = '["hello world"]' ]
}

@test "env_file_path keeps a value containing spaces (issue #3)" {
export_from_env_file_path 'SOLR_JAVA_MEM=-Xms1536m -Xmx1536m' SOLR_JAVA_MEM
[ "$status" -eq 0 ]
[ "$output" = "[-Xms1536m -Xmx1536m]" ]
}

@test "env_file_path does not drop the last line of the file (issue #3)" {
# A real file ends in a newline, unlike the env_file input; the read loop
# has to handle both without losing or duplicating the final entry.
export_from_env_file_path $'DB_USER=plone\nDB_NAME=site\nLAST=here' LAST
[ "$status" -eq 0 ]
[ "$output" = "[here]" ]
}

@test "env_file_path rejects a malformed line (issue #3)" {
printf '%s\n' 'not-a-pair' > "${BATS_TEST_TMPDIR}/bad.env"
run env -i PATH="${PATH}" HOME="${BATS_TEST_TMPDIR}" \
ENV_FILE_PATH="${BATS_TEST_TMPDIR}/bad.env" \
bash -c "source '${ENTRYPOINT}'; configure_env_file_path"
[ "$status" -eq 1 ]
[[ "$output" == *"'not-a-pair' is not in NAME=VALUE format"* ]]
}

@test "env_file_path says which file it read (issue #3)" {
printf '%s\n' 'DB_USER=plone' > "${BATS_TEST_TMPDIR}/vars.env"
run env -i PATH="${PATH}" HOME="${BATS_TEST_TMPDIR}" \
ENV_FILE_PATH="${BATS_TEST_TMPDIR}/vars.env" \
bash -c "source '${ENTRYPOINT}'; configure_env_file_path"
[ "$status" -eq 0 ]
[[ "$output" == *"Environment Variables: Reading ${BATS_TEST_TMPDIR}/vars.env"* ]]
}

@test "env_file_path fails when the file does not exist (issue #3)" {
run_entrypoint ENV_FILE_PATH=/nope/missing.env
[ "$status" -eq 1 ]
[[ "$output" == *"/nope/missing.env does not exist."* ]]
}

@test "env_file and env_file_path together are rejected (issue #3)" {
printf '%s\n' 'DB_USER=plone' > "${BATS_TEST_TMPDIR}/vars.env"
run_entrypoint ENV_FILE="DB_NAME=site" \
ENV_FILE_PATH="${BATS_TEST_TMPDIR}/vars.env"
[ "$status" -eq 1 ]
[[ "$output" == *"env_file and env_file_path are mutually exclusive"* ]]
}

@test "env_file_path is read as part of the deploy flow (issue #3)" {
# The tests above call configure_env_file_path directly; this one proves the
# input is actually wired into the script's flow.
printf '%s\n' 'DB_USER=plone' > "${BATS_TEST_TMPDIR}/vars.env"
run_entrypoint ENV_FILE_PATH="${BATS_TEST_TMPDIR}/vars.env"
[ "$status" -eq 1 ]
[[ "$output" == *"Environment Variables: Reading ${BATS_TEST_TMPDIR}/vars.env"* ]]
[[ "$output" == *"Input remote_host is required!"* ]]
}

@test "env_file still works when env_file_path is unset (issue #3)" {
# Backwards compatibility is the whole point of adding an input rather than
# changing the meaning of the existing one.
run_entrypoint ENV_FILE="DB_USER=plone"
[ "$status" -eq 1 ]
[[ "$output" == *"Environment Variables: Additional values"* ]]
[[ "$output" == *"Input remote_host is required!"* ]]
}

# --- scale_after --------------------------------------------------------------

@test "scale_after does nothing when unset" {
Expand Down
Loading