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>
116 lines
3.8 KiB
Bash
Executable file
116 lines
3.8 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# deployment-node.sh - To be run on the execution node (SOLARIA, PIHA, VPS)
|
|
# This script pulls the latest changes and ensures services are running.
|
|
|
|
set -e
|
|
|
|
# Configuration
|
|
REPO_PATH="${HOME}/homelab-codex-ws"
|
|
# Env-overridable so the script can be exercised against a scratch runtime dir
|
|
# without writing into the node's real /opt/homelab.
|
|
RUNTIME_PATH="${RUNTIME_PATH:-/opt/homelab}"
|
|
HOSTNAME=$(hostname | tr '[:lower:]' '[:upper:]')
|
|
CURRENT_OS_HOST=$(hostname)
|
|
|
|
echo "--- Starting Deployment on ${HOSTNAME} ---"
|
|
|
|
# 1. Update Repository
|
|
if [ ! -d "$REPO_PATH" ]; then
|
|
echo "Error: Repository not found at $REPO_PATH"
|
|
exit 1
|
|
fi
|
|
|
|
cd "$REPO_PATH"
|
|
echo "Pulling latest changes..."
|
|
git pull
|
|
|
|
# Resolve host directory: search hosts/*/host.yaml for os_hostname == $(hostname)
|
|
HOST_LOGICAL=$(python3 -c "
|
|
import yaml, os, sys
|
|
current = '${CURRENT_OS_HOST}'
|
|
hosts_dir = os.path.join('${REPO_PATH}', 'hosts')
|
|
for entry in sorted(os.listdir(hosts_dir)):
|
|
host_yaml = os.path.join(hosts_dir, entry, 'host.yaml')
|
|
if not os.path.isfile(host_yaml):
|
|
continue
|
|
try:
|
|
with open(host_yaml) as f:
|
|
data = yaml.safe_load(f)
|
|
if data and data.get('os_hostname') == current:
|
|
print(entry)
|
|
sys.exit(0)
|
|
except Exception:
|
|
pass
|
|
")
|
|
|
|
if [ -n "$HOST_LOGICAL" ]; then
|
|
HOST_DIR="${REPO_PATH}/hosts/${HOST_LOGICAL}"
|
|
else
|
|
HOST_LOGICAL_FB="$(echo "$CURRENT_OS_HOST" | tr '[:upper:]' '[:lower:]')"
|
|
echo "WARN: no os_hostname match for ${CURRENT_OS_HOST}, falling back to lowercase hostname dir" >&2
|
|
HOST_DIR="${REPO_PATH}/hosts/${HOST_LOGICAL_FB}"
|
|
fi
|
|
|
|
if [ ! -d "$HOST_DIR" ]; then
|
|
echo "Error: No host directory found for ${CURRENT_OS_HOST} (tried ${HOST_DIR})" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# 2. Identify Services
|
|
SERVICES=()
|
|
if [ -f "${HOST_DIR}/services.txt" ]; then
|
|
mapfile -t SERVICES < <(grep -v '^\s*#' "${HOST_DIR}/services.txt" | grep -v '^\s*$')
|
|
elif [ -f "${HOST_DIR}/services.yaml" ]; then
|
|
SERVICES=($(python3 -c "
|
|
import yaml, sys
|
|
try:
|
|
with open('${HOST_DIR}/services.yaml', 'r') as f:
|
|
data = yaml.safe_load(f)
|
|
if data and 'services' in data:
|
|
if isinstance(data['services'], dict):
|
|
print(' '.join(data['services'].keys()))
|
|
elif isinstance(data['services'], list):
|
|
print(' '.join(data['services']))
|
|
except Exception as e:
|
|
print(f'Error parsing YAML: {e}', file=sys.stderr)
|
|
sys.exit(1)
|
|
"))
|
|
fi
|
|
|
|
if [ ${#SERVICES[@]} -eq 0 ]; then
|
|
echo "No services found for ${HOSTNAME}. Skipping service deployment."
|
|
exit 0
|
|
fi
|
|
|
|
# 3. Deploy Services
|
|
#
|
|
# The per-service compose invocation lives in deploy-service.sh, shared with the
|
|
# agent-driven single-service redeploy (services/deploy-runner/). Both callers
|
|
# MUST assemble the -f arguments identically: docker compose derives the project
|
|
# name from the first -f file's directory, and a mismatch plus --remove-orphans
|
|
# is what wiped the control-plane on VPS on 2026-06-25. Services with their own
|
|
# deploy-local.sh are skipped by deploy-service.sh (exit 3).
|
|
DEPLOY_SERVICE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/deploy-service.sh"
|
|
|
|
for service in "${SERVICES[@]}"; do
|
|
TARGET_DIR="${RUNTIME_PATH}/services/${service}"
|
|
mkdir -p "$TARGET_DIR"
|
|
|
|
rc=0
|
|
"$DEPLOY_SERVICE" \
|
|
--repo "$REPO_PATH" \
|
|
--host-dir "$HOST_DIR" \
|
|
--service "$service" \
|
|
--build-if-needed \
|
|
--remove-orphans || rc=$?
|
|
|
|
case "$rc" in
|
|
0) ;; # deployed
|
|
3) ;; # owns its deploy path — already reported, skip
|
|
2) echo "Warning: skipping ${service} (validation failed, see above)" ;;
|
|
*) exit "$rc" ;; # real deploy failure — preserve set -e semantics
|
|
esac
|
|
done
|
|
|
|
echo "--- Deployment Complete ---"
|