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,398 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Container management routes implemented with a class and dependency injection.
|
|
3
|
+
"""
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
|
|
9
|
+
from python_on_whales.exceptions import DockerException
|
|
10
|
+
|
|
11
|
+
from ..models.container import (
|
|
12
|
+
ContainerInfo,
|
|
13
|
+
ContainersInfoResponse,
|
|
14
|
+
ContainerControlResponse,
|
|
15
|
+
)
|
|
16
|
+
from ..services.config import ConfigService
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ContainerRoutes:
|
|
22
|
+
"""
|
|
23
|
+
Docker containers router built with dependency injection.
|
|
24
|
+
|
|
25
|
+
Provides endpoints for retrieving container status, controlling containers,
|
|
26
|
+
and streaming container logs via WebSocket.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
config: Instance of `ConfigService` for Compose and Docker access.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
config: Injected configuration service.
|
|
33
|
+
router: Instance of `APIRouter` with `/containers` endpoints.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, config: ConfigService) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Initialize container routes.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
config: Configuration service instance for dependency injection.
|
|
42
|
+
"""
|
|
43
|
+
self.config = config
|
|
44
|
+
self.router = self._build_router()
|
|
45
|
+
|
|
46
|
+
def _build_router(self) -> APIRouter:
|
|
47
|
+
"""
|
|
48
|
+
Build and configure the router with container management endpoints.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
APIRouter configured with GET and POST handlers for /containers.
|
|
52
|
+
"""
|
|
53
|
+
router = APIRouter(prefix="/containers", tags=["Containers"])
|
|
54
|
+
# GET /containers - list all containers
|
|
55
|
+
router.add_api_route(
|
|
56
|
+
"",
|
|
57
|
+
self.get_containers,
|
|
58
|
+
methods=["GET"],
|
|
59
|
+
response_model=ContainersInfoResponse,
|
|
60
|
+
)
|
|
61
|
+
# GET /containers/<container_name> - get specific container info
|
|
62
|
+
router.add_api_route(
|
|
63
|
+
"/{container_name}",
|
|
64
|
+
self.get_container,
|
|
65
|
+
methods=["GET"],
|
|
66
|
+
response_model=ContainerInfo,
|
|
67
|
+
)
|
|
68
|
+
# POST /containers/<container_name>/start - start container
|
|
69
|
+
router.add_api_route(
|
|
70
|
+
"/{container_name}/start",
|
|
71
|
+
self.container_start,
|
|
72
|
+
methods=["POST"],
|
|
73
|
+
response_model=ContainerControlResponse,
|
|
74
|
+
)
|
|
75
|
+
# POST /containers/<container_name>/stop - stop container
|
|
76
|
+
router.add_api_route(
|
|
77
|
+
"/{container_name}/stop",
|
|
78
|
+
self.container_stop,
|
|
79
|
+
methods=["POST"],
|
|
80
|
+
response_model=ContainerControlResponse,
|
|
81
|
+
)
|
|
82
|
+
# POST /containers/<container_name>/restart - restart container
|
|
83
|
+
router.add_api_route(
|
|
84
|
+
"/{container_name}/restart",
|
|
85
|
+
self.container_restart,
|
|
86
|
+
methods=["POST"],
|
|
87
|
+
response_model=ContainerControlResponse,
|
|
88
|
+
)
|
|
89
|
+
# WebSocket /containers/<container_name>/logs - stream container logs
|
|
90
|
+
router.websocket("/{container_name}/logs")(self.container_logs)
|
|
91
|
+
return router
|
|
92
|
+
|
|
93
|
+
async def get_containers(self) -> ContainersInfoResponse:
|
|
94
|
+
"""
|
|
95
|
+
Get status of all containers defined in compose.yaml.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
ContainersInfoResponse with list of containers and their current states.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
HTTPException: If unable to retrieve container information from Docker.
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
logger.debug("Fetching container status from Docker")
|
|
105
|
+
services = self.config.compose_services
|
|
106
|
+
client = self.config.get_docker_client()
|
|
107
|
+
containers = []
|
|
108
|
+
for service_name, service_config in services.items():
|
|
109
|
+
container_name = service_config.get("container_name", service_name)
|
|
110
|
+
ports = service_config.get("expose", [])
|
|
111
|
+
try:
|
|
112
|
+
if not client.container.exists(container_name):
|
|
113
|
+
containers.append(
|
|
114
|
+
ContainerInfo(
|
|
115
|
+
name=service_name,
|
|
116
|
+
image=service_config.get("image", ""),
|
|
117
|
+
status="Container not created",
|
|
118
|
+
started_at=None,
|
|
119
|
+
ports=ports,
|
|
120
|
+
depends_on=self.config.get_service_dependencies(
|
|
121
|
+
service_name
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
continue
|
|
126
|
+
container_inspect = client.container.inspect(container_name)
|
|
127
|
+
status = container_inspect.state.status or "unknown"
|
|
128
|
+
started_at = container_inspect.state.started_at
|
|
129
|
+
except DockerException as e:
|
|
130
|
+
raise HTTPException(
|
|
131
|
+
status_code=500,
|
|
132
|
+
detail=f"Docker error while inspecting {container_name}: {e}",
|
|
133
|
+
)
|
|
134
|
+
containers.append(
|
|
135
|
+
ContainerInfo(
|
|
136
|
+
name=service_name,
|
|
137
|
+
image=service_config.get("image", ""),
|
|
138
|
+
status=status,
|
|
139
|
+
started_at=started_at,
|
|
140
|
+
ports=ports,
|
|
141
|
+
depends_on=self.config.get_service_dependencies(service_name),
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
logger.info(f"Successfully retrieved status for {len(containers)} containers")
|
|
145
|
+
return ContainersInfoResponse(containers=containers)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"Failed to get container status: {e}")
|
|
148
|
+
raise HTTPException(
|
|
149
|
+
status_code=500, detail=f"Failed to get container status: {e}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
async def get_container(self, container_name: str) -> ContainerInfo:
|
|
153
|
+
"""
|
|
154
|
+
Get information about a specific container.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
container_name: Name of the service/container from compose.yaml.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
ContainerInfo with container information.
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
HTTPException: If service/container not found.
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
logger.debug(f"Fetching info for container: {container_name}")
|
|
167
|
+
services = self.config.compose_services
|
|
168
|
+
if container_name not in services:
|
|
169
|
+
logger.warning(f"Service not found: {container_name}")
|
|
170
|
+
raise HTTPException(
|
|
171
|
+
status_code=404,
|
|
172
|
+
detail=f"Service '{container_name}' not found in compose.yaml",
|
|
173
|
+
)
|
|
174
|
+
service_config = services[container_name]
|
|
175
|
+
actual_container_name = service_config.get("container_name", container_name)
|
|
176
|
+
client = self.config.get_docker_client()
|
|
177
|
+
|
|
178
|
+
if not client.container.exists(actual_container_name):
|
|
179
|
+
logger.warning(f"Container not found: {actual_container_name}")
|
|
180
|
+
raise HTTPException(
|
|
181
|
+
status_code=404,
|
|
182
|
+
detail=f"Container '{actual_container_name}' not found in Docker",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
container_inspect = client.container.inspect(actual_container_name)
|
|
186
|
+
logger.info(f"Successfully retrieved info for {container_name}")
|
|
187
|
+
return ContainerInfo(
|
|
188
|
+
name=container_name,
|
|
189
|
+
image=service_config.get("image", ""),
|
|
190
|
+
status=container_inspect.state.status or "unknown",
|
|
191
|
+
started_at=container_inspect.state.started_at,
|
|
192
|
+
ports=service_config.get("expose", []),
|
|
193
|
+
depends_on=self.config.get_service_dependencies(container_name),
|
|
194
|
+
)
|
|
195
|
+
except HTTPException:
|
|
196
|
+
raise
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logger.error(f"Failed to get container info: {e}")
|
|
199
|
+
raise HTTPException(
|
|
200
|
+
status_code=500, detail=f"Failed to get container info: {e}"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
async def container_start(
|
|
204
|
+
self, container_name: str
|
|
205
|
+
) -> ContainerControlResponse:
|
|
206
|
+
"""
|
|
207
|
+
Start a container.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
container_name: Name of the service/container from compose.yaml.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
ContainerControlResponse with action result.
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
HTTPException: If service/container not found or action fails.
|
|
217
|
+
"""
|
|
218
|
+
return await self._control_container(container_name, "start")
|
|
219
|
+
|
|
220
|
+
async def container_stop(
|
|
221
|
+
self, container_name: str
|
|
222
|
+
) -> ContainerControlResponse:
|
|
223
|
+
"""
|
|
224
|
+
Stop a container.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
container_name: Name of the service/container from compose.yaml.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
ContainerControlResponse with action result.
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
HTTPException: If service/container not found or action fails.
|
|
234
|
+
"""
|
|
235
|
+
return await self._control_container(container_name, "stop")
|
|
236
|
+
|
|
237
|
+
async def container_restart(
|
|
238
|
+
self, container_name: str
|
|
239
|
+
) -> ContainerControlResponse:
|
|
240
|
+
"""
|
|
241
|
+
Restart a container.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
container_name: Name of the service/container from compose.yaml.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
ContainerControlResponse with action result.
|
|
248
|
+
|
|
249
|
+
Raises:
|
|
250
|
+
HTTPException: If service/container not found or action fails.
|
|
251
|
+
"""
|
|
252
|
+
return await self._control_container(container_name, "restart")
|
|
253
|
+
|
|
254
|
+
async def _control_container(
|
|
255
|
+
self, container_name: str, action: str
|
|
256
|
+
) -> ContainerControlResponse:
|
|
257
|
+
"""
|
|
258
|
+
Internal method to control a container (start/stop/restart).
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
container_name: Name of the service/container from compose.yaml.
|
|
262
|
+
action: Action to perform (start, stop, restart).
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
ContainerControlResponse with action result.
|
|
266
|
+
|
|
267
|
+
Raises:
|
|
268
|
+
HTTPException: If service/container not found or action fails.
|
|
269
|
+
"""
|
|
270
|
+
try:
|
|
271
|
+
logger.debug(f"Control request for container: {container_name}, action: {action}")
|
|
272
|
+
services = self.config.compose_services
|
|
273
|
+
if container_name not in services:
|
|
274
|
+
logger.warning(f"Service not found: {container_name}")
|
|
275
|
+
raise HTTPException(
|
|
276
|
+
status_code=404,
|
|
277
|
+
detail=f"Service '{container_name}' not found in compose.yaml",
|
|
278
|
+
)
|
|
279
|
+
service_config = services[container_name]
|
|
280
|
+
actual_container_name = service_config.get("container_name", container_name)
|
|
281
|
+
client = self.config.get_docker_client()
|
|
282
|
+
if not client.container.exists(actual_container_name):
|
|
283
|
+
logger.warning(f"Container not found: {actual_container_name}")
|
|
284
|
+
raise HTTPException(
|
|
285
|
+
status_code=404,
|
|
286
|
+
detail=f"Container '{actual_container_name}' not found in Docker",
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
logger.info(f"Executing {action} on container: {actual_container_name}")
|
|
290
|
+
try:
|
|
291
|
+
if action == "start":
|
|
292
|
+
client.container.start(actual_container_name)
|
|
293
|
+
elif action == "stop":
|
|
294
|
+
client.container.stop(actual_container_name)
|
|
295
|
+
elif action == "restart":
|
|
296
|
+
client.container.restart(actual_container_name)
|
|
297
|
+
logger.info(f"Container {actual_container_name} {action}ed successfully")
|
|
298
|
+
except DockerException as e:
|
|
299
|
+
logger.error(
|
|
300
|
+
f"Docker error while executing {action} on {actual_container_name}: {e}"
|
|
301
|
+
)
|
|
302
|
+
raise HTTPException(status_code=500, detail=f"Docker error: {e}")
|
|
303
|
+
return ContainerControlResponse(
|
|
304
|
+
success=True,
|
|
305
|
+
container=container_name,
|
|
306
|
+
action=action,
|
|
307
|
+
message=f"Container {action}ed successfully",
|
|
308
|
+
)
|
|
309
|
+
except HTTPException:
|
|
310
|
+
raise
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.error(f"Failed to {action} container {container_name}: {e}")
|
|
313
|
+
raise HTTPException(
|
|
314
|
+
status_code=500,
|
|
315
|
+
detail=f"Failed to {action} container: {e}",
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
async def container_logs(self, websocket: WebSocket, container_name: str) -> None:
|
|
319
|
+
"""
|
|
320
|
+
Stream container logs in real-time via WebSocket.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
websocket: WebSocket connection.
|
|
324
|
+
container_name: Name of the service/container from compose.yaml.
|
|
325
|
+
"""
|
|
326
|
+
logger.info(
|
|
327
|
+
f"WebSocket connection established for logs of container: {container_name}"
|
|
328
|
+
)
|
|
329
|
+
await websocket.accept()
|
|
330
|
+
try:
|
|
331
|
+
services = self.config.compose_services
|
|
332
|
+
if container_name not in services:
|
|
333
|
+
logger.warning(
|
|
334
|
+
f"WebSocket logs requested for non-existent service: {container_name}"
|
|
335
|
+
)
|
|
336
|
+
await websocket.send_json(
|
|
337
|
+
{"error": f"Service '{container_name}' not found in compose.yaml"}
|
|
338
|
+
)
|
|
339
|
+
await websocket.close()
|
|
340
|
+
return
|
|
341
|
+
actual_container_name = self.config.get_container_name_by_service(
|
|
342
|
+
container_name
|
|
343
|
+
)
|
|
344
|
+
client = self.config.get_docker_client()
|
|
345
|
+
if not client.container.exists(actual_container_name):
|
|
346
|
+
logger.warning(
|
|
347
|
+
f"WebSocket logs requested for non-existent container: {actual_container_name}"
|
|
348
|
+
)
|
|
349
|
+
await websocket.send_json(
|
|
350
|
+
{"error": f"Container '{actual_container_name}' not found in Docker"}
|
|
351
|
+
)
|
|
352
|
+
await websocket.close()
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
# Stream all logs (historical + follow new logs in real-time)
|
|
357
|
+
logger.debug(f"Starting log stream for {actual_container_name}")
|
|
358
|
+
|
|
359
|
+
# Get log generator - this call is fast and non-blocking
|
|
360
|
+
log_generator = client.container.logs(
|
|
361
|
+
actual_container_name,
|
|
362
|
+
follow=True,
|
|
363
|
+
stream=True,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
for log_line in log_generator:
|
|
367
|
+
# Decode bytes to text
|
|
368
|
+
text_line = (
|
|
369
|
+
log_line.decode("utf-8")
|
|
370
|
+
if isinstance(log_line, (bytes, bytearray))
|
|
371
|
+
else str(log_line)
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Send to WebSocket client
|
|
375
|
+
await websocket.send_text(text_line)
|
|
376
|
+
|
|
377
|
+
# Yield control to allow other coroutines to run
|
|
378
|
+
await asyncio.sleep(0)
|
|
379
|
+
|
|
380
|
+
except DockerException as e:
|
|
381
|
+
logger.error(f"Failed to stream logs from {actual_container_name}: {e}")
|
|
382
|
+
await websocket.send_json({"error": f"Failed to stream logs: {e}"})
|
|
383
|
+
except WebSocketDisconnect:
|
|
384
|
+
logger.debug(f"WebSocket client disconnected for {container_name}")
|
|
385
|
+
except Exception as e:
|
|
386
|
+
logger.error(
|
|
387
|
+
f"Unexpected error in WebSocket handler for {container_name}: {e}"
|
|
388
|
+
)
|
|
389
|
+
try:
|
|
390
|
+
await websocket.send_json({"error": f"Failed to stream logs: {e}"})
|
|
391
|
+
except:
|
|
392
|
+
pass
|
|
393
|
+
finally:
|
|
394
|
+
try:
|
|
395
|
+
await websocket.close()
|
|
396
|
+
except:
|
|
397
|
+
pass
|
|
398
|
+
logger.info(f"WebSocket connection closed for {container_name}")
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Deployment management routes implemented with a class and dependency injection.
|
|
3
|
+
Manages deployment-wide operations: status, up, stop, down, restart, ping.
|
|
4
|
+
"""
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, HTTPException
|
|
9
|
+
|
|
10
|
+
from ..models.deployment import DeploymentMetadata, DeploymentStatus, DeploymentInfoResponse, DeploymentPingResponse, DeploymentActionResponse
|
|
11
|
+
from ..models.compose import ComposeActionResponse
|
|
12
|
+
from ..models.environment import EnvVariable
|
|
13
|
+
from ..services.config import ConfigService
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DeploymentRoutes:
|
|
19
|
+
"""
|
|
20
|
+
Root deployment router built with dependency injection.
|
|
21
|
+
|
|
22
|
+
Manages deployment-wide operations at the root endpoint and control endpoints:
|
|
23
|
+
- GET / - Get deployment status with metadata and env-vars
|
|
24
|
+
- POST /up - Start deployment
|
|
25
|
+
- POST /stop - Stop deployment
|
|
26
|
+
- POST /down - Down deployment (stop and remove containers)
|
|
27
|
+
- POST /restart - Restart deployment
|
|
28
|
+
- GET /ping - Health check
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
config: Instance of `ConfigService` for Compose and Docker access.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
config: Injected configuration service.
|
|
35
|
+
router: Instance of `APIRouter` with root endpoints.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, config: ConfigService) -> None:
|
|
39
|
+
"""
|
|
40
|
+
Initialize deployment routes.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
config: Configuration service instance for dependency injection.
|
|
44
|
+
"""
|
|
45
|
+
self.config = config
|
|
46
|
+
self.router = self._build_router()
|
|
47
|
+
|
|
48
|
+
def _build_router(self) -> APIRouter:
|
|
49
|
+
"""
|
|
50
|
+
Build and configure the router with deployment endpoints at root level.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
APIRouter configured with GET and POST handlers.
|
|
54
|
+
"""
|
|
55
|
+
router = APIRouter(tags=["Deployment"])
|
|
56
|
+
# Note: These routes will be registered without prefix in main.py
|
|
57
|
+
# to mount them at root level: /, /ping, /up, /stop, /down, /restart
|
|
58
|
+
router.add_api_route(
|
|
59
|
+
"/",
|
|
60
|
+
self.get_deployment_info,
|
|
61
|
+
methods=["GET"],
|
|
62
|
+
)
|
|
63
|
+
router.add_api_route(
|
|
64
|
+
"/ping",
|
|
65
|
+
self.ping,
|
|
66
|
+
methods=["GET"],
|
|
67
|
+
)
|
|
68
|
+
router.add_api_route(
|
|
69
|
+
"/up",
|
|
70
|
+
self.deploy_up,
|
|
71
|
+
methods=["POST"],
|
|
72
|
+
)
|
|
73
|
+
router.add_api_route(
|
|
74
|
+
"/stop",
|
|
75
|
+
self.deploy_stop,
|
|
76
|
+
methods=["POST"],
|
|
77
|
+
)
|
|
78
|
+
router.add_api_route(
|
|
79
|
+
"/down",
|
|
80
|
+
self.deploy_down,
|
|
81
|
+
methods=["POST"],
|
|
82
|
+
)
|
|
83
|
+
router.add_api_route(
|
|
84
|
+
"/restart",
|
|
85
|
+
self.deploy_restart,
|
|
86
|
+
methods=["POST"],
|
|
87
|
+
)
|
|
88
|
+
return router
|
|
89
|
+
|
|
90
|
+
async def get_deployment_info(self) -> DeploymentInfoResponse:
|
|
91
|
+
"""
|
|
92
|
+
Get deployment information with status, metadata, and environment variables.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
DeploymentInfoResponse with status, metadata, and env-vars fields.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
HTTPException: If unable to retrieve deployment information.
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
logger.debug("Fetching deployment info")
|
|
102
|
+
|
|
103
|
+
# Get metadata
|
|
104
|
+
metadata_dict: DeploymentMetadata = self.config.get_deployment_metadata()
|
|
105
|
+
|
|
106
|
+
# Get status
|
|
107
|
+
status: DeploymentStatus = self.config.get_deployment_status()
|
|
108
|
+
|
|
109
|
+
# Get environment variables
|
|
110
|
+
schema = self.config.get_env_vars_schema()
|
|
111
|
+
current_values = self.config.load_env_values()
|
|
112
|
+
env_vars: dict[str, EnvVariable] = {}
|
|
113
|
+
for var_name, var_schema in schema.items():
|
|
114
|
+
default_val = var_schema.get("default", "")
|
|
115
|
+
current_val = current_values.get(var_name)
|
|
116
|
+
env_vars[var_name] = EnvVariable(
|
|
117
|
+
name=var_name,
|
|
118
|
+
description=var_schema.get("description", ""),
|
|
119
|
+
default=default_val,
|
|
120
|
+
value=current_val if current_val is not None else default_val,
|
|
121
|
+
type=var_schema.get("type", "string"),
|
|
122
|
+
advanced=var_schema.get("advanced", False),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
logger.info("Successfully retrieved deployment info")
|
|
126
|
+
return DeploymentInfoResponse(
|
|
127
|
+
metadata=metadata_dict,
|
|
128
|
+
status=status,
|
|
129
|
+
env_vars=env_vars)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.error(f"Failed to get deployment info: {e}")
|
|
132
|
+
raise HTTPException(
|
|
133
|
+
status_code=500, detail=f"Failed to get deployment info: {e}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
async def ping(self) -> DeploymentPingResponse:
|
|
137
|
+
"""
|
|
138
|
+
Health check endpoint.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
DeploymentPingResponse indicating API is operational.
|
|
142
|
+
"""
|
|
143
|
+
logger.debug("Ping request received")
|
|
144
|
+
return DeploymentPingResponse(success=True, message="API is operational")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async def deploy_up(self) -> DeploymentActionResponse:
|
|
148
|
+
"""
|
|
149
|
+
Start the deployment (docker compose up).
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
DeploymentActionResponse with success status and message.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
HTTPException: If deployment startup fails.
|
|
156
|
+
"""
|
|
157
|
+
try:
|
|
158
|
+
logger.info("Starting deployment (up)")
|
|
159
|
+
result: ComposeActionResponse = self.config.docker_compose_up()
|
|
160
|
+
|
|
161
|
+
if not result.success:
|
|
162
|
+
logger.error(f"Failed to start deployment: {result.message}")
|
|
163
|
+
raise HTTPException(
|
|
164
|
+
status_code=500, detail=result.message
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
logger.info("Deployment started successfully")
|
|
168
|
+
return DeploymentActionResponse(
|
|
169
|
+
success=result.success,
|
|
170
|
+
action="up",
|
|
171
|
+
message=result.message
|
|
172
|
+
)
|
|
173
|
+
except HTTPException:
|
|
174
|
+
raise
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.error(f"Error starting deployment: {e}")
|
|
177
|
+
raise HTTPException(
|
|
178
|
+
status_code=500, detail=f"Failed to start deployment: {str(e)}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
async def deploy_stop(self) -> DeploymentActionResponse:
|
|
182
|
+
"""
|
|
183
|
+
Stop the deployment (docker compose stop).
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
DeploymentActionResponse with success status and message.
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
HTTPException: If deployment stop fails.
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
logger.info("Stopping deployment")
|
|
193
|
+
result: ComposeActionResponse = self.config.docker_compose_stop()
|
|
194
|
+
|
|
195
|
+
if not result.success:
|
|
196
|
+
logger.error(f"Failed to stop deployment: {result.message}")
|
|
197
|
+
raise HTTPException(
|
|
198
|
+
status_code=500, detail=result.message
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
logger.info("Deployment stopped successfully")
|
|
202
|
+
return DeploymentActionResponse(
|
|
203
|
+
success=result.success,
|
|
204
|
+
action="stop",
|
|
205
|
+
message=result.message
|
|
206
|
+
)
|
|
207
|
+
except HTTPException:
|
|
208
|
+
raise
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.error(f"Error stopping deployment: {e}")
|
|
211
|
+
raise HTTPException(
|
|
212
|
+
status_code=500, detail=f"Failed to stop deployment: {str(e)}"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
async def deploy_down(self) -> DeploymentActionResponse:
|
|
216
|
+
"""
|
|
217
|
+
Down the deployment (docker compose down and remove volumes).
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
DeploymentActionResponse with success status and message.
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
HTTPException: If deployment down fails.
|
|
224
|
+
"""
|
|
225
|
+
try:
|
|
226
|
+
logger.info("Downing deployment (removing containers and volumes)")
|
|
227
|
+
result: ComposeActionResponse = self.config.docker_compose_down()
|
|
228
|
+
|
|
229
|
+
if not result.success:
|
|
230
|
+
logger.error(f"Failed to down deployment: {result.message}")
|
|
231
|
+
raise HTTPException(
|
|
232
|
+
status_code=500, detail=result.message
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
logger.info("Deployment downed successfully")
|
|
236
|
+
return DeploymentActionResponse(
|
|
237
|
+
success=result.success,
|
|
238
|
+
action="down",
|
|
239
|
+
message=result.message
|
|
240
|
+
)
|
|
241
|
+
except HTTPException:
|
|
242
|
+
raise
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.error(f"Error downing deployment: {e}")
|
|
245
|
+
raise HTTPException(
|
|
246
|
+
status_code=500, detail=f"Failed to down deployment: {str(e)}"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
async def deploy_restart(self) -> DeploymentActionResponse:
|
|
250
|
+
"""
|
|
251
|
+
Restart the deployment (docker compose down then up).
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
DeploymentActionResponse with success status and message.
|
|
255
|
+
|
|
256
|
+
Raises:
|
|
257
|
+
HTTPException: If deployment restart fails.
|
|
258
|
+
"""
|
|
259
|
+
try:
|
|
260
|
+
logger.info("Restarting deployment")
|
|
261
|
+
result: ComposeActionResponse = self.config.docker_compose_restart()
|
|
262
|
+
|
|
263
|
+
if not result.success:
|
|
264
|
+
logger.error(f"Failed to stop deployment (while restarting): {result.message}")
|
|
265
|
+
raise HTTPException(
|
|
266
|
+
status_code=500, detail=result.message
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
logger.info("Deployment restarted successfully")
|
|
270
|
+
return DeploymentActionResponse(
|
|
271
|
+
success=result.success,
|
|
272
|
+
action="restart",
|
|
273
|
+
message=result.message
|
|
274
|
+
)
|
|
275
|
+
except HTTPException:
|
|
276
|
+
raise
|
|
277
|
+
except Exception as e:
|
|
278
|
+
logger.error(f"Error restarting deployment: {e}")
|
|
279
|
+
raise HTTPException(
|
|
280
|
+
status_code=500, detail=f"Failed to restart deployment: {str(e)}"
|
|
281
|
+
)
|