feat: registry_api + fn sync — sincronización de registry.db entre PCs

Nuevo sistema para mantener datos no regenerables (proposals, apps, projects,
analysis, vaults, pc_locations) sincronizados entre múltiples máquinas via
una API HTTP central desplegada en organic-machine.com.

- Migración 011: tabla pc_locations (mapa de ubicaciones por PC)
- registry/models.go: struct PcLocation
- registry/store.go: CRUD PcLocation + helpers de sync
- cmd/fn/sync.go: subcomando fn sync (push+pull, status, locations)
- bash/functions/infra/setup_registry_api: pipeline de deploy Docker+Traefik
- CLAUDE.md: documentación de sync y pc_locations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 02:12:38 +02:00
parent 295ab491a3
commit 28364cf212
9 changed files with 820 additions and 2 deletions
@@ -0,0 +1,85 @@
---
name: setup_registry_api
kind: pipeline
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "setup_registry_api(ssh_host: string, api_token: string, basic_auth_user: string, basic_auth_pass: string) -> json"
description: "Deploy completo de registry_api en VPS con Docker + Traefik (Coolify proxy). Sincroniza el repo via rsync, genera el hash bcrypt para basicAuth, sube el traefik-dynamic.yml, crea el .env con el token, hace docker compose build+up y verifica el health check."
tags: [launcher, deploy, docker, traefik, registry, coolify, infra, ssh]
uses_functions: [rsync_deploy_bash_infra, ssh_exec_go_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: ssh_host
desc: "Alias SSH del VPS destino definido en ~/.ssh/config (default: organic-machine.com)"
- name: api_token
desc: "Token de autenticación para la registry_api (REGISTRY_API_TOKEN). Se escribe en el .env remoto."
- name: basic_auth_user
desc: "Usuario para basicAuth de Traefik (default: lucas). Se usa para generar el hash bcrypt con htpasswd."
- name: basic_auth_pass
desc: "Password para basicAuth de Traefik. Se hashea con bcrypt (htpasswd -nB -C 10) y se escapa a $$ para Traefik."
output: "JSON con status (ok|error), url del servicio, http_code del health check, duration_ms, ssh_host y remote_dir. Exit code 1 si algún paso falla."
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/setup_registry_api.sh"
---
## Requisitos previos
- `htpasswd` instalado localmente (`apt install apache2-utils`)
- `rsync` instalado localmente
- SSH alias `organic-machine.com` (o el host indicado) configurado en `~/.ssh/config`
- El usuario SSH debe tener `sudo` sin password para `mkdir -p /data/coolify/proxy/dynamic/` y `tee` en esa ruta
- Red Docker `coolify` existente en el VPS (se crea automáticamente si no existe)
- Traefik corriendo con file watcher en `/data/coolify/proxy/dynamic/` (Coolify proxy estándar)
## Ejemplo
```bash
# Forma directa como script
bash bash/functions/infra/setup_registry_api.sh \
organic-machine.com \
"mi-token-secreto" \
lucas \
"mi-password"
# Como función sourced
source bash/functions/infra/setup_registry_api.sh
result=$(setup_registry_api \
"organic-machine.com" \
"mi-token-secreto" \
"lucas" \
"mi-password")
echo "$result"
# {"status":"ok","url":"https://registry.organic-machine.com/api/status","http_code":"200","duration_ms":45231,"ssh_host":"organic-machine.com","remote_dir":"/opt/fn-registry-build/apps/registry_api"}
# Via variables de entorno
export REGISTRY_API_TOKEN="mi-token-secreto"
export BASIC_AUTH_PASS="mi-password"
bash bash/functions/infra/setup_registry_api.sh
```
## Pasos del pipeline
1. **Verificar SSH**`ssh -o BatchMode=yes -o ConnectTimeout=10` para confirmar acceso al VPS
2. **Generar hash bcrypt**`htpasswd -nB -C 10` localmente, escapar `$` a `$$` para Traefik
3. **rsync del repo** — sube el repo completo a `/opt/fn-registry-build/` en el VPS (el Dockerfile necesita el contexto raíz)
4. **Subir traefik-dynamic.yml** — reemplaza el placeholder del hash en el template local y lo sube a `/data/coolify/proxy/dynamic/registry-api-organic-machine-com.yml` via `sudo tee`
5. **Crear .env** — escribe `REGISTRY_API_TOKEN=...` en `apps/registry_api/.env` en el VPS
6. **docker compose build && up -d** — construye la imagen (multi-stage, CGO+FTS5) y levanta el container con la red `coolify`
7. **Health check** — polling a `https://registry.organic-machine.com/api/status` cada 10s, máximo 12 intentos (2 minutos)
## Notas
- El `docker-compose.yml` de la app usa `context: ../../` para incluir `registry/`, `functions/`, `cmd/` y `apps/registry_api/` en el build. Por eso se sincroniza el repo completo y no solo la app.
- El Dockerfile genera el binario `registry_api` con `CGO_ENABLED=1 -tags fts5` (SQLite + FTS5). El `registry.db` se genera en el primer arranque via `fn index` dentro del container, o puede montarse externamente via el volumen `/data`.
- Traefik detecta el cambio en `/data/coolify/proxy/dynamic/` automáticamente (file provider con file watcher), sin necesidad de reiniciar Traefik.
- Para re-deploys: ejecutar el mismo script — rsync es idempotente y `docker compose up -d` recrea el container si la imagen cambió.
- Si `REGISTRY_API_TOKEN` está vacío, la API arranca sin autenticación (solo basicAuth de Traefik protege el acceso).
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env bash
# setup_registry_api — Deploy completo de registry_api en VPS con Docker + Traefik (Coolify proxy)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../../../" && pwd)"
source "$SCRIPT_DIR/rsync_deploy.sh"
setup_registry_api() {
local ssh_host="${1:-organic-machine.com}"
local api_token="${2:-}"
local basic_auth_user="${3:-lucas}"
local basic_auth_pass="${4:-}"
if [[ -z "$api_token" ]]; then
echo "setup_registry_api: REGISTRY_API_TOKEN es obligatorio (parametro 2)" >&2
return 1
fi
if [[ -z "$basic_auth_pass" ]]; then
echo "setup_registry_api: basic_auth_pass es obligatorio (parametro 4)" >&2
return 1
fi
local start_ts
start_ts=$(date +%s)
# 1. Verificar conectividad SSH
echo "==> [1/7] Verificando conectividad SSH a '$ssh_host'..." >&2
if ! ssh -o BatchMode=yes -o ConnectTimeout=10 "$ssh_host" true 2>/dev/null; then
echo "setup_registry_api: no se puede conectar a '$ssh_host' via SSH" >&2
return 1
fi
echo " OK: SSH conectado." >&2
# 2. Generar hash bcrypt para basicAuth de Traefik
echo "==> [2/7] Generando hash bcrypt para basicAuth..." >&2
if ! command -v htpasswd &>/dev/null; then
echo "setup_registry_api: 'htpasswd' no encontrado. Instalar con: apt install apache2-utils" >&2
return 1
fi
local traefik_hash
traefik_hash=$(htpasswd -nbB "$basic_auth_user" "$basic_auth_pass" 2>/dev/null)
if [[ -z "$traefik_hash" ]]; then
echo "setup_registry_api: htpasswd no generó un hash válido" >&2
return 1
fi
# For Traefik file provider, use single $ (NOT $$ — that's only for Docker labels)
echo " OK: hash generado para usuario '$basic_auth_user'." >&2
# 3. Subir el repo completo al VPS via rsync (el Dockerfile necesita el contexto completo)
local remote_build_dir="/opt/fn-registry-build"
echo "==> [3/7] Sincronizando repo a '$ssh_host:$remote_build_dir' via rsync..." >&2
rsync_deploy "$REGISTRY_ROOT/" "$ssh_host" "$remote_build_dir" >/dev/null || {
echo "setup_registry_api: rsync falló" >&2
return 1
}
echo " OK: repo sincronizado." >&2
# 4. Subir traefik-dynamic.yml con el hash real a la ruta de Coolify
local traefik_dynamic_path="/data/coolify/proxy/dynamic/registry-api-organic-machine-com.yml"
echo "==> [4/7] Generando y subiendo traefik-dynamic.yml a '$ssh_host:$traefik_dynamic_path'..." >&2
# Leer el template local y sustituir el placeholder
local traefik_template
traefik_template=$(< "$REGISTRY_ROOT/apps/registry_api/traefik-dynamic.yml")
# Reemplazar la línea del usuario placeholder con el hash real
local traefik_rendered
traefik_rendered=$(echo "$traefik_template" | sed "s|.*PLACEHOLDER_BASICAUTH_LINE.*| - \"${traefik_hash}\"|g")
# Crear directorio si no existe y subir
ssh "$ssh_host" "sudo mkdir -p /data/coolify/proxy/dynamic/" >&2
echo "$traefik_rendered" | ssh "$ssh_host" \
"sudo tee '$traefik_dynamic_path' > /dev/null"
echo " OK: traefik-dynamic.yml desplegado en '$traefik_dynamic_path'." >&2
# 5. Crear .env en el VPS con el token de la API
local remote_app_dir="$remote_build_dir/apps/registry_api"
echo "==> [5/7] Creando .env en '$ssh_host:$remote_app_dir'..." >&2
ssh "$ssh_host" "cat > '$remote_app_dir/.env'" <<EOF
REGISTRY_API_TOKEN=${api_token}
EOF
echo " OK: .env creado." >&2
# 6. Verificar que la red coolify existe; si no, crearla
echo "==> [6/7] Verificando red Docker 'coolify' y levantando el stack..." >&2
ssh "$ssh_host" bash <<'REMOTE'
set -euo pipefail
if ! docker network ls --format '{{.Name}}' | grep -q '^coolify$'; then
echo " Creando red Docker 'coolify'..."
docker network create coolify
fi
echo " Red 'coolify' disponible."
REMOTE
# docker compose build && up desde el directorio de la app (contexto es ../../ = remote_build_dir)
ssh "$ssh_host" bash <<REMOTE
set -euo pipefail
cd '$remote_app_dir'
echo " docker compose build..."
docker compose build
echo " docker compose up -d..."
docker compose up -d
echo " Contenedor levantado."
REMOTE
echo " OK: stack Docker levantado." >&2
# 7. Health check
local health_url="https://registry.organic-machine.com/api/status"
echo "==> [7/7] Esperando health check en '$health_url'..." >&2
local attempts=0
local max_attempts=12
local status_code=""
while [[ $attempts -lt $max_attempts ]]; do
status_code=$(curl -sk -o /dev/null -w "%{http_code}" \
-u "${basic_auth_user}:${basic_auth_pass}" \
"$health_url" 2>/dev/null || echo "000")
if [[ "$status_code" == "200" ]]; then
break
fi
attempts=$((attempts + 1))
echo " Intento $attempts/$max_attempts — HTTP $status_code, esperando 10s..." >&2
sleep 10
done
local end_ts
end_ts=$(date +%s)
local duration_ms=$(( (end_ts - start_ts) * 1000 ))
if [[ "$status_code" != "200" ]]; then
printf '{"status":"error","url":"%s","http_code":"%s","duration_ms":%d,"msg":"health check timeout tras %d intentos"}\n' \
"$health_url" "$status_code" "$duration_ms" "$max_attempts"
return 1
fi
echo " OK: servicio respondiendo HTTP 200." >&2
printf '{"status":"ok","url":"%s","http_code":"%s","duration_ms":%d,"ssh_host":"%s","remote_dir":"%s"}\n' \
"$health_url" "$status_code" "$duration_ms" "$ssh_host" "$remote_app_dir"
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
# Uso: setup_registry_api.sh [ssh_host] [api_token] [basic_auth_user] [basic_auth_pass]
# Variables de entorno alternativas: SSH_HOST, REGISTRY_API_TOKEN, BASIC_AUTH_USER, BASIC_AUTH_PASS
setup_registry_api \
"${1:-${SSH_HOST:-organic-machine.com}}" \
"${2:-${REGISTRY_API_TOKEN:-}}" \
"${3:-${BASIC_AUTH_USER:-lucas}}" \
"${4:-${BASIC_AUTH_PASS:-}}"
fi