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.
- pactown/__init__.py +23 -0
- pactown/cli.py +347 -0
- pactown/config.py +158 -0
- pactown/deploy/__init__.py +17 -0
- pactown/deploy/base.py +263 -0
- pactown/deploy/compose.py +359 -0
- pactown/deploy/docker.py +299 -0
- pactown/deploy/kubernetes.py +449 -0
- pactown/deploy/podman.py +400 -0
- pactown/generator.py +212 -0
- pactown/network.py +245 -0
- pactown/orchestrator.py +455 -0
- pactown/parallel.py +268 -0
- pactown/registry/__init__.py +12 -0
- pactown/registry/client.py +253 -0
- pactown/registry/models.py +150 -0
- pactown/registry/server.py +207 -0
- pactown/resolver.py +160 -0
- pactown/sandbox_manager.py +328 -0
- pactown-0.1.4.dist-info/METADATA +308 -0
- pactown-0.1.4.dist-info/RECORD +24 -0
- pactown-0.1.4.dist-info/WHEEL +4 -0
- pactown-0.1.4.dist-info/entry_points.txt +3 -0
- pactown-0.1.4.dist-info/licenses/LICENSE +201 -0
pactown/deploy/docker.py
ADDED
|
@@ -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"}
|