base-deployment-controller 0.1.0__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.
- base_deployment_controller/__init__.py +59 -0
- base_deployment_controller/builder.py +90 -0
- base_deployment_controller/models/__init__.py +33 -0
- base_deployment_controller/models/compose.py +11 -0
- base_deployment_controller/models/container.py +31 -0
- base_deployment_controller/models/deployment.py +57 -0
- base_deployment_controller/models/environment.py +42 -0
- base_deployment_controller/routers/__init__.py +7 -0
- base_deployment_controller/routers/container.py +398 -0
- base_deployment_controller/routers/deployment.py +281 -0
- base_deployment_controller/routers/environment.py +174 -0
- base_deployment_controller/services/__init__.py +5 -0
- base_deployment_controller/services/config.py +560 -0
- base_deployment_controller-0.1.0.dist-info/METADATA +184 -0
- base_deployment_controller-0.1.0.dist-info/RECORD +17 -0
- base_deployment_controller-0.1.0.dist-info/WHEEL +5 -0
- base_deployment_controller-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration service module.
|
|
3
|
+
Manages reading/writing .env and compose.yaml, validation and Docker client.
|
|
4
|
+
"""
|
|
5
|
+
from typing import Dict, Any, List, Optional
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import re
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
from dotenv import dotenv_values, set_key
|
|
13
|
+
from python_on_whales import DockerClient
|
|
14
|
+
|
|
15
|
+
from ..models.deployment import DeploymentStatus, DeploymentMetadata
|
|
16
|
+
from ..models.compose import ComposeActionResponse
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConfigService:
|
|
22
|
+
"""
|
|
23
|
+
Configuration and Docker access service.
|
|
24
|
+
|
|
25
|
+
Centralizes reading/writing of `.env` and `compose.yaml`, validation
|
|
26
|
+
of variables according to `x-env-vars`, and Docker client access.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
compose_file: Path to Docker Compose file (default "compose.yaml").
|
|
30
|
+
env_file: Path to environment variables file (default ".env").
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
compose_path: Path to compose.yaml file as `Path`.
|
|
34
|
+
env_path: Path to .env file as `Path`.
|
|
35
|
+
compose_schema: Parsed compose.yaml content as dictionary.
|
|
36
|
+
compose_services: Services section from compose.yaml.
|
|
37
|
+
env_to_services_map: Mapping of environment variables to services that use them.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, compose_file: str = "compose.yaml", env_file: str = ".env") -> None:
|
|
41
|
+
"""
|
|
42
|
+
Initialize the configuration service.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
compose_file: Path to Docker Compose file.
|
|
46
|
+
env_file: Path to environment variables file.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
FileNotFoundError: If compose file doesn't exist.
|
|
50
|
+
"""
|
|
51
|
+
self.compose_path = Path(compose_file)
|
|
52
|
+
self.env_path = Path(env_file)
|
|
53
|
+
self.compose_schema: Dict[str, Any] = self._load_compose_schema()
|
|
54
|
+
self.compose_services: Dict[str, Any] = self._load_compose_services()
|
|
55
|
+
self.env_to_services_map: Dict[str, List[str]] = self._build_env_to_services_map()
|
|
56
|
+
|
|
57
|
+
def _load_compose_schema(self) -> Dict[str, Any]:
|
|
58
|
+
"""
|
|
59
|
+
Load and parse the compose.yaml file.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Dict containing the parsed compose.yaml content.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
FileNotFoundError: If compose.yaml doesn't exist.
|
|
66
|
+
yaml.YAMLError: If YAML parsing fails.
|
|
67
|
+
"""
|
|
68
|
+
if not self.compose_path.exists():
|
|
69
|
+
raise FileNotFoundError(f"Compose file not found: {self.compose_path}")
|
|
70
|
+
with open(self.compose_path, "r") as f:
|
|
71
|
+
return yaml.safe_load(f)
|
|
72
|
+
|
|
73
|
+
def _load_compose_services(self) -> Dict[str, Any]:
|
|
74
|
+
"""
|
|
75
|
+
Load services section from compose.yaml.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Dictionary containing service definitions from compose.yaml.
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
compose = self.compose_schema
|
|
82
|
+
return compose.get("services", {})
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.warning(f"Failed to load services from compose.yaml: {e}")
|
|
85
|
+
return {}
|
|
86
|
+
|
|
87
|
+
# File Operations
|
|
88
|
+
def _build_env_to_services_map(self) -> Dict[str, List[str]]:
|
|
89
|
+
"""
|
|
90
|
+
Build a mapping of environment variables to services that use them.
|
|
91
|
+
|
|
92
|
+
Parses the compose.yaml to identify which services reference each environment
|
|
93
|
+
variable in their environment section or network configuration.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Dict mapping environment variable names to list of service names that use them.
|
|
97
|
+
"""
|
|
98
|
+
env_to_services: Dict[str, List[str]] = {}
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
services = self.compose_services
|
|
102
|
+
|
|
103
|
+
# Pattern to match ${VAR_NAME} in strings
|
|
104
|
+
env_var_pattern = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
|
|
105
|
+
|
|
106
|
+
for service_name, service_config in services.items():
|
|
107
|
+
# Check environment variables section
|
|
108
|
+
environment = service_config.get("environment", [])
|
|
109
|
+
if isinstance(environment, dict):
|
|
110
|
+
# Dict format: key: value
|
|
111
|
+
for key, value in environment.items():
|
|
112
|
+
if isinstance(value, str):
|
|
113
|
+
matches = env_var_pattern.findall(value)
|
|
114
|
+
for var_name in matches:
|
|
115
|
+
if var_name not in env_to_services:
|
|
116
|
+
env_to_services[var_name] = []
|
|
117
|
+
if service_name not in env_to_services[var_name]:
|
|
118
|
+
env_to_services[var_name].append(service_name)
|
|
119
|
+
elif isinstance(environment, list):
|
|
120
|
+
# List format: ["KEY=value", ...]
|
|
121
|
+
for entry in environment:
|
|
122
|
+
if isinstance(entry, str):
|
|
123
|
+
matches = env_var_pattern.findall(entry)
|
|
124
|
+
for var_name in matches:
|
|
125
|
+
if var_name not in env_to_services:
|
|
126
|
+
env_to_services[var_name] = []
|
|
127
|
+
if service_name not in env_to_services[var_name]:
|
|
128
|
+
env_to_services[var_name].append(service_name)
|
|
129
|
+
|
|
130
|
+
# Check network configuration (IPv4 addresses often use env vars)
|
|
131
|
+
networks = service_config.get("networks", {})
|
|
132
|
+
if isinstance(networks, dict):
|
|
133
|
+
for network_name, network_config in networks.items():
|
|
134
|
+
if isinstance(network_config, dict):
|
|
135
|
+
ipv4_addr = network_config.get("ipv4_address", "")
|
|
136
|
+
if isinstance(ipv4_addr, str):
|
|
137
|
+
matches = env_var_pattern.findall(ipv4_addr)
|
|
138
|
+
for var_name in matches:
|
|
139
|
+
if var_name not in env_to_services:
|
|
140
|
+
env_to_services[var_name] = []
|
|
141
|
+
if service_name not in env_to_services[var_name]:
|
|
142
|
+
env_to_services[var_name].append(service_name)
|
|
143
|
+
|
|
144
|
+
# Check ports configuration
|
|
145
|
+
ports = service_config.get("ports", [])
|
|
146
|
+
for port_mapping in ports:
|
|
147
|
+
if isinstance(port_mapping, str):
|
|
148
|
+
matches = env_var_pattern.findall(port_mapping)
|
|
149
|
+
for var_name in matches:
|
|
150
|
+
if var_name not in env_to_services:
|
|
151
|
+
env_to_services[var_name] = []
|
|
152
|
+
if service_name not in env_to_services[var_name]:
|
|
153
|
+
env_to_services[var_name].append(service_name)
|
|
154
|
+
|
|
155
|
+
except Exception as e:
|
|
156
|
+
# If we can't build the map, log the error but don't fail initialization
|
|
157
|
+
logger.warning(f"Failed to build env-to-services map: {e}")
|
|
158
|
+
return {}
|
|
159
|
+
|
|
160
|
+
return env_to_services
|
|
161
|
+
|
|
162
|
+
def get_env_vars_schema(self) -> Dict[str, Dict[str, Any]]:
|
|
163
|
+
"""
|
|
164
|
+
Extract the x-env-vars schema from compose.yaml.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Dict mapping variable names to their metadata.
|
|
168
|
+
"""
|
|
169
|
+
compose = self.compose_schema
|
|
170
|
+
return compose.get("x-env-vars", {})
|
|
171
|
+
|
|
172
|
+
def load_env_values(self) -> Dict[str, Optional[str]]:
|
|
173
|
+
"""
|
|
174
|
+
Load current values from .env file.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Dict mapping variable names to their current values.
|
|
178
|
+
"""
|
|
179
|
+
if not self.env_path.exists():
|
|
180
|
+
return {}
|
|
181
|
+
return dict(dotenv_values(self.env_path))
|
|
182
|
+
|
|
183
|
+
def update_env_file(self, updates: Dict[str, str]) -> None:
|
|
184
|
+
"""
|
|
185
|
+
Update variables in .env file atomically.
|
|
186
|
+
Only modifies specified variables, preserves others and comments.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
updates: Dict mapping variable names to new values.
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
IOError: If file operations fail.
|
|
193
|
+
"""
|
|
194
|
+
if not self.env_path.exists():
|
|
195
|
+
self.env_path.touch()
|
|
196
|
+
for key, value in updates.items():
|
|
197
|
+
set_key(self.env_path, key, value)
|
|
198
|
+
|
|
199
|
+
# Validation
|
|
200
|
+
def parse_type_constraint(self, type_str: str) -> Dict[str, Any]:
|
|
201
|
+
"""
|
|
202
|
+
Parse the type constraint string from x-env-vars.
|
|
203
|
+
|
|
204
|
+
Format examples:
|
|
205
|
+
- "string:0;^\\d{3}$" -> string with regex pattern (0 = show in UI, 1 = do not show)
|
|
206
|
+
- "integer:0;2048" -> integer with min/max bounds
|
|
207
|
+
- "enum:tun,tap" -> enum with allowed values
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
type_str: Type constraint string.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Dict with parsed constraint information.
|
|
214
|
+
"""
|
|
215
|
+
parts = type_str.split(":", 1)
|
|
216
|
+
type_name = parts[0]
|
|
217
|
+
result: Dict[str, Any] = {"type": type_name}
|
|
218
|
+
if len(parts) > 1:
|
|
219
|
+
constraint = parts[1]
|
|
220
|
+
if type_name == "string":
|
|
221
|
+
cp = constraint.split(";", 1)
|
|
222
|
+
if len(cp) == 2:
|
|
223
|
+
result["hide"] = bool(int(cp[0]))
|
|
224
|
+
result["pattern"] = cp[1]
|
|
225
|
+
elif type_name == "integer":
|
|
226
|
+
cp = constraint.split(";")
|
|
227
|
+
if len(cp) == 2:
|
|
228
|
+
result["min"] = int(cp[0])
|
|
229
|
+
result["max"] = int(cp[1])
|
|
230
|
+
elif type_name == "enum":
|
|
231
|
+
result["values"] = constraint.split(",")
|
|
232
|
+
return result
|
|
233
|
+
|
|
234
|
+
def validate_variable_value(self, name: str, value: str, type_str: str) -> None:
|
|
235
|
+
"""
|
|
236
|
+
Validate a variable value against its type constraint.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
name: Variable name.
|
|
240
|
+
value: Value to validate.
|
|
241
|
+
type_str: Type constraint string from x-env-vars.
|
|
242
|
+
|
|
243
|
+
Raises:
|
|
244
|
+
ValueError: If validation fails.
|
|
245
|
+
"""
|
|
246
|
+
constraint = self.parse_type_constraint(type_str)
|
|
247
|
+
if constraint["type"] == "string":
|
|
248
|
+
pattern = constraint.get("pattern")
|
|
249
|
+
if pattern and not re.match(pattern, value):
|
|
250
|
+
raise ValueError(
|
|
251
|
+
f"Variable '{name}' value '{value}' doesn't match pattern {pattern}"
|
|
252
|
+
)
|
|
253
|
+
elif constraint["type"] == "integer":
|
|
254
|
+
try:
|
|
255
|
+
int_value = int(value)
|
|
256
|
+
except ValueError:
|
|
257
|
+
raise ValueError(f"Variable '{name}' must be an integer, got '{value}'")
|
|
258
|
+
if "min" in constraint and int_value < constraint["min"]:
|
|
259
|
+
raise ValueError(
|
|
260
|
+
f"Variable '{name}' value {int_value} is below minimum {constraint['min']}"
|
|
261
|
+
)
|
|
262
|
+
if "max" in constraint and int_value > constraint["max"]:
|
|
263
|
+
raise ValueError(
|
|
264
|
+
f"Variable '{name}' value {int_value} exceeds maximum {constraint['max']}"
|
|
265
|
+
)
|
|
266
|
+
elif constraint["type"] == "enum":
|
|
267
|
+
values = constraint.get("values", [])
|
|
268
|
+
if values and value not in values:
|
|
269
|
+
raise ValueError(
|
|
270
|
+
f"Variable '{name}' value '{value}' not in allowed values: {values}"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def get_service_dependencies(self, service_name: str) -> List[str]:
|
|
274
|
+
"""
|
|
275
|
+
Extract service dependencies from compose.yaml.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
service_name: Name of the service.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
List of service names this service depends on.
|
|
282
|
+
"""
|
|
283
|
+
service = self.compose_services.get(service_name, {})
|
|
284
|
+
depends_on = service.get("depends_on", [])
|
|
285
|
+
if isinstance(depends_on, dict):
|
|
286
|
+
return list(depends_on.keys())
|
|
287
|
+
return depends_on
|
|
288
|
+
|
|
289
|
+
def get_container_name_by_service(self, service_name: str) -> str:
|
|
290
|
+
"""
|
|
291
|
+
Get the container name for a given service from compose.yaml.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
service_name: Name of the service.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Container name if specified, else service name.
|
|
298
|
+
"""
|
|
299
|
+
service = self.compose_services.get(service_name, {})
|
|
300
|
+
return service.get("container_name", "")
|
|
301
|
+
|
|
302
|
+
# Docker
|
|
303
|
+
def get_docker_client(self) -> DockerClient:
|
|
304
|
+
"""
|
|
305
|
+
Get Docker client instance.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Docker client connected to local daemon.
|
|
309
|
+
"""
|
|
310
|
+
docker = DockerClient(compose_files=[str(self.compose_path)])
|
|
311
|
+
return docker
|
|
312
|
+
|
|
313
|
+
def get_affected_services(self, changed_vars: List[str]) -> List[str]:
|
|
314
|
+
"""
|
|
315
|
+
Get list of services affected by changes to specific environment variables.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
changed_vars: List of environment variable names that changed.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
List of unique service names that use any of the changed variables.
|
|
322
|
+
"""
|
|
323
|
+
affected_services = set()
|
|
324
|
+
for var_name in changed_vars:
|
|
325
|
+
services = self.env_to_services_map.get(var_name, [])
|
|
326
|
+
affected_services.update(services)
|
|
327
|
+
return list(affected_services)
|
|
328
|
+
|
|
329
|
+
def restart_services(self, service_names: List[str]) -> Dict[str, bool]:
|
|
330
|
+
"""
|
|
331
|
+
Restart specified Docker services/containers.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
service_names: List of service names from compose.yaml to restart.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Dict mapping service names to restart success status (True/False).
|
|
338
|
+
"""
|
|
339
|
+
results: Dict[str, bool] = {}
|
|
340
|
+
|
|
341
|
+
if not service_names:
|
|
342
|
+
return results
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
client = self.get_docker_client()
|
|
346
|
+
|
|
347
|
+
for service_name in service_names:
|
|
348
|
+
# Get container name from service
|
|
349
|
+
container_name = self.get_container_name_by_service(service_name)
|
|
350
|
+
if not container_name:
|
|
351
|
+
results[service_name] = False
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
if client.container.exists(container_name):
|
|
356
|
+
# Only restart if container exists
|
|
357
|
+
container_inspect = client.container.inspect(container_name)
|
|
358
|
+
if container_inspect.state.status == "running":
|
|
359
|
+
client.container.restart(container_name)
|
|
360
|
+
results[service_name] = True
|
|
361
|
+
else:
|
|
362
|
+
# Container exists but not running, don't restart
|
|
363
|
+
results[service_name] = False
|
|
364
|
+
else:
|
|
365
|
+
# Container doesn't exist, can't restart
|
|
366
|
+
results[service_name] = False
|
|
367
|
+
except Exception as e:
|
|
368
|
+
logger.error(f"Error restarting service {service_name}: {e}")
|
|
369
|
+
results[service_name] = False
|
|
370
|
+
|
|
371
|
+
except Exception as e:
|
|
372
|
+
logger.error(f"Error in restart_services: {e}")
|
|
373
|
+
|
|
374
|
+
return results
|
|
375
|
+
|
|
376
|
+
# Docker Compose Operations
|
|
377
|
+
def get_deployment_metadata(self) -> DeploymentMetadata:
|
|
378
|
+
"""
|
|
379
|
+
Extract metadata from x-metadata section in compose.yaml.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Dict with deployment metadata (id, name, description, version, author, changelog, documentation_url).
|
|
383
|
+
"""
|
|
384
|
+
compose = self.compose_schema
|
|
385
|
+
metadata = compose.get("x-metadata", {})
|
|
386
|
+
return DeploymentMetadata(
|
|
387
|
+
id=metadata.get("id", "unknown"),
|
|
388
|
+
name=metadata.get("name", "Unknown"),
|
|
389
|
+
description=metadata.get("description", ""),
|
|
390
|
+
version=metadata.get("version", "1.0"),
|
|
391
|
+
author=metadata.get("author", ""),
|
|
392
|
+
changelog=metadata.get("changelog", ""),
|
|
393
|
+
documentation_url=metadata.get("documentation_url", ""),
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
def get_deployment_status(self) -> DeploymentStatus:
|
|
397
|
+
"""
|
|
398
|
+
Get current deployment status by checking if services are running.
|
|
399
|
+
|
|
400
|
+
Analyzes Docker containers and determines overall deployment state:
|
|
401
|
+
- "running": All or most critical services are running
|
|
402
|
+
- "partially_running": Some services are running but others are stopped
|
|
403
|
+
- "stopped": All services are stopped
|
|
404
|
+
- "unknown": Unable to determine overall state
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Dict with current_state, desired_state, transitioning, and last_state_change.
|
|
408
|
+
"""
|
|
409
|
+
services = self.compose_services
|
|
410
|
+
if not services:
|
|
411
|
+
return DeploymentStatus.UNKNOWN
|
|
412
|
+
|
|
413
|
+
client = self.get_docker_client()
|
|
414
|
+
running = 0
|
|
415
|
+
total = 0
|
|
416
|
+
|
|
417
|
+
for service_name, service_config in services.items():
|
|
418
|
+
container_name = service_config.get("container_name", service_name)
|
|
419
|
+
total += 1
|
|
420
|
+
if client.container.exists(container_name):
|
|
421
|
+
container_inspect = client.container.inspect(container_name)
|
|
422
|
+
if container_inspect.state.status == "running":
|
|
423
|
+
running += 1
|
|
424
|
+
|
|
425
|
+
if running == 0:
|
|
426
|
+
return DeploymentStatus.STOPPED
|
|
427
|
+
elif running == total:
|
|
428
|
+
return DeploymentStatus.RUNNING
|
|
429
|
+
else:
|
|
430
|
+
return DeploymentStatus.PARTIALLY_RUNNING
|
|
431
|
+
|
|
432
|
+
def docker_compose_up(self) -> ComposeActionResponse:
|
|
433
|
+
"""
|
|
434
|
+
Execute docker compose up and return the result.
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
ComposeActionResponse with success status and message.
|
|
438
|
+
"""
|
|
439
|
+
try:
|
|
440
|
+
client = self.get_docker_client()
|
|
441
|
+
client.compose.up(detach=True)
|
|
442
|
+
return ComposeActionResponse(
|
|
443
|
+
success=True,
|
|
444
|
+
message="Deployment started successfully",
|
|
445
|
+
)
|
|
446
|
+
except Exception as e:
|
|
447
|
+
logger.error(f"Failed to start deployment: {e}")
|
|
448
|
+
return ComposeActionResponse(
|
|
449
|
+
success=False,
|
|
450
|
+
message=str(e),
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
def docker_compose_stop(self) -> ComposeActionResponse:
|
|
454
|
+
"""
|
|
455
|
+
Execute docker compose stop and return the result.
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
ComposeActionResponse with success status and message.
|
|
459
|
+
"""
|
|
460
|
+
try:
|
|
461
|
+
client = self.get_docker_client()
|
|
462
|
+
client.compose.stop()
|
|
463
|
+
return ComposeActionResponse(
|
|
464
|
+
success=True,
|
|
465
|
+
message="Deployment stopped successfully",
|
|
466
|
+
)
|
|
467
|
+
except Exception as e:
|
|
468
|
+
logger.error(f"Failed to stop deployment: {e}")
|
|
469
|
+
return ComposeActionResponse(
|
|
470
|
+
success=False,
|
|
471
|
+
message=str(e),
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
def docker_compose_down(self) -> ComposeActionResponse:
|
|
475
|
+
"""
|
|
476
|
+
Execute docker compose down and return the result.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
ComposeActionResponse with success status and message
|
|
480
|
+
"""
|
|
481
|
+
try:
|
|
482
|
+
client = self.get_docker_client()
|
|
483
|
+
client.compose.down(volumes=True)
|
|
484
|
+
return ComposeActionResponse(
|
|
485
|
+
success=True,
|
|
486
|
+
message="Deployment downed successfully"
|
|
487
|
+
)
|
|
488
|
+
except Exception as e:
|
|
489
|
+
logger.error(f"Failed to down deployment: {e}")
|
|
490
|
+
return ComposeActionResponse(
|
|
491
|
+
success=False,
|
|
492
|
+
message=str(e)
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
def docker_compose_restart(self) -> ComposeActionResponse:
|
|
496
|
+
"""
|
|
497
|
+
Execute docker compose stop then up and return the result.
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
ComposeActionResponse with success status and message.
|
|
501
|
+
"""
|
|
502
|
+
try:
|
|
503
|
+
down_result = self.docker_compose_stop()
|
|
504
|
+
if not down_result.success:
|
|
505
|
+
return ComposeActionResponse(
|
|
506
|
+
success=False,
|
|
507
|
+
message=down_result.message,
|
|
508
|
+
)
|
|
509
|
+
up_result = self.docker_compose_up()
|
|
510
|
+
return ComposeActionResponse(
|
|
511
|
+
success=up_result.success,
|
|
512
|
+
message=up_result.message,
|
|
513
|
+
)
|
|
514
|
+
except Exception as e:
|
|
515
|
+
logger.error(f"Failed to restart deployment: {e}")
|
|
516
|
+
return ComposeActionResponse(
|
|
517
|
+
success=False,
|
|
518
|
+
message=str(e),
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
def wait_for_containers(self, service_names: List[str], timeout: int = 60) -> Dict[str, bool]:
|
|
522
|
+
"""
|
|
523
|
+
Wait for specified containers to become running.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
service_names: List of service names to wait for.
|
|
527
|
+
timeout: Timeout in seconds.
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
Dict mapping service names to True if running, False otherwise.
|
|
531
|
+
"""
|
|
532
|
+
start_time = time.time()
|
|
533
|
+
results: Dict[str, bool] = {}
|
|
534
|
+
|
|
535
|
+
while time.time() - start_time < timeout:
|
|
536
|
+
all_running = True
|
|
537
|
+
for service_name in service_names:
|
|
538
|
+
container_name = self.get_container_name_by_service(service_name)
|
|
539
|
+
if not container_name:
|
|
540
|
+
results[service_name] = False
|
|
541
|
+
all_running = False
|
|
542
|
+
continue
|
|
543
|
+
client = self.get_docker_client()
|
|
544
|
+
if not client.container.exists(container_name):
|
|
545
|
+
results[service_name] = False
|
|
546
|
+
all_running = False
|
|
547
|
+
continue
|
|
548
|
+
container_inspect = client.container.inspect(container_name)
|
|
549
|
+
if container_inspect.state.status != "running":
|
|
550
|
+
results[service_name] = False
|
|
551
|
+
all_running = False
|
|
552
|
+
continue
|
|
553
|
+
results[service_name] = True
|
|
554
|
+
|
|
555
|
+
if all_running:
|
|
556
|
+
return results
|
|
557
|
+
|
|
558
|
+
time.sleep(2)
|
|
559
|
+
|
|
560
|
+
return results
|