diff --git a/bash/functions/infra/docker_compose_remote_deploy.md b/bash/functions/infra/docker_compose_remote_deploy.md new file mode 100644 index 00000000..52f02847 --- /dev/null +++ b/bash/functions/infra/docker_compose_remote_deploy.md @@ -0,0 +1,64 @@ +--- +name: docker_compose_remote_deploy +kind: function +lang: bash +domain: infra +version: "1.0.0" +purity: impure +signature: "docker_compose_remote_deploy(host: string, remote_dir: string, branch: string, compose_files: string) -> json" +description: "Despliega un stack Docker Compose en un host remoto via SSH. Verifica conectividad, hace git pull del branch indicado, actualiza imagenes con docker-compose pull y levanta/recrea los servicios modificados con docker-compose up -d. Soporta compose files adicionales. Retorna JSON con status, containers corriendo y duracion." +tags: [docker, compose, deploy, ssh, remote, git, infra, cicd] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +params: + - name: host + desc: "alias SSH del host remoto definido en ~/.ssh/config (ej: prod-server)" + - name: remote_dir + desc: "ruta absoluta en el host donde esta el repo con docker-compose.yml (ej: /opt/apps/element)" + - name: branch + desc: "branch de git a hacer pull; default 'main'" + - name: compose_files + desc: "archivos compose adicionales separados por coma (ej: 'docker-compose.livekit.yml,docker-compose.monitoring.yml'); si vacio usa solo docker-compose.yml" +output: "JSON con status ('ok'), host, remote_dir, branch, containers (array de nombres corriendo tras el deploy), duration_ms" +tested: false +tests: [] +test_file_path: "" +file_path: "bash/functions/infra/docker_compose_remote_deploy.sh" +--- + +## Ejemplo + +```bash +source bash/functions/infra/docker_compose_remote_deploy.sh + +# Deploy basico (solo docker-compose.yml, branch main) +result=$(docker_compose_remote_deploy "prod-server" "/opt/apps/element") +echo "$result" +# {"status":"ok","host":"prod-server","remote_dir":"/opt/apps/element","branch":"main","containers":["element-web","synapse","postgres"],"duration_ms":4200} + +# Deploy con compose files adicionales y branch especifico +result=$(docker_compose_remote_deploy "prod-server" "/opt/apps/element" "release" "docker-compose.livekit.yml,docker-compose.monitoring.yml") +echo "$result" +# {"status":"ok","host":"prod-server","remote_dir":"/opt/apps/element","branch":"release","containers":[...],"duration_ms":8100} + +# Uso desde un pipeline CI/CD +source bash/functions/infra/docker_compose_remote_deploy.sh +docker_compose_remote_deploy "$SSH_HOST" "$REMOTE_DIR" "$GIT_BRANCH" "$EXTRA_COMPOSE" || exit 1 +``` + +## Notas + +- Flujo: verificar SSH → git pull → docker-compose pull → docker-compose up -d → listar containers. +- La verificacion SSH usa `-o BatchMode=yes -o ConnectTimeout=5` para fallar rapido sin pedir password. +- Los compose files adicionales se pasan como `-f file1.yml -f file2.yml` a todos los subcomandos compose. +- `docker-compose up -d` solo recrea los servicios cuya imagen o config cambio (comportamiento nativo de compose). +- La lista de containers al final incluye TODOS los containers corriendo en el host, no solo los del stack. +- Requiere `jq` instalado en el host remoto para serializar la lista de containers. Si no esta, `containers` sera `[]`. +- Los mensajes de progreso van a stderr; el JSON final va a stdout. +- Exit code 1 en cualquier fallo (SSH, git pull, compose pull, compose up); el JSON de error NO se emite — el caller debe manejar el exit code. +- El `host` se resuelve con `~/.ssh/config` incluyendo host, user, identityfile y puerto. +- Diferencia con `rsync_deploy`: este flujo asume que el codigo ya esta en el remoto (via git) y usa compose. `rsync_deploy` sube archivos locales sin git. diff --git a/bash/functions/infra/docker_compose_remote_deploy.sh b/bash/functions/infra/docker_compose_remote_deploy.sh new file mode 100644 index 00000000..abf10c40 --- /dev/null +++ b/bash/functions/infra/docker_compose_remote_deploy.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# docker_compose_remote_deploy — Despliega un stack Docker Compose en un host remoto via SSH +set -euo pipefail + +docker_compose_remote_deploy() { + local host="$1" + local remote_dir="$2" + local branch="${3:-main}" + local compose_files="${4:-}" + + if [[ -z "$host" || -z "$remote_dir" ]]; then + echo "docker_compose_remote_deploy: se requieren host y remote_dir" >&2 + return 1 + fi + + local start_ts + start_ts=$(date +%s) + + # 1. Verificar conectividad SSH + echo "docker_compose_remote_deploy: verificando conectividad SSH a '$host'..." >&2 + if ! ssh -o BatchMode=yes -o ConnectTimeout=5 "$host" true 2>/dev/null; then + echo "docker_compose_remote_deploy: no se puede conectar a '$host' via SSH" >&2 + return 1 + fi + + # 2. Git pull en el host remoto + echo "docker_compose_remote_deploy: git pull origin $branch en '$remote_dir'..." >&2 + if ! ssh "$host" "cd '$remote_dir' && git pull origin '$branch'" >&2; then + echo "docker_compose_remote_deploy: git pull falló en '$host:$remote_dir'" >&2 + return 1 + fi + + # 3. Construir los argumentos -f para docker-compose + local compose_args="-f docker-compose.yml" + if [[ -n "$compose_files" ]]; then + local IFS="," + local extra_file + for extra_file in $compose_files; do + extra_file="${extra_file// /}" # trim spaces + if [[ -n "$extra_file" ]]; then + compose_args="$compose_args -f $extra_file" + fi + done + unset IFS + fi + + # 4. docker-compose pull + echo "docker_compose_remote_deploy: actualizando imagenes ($compose_args)..." >&2 + if ! ssh "$host" "cd '$remote_dir' && docker-compose $compose_args pull" >&2; then + echo "docker_compose_remote_deploy: docker-compose pull falló en '$host:$remote_dir'" >&2 + return 1 + fi + + # 5. docker-compose up -d + echo "docker_compose_remote_deploy: levantando servicios ($compose_args)..." >&2 + if ! ssh "$host" "cd '$remote_dir' && docker-compose $compose_args up -d" >&2; then + echo "docker_compose_remote_deploy: docker-compose up -d falló en '$host:$remote_dir'" >&2 + return 1 + fi + + # 6. Recopilar containers corriendo tras el deploy + local containers_json + containers_json=$(ssh "$host" \ + "docker ps --format '{{.Names}}' 2>/dev/null | jq -R . | jq -sc ." 2>/dev/null || echo '[]') + + local end_ts + end_ts=$(date +%s) + local duration_ms=$(( (end_ts - start_ts) * 1000 )) + + # Emitir JSON a stdout + printf '{"status":"ok","host":"%s","remote_dir":"%s","branch":"%s","containers":%s,"duration_ms":%d}\n' \ + "$host" \ + "$remote_dir" \ + "$branch" \ + "$containers_json" \ + "$duration_ms" +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + docker_compose_remote_deploy "$@" +fi