Skip to content

Commit 0957a2e

Browse files
committed
modification des scrips python
1 parent cfc5288 commit 0957a2e

File tree

2 files changed

+232
-46
lines changed

2 files changed

+232
-46
lines changed

tools/bdtopo_download_and_update

Lines changed: 113 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import os
3838
import platform
3939
import re
4040
import shutil
41+
import signal
4142
import subprocess
4243
import sys
4344
import tempfile
@@ -93,6 +94,99 @@ def _get_7z_installation_instructions() -> str:
9394
return "Installez p7zip depuis https://www.7-zip.org/download.html"
9495

9596

97+
def _load_dotenv_file(path: Path, loaded_env: dict[str, str]) -> None:
98+
"""Charge un fichier .env simple en mémoire, sans écraser l'environnement exporté."""
99+
if not path.exists():
100+
return
101+
102+
for raw_line in path.read_text(encoding="utf-8").splitlines():
103+
line = raw_line.strip()
104+
105+
if not line or line.startswith("#") or "=" not in line:
106+
continue
107+
108+
key, value = line.split("=", 1)
109+
key = key.strip()
110+
value = value.strip()
111+
112+
if not key:
113+
continue
114+
115+
if (value.startswith('"') and value.endswith('"')) or (
116+
value.startswith("'") and value.endswith("'")
117+
):
118+
value = value[1:-1]
119+
120+
def _replace_env_var(match: re.Match[str]) -> str:
121+
variable_name = match.group(1)
122+
return os.environ.get(variable_name, loaded_env.get(variable_name, ""))
123+
124+
value = re.sub(r"\$\{([A-Z0-9_]+)\}", _replace_env_var, value)
125+
126+
if key not in os.environ:
127+
loaded_env[key] = value
128+
129+
130+
def _load_project_env() -> None:
131+
"""
132+
Charge `.env` puis `.env.local`, avec surcharge par `.env.local`,
133+
sans écraser les variables déjà exportées dans le shell.
134+
"""
135+
project_root = Path(__file__).parent.parent
136+
loaded_env: dict[str, str] = {}
137+
138+
for env_file in (project_root / ".env", project_root / ".env.local"):
139+
_load_dotenv_file(env_file, loaded_env)
140+
141+
for key, value in loaded_env.items():
142+
os.environ.setdefault(key, value)
143+
144+
145+
def _get_prod_target_config(target: str) -> tuple[str, str]:
146+
if target == "2025":
147+
return "BDTOPO_2025_DATABASE_URL", "dialog-bdtopo-2025"
148+
149+
if target == "2025_2":
150+
return "BDTOPO_2025_2_DATABASE_URL", "dialog-bdtopo-2025-2"
151+
152+
raise ValueError(f"Cible de production inconnue: {target}")
153+
154+
155+
def _open_prod_tunnel(target: str, port: int = 10002) -> tuple[subprocess.Popen[str], str]:
156+
env_var_name, app_name = _get_prod_target_config(target)
157+
database_url = os.environ.get(env_var_name)
158+
159+
if not database_url:
160+
print(
161+
f"ERROR: {env_var_name} est absente de l'environnement",
162+
file=sys.stderr,
163+
)
164+
return None, ""
165+
166+
print(f"Ouverture d'un tunnel vers {app_name}...")
167+
168+
tunnel_proc = subprocess.Popen(
169+
["./tools/scalingodbtunnel", app_name, "--host-url", "--port", str(port)],
170+
stdout=subprocess.PIPE,
171+
text=True,
172+
)
173+
174+
tunnel_url = tunnel_proc.stdout.readline().strip() if tunnel_proc.stdout else ""
175+
176+
if not tunnel_url:
177+
tunnel_proc.send_signal(signal.SIGINT)
178+
tunnel_proc.wait()
179+
print(
180+
f"ERROR: impossible d'ouvrir le tunnel vers {app_name}",
181+
file=sys.stderr,
182+
)
183+
return None, ""
184+
185+
print(f"Tunnel ouvert vers {app_name} sur 127.0.0.1:{port}")
186+
187+
return tunnel_proc, tunnel_url
188+
189+
96190
def _download_file(url: str, dest_path: Path, chunk_size: int = 8192) -> None:
97191
"""Télécharge un fichier depuis une URL."""
98192
print(f"Téléchargement: {url}")
@@ -276,31 +370,17 @@ def _resolve_target_database_url(args: argparse.Namespace) -> tuple[str | None,
276370
return args.url, "--url"
277371

278372
if args.prod == "2025":
279-
database_url = os.environ.get("BDTOPO_2025_DATABASE_URL")
280-
if not database_url:
281-
print(
282-
"ERROR: BDTOPO_2025_DATABASE_URL est absente de l'environnement",
283-
file=sys.stderr,
284-
)
285-
return None, None
286-
print("Utilisation de BDTOPO_2025_DATABASE_URL pour charger la base cible")
287-
return database_url, "BDTOPO_2025_DATABASE_URL"
373+
return os.environ.get("BDTOPO_2025_DATABASE_URL"), "BDTOPO_2025_DATABASE_URL"
288374

289375
if args.prod == "2025_2":
290-
database_url = os.environ.get("BDTOPO_2025_2_DATABASE_URL")
291-
if not database_url:
292-
print(
293-
"ERROR: BDTOPO_2025_2_DATABASE_URL est absente de l'environnement",
294-
file=sys.stderr,
295-
)
296-
return None, None
297-
print("Utilisation de BDTOPO_2025_2_DATABASE_URL pour charger la base cible")
298-
return database_url, "BDTOPO_2025_2_DATABASE_URL"
376+
return os.environ.get("BDTOPO_2025_2_DATABASE_URL"), "BDTOPO_2025_2_DATABASE_URL"
299377

300378
return None, None
301379

302380

303381
def main() -> int:
382+
_load_project_env()
383+
304384
parser = argparse.ArgumentParser(
305385
description="Télécharge, dézippe et importe les données BDTOPO EXPRESS",
306386
formatter_class=argparse.RawDescriptionHelpFormatter,
@@ -514,10 +594,15 @@ def main() -> int:
514594

515595
database_url_from_env, database_url_source = _resolve_target_database_url(args)
516596
database_url_for_migrations = None # Sera utilisé pour les migrations
597+
tunnel_proc = None
517598

518-
if database_url_from_env:
519-
cmd.extend(["--url", database_url_from_env])
520-
database_url_for_migrations = database_url_from_env
599+
if args.prod:
600+
tunnel_proc, tunnel_url = _open_prod_tunnel(args.prod)
601+
if tunnel_proc is None:
602+
return 1
603+
604+
cmd.extend(["--url", tunnel_url])
605+
database_url_for_migrations = tunnel_url
521606
print(f"Base cible sélectionnée via {database_url_source}")
522607
elif args.url:
523608
cmd.extend(["--url", args.url])
@@ -535,7 +620,13 @@ def main() -> int:
535620
cmd.append("-y")
536621

537622
print(f"Exécution de: {' '.join(cmd)}")
538-
result = subprocess.run(cmd)
623+
try:
624+
result = subprocess.run(cmd)
625+
finally:
626+
if tunnel_proc is not None:
627+
print("Fermeture du tunnel...")
628+
tunnel_proc.send_signal(signal.SIGINT)
629+
tunnel_proc.wait()
539630

540631
if result.returncode != 0:
541632
print("ERROR: L'import a échoué", file=sys.stderr)

tools/scalingodbtunnel

Lines changed: 119 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
#!/usr/bin/env python3
22
import argparse
3+
import platform
34
import shlex
5+
import socket
46
import subprocess
57
import sys
8+
import threading
69
from contextlib import contextmanager
710
from urllib.parse import urlparse, urlunparse
811

@@ -38,6 +41,86 @@ def _popen_terminate_on_exit(*args, **kwargs):
3841
proc.terminate()
3942

4043

44+
@contextmanager
45+
def _tcp_proxy_on_exit(listen_host: str, listen_port: int, target_host: str, target_port: int):
46+
stop_event = threading.Event()
47+
client_threads = []
48+
49+
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
50+
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
51+
server_socket.bind((listen_host, listen_port))
52+
server_socket.listen()
53+
server_socket.settimeout(0.5)
54+
55+
def _pipe(source: socket.socket, destination: socket.socket):
56+
try:
57+
while not stop_event.is_set():
58+
data = source.recv(65536)
59+
if not data:
60+
break
61+
destination.sendall(data)
62+
except OSError:
63+
pass
64+
finally:
65+
try:
66+
destination.shutdown(socket.SHUT_WR)
67+
except OSError:
68+
pass
69+
70+
def _handle_client(client_socket: socket.socket):
71+
upstream_socket = None
72+
try:
73+
upstream_socket = socket.create_connection((target_host, target_port))
74+
upstream_thread = threading.Thread(
75+
target=_pipe,
76+
args=(client_socket, upstream_socket),
77+
daemon=True,
78+
)
79+
downstream_thread = threading.Thread(
80+
target=_pipe,
81+
args=(upstream_socket, client_socket),
82+
daemon=True,
83+
)
84+
upstream_thread.start()
85+
downstream_thread.start()
86+
upstream_thread.join()
87+
downstream_thread.join()
88+
except OSError:
89+
pass
90+
finally:
91+
client_socket.close()
92+
if upstream_socket is not None:
93+
upstream_socket.close()
94+
95+
def _serve():
96+
try:
97+
while not stop_event.is_set():
98+
try:
99+
client_socket, _ = server_socket.accept()
100+
except socket.timeout:
101+
continue
102+
except OSError:
103+
break
104+
105+
thread = threading.Thread(target=_handle_client, args=(client_socket,), daemon=True)
106+
client_threads.append(thread)
107+
thread.start()
108+
finally:
109+
server_socket.close()
110+
111+
server_thread = threading.Thread(target=_serve, daemon=True)
112+
server_thread.start()
113+
114+
try:
115+
yield
116+
finally:
117+
stop_event.set()
118+
server_socket.close()
119+
server_thread.join(timeout=1)
120+
for thread in client_threads:
121+
thread.join(timeout=1)
122+
123+
41124
def main(
42125
app: str,
43126
port: int,
@@ -74,38 +157,50 @@ def main(
74157
# 2) Forward host -> Docker.
75158
# This allows PHP/Doctrine container to access the Scalingo database.
76159

77-
username = _get_output(["whoami"])
78-
79-
# https://stackoverflow.com/a/55224655
80-
docker_bridge_ip = _get_output(
81-
[
82-
"docker",
83-
"network",
84-
"inspect",
85-
"bridge",
86-
"--format={{(index .IPAM.Config 0).Gateway}}",
160+
displayed_port = port
161+
162+
if platform.system().lower() == "darwin":
163+
# Docker Desktop n'expose pas l'IP du bridge Linux sur l'hôte macOS.
164+
# On republie donc le tunnel sur un second port accessible via
165+
# `host.docker.internal`, pour éviter une boucle de forwarding sur
166+
# le port déjà utilisé par `scalingo db-tunnel`.
167+
docker_bind_port = port + 1
168+
docker_tunnel_origin = f"0.0.0.0:{docker_bind_port}"
169+
displayed_port = docker_bind_port
170+
docker_tunnel_context = _tcp_proxy_on_exit("0.0.0.0", docker_bind_port, "127.0.0.1", port)
171+
else:
172+
username = _get_output(["whoami"])
173+
# https://stackoverflow.com/a/55224655
174+
docker_bridge_ip = _get_output(
175+
[
176+
"docker",
177+
"network",
178+
"inspect",
179+
"bridge",
180+
"--format={{(index .IPAM.Config 0).Gateway}}",
181+
]
182+
)
183+
docker_tunnel_origin = f"{docker_bridge_ip}:{port}"
184+
displayed_port = port
185+
# https://stackoverflow.com/a/52120176
186+
docker_tunnel_command = [
187+
"ssh",
188+
"-N",
189+
"-L",
190+
f"{docker_tunnel_origin}:127.0.0.1:{port}",
191+
f"{username}@localhost",
87192
]
88-
)
89-
docker_tunnel_origin = f"{docker_bridge_ip}:{port}"
90-
91-
# https://stackoverflow.com/a/52120176
92-
docker_tunnel_command = [
93-
"ssh",
94-
"-N",
95-
"-L",
96-
f"{docker_tunnel_origin}:127.0.0.1:{port}",
97-
f"{username}@localhost",
98-
]
99-
100-
with _popen_terminate_on_exit(docker_tunnel_command):
193+
docker_tunnel_context = _popen_terminate_on_exit(docker_tunnel_command)
194+
195+
with docker_tunnel_context:
101196
# The tunnel database URL uses the same credentials, so we grab them
102197
# from the configured DATABASE_URL.
103198
database_url = urlparse(
104199
_get_output(["scalingo", "--app", app, "env-get", "DATABASE_URL"])
105200
)
106201

107202
# DB tunnel is always available on localhost, so we can show either.
108-
displayed_origin = f"127.0.0.1:{port}" if host_url else docker_tunnel_origin
203+
displayed_origin = f"127.0.0.1:{displayed_port}" if host_url else docker_tunnel_origin
109204

110205
db_tunnel_database_url = database_url._replace(
111206
netloc=f"{database_url.username}:{database_url.password}@{displayed_origin}"

0 commit comments

Comments
 (0)