homelab-codex-ws/scripts/deploy/deploy-service.sh

135 lines
5.1 KiB
Bash
Raw Normal View History

fix(control-plane): redeploy wykonywalny — dispatch do host-side deploy-runnera Executor odpalal scripts/deploy/deploy-node.sh <node> <service> wewnatrz swojego kontenera: skrypt ignoruje oba argumenty i wymaga repo w ${HOME}/homelab-codex-ws (w kontenerze HOME=/home/homelab) -> exit 1 w 18. linii. Za tym brak git, brak klienta docker w obrazie, a gdyby przeszedl — deploy calego zestawu uslug hosta executora zamiast wezla z akcji. Kazdy redeploy padal (recon D14/D15; 18 pending / 0 completed). Redeploy idzie teraz ta sama sciezka pull co container_restart — VPS nigdy nie inicjuje polaczenia do wezla: executor -> actions/deploy/<node>/<id>.json -> deploy-runner (systemd na hoscie) rsync-pull, walidacja, deploy -> action_result event -> executor rozlicza completed/failed - scripts/deploy/deploy-service.sh: deploy jednej uslugi, wspoldzielony z deploy-node.sh, wiec inwokacja compose (a przez to nazwa projektu) jest identyczna jak przy deployu recznym - jobs/deploy-runner/: host-level, nie kontener — compose rozwiazuje wzgledne bindy i nazwe projektu tak jak przy deployu czlowieka; niezalezny od node-agenta, wiec potrafi zredeployowac takze jego - walidacja: tylko typ redeploy, node musi sie zgadzac, usluga musi byc w hosts/<node>/services.yaml, zadna tresc z payloadu nie trafia do shella - --force-recreate bez --build i bez --remove-orphans: redeploy to rekoncyliacja, nie wysylka kodu - executor: REDEPLOY_TIMEOUT_SECS=900, /repo zjechany do :ro (nieuzywany) 248 testow zielonych; deploy-node.sh przecwiczony na atrapie dockera — argv compose bez zmian. Instalacja unitow na wezlach i E2E: backlog. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 18:26:30 +02:00
#!/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[@]}"