118 lines
4.7 KiB
Bash
118 lines
4.7 KiB
Bash
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
umask 027
|
|
|
|
TAG="${1:?Usage: deploy.sh VERSION [--build-only]}"
|
|
MODE="${2:-}"
|
|
[[ "$TAG" =~ ^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,79}$ ]] || { echo 'Invalid version' >&2; exit 1; }
|
|
[[ -z "$MODE" || "$MODE" == '--build-only' ]] || { echo 'Invalid deployment mode' >&2; exit 1; }
|
|
BASE_DIR="${SW_DEPLOY_BASE:-/www/wwwroot/sw2.fuhua91.com}"
|
|
BASE_DIR=$(cd -- "$BASE_DIR" && pwd -P)
|
|
RELEASE=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
|
|
[[ "$RELEASE" == "$BASE_DIR/frontend-releases/$TAG" ]] || { echo 'Release directory does not match version' >&2; exit 1; }
|
|
NAME=sw2-frontend
|
|
PREVIOUS=sw2-frontend-previous
|
|
CANDIDATE="sw2-frontend-candidate-$$"
|
|
IMAGE="sw2/frontend:$TAG"
|
|
NETWORK=sw2-net
|
|
ATTEMPTS="${SW_HEALTH_ATTEMPTS:-20}"
|
|
[[ "$ATTEMPTS" =~ ^[1-9][0-9]?$ ]] || { echo 'Invalid health-check attempts' >&2; exit 1; }
|
|
|
|
for tool in docker curl flock; do command -v "$tool" >/dev/null; done
|
|
exec 9>"$BASE_DIR/.frontend-deploy.lock"
|
|
flock -n 9 || { echo 'Another frontend deployment is running' >&2; exit 1; }
|
|
|
|
exists() { docker inspect "$1" >/dev/null 2>&1; }
|
|
managed() { [[ "$(docker inspect --format '{{index .Config.Labels "com.sw2.application"}}' "$1")" == sw2 ]]; }
|
|
for container in "$NAME" "$PREVIOUS"; do
|
|
if exists "$container"; then
|
|
managed "$container" || { echo "Refusing to replace unmanaged container: $container" >&2; exit 1; }
|
|
fi
|
|
done
|
|
if exists "$PREVIOUS"; then
|
|
[[ "$(docker inspect --format '{{.State.Running}}' "$PREVIOUS")" == false ]] || { echo 'Previous frontend container is still running' >&2; exit 1; }
|
|
fi
|
|
! exists "$CANDIDATE" || { echo 'Candidate container already exists' >&2; exit 1; }
|
|
docker network inspect "$NETWORK" >/dev/null
|
|
test -s "$RELEASE/dist/index.html"
|
|
[[ "$(cat "$RELEASE/dist/version.txt")" == "$TAG" ]] || { echo 'Frontend version marker mismatch' >&2; exit 1; }
|
|
|
|
old_present=false
|
|
old_stopped=false
|
|
moved=false
|
|
created=false
|
|
committed=false
|
|
old_version=''
|
|
if exists "$NAME"; then
|
|
old_present=true
|
|
old_version=$(docker inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$NAME")
|
|
fi
|
|
|
|
finish() {
|
|
result=$?
|
|
trap - EXIT INT TERM
|
|
set +e
|
|
if [[ "$result" -ne 0 && "$committed" == false ]]; then
|
|
if [[ "$created" == true ]] && exists "$NAME" && managed "$NAME"; then docker rm -f "$NAME" >/dev/null; fi
|
|
if [[ "$moved" == true ]]; then
|
|
docker rename "$PREVIOUS" "$NAME"
|
|
docker start "$NAME" >/dev/null
|
|
echo 'Frontend deployment failed; previous frontend restored.' >&2
|
|
elif [[ "$old_stopped" == true ]] && exists "$NAME"; then
|
|
docker start "$NAME" >/dev/null
|
|
fi
|
|
fi
|
|
if exists "$CANDIDATE"; then docker rm -f "$CANDIDATE" >/dev/null; fi
|
|
exit "$result"
|
|
}
|
|
trap finish EXIT
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
|
|
healthy() {
|
|
local address=$1
|
|
local attempt
|
|
for ((attempt=1; attempt<=ATTEMPTS; attempt++)); do
|
|
if [[ "$(curl -fsS --max-time 3 "$address/healthz" 2>/dev/null)" == ok ]] &&
|
|
[[ "$(curl -fsS --max-time 3 "$address/version.txt" 2>/dev/null)" == "$TAG" ]] &&
|
|
curl -fsS --max-time 5 "$address/api/health" >/dev/null 2>&1; then return 0; fi
|
|
if ((attempt < ATTEMPTS)); then sleep 1; fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
docker build --build-arg "VERSION=$TAG" -t "$IMAGE" "$RELEASE"
|
|
docker run --rm "$IMAGE" nginx -t
|
|
docker run -d --name "$CANDIDATE" --label com.sw2.application=sw2 --label com.sw2.component=frontend \
|
|
--network "$NETWORK" -p 127.0.0.1::80 "$IMAGE" >/dev/null
|
|
candidate_address=$(docker port "$CANDIDATE" 80/tcp)
|
|
[[ "$candidate_address" =~ ^127\.0\.0\.1:[0-9]+$ ]] || { echo 'Unexpected candidate binding' >&2; exit 1; }
|
|
healthy "http://$candidate_address" || { echo 'Candidate frontend or API proxy failed preflight' >&2; exit 1; }
|
|
if [[ "$MODE" == '--build-only' ]]; then
|
|
echo "Frontend image and API proxy preflight passed: $TAG"
|
|
exit 0
|
|
fi
|
|
|
|
if exists "$PREVIOUS"; then docker rm "$PREVIOUS" >/dev/null; fi
|
|
if [[ "$old_present" == true ]]; then
|
|
old_stopped=true
|
|
docker stop -t 15 "$NAME" >/dev/null
|
|
docker rename "$NAME" "$PREVIOUS"
|
|
moved=true
|
|
fi
|
|
created=true
|
|
docker run -d --name "$NAME" --label com.sw2.application=sw2 --label com.sw2.component=frontend \
|
|
--restart unless-stopped --network "$NETWORK" \
|
|
--log-opt max-size=20m --log-opt max-file=3 \
|
|
-p 127.0.0.1:9601:80 "$IMAGE" >/dev/null
|
|
healthy http://127.0.0.1:9601 || { echo 'Published frontend failed health checks' >&2; exit 1; }
|
|
|
|
if [[ -n "$old_version" ]]; then
|
|
printf '%s\n' "$old_version" > "$BASE_DIR/previous-frontend-release.tmp"
|
|
mv -f -- "$BASE_DIR/previous-frontend-release.tmp" "$BASE_DIR/previous-frontend-release"
|
|
fi
|
|
printf '%s\n' "$TAG" > "$BASE_DIR/current-frontend-release.tmp"
|
|
mv -f -- "$BASE_DIR/current-frontend-release.tmp" "$BASE_DIR/current-frontend-release"
|
|
committed=true
|
|
printf 'Frontend deployed: %s\nBackend container was not modified.\n' "$TAG"
|