314 lines
11 KiB
Python
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()
|