Files
utility-scripts/update_docker_images.sh
T

128 lines
4.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Usage: ./update_docker_images.sh [--prune] [--build] [compose-directory]
# By default, pull non-buildable images and update services without pruning or building.
# Continue after project failures; report them and exit nonzero at the end.
# WARNING: Pruning removes stopped containers, unused images, build cache,
# and unused anonymous volumes.
set -euo pipefail
usage() {
printf 'Usage: %s [--prune] [--build] [compose-directory]\n' "$0"
printf '%s\n' \
' --prune Remove unused Docker resources before updating (including volumes).' \
' --build Build images with --pull before updating services.' \
' -h, --help Show this help.' \
'Default: pull images for services without build definitions and update services.' \
'Services with build definitions keep their local images unless --build is used.' \
'Project failures are reported at the end; other projects are still updated.'
}
prune=false
build=false
positional=()
while (( $# )); do
case "$1" in
--prune) prune=true ;;
--build) build=true ;;
-h|--help) usage; exit 0 ;;
--) shift; positional+=("$@"); break ;;
-*) printf 'Unknown option: %s\n' "$1" >&2; usage >&2; exit 2 ;;
*) positional+=("$1") ;;
esac
shift
done
if (( ${#positional[@]} > 1 )); then
usage >&2
exit 2
fi
compose_root=${positional[0]:-/etc/docker/compose}
if [[ ! -d "$compose_root" ]]; then
printf 'Compose directory not found: %s\n' "$compose_root" >&2
exit 1
fi
created_builder=
cleanup() {
local status=$?
trap - EXIT
if [[ -n "$created_builder" ]]; then
if ! docker buildx rm "$created_builder"; then
printf 'Could not remove builder: %s\n' "$created_builder" >&2
if (( status == 0 )); then
status=1
fi
fi
fi
exit "$status"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
if "$prune"; then
docker system df
docker system prune -a --volumes -f
docker system df
fi
if "$build"; then
# Select an existing container builder, not merely check whether one exists.
# Capture the list separately so a failed Docker command stops the script.
# DriverEndpoint contains the driver for builders and the endpoint for nodes.
builders=$(docker buildx ls --format '{{.Name}} {{.DriverEndpoint}}')
builder=
while read -r name driver; do
if [[ "$driver" == docker-container ]]; then
builder=$name
break
fi
done <<< "$builders"
if [[ -z "$builder" ]]; then
# Bootstrap separately so cleanup can run if startup fails.
created_builder=$(docker buildx create \
--buildkitd-config /etc/docker/buildx/buildkit.toml \
--driver docker-container \
--driver-opt 'network=treehouse')
builder=$created_builder
fi
# Select the builder for this script without changing the user's default.
export BUILDX_BUILDER="$builder"
docker buildx inspect "$builder" --bootstrap
fi
failed_projects=()
for directory in "$compose_root"/*; do
[[ -d "$directory" ]] || continue
[[ -f "$directory/docker-compose.yml" ]] || continue
# Manually update Minecraft, so we're not always on the cutting edge
[[ "$directory" == *minecraft* ]] && continue
printf '\nUpdating %s\n' "$directory"
# A subshell restores the working directory even when a command fails.
# Because it is tested by 'if', explicitly stop on each failed command;
# Bash does not apply 'set -e' inside this conditional subshell.
if (
cd -- "$directory" || exit "$?"
docker compose pull --ignore-buildable || exit "$?"
if "$build"; then
docker compose build --pull || exit "$?"
fi
# Do not let Compose implicitly build images when starting services.
docker compose up -d --no-build
); then
printf 'Updated %s\n' "$directory"
else
status=$?
printf 'Update failed for %s (exit %s); continuing.\n' "$directory" "$status" >&2
failed_projects+=("$directory")
fi
done
if (( ${#failed_projects[@]} )); then
printf '\nFailed projects:\n' >&2
printf ' %s\n' "${failed_projects[@]}" >&2
exit 1
fi