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,570 @@
|
|
|
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
|
+
GCP Cloud Run Driver
|
|
9
|
+
|
|
10
|
+
This module implements deployment to Google Cloud Platform Cloud Run.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import subprocess
|
|
14
|
+
import time
|
|
15
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
|
|
19
|
+
from supervaizer.common import log
|
|
20
|
+
from supervaizer.deploy.drivers.base import (
|
|
21
|
+
ActionType,
|
|
22
|
+
BaseDriver,
|
|
23
|
+
DeploymentPlan,
|
|
24
|
+
DeploymentResult,
|
|
25
|
+
ResourceAction,
|
|
26
|
+
ResourceType,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
console = Console()
|
|
30
|
+
|
|
31
|
+
# Conditional imports for Google Cloud libraries
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from google.cloud import artifactregistry_v1
|
|
34
|
+
from google.cloud import run_v2
|
|
35
|
+
from google.cloud import secretmanager
|
|
36
|
+
from google.cloud.exceptions import NotFound
|
|
37
|
+
|
|
38
|
+
GOOGLE_CLOUD_AVAILABLE = True
|
|
39
|
+
else:
|
|
40
|
+
try:
|
|
41
|
+
from google.cloud import artifactregistry_v1
|
|
42
|
+
from google.cloud import run_v2
|
|
43
|
+
from google.cloud import secretmanager
|
|
44
|
+
from google.cloud.exceptions import NotFound
|
|
45
|
+
|
|
46
|
+
GOOGLE_CLOUD_AVAILABLE = True
|
|
47
|
+
except ImportError:
|
|
48
|
+
GOOGLE_CLOUD_AVAILABLE = False
|
|
49
|
+
|
|
50
|
+
# Create dummy classes for type hints when not available
|
|
51
|
+
class NotFound(Exception):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
class artifactregistry_v1:
|
|
55
|
+
class ArtifactRegistryClient:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
class run_v2:
|
|
59
|
+
class ServicesClient:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
class secretmanager:
|
|
63
|
+
class SecretManagerServiceClient:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class CloudRunDriver(BaseDriver):
|
|
68
|
+
"""Driver for deploying to GCP Cloud Run."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, region: str, project_id: str):
|
|
71
|
+
"""Initialize Cloud Run driver."""
|
|
72
|
+
if not GOOGLE_CLOUD_AVAILABLE:
|
|
73
|
+
raise ImportError(
|
|
74
|
+
"Google Cloud libraries not available. Install with: "
|
|
75
|
+
"pip install google-cloud-run google-cloud-secret-manager google-cloud-artifact-registry"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
super().__init__(region, project_id)
|
|
79
|
+
self.project_id = project_id
|
|
80
|
+
|
|
81
|
+
# Initialize clients
|
|
82
|
+
self.run_client = run_v2.ServicesClient()
|
|
83
|
+
self.secret_client = secretmanager.SecretManagerServiceClient()
|
|
84
|
+
self.registry_client = artifactregistry_v1.ArtifactRegistryClient()
|
|
85
|
+
|
|
86
|
+
# Service configuration
|
|
87
|
+
self.service_parent = f"projects/{project_id}/locations/{region}"
|
|
88
|
+
self.registry_location = f"projects/{project_id}/locations/{region}"
|
|
89
|
+
|
|
90
|
+
def plan_deployment(
|
|
91
|
+
self,
|
|
92
|
+
service_name: str,
|
|
93
|
+
environment: str,
|
|
94
|
+
image_tag: str,
|
|
95
|
+
port: int = 8000,
|
|
96
|
+
env_vars: Optional[Dict[str, str]] = None,
|
|
97
|
+
secrets: Optional[Dict[str, str]] = None,
|
|
98
|
+
) -> DeploymentPlan:
|
|
99
|
+
"""Plan deployment changes without applying them."""
|
|
100
|
+
full_service_name = self.get_service_key(service_name, environment)
|
|
101
|
+
service_path = f"{self.service_parent}/services/{full_service_name}"
|
|
102
|
+
|
|
103
|
+
actions = []
|
|
104
|
+
current_image = None
|
|
105
|
+
current_url = None
|
|
106
|
+
current_status = None
|
|
107
|
+
|
|
108
|
+
# Check if service exists
|
|
109
|
+
try:
|
|
110
|
+
service = self.run_client.get_service(name=service_path)
|
|
111
|
+
current_image = service.template.containers[0].image
|
|
112
|
+
current_url = service.uri
|
|
113
|
+
current_status = "running"
|
|
114
|
+
|
|
115
|
+
# Check if update is needed
|
|
116
|
+
if current_image != image_tag:
|
|
117
|
+
actions.append(
|
|
118
|
+
ResourceAction(
|
|
119
|
+
resource_type=ResourceType.SERVICE,
|
|
120
|
+
action_type=ActionType.UPDATE,
|
|
121
|
+
resource_name=full_service_name,
|
|
122
|
+
description=f"Update service image from {current_image} to {image_tag}",
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
else:
|
|
126
|
+
actions.append(
|
|
127
|
+
ResourceAction(
|
|
128
|
+
resource_type=ResourceType.SERVICE,
|
|
129
|
+
action_type=ActionType.NOOP,
|
|
130
|
+
resource_name=full_service_name,
|
|
131
|
+
description="Service image is up to date",
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
except NotFound:
|
|
135
|
+
# Service doesn't exist, need to create
|
|
136
|
+
actions.append(
|
|
137
|
+
ResourceAction(
|
|
138
|
+
resource_type=ResourceType.SERVICE,
|
|
139
|
+
action_type=ActionType.CREATE,
|
|
140
|
+
resource_name=full_service_name,
|
|
141
|
+
description=f"Create new Cloud Run service with image {image_tag}",
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Check secrets
|
|
146
|
+
if secrets:
|
|
147
|
+
for secret_name, secret_value in secrets.items():
|
|
148
|
+
secret_path = f"projects/{self.project_id}/secrets/{secret_name}"
|
|
149
|
+
try:
|
|
150
|
+
self.secret_client.get_secret(name=secret_path)
|
|
151
|
+
actions.append(
|
|
152
|
+
ResourceAction(
|
|
153
|
+
resource_type=ResourceType.SECRET,
|
|
154
|
+
action_type=ActionType.UPDATE,
|
|
155
|
+
resource_name=secret_name,
|
|
156
|
+
description=f"Update secret {secret_name}",
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
except NotFound:
|
|
160
|
+
actions.append(
|
|
161
|
+
ResourceAction(
|
|
162
|
+
resource_type=ResourceType.SECRET,
|
|
163
|
+
action_type=ActionType.CREATE,
|
|
164
|
+
resource_name=secret_name,
|
|
165
|
+
description=f"Create secret {secret_name}",
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return DeploymentPlan(
|
|
170
|
+
platform="cloud-run",
|
|
171
|
+
service_name=service_name,
|
|
172
|
+
environment=environment,
|
|
173
|
+
region=self.region,
|
|
174
|
+
project_id=self.project_id,
|
|
175
|
+
actions=actions,
|
|
176
|
+
current_image=current_image,
|
|
177
|
+
current_url=current_url,
|
|
178
|
+
current_status=current_status,
|
|
179
|
+
target_image=image_tag,
|
|
180
|
+
target_port=port,
|
|
181
|
+
target_env_vars=env_vars or {},
|
|
182
|
+
target_secrets=secrets or {},
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def deploy_service(
|
|
186
|
+
self,
|
|
187
|
+
service_name: str,
|
|
188
|
+
environment: str,
|
|
189
|
+
image_tag: str,
|
|
190
|
+
port: int = 8000,
|
|
191
|
+
env_vars: Optional[Dict[str, str]] = None,
|
|
192
|
+
secrets: Optional[Dict[str, str]] = None,
|
|
193
|
+
timeout: int = 300,
|
|
194
|
+
) -> DeploymentResult:
|
|
195
|
+
"""Deploy or update the service."""
|
|
196
|
+
start_time = time.time()
|
|
197
|
+
full_service_name = self.get_service_key(service_name, environment)
|
|
198
|
+
service_path = f"{self.service_parent}/services/{full_service_name}"
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
# Create/update secrets first
|
|
202
|
+
if secrets:
|
|
203
|
+
self._create_or_update_secrets(secrets)
|
|
204
|
+
|
|
205
|
+
# Create/update service
|
|
206
|
+
service = self._create_or_update_service(
|
|
207
|
+
full_service_name,
|
|
208
|
+
image_tag,
|
|
209
|
+
port,
|
|
210
|
+
env_vars or {},
|
|
211
|
+
secrets or {},
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Wait for service to be ready
|
|
215
|
+
service_url = self._wait_for_service_ready(service_path, timeout)
|
|
216
|
+
|
|
217
|
+
# Set SUPERVAIZER_PUBLIC_URL
|
|
218
|
+
if service_url:
|
|
219
|
+
self._set_public_url(service_path, service_url)
|
|
220
|
+
|
|
221
|
+
# Verify health
|
|
222
|
+
health_status = (
|
|
223
|
+
"healthy"
|
|
224
|
+
if service_url and self.verify_health(service_url)
|
|
225
|
+
else "unhealthy"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
deployment_time = time.time() - start_time
|
|
229
|
+
|
|
230
|
+
return DeploymentResult(
|
|
231
|
+
success=True,
|
|
232
|
+
service_url=service_url,
|
|
233
|
+
service_id=service.name,
|
|
234
|
+
revision=service.latest_ready_revision,
|
|
235
|
+
status="running",
|
|
236
|
+
health_status=health_status,
|
|
237
|
+
deployment_time=deployment_time,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
except Exception as e:
|
|
241
|
+
log.error(f"Deployment failed: {e}")
|
|
242
|
+
return DeploymentResult(
|
|
243
|
+
success=False,
|
|
244
|
+
error_message=str(e),
|
|
245
|
+
error_details={"exception_type": type(e).__name__},
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def destroy_service(
|
|
249
|
+
self,
|
|
250
|
+
service_name: str,
|
|
251
|
+
environment: str,
|
|
252
|
+
keep_secrets: bool = False,
|
|
253
|
+
) -> DeploymentResult:
|
|
254
|
+
"""Destroy the service and cleanup resources."""
|
|
255
|
+
full_service_name = self.get_service_key(service_name, environment)
|
|
256
|
+
service_path = f"{self.service_parent}/services/{full_service_name}"
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
# Delete service
|
|
260
|
+
self.run_client.delete_service(name=service_path)
|
|
261
|
+
log.info(f"Deleted Cloud Run service: {full_service_name}")
|
|
262
|
+
|
|
263
|
+
# Delete secrets if requested
|
|
264
|
+
if not keep_secrets:
|
|
265
|
+
self._delete_secrets(full_service_name)
|
|
266
|
+
|
|
267
|
+
return DeploymentResult(
|
|
268
|
+
success=True,
|
|
269
|
+
status="deleted",
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
except NotFound:
|
|
273
|
+
log.warning(f"Service {full_service_name} not found")
|
|
274
|
+
return DeploymentResult(
|
|
275
|
+
success=True,
|
|
276
|
+
status="not_found",
|
|
277
|
+
)
|
|
278
|
+
except Exception as e:
|
|
279
|
+
log.error(f"Failed to destroy service: {e}")
|
|
280
|
+
return DeploymentResult(
|
|
281
|
+
success=False,
|
|
282
|
+
error_message=str(e),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def get_service_status(
|
|
286
|
+
self,
|
|
287
|
+
service_name: str,
|
|
288
|
+
environment: str,
|
|
289
|
+
) -> DeploymentResult:
|
|
290
|
+
"""Get current service status and health."""
|
|
291
|
+
full_service_name = self.get_service_key(service_name, environment)
|
|
292
|
+
service_path = f"{self.service_parent}/services/{full_service_name}"
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
service = self.run_client.get_service(name=service_path)
|
|
296
|
+
|
|
297
|
+
# Check health
|
|
298
|
+
health_status = "unknown"
|
|
299
|
+
if service.uri:
|
|
300
|
+
health_status = (
|
|
301
|
+
"healthy" if self.verify_health(service.uri) else "unhealthy"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return DeploymentResult(
|
|
305
|
+
success=True,
|
|
306
|
+
service_url=service.uri,
|
|
307
|
+
service_id=service.name,
|
|
308
|
+
revision=service.latest_ready_revision,
|
|
309
|
+
status="running",
|
|
310
|
+
health_status=health_status,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
except NotFound:
|
|
314
|
+
return DeploymentResult(
|
|
315
|
+
success=False,
|
|
316
|
+
status="not_found",
|
|
317
|
+
error_message="Service not found",
|
|
318
|
+
)
|
|
319
|
+
except Exception as e:
|
|
320
|
+
return DeploymentResult(
|
|
321
|
+
success=False,
|
|
322
|
+
error_message=str(e),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
def verify_health(self, service_url: str, timeout: int = 60) -> bool:
|
|
326
|
+
"""Verify service health by checking the health endpoint."""
|
|
327
|
+
return self.verify_health_enhanced(service_url, timeout=timeout)
|
|
328
|
+
|
|
329
|
+
def check_prerequisites(self) -> List[str]:
|
|
330
|
+
"""Check prerequisites and return list of missing requirements."""
|
|
331
|
+
errors = []
|
|
332
|
+
|
|
333
|
+
# Check gcloud CLI
|
|
334
|
+
try:
|
|
335
|
+
result = subprocess.run(
|
|
336
|
+
["gcloud", "version"], capture_output=True, text=True, check=True
|
|
337
|
+
)
|
|
338
|
+
log.debug(f"gcloud version: {result.stdout}")
|
|
339
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
340
|
+
errors.append("gcloud CLI not found or not working")
|
|
341
|
+
|
|
342
|
+
# Check authentication
|
|
343
|
+
try:
|
|
344
|
+
result = subprocess.run(
|
|
345
|
+
[
|
|
346
|
+
"gcloud",
|
|
347
|
+
"auth",
|
|
348
|
+
"list",
|
|
349
|
+
"--filter=status:ACTIVE",
|
|
350
|
+
"--format=value(account)",
|
|
351
|
+
],
|
|
352
|
+
capture_output=True,
|
|
353
|
+
text=True,
|
|
354
|
+
check=True,
|
|
355
|
+
)
|
|
356
|
+
if not result.stdout.strip():
|
|
357
|
+
errors.append("No active gcloud authentication found")
|
|
358
|
+
except subprocess.CalledProcessError:
|
|
359
|
+
errors.append("gcloud authentication check failed")
|
|
360
|
+
|
|
361
|
+
# Check project
|
|
362
|
+
try:
|
|
363
|
+
result = subprocess.run(
|
|
364
|
+
["gcloud", "config", "get-value", "project"],
|
|
365
|
+
capture_output=True,
|
|
366
|
+
text=True,
|
|
367
|
+
check=True,
|
|
368
|
+
)
|
|
369
|
+
if not result.stdout.strip():
|
|
370
|
+
errors.append("No gcloud project configured")
|
|
371
|
+
except subprocess.CalledProcessError:
|
|
372
|
+
errors.append("gcloud project configuration check failed")
|
|
373
|
+
|
|
374
|
+
# Check APIs
|
|
375
|
+
required_apis = [
|
|
376
|
+
"run.googleapis.com",
|
|
377
|
+
"secretmanager.googleapis.com",
|
|
378
|
+
"artifactregistry.googleapis.com",
|
|
379
|
+
]
|
|
380
|
+
|
|
381
|
+
for api in required_apis:
|
|
382
|
+
try:
|
|
383
|
+
result = subprocess.run(
|
|
384
|
+
["gcloud", "services", "list", "--enabled", f"--filter=name:{api}"],
|
|
385
|
+
capture_output=True,
|
|
386
|
+
text=True,
|
|
387
|
+
check=True,
|
|
388
|
+
)
|
|
389
|
+
if api not in result.stdout:
|
|
390
|
+
errors.append(f"API {api} not enabled")
|
|
391
|
+
except subprocess.CalledProcessError:
|
|
392
|
+
errors.append(f"Failed to check API {api}")
|
|
393
|
+
|
|
394
|
+
return errors
|
|
395
|
+
|
|
396
|
+
def _create_or_update_secrets(self, secrets: Dict[str, str]) -> None:
|
|
397
|
+
"""Create or update secrets in Secret Manager."""
|
|
398
|
+
for secret_name, secret_value in secrets.items():
|
|
399
|
+
secret_path = f"projects/{self.project_id}/secrets/{secret_name}"
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
# Check if secret exists
|
|
403
|
+
self.secret_client.get_secret(name=secret_path)
|
|
404
|
+
|
|
405
|
+
# Update existing secret
|
|
406
|
+
parent = f"projects/{self.project_id}"
|
|
407
|
+
self.secret_client.add_secret_version(
|
|
408
|
+
request={
|
|
409
|
+
"parent": secret_path,
|
|
410
|
+
"payload": {"data": secret_value.encode("utf-8")},
|
|
411
|
+
}
|
|
412
|
+
)
|
|
413
|
+
log.info(f"Updated secret {secret_name}")
|
|
414
|
+
|
|
415
|
+
except NotFound:
|
|
416
|
+
# Create new secret
|
|
417
|
+
parent = f"projects/{self.project_id}"
|
|
418
|
+
secret = self.secret_client.create_secret(
|
|
419
|
+
request={
|
|
420
|
+
"parent": parent,
|
|
421
|
+
"secret_id": secret_name,
|
|
422
|
+
"secret": {"replication": {"automatic": {}}},
|
|
423
|
+
}
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Add secret version
|
|
427
|
+
self.secret_client.add_secret_version(
|
|
428
|
+
request={
|
|
429
|
+
"parent": secret.name,
|
|
430
|
+
"payload": {"data": secret_value.encode("utf-8")},
|
|
431
|
+
}
|
|
432
|
+
)
|
|
433
|
+
log.info(f"Created secret {secret_name}")
|
|
434
|
+
|
|
435
|
+
def _create_or_update_service(
|
|
436
|
+
self,
|
|
437
|
+
service_name: str,
|
|
438
|
+
image_tag: str,
|
|
439
|
+
port: int,
|
|
440
|
+
env_vars: Dict[str, str],
|
|
441
|
+
secrets: Dict[str, str],
|
|
442
|
+
) -> Any:
|
|
443
|
+
"""Create or update Cloud Run service."""
|
|
444
|
+
service_path = f"{self.service_parent}/services/{service_name}"
|
|
445
|
+
|
|
446
|
+
# Build environment variables
|
|
447
|
+
env_vars_list = []
|
|
448
|
+
for key, value in env_vars.items():
|
|
449
|
+
env_vars_list.append({"name": key, "value": value})
|
|
450
|
+
|
|
451
|
+
# Build secret references
|
|
452
|
+
secret_refs = []
|
|
453
|
+
for secret_name in secrets.keys():
|
|
454
|
+
secret_refs.append(
|
|
455
|
+
{
|
|
456
|
+
"name": secret_name,
|
|
457
|
+
"value_source": {
|
|
458
|
+
"secret_key_ref": {
|
|
459
|
+
"secret": f"projects/{self.project_id}/secrets/{secret_name}",
|
|
460
|
+
"version": "latest",
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
}
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
# Service configuration
|
|
467
|
+
service_config = {
|
|
468
|
+
"template": {
|
|
469
|
+
"containers": [
|
|
470
|
+
{
|
|
471
|
+
"image": image_tag,
|
|
472
|
+
"ports": [{"container_port": port}],
|
|
473
|
+
"env": env_vars_list + secret_refs,
|
|
474
|
+
"resources": {
|
|
475
|
+
"limits": {"cpu": "1", "memory": "512Mi"},
|
|
476
|
+
},
|
|
477
|
+
}
|
|
478
|
+
],
|
|
479
|
+
"scaling": {
|
|
480
|
+
"min_instance_count": 1,
|
|
481
|
+
"max_instance_count": 10,
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
"traffic": [
|
|
485
|
+
{"percent": 100, "type": "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"}
|
|
486
|
+
],
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
try:
|
|
490
|
+
# Try to update existing service
|
|
491
|
+
service = self.run_client.update_service(
|
|
492
|
+
request={"service": service_config, "name": service_path}
|
|
493
|
+
)
|
|
494
|
+
log.info(f"Updated Cloud Run service: {service_name}")
|
|
495
|
+
return service
|
|
496
|
+
|
|
497
|
+
except NotFound:
|
|
498
|
+
# Create new service
|
|
499
|
+
service_config["name"] = service_path
|
|
500
|
+
service = self.run_client.create_service(
|
|
501
|
+
request={"parent": self.service_parent, "service": service_config}
|
|
502
|
+
)
|
|
503
|
+
log.info(f"Created Cloud Run service: {service_name}")
|
|
504
|
+
return service
|
|
505
|
+
|
|
506
|
+
def _wait_for_service_ready(self, service_path: str, timeout: int) -> Optional[str]:
|
|
507
|
+
"""Wait for service to be ready and return URL."""
|
|
508
|
+
start_time = time.time()
|
|
509
|
+
|
|
510
|
+
while time.time() - start_time < timeout:
|
|
511
|
+
try:
|
|
512
|
+
service = self.run_client.get_service(name=service_path)
|
|
513
|
+
if service.uri:
|
|
514
|
+
log.info(f"Service ready at: {service.uri}")
|
|
515
|
+
return service.uri
|
|
516
|
+
except Exception as e:
|
|
517
|
+
log.debug(f"Waiting for service to be ready: {e}")
|
|
518
|
+
|
|
519
|
+
time.sleep(5)
|
|
520
|
+
|
|
521
|
+
raise TimeoutError(f"Service did not become ready within {timeout} seconds")
|
|
522
|
+
|
|
523
|
+
def _set_public_url(self, service_path: str, public_url: str) -> None:
|
|
524
|
+
"""Set SUPERVAIZER_PUBLIC_URL environment variable."""
|
|
525
|
+
try:
|
|
526
|
+
service = self.run_client.get_service(name=service_path)
|
|
527
|
+
|
|
528
|
+
# Update environment variables
|
|
529
|
+
env_vars = []
|
|
530
|
+
for env_var in service.template.containers[0].env:
|
|
531
|
+
if env_var.name != "SUPERVAIZER_PUBLIC_URL":
|
|
532
|
+
env_vars.append(env_var)
|
|
533
|
+
|
|
534
|
+
# Add the public URL
|
|
535
|
+
env_vars.append(
|
|
536
|
+
{
|
|
537
|
+
"name": "SUPERVAIZER_PUBLIC_URL",
|
|
538
|
+
"value": public_url,
|
|
539
|
+
}
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
# Update service
|
|
543
|
+
service.template.containers[0].env = env_vars
|
|
544
|
+
self.run_client.update_service(
|
|
545
|
+
request={"service": service, "name": service_path}
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
log.info(f"Set SUPERVAIZER_PUBLIC_URL to {public_url}")
|
|
549
|
+
|
|
550
|
+
except Exception as e:
|
|
551
|
+
log.error(f"Failed to set SUPERVAIZER_PUBLIC_URL: {e}")
|
|
552
|
+
|
|
553
|
+
def _delete_secrets(self, service_name: str) -> None:
|
|
554
|
+
"""Delete secrets associated with the service."""
|
|
555
|
+
# This is a simplified implementation
|
|
556
|
+
# In practice, you might want to be more selective about which secrets to delete
|
|
557
|
+
common_secrets = [
|
|
558
|
+
f"{service_name}-api-key",
|
|
559
|
+
f"{service_name}-rsa-key",
|
|
560
|
+
]
|
|
561
|
+
|
|
562
|
+
for secret_name in common_secrets:
|
|
563
|
+
secret_path = f"projects/{self.project_id}/secrets/{secret_name}"
|
|
564
|
+
try:
|
|
565
|
+
self.secret_client.delete_secret(name=secret_path)
|
|
566
|
+
log.info(f"Deleted secret {secret_name}")
|
|
567
|
+
except NotFound:
|
|
568
|
+
pass # Secret doesn't exist, that's fine
|
|
569
|
+
except Exception as e:
|
|
570
|
+
log.warning(f"Failed to delete secret {secret_name}: {e}")
|