SuiteCoffee/suitecoffee.py
msaldain c42814f963 Varios cambios realizados en cuando a la organización de los compose de docker.
Se adoptó la versión actualmente recomandada por el equipo de docker el formado "compose.yaml"

También se crearon nuevos scripts y actualizaron algunos para adaptarse a los nuevos archivos.
2025-08-19 18:26:21 +00:00

387 lines
14 KiB
Python

#!/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 Compose (entornos) ===
BASE_COMPOSE = os.path.join(PROJECT_ROOT, "compose.yaml")
DEV_COMPOSE = os.path.join(PROJECT_ROOT, "compose.dev.yaml")
PROD_COMPOSE = os.path.join(PROJECT_ROOT, "compose.prod.yaml")
# === Archivos Compose (globales) ===
NPM_COMPOSE = os.path.join(PROJECT_ROOT, "compose.npm.yaml")
DBEAVER_COMPOSE = os.path.join(PROJECT_ROOT, "compose.dbeaver.yaml")
# Archivos .env
ENV_FILES = {
"development": ".env.development",
"production": ".env.production",
}
# Nombres de proyecto para permitir DEV y PROD simultáneos
def _base_project():
return os.path.basename(PROJECT_ROOT).lower() or "composeproj"
PROJECT_NAMES = {
"development": f"{_base_project()}_dev",
"production": f"{_base_project()}_prod",
}
# Nombre de proyecto global (ambos yaml globales usan name: suitecoffee)
GLOBAL_PROJECT_NAME = "suitecoffee"
# ---------- Utilidades ----------
def check_prereqs():
if which("docker") is None:
fail("No se encontró 'docker' en el PATH.")
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(env_key):
"""
Devuelve los -f correctos según el entorno (dev/prod) + base.
"""
if not os.path.exists(BASE_COMPOSE):
fail("No se encontró compose.yaml en la raíz del proyecto.")
args = ["-f", BASE_COMPOSE]
if env_key == "development":
if not os.path.exists(DEV_COMPOSE):
fail("No se encontró compose.dev.yaml.")
args += ["-f", DEV_COMPOSE]
elif env_key == "production":
if not os.path.exists(PROD_COMPOSE):
fail("No se encontró compose.prod.yaml.")
args += ["-f", PROD_COMPOSE]
else:
fail(f"Entorno desconocido: {env_key}")
return args
def compose_files_args_global(kind):
"""
Devuelve los -f correctos para servicios globales (npm/dbeaver).
"""
if kind == "npm":
if not os.path.exists(NPM_COMPOSE):
fail("No se encontró compose.npm.yaml.")
return ["-f", NPM_COMPOSE]
elif kind == "dbeaver":
if not os.path.exists(DBEAVER_COMPOSE):
fail("No se encontró compose.dbeaver.yaml.")
return ["-f", DBEAVER_COMPOSE]
else:
fail(f"Servicio global desconocido: {kind}")
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_key, env_file=None, project_name=None):
"""
Construye: docker compose -f base -f env --env-file ... -p <name> <COMANDO> [OPCIONES]
(importante: --env-file y -p son opciones globales y van antes del subcomando)
"""
cmd = ["docker", "compose"]
cmd += compose_files_args(env_key)
if env_file:
cmd += ["--env-file", env_file]
if project_name:
cmd += ["-p", project_name]
cmd += base_args # ["up","-d","--force-recreate"] o ["ps","--status","running","-q"]
return cmd
def compose_cmd_global(base_args, kind, project_name=GLOBAL_PROJECT_NAME):
"""
Comandos para servicios globales (npm/dbeaver):
docker compose -f compose.<kind>.yaml -p suitecoffee <COMANDO> ...
"""
cmd = ["docker", "compose"]
cmd += compose_files_args_global(kind)
if project_name:
cmd += ["-p", project_name]
cmd += base_args
return cmd
def yes_no(prompt, default="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)
# ---------- Helpers globales (servicios por archivo y ps por labels) ----------
def list_services_from_compose_file(compose_path):
"""Obtiene la lista de servicios definidos en un archivo compose específico."""
cmd = ["docker", "compose", "-f", compose_path, "config", "--services"]
proc = run(cmd, capture_output=True)
if proc.returncode != 0:
return []
return [s.strip() for s in proc.stdout.splitlines() if s.strip()]
def list_services_global(kind):
compose_path = NPM_COMPOSE if kind == "npm" else DBEAVER_COMPOSE
return list_services_from_compose_file(compose_path)
def docker_ps_by_labels(project, service=None, running_only=True):
"""Lista contenedores por labels de compose (project/service)."""
cmd = ["docker", "ps"]
if running_only:
# por defecto docker ps ya lista solo running; se deja explícito por claridad
pass
cmd += ["--filter", f"label=com.docker.compose.project={project}"]
if service:
cmd += ["--filter", f"label=com.docker.compose.service={service}"]
cmd += ["-q"]
proc = run(cmd, capture_output=True)
if proc.returncode != 0:
return []
return [l.strip() for l in proc.stdout.splitlines() if l.strip()]
# ---------- Acciones (entornos) ----------
def bring_up(env_key, force_recreate=False):
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.")
pname = PROJECT_NAMES.get(env_key)
base_args = ["up", "-d"]
if force_recreate:
base_args.append("--force-recreate")
cmd = compose_cmd(base_args, env_key=env_key, env_file=env_path, project_name=pname)
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):
env_path = env_file_path(env_key)
pname = PROJECT_NAMES.get(env_key)
cmd = compose_cmd(["down"], env_key=env_key, env_file=env_path, project_name=pname)
info("Ejecutando: " + " ".join(cmd))
proc = run(cmd)
if proc.returncode == 0:
ok(f"Entorno '{env_key}' detenido correctamente.")
else:
fail(f"Fallo al detener entorno '{env_key}'. Código: {proc.returncode}")
def running_ids(env_key):
env_path = env_file_path(env_key)
pname = PROJECT_NAMES.get(env_key)
cmd = compose_cmd(["ps", "--status", "running", "-q"], env_key=env_key, env_file=env_path, project_name=pname)
proc = run(cmd, capture_output=True)
if proc.returncode != 0:
return []
return [l.strip() for l in proc.stdout.splitlines() if l.strip()]
# ---------- Acciones (globales) ----------
def bring_up_global(kind, force_recreate=False):
base_args = ["up", "-d"]
if force_recreate:
base_args.append("--force-recreate")
cmd = compose_cmd_global(base_args, kind=kind)
info("Ejecutando: " + " ".join(cmd))
proc = run(cmd)
if proc.returncode == 0:
ok(f"Servicio global '{kind}' levantado correctamente.")
else:
fail(f"Fallo al levantar servicio global '{kind}'. Código: {proc.returncode}")
def bring_down_global(kind, remove=False):
"""
Apaga SOLO los servicios definidos en el compose global indicado.
- Primero 'stop <servicios>'.
- Opcionalmente, 'rm -f <servicios>' si remove=True.
"""
services = list_services_global(kind)
if not services:
warn(f"No se encontraron servicios en compose.{kind}.yaml.")
return
# stop específico
cmd = compose_cmd_global(["stop"] + services, kind=kind)
info("Ejecutando: " + " ".join(cmd))
proc = run(cmd)
if proc.returncode != 0:
fail(f"Fallo al detener servicios de '{kind}'. Código: {proc.returncode}")
if remove:
cmd_rm = compose_cmd_global(["rm", "-f"] + services, kind=kind)
info("Ejecutando: " + " ".join(cmd_rm))
proc_rm = run(cmd_rm)
if proc_rm.returncode != 0:
fail(f"Fallo al remover servicios de '{kind}'. Código: {proc_rm.returncode}")
ok(f"Servicio(s) global(es) '{kind}' detenido(s).")
def running_ids_global(kind):
"""
Detecta si el compose global está corriendo revisando contenedores por servicio,
filtrando por labels (project+service).
"""
services = list_services_global(kind)
ids = []
for svc in services:
ids += docker_ps_by_labels(GLOBAL_PROJECT_NAME, service=svc, running_only=True)
# eliminar duplicados
return list(dict.fromkeys(ids))
# ---------- Estado y flujo ----------
def detect_status_summary():
dev_running = running_ids("development")
prod_running = running_ids("production")
npm_running = running_ids_global("npm")
dbeaver_running = running_ids_global("dbeaver")
print_header("Estado actual")
info(f"DESARROLLO: {len(dev_running)} contenedor(es) en ejecución.")
info(f"PRODUCCIÓN: {len(prod_running)} contenedor(es) en ejecución.")
info(f"NPM (global): {len(npm_running)} contenedor(es) en ejecución.")
info(f"DBEAVER (global): {len(dbeaver_running)} contenedor(es) en ejecución.\n")
return bool(dev_running), bool(prod_running), bool(npm_running), bool(dbeaver_running)
def detect_and_optionally_shutdown():
"""Muestra estado y ofrece (opcional) apagar dev/prod.
Los servicios globales se gestionan desde el menú principal (levantar/apagar).
"""
dev_on, prod_on, _npm_on, _dbeaver_on = detect_status_summary()
options = []
if dev_on:
options.append(("1", "Apagar entorno de DESARROLLO", "development"))
if prod_on:
options.append(("2", "Apagar entorno de PRODUCCIÓN", "production"))
options.append(("3", "Continuar sin detener nada", None))
if len(options) == 1:
return
print("Selecciona una opción:")
for opt in options:
key, label = opt[0], opt[1]
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" or selected[2] is None:
ok("Continuamos sin detener nada.")
return
env_key = selected[2]
bring_down(env_key)
return
def main_menu():
# Consultar estado de globales para decidir si mostrar opciones de "Levantar" o "Apagar"
_dev_on, _prod_on, npm_on, dbeaver_on = detect_status_summary()
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")
dynamic_keys = {}
next_key = 3
# NPM: opción según estado
if not npm_on:
print(f" {next_key}) Levantar NPM (compose.npm.yaml)")
dynamic_keys[str(next_key)] = ("global_up", "npm")
else:
print(f" {next_key}) Apagar NPM (compose.npm.yaml)")
dynamic_keys[str(next_key)] = ("global_down", "npm")
next_key += 1
# DBEAVER: opción según estado
if not dbeaver_on:
print(f" {next_key}) Levantar DBEAVER (compose.dbeaver.yaml)")
dynamic_keys[str(next_key)] = ("global_up", "dbeaver")
else:
print(f" {next_key}) Apagar DBEAVER (compose.dbeaver.yaml)")
dynamic_keys[str(next_key)] = ("global_down", "dbeaver")
next_key += 1
# Salir
print(f" {next_key}) Salir")
exit_key = str(next_key)
while True:
choice = input("> ").strip()
if choice == "1":
force = yes_no("¿Usar --force-recreate para DESARROLLO?", default="n")
bring_up("development", force_recreate=force)
return
elif choice == "2":
force = yes_no("¿Usar --force-recreate para PRODUCCIÓN?", default="n")
bring_up("production", force_recreate=force)
return
elif choice in dynamic_keys:
action, kind = dynamic_keys[choice]
if action == "global_up":
force = yes_no(f"¿Usar --force-recreate para {kind.upper()}?", default="n")
bring_up_global(kind, force_recreate=force)
return
elif action == "global_down":
remove = yes_no(f"¿También remover contenedores de {kind.upper()}? (rm -f)", default="n")
bring_down_global(kind, remove=remove)
return
elif choice == exit_key:
ok("Saliendo.")
sys.exit(0)
else:
print("Opción inválida. Elige una de las opciones listadas.")
def main():
try:
check_prereqs()
# Mostrar estado y permitir opcionalmente apagar dev/prod
detect_and_optionally_shutdown()
# Menú de gestión (incluye globales: levantar o apagar según estado)
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()