supervaizer 0.9.8__py3-none-any.whl → 0.10.1__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 +11 -2
- supervaizer/__version__.py +1 -1
- supervaizer/account.py +4 -0
- supervaizer/account_service.py +7 -1
- supervaizer/admin/routes.py +24 -8
- supervaizer/admin/templates/agents.html +74 -0
- supervaizer/admin/templates/agents_grid.html +5 -3
- supervaizer/admin/templates/navigation.html +11 -1
- supervaizer/admin/templates/supervaize_instructions.html +212 -0
- supervaizer/agent.py +28 -6
- supervaizer/case.py +46 -14
- supervaizer/cli.py +247 -7
- supervaizer/common.py +45 -4
- supervaizer/deploy/__init__.py +16 -0
- supervaizer/deploy/cli.py +296 -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 +378 -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/examples/controller_template.py +1 -1
- supervaizer/job.py +18 -5
- supervaizer/job_service.py +6 -5
- supervaizer/parameter.py +13 -1
- supervaizer/protocol/__init__.py +2 -2
- supervaizer/protocol/a2a/routes.py +1 -1
- supervaizer/routes.py +141 -17
- supervaizer/server.py +5 -11
- supervaizer/utils/__init__.py +16 -0
- supervaizer/utils/version_check.py +56 -0
- {supervaizer-0.9.8.dist-info → supervaizer-0.10.1.dist-info}/METADATA +105 -34
- supervaizer-0.10.1.dist-info/RECORD +76 -0
- {supervaizer-0.9.8.dist-info → supervaizer-0.10.1.dist-info}/WHEEL +1 -1
- supervaizer/protocol/acp/__init__.py +0 -21
- supervaizer/protocol/acp/model.py +0 -198
- supervaizer/protocol/acp/routes.py +0 -74
- supervaizer-0.9.8.dist-info/RECORD +0 -52
- {supervaizer-0.9.8.dist-info → supervaizer-0.10.1.dist-info}/entry_points.txt +0 -0
- {supervaizer-0.9.8.dist-info → supervaizer-0.10.1.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -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}=***")
|
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
Status Command
|
|
9
|
+
|
|
10
|
+
Show deployment status and health information.
|
|
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.drivers.base import DeploymentResult
|
|
22
|
+
from supervaizer.deploy.state import DeploymentState, StateManager
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def deploy_status(
|
|
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
|
+
"""Show deployment status and health information."""
|
|
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]Deployment Status for {name}-{env}[/bold blue]")
|
|
50
|
+
console.print(f"Platform: {platform}")
|
|
51
|
+
console.print(f"Region: {region}")
|
|
52
|
+
if project_id:
|
|
53
|
+
console.print(f"Project ID: {project_id}")
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
# Check local state first
|
|
57
|
+
deployment_dir = (source_dir or Path.cwd()) / ".deployment"
|
|
58
|
+
if deployment_dir.exists():
|
|
59
|
+
state_manager = StateManager(deployment_dir)
|
|
60
|
+
state = state_manager.load_state()
|
|
61
|
+
|
|
62
|
+
if state:
|
|
63
|
+
console.print("\n[bold]Local State:[/bold]")
|
|
64
|
+
_display_state(state)
|
|
65
|
+
|
|
66
|
+
# Get live status from platform
|
|
67
|
+
driver = create_driver(platform, region, project_id)
|
|
68
|
+
|
|
69
|
+
# Check prerequisites
|
|
70
|
+
prerequisites = driver.check_prerequisites()
|
|
71
|
+
if prerequisites:
|
|
72
|
+
console.print("[bold red]Prerequisites not met:[/]")
|
|
73
|
+
for prereq in prerequisites:
|
|
74
|
+
console.print(f" • {prereq}")
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
# Get service status
|
|
78
|
+
result = driver.get_service_status(name, env)
|
|
79
|
+
|
|
80
|
+
if result.success:
|
|
81
|
+
console.print("\n[bold]Live Status:[/bold]")
|
|
82
|
+
_display_service_status(result)
|
|
83
|
+
else:
|
|
84
|
+
console.print(
|
|
85
|
+
f"\n[bold red]Service not found or error:[/] {result.error_message}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
log.error(f"Status check failed: {e}")
|
|
90
|
+
console.print(f"[bold red]Status check failed:[/] {e}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _get_default_region(platform: str) -> str:
|
|
94
|
+
"""Get default region for platform."""
|
|
95
|
+
defaults = {
|
|
96
|
+
"cloud-run": "us-central1",
|
|
97
|
+
"aws-app-runner": "us-east-1",
|
|
98
|
+
"do-app-platform": "nyc3",
|
|
99
|
+
}
|
|
100
|
+
return defaults.get(platform, "us-central1")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _display_state(state: DeploymentState) -> None:
|
|
104
|
+
"""Display deployment state."""
|
|
105
|
+
table = Table(title="Deployment State")
|
|
106
|
+
table.add_column("Property", style="cyan")
|
|
107
|
+
table.add_column("Value", style="white")
|
|
108
|
+
|
|
109
|
+
table.add_row("Service Name", state.service_name)
|
|
110
|
+
table.add_row("Platform", state.platform)
|
|
111
|
+
table.add_row("Environment", state.environment)
|
|
112
|
+
table.add_row("Region", state.region)
|
|
113
|
+
if state.project_id:
|
|
114
|
+
table.add_row("Project ID", state.project_id)
|
|
115
|
+
table.add_row("Image Tag", state.image_tag)
|
|
116
|
+
if state.image_digest:
|
|
117
|
+
table.add_row("Image Digest", state.image_digest[:16] + "...")
|
|
118
|
+
if state.service_url:
|
|
119
|
+
table.add_row("Service URL", state.service_url)
|
|
120
|
+
if state.revision:
|
|
121
|
+
table.add_row("Revision", state.revision)
|
|
122
|
+
table.add_row("Status", state.status)
|
|
123
|
+
table.add_row("Health Status", state.health_status)
|
|
124
|
+
table.add_row("Port", str(state.port))
|
|
125
|
+
table.add_row("API Key Generated", str(state.api_key_generated))
|
|
126
|
+
table.add_row("RSA Key Generated", str(state.rsa_key_generated))
|
|
127
|
+
table.add_row("Created At", state.created_at.isoformat())
|
|
128
|
+
table.add_row("Updated At", state.updated_at.isoformat())
|
|
129
|
+
|
|
130
|
+
console.print(table)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _display_service_status(result: DeploymentResult) -> None:
|
|
134
|
+
"""Display service status."""
|
|
135
|
+
table = Table(title="Service Status")
|
|
136
|
+
table.add_column("Property", style="cyan")
|
|
137
|
+
table.add_column("Value", style="white")
|
|
138
|
+
|
|
139
|
+
if result.service_url:
|
|
140
|
+
table.add_row("Service URL", result.service_url)
|
|
141
|
+
table.add_row("API Documentation", f"{result.service_url}/docs")
|
|
142
|
+
table.add_row("ReDoc Documentation", f"{result.service_url}/redoc")
|
|
143
|
+
|
|
144
|
+
if result.service_id:
|
|
145
|
+
table.add_row("Service ID", result.service_id)
|
|
146
|
+
|
|
147
|
+
if result.revision:
|
|
148
|
+
table.add_row("Revision", result.revision)
|
|
149
|
+
|
|
150
|
+
if result.image_digest:
|
|
151
|
+
table.add_row("Image Digest", result.image_digest[:16] + "...")
|
|
152
|
+
|
|
153
|
+
table.add_row("Status", result.status)
|
|
154
|
+
table.add_row("Health Status", result.health_status)
|
|
155
|
+
|
|
156
|
+
if result.deployment_time:
|
|
157
|
+
table.add_row("Deployment Time", f"{result.deployment_time:.1f}s")
|
|
158
|
+
|
|
159
|
+
console.print(table)
|
|
160
|
+
|
|
161
|
+
# Health check details
|
|
162
|
+
if result.service_url:
|
|
163
|
+
console.print("\n[bold]Health Check:[/bold]")
|
|
164
|
+
if result.health_status == "healthy":
|
|
165
|
+
console.print("[green]✓[/green] Service is healthy")
|
|
166
|
+
elif result.health_status == "unhealthy":
|
|
167
|
+
console.print("[red]✗[/red] Service is unhealthy")
|
|
168
|
+
else:
|
|
169
|
+
console.print("[yellow]?[/yellow] Health status unknown")
|
|
@@ -0,0 +1,281 @@
|
|
|
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
|
+
Up Command
|
|
9
|
+
|
|
10
|
+
Deploy or update the service.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import secrets
|
|
14
|
+
import string
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
20
|
+
|
|
21
|
+
from supervaizer.common import log
|
|
22
|
+
from supervaizer.deploy.docker import DockerManager, ensure_docker_running
|
|
23
|
+
from supervaizer.deploy.driver_factory import create_driver, get_supported_platforms
|
|
24
|
+
from supervaizer.deploy.drivers.base import DeploymentResult
|
|
25
|
+
from supervaizer.deploy.state import StateManager
|
|
26
|
+
from supervaizer.deploy.utils import create_deployment_directory, get_git_sha
|
|
27
|
+
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def deploy_up(
|
|
32
|
+
platform: str,
|
|
33
|
+
name: Optional[str] = None,
|
|
34
|
+
env: str = "dev",
|
|
35
|
+
region: Optional[str] = None,
|
|
36
|
+
project_id: Optional[str] = None,
|
|
37
|
+
image: Optional[str] = None,
|
|
38
|
+
port: int = 8000,
|
|
39
|
+
generate_api_key: bool = False,
|
|
40
|
+
generate_rsa: bool = False,
|
|
41
|
+
yes: bool = False,
|
|
42
|
+
no_rollback: bool = False,
|
|
43
|
+
timeout: int = 300,
|
|
44
|
+
verbose: bool = False,
|
|
45
|
+
source_dir: Optional[Path] = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Deploy or update the service."""
|
|
48
|
+
# Validate platform
|
|
49
|
+
if platform not in get_supported_platforms():
|
|
50
|
+
console.print(f"[bold red]Error:[/] Unsupported platform: {platform}")
|
|
51
|
+
console.print(f"Supported platforms: {', '.join(get_supported_platforms())}")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
# Set defaults
|
|
55
|
+
if not name:
|
|
56
|
+
name = (source_dir or Path.cwd()).name
|
|
57
|
+
if not region:
|
|
58
|
+
region = _get_default_region(platform)
|
|
59
|
+
|
|
60
|
+
console.print(f"[bold green]Deploying to {platform}[/bold green]")
|
|
61
|
+
console.print(f"Service name: {name}")
|
|
62
|
+
console.print(f"Environment: {env}")
|
|
63
|
+
console.print(f"Region: {region}")
|
|
64
|
+
console.print(f"Port: {port}")
|
|
65
|
+
if project_id:
|
|
66
|
+
console.print(f"Project ID: {project_id}")
|
|
67
|
+
if image:
|
|
68
|
+
console.print(f"Image: {image}")
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
# Check Docker
|
|
72
|
+
if not ensure_docker_running():
|
|
73
|
+
console.print("[bold red]Error:[/] Docker is not running")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
# Create deployment directory
|
|
77
|
+
deployment_dir = create_deployment_directory(source_dir or Path.cwd())
|
|
78
|
+
state_manager = StateManager(deployment_dir)
|
|
79
|
+
|
|
80
|
+
# Create driver
|
|
81
|
+
driver = create_driver(platform, region, project_id)
|
|
82
|
+
|
|
83
|
+
# Check prerequisites
|
|
84
|
+
prerequisites = driver.check_prerequisites()
|
|
85
|
+
if prerequisites:
|
|
86
|
+
console.print("[bold red]Prerequisites not met:[/]")
|
|
87
|
+
for prereq in prerequisites:
|
|
88
|
+
console.print(f" • {prereq}")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
# Generate image tag
|
|
92
|
+
if not image:
|
|
93
|
+
image = _generate_image_tag(name, env)
|
|
94
|
+
|
|
95
|
+
# Generate secrets
|
|
96
|
+
secrets_dict = _generate_secrets(name, env, generate_api_key, generate_rsa)
|
|
97
|
+
|
|
98
|
+
# Build and push Docker image
|
|
99
|
+
with Progress(
|
|
100
|
+
SpinnerColumn(),
|
|
101
|
+
TextColumn("[progress.description]{task.description}"),
|
|
102
|
+
console=console,
|
|
103
|
+
) as progress:
|
|
104
|
+
task = progress.add_task("Building Docker image...", total=None)
|
|
105
|
+
|
|
106
|
+
docker_manager = DockerManager()
|
|
107
|
+
|
|
108
|
+
# Generate Docker files
|
|
109
|
+
dockerfile_path = deployment_dir / "Dockerfile"
|
|
110
|
+
dockerignore_path = deployment_dir / ".dockerignore"
|
|
111
|
+
compose_path = deployment_dir / "docker-compose.yml"
|
|
112
|
+
|
|
113
|
+
docker_manager.generate_dockerfile(
|
|
114
|
+
output_path=dockerfile_path,
|
|
115
|
+
app_port=port,
|
|
116
|
+
)
|
|
117
|
+
docker_manager.generate_dockerignore(dockerignore_path)
|
|
118
|
+
docker_manager.generate_docker_compose(
|
|
119
|
+
compose_path,
|
|
120
|
+
port=port,
|
|
121
|
+
service_name=name,
|
|
122
|
+
environment=env,
|
|
123
|
+
api_key=secrets_dict.get("api_key", "test-api-key"),
|
|
124
|
+
rsa_key=secrets_dict.get("rsa_private_key", "test-rsa-key"),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Build image
|
|
128
|
+
progress.update(task, description="Building Docker image...")
|
|
129
|
+
|
|
130
|
+
# Get build arguments for environment variables
|
|
131
|
+
from supervaizer.deploy.docker import get_docker_build_args
|
|
132
|
+
|
|
133
|
+
build_args = get_docker_build_args(port)
|
|
134
|
+
|
|
135
|
+
docker_manager.build_image(
|
|
136
|
+
image, source_dir or Path.cwd(), dockerfile_path, build_args=build_args
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Push image (this would be platform-specific)
|
|
140
|
+
progress.update(task, description="Pushing Docker image...")
|
|
141
|
+
# Note: Actual push would depend on the platform's registry
|
|
142
|
+
|
|
143
|
+
# Deploy service
|
|
144
|
+
with Progress(
|
|
145
|
+
SpinnerColumn(),
|
|
146
|
+
TextColumn("[progress.description]{task.description}"),
|
|
147
|
+
console=console,
|
|
148
|
+
) as progress:
|
|
149
|
+
task = progress.add_task("Deploying service...", total=None)
|
|
150
|
+
|
|
151
|
+
result = driver.deploy_service(
|
|
152
|
+
service_name=name,
|
|
153
|
+
environment=env,
|
|
154
|
+
image_tag=image,
|
|
155
|
+
port=port,
|
|
156
|
+
env_vars=_get_default_env_vars(env),
|
|
157
|
+
secrets=secrets_dict,
|
|
158
|
+
timeout=timeout,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Update state
|
|
162
|
+
if result.success:
|
|
163
|
+
state_manager.update_state(
|
|
164
|
+
service_name=name,
|
|
165
|
+
platform=platform,
|
|
166
|
+
environment=env,
|
|
167
|
+
region=region,
|
|
168
|
+
project_id=project_id,
|
|
169
|
+
image_tag=image,
|
|
170
|
+
image_digest=result.image_digest,
|
|
171
|
+
service_url=result.service_url,
|
|
172
|
+
revision=result.revision,
|
|
173
|
+
status=result.status,
|
|
174
|
+
health_status=result.health_status,
|
|
175
|
+
port=port,
|
|
176
|
+
api_key_generated=generate_api_key,
|
|
177
|
+
rsa_key_generated=generate_rsa,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Display results
|
|
181
|
+
_display_deployment_result(result)
|
|
182
|
+
else:
|
|
183
|
+
console.print(f"[bold red]Deployment failed:[/] {result.error_message}")
|
|
184
|
+
if not no_rollback:
|
|
185
|
+
console.print(
|
|
186
|
+
"[yellow]Consider using --no-rollback to keep failed revision[/yellow]"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
except Exception as e:
|
|
190
|
+
log.error(f"Deployment failed: {e}")
|
|
191
|
+
console.print(f"[bold red]Deployment failed:[/] {e}")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _get_default_region(platform: str) -> str:
|
|
195
|
+
"""Get default region for platform."""
|
|
196
|
+
defaults = {
|
|
197
|
+
"cloud-run": "us-central1",
|
|
198
|
+
"aws-app-runner": "us-east-1",
|
|
199
|
+
"do-app-platform": "nyc3",
|
|
200
|
+
}
|
|
201
|
+
return defaults.get(platform, "us-central1")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _generate_image_tag(service_name: str, environment: str) -> str:
|
|
205
|
+
"""Generate image tag for deployment."""
|
|
206
|
+
git_sha = get_git_sha()
|
|
207
|
+
return f"{service_name}-{environment}:{git_sha}"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _generate_secrets(
|
|
211
|
+
service_name: str, environment: str, generate_api_key: bool, generate_rsa: bool
|
|
212
|
+
) -> dict[str, str]:
|
|
213
|
+
"""Generate secrets for deployment."""
|
|
214
|
+
secrets_dict = {}
|
|
215
|
+
|
|
216
|
+
if generate_api_key:
|
|
217
|
+
api_key = _generate_api_key()
|
|
218
|
+
secrets_dict[f"{service_name}-{environment}-api-key"] = api_key
|
|
219
|
+
|
|
220
|
+
if generate_rsa:
|
|
221
|
+
rsa_key = _generate_rsa_key()
|
|
222
|
+
secrets_dict[f"{service_name}-{environment}-rsa-key"] = rsa_key
|
|
223
|
+
|
|
224
|
+
return secrets_dict
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _generate_api_key() -> str:
|
|
228
|
+
"""Generate a secure API key."""
|
|
229
|
+
alphabet = string.ascii_letters + string.digits
|
|
230
|
+
return "".join(secrets.choice(alphabet) for _ in range(32))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _generate_rsa_key() -> str:
|
|
234
|
+
"""Generate an RSA private key."""
|
|
235
|
+
from cryptography.hazmat.primitives import serialization
|
|
236
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
237
|
+
|
|
238
|
+
private_key = rsa.generate_private_key(
|
|
239
|
+
public_exponent=65537,
|
|
240
|
+
key_size=2048,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
pem = private_key.private_bytes(
|
|
244
|
+
encoding=serialization.Encoding.PEM,
|
|
245
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
246
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
return pem.decode("utf-8")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _get_default_env_vars(environment: str) -> dict[str, str]:
|
|
253
|
+
"""Get default environment variables."""
|
|
254
|
+
return {
|
|
255
|
+
"SUPERVAIZER_ENVIRONMENT": environment,
|
|
256
|
+
"SUPERVAIZER_HOST": "0.0.0.0",
|
|
257
|
+
"SUPERVAIZER_PORT": "8000",
|
|
258
|
+
"SV_LOG_LEVEL": "INFO",
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _display_deployment_result(result: DeploymentResult) -> None:
|
|
263
|
+
"""Display deployment result."""
|
|
264
|
+
console.print("\n[bold green]Deployment successful![/bold green]")
|
|
265
|
+
|
|
266
|
+
if result.service_url:
|
|
267
|
+
console.print(f"Service URL: {result.service_url}")
|
|
268
|
+
console.print(f"API Documentation: {result.service_url}/docs")
|
|
269
|
+
console.print(f"ReDoc Documentation: {result.service_url}/redoc")
|
|
270
|
+
|
|
271
|
+
if result.service_id:
|
|
272
|
+
console.print(f"Service ID: {result.service_id}")
|
|
273
|
+
|
|
274
|
+
if result.revision:
|
|
275
|
+
console.print(f"Revision: {result.revision}")
|
|
276
|
+
|
|
277
|
+
console.print(f"Status: {result.status}")
|
|
278
|
+
console.print(f"Health: {result.health_status}")
|
|
279
|
+
|
|
280
|
+
if result.deployment_time:
|
|
281
|
+
console.print(f"Deployment time: {result.deployment_time:.1f}s")
|