- #!/usr/bin/env bash
- set -Eeuo pipefail
-
- # Apply an incremental image update on the target runtime server.
- # Usage:
- # bash apply-update.sh
- # bash apply-update.sh emp-admin emp-monitor
- # DEPLOY_ENV=emp-uat ENV_FILE=.env COMPOSE_FILE=docker-compose.yml bash apply-update.sh
- # COMPOSE_FILES="/path/docker-compose.yml:/path/docker-compose.logs.yml" bash apply-update.sh
-
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
- cd "$SCRIPT_DIR"
-
- DEPLOY_ENV="${DEPLOY_ENV:-emp-test}"
- PROJECT_NAME="${PROJECT_NAME:-$DEPLOY_ENV}"
- DEPLOY_HOME="${DEPLOY_HOME:-/home/admin-x99/$DEPLOY_ENV}"
- ENV_FILE="${ENV_FILE:-.env}"
- COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}"
- COMPOSE_FILES="${COMPOSE_FILES:-}"
- IMAGE_TAR="${IMAGE_TAR:-images.tar}"
- SERVICE_FILE="${SERVICE_FILE:-services.txt}"
-
- log() {
- echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
- }
-
- die() {
- echo "ERROR: $*" >&2
- exit 1
- }
-
- need_cmd() {
- command -v "$1" >/dev/null 2>&1 || die "Missing command: $1"
- }
-
- normalize_service() {
- case "$1" in
- gateway|emp-gateway) echo "emp-gateway" ;;
- auth|emp-auth) echo "emp-auth" ;;
- monitor|emp-monitor) echo "emp-monitor" ;;
- data|emp-data) echo "emp-data" ;;
- pdf|emp-pdf) echo "emp-pdf" ;;
- ws|emp-ws) echo "emp-ws" ;;
- admin|emp-admin) echo "emp-admin" ;;
- *) die "Unknown service: $1. Allowed: gateway auth monitor data pdf ws admin" ;;
- esac
- }
-
- resolve_compose_cmd() {
- if docker compose version >/dev/null 2>&1; then
- echo "docker compose"
- elif command -v docker-compose >/dev/null 2>&1; then
- echo "docker-compose"
- else
- die "Missing docker compose"
- fi
- }
-
- resolve_runtime_file() {
- local requested="$1"
- local default_name="$2"
- if [[ -f "$requested" ]]; then
- echo "$requested"
- return
- fi
- if [[ "$requested" != "$default_name" ]]; then
- echo "$requested"
- return
- fi
-
- local candidates=(
- "../runtime/$default_name"
- "$DEPLOY_HOME/runtime/$default_name"
- "$DEPLOY_HOME/$default_name"
- "/opt/$DEPLOY_ENV/$default_name"
- )
- local candidate
- for candidate in "${candidates[@]}"; do
- if [[ -f "$candidate" ]]; then
- echo "$candidate"
- return
- fi
- done
-
- echo "$requested"
- }
-
- resolve_compose_files() {
- local raw_files=()
- local file resolved
- if [[ -n "$COMPOSE_FILES" ]]; then
- IFS=':' read -r -a raw_files <<< "$COMPOSE_FILES"
- else
- raw_files=("$COMPOSE_FILE")
- fi
-
- COMPOSE_ARGS=()
- RESOLVED_COMPOSE_FILES=()
- for file in "${raw_files[@]}"; do
- [[ -n "$file" ]] || continue
- resolved="$(resolve_runtime_file "$file" "docker-compose.yml")"
- [[ -f "$resolved" ]] || die "Missing compose file: $resolved"
- RESOLVED_COMPOSE_FILES+=("$resolved")
- COMPOSE_ARGS+=("-f" "$resolved")
- done
-
- [[ "${#COMPOSE_ARGS[@]}" -gt 0 ]] || die "No compose files resolved."
- }
-
- read_services() {
- local raw_services=()
- if [[ "$#" -gt 0 ]]; then
- raw_services=("$@")
- elif [[ -f "$SERVICE_FILE" ]]; then
- mapfile -t raw_services < "$SERVICE_FILE"
- else
- die "No services specified. Pass services as args or provide $SERVICE_FILE."
- fi
-
- local service
- for service in "${raw_services[@]}"; do
- [[ -n "$service" ]] || continue
- normalize_service "$service"
- done
- }
-
- contains_service() {
- local target="$1"
- shift
- local service
- for service in "$@"; do
- if [[ "$service" == "$target" ]]; then
- return 0
- fi
- done
- return 1
- }
-
- escape_sed_replacement() {
- local value="$1"
- value="${value//\\/\\\\}"
- value="${value//&/\\&}"
- value="${value//|/\\|}"
- printf '%s\n' "$value"
- }
-
- apply_runtime_env_defaults() {
- local defaults_file="$SCRIPT_DIR/update-env.defaults"
- [[ -f "$defaults_file" ]] || return 0
-
- local backup_file line key value escaped
- backup_file="${ENV_FILE}.bak-$(date '+%Y%m%d%H%M%S')"
- cp "$ENV_FILE" "$backup_file"
- log "Apply runtime env defaults: $defaults_file"
- log "Runtime env backup: $backup_file"
-
- while IFS= read -r line || [[ -n "$line" ]]; do
- [[ -n "$line" ]] || continue
- [[ "$line" == \#* ]] && continue
- [[ "$line" == *"="* ]] || continue
-
- key="${line%%=*}"
- value="${line#*=}"
- [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || die "Invalid env key in $defaults_file: $key"
-
- escaped="$(escape_sed_replacement "$value")"
- if grep -q "^${key}=" "$ENV_FILE"; then
- sed -i "s|^${key}=.*|${key}=${escaped}|" "$ENV_FILE"
- else
- printf '\n%s=%s\n' "$key" "$value" >> "$ENV_FILE"
- fi
- done < "$defaults_file"
- }
-
- need_cmd docker
- ENV_FILE="$(resolve_runtime_file "$ENV_FILE" ".env")"
- resolve_compose_files
- [[ -f "$ENV_FILE" ]] || die "Missing env file: $ENV_FILE"
- [[ -f "$IMAGE_TAR" ]] || die "Missing image tar: $IMAGE_TAR"
-
- mapfile -t UPDATE_SERVICES < <(read_services "$@")
- [[ "${#UPDATE_SERVICES[@]}" -gt 0 ]] || die "No services to update."
-
- COMPOSE_CMD="$(resolve_compose_cmd)"
-
- apply_runtime_env_defaults
-
- log "Compose files: ${RESOLVED_COMPOSE_FILES[*]}"
- log "Load images: $IMAGE_TAR"
- docker load -i "$IMAGE_TAR"
-
- log "Recreate services: ${UPDATE_SERVICES[*]}"
- # shellcheck disable=SC2086
- $COMPOSE_CMD \
- --env-file "$ENV_FILE" \
- "${COMPOSE_ARGS[@]}" \
- -p "$PROJECT_NAME" \
- up -d --no-deps --force-recreate "${UPDATE_SERVICES[@]}"
-
- if ! contains_service "emp-admin" "${UPDATE_SERVICES[@]}" \
- && { contains_service "emp-ws" "${UPDATE_SERVICES[@]}" || contains_service "emp-gateway" "${UPDATE_SERVICES[@]}"; }; then
- log "Restart emp-admin to refresh Nginx upstream DNS"
- # shellcheck disable=SC2086
- $COMPOSE_CMD \
- --env-file "$ENV_FILE" \
- "${COMPOSE_ARGS[@]}" \
- -p "$PROJECT_NAME" \
- restart emp-admin
- fi
-
- log "Current service status"
- # shellcheck disable=SC2086
- $COMPOSE_CMD \
- --env-file "$ENV_FILE" \
- "${COMPOSE_ARGS[@]}" \
- -p "$PROJECT_NAME" \
- ps "${UPDATE_SERVICES[@]}"
|