pactown 0.1.4__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.
@@ -0,0 +1,299 @@
1
+ """Docker deployment backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Optional, Any
9
+
10
+ from .base import (
11
+ DeploymentBackend,
12
+ DeploymentConfig,
13
+ DeploymentResult,
14
+ RuntimeType,
15
+ )
16
+
17
+
18
+ class DockerBackend(DeploymentBackend):
19
+ """Docker container runtime backend."""
20
+
21
+ @property
22
+ def runtime_type(self) -> RuntimeType:
23
+ return RuntimeType.DOCKER
24
+
25
+ def is_available(self) -> bool:
26
+ """Check if Docker is available."""
27
+ try:
28
+ result = subprocess.run(
29
+ ["docker", "version", "--format", "{{.Server.Version}}"],
30
+ capture_output=True,
31
+ text=True,
32
+ timeout=5,
33
+ )
34
+ return result.returncode == 0
35
+ except (subprocess.TimeoutExpired, FileNotFoundError):
36
+ return False
37
+
38
+ def build_image(
39
+ self,
40
+ service_name: str,
41
+ dockerfile_path: Path,
42
+ context_path: Path,
43
+ tag: Optional[str] = None,
44
+ ) -> DeploymentResult:
45
+ """Build Docker image."""
46
+ image_name = f"{self.config.image_prefix}/{service_name}"
47
+ if tag:
48
+ image_name = f"{image_name}:{tag}"
49
+ else:
50
+ image_name = f"{image_name}:latest"
51
+
52
+ cmd = [
53
+ "docker", "build",
54
+ "-t", image_name,
55
+ "-f", str(dockerfile_path),
56
+ str(context_path),
57
+ ]
58
+
59
+ # Add labels
60
+ for key, value in self.config.labels.items():
61
+ cmd.extend(["--label", f"{key}={value}"])
62
+
63
+ try:
64
+ result = subprocess.run(
65
+ cmd,
66
+ capture_output=True,
67
+ text=True,
68
+ timeout=300,
69
+ )
70
+
71
+ if result.returncode == 0:
72
+ return DeploymentResult(
73
+ success=True,
74
+ service_name=service_name,
75
+ runtime=self.runtime_type,
76
+ image_name=image_name,
77
+ )
78
+ else:
79
+ return DeploymentResult(
80
+ success=False,
81
+ service_name=service_name,
82
+ runtime=self.runtime_type,
83
+ error=result.stderr,
84
+ )
85
+ except subprocess.TimeoutExpired:
86
+ return DeploymentResult(
87
+ success=False,
88
+ service_name=service_name,
89
+ runtime=self.runtime_type,
90
+ error="Build timed out",
91
+ )
92
+
93
+ def push_image(
94
+ self,
95
+ image_name: str,
96
+ registry: Optional[str] = None,
97
+ ) -> DeploymentResult:
98
+ """Push image to registry."""
99
+ target = image_name
100
+ if registry:
101
+ target = f"{registry}/{image_name}"
102
+ # Tag for registry
103
+ subprocess.run(
104
+ ["docker", "tag", image_name, target],
105
+ capture_output=True,
106
+ )
107
+
108
+ try:
109
+ result = subprocess.run(
110
+ ["docker", "push", target],
111
+ capture_output=True,
112
+ text=True,
113
+ timeout=300,
114
+ )
115
+
116
+ return DeploymentResult(
117
+ success=result.returncode == 0,
118
+ service_name=image_name.split("/")[-1].split(":")[0],
119
+ runtime=self.runtime_type,
120
+ image_name=target,
121
+ error=result.stderr if result.returncode != 0 else None,
122
+ )
123
+ except subprocess.TimeoutExpired:
124
+ return DeploymentResult(
125
+ success=False,
126
+ service_name=image_name,
127
+ runtime=self.runtime_type,
128
+ error="Push timed out",
129
+ )
130
+
131
+ def deploy(
132
+ self,
133
+ service_name: str,
134
+ image_name: str,
135
+ port: int,
136
+ env: dict[str, str],
137
+ health_check: Optional[str] = None,
138
+ ) -> DeploymentResult:
139
+ """Deploy a container."""
140
+ container_name = f"{self.config.namespace}-{service_name}"
141
+
142
+ # Stop existing container if running
143
+ subprocess.run(
144
+ ["docker", "rm", "-f", container_name],
145
+ capture_output=True,
146
+ )
147
+
148
+ cmd = [
149
+ "docker", "run",
150
+ "-d",
151
+ "--name", container_name,
152
+ "--network", self.config.network_name,
153
+ "--restart", "unless-stopped",
154
+ ]
155
+
156
+ # Port mapping
157
+ if self.config.expose_ports:
158
+ cmd.extend(["-p", f"{port}:{port}"])
159
+
160
+ # Environment variables
161
+ for key, value in env.items():
162
+ cmd.extend(["-e", f"{key}={value}"])
163
+
164
+ # Resource limits
165
+ if self.config.memory_limit:
166
+ cmd.extend(["--memory", self.config.memory_limit])
167
+ if self.config.cpu_limit:
168
+ cmd.extend(["--cpus", self.config.cpu_limit])
169
+
170
+ # Security options
171
+ if self.config.read_only_fs:
172
+ cmd.append("--read-only")
173
+ cmd.extend(["--tmpfs", "/tmp"])
174
+
175
+ if self.config.no_new_privileges:
176
+ cmd.append("--security-opt=no-new-privileges:true")
177
+
178
+ if self.config.drop_capabilities:
179
+ for cap in self.config.drop_capabilities:
180
+ cmd.extend(["--cap-drop", cap])
181
+
182
+ if self.config.add_capabilities:
183
+ for cap in self.config.add_capabilities:
184
+ cmd.extend(["--cap-add", cap])
185
+
186
+ # Health check
187
+ if health_check:
188
+ cmd.extend([
189
+ "--health-cmd", f"curl -f http://localhost:{port}{health_check} || exit 1",
190
+ "--health-interval", self.config.health_check_interval,
191
+ "--health-timeout", self.config.health_check_timeout,
192
+ "--health-retries", str(self.config.health_check_retries),
193
+ ])
194
+
195
+ # Labels
196
+ for key, value in self.config.labels.items():
197
+ cmd.extend(["--label", f"{key}={value}"])
198
+
199
+ cmd.append(image_name)
200
+
201
+ try:
202
+ # Ensure network exists
203
+ subprocess.run(
204
+ ["docker", "network", "create", self.config.network_name],
205
+ capture_output=True,
206
+ )
207
+
208
+ result = subprocess.run(
209
+ cmd,
210
+ capture_output=True,
211
+ text=True,
212
+ timeout=60,
213
+ )
214
+
215
+ if result.returncode == 0:
216
+ container_id = result.stdout.strip()[:12]
217
+ endpoint = f"http://{container_name}:{port}" if self.config.use_internal_dns else f"http://localhost:{port}"
218
+
219
+ return DeploymentResult(
220
+ success=True,
221
+ service_name=service_name,
222
+ runtime=self.runtime_type,
223
+ container_id=container_id,
224
+ image_name=image_name,
225
+ endpoint=endpoint,
226
+ )
227
+ else:
228
+ return DeploymentResult(
229
+ success=False,
230
+ service_name=service_name,
231
+ runtime=self.runtime_type,
232
+ error=result.stderr,
233
+ )
234
+ except subprocess.TimeoutExpired:
235
+ return DeploymentResult(
236
+ success=False,
237
+ service_name=service_name,
238
+ runtime=self.runtime_type,
239
+ error="Deploy timed out",
240
+ )
241
+
242
+ def stop(self, service_name: str) -> DeploymentResult:
243
+ """Stop a container."""
244
+ container_name = f"{self.config.namespace}-{service_name}"
245
+
246
+ result = subprocess.run(
247
+ ["docker", "stop", container_name],
248
+ capture_output=True,
249
+ text=True,
250
+ )
251
+
252
+ subprocess.run(
253
+ ["docker", "rm", container_name],
254
+ capture_output=True,
255
+ )
256
+
257
+ return DeploymentResult(
258
+ success=result.returncode == 0,
259
+ service_name=service_name,
260
+ runtime=self.runtime_type,
261
+ error=result.stderr if result.returncode != 0 else None,
262
+ )
263
+
264
+ def logs(self, service_name: str, tail: int = 100) -> str:
265
+ """Get container logs."""
266
+ container_name = f"{self.config.namespace}-{service_name}"
267
+
268
+ result = subprocess.run(
269
+ ["docker", "logs", "--tail", str(tail), container_name],
270
+ capture_output=True,
271
+ text=True,
272
+ )
273
+
274
+ return result.stdout + result.stderr
275
+
276
+ def status(self, service_name: str) -> dict[str, Any]:
277
+ """Get container status."""
278
+ container_name = f"{self.config.namespace}-{service_name}"
279
+
280
+ result = subprocess.run(
281
+ ["docker", "inspect", container_name],
282
+ capture_output=True,
283
+ text=True,
284
+ )
285
+
286
+ if result.returncode != 0:
287
+ return {"running": False, "error": "Container not found"}
288
+
289
+ try:
290
+ data = json.loads(result.stdout)[0]
291
+ return {
292
+ "running": data["State"]["Running"],
293
+ "status": data["State"]["Status"],
294
+ "health": data["State"].get("Health", {}).get("Status", "unknown"),
295
+ "started_at": data["State"]["StartedAt"],
296
+ "container_id": data["Id"][:12],
297
+ }
298
+ except (json.JSONDecodeError, KeyError, IndexError):
299
+ return {"running": False, "error": "Failed to parse status"}