supervaizer 0.10.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- supervaizer/__init__.py +97 -0
- supervaizer/__version__.py +10 -0
- supervaizer/account.py +308 -0
- supervaizer/account_service.py +93 -0
- supervaizer/admin/routes.py +1293 -0
- supervaizer/admin/static/js/job-start-form.js +373 -0
- supervaizer/admin/templates/agent_detail.html +145 -0
- supervaizer/admin/templates/agents.html +249 -0
- supervaizer/admin/templates/agents_grid.html +82 -0
- supervaizer/admin/templates/base.html +233 -0
- supervaizer/admin/templates/case_detail.html +230 -0
- supervaizer/admin/templates/cases_list.html +182 -0
- supervaizer/admin/templates/cases_table.html +134 -0
- supervaizer/admin/templates/console.html +389 -0
- supervaizer/admin/templates/dashboard.html +153 -0
- supervaizer/admin/templates/job_detail.html +192 -0
- supervaizer/admin/templates/job_start_test.html +109 -0
- supervaizer/admin/templates/jobs_list.html +180 -0
- supervaizer/admin/templates/jobs_table.html +122 -0
- supervaizer/admin/templates/navigation.html +163 -0
- supervaizer/admin/templates/recent_activity.html +81 -0
- supervaizer/admin/templates/server.html +105 -0
- supervaizer/admin/templates/server_status_cards.html +121 -0
- supervaizer/admin/templates/supervaize_instructions.html +212 -0
- supervaizer/agent.py +956 -0
- supervaizer/case.py +432 -0
- supervaizer/cli.py +395 -0
- supervaizer/common.py +324 -0
- supervaizer/deploy/__init__.py +16 -0
- supervaizer/deploy/cli.py +305 -0
- supervaizer/deploy/commands/__init__.py +9 -0
- supervaizer/deploy/commands/clean.py +294 -0
- supervaizer/deploy/commands/down.py +119 -0
- supervaizer/deploy/commands/local.py +460 -0
- supervaizer/deploy/commands/plan.py +167 -0
- supervaizer/deploy/commands/status.py +169 -0
- supervaizer/deploy/commands/up.py +281 -0
- supervaizer/deploy/docker.py +377 -0
- supervaizer/deploy/driver_factory.py +42 -0
- supervaizer/deploy/drivers/__init__.py +39 -0
- supervaizer/deploy/drivers/aws_app_runner.py +607 -0
- supervaizer/deploy/drivers/base.py +196 -0
- supervaizer/deploy/drivers/cloud_run.py +570 -0
- supervaizer/deploy/drivers/do_app_platform.py +504 -0
- supervaizer/deploy/health.py +404 -0
- supervaizer/deploy/state.py +210 -0
- supervaizer/deploy/templates/Dockerfile.template +44 -0
- supervaizer/deploy/templates/debug_env.py +69 -0
- supervaizer/deploy/templates/docker-compose.yml.template +37 -0
- supervaizer/deploy/templates/dockerignore.template +66 -0
- supervaizer/deploy/templates/entrypoint.sh +20 -0
- supervaizer/deploy/utils.py +52 -0
- supervaizer/event.py +181 -0
- supervaizer/examples/controller_template.py +196 -0
- supervaizer/instructions.py +145 -0
- supervaizer/job.py +392 -0
- supervaizer/job_service.py +156 -0
- supervaizer/lifecycle.py +417 -0
- supervaizer/parameter.py +233 -0
- supervaizer/protocol/__init__.py +11 -0
- supervaizer/protocol/a2a/__init__.py +21 -0
- supervaizer/protocol/a2a/model.py +227 -0
- supervaizer/protocol/a2a/routes.py +99 -0
- supervaizer/py.typed +1 -0
- supervaizer/routes.py +917 -0
- supervaizer/server.py +553 -0
- supervaizer/server_utils.py +54 -0
- supervaizer/storage.py +462 -0
- supervaizer/telemetry.py +81 -0
- supervaizer/utils/__init__.py +16 -0
- supervaizer/utils/version_check.py +56 -0
- supervaizer-0.10.5.dist-info/METADATA +317 -0
- supervaizer-0.10.5.dist-info/RECORD +76 -0
- supervaizer-0.10.5.dist-info/WHEEL +4 -0
- supervaizer-0.10.5.dist-info/entry_points.txt +2 -0
- supervaizer-0.10.5.dist-info/licenses/LICENSE.md +346 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
# Copyright (c) 2024-2025 Alain Prasquier - Supervaize.com. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
|
|
4
|
+
# If a copy of the MPL was not distributed with this file, you can obtain one at
|
|
5
|
+
# https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Local Testing Command
|
|
9
|
+
|
|
10
|
+
This module provides local testing functionality using Docker Compose.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import subprocess
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
from rich.panel import Panel
|
|
22
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
23
|
+
from rich.table import Table
|
|
24
|
+
|
|
25
|
+
from supervaizer.deploy.docker import DockerManager
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def local_docker(
|
|
31
|
+
name: str,
|
|
32
|
+
env: str,
|
|
33
|
+
port: int,
|
|
34
|
+
generate_api_key: bool,
|
|
35
|
+
generate_rsa: bool,
|
|
36
|
+
timeout: int,
|
|
37
|
+
verbose: bool,
|
|
38
|
+
docker_files_only: bool = False,
|
|
39
|
+
source_dir: str = ".",
|
|
40
|
+
controller_file: str = "supervaizer_control.py",
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Test deployment locally using Docker Compose."""
|
|
43
|
+
if docker_files_only:
|
|
44
|
+
console.print(
|
|
45
|
+
Panel.fit("[bold blue]Generate Docker Files Only[/]", border_style="blue")
|
|
46
|
+
)
|
|
47
|
+
else:
|
|
48
|
+
console.print(
|
|
49
|
+
Panel.fit("[bold blue]Local Docker Testing[/]", border_style="blue")
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Determine service name
|
|
53
|
+
if name is None:
|
|
54
|
+
name = Path(source_dir).name.lower().replace("_", "-")
|
|
55
|
+
|
|
56
|
+
service_name = f"{name}-{env}"
|
|
57
|
+
|
|
58
|
+
console.print(f"[bold]Testing service:[/] {service_name}")
|
|
59
|
+
console.print(f"[bold]Environment:[/] {env}")
|
|
60
|
+
console.print(f"[bold]Port:[/] {port}")
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
# Step 1: Check Docker availability
|
|
64
|
+
console.print("\n[bold]Step 1:[/] Checking Docker availability...")
|
|
65
|
+
if not _check_docker_available():
|
|
66
|
+
console.print("[bold red]Error:[/] Docker is not available or not running")
|
|
67
|
+
raise RuntimeError("Docker not available")
|
|
68
|
+
console.print("[green]✓[/] Docker is available")
|
|
69
|
+
|
|
70
|
+
# Step 2: Generate secrets if needed
|
|
71
|
+
console.print("\n[bold]Step 2:[/] Setting up secrets...")
|
|
72
|
+
secrets = _generate_test_secrets(generate_api_key, generate_rsa)
|
|
73
|
+
console.print("[green]✓[/] Test secrets configured")
|
|
74
|
+
|
|
75
|
+
# Step 3: Generate deployment files
|
|
76
|
+
console.print("\n[bold]Step 3:[/] Generating deployment files...")
|
|
77
|
+
docker_manager = DockerManager(require_docker=False)
|
|
78
|
+
docker_manager.generate_dockerfile(
|
|
79
|
+
controller_file=controller_file,
|
|
80
|
+
app_port=port,
|
|
81
|
+
)
|
|
82
|
+
docker_manager.generate_dockerignore()
|
|
83
|
+
docker_manager.generate_docker_compose(
|
|
84
|
+
port=port,
|
|
85
|
+
service_name=service_name,
|
|
86
|
+
environment=env,
|
|
87
|
+
api_key=secrets.get("api_key", "test-api-key"),
|
|
88
|
+
rsa_key=secrets.get("rsa_private_key", "test-rsa-key"),
|
|
89
|
+
)
|
|
90
|
+
console.print("[green]✓[/] Deployment files generated")
|
|
91
|
+
|
|
92
|
+
# If docker_files_only is True, stop here
|
|
93
|
+
if docker_files_only:
|
|
94
|
+
console.print("\n[bold green]✓ Docker files generated successfully![/]")
|
|
95
|
+
console.print("[bold]Generated files:[/]")
|
|
96
|
+
console.print(" • .deployment/Dockerfile")
|
|
97
|
+
console.print(" • .deployment/.dockerignore")
|
|
98
|
+
console.print(" • .deployment/docker-compose.yml")
|
|
99
|
+
console.print("\n[bold]To start the services:[/]")
|
|
100
|
+
console.print("[dim]docker-compose -f .deployment/docker-compose.yml up[/]")
|
|
101
|
+
console.print("\n[bold]To debug environment variables:[/]")
|
|
102
|
+
console.print(
|
|
103
|
+
f"[dim]docker-compose -f .deployment/docker-compose.yml run --rm {service_name} python debug_env.py[/]"
|
|
104
|
+
)
|
|
105
|
+
console.print(
|
|
106
|
+
"\n[bold]Note:[/] Environment variables are automatically included from your host environment."
|
|
107
|
+
)
|
|
108
|
+
console.print("Make sure to set the following variables if needed:")
|
|
109
|
+
console.print(" • SUPERVAIZE_API_KEY")
|
|
110
|
+
console.print(" • SUPERVAIZE_WORKSPACE_ID")
|
|
111
|
+
console.print(" • SUPERVAIZE_API_URL")
|
|
112
|
+
console.print(" • SUPERVAIZER_PUBLIC_URL")
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
# Step 4: Build Docker image
|
|
116
|
+
console.print("\n[bold]Step 4:[/] Building Docker image...")
|
|
117
|
+
image_tag = f"{service_name}:local-test"
|
|
118
|
+
# Create a new DockerManager instance that requires Docker for building
|
|
119
|
+
build_docker_manager = DockerManager(require_docker=True)
|
|
120
|
+
|
|
121
|
+
# Get build arguments for environment variables
|
|
122
|
+
from supervaizer.deploy.docker import get_docker_build_args
|
|
123
|
+
|
|
124
|
+
build_args = get_docker_build_args(port)
|
|
125
|
+
|
|
126
|
+
build_docker_manager.build_image(
|
|
127
|
+
image_tag, verbose=verbose, build_args=build_args
|
|
128
|
+
)
|
|
129
|
+
console.print(f"[green]✓[/] Image built: {image_tag}")
|
|
130
|
+
|
|
131
|
+
# Step 5: Start services with Docker Compose
|
|
132
|
+
console.print("\n[bold]Step 5:[/] Starting services...")
|
|
133
|
+
_start_docker_compose(
|
|
134
|
+
service_name=service_name, port=port, secrets=secrets, verbose=verbose
|
|
135
|
+
)
|
|
136
|
+
console.print("[green]✓[/] Services started")
|
|
137
|
+
|
|
138
|
+
# Step 6: Wait for service to be ready
|
|
139
|
+
console.print("\n[bold]Step 6:[/] Waiting for service to be ready...")
|
|
140
|
+
service_url = f"http://localhost:{port}"
|
|
141
|
+
if _wait_for_service(service_url, timeout):
|
|
142
|
+
console.print("[green]✓[/] Service is ready")
|
|
143
|
+
else:
|
|
144
|
+
console.print("[bold red]Error:[/] Service failed to start within timeout")
|
|
145
|
+
_show_service_logs(service_name)
|
|
146
|
+
raise RuntimeError("Service startup timeout")
|
|
147
|
+
|
|
148
|
+
# Step 7: Run health checks
|
|
149
|
+
console.print("\n[bold]Step 7:[/] Running health checks...")
|
|
150
|
+
health_results = _run_health_checks(service_url, secrets.get("api_key"))
|
|
151
|
+
_display_health_results(health_results)
|
|
152
|
+
|
|
153
|
+
# Step 8: Display service information
|
|
154
|
+
console.print("\n[bold]Step 8:[/] Service Information")
|
|
155
|
+
_display_service_info(service_name, service_url, port, secrets)
|
|
156
|
+
|
|
157
|
+
console.print("\n[bold green]✓ Local testing completed successfully![/]")
|
|
158
|
+
console.print(f"[bold]Service URL:[/] {service_url}")
|
|
159
|
+
console.print(f"[bold]API Documentation:[/] {service_url}/docs")
|
|
160
|
+
console.print(f"[bold]ReDoc:[/] {service_url}/redoc")
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
console.print(f"\n[bold red]Error during local testing:[/] {e}")
|
|
164
|
+
_cleanup_test_resources(service_name)
|
|
165
|
+
raise
|
|
166
|
+
finally:
|
|
167
|
+
# Always show cleanup instructions
|
|
168
|
+
console.print("\n[bold]To stop the test services:[/]")
|
|
169
|
+
console.print("[dim]docker-compose -f .deployment/docker-compose.yml down[/]")
|
|
170
|
+
console.print("\n[bold]To debug environment variables:[/]")
|
|
171
|
+
console.print(
|
|
172
|
+
f"[dim]docker-compose -f .deployment/docker-compose.yml run --rm {service_name} python debug_env.py[/]"
|
|
173
|
+
)
|
|
174
|
+
console.print("\n[bold]To clean up all deployment files:[/]")
|
|
175
|
+
console.print("[dim]supervaizer deploy clean[/]")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _check_docker_available() -> bool:
|
|
179
|
+
"""Check if Docker is available and running."""
|
|
180
|
+
try:
|
|
181
|
+
result = subprocess.run(
|
|
182
|
+
["docker", "version"], capture_output=True, text=True, timeout=10
|
|
183
|
+
)
|
|
184
|
+
return result.returncode == 0
|
|
185
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _generate_test_secrets(generate_api_key: bool, generate_rsa: bool) -> dict:
|
|
190
|
+
"""Generate test secrets for local testing."""
|
|
191
|
+
secrets = {}
|
|
192
|
+
|
|
193
|
+
if generate_api_key:
|
|
194
|
+
# Generate a test API key
|
|
195
|
+
import secrets as secrets_module
|
|
196
|
+
|
|
197
|
+
secrets["api_key"] = secrets_module.token_urlsafe(32)
|
|
198
|
+
else:
|
|
199
|
+
secrets["api_key"] = "test-api-key-local"
|
|
200
|
+
|
|
201
|
+
if generate_rsa:
|
|
202
|
+
# Generate a test RSA key
|
|
203
|
+
from cryptography.hazmat.primitives import serialization
|
|
204
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
205
|
+
|
|
206
|
+
private_key = rsa.generate_private_key(
|
|
207
|
+
public_exponent=65537,
|
|
208
|
+
key_size=2048,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
private_pem = private_key.private_bytes(
|
|
212
|
+
encoding=serialization.Encoding.PEM,
|
|
213
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
214
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
215
|
+
)
|
|
216
|
+
secrets["rsa_private_key"] = private_pem.decode()
|
|
217
|
+
else:
|
|
218
|
+
secrets["rsa_private_key"] = "test-rsa-key-local"
|
|
219
|
+
|
|
220
|
+
return secrets
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _start_docker_compose(
|
|
224
|
+
service_name: str, port: int, secrets: dict, verbose: bool = False
|
|
225
|
+
) -> None:
|
|
226
|
+
"""Start services using Docker Compose."""
|
|
227
|
+
compose_file = Path(".deployment/docker-compose.yml")
|
|
228
|
+
|
|
229
|
+
if not compose_file.exists():
|
|
230
|
+
raise RuntimeError("Docker Compose file not found")
|
|
231
|
+
|
|
232
|
+
# Set environment variables for Docker Compose
|
|
233
|
+
env = os.environ.copy()
|
|
234
|
+
env.update(
|
|
235
|
+
{
|
|
236
|
+
"SERVICE_NAME": service_name,
|
|
237
|
+
"SERVICE_PORT": str(port),
|
|
238
|
+
"SUPERVAIZER_API_KEY": secrets["api_key"],
|
|
239
|
+
"SV_RSA_PRIVATE_KEY": secrets["rsa_private_key"],
|
|
240
|
+
"SUPERVAIZER_ENVIRONMENT": "dev",
|
|
241
|
+
"SUPERVAIZER_HOST": "0.0.0.0",
|
|
242
|
+
"SUPERVAIZER_PORT": str(port),
|
|
243
|
+
"SV_LOG_LEVEL": "INFO",
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
cmd = ["docker-compose", "-f", str(compose_file), "up", "-d"]
|
|
248
|
+
|
|
249
|
+
if verbose:
|
|
250
|
+
# When verbose, capture output to display it
|
|
251
|
+
result = subprocess.run(cmd, env=env, capture_output=True, text=True)
|
|
252
|
+
# Display the captured output
|
|
253
|
+
if result.stdout:
|
|
254
|
+
console.print(result.stdout)
|
|
255
|
+
if result.stderr:
|
|
256
|
+
console.print(f"[yellow]Stderr:[/] {result.stderr}")
|
|
257
|
+
else:
|
|
258
|
+
# When not verbose, capture output silently
|
|
259
|
+
result = subprocess.run(cmd, env=env, capture_output=True, text=True)
|
|
260
|
+
|
|
261
|
+
if result.returncode != 0:
|
|
262
|
+
error_msg = result.stderr if result.stderr else "Unknown error"
|
|
263
|
+
raise RuntimeError(f"Failed to start Docker Compose: {error_msg}")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _wait_for_service(url: str, timeout: int) -> bool:
|
|
267
|
+
"""Wait for service to be ready."""
|
|
268
|
+
start_time = time.time()
|
|
269
|
+
|
|
270
|
+
with Progress(
|
|
271
|
+
SpinnerColumn(),
|
|
272
|
+
TextColumn("[progress.description]{task.description}"),
|
|
273
|
+
console=console,
|
|
274
|
+
) as progress:
|
|
275
|
+
progress.add_task("Waiting for service...", total=None)
|
|
276
|
+
|
|
277
|
+
while time.time() - start_time < timeout:
|
|
278
|
+
try:
|
|
279
|
+
response = httpx.get(f"{url}/.well-known/health", timeout=5)
|
|
280
|
+
if response.status_code == 200:
|
|
281
|
+
return True
|
|
282
|
+
except httpx.RequestError:
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
time.sleep(2)
|
|
286
|
+
|
|
287
|
+
return False
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _run_health_checks(url: str, api_key: Optional[str]) -> dict:
|
|
291
|
+
"""Run comprehensive health checks."""
|
|
292
|
+
results = {}
|
|
293
|
+
|
|
294
|
+
# Basic health check
|
|
295
|
+
try:
|
|
296
|
+
response = httpx.get(f"{url}/.well-known/health", timeout=10)
|
|
297
|
+
results["health_endpoint"] = {
|
|
298
|
+
"status": response.status_code,
|
|
299
|
+
"success": response.status_code == 200,
|
|
300
|
+
"response_time": response.elapsed.total_seconds(),
|
|
301
|
+
}
|
|
302
|
+
except Exception as e:
|
|
303
|
+
results["health_endpoint"] = {
|
|
304
|
+
"status": None,
|
|
305
|
+
"success": False,
|
|
306
|
+
"error": str(e),
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# API health check (if API key available)
|
|
310
|
+
if api_key:
|
|
311
|
+
try:
|
|
312
|
+
headers = {"X-API-Key": api_key}
|
|
313
|
+
response = httpx.get(f"{url}/agents/health", headers=headers, timeout=10)
|
|
314
|
+
results["api_health_endpoint"] = {
|
|
315
|
+
"status": response.status_code,
|
|
316
|
+
"success": response.status_code == 200,
|
|
317
|
+
"response_time": response.elapsed.total_seconds(),
|
|
318
|
+
}
|
|
319
|
+
except Exception as e:
|
|
320
|
+
results["api_health_endpoint"] = {
|
|
321
|
+
"status": None,
|
|
322
|
+
"success": False,
|
|
323
|
+
"error": str(e),
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# API documentation check
|
|
327
|
+
try:
|
|
328
|
+
response = httpx.get(f"{url}/docs", timeout=10)
|
|
329
|
+
results["api_docs"] = {
|
|
330
|
+
"status": response.status_code,
|
|
331
|
+
"success": response.status_code == 200,
|
|
332
|
+
}
|
|
333
|
+
except Exception as e:
|
|
334
|
+
results["api_docs"] = {
|
|
335
|
+
"status": None,
|
|
336
|
+
"success": False,
|
|
337
|
+
"error": str(e),
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return results
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _display_health_results(results: dict) -> None:
|
|
344
|
+
"""Display health check results in a table."""
|
|
345
|
+
table = Table(title="Health Check Results")
|
|
346
|
+
table.add_column("Endpoint", style="cyan")
|
|
347
|
+
table.add_column("Status", style="green")
|
|
348
|
+
table.add_column("Response Time", style="yellow")
|
|
349
|
+
table.add_column("Details", style="white")
|
|
350
|
+
|
|
351
|
+
for endpoint, result in results.items():
|
|
352
|
+
if result["success"]:
|
|
353
|
+
status = f"[green]{result['status']}[/]"
|
|
354
|
+
response_time = f"{result.get('response_time', 0):.3f}s"
|
|
355
|
+
details = "✓ OK"
|
|
356
|
+
else:
|
|
357
|
+
status = f"[red]{result.get('status', 'ERROR')}[/]"
|
|
358
|
+
response_time = "N/A"
|
|
359
|
+
details = result.get("error", "Failed")
|
|
360
|
+
|
|
361
|
+
table.add_row(
|
|
362
|
+
endpoint.replace("_", " ").title(), status, response_time, details
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
console.print(table)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _display_service_info(
|
|
369
|
+
service_name: str, url: str, port: int, secrets: dict
|
|
370
|
+
) -> None:
|
|
371
|
+
"""Display service information."""
|
|
372
|
+
info_table = Table(title="Service Information")
|
|
373
|
+
info_table.add_column("Property", style="cyan")
|
|
374
|
+
info_table.add_column("Value", style="white")
|
|
375
|
+
|
|
376
|
+
info_table.add_row("Service Name", service_name)
|
|
377
|
+
info_table.add_row("URL", url)
|
|
378
|
+
info_table.add_row("Port", str(port))
|
|
379
|
+
info_table.add_row(
|
|
380
|
+
"API Key",
|
|
381
|
+
secrets["api_key"][:8] + "..."
|
|
382
|
+
if len(secrets["api_key"]) > 8
|
|
383
|
+
else secrets["api_key"],
|
|
384
|
+
)
|
|
385
|
+
info_table.add_row("Environment", "dev")
|
|
386
|
+
|
|
387
|
+
console.print(info_table)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _show_service_logs(service_name: str) -> None:
|
|
391
|
+
"""Show service logs for debugging."""
|
|
392
|
+
console.print("\n[bold]Service Logs:[/]")
|
|
393
|
+
try:
|
|
394
|
+
# Try docker-compose logs first
|
|
395
|
+
result = subprocess.run(
|
|
396
|
+
[
|
|
397
|
+
"docker-compose",
|
|
398
|
+
"-f",
|
|
399
|
+
".deployment/docker-compose.yml",
|
|
400
|
+
"logs",
|
|
401
|
+
"--tail=100",
|
|
402
|
+
service_name,
|
|
403
|
+
],
|
|
404
|
+
capture_output=True,
|
|
405
|
+
text=True,
|
|
406
|
+
)
|
|
407
|
+
if result.stdout:
|
|
408
|
+
console.print(result.stdout)
|
|
409
|
+
if result.stderr:
|
|
410
|
+
console.print(f"[yellow]Stderr:[/] {result.stderr}")
|
|
411
|
+
|
|
412
|
+
# Also try direct docker logs as fallback
|
|
413
|
+
if not result.stdout:
|
|
414
|
+
console.print("\n[bold]Trying direct docker logs:[/]")
|
|
415
|
+
docker_logs = subprocess.run(
|
|
416
|
+
[
|
|
417
|
+
"docker",
|
|
418
|
+
"logs",
|
|
419
|
+
f"deployment-{service_name}-1",
|
|
420
|
+
"--tail=100",
|
|
421
|
+
],
|
|
422
|
+
capture_output=True,
|
|
423
|
+
text=True,
|
|
424
|
+
)
|
|
425
|
+
if docker_logs.stdout:
|
|
426
|
+
console.print(docker_logs.stdout)
|
|
427
|
+
if docker_logs.stderr:
|
|
428
|
+
console.print(f"[yellow]Stderr:[/] {docker_logs.stderr}")
|
|
429
|
+
|
|
430
|
+
# Also try to get container status
|
|
431
|
+
console.print("\n[bold]Container Status:[/]")
|
|
432
|
+
status_result = subprocess.run(
|
|
433
|
+
[
|
|
434
|
+
"docker-compose",
|
|
435
|
+
"-f",
|
|
436
|
+
".deployment/docker-compose.yml",
|
|
437
|
+
"ps",
|
|
438
|
+
"-a",
|
|
439
|
+
],
|
|
440
|
+
capture_output=True,
|
|
441
|
+
text=True,
|
|
442
|
+
)
|
|
443
|
+
if status_result.stdout:
|
|
444
|
+
console.print(status_result.stdout)
|
|
445
|
+
except Exception as e:
|
|
446
|
+
console.print(f"[red]Failed to get logs:[/] {e}")
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _cleanup_test_resources(service_name: str) -> None:
|
|
450
|
+
"""Clean up test resources."""
|
|
451
|
+
console.print("\n[bold]Cleaning up test resources...[/]")
|
|
452
|
+
try:
|
|
453
|
+
subprocess.run(
|
|
454
|
+
["docker-compose", "-f", ".deployment/docker-compose.yml", "down"],
|
|
455
|
+
capture_output=True,
|
|
456
|
+
text=True,
|
|
457
|
+
)
|
|
458
|
+
console.print("[green]✓[/] Test resources cleaned up")
|
|
459
|
+
except Exception as e:
|
|
460
|
+
console.print(f"[yellow]Warning:[/] Failed to cleanup resources: {e}")
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Copyright (c) 2024-2025 Alain Prasquier - Supervaize.com. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
|
|
4
|
+
# If a copy of the MPL was not distributed with this file, you can obtain one at
|
|
5
|
+
# https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Plan Command
|
|
9
|
+
|
|
10
|
+
Shows what changes will be made during deployment.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
|
|
19
|
+
from supervaizer.common import log
|
|
20
|
+
from supervaizer.deploy.driver_factory import create_driver, get_supported_platforms
|
|
21
|
+
from supervaizer.deploy.utils import get_git_sha
|
|
22
|
+
from supervaizer.deploy.drivers.base import DeploymentPlan
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def plan_deployment(
|
|
28
|
+
platform: str,
|
|
29
|
+
name: Optional[str] = None,
|
|
30
|
+
env: str = "dev",
|
|
31
|
+
region: Optional[str] = None,
|
|
32
|
+
project_id: Optional[str] = None,
|
|
33
|
+
verbose: bool = False,
|
|
34
|
+
source_dir: Optional[Path] = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Plan deployment changes without applying them."""
|
|
37
|
+
# Validate platform
|
|
38
|
+
if platform not in get_supported_platforms():
|
|
39
|
+
console.print(f"[bold red]Error:[/] Unsupported platform: {platform}")
|
|
40
|
+
console.print(f"Supported platforms: {', '.join(get_supported_platforms())}")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
# Set defaults
|
|
44
|
+
if not name:
|
|
45
|
+
name = (source_dir or Path.cwd()).name
|
|
46
|
+
if not region:
|
|
47
|
+
region = _get_default_region(platform)
|
|
48
|
+
|
|
49
|
+
console.print(f"[bold blue]Planning deployment to {platform}[/bold blue]")
|
|
50
|
+
console.print(f"Service name: {name}")
|
|
51
|
+
console.print(f"Environment: {env}")
|
|
52
|
+
console.print(f"Region: {region}")
|
|
53
|
+
if project_id:
|
|
54
|
+
console.print(f"Project ID: {project_id}")
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
# Create driver
|
|
58
|
+
driver = create_driver(platform, region, project_id)
|
|
59
|
+
|
|
60
|
+
# Check prerequisites
|
|
61
|
+
prerequisites = driver.check_prerequisites()
|
|
62
|
+
if prerequisites:
|
|
63
|
+
console.print("[bold red]Prerequisites not met:[/]")
|
|
64
|
+
for prereq in prerequisites:
|
|
65
|
+
console.print(f" • {prereq}")
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
# Generate image tag
|
|
69
|
+
image_tag = _generate_image_tag(name, env)
|
|
70
|
+
|
|
71
|
+
# Create deployment plan
|
|
72
|
+
plan = driver.plan_deployment(
|
|
73
|
+
service_name=name,
|
|
74
|
+
environment=env,
|
|
75
|
+
image_tag=image_tag,
|
|
76
|
+
port=8000,
|
|
77
|
+
env_vars=_get_default_env_vars(env),
|
|
78
|
+
secrets=_get_default_secrets(name, env),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Display plan
|
|
82
|
+
_display_plan(plan)
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
log.error(f"Planning failed: {e}")
|
|
86
|
+
console.print(f"[bold red]Planning failed:[/] {e}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _get_default_region(platform: str) -> str:
|
|
90
|
+
"""Get default region for platform."""
|
|
91
|
+
defaults = {
|
|
92
|
+
"cloud-run": "us-central1",
|
|
93
|
+
"aws-app-runner": "us-east-1",
|
|
94
|
+
"do-app-platform": "nyc3",
|
|
95
|
+
}
|
|
96
|
+
return defaults.get(platform, "us-central1")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _generate_image_tag(service_name: str, environment: str) -> str:
|
|
100
|
+
"""Generate image tag for deployment."""
|
|
101
|
+
git_sha = get_git_sha()
|
|
102
|
+
return f"{service_name}-{environment}:{git_sha}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _get_default_env_vars(environment: str) -> dict[str, str]:
|
|
106
|
+
"""Get default environment variables."""
|
|
107
|
+
return {
|
|
108
|
+
"SUPERVAIZER_ENVIRONMENT": environment,
|
|
109
|
+
"SUPERVAIZER_HOST": "0.0.0.0",
|
|
110
|
+
"SUPERVAIZER_PORT": "8000",
|
|
111
|
+
"SV_LOG_LEVEL": "INFO",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _get_default_secrets(service_name: str, environment: str) -> dict[str, str]:
|
|
116
|
+
"""Get default secrets for deployment."""
|
|
117
|
+
return {
|
|
118
|
+
f"{service_name}-{environment}-api-key": "placeholder-api-key",
|
|
119
|
+
f"{service_name}-{environment}-rsa-key": "placeholder-rsa-key",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _display_plan(plan: DeploymentPlan) -> None:
|
|
124
|
+
"""Display deployment plan."""
|
|
125
|
+
console.print(
|
|
126
|
+
f"\n[bold]Deployment Plan for {plan.service_name}-{plan.environment}[/bold]"
|
|
127
|
+
)
|
|
128
|
+
console.print(f"Platform: {plan.platform}")
|
|
129
|
+
console.print(f"Region: {plan.region}")
|
|
130
|
+
console.print(f"Target Image: {plan.target_image}")
|
|
131
|
+
|
|
132
|
+
if plan.current_image:
|
|
133
|
+
console.print(f"Current Image: {plan.current_image}")
|
|
134
|
+
if plan.current_url:
|
|
135
|
+
console.print(f"Current URL: {plan.current_url}")
|
|
136
|
+
|
|
137
|
+
# Display actions
|
|
138
|
+
if plan.actions:
|
|
139
|
+
table = Table(title="Actions")
|
|
140
|
+
table.add_column("Type", style="cyan")
|
|
141
|
+
table.add_column("Resource", style="magenta")
|
|
142
|
+
table.add_column("Action", style="green")
|
|
143
|
+
table.add_column("Description", style="white")
|
|
144
|
+
|
|
145
|
+
for action in plan.actions:
|
|
146
|
+
table.add_row(
|
|
147
|
+
action.resource_type.value,
|
|
148
|
+
action.resource_name,
|
|
149
|
+
action.action_type.value,
|
|
150
|
+
action.description,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
console.print(table)
|
|
154
|
+
else:
|
|
155
|
+
console.print("[yellow]No actions required[/yellow]")
|
|
156
|
+
|
|
157
|
+
# Display environment variables
|
|
158
|
+
if plan.target_env_vars:
|
|
159
|
+
console.print("\n[bold]Environment Variables:[/bold]")
|
|
160
|
+
for key, value in plan.target_env_vars.items():
|
|
161
|
+
console.print(f" {key}={value}")
|
|
162
|
+
|
|
163
|
+
# Display secrets
|
|
164
|
+
if plan.target_secrets:
|
|
165
|
+
console.print("\n[bold]Secrets:[/bold]")
|
|
166
|
+
for key in plan.target_secrets.keys():
|
|
167
|
+
console.print(f" {key}=***")
|