Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f48860fcd8 | ||
|
|
955eec6e75 | ||
|
|
9d04c73b20 | ||
|
|
3640cd5754 | ||
|
|
78b97bdd59 | ||
|
|
40cb058fd4 | ||
|
|
0bf571cb93 | ||
|
|
acf7521990 |
No files matched your search
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# Reconecta a la sesion tmux persistente "lab" dentro del contenedor
|
||||
# tmux-host. Si la sesion no existe (p.ej. el contenedor se acaba de
|
||||
# recrear), la crea. El comando es siempre el mismo sin importar el
|
||||
# estado previo.
|
||||
#
|
||||
# Ver docs-portal: arquitectura/sesion-tmux-persistente.md
|
||||
|
||||
CONTAINER="tmux-host"
|
||||
SESSION="lab"
|
||||
# Repo raiz del lab (CLAUDE.md, apps-registry/, platform-infra/, scripts/).
|
||||
# Mismo path que el WORKDIR de scripts/tmux-host/Dockerfile -- pasado
|
||||
# explicito acá (-c) para no depender de que docker exec herede el WORKDIR
|
||||
# de la imagen.
|
||||
WORKDIR="/DATA/AppData/k3d-lab-v3/lab-cruzcloud"
|
||||
|
||||
if ! docker ps --format '{{.Names}}' | grep -qx "$CONTAINER"; then
|
||||
echo "El contenedor '$CONTAINER' no esta corriendo. Verifica con 'docker ps -a'." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker exec -it "$CONTAINER" tmux attach -t "$SESSION" || \
|
||||
docker exec -it "$CONTAINER" tmux new -s "$SESSION" -c "$WORKDIR"
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# Promueve el tag de imagen del frontend actualmente desplegado en Staging
|
||||
# (workloads/ecommerce-staging/frontend/patches/deployment.yaml) al
|
||||
# manifiesto real de Producción (workloads/ecommerce/frontend/patches/
|
||||
# deployment.yaml). Es la contraparte manual del job "Actualizar
|
||||
# Manifiesto GitOps (CD)" de .gitea/workflows/build.yaml, que desde
|
||||
# 2026-09-14 solo actualiza Staging automáticamente en cada push a main.
|
||||
#
|
||||
# Asume que este repo (scripts) y apps-registry están clonados como
|
||||
# hermanos bajo el mismo directorio del lab (mismo layout que
|
||||
# lab-cruzcloud/{apps-registry,platform-infra,scripts} en esta NAS).
|
||||
|
||||
APPS_REGISTRY_DIR="${APPS_REGISTRY_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../apps-registry" && pwd)}"
|
||||
STAGING_MANIFEST="${APPS_REGISTRY_DIR}/workloads/ecommerce-staging/frontend/patches/deployment.yaml"
|
||||
PROD_MANIFEST="${APPS_REGISTRY_DIR}/workloads/ecommerce/frontend/patches/deployment.yaml"
|
||||
IMAGE_NAME="gitea.cruzcloud.net/devops/ecommerce-frontend"
|
||||
|
||||
test -d "${APPS_REGISTRY_DIR}/.git" || {
|
||||
echo "ERROR: no se encontró un checkout de apps-registry en ${APPS_REGISTRY_DIR}."
|
||||
echo "Definí APPS_REGISTRY_DIR=/ruta/al/checkout si no está junto a este repo."
|
||||
exit 1
|
||||
}
|
||||
|
||||
STAGING_VERSION="$(grep -oE "${IMAGE_NAME}:v[0-9.]+" "${STAGING_MANIFEST}" | head -1 | cut -d: -f2)"
|
||||
test -n "${STAGING_VERSION}" || {
|
||||
echo "ERROR: no se pudo leer la versión actual de Staging desde ${STAGING_MANIFEST}."
|
||||
exit 1
|
||||
}
|
||||
|
||||
CURRENT_PROD_VERSION="$(grep -oE "${IMAGE_NAME}:v[0-9.]+" "${PROD_MANIFEST}" | head -1 | cut -d: -f2)"
|
||||
|
||||
echo "Staging: ${STAGING_VERSION}"
|
||||
echo "Producción: ${CURRENT_PROD_VERSION}"
|
||||
|
||||
if [[ "${STAGING_VERSION}" == "${CURRENT_PROD_VERSION}" ]]; then
|
||||
echo "Producción ya está en ${STAGING_VERSION}. Nada que promover."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
read -rp "¿Promover ${STAGING_VERSION} a Producción? [s/N] " CONFIRM
|
||||
[[ "${CONFIRM}" =~ ^[sS]$ ]] || { echo "Cancelado."; exit 1; }
|
||||
|
||||
cd "${APPS_REGISTRY_DIR}"
|
||||
git fetch origin main
|
||||
git checkout -B main origin/main
|
||||
|
||||
sed -i -E "s|(image: ${IMAGE_NAME}:).*|\1${STAGING_VERSION}|g" "${PROD_MANIFEST}"
|
||||
|
||||
# Mismo bump que hace build.yaml en Staging -- el footer lee APP_VERSION
|
||||
# para mostrar la version real desplegada.
|
||||
perl -0777 -pi -e "s/(- name: APP_VERSION\n\s*value: ).*/\${1}${STAGING_VERSION}/" "${PROD_MANIFEST}"
|
||||
|
||||
git add "${PROD_MANIFEST}"
|
||||
if git diff --cached --quiet; then
|
||||
echo "No hay cambios en el manifiesto de Producción."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git commit -m "chore: promote ${STAGING_VERSION} to production"
|
||||
git push origin HEAD:main
|
||||
|
||||
echo "Promovido. Argo CD sincroniza ecommerce-app en unos segundos."
|
||||
@@ -0,0 +1,23 @@
|
||||
# Imagen de tmux-host: sesion tmux persistente para Claude Code sobre el
|
||||
# lab GitOps de cruzcloud. Reemplaza el `docker run ... alpine sh -c "apk
|
||||
# add ... && npm install ..."` inline documentado antes en
|
||||
# docs/arquitectura/sesion-tmux-persistente.md — mismo contenido, pero
|
||||
# construido una vez como imagen en vez de reinstalar todo en cada arranque
|
||||
# del contenedor.
|
||||
FROM alpine:3.24
|
||||
|
||||
RUN apk add --no-cache tmux docker-cli bash curl git kubectl nodejs npm \
|
||||
&& npm install -g @anthropic-ai/claude-code \
|
||||
&& git config --global --add safe.directory '*'
|
||||
|
||||
# Repo raiz del lab (CLAUDE.md, apps-registry/, platform-infra/, scripts/).
|
||||
# docker exec hereda este WORKDIR como cwd por defecto, y ademas
|
||||
# lab-session.sh y healthcheck.sh lo pasan explicito via `tmux ... -c`
|
||||
# para no depender de ese comportamiento implicito.
|
||||
WORKDIR /DATA/AppData/k3d-lab-v3/lab-cruzcloud
|
||||
|
||||
COPY healthcheck.sh /usr/local/bin/healthcheck.sh
|
||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/healthcheck.sh /usr/local/bin/entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
# Proceso principal (PID 1) del contenedor tmux-host: lanza el chequeo de
|
||||
# salud en background y mantiene el contenedor vivo, igual que el
|
||||
# `tail -f /dev/null` del `docker run` inline original.
|
||||
set -eu
|
||||
|
||||
/usr/local/bin/healthcheck.sh &
|
||||
|
||||
exec tail -f /dev/null
|
||||
Executable
+155
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env bash
|
||||
# Chequeo de salud de tmux-host, corre en background dentro del propio
|
||||
# contenedor (ver entrypoint.sh). Cada HEALTHCHECK_INTERVAL_SEC (30 min
|
||||
# por defecto):
|
||||
#
|
||||
# a) Conectividad de red real (curl a gitea.cruzcloud.net). Si falla,
|
||||
# SOLO se registra en el log y se notifica por Telegram (si el canal
|
||||
# del lab-agent ya esta configurado) -- nunca se intenta "arreglar"
|
||||
# la red del host desde aca, eso puede ser un problema de
|
||||
# hardware/BIOS fuera del alcance del contenedor.
|
||||
# b) Existencia de la sesion tmux "lab". Si no existe (crash, o el
|
||||
# contenedor se acaba de recrear), SI se recrea automaticamente --
|
||||
# es solo recuperar el propio entorno de trabajo, no tocar
|
||||
# infraestructura externa. Al recrearla tambien se lanza `claude`
|
||||
# adentro (ver launch_claude_in_session) -- una sesion recreada sin
|
||||
# esto es un shell pelado, no "el entorno de trabajo".
|
||||
#
|
||||
# Todo evento (chequeo ok, falla, accion tomada) queda en LOG_FILE, que
|
||||
# vive bajo /DATA (persistente, sobrevive a recrear el contenedor) y no
|
||||
# solo en el stdout del contenedor.
|
||||
#
|
||||
# set -u sin 'e': un check individual que falle no debe matar el loop
|
||||
# completo -- el siguiente ciclo tiene que seguir corriendo igual.
|
||||
set -uo pipefail
|
||||
|
||||
WORKDIR="/DATA/AppData/k3d-lab-v3/lab-cruzcloud"
|
||||
SESSION="lab"
|
||||
LOG_DIR="/DATA/AppData/k3d-lab-v3/tmux-host/logs"
|
||||
LOG_FILE="${LOG_DIR}/health.log"
|
||||
# Reusa el mismo canal de Telegram que ya configuro scripts/lab-agent/ --
|
||||
# una sola fuente de verdad para el bot token / chat id, en vez de
|
||||
# duplicar credenciales entre los dos agentes del lab.
|
||||
LAB_AGENT_ENV="${WORKDIR}/scripts/lab-agent/lab-agent.env"
|
||||
NETWORK_CHECK_URL="https://gitea.cruzcloud.net/"
|
||||
INTERVAL_SEC="${HEALTHCHECK_INTERVAL_SEC:-1800}"
|
||||
# Cuanto esperar, SOLO al arrancar, a que WORKDIR este disponible antes de
|
||||
# tocarlo. Tras un `docker restart` normal el host y sus mounts ya estaban
|
||||
# arriba; tras un reinicio COMPLETO del NAS (corte de luz), Docker puede
|
||||
# arrancar este contenedor antes de que /media/HDD-Storage termine de
|
||||
# montarse en el host -- y WORKDIR llega a el via el symlink absoluto
|
||||
# /DATA/AppData -> /media/HDD-Storage/AppData (ver el gotcha del symlink en
|
||||
# docs-portal/docs/arquitectura/sesion-tmux-persistente.md). Sin esta
|
||||
# espera, `mkdir -p "$LOG_DIR"` de mas abajo fallaba en silencio (set -u sin
|
||||
# -e) y ese arranque quedaba SIN NINGUN rastro en el log -- asi se paso por
|
||||
# alto originalmente el gotcha de "no lanzo claude" reportado 2026-09-19.
|
||||
STARTUP_MOUNT_WAIT_MAX_SEC="${STARTUP_MOUNT_WAIT_MAX_SEC:-120}"
|
||||
STARTUP_MOUNT_WAIT_STEP_SEC=5
|
||||
|
||||
wait_for_workdir() {
|
||||
local waited=0
|
||||
while [[ ! -d "$WORKDIR" ]] && [[ "$waited" -lt "$STARTUP_MOUNT_WAIT_MAX_SEC" ]]; do
|
||||
sleep "$STARTUP_MOUNT_WAIT_STEP_SEC"
|
||||
waited=$((waited + STARTUP_MOUNT_WAIT_STEP_SEC))
|
||||
done
|
||||
echo "$waited"
|
||||
}
|
||||
|
||||
STARTUP_WAITED_SEC="$(wait_for_workdir)"
|
||||
|
||||
mkdir -p "$LOG_DIR" 2>/dev/null || true
|
||||
|
||||
log() {
|
||||
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" >> "$LOG_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
if [[ ! -d "$WORKDIR" ]]; then
|
||||
log "ARRANQUE: ERROR -- ${WORKDIR} sigue sin existir tras esperar ${STARTUP_MOUNT_WAIT_MAX_SEC}s. El mount de /DATA o /media/HDD-Storage del host no esta listo; revisar fuera del contenedor."
|
||||
elif [[ "$STARTUP_WAITED_SEC" -gt 0 ]]; then
|
||||
log "ARRANQUE: ${WORKDIR} tardo ${STARTUP_WAITED_SEC}s en estar disponible (probable reinicio completo del host reordenando el montaje de /DATA)."
|
||||
fi
|
||||
|
||||
notify_telegram() {
|
||||
local title="$1" message="$2"
|
||||
if [[ ! -f "$LAB_AGENT_ENV" ]]; then
|
||||
log "NOTIFY omitida: no existe ${LAB_AGENT_ENV}"
|
||||
return 0
|
||||
fi
|
||||
local token_file chat_id token resp
|
||||
token_file="$(grep -E '^TELEGRAM_BOT_TOKEN_FILE=' "$LAB_AGENT_ENV" | cut -d= -f2-)"
|
||||
chat_id="$(grep -E '^TELEGRAM_CHAT_ID=' "$LAB_AGENT_ENV" | cut -d= -f2-)"
|
||||
if [[ -z "$token_file" || ! -r "$token_file" || -z "$chat_id" ]]; then
|
||||
log "NOTIFY omitida: canal de Telegram del lab-agent no esta configurado todavia"
|
||||
return 0
|
||||
fi
|
||||
token="$(<"$token_file")"
|
||||
resp="$(curl -s -m 10 -X POST "https://api.telegram.org/bot${token}/sendMessage" \
|
||||
--data-urlencode "chat_id=${chat_id}" \
|
||||
--data-urlencode "text=🤖 tmux-host healthcheck
|
||||
${title}
|
||||
──────────────
|
||||
${message}" 2>&1)"
|
||||
token=""
|
||||
[[ "$resp" == *'"ok":true'* ]] || log "WARN: fallo al notificar por Telegram: ${resp}"
|
||||
}
|
||||
|
||||
check_network() {
|
||||
local code
|
||||
code="$(curl -sL -o /dev/null -w '%{http_code}' --max-time 10 "$NETWORK_CHECK_URL" 2>/dev/null || echo "000")"
|
||||
if [[ "$code" == "000" ]]; then
|
||||
log "RED: sin conectividad real (curl ${NETWORK_CHECK_URL} sin respuesta). No se intenta corregir desde el contenedor."
|
||||
notify_telegram "Sin conectividad" "curl ${NETWORK_CHECK_URL} no respondio desde tmux-host (posible falla de red del host/NAS, ej. hardware/BIOS). El contenedor NO intenta corregir esto, solo lo registra."
|
||||
else
|
||||
log "RED: ok (curl ${NETWORK_CHECK_URL} -> HTTP ${code})"
|
||||
fi
|
||||
}
|
||||
|
||||
launch_claude_in_session() {
|
||||
# El pane recien creado tarda un instante en tener su shell listo para
|
||||
# recibir input -- un send-keys inmediato a veces se pierde. 1s alcanza
|
||||
# sobrado para un shell interactivo local (no depende de red).
|
||||
sleep 1
|
||||
# --remote-control: sin esto la sesion nunca queda visible como
|
||||
# controlable remotamente en claude.ai (confirmado en vivo 2026-09-20 --
|
||||
# sin la flag, "remoteEnabled" queda en false para siempre porque nadie
|
||||
# la pide). El nombre de sesion remota usa el hostname del contenedor
|
||||
# (ZimaOS) como prefijo por defecto.
|
||||
if tmux send-keys -t "$SESSION" "claude --remote-control" Enter 2>>"$LOG_FILE"; then
|
||||
log "TMUX: 'claude --remote-control' lanzado dentro de la sesion recien creada"
|
||||
else
|
||||
log "TMUX: ERROR al lanzar 'claude --remote-control' dentro de la sesion recien creada"
|
||||
fi
|
||||
}
|
||||
|
||||
check_tmux_session() {
|
||||
if tmux has-session -t "$SESSION" 2>/dev/null; then
|
||||
log "TMUX: sesion '${SESSION}' ok"
|
||||
return
|
||||
fi
|
||||
if [[ ! -d "$WORKDIR" ]]; then
|
||||
# No se crea sobre un WORKDIR ausente (quedaria posicionada en /). Se
|
||||
# reintenta en el proximo ciclo -- ver STARTUP_MOUNT_WAIT_MAX_SEC arriba
|
||||
# para el caso especifico de arranque en frio del host.
|
||||
log "TMUX: sesion '${SESSION}' no existe y ${WORKDIR} tampoco -- no se crea esta vuelta, se reintenta en el proximo ciclo (${INTERVAL_SEC}s)."
|
||||
return
|
||||
fi
|
||||
log "TMUX: sesion '${SESSION}' no existe -- recreando en ${WORKDIR}"
|
||||
if tmux new-session -d -s "$SESSION" -c "$WORKDIR" 2>>"$LOG_FILE"; then
|
||||
log "TMUX: sesion '${SESSION}' recreada exitosamente"
|
||||
# Una sesion recreada sin esto es un shell vacio hasta que alguien
|
||||
# entre a mano y escriba "claude" -- exactamente el gotcha reportado
|
||||
# tras el reinicio completo del NAS del 2026-09-18/19: la sesion SI se
|
||||
# recreaba sola (confirmado en el log de ese arranque), pero Claude
|
||||
# Code nunca arrancaba adentro.
|
||||
launch_claude_in_session
|
||||
else
|
||||
log "TMUX: ERROR al recrear la sesion '${SESSION}'"
|
||||
fi
|
||||
}
|
||||
|
||||
log "=== healthcheck inicio (intervalo=${INTERVAL_SEC}s) ==="
|
||||
while true; do
|
||||
check_network
|
||||
check_tmux_session
|
||||
sleep "$INTERVAL_SEC"
|
||||
done
|
||||
Reference in new issue
Block a user