135 lines
5.1 KiB
Bash
135 lines
5.1 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
# scripts/deploy/deploy-service.sh — deploy exactly ONE service on the local node.
|
||
|
|
#
|
||
|
|
# Extracted from deploy-node.sh so that the human whole-node deploy and the
|
||
|
|
# agent-driven single-service redeploy (services/deploy-runner/) share ONE
|
||
|
|
# compose invocation. That sharing is the whole point: docker compose derives
|
||
|
|
# the PROJECT NAME from the directory of the first -f file (verified:
|
||
|
|
# `docker compose -f services/node-agent/docker-compose.yml config` → name
|
||
|
|
# "node-agent", independent of cwd; adding --project-directory changes it).
|
||
|
|
# A caller that assembles the -f arguments differently lands in a DIFFERENT
|
||
|
|
# project, where `--remove-orphans` tears down the already-running stack —
|
||
|
|
# exactly what wiped the control-plane on VPS on 2026-06-25. Every caller goes
|
||
|
|
# through here.
|
||
|
|
#
|
||
|
|
# Usage:
|
||
|
|
# deploy-service.sh --repo <repo-path> --host-dir <hosts/<node> path>
|
||
|
|
# --service <name>
|
||
|
|
# [--build-if-needed] [--force-recreate] [--remove-orphans]
|
||
|
|
#
|
||
|
|
# Flags:
|
||
|
|
# --build-if-needed add --build when the service ships its own Dockerfile
|
||
|
|
# (whole-node deploy path — see the rationale below)
|
||
|
|
# --force-recreate recreate containers even when config is unchanged
|
||
|
|
# (remediation path: plain `up -d` is a no-op for an
|
||
|
|
# unhealthy-but-unchanged service, i.e. it would "succeed"
|
||
|
|
# without doing anything)
|
||
|
|
# --remove-orphans human whole-node deploy only; never used by the agent
|
||
|
|
# path, where a project-name mismatch must not be able to
|
||
|
|
# delete containers
|
||
|
|
#
|
||
|
|
# Exit codes:
|
||
|
|
# 0 deployed
|
||
|
|
# 2 usage / validation error
|
||
|
|
# 3 service owns its deploy path (services/<svc>/deploy-local.sh) — skipped
|
||
|
|
# * docker compose exit code
|
||
|
|
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
REPO_PATH=""
|
||
|
|
HOST_DIR=""
|
||
|
|
SERVICE=""
|
||
|
|
BUILD_IF_NEEDED=false
|
||
|
|
FORCE_RECREATE=false
|
||
|
|
REMOVE_ORPHANS=false
|
||
|
|
|
||
|
|
usage() {
|
||
|
|
sed -n '2,30p' "${BASH_SOURCE[0]}" >&2
|
||
|
|
exit 2
|
||
|
|
}
|
||
|
|
|
||
|
|
while [[ $# -gt 0 ]]; do
|
||
|
|
case "$1" in
|
||
|
|
--repo) REPO_PATH="${2:-}"; shift 2 ;;
|
||
|
|
--host-dir) HOST_DIR="${2:-}"; shift 2 ;;
|
||
|
|
--service) SERVICE="${2:-}"; shift 2 ;;
|
||
|
|
--build-if-needed) BUILD_IF_NEEDED=true; shift ;;
|
||
|
|
--force-recreate) FORCE_RECREATE=true; shift ;;
|
||
|
|
--remove-orphans) REMOVE_ORPHANS=true; shift ;;
|
||
|
|
-h|--help) usage ;;
|
||
|
|
*) echo "deploy-service.sh: unknown argument: $1" >&2; usage ;;
|
||
|
|
esac
|
||
|
|
done
|
||
|
|
|
||
|
|
[[ -n "$REPO_PATH" && -n "$HOST_DIR" && -n "$SERVICE" ]] || {
|
||
|
|
echo "deploy-service.sh: --repo, --host-dir and --service are required" >&2
|
||
|
|
usage
|
||
|
|
}
|
||
|
|
|
||
|
|
# Service names are used to build filesystem paths. Reject anything that is not
|
||
|
|
# a plain kebab-case service name so a malformed (or maliciously crafted)
|
||
|
|
# dispatched action can never escape services/ via ../.
|
||
|
|
if [[ ! "$SERVICE" =~ ^[a-z0-9][a-z0-9._-]{0,63}$ ]]; then
|
||
|
|
echo "deploy-service.sh: invalid service name: '${SERVICE}'" >&2
|
||
|
|
exit 2
|
||
|
|
fi
|
||
|
|
|
||
|
|
[[ -d "$REPO_PATH" ]] || { echo "deploy-service.sh: repo not found: ${REPO_PATH}" >&2; exit 2; }
|
||
|
|
[[ -d "$HOST_DIR" ]] || { echo "deploy-service.sh: host dir not found: ${HOST_DIR}" >&2; exit 2; }
|
||
|
|
|
||
|
|
SERVICE_DIR="${REPO_PATH}/services/${SERVICE}"
|
||
|
|
COMPOSE_FILE="${SERVICE_DIR}/docker-compose.yml"
|
||
|
|
|
||
|
|
if [[ ! -f "$COMPOSE_FILE" ]]; then
|
||
|
|
echo "deploy-service.sh: compose file not found for ${SERVICE} at ${COMPOSE_FILE}" >&2
|
||
|
|
exit 2
|
||
|
|
fi
|
||
|
|
|
||
|
|
# Services that ship their own deploy-local.sh have a dedicated, orchestrated
|
||
|
|
# deploy path (e.g. control-plane via deploy-control-plane.sh → deploy-local.sh)
|
||
|
|
# which does host-level preparation (dirs, ownership, sudo) this script cannot
|
||
|
|
# do. Deploying them from here would also run `up` against a stack whose own
|
||
|
|
# path expects to own it. Their own deploy path owns them; skip.
|
||
|
|
if [[ -f "${SERVICE_DIR}/deploy-local.sh" ]]; then
|
||
|
|
echo "Skipping ${SERVICE}: ma własną ścieżkę deployu (services/${SERVICE}/deploy-local.sh), pomijam"
|
||
|
|
exit 3
|
||
|
|
fi
|
||
|
|
|
||
|
|
COMPOSE_ARGS=(docker compose -f "$COMPOSE_FILE")
|
||
|
|
|
||
|
|
OVERRIDE_FILE="${HOST_DIR}/runtime/${SERVICE}/docker-compose.override.yml"
|
||
|
|
if [[ -f "$OVERRIDE_FILE" ]]; then
|
||
|
|
echo "Using override file for ${SERVICE}"
|
||
|
|
COMPOSE_ARGS+=(-f "$OVERRIDE_FILE")
|
||
|
|
fi
|
||
|
|
|
||
|
|
ENV_FILE="${SERVICE_DIR}/.env"
|
||
|
|
if [[ -f "$ENV_FILE" ]]; then
|
||
|
|
COMPOSE_ARGS+=(--env-file "$ENV_FILE")
|
||
|
|
fi
|
||
|
|
|
||
|
|
COMPOSE_ARGS+=(up -d)
|
||
|
|
|
||
|
|
# Services with their own Dockerfile need a rebuild on a whole-node deploy —
|
||
|
|
# otherwise `up -d` sees "container running, image tag unchanged" and skips
|
||
|
|
# rebuilding even when src/ changed, silently leaving the old code running
|
||
|
|
# (backlog c858dbc). The agent redeploy path deliberately does NOT pass
|
||
|
|
# --build-if-needed: a redeploy is a reconcile, not a code ship, and building
|
||
|
|
# on the 4 GiB VPS mid-incident is an OOM risk.
|
||
|
|
if [[ "$BUILD_IF_NEEDED" == "true" && -f "${SERVICE_DIR}/Dockerfile" ]]; then
|
||
|
|
echo "Building ${SERVICE} (has Dockerfile)"
|
||
|
|
COMPOSE_ARGS+=(--build)
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [[ "$FORCE_RECREATE" == "true" ]]; then
|
||
|
|
COMPOSE_ARGS+=(--force-recreate)
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [[ "$REMOVE_ORPHANS" == "true" ]]; then
|
||
|
|
COMPOSE_ARGS+=(--remove-orphans)
|
||
|
|
fi
|
||
|
|
|
||
|
|
echo "Deploying service: ${SERVICE}..."
|
||
|
|
echo "+ ${COMPOSE_ARGS[*]}"
|
||
|
|
"${COMPOSE_ARGS[@]}"
|