121 lines
5.3 KiB
Bash
121 lines
5.3 KiB
Bash
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
BASE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
cd "$BASE"
|
||
|
||
# The Codex container can mount its default Docker config read-only. Keep build
|
||
# metadata in a disposable location so a deployment never fails for that reason.
|
||
export DOCKER_CONFIG="${CODEXPI_DOCKER_CONFIG:-/tmp/codexpi-docker-config}"
|
||
mkdir -p "$DOCKER_CONFIG"
|
||
|
||
command -v docker >/dev/null || { echo "Docker est requis." >&2; exit 1; }
|
||
if ! docker info >/dev/null 2>&1; then
|
||
socket_gid="$(stat -c '%g' /var/run/docker.sock 2>/dev/null || true)"
|
||
if [[ -n "$socket_gid" && " $(id -G) " != *" $socket_gid "* ]]; then
|
||
echo "Docker n’est pas accessible : le processus courant n’appartient pas au groupe $socket_gid du socket." >&2
|
||
echo "Depuis l’hôte, installez le service de boot puis relancez ce script pour recréer l’API avec le bon groupe :" >&2
|
||
echo " ./scripts/install-boot-service.sh" >&2
|
||
echo " ./scripts/redeploy.sh" >&2
|
||
else
|
||
echo "Docker n’est pas accessible." >&2
|
||
fi
|
||
exit 1
|
||
fi
|
||
docker_gid="$(stat -c '%g' /var/run/docker.sock)"
|
||
|
||
if docker ps --format '{{.Names}}' | grep -q '^codexpi-safe-redeploy-'; then
|
||
echo "Un déploiement CodexPi est déjà en cours ; aucune seconde bascule ne sera lancée."
|
||
exit 0
|
||
fi
|
||
if docker inspect codexpi-api-next >/dev/null 2>&1 || docker inspect codexpi-django-next >/dev/null 2>&1; then
|
||
echo "Une version de relève est encore active. Vérifiez-la avant de relancer un déploiement." >&2
|
||
exit 1
|
||
fi
|
||
|
||
stamp="$(date +%s)"
|
||
image="codexpi-app:deploy-$stamp"
|
||
echo "Construction isolée de la nouvelle image CodexPi ($image)…"
|
||
docker build --target production -t "$image" "$BASE"
|
||
|
||
helper="codexpi-safe-redeploy-$stamp"
|
||
docker run -d --rm --name "$helper" \
|
||
-e CODEXPI_DEPLOY_IMAGE="$image" \
|
||
-e DOCKER_GID="$docker_gid" \
|
||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||
-v "$BASE:$BASE" -w "$BASE" \
|
||
docker:cli sh -c '
|
||
set -eu
|
||
api_next=codexpi-api-next
|
||
django_next=codexpi-django-next
|
||
switched=0
|
||
cleanup() {
|
||
if [ "$switched" = 0 ]; then
|
||
docker rm -f "$api_next" "$django_next" >/dev/null 2>&1 || true
|
||
fi
|
||
}
|
||
trap cleanup EXIT
|
||
wait_healthy() {
|
||
container="$1"
|
||
attempts=0
|
||
while [ "$attempts" -lt 60 ]; do
|
||
health=$(docker inspect -f "{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}" "$container" 2>/dev/null || true)
|
||
[ "$health" = healthy ] && return 0
|
||
attempts=$((attempts + 1))
|
||
sleep 2
|
||
done
|
||
echo "Le conteneur $container n’est pas devenu sain." >&2
|
||
return 1
|
||
}
|
||
wait_drained() {
|
||
container="$1"
|
||
quiet=0
|
||
echo "Drainage de $container : les discussions en cours restent actives…"
|
||
# Five consecutive quiet checks are enough to confirm that the old API no
|
||
# longer owns a live session, without delaying every deployment by five
|
||
# minutes once the last worker has exited.
|
||
while [ "$quiet" -lt 5 ]; do
|
||
if docker top "$container" -eo pid,args 2>/dev/null | grep -Eq "codexpi\.session_worker|/codex .* exec"; then
|
||
quiet=0
|
||
else
|
||
quiet=$((quiet + 1))
|
||
fi
|
||
sleep 2
|
||
done
|
||
}
|
||
echo "Démarrage de la nouvelle version en parallèle…"
|
||
docker rm -f "$api_next" "$django_next" >/dev/null 2>&1 || true
|
||
CODEXPI_IMAGE="$CODEXPI_DEPLOY_IMAGE" docker compose --project-directory "'"$BASE"'" -f "'"$BASE"'/docker-compose.yml" run -d --no-deps --name "$api_next" -e CODEXPI_SKIP_WORKER_RECOVERY=true api >/dev/null
|
||
CODEXPI_IMAGE="$CODEXPI_DEPLOY_IMAGE" docker compose --project-directory "'"$BASE"'" -f "'"$BASE"'/docker-compose.yml" run -d --no-deps --name "$django_next" django-web >/dev/null
|
||
wait_healthy "$api_next"
|
||
wait_healthy "$django_next"
|
||
|
||
echo "Bascule Nginx vers la nouvelle version saine…"
|
||
printf "upstream django_ui { server %s:8001; }\\nupstream fastapi_api { server %s:8000; }\\n" "$django_next" "$api_next" | docker exec -i codexpi-nginx-1 sh -c "cat > /tmp/codexpi-upstreams.conf"
|
||
docker exec codexpi-nginx-1 nginx -t
|
||
docker exec codexpi-nginx-1 nginx -s reload
|
||
switched=1
|
||
|
||
wait_drained codexpi-api-1
|
||
echo "Remplacement de l’ancienne version désormais inactive…"
|
||
# Promote only the image which already passed both health checks. The
|
||
# stable tag is what systemd/docker compose will reuse after a host reboot.
|
||
docker image tag "$CODEXPI_DEPLOY_IMAGE" codexpi-app:local
|
||
CODEXPI_IMAGE=codexpi-app:local docker compose --project-directory "'"$BASE"'" -f "'"$BASE"'/docker-compose.yml" up -d --no-deps --force-recreate api django-web
|
||
wait_healthy codexpi-api-1
|
||
wait_healthy codexpi-django-web-1
|
||
|
||
echo "Retour vers les services principaux et nettoyage…"
|
||
printf "upstream django_ui { server django-web:8001; }\\nupstream fastapi_api { server api:8000; }\\n" | docker exec -i codexpi-nginx-1 sh -c "cat > /tmp/codexpi-upstreams.conf"
|
||
docker exec codexpi-nginx-1 nginx -t
|
||
docker exec codexpi-nginx-1 nginx -s reload
|
||
wait_drained "$api_next"
|
||
docker rm -f "$api_next" "$django_next" >/dev/null
|
||
switched=0
|
||
trap - EXIT
|
||
echo "Redéploiement terminé sans coupure."
|
||
' >/dev/null
|
||
|
||
echo "Image construite. Déploiement blue/green programmé ($helper)."
|
||
echo "Les anciennes connexions resteront actives jusqu’à la fin de leur discussion."
|