merge: quick/projects-and-docker-deploy — Docker compose deploy, jupyter fixes, projects structure
This commit is contained in:
@@ -18,3 +18,4 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
|
|||||||
| 12 | [notebook_collaboration.md](notebook_collaboration.md) | Colaboración en notebooks Jupyter via funciones del registry |
|
| 12 | [notebook_collaboration.md](notebook_collaboration.md) | Colaboración en notebooks Jupyter via funciones del registry |
|
||||||
| 13 | [frontend_theming.md](frontend_theming.md) | Componentes propios y sistema de temas en frontends |
|
| 13 | [frontend_theming.md](frontend_theming.md) | Componentes propios y sistema de temas en frontends |
|
||||||
| 14 | [deploy.md](deploy.md) | Deploy de apps a VPS remotos via SSH + systemd + rsync |
|
| 14 | [deploy.md](deploy.md) | Deploy de apps a VPS remotos via SSH + systemd + rsync |
|
||||||
|
| 15 | [projects.md](projects.md) | Projects: agrupar apps, analysis y vaults bajo un tema |
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
## Projects: apps, analysis y vaults bajo un tema comun
|
||||||
|
|
||||||
|
Un project agrupa apps, analyses y vaults relacionados. Vive en `projects/{nombre}/` con esta estructura:
|
||||||
|
|
||||||
|
```
|
||||||
|
projects/{nombre}/
|
||||||
|
project.md # Frontmatter obligatorio (name, description, tags)
|
||||||
|
apps/ # Apps del proyecto (cada una con app.md)
|
||||||
|
{app_name}/
|
||||||
|
app.md
|
||||||
|
...
|
||||||
|
analysis/ # Analyses del proyecto (cada uno con analysis.md)
|
||||||
|
{analysis_name}/
|
||||||
|
analysis.md
|
||||||
|
.venv/
|
||||||
|
notebooks/
|
||||||
|
run-jupyter-lab.sh
|
||||||
|
...
|
||||||
|
vaults/ # Datos del proyecto
|
||||||
|
vault.yaml # Manifest de vaults (nombre, descripcion, path, tags)
|
||||||
|
{vault_name} -> /abs/path # Symlinks a directorios reales de datos
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reglas
|
||||||
|
|
||||||
|
- `project.md` sigue el template de `docs/templates/project.md` — campos: `name`, `description`, `tags`, `repo_url`
|
||||||
|
- `analysis.md` sigue el template de `docs/templates/analysis.md` — `dir_path` debe apuntar a `projects/{nombre}/analysis/{tema}/`
|
||||||
|
- `vault.yaml` lista los vaults con nombre, descripcion, path absoluto y tags
|
||||||
|
- Los vaults reales viven fuera del repo (ej: `~/vaults/{nombre}/`) con symlinks en el proyecto
|
||||||
|
- `fn index` escanea `projects/*/` y setea `project_id` automaticamente en apps, analyses y vaults
|
||||||
|
- Apps y analyses sueltos (sin proyecto) siguen en `apps/` y `analysis/` en la raiz
|
||||||
|
|
||||||
|
### Raiz vs proyecto
|
||||||
|
|
||||||
|
| Ubicacion | Para que |
|
||||||
|
|-----------|---------|
|
||||||
|
| `apps/` | Apps independientes que no pertenecen a ningun proyecto |
|
||||||
|
| `analysis/` | Analyses independientes |
|
||||||
|
| `projects/{nombre}/apps/` | Apps de un proyecto — `project_id` se setea automaticamente |
|
||||||
|
| `projects/{nombre}/analysis/` | Analyses de un proyecto — `project_id` se setea automaticamente |
|
||||||
|
|
||||||
|
### Crear un proyecto nuevo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Crear estructura
|
||||||
|
mkdir -p projects/{nombre}/{apps,analysis,vaults}
|
||||||
|
|
||||||
|
# 2. Crear project.md con frontmatter
|
||||||
|
fn add -k project # genera template
|
||||||
|
|
||||||
|
# 3. Crear vault (datos fuera del repo, symlink dentro)
|
||||||
|
mkdir -p ~/vaults/{vault_name}/{raw,processed,exports}
|
||||||
|
ln -s ~/vaults/{vault_name} projects/{nombre}/vaults/{vault_name}
|
||||||
|
# Crear vault.yaml con la entrada
|
||||||
|
|
||||||
|
# 4. Crear analysis dentro del proyecto
|
||||||
|
fn run init_jupyter_analysis {nombre_analysis} [paquetes...]
|
||||||
|
mv analysis/{nombre_analysis} projects/{nombre}/analysis/
|
||||||
|
# Crear analysis.md con dir_path correcto
|
||||||
|
# Regenerar launcher y kernel startup:
|
||||||
|
source bash/functions/infra/write_jupyter_launcher.sh && write_jupyter_launcher projects/{nombre}/analysis/{tema}
|
||||||
|
source bash/functions/infra/write_jupyter_registry_kernel.sh && write_jupyter_registry_kernel projects/{nombre}/analysis/{tema}
|
||||||
|
|
||||||
|
# 5. Indexar
|
||||||
|
fn index
|
||||||
|
fn show {nombre} # verifica el project y sus componentes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Consultas utiles
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Listar proyectos
|
||||||
|
SELECT id, description FROM projects;
|
||||||
|
|
||||||
|
-- Analysis de un proyecto
|
||||||
|
SELECT id, name, dir_path FROM analysis WHERE project_id = 'app_turismo';
|
||||||
|
|
||||||
|
-- Vaults de un proyecto
|
||||||
|
SELECT id, name, path, symlink FROM vaults WHERE project_id = 'app_turismo';
|
||||||
|
|
||||||
|
-- Apps de un proyecto
|
||||||
|
SELECT id, name, dir_path FROM apps WHERE project_id = 'app_turismo';
|
||||||
|
|
||||||
|
-- Todo lo que pertenece a un proyecto
|
||||||
|
SELECT 'analysis' as tipo, id, name FROM analysis WHERE project_id = ?
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'vault', id, name FROM vaults WHERE project_id = ?
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'app', id, name FROM apps WHERE project_id = ?;
|
||||||
|
```
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -34,6 +34,11 @@ echo $PORT > .jupyter-port
|
|||||||
|
|
||||||
source .venv/bin/activate 2>/dev/null || true
|
source .venv/bin/activate 2>/dev/null || true
|
||||||
|
|
||||||
|
# IPython startup: cargar .ipython/ local (FN_REGISTRY_ROOT, helpers, sys.path)
|
||||||
|
if [ -d "$(pwd)/.ipython" ]; then
|
||||||
|
export IPYTHONDIR="$(pwd)/.ipython"
|
||||||
|
fi
|
||||||
|
|
||||||
if ! python -c "import jupyter_collaboration" 2>/dev/null; then
|
if ! python -c "import jupyter_collaboration" 2>/dev/null; then
|
||||||
echo "ERROR: jupyter-collaboration no esta instalado"
|
echo "ERROR: jupyter-collaboration no esta instalado"
|
||||||
echo "Instala con: uv add jupyter-collaboration"
|
echo "Instala con: uv add jupyter-collaboration"
|
||||||
|
|||||||
@@ -33,7 +33,24 @@ import sqlite3
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# ── FN_REGISTRY_ROOT ────────────────────────────────────────
|
# ── FN_REGISTRY_ROOT ────────────────────────────────────────
|
||||||
FN_REGISTRY_ROOT = Path("${registry_root}")
|
# Prioridad: env var > path hardcoded > descubrimiento automatico
|
||||||
|
def _discover_registry_root():
|
||||||
|
if os.environ.get("FN_REGISTRY_ROOT"):
|
||||||
|
return Path(os.environ["FN_REGISTRY_ROOT"]).resolve()
|
||||||
|
hardcoded = Path("${registry_root}")
|
||||||
|
if (hardcoded / "registry.db").exists():
|
||||||
|
return hardcoded
|
||||||
|
# Subir desde CWD hasta encontrar registry.db
|
||||||
|
p = Path.cwd()
|
||||||
|
for _ in range(10):
|
||||||
|
if (p / "registry.db").exists():
|
||||||
|
return p
|
||||||
|
if p.parent == p:
|
||||||
|
break
|
||||||
|
p = p.parent
|
||||||
|
return hardcoded
|
||||||
|
|
||||||
|
FN_REGISTRY_ROOT = _discover_registry_root()
|
||||||
os.environ["FN_REGISTRY_ROOT"] = str(FN_REGISTRY_ROOT)
|
os.environ["FN_REGISTRY_ROOT"] = str(FN_REGISTRY_ROOT)
|
||||||
|
|
||||||
# ── sys.path: importar funciones Python del registry ────────
|
# ── sys.path: importar funciones Python del registry ────────
|
||||||
|
|||||||
Reference in New Issue
Block a user