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.
387 lines
14 KiB
Python
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", "sí"):
|
|
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()
|