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."
|