feat(infra): auto-commit con 56 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
---
|
||||
name: check_service_health_via_ssh
|
||||
kind: function
|
||||
lang: bash
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "check_service_health_via_ssh(ssh_host: string, local_url: string, [--token-from-env <remote_env_path> <ENV_VAR>], [--token <literal>], [--expect-status <code>], [--connect-timeout <s>], [--curl-timeout <s>]) -> json"
|
||||
description: "Comprueba la salud de un service HTTP que solo escucha en loopback (127.0.0.1) de un host remoto, entrando por SSH y haciendo curl con bearer token opcional. El token se resuelve dentro del host remoto (leyendo una variable de un .env remoto via grep, o pasado literal) y NUNCA se imprime ni se hardcodea. Emite JSON con http_code y healthy. Reemplaza el patron inline 'ssh host -> grep token .env -> curl -H Authorization: Bearer' repetido en monitorizacion."
|
||||
tags: [ssh, systemd, health, curl, remote, service, bearer, loopback, monitoring, infra]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: ssh_host
|
||||
desc: "alias SSH del host remoto definido en ~/.ssh/config (ej: om, organic-machine). Resuelve user/puerto/identityfile del config."
|
||||
- name: local_url
|
||||
desc: "URL del endpoint que el service expone en loopback del host remoto (ej: http://127.0.0.1:8487/agent). No es accesible desde fuera del host."
|
||||
- name: --token-from-env
|
||||
desc: "dos valores: <remote_env_path> <ENV_VAR>. Lee el bearer del .env remoto con grep '^ENV_VAR=' (ej: /home/ubuntu/app/.env AGENTS_API_KEY). El token se resuelve dentro del host, no viaja en argv local."
|
||||
- name: --token
|
||||
desc: "bearer literal (alternativa a --token-from-env). Util para tokens ya en variables de entorno locales; preferir --token-from-env para secretos en disco remoto."
|
||||
- name: --expect-status
|
||||
desc: "codigo HTTP exacto que marca healthy (ej: 200). Si se omite, cualquier 2xx cuenta como healthy."
|
||||
- name: --connect-timeout
|
||||
desc: "timeout de conexion SSH en segundos (default 5)."
|
||||
- name: --curl-timeout
|
||||
desc: "timeout maximo del curl remoto en segundos (default 10)."
|
||||
output: "JSON a stdout: {\"status\":\"ok|error\",\"host\":\"...\",\"url\":\"...\",\"http_code\":NNN,\"healthy\":true|false}. status=error si el SSH fallo sin obtener codigo. healthy=true si http_code coincide con expect-status (o es 2xx por defecto). Exit 0 si healthy, 1 si no, 2 en error de uso."
|
||||
tested: true
|
||||
tests: ["service healthy con token desde env remoto", "service no healthy con http_code 503", "salida JSON nunca filtra el token", "sin token 2xx por defecto es healthy", "falta argumento obligatorio devuelve error de uso", "falta argumento sale con codigo distinto de 0"]
|
||||
test_file_path: "bash/functions/infra/check_service_health_via_ssh_test.sh"
|
||||
file_path: "bash/functions/infra/check_service_health_via_ssh.sh"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
source bash/functions/infra/check_service_health_via_ssh.sh
|
||||
|
||||
# 1) Service en loopback del host 'om' con bearer leido de un .env remoto.
|
||||
# Reemplaza el patron inline de monitorizacion del agents_and_robots.
|
||||
result=$(check_service_health_via_ssh om "http://127.0.0.1:8487/agent" \
|
||||
--token-from-env /home/ubuntu/CodeProyects/agents_and_robots/.env AGENTS_API_KEY \
|
||||
--expect-status 200)
|
||||
echo "$result"
|
||||
# {"status":"ok","host":"om","url":"http://127.0.0.1:8487/agent","http_code":200,"healthy":true}
|
||||
|
||||
# 2) Sin token (endpoint publico del host pero solo accesible por loopback).
|
||||
check_service_health_via_ssh organic-machine "http://127.0.0.1:8080/healthz"
|
||||
# {"status":"ok","host":"organic-machine","url":"http://127.0.0.1:8080/healthz","http_code":200,"healthy":true}
|
||||
|
||||
# 3) Uso como gate en un script de monitorizacion (exit code).
|
||||
if check_service_health_via_ssh om "http://127.0.0.1:8487/agent" \
|
||||
--token-from-env /home/ubuntu/CodeProyects/agents_and_robots/.env AGENTS_API_KEY >/dev/null; then
|
||||
echo "service vivo"
|
||||
else
|
||||
echo "service caido — alertar"
|
||||
fi
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Usala cuando necesites comprobar si un service HTTP de un host remoto esta sano y ese
|
||||
service **solo escucha en loopback** (127.0.0.1) del host, por lo que no puedes
|
||||
curl-earlo directamente desde tu maquina. Tipico de APIs internas detras de un reverse
|
||||
proxy, daemons con bearer auth, o services systemd que exponen un `/health` privado.
|
||||
Antes de reiniciar un service, en un cron de monitorizacion, o como `e2e_check` de un
|
||||
deploy.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Requiere **SSH por key auth** al host (usa `-o BatchMode=yes`): si el host pide
|
||||
password, falla en vez de colgarse. El alias debe estar en `~/.ssh/config`.
|
||||
- El service objetivo **debe escuchar en loopback del host remoto** — la URL se
|
||||
resuelve *dentro* del host. `http://127.0.0.1:PORT` apunta al host remoto, no a tu PC.
|
||||
- **No requiere sudo**: solo lee un `.env` (grep) y hace curl como el usuario SSH.
|
||||
El usuario SSH debe tener permiso de lectura sobre el `.env` remoto.
|
||||
- El **token nunca se imprime ni se hardcodea**: con `--token-from-env` se resuelve
|
||||
dentro del host y solo se usa en el header `Authorization`. Con `--token <literal>`
|
||||
el secreto queda en el argv del comando ssh local — preferir `--token-from-env`
|
||||
para secretos persistidos en disco.
|
||||
- `grep` del `.env` toma la **primera** linea que matchea `^<ENV_VAR>=` y recorta
|
||||
comillas/espacios. Si la var aparece varias veces o usa interpolacion, revisa el match.
|
||||
- `curl -sf` no sigue redirects: un 3xx cuenta como no-2xx (healthy=false salvo
|
||||
`--expect-status` explicito).
|
||||
- Requiere `curl` instalado en el **host remoto** (no en el local).
|
||||
- El JSON de salida se emite siempre (incluso en fallo); el caller decide por el
|
||||
`exit code` (0 healthy, 1 no healthy, 2 error de uso) o por el campo `healthy`.
|
||||
|
||||
## Notas
|
||||
|
||||
- Testeable sin red: el runner SSH es inyectable via `CHECK_HEALTH_SSH_BIN` (un stub
|
||||
que emite el `http_code` deseado), por eso los tests no abren conexiones reales.
|
||||
- El snippet remoto normaliza la salida de curl a un unico `http_code` aunque
|
||||
`curl -sf` devuelva error (emite `<curl_rc>:<http_code>` y la funcion extrae el codigo).
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env bash
|
||||
# check_service_health_via_ssh — Comprueba la salud de un service HTTP que solo
|
||||
# escucha en loopback de un host remoto, entrando por SSH y haciendo curl con
|
||||
# bearer token opcional (leido de un .env remoto o pasado literal).
|
||||
set -euo pipefail
|
||||
|
||||
check_service_health_via_ssh() {
|
||||
local ssh_host="" local_url=""
|
||||
local remote_env_path="" env_var=""
|
||||
local token_literal=""
|
||||
local expect_status="" # vacio = aceptar cualquier 2xx
|
||||
local connect_timeout=5
|
||||
local curl_timeout=10
|
||||
|
||||
# --- parseo de args (posicionales + flags) ---
|
||||
local positional=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--token-from-env)
|
||||
remote_env_path="${2:-}"
|
||||
env_var="${3:-}"
|
||||
if [[ -z "$remote_env_path" || -z "$env_var" ]]; then
|
||||
echo "check_service_health_via_ssh: --token-from-env requiere <remote_env_path> <ENV_VAR>" >&2
|
||||
return 2
|
||||
fi
|
||||
shift 3
|
||||
;;
|
||||
--token)
|
||||
token_literal="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--expect-status)
|
||||
expect_status="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--connect-timeout)
|
||||
connect_timeout="${2:-5}"
|
||||
shift 2
|
||||
;;
|
||||
--curl-timeout)
|
||||
curl_timeout="${2:-10}"
|
||||
shift 2
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
;;
|
||||
-*)
|
||||
echo "check_service_health_via_ssh: flag desconocida '$1'" >&2
|
||||
return 2
|
||||
;;
|
||||
*)
|
||||
positional+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
ssh_host="${positional[0]:-}"
|
||||
local_url="${positional[1]:-}"
|
||||
|
||||
if [[ -z "$ssh_host" || -z "$local_url" ]]; then
|
||||
echo "check_service_health_via_ssh: uso: check_service_health_via_ssh <ssh_host> <local_url> [--token-from-env <remote_env_path> <ENV_VAR>] [--token <literal>] [--expect-status 200]" >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
# --- construir el snippet remoto que se ejecuta dentro del host via SSH ---
|
||||
# El token NUNCA se imprime: se resuelve dentro del host remoto y se usa
|
||||
# directamente en el header Authorization. El snippet emite SOLO el http_code.
|
||||
#
|
||||
# Casos de token:
|
||||
# 1) --token-from-env: lee el valor de <ENV_VAR>= del .env remoto.
|
||||
# 2) --token <literal>: el literal se inyecta en el snippet (cuidado: queda
|
||||
# en argv del comando ssh local; preferir --token-from-env para secretos).
|
||||
# 3) sin token: curl sin header Authorization.
|
||||
local remote_script
|
||||
if [[ -n "$remote_env_path" ]]; then
|
||||
# grep el valor del .env remoto, recortando posibles comillas y espacios.
|
||||
remote_script=$(cat <<REMOTE
|
||||
set -e
|
||||
TOKEN=\$(grep -E '^[[:space:]]*${env_var}[[:space:]]*=' '${remote_env_path}' 2>/dev/null | head -n1 | cut -d= -f2- | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*\$//' -e 's/^["'\'']//' -e 's/["'\'']\$//')
|
||||
if [ -z "\$TOKEN" ]; then
|
||||
echo "000"
|
||||
exit 7
|
||||
fi
|
||||
curl -sf -o /dev/null -w '%{http_code}' --max-time ${curl_timeout} -H "Authorization: Bearer \$TOKEN" '${local_url}' || echo "\$?:\$(curl -s -o /dev/null -w '%{http_code}' --max-time ${curl_timeout} -H "Authorization: Bearer \$TOKEN" '${local_url}' 2>/dev/null)"
|
||||
REMOTE
|
||||
)
|
||||
elif [[ -n "$token_literal" ]]; then
|
||||
remote_script=$(cat <<REMOTE
|
||||
set -e
|
||||
TOKEN='${token_literal}'
|
||||
curl -sf -o /dev/null -w '%{http_code}' --max-time ${curl_timeout} -H "Authorization: Bearer \$TOKEN" '${local_url}' || echo "\$?:\$(curl -s -o /dev/null -w '%{http_code}' --max-time ${curl_timeout} -H "Authorization: Bearer \$TOKEN" '${local_url}' 2>/dev/null)"
|
||||
REMOTE
|
||||
)
|
||||
else
|
||||
remote_script=$(cat <<REMOTE
|
||||
set -e
|
||||
curl -sf -o /dev/null -w '%{http_code}' --max-time ${curl_timeout} '${local_url}' || echo "\$?:\$(curl -s -o /dev/null -w '%{http_code}' --max-time ${curl_timeout} '${local_url}' 2>/dev/null)"
|
||||
REMOTE
|
||||
)
|
||||
fi
|
||||
|
||||
# --- ejecutar via SSH (o via runner inyectado en tests) ---
|
||||
# CHECK_HEALTH_SSH_BIN permite a los tests sustituir el comando ssh por un
|
||||
# stub que devuelve un http_code fijo, sin tocar la red.
|
||||
local ssh_bin="${CHECK_HEALTH_SSH_BIN:-ssh}"
|
||||
local raw rc=0
|
||||
raw=$("$ssh_bin" -o BatchMode=yes -o ConnectTimeout="$connect_timeout" "$ssh_host" "$remote_script" 2>/dev/null) || rc=$?
|
||||
|
||||
# El snippet remoto, cuando curl -sf falla, emite "<curl_rc>:<http_code>".
|
||||
# Cuando curl tiene exito, emite solo "<http_code>". Normalizamos a http_code.
|
||||
local http_code
|
||||
if [[ "$raw" == *:* ]]; then
|
||||
http_code="${raw##*:}"
|
||||
else
|
||||
http_code="$raw"
|
||||
fi
|
||||
# sanitizar: solo digitos; cualquier otra cosa => 000
|
||||
if [[ ! "$http_code" =~ ^[0-9]+$ ]]; then
|
||||
http_code="000"
|
||||
fi
|
||||
|
||||
# Si el SSH en si fallo (conexion, host caido) y no hay codigo util.
|
||||
local status="ok"
|
||||
if [[ "$rc" -ne 0 && "$http_code" == "000" ]]; then
|
||||
status="error"
|
||||
fi
|
||||
|
||||
# --- decidir healthy ---
|
||||
local healthy="false"
|
||||
if [[ -n "$expect_status" ]]; then
|
||||
[[ "$http_code" == "$expect_status" ]] && healthy="true"
|
||||
else
|
||||
# default: cualquier 2xx
|
||||
[[ "$http_code" =~ ^2[0-9][0-9]$ ]] && healthy="true"
|
||||
fi
|
||||
|
||||
printf '{"status":"%s","host":"%s","url":"%s","http_code":%s,"healthy":%s}\n' \
|
||||
"$status" "$ssh_host" "$local_url" "$http_code" "$healthy"
|
||||
|
||||
[[ "$healthy" == "true" ]] && return 0 || return 1
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||
check_service_health_via_ssh "$@"
|
||||
fi
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests para check_service_health_via_ssh
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/check_service_health_via_ssh.sh"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
assert_contains() {
|
||||
local test_name="$1" needle="$2" haystack="$3"
|
||||
if echo "$haystack" | grep -qF "$needle"; then
|
||||
echo "PASS: $test_name"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL: $test_name — expected to contain '$needle'"
|
||||
echo " got: $haystack"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
assert_not_contains() {
|
||||
local test_name="$1" needle="$2" haystack="$3"
|
||||
if ! echo "$haystack" | grep -qF "$needle"; then
|
||||
echo "PASS: $test_name"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL: $test_name — expected NOT to contain '$needle'"
|
||||
echo " got: $haystack"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
# --- stub SSH: en vez de conectarse, lee el .env remoto fake (si el snippet lo
|
||||
# referencia) y emite el http_code de la env var STUB_HTTP_CODE. Simula tanto el
|
||||
# caso "curl exito" (solo http_code) como "curl fallo" (<rc>:<http_code>). ---
|
||||
STUB=$(mktemp)
|
||||
chmod +x "$STUB"
|
||||
cat > "$STUB" <<'STUBEOF'
|
||||
#!/usr/bin/env bash
|
||||
# Stub de ssh para tests. Ignora flags -o ... y el host; el ultimo arg es el
|
||||
# script remoto. Emite el codigo segun STUB_HTTP_CODE / STUB_CURL_RC.
|
||||
code="${STUB_HTTP_CODE:-200}"
|
||||
rc="${STUB_CURL_RC:-0}"
|
||||
# Si el script remoto referencia un .env y STUB_TOKEN_EMPTY=1, simular token vacio.
|
||||
if [[ "${STUB_TOKEN_EMPTY:-0}" == "1" ]]; then
|
||||
echo "000"
|
||||
exit 7
|
||||
fi
|
||||
if [[ "$rc" == "0" ]]; then
|
||||
echo "$code"
|
||||
else
|
||||
echo "${rc}:${code}"
|
||||
exit 0
|
||||
fi
|
||||
STUBEOF
|
||||
chmod +x "$STUB"
|
||||
|
||||
FAKE_ENV=$(mktemp)
|
||||
cat > "$FAKE_ENV" <<'ENVEOF'
|
||||
SOME_OTHER=foo
|
||||
AGENTS_API_KEY=supersecret-token-123
|
||||
ANOTHER=bar
|
||||
ENVEOF
|
||||
|
||||
trap 'rm -f "$STUB" "$FAKE_ENV"' EXIT
|
||||
|
||||
# --- Test: service healthy con token desde .env remoto (200 esperado) ---
|
||||
result=$(CHECK_HEALTH_SSH_BIN="$STUB" STUB_HTTP_CODE=200 \
|
||||
check_service_health_via_ssh om "http://127.0.0.1:8487/agent" \
|
||||
--token-from-env "$FAKE_ENV" AGENTS_API_KEY --expect-status 200) || true
|
||||
assert_contains "service healthy con token desde env remoto" '"healthy":true' "$result"
|
||||
assert_contains "service healthy con token desde env remoto" '"http_code":200' "$result"
|
||||
assert_contains "service healthy con token desde env remoto" '"status":"ok"' "$result"
|
||||
assert_not_contains "service healthy con token desde env remoto" 'supersecret' "$result"
|
||||
|
||||
# --- Test: service no healthy cuando http_code no coincide con expect-status ---
|
||||
result=$(CHECK_HEALTH_SSH_BIN="$STUB" STUB_HTTP_CODE=503 STUB_CURL_RC=22 \
|
||||
check_service_health_via_ssh om "http://127.0.0.1:8487/agent" \
|
||||
--token-from-env "$FAKE_ENV" AGENTS_API_KEY --expect-status 200) || true
|
||||
assert_contains "service no healthy con http_code 503" '"healthy":false' "$result"
|
||||
assert_contains "service no healthy con http_code 503" '"http_code":503' "$result"
|
||||
|
||||
# --- Test: salida JSON nunca filtra el token ---
|
||||
result=$(CHECK_HEALTH_SSH_BIN="$STUB" STUB_HTTP_CODE=200 \
|
||||
check_service_health_via_ssh om "http://127.0.0.1:9000/health" \
|
||||
--token literal-secret-xyz) || true
|
||||
assert_not_contains "salida JSON nunca filtra el token" 'literal-secret-xyz' "$result"
|
||||
assert_contains "salida JSON nunca filtra el token" '"healthy":true' "$result"
|
||||
|
||||
# --- Test: sin token y 2xx por defecto cuenta como healthy ---
|
||||
result=$(CHECK_HEALTH_SSH_BIN="$STUB" STUB_HTTP_CODE=204 \
|
||||
check_service_health_via_ssh om "http://127.0.0.1:8080/ping") || true
|
||||
assert_contains "sin token 2xx por defecto es healthy" '"healthy":true' "$result"
|
||||
assert_contains "sin token 2xx por defecto es healthy" '"http_code":204' "$result"
|
||||
|
||||
# --- Test: falta argumento obligatorio devuelve error de uso ---
|
||||
set +e
|
||||
err=$(check_service_health_via_ssh om 2>&1)
|
||||
ec=$?
|
||||
set -e
|
||||
assert_contains "falta argumento obligatorio devuelve error de uso" 'uso:' "$err"
|
||||
if [[ "$ec" -ne 0 ]]; then
|
||||
echo "PASS: falta argumento sale con codigo distinto de 0"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "FAIL: falta argumento deberia salir != 0 (got $ec)"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
|
||||
echo "---"
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]] || exit 1
|
||||
Reference in New Issue
Block a user