#!/bin/bash set -e # Defaults HEALTHCHECK_TIMEOUT=60 NO_HEALTHCHECK_TIMEOUT=10 # Print metadata for Docker CLI plugin if [[ "$1" == "docker-cli-plugin-metadata" ]]; then cat </dev/null 2>&1; then # shellcheck disable=SC2086 # DOCKER_ARGS must be unquoted to allow multiple arguments COMPOSE_COMMAND="docker $DOCKER_ARGS compose" elif docker-compose >/dev/null 2>&1; then COMPOSE_COMMAND="docker-compose" else echo "docker compose or docker-compose is required" exit 1 fi usage() { cat < Service '$SERVICE' is not running. Starting the service." $COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES up --detach --no-recreate "$SERVICE" exit 0 fi # shellcheck disable=SC2086 # COMPOSE_FILES and ENV_FILES must be unquoted to allow multiple files OLD_CONTAINER_IDS_STRING=$($COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES ps --quiet "$SERVICE") OLD_CONTAINER_IDS=() for container_id in $OLD_CONTAINER_IDS_STRING; do OLD_CONTAINER_IDS+=("$container_id") done SCALE=${#OLD_CONTAINER_IDS[@]} SCALE_TIMES_TWO=$((SCALE * 2)) echo "==> Scaling '$SERVICE' to '$SCALE_TIMES_TWO' instances" scale "$SERVICE" $SCALE_TIMES_TWO # Create a variable that contains the IDs of the new containers, but not the old ones # shellcheck disable=SC2086 # COMPOSE_FILES and ENV_FILES must be unquoted to allow multiple files NEW_CONTAINER_IDS_STRING=$($COMPOSE_COMMAND $COMPOSE_FILES $ENV_FILES ps --quiet "$SERVICE" | grep --invert-match --file <(echo "$OLD_CONTAINER_IDS_STRING")) NEW_CONTAINER_IDS=() for container_id in $NEW_CONTAINER_IDS_STRING; do NEW_CONTAINER_IDS+=("$container_id") done # Check if first container has healthcheck # shellcheck disable=SC2086 # DOCKER_ARGS must be unquoted to allow multiple arguments if docker $DOCKER_ARGS inspect --format='{{json .State.Health}}' "${OLD_CONTAINER_IDS[0]}" | grep --quiet "Status"; then echo "==> Waiting for new containers to be healthy (timeout: $HEALTHCHECK_TIMEOUT seconds)" for _ in $(seq 1 "$HEALTHCHECK_TIMEOUT"); do SUCCESS=0 for NEW_CONTAINER_ID in "${NEW_CONTAINER_IDS[@]}"; do if healthcheck "$NEW_CONTAINER_ID"; then SUCCESS=$((SUCCESS + 1)) fi done if [[ "$SUCCESS" == "$SCALE" ]]; then break fi sleep 1 done SUCCESS=0 for NEW_CONTAINER_ID in "${NEW_CONTAINER_IDS[@]}"; do if healthcheck "$NEW_CONTAINER_ID"; then SUCCESS=$((SUCCESS + 1)) fi done if [[ "$SUCCESS" != "$SCALE" ]]; then echo "==> New containers are not healthy. Rolling back." >&2 for NEW_CONTAINER_ID in "${NEW_CONTAINER_IDS[@]}"; do # shellcheck disable=SC2086 # DOCKER_ARGS must be unquoted to allow multiple arguments docker $DOCKER_ARGS stop "$NEW_CONTAINER_ID" # shellcheck disable=SC2086 # DOCKER_ARGS must be unquoted to allow multiple arguments docker $DOCKER_ARGS rm "$NEW_CONTAINER_ID" done exit 1 fi else echo "==> Waiting for new containers to be ready ($NO_HEALTHCHECK_TIMEOUT seconds)" sleep "$NO_HEALTHCHECK_TIMEOUT" fi echo "==> Stopping old containers" for OLD_CONTAINER_ID in "${OLD_CONTAINER_IDS[@]}"; do # shellcheck disable=SC2086 # DOCKER_ARGS must be unquoted to allow multiple arguments docker $DOCKER_ARGS stop "$OLD_CONTAINER_ID" # shellcheck disable=SC2086 # DOCKER_ARGS must be unquoted to allow multiple arguments docker $DOCKER_ARGS rm "$OLD_CONTAINER_ID" done } while [[ $# -gt 0 ]]; do case "$1" in -h | --help) usage exit 0 ;; -f | --file) COMPOSE_FILES="$COMPOSE_FILES -f $2" shift 2 ;; --env-file) ENV_FILES="$ENV_FILES --env-file $2" shift 2 ;; -t | --timeout) HEALTHCHECK_TIMEOUT="$2" shift 2 ;; -w | --wait) NO_HEALTHCHECK_TIMEOUT="$2" shift 2 ;; -*) echo "Unknown option: $1" exit_with_usage ;; *) if [[ -n "$SERVICE" ]]; then echo "SERVICE is already set to '$SERVICE'" exit_with_usage fi SERVICE="$1" shift ;; esac done # Require SERVICE argument if [[ -z "$SERVICE" ]]; then echo "SERVICE is missing" exit_with_usage fi main