Script suiecoffee.py detecta contenedores de un entorno u otro ene ejeccuón, también los inicia o deteniene desde un menú.

Se agregó dentro del  docker-compose.yml la variable

name: ${COMPOSE_PROJECT_NAME:-suitecoffee}

Para crear el nombre del proyecto, el nombre está defininido en los .env respectivos para cada entorno
This commit is contained in:
Mateo Saldain 2025-08-17 04:37:18 +00:00
parent 97db600b1f
commit 2b47faf66a
3 changed files with 284 additions and 7 deletions

View File

@ -160,12 +160,12 @@ services:
# - suitecoffee-net
volumes:
dev-tenants-data:
dev-suitecoffee-data:
dev-npm_data:
dev-npm_letsencrypt:
dev-dbeaver_logs:
dev-dbeaver_workspace:
tenants-data:
suitecoffee-data:
npm_data:
npm_letsencrypt:
dbeaver_logs:
dbeaver_workspace:
networks:
suitecoffee-net:

View File

@ -1,5 +1,6 @@
# docker-compose.yml
# Docker Comose para entorno deproducción o production.
# Docker Comose para entorno de producción o production.
name: ${COMPOSE_PROJECT_NAME:-suitecoffee}
services:

View File

@ -0,0 +1,276 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import subprocess
from shutil import which
PROJECT_ROOT = os.path.abspath(os.getcwd())
# Archivos comunes
BASE_COMPOSE = os.path.join(PROJECT_ROOT, "docker-compose.yml")
OVERRIDE_COMPOSE = os.path.join(PROJECT_ROOT, "docker-compose.override.yml")
# Mapeo de entornos -> archivo .env
ENV_FILES = {
"development": ".env.development",
"production": ".env.production",
}
# ---------- Nuevas utilidades ----------
def resolve_project_name(env_file=None, include_override=True):
"""
Obtiene el 'project name' que usará docker compose para esta combinación de archivos/env,
consultando a 'docker compose config --format json'. Si falla, usa el nombre de la carpeta.
"""
cmd = ["docker", "compose"] + compose_files_args(include_override=include_override)
if env_file:
cmd += ["--env-file", env_file]
cmd += ["config", "--format", "json"]
proc = run(cmd, capture_output=True)
if proc.returncode == 0:
try:
data = json.loads(proc.stdout)
# Compose v2 devuelve 'name' en el JSON; si no, fallback
return data.get("name") or os.path.basename(PROJECT_ROOT)
except Exception:
return os.path.basename(PROJECT_ROOT)
return os.path.basename(PROJECT_ROOT)
def list_project_containers(project_name, all_states=True):
"""
Lista contenedores del proyecto por etiqueta com.docker.compose.project=<name>.
Si all_states=False, solo los running.
Devuelve lista de dicts con {id, name, status, image}.
"""
base = ["docker", "ps"]
if all_states:
base.append("-a")
base += ["--filter", f"label=com.docker.compose.project={project_name}",
"--format", "{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Image}}"]
proc = run(base, capture_output=True)
if proc.returncode != 0:
return []
rows = []
for line in proc.stdout.splitlines():
parts = line.strip().split("\t")
if len(parts) >= 4:
rows.append({"id": parts[0], "name": parts[1], "status": parts[2], "image": parts[3]})
return rows
def print_containers_table(title, rows):
print_header(title)
if not rows:
print("(ninguno)\n")
return
# ancho simple, sin dependencias
print(f"{'ID':<12} {'NAME':<40} {'STATUS':<20} IMAGE")
for r in rows:
print(f"{r['id']:<12} {r['name']:<40} {r['status']:<20} {r['image']}")
print()
# ---------- Utilidades ----------
def check_prereqs():
if which("docker") is None:
fail("No se encontró 'docker' en el PATH.")
# Verificar que docker compose esté disponible (subcomando integrado)
try:
run(["docker", "compose", "version"], check=True, capture_output=True)
except Exception:
fail("No se pudo ejecutar 'docker compose'. Asegúrate de tener Docker Compose v2.")
def run(cmd, check=False, capture_output=False):
return subprocess.run(
cmd,
check=check,
capture_output=capture_output,
text=True
)
def compose_files_args(include_override=True):
args = []
if os.path.exists(BASE_COMPOSE):
args += ["-f", BASE_COMPOSE]
else:
fail("No se encontró docker-compose.yml en la raíz del proyecto.")
if include_override and os.path.exists(OVERRIDE_COMPOSE):
args += ["-f", OVERRIDE_COMPOSE]
return args
def env_file_path(env_key):
fname = ENV_FILES.get(env_key)
if not fname:
return None
path = os.path.join(PROJECT_ROOT, fname)
return path if os.path.exists(path) else None
def compose_cmd(base_args, env_file=None, include_override=True):
"""
Construye el comando docker compose con los -f adecuados
y opcionalmente --env-file si existe (antes del subcomando).
"""
cmd = ["docker", "compose"]
cmd += compose_files_args(include_override=include_override)
if env_file:
cmd += ["--env-file", env_file] # opción global antes del subcomando
cmd += base_args
return cmd
def get_running_containers():
"""
Devuelve lista de container IDs en estado 'running' para este proyecto.
"""
cmd = compose_cmd(["ps", "--status", "running", "-q"])
proc = run(cmd, capture_output=True)
if proc.returncode != 0:
return []
lines = [l.strip() for l in proc.stdout.splitlines() if l.strip()]
return lines
def yes_no(prompt, default="n"):
"""
Pregunta si/no. default: 'y' o 'n'
"""
default = default.lower()
hint = "[Y/n]" if default == "y" else "[y/N]"
while True:
resp = input(f"{prompt} {hint} ").strip().lower()
if not resp:
return default == "y"
if resp in ("y", "yes", "s", "si", ""):
return True
if resp in ("n", "no"):
return False
print("Respuesta no reconocida. Por favor, responde con 'y' o 'n'.")
def print_header(title):
print("\n" + "=" * 60)
print(title)
print("=" * 60 + "\n")
def info(msg): print(f"{msg}")
def ok(msg): print(f"{msg}")
def warn(msg): print(f"! {msg}")
def fail(msg):
print(f"{msg}")
sys.exit(1)
# ---------- Acciones ----------
def bring_up(env_key, include_override=True):
env_path = env_file_path(env_key)
if not env_path:
warn(f"No se encontró archivo de entorno para '{env_key}'. Continuando sin --env-file.")
cmd = compose_cmd(["up", "-d"], env_file=env_path, include_override=include_override)
info("Ejecutando: " + " ".join(cmd))
proc = run(cmd)
if proc.returncode == 0:
ok(f"Entorno '{env_key}' levantado correctamente.")
else:
fail(f"Fallo al levantar entorno '{env_key}'. Código: {proc.returncode}")
def bring_down(env_key=None):
"""
Intenta apagar usando el env proporcionado si existe el .env.
Si no se pasa env_key o no existe el .env, hace un down genérico.
"""
env_path = env_file_path(env_key) if env_key else None
cmd = compose_cmd(["down"], env_file=env_path)
info("Ejecutando: " + " ".join(cmd))
proc = run(cmd)
if proc.returncode == 0:
ok("Contenedores detenidos y red/volúmenes del proyecto desmontados (según corresponda).")
else:
fail(f"Fallo al detener el entorno. Código: {proc.returncode}")
def main_menu():
print_header("Gestor de entornos Docker Compose")
print("Selecciona una opción:")
print(" 1) Levantar entorno de DESARROLLO")
print(" 2) Levantar entorno de PRODUCCIÓN")
print(" 3) Salir")
while True:
choice = input("> ").strip()
if choice == "1":
bring_up("development") # incluye override
return
elif choice == "2":
bring_up("production", include_override=False) # sin override
return
elif choice == "3":
ok("Saliendo.")
sys.exit(0)
else:
print("Opción inválida. Elige 1, 2 o 3.")
def detect_and_offer_shutdown():
# Paths de env (si existen)
dev_env = env_file_path("development")
prod_env = env_file_path("production")
# Helper: obtiene IDs running para una combinación de files/env
def running_ids(env_path, include_override):
cmd = compose_cmd(["ps", "--status", "running", "-q"],
env_file=env_path,
include_override=include_override)
proc = run(cmd, capture_output=True)
if proc.returncode != 0:
return []
return [l.strip() for l in proc.stdout.splitlines() if l.strip()]
# Dev usa override; Prod no
dev_running = running_ids(dev_env, include_override=True)
prod_running = running_ids(prod_env, include_override=False)
any_running = bool(dev_running or prod_running)
if any_running:
print_header("Contenedores activos detectados")
info(f"DESARROLLO: {len(dev_running)} contenedor(es) en ejecución.")
info(f"PRODUCCIÓN: {len(prod_running)} contenedor(es) en ejecución.\n")
options = []
if dev_running:
options.append(("1", "Apagar entorno de DESARROLLO", ("development", True)))
if prod_running:
options.append(("2", "Apagar entorno de PRODUCCIÓN", ("production", False)))
options.append(("3", "Mantener todo como está y salir", None))
print("Selecciona una opción:")
for key, label, _ in options:
print(f" {key}) {label}")
while True:
choice = input("> ").strip()
selected = next((opt for opt in options if opt[0] == choice), None)
if not selected:
print("Opción inválida.")
continue
if choice == "3":
ok("Se mantiene el estado actual.")
sys.exit(0)
env_key, _include_override = selected[2]
info(f"Intentando apagar entorno de {env_key.upper()}")
bring_down(env_key) # ya respeta --env-file y el include_override de prod no usa override
ok("Listo.")
break
else:
info("No hay contenedores activos del proyecto.")
def main():
try:
check_prereqs()
detect_and_offer_shutdown()
main_menu()
except KeyboardInterrupt:
print("\n")
ok("Interrumpido por el usuario (Ctrl+C). Saliendo.")
sys.exit(0)
except Exception as e:
fail(f"Ocurrió un error inesperado: {e}")
if __name__ == "__main__":
main()