pactown 0.1.4__py3-none-any.whl → 0.1.47__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/deploy/docker.py CHANGED
@@ -2,14 +2,13 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import subprocess
6
5
  import json
6
+ import subprocess
7
7
  from pathlib import Path
8
- from typing import Optional, Any
8
+ from typing import Any, Optional
9
9
 
10
10
  from .base import (
11
11
  DeploymentBackend,
12
- DeploymentConfig,
13
12
  DeploymentResult,
14
13
  RuntimeType,
15
14
  )
@@ -17,11 +16,11 @@ from .base import (
17
16
 
18
17
  class DockerBackend(DeploymentBackend):
19
18
  """Docker container runtime backend."""
20
-
19
+
21
20
  @property
22
21
  def runtime_type(self) -> RuntimeType:
23
22
  return RuntimeType.DOCKER
24
-
23
+
25
24
  def is_available(self) -> bool:
26
25
  """Check if Docker is available."""
27
26
  try:
@@ -34,7 +33,7 @@ class DockerBackend(DeploymentBackend):
34
33
  return result.returncode == 0
35
34
  except (subprocess.TimeoutExpired, FileNotFoundError):
36
35
  return False
37
-
36
+
38
37
  def build_image(
39
38
  self,
40
39
  service_name: str,
@@ -48,18 +47,18 @@ class DockerBackend(DeploymentBackend):
48
47
  image_name = f"{image_name}:{tag}"
49
48
  else:
50
49
  image_name = f"{image_name}:latest"
51
-
50
+
52
51
  cmd = [
53
52
  "docker", "build",
54
53
  "-t", image_name,
55
54
  "-f", str(dockerfile_path),
56
55
  str(context_path),
57
56
  ]
58
-
57
+
59
58
  # Add labels
60
59
  for key, value in self.config.labels.items():
61
60
  cmd.extend(["--label", f"{key}={value}"])
62
-
61
+
63
62
  try:
64
63
  result = subprocess.run(
65
64
  cmd,
@@ -67,7 +66,7 @@ class DockerBackend(DeploymentBackend):
67
66
  text=True,
68
67
  timeout=300,
69
68
  )
70
-
69
+
71
70
  if result.returncode == 0:
72
71
  return DeploymentResult(
73
72
  success=True,
@@ -89,7 +88,7 @@ class DockerBackend(DeploymentBackend):
89
88
  runtime=self.runtime_type,
90
89
  error="Build timed out",
91
90
  )
92
-
91
+
93
92
  def push_image(
94
93
  self,
95
94
  image_name: str,
@@ -104,7 +103,7 @@ class DockerBackend(DeploymentBackend):
104
103
  ["docker", "tag", image_name, target],
105
104
  capture_output=True,
106
105
  )
107
-
106
+
108
107
  try:
109
108
  result = subprocess.run(
110
109
  ["docker", "push", target],
@@ -112,7 +111,7 @@ class DockerBackend(DeploymentBackend):
112
111
  text=True,
113
112
  timeout=300,
114
113
  )
115
-
114
+
116
115
  return DeploymentResult(
117
116
  success=result.returncode == 0,
118
117
  service_name=image_name.split("/")[-1].split(":")[0],
@@ -127,7 +126,7 @@ class DockerBackend(DeploymentBackend):
127
126
  runtime=self.runtime_type,
128
127
  error="Push timed out",
129
128
  )
130
-
129
+
131
130
  def deploy(
132
131
  self,
133
132
  service_name: str,
@@ -138,13 +137,13 @@ class DockerBackend(DeploymentBackend):
138
137
  ) -> DeploymentResult:
139
138
  """Deploy a container."""
140
139
  container_name = f"{self.config.namespace}-{service_name}"
141
-
140
+
142
141
  # Stop existing container if running
143
142
  subprocess.run(
144
143
  ["docker", "rm", "-f", container_name],
145
144
  capture_output=True,
146
145
  )
147
-
146
+
148
147
  cmd = [
149
148
  "docker", "run",
150
149
  "-d",
@@ -152,37 +151,37 @@ class DockerBackend(DeploymentBackend):
152
151
  "--network", self.config.network_name,
153
152
  "--restart", "unless-stopped",
154
153
  ]
155
-
154
+
156
155
  # Port mapping
157
156
  if self.config.expose_ports:
158
157
  cmd.extend(["-p", f"{port}:{port}"])
159
-
158
+
160
159
  # Environment variables
161
160
  for key, value in env.items():
162
161
  cmd.extend(["-e", f"{key}={value}"])
163
-
162
+
164
163
  # Resource limits
165
164
  if self.config.memory_limit:
166
165
  cmd.extend(["--memory", self.config.memory_limit])
167
166
  if self.config.cpu_limit:
168
167
  cmd.extend(["--cpus", self.config.cpu_limit])
169
-
168
+
170
169
  # Security options
171
170
  if self.config.read_only_fs:
172
171
  cmd.append("--read-only")
173
172
  cmd.extend(["--tmpfs", "/tmp"])
174
-
173
+
175
174
  if self.config.no_new_privileges:
176
175
  cmd.append("--security-opt=no-new-privileges:true")
177
-
176
+
178
177
  if self.config.drop_capabilities:
179
178
  for cap in self.config.drop_capabilities:
180
179
  cmd.extend(["--cap-drop", cap])
181
-
180
+
182
181
  if self.config.add_capabilities:
183
182
  for cap in self.config.add_capabilities:
184
183
  cmd.extend(["--cap-add", cap])
185
-
184
+
186
185
  # Health check
187
186
  if health_check:
188
187
  cmd.extend([
@@ -191,31 +190,31 @@ class DockerBackend(DeploymentBackend):
191
190
  "--health-timeout", self.config.health_check_timeout,
192
191
  "--health-retries", str(self.config.health_check_retries),
193
192
  ])
194
-
193
+
195
194
  # Labels
196
195
  for key, value in self.config.labels.items():
197
196
  cmd.extend(["--label", f"{key}={value}"])
198
-
197
+
199
198
  cmd.append(image_name)
200
-
199
+
201
200
  try:
202
201
  # Ensure network exists
203
202
  subprocess.run(
204
203
  ["docker", "network", "create", self.config.network_name],
205
204
  capture_output=True,
206
205
  )
207
-
206
+
208
207
  result = subprocess.run(
209
208
  cmd,
210
209
  capture_output=True,
211
210
  text=True,
212
211
  timeout=60,
213
212
  )
214
-
213
+
215
214
  if result.returncode == 0:
216
215
  container_id = result.stdout.strip()[:12]
217
216
  endpoint = f"http://{container_name}:{port}" if self.config.use_internal_dns else f"http://localhost:{port}"
218
-
217
+
219
218
  return DeploymentResult(
220
219
  success=True,
221
220
  service_name=service_name,
@@ -238,54 +237,54 @@ class DockerBackend(DeploymentBackend):
238
237
  runtime=self.runtime_type,
239
238
  error="Deploy timed out",
240
239
  )
241
-
240
+
242
241
  def stop(self, service_name: str) -> DeploymentResult:
243
242
  """Stop a container."""
244
243
  container_name = f"{self.config.namespace}-{service_name}"
245
-
244
+
246
245
  result = subprocess.run(
247
246
  ["docker", "stop", container_name],
248
247
  capture_output=True,
249
248
  text=True,
250
249
  )
251
-
250
+
252
251
  subprocess.run(
253
252
  ["docker", "rm", container_name],
254
253
  capture_output=True,
255
254
  )
256
-
255
+
257
256
  return DeploymentResult(
258
257
  success=result.returncode == 0,
259
258
  service_name=service_name,
260
259
  runtime=self.runtime_type,
261
260
  error=result.stderr if result.returncode != 0 else None,
262
261
  )
263
-
262
+
264
263
  def logs(self, service_name: str, tail: int = 100) -> str:
265
264
  """Get container logs."""
266
265
  container_name = f"{self.config.namespace}-{service_name}"
267
-
266
+
268
267
  result = subprocess.run(
269
268
  ["docker", "logs", "--tail", str(tail), container_name],
270
269
  capture_output=True,
271
270
  text=True,
272
271
  )
273
-
272
+
274
273
  return result.stdout + result.stderr
275
-
274
+
276
275
  def status(self, service_name: str) -> dict[str, Any]:
277
276
  """Get container status."""
278
277
  container_name = f"{self.config.namespace}-{service_name}"
279
-
278
+
280
279
  result = subprocess.run(
281
280
  ["docker", "inspect", container_name],
282
281
  capture_output=True,
283
282
  text=True,
284
283
  )
285
-
284
+
286
285
  if result.returncode != 0:
287
286
  return {"running": False, "error": "Container not found"}
288
-
287
+
289
288
  try:
290
289
  data = json.loads(result.stdout)[0]
291
290
  return {
@@ -2,11 +2,12 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import subprocess
6
5
  import json
7
- import yaml
6
+ import subprocess
8
7
  from pathlib import Path
9
- from typing import Optional, Any
8
+ from typing import Any, Optional
9
+
10
+ import yaml
10
11
 
11
12
  from .base import (
12
13
  DeploymentBackend,
@@ -19,7 +20,7 @@ from .base import (
19
20
  class KubernetesBackend(DeploymentBackend):
20
21
  """
21
22
  Kubernetes deployment backend for production environments.
22
-
23
+
23
24
  Generates and applies Kubernetes manifests for:
24
25
  - Deployments with rolling updates
25
26
  - Services for internal/external access
@@ -28,22 +29,22 @@ class KubernetesBackend(DeploymentBackend):
28
29
  - HorizontalPodAutoscaler for scaling
29
30
  - NetworkPolicies for security
30
31
  """
31
-
32
+
32
33
  def __init__(self, config: DeploymentConfig, kubeconfig: Optional[str] = None):
33
34
  super().__init__(config)
34
35
  self.kubeconfig = kubeconfig
35
-
36
+
36
37
  @property
37
38
  def runtime_type(self) -> RuntimeType:
38
39
  return RuntimeType.KUBERNETES
39
-
40
+
40
41
  def _kubectl(self, *args: str, input_data: Optional[str] = None) -> subprocess.CompletedProcess:
41
42
  """Run kubectl command."""
42
43
  cmd = ["kubectl"]
43
44
  if self.kubeconfig:
44
45
  cmd.extend(["--kubeconfig", self.kubeconfig])
45
46
  cmd.extend(args)
46
-
47
+
47
48
  return subprocess.run(
48
49
  cmd,
49
50
  capture_output=True,
@@ -51,7 +52,7 @@ class KubernetesBackend(DeploymentBackend):
51
52
  input=input_data,
52
53
  timeout=60,
53
54
  )
54
-
55
+
55
56
  def is_available(self) -> bool:
56
57
  """Check if kubectl is available and cluster is reachable."""
57
58
  try:
@@ -59,7 +60,7 @@ class KubernetesBackend(DeploymentBackend):
59
60
  return result.returncode == 0
60
61
  except (subprocess.TimeoutExpired, FileNotFoundError):
61
62
  return False
62
-
63
+
63
64
  def build_image(
64
65
  self,
65
66
  service_name: str,
@@ -74,7 +75,7 @@ class KubernetesBackend(DeploymentBackend):
74
75
  image_name = f"{image_name}:{tag}"
75
76
  else:
76
77
  image_name = f"{image_name}:latest"
77
-
78
+
78
79
  # Try podman first, then docker
79
80
  for runtime in ["podman", "docker"]:
80
81
  try:
@@ -93,14 +94,14 @@ class KubernetesBackend(DeploymentBackend):
93
94
  )
94
95
  except FileNotFoundError:
95
96
  continue
96
-
97
+
97
98
  return DeploymentResult(
98
99
  success=False,
99
100
  service_name=service_name,
100
101
  runtime=self.runtime_type,
101
102
  error="No container runtime (docker/podman) available",
102
103
  )
103
-
104
+
104
105
  def push_image(
105
106
  self,
106
107
  image_name: str,
@@ -124,14 +125,14 @@ class KubernetesBackend(DeploymentBackend):
124
125
  )
125
126
  except FileNotFoundError:
126
127
  continue
127
-
128
+
128
129
  return DeploymentResult(
129
130
  success=False,
130
131
  service_name=image_name,
131
132
  runtime=self.runtime_type,
132
133
  error="Failed to push image",
133
134
  )
134
-
135
+
135
136
  def deploy(
136
137
  self,
137
138
  service_name: str,
@@ -148,12 +149,12 @@ class KubernetesBackend(DeploymentBackend):
148
149
  env=env,
149
150
  health_check=health_check,
150
151
  )
151
-
152
+
152
153
  # Apply all manifests
153
154
  for manifest in manifests:
154
155
  manifest_yaml = yaml.dump(manifest, default_flow_style=False)
155
156
  result = self._kubectl("apply", "-f", "-", input_data=manifest_yaml)
156
-
157
+
157
158
  if result.returncode != 0:
158
159
  return DeploymentResult(
159
160
  success=False,
@@ -161,16 +162,16 @@ class KubernetesBackend(DeploymentBackend):
161
162
  runtime=self.runtime_type,
162
163
  error=result.stderr,
163
164
  )
164
-
165
+
165
166
  # Get service endpoint
166
167
  result = self._kubectl(
167
168
  "get", "service", service_name,
168
169
  "-n", self.config.namespace,
169
170
  "-o", "jsonpath={.status.loadBalancer.ingress[0].ip}",
170
171
  )
171
-
172
+
172
173
  endpoint = f"http://{result.stdout}:{port}" if result.stdout else f"http://{service_name}.{self.config.namespace}.svc.cluster.local:{port}"
173
-
174
+
174
175
  return DeploymentResult(
175
176
  success=True,
176
177
  service_name=service_name,
@@ -178,7 +179,7 @@ class KubernetesBackend(DeploymentBackend):
178
179
  image_name=image_name,
179
180
  endpoint=endpoint,
180
181
  )
181
-
182
+
182
183
  def stop(self, service_name: str) -> DeploymentResult:
183
184
  """Delete Kubernetes resources."""
184
185
  result = self._kubectl(
@@ -186,14 +187,14 @@ class KubernetesBackend(DeploymentBackend):
186
187
  "-l", f"app={service_name}",
187
188
  "-n", self.config.namespace,
188
189
  )
189
-
190
+
190
191
  return DeploymentResult(
191
192
  success=result.returncode == 0,
192
193
  service_name=service_name,
193
194
  runtime=self.runtime_type,
194
195
  error=result.stderr if result.returncode != 0 else None,
195
196
  )
196
-
197
+
197
198
  def logs(self, service_name: str, tail: int = 100) -> str:
198
199
  """Get pod logs."""
199
200
  result = self._kubectl(
@@ -203,7 +204,7 @@ class KubernetesBackend(DeploymentBackend):
203
204
  "--tail", str(tail),
204
205
  )
205
206
  return result.stdout + result.stderr
206
-
207
+
207
208
  def status(self, service_name: str) -> dict[str, Any]:
208
209
  """Get deployment status."""
209
210
  result = self._kubectl(
@@ -211,10 +212,10 @@ class KubernetesBackend(DeploymentBackend):
211
212
  "-n", self.config.namespace,
212
213
  "-o", "json",
213
214
  )
214
-
215
+
215
216
  if result.returncode != 0:
216
217
  return {"running": False, "error": "Deployment not found"}
217
-
218
+
218
219
  try:
219
220
  data = json.loads(result.stdout)
220
221
  status = data.get("status", {})
@@ -227,7 +228,7 @@ class KubernetesBackend(DeploymentBackend):
227
228
  }
228
229
  except json.JSONDecodeError:
229
230
  return {"running": False, "error": "Failed to parse status"}
230
-
231
+
231
232
  def generate_manifests(
232
233
  self,
233
234
  service_name: str,
@@ -243,9 +244,9 @@ class KubernetesBackend(DeploymentBackend):
243
244
  "managed-by": "pactown",
244
245
  **self.config.labels,
245
246
  }
246
-
247
+
247
248
  manifests = []
248
-
249
+
249
250
  # Namespace
250
251
  manifests.append({
251
252
  "apiVersion": "v1",
@@ -255,7 +256,7 @@ class KubernetesBackend(DeploymentBackend):
255
256
  "labels": {"managed-by": "pactown"},
256
257
  },
257
258
  })
258
-
259
+
259
260
  # ConfigMap for environment variables
260
261
  if env:
261
262
  manifests.append({
@@ -268,7 +269,7 @@ class KubernetesBackend(DeploymentBackend):
268
269
  },
269
270
  "data": env,
270
271
  })
271
-
272
+
272
273
  # Deployment
273
274
  container_spec = {
274
275
  "name": service_name,
@@ -294,12 +295,12 @@ class KubernetesBackend(DeploymentBackend):
294
295
  },
295
296
  },
296
297
  }
297
-
298
+
298
299
  if env:
299
300
  container_spec["envFrom"] = [
300
301
  {"configMapRef": {"name": f"{service_name}-config"}}
301
302
  ]
302
-
303
+
303
304
  if health_check:
304
305
  container_spec["livenessProbe"] = {
305
306
  "httpGet": {"path": health_check, "port": port},
@@ -315,7 +316,7 @@ class KubernetesBackend(DeploymentBackend):
315
316
  "timeoutSeconds": 3,
316
317
  "failureThreshold": 3,
317
318
  }
318
-
319
+
319
320
  deployment = {
320
321
  "apiVersion": "apps/v1",
321
322
  "kind": "Deployment",
@@ -350,7 +351,7 @@ class KubernetesBackend(DeploymentBackend):
350
351
  },
351
352
  }
352
353
  manifests.append(deployment)
353
-
354
+
354
355
  # Service
355
356
  service = {
356
357
  "apiVersion": "v1",
@@ -367,7 +368,7 @@ class KubernetesBackend(DeploymentBackend):
367
368
  },
368
369
  }
369
370
  manifests.append(service)
370
-
371
+
371
372
  # NetworkPolicy for security
372
373
  network_policy = {
373
374
  "apiVersion": "networking.k8s.io/v1",
@@ -389,9 +390,9 @@ class KubernetesBackend(DeploymentBackend):
389
390
  },
390
391
  }
391
392
  manifests.append(network_policy)
392
-
393
+
393
394
  return manifests
394
-
395
+
395
396
  def generate_hpa(
396
397
  self,
397
398
  service_name: str,
@@ -427,7 +428,7 @@ class KubernetesBackend(DeploymentBackend):
427
428
  }],
428
429
  },
429
430
  }
430
-
431
+
431
432
  def save_manifests(
432
433
  self,
433
434
  service_name: str,
@@ -437,13 +438,13 @@ class KubernetesBackend(DeploymentBackend):
437
438
  """Save manifests to files."""
438
439
  output_dir = Path(output_dir)
439
440
  output_dir.mkdir(parents=True, exist_ok=True)
440
-
441
+
441
442
  output_file = output_dir / f"{service_name}.yaml"
442
-
443
+
443
444
  with open(output_file, "w") as f:
444
445
  for i, manifest in enumerate(manifests):
445
446
  if i > 0:
446
447
  f.write("---\n")
447
448
  yaml.dump(manifest, f, default_flow_style=False)
448
-
449
+
449
450
  return output_file