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:
parent
97db600b1f
commit
2b47faf66a
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
276
suitecoffee.py
276
suitecoffee.py
@ -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", "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)
|
||||
|
||||
# ---------- 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()
|
||||
Loading…
x
Reference in New Issue
Block a user