Files
shoottracker/deploy_shoottracker.py
T
2026-04-30 22:44:27 +02:00

314 lines
11 KiB
Python

#!/usr/bin/env python3
"""
deploy_shoottracker.py
──────────────────────
Déploie ShootTracker sur domench.fr via Gitea + Coolify.
Architecture : Docker Compose — un seul dépôt contient
• l'app principale (frontend React + backend Node/Express + SQLite)
• le microservice IA (FastAPI + YOLOv8)
Usage :
python deploy_shoottracker.py
Prérequis (variables d'environnement ou saisie interactive) :
GITEA_URL ex. https://git.domench.fr
GITEA_USER ex. gael
GITEA_TOKEN token API Gitea avec droits repo
COOLIFY_URL ex. https://coolify.domench.fr
COOLIFY_TOKEN token API Coolify (Settings → API)
"""
import os
import sys
import secrets
import subprocess
import json
import time
import textwrap
import urllib.request
import urllib.parse
import urllib.error
# ─── Couleurs terminal ────────────────────────────────────────────────────────
GREEN = "\033[92m"
YELLOW = "\033[93m"
RED = "\033[91m"
CYAN = "\033[96m"
BOLD = "\033[1m"
RESET = "\033[0m"
def ok(msg): print(f"{GREEN}{msg}{RESET}")
def warn(msg): print(f"{YELLOW}{msg}{RESET}")
def err(msg): print(f"{RED}{msg}{RESET}")
def info(msg): print(f"{CYAN}{msg}{RESET}")
def step(n, msg): print(f"\n{BOLD}── Étape {n} : {msg} ──{RESET}")
# ─── HTTP helpers ─────────────────────────────────────────────────────────────
def api_call(url, method="GET", data=None, headers=None, token=None, base_url=None):
"""Appel HTTP JSON simple (sans dépendance externe)."""
full_url = url if url.startswith("http") else f"{base_url}{url}"
body = json.dumps(data).encode() if data is not None else None
h = {"Content-Type": "application/json", "Accept": "application/json"}
if token:
h["Authorization"] = f"Bearer {token}"
if headers:
h.update(headers)
req = urllib.request.Request(full_url, data=body, headers=h, method=method)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
raw = resp.read()
return json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:
body_text = e.read().decode(errors="replace")
raise RuntimeError(f"HTTP {e.code} {e.reason}{body_text}") from e
# ─── Configuration ────────────────────────────────────────────────────────────
def get_config():
def ask(env, prompt, secret=False):
val = os.environ.get(env, "").strip()
if val:
return val
if secret:
import getpass
return getpass.getpass(f" {prompt} : ").strip()
return input(f" {prompt} : ").strip()
print(f"\n{BOLD}=== Déploiement ShootTracker ==={RESET}")
print("Renseignez les paramètres (ou définissez les variables d'environnement) :\n")
cfg = {
"gitea_url": ask("GITEA_URL", "Gitea URL (ex: https://git.domench.fr)").rstrip("/"),
"gitea_user": ask("GITEA_USER", "Gitea username"),
"gitea_token": ask("GITEA_TOKEN", "Gitea API token", secret=True),
"coolify_url": ask("COOLIFY_URL", "Coolify URL (ex: https://coolify.domench.fr)").rstrip("/"),
"coolify_token": ask("COOLIFY_TOKEN","Coolify API token", secret=True),
}
# Paramètres optionnels avec valeurs par défaut
cfg["repo_name"] = os.environ.get("REPO_NAME", "shoottracker")
cfg["app_domain"] = os.environ.get("APP_DOMAIN", "shoottracker.domench.fr")
# JWT_SECRET généré automatiquement (64 octets hex = 128 chars)
cfg["jwt_secret"] = secrets.token_hex(64)
return cfg
# ─── Étape 1 : Créer le dépôt Gitea ─────────────────────────────────────────
def create_gitea_repo(cfg):
step(1, "Création du dépôt Gitea")
base = cfg["gitea_url"]
tok = cfg["gitea_token"]
user = cfg["gitea_user"]
name = cfg["repo_name"]
# Vérifier si le dépôt existe déjà
try:
api_call(f"{base}/api/v1/repos/{user}/{name}", token=tok)
warn(f"Le dépôt {user}/{name} existe déjà → on continue")
return f"{base}/{user}/{name}.git"
except RuntimeError:
pass # N'existe pas encore
resp = api_call(
f"{base}/api/v1/user/repos",
method="POST",
data={
"name": name,
"description": "ShootTracker — application de suivi des performances de tir sportif",
"private": True,
"auto_init": False,
},
token=tok,
)
ok(f"Dépôt créé : {resp['html_url']}")
return resp["clone_url"]
# ─── Étape 2 : Push du code vers Gitea ───────────────────────────────────────
def push_code(cfg, clone_url):
step(2, "Push du code vers Gitea")
script_dir = os.path.dirname(os.path.abspath(__file__))
os.chdir(script_dir)
# Construire l'URL authentifiée
from urllib.parse import urlparse, urlunparse
parsed = urlparse(clone_url)
auth_url = urlunparse(parsed._replace(
netloc=f"{cfg['gitea_user']}:{cfg['gitea_token']}@{parsed.netloc}"
))
def run(cmd, **kw):
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, **kw)
if result.returncode != 0:
raise RuntimeError(f"Commande échouée : {cmd}\n{result.stderr}")
return result.stdout.strip()
# Init git si nécessaire
if not os.path.isdir(".git"):
run("git init")
run("git checkout -b main")
ok("Dépôt git initialisé")
# .gitignore
gitignore = textwrap.dedent("""\
node_modules/
dist/
.env
data/
*.db
__pycache__/
.pytest_cache/
*.pyc
.DS_Store
""")
with open(".gitignore", "w") as f:
f.write(gitignore)
# Remote
try:
run("git remote remove origin")
except RuntimeError:
pass
run(f"git remote add origin {auth_url}")
run("git add -A")
try:
run('git commit -m "feat: ShootTracker — SQLite + JWT + YOLOv8 AI"')
except RuntimeError:
warn("Rien à committer (arbre propre)")
run("git push -u origin main --force")
ok("Code pushé sur Gitea")
# ─── Étape 3 : Créer l'application Coolify ───────────────────────────────────
def create_coolify_app(cfg, clone_url):
step(3, "Création de l'application Coolify")
base = cfg["coolify_url"]
tok = cfg["coolify_token"]
# Récupérer le premier serveur disponible
servers = api_call(f"{base}/api/v1/servers", token=tok)
if not servers:
raise RuntimeError("Aucun serveur trouvé dans Coolify")
server_uuid = servers[0]["uuid"]
info(f"Serveur Coolify : {servers[0].get('name', server_uuid)}")
# Variables d'environnement de l'application
env_vars = "\n".join([
f"NODE_ENV=production",
f"PORT=3001",
f"JWT_SECRET={cfg['jwt_secret']}",
f"AI_SERVICE_URL=http://shoottracker-ai:8000",
f"CORS_ORIGIN=https://{cfg['app_domain']}",
f"UPLOADS_DIR=/app/data/uploads",
])
payload = {
"name": "shoottracker",
"description": "ShootTracker — tir sportif IA",
"server_uuid": server_uuid,
"git_repository": clone_url,
"git_branch": "main",
"build_pack": "dockercompose",
"docker_compose_location": "/docker-compose.yml",
"fqdn": f"https://{cfg['app_domain']}",
"ports_exposes": "3001",
"environment_variables": env_vars,
"instant_deploy": False,
}
# Vérifier si l'app existe déjà
try:
apps = api_call(f"{base}/api/v1/applications", token=tok)
existing = next((a for a in apps if a.get("name") == "shoottracker"), None)
if existing:
app_uuid = existing["uuid"]
warn(f"Application 'shoottracker' existe déjà (uuid={app_uuid}) → mise à jour des variables")
# Mettre à jour les env vars
api_call(
f"{base}/api/v1/applications/{app_uuid}",
method="PATCH",
data={"environment_variables": env_vars},
token=tok,
)
ok("Variables d'environnement mises à jour")
return app_uuid
except Exception:
pass
resp = api_call(
f"{base}/api/v1/applications",
method="POST",
data=payload,
token=tok,
)
app_uuid = resp.get("uuid") or resp.get("id")
ok(f"Application créée (uuid={app_uuid})")
return app_uuid
# ─── Étape 4 : Déployer ───────────────────────────────────────────────────────
def trigger_deploy(cfg, app_uuid):
step(4, "Déclenchement du déploiement")
base = cfg["coolify_url"]
tok = cfg["coolify_token"]
try:
resp = api_call(
f"{base}/api/v1/applications/{app_uuid}/deploy",
method="POST",
token=tok,
)
deploy_id = resp.get("deployment_uuid") or resp.get("id", "")
ok(f"Déploiement déclenché (id={deploy_id})")
info(f"Suivez l'avancement sur : {base}/project/shoottracker/deployments")
except RuntimeError as e:
warn(f"Impossible de déclencher via API : {e}")
info("Déclenchez manuellement depuis l'interface Coolify")
# ─── Résumé final ─────────────────────────────────────────────────────────────
def print_summary(cfg):
box = "" * 60
print(f"\n{GREEN}{BOLD}{box}")
print(" ✓ ShootTracker déployé avec succès !")
print(box + RESET)
print(f"""
🌐 App : https://{cfg['app_domain']}
📦 Gitea : {cfg['gitea_url']}/{cfg['gitea_user']}/{cfg['repo_name']}
🚀 Coolify : {cfg['coolify_url']}
{YELLOW}⚠ Conservez ce JWT_SECRET en lieu sûr (non récupérable) :{RESET}
{BOLD}{cfg['jwt_secret']}{RESET}
Données persistées dans le volume Docker : shoottracker_data
(SQLite DB + photos cibles + avatars)
""")
# ─── Main ─────────────────────────────────────────────────────────────────────
def main():
try:
cfg = get_config()
clone_url = create_gitea_repo(cfg)
push_code(cfg, clone_url)
app_uuid = create_coolify_app(cfg, clone_url)
trigger_deploy(cfg, app_uuid)
print_summary(cfg)
except KeyboardInterrupt:
print(f"\n{YELLOW}Annulé.{RESET}")
sys.exit(1)
except RuntimeError as e:
err(str(e))
sys.exit(1)
if __name__ == "__main__":
main()