djaploy 1.6.1__tar.gz → 1.6.2__tar.gz
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.
- {djaploy-1.6.1 → djaploy-1.6.2}/PKG-INFO +1 -1
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/deploy.py +9 -1
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/infra/gitops.py +40 -5
- djaploy-1.6.2/djaploy/version.py +1 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy.egg-info/PKG-INFO +1 -1
- {djaploy-1.6.1 → djaploy-1.6.2}/pyproject.toml +1 -1
- {djaploy-1.6.1 → djaploy-1.6.2}/tests/test_gitops.py +89 -3
- djaploy-1.6.1/djaploy/version.py +0 -1
- {djaploy-1.6.1 → djaploy-1.6.2}/LICENSE +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/README.md +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/app.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/borg/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/borg/infra/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/borg/infra/djaploy_hooks.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/janitor/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/janitor/apps.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/janitor/infra/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/janitor/infra/commands/createjanitoruser.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/janitor/infra/djaploy_hooks.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/nginx/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/nginx/apps.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/nginx/infra/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/nginx/infra/djaploy_hooks.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/rclone/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/rclone/apps.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/rclone/infra/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/rclone/infra/djaploy_hooks.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/sync_certs/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/sync_certs/apps.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/sync_certs/infra/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/sync_certs/infra/djaploy_hooks.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/systemd/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/systemd/apps.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/systemd/infra/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/systemd/infra/djaploy_hooks.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/tailscale/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/tailscale/apps.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/tailscale/infra/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/tailscale/infra/djaploy_hooks.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/versioning/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/versioning/apps.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/versioning/infra/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/apps/versioning/infra/djaploy_hooks.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/artifact.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/bin/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/bin/django_pyinfra.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/bin/gunicornherder.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/builtin_hooks.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/certificates.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/changelog.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/commands/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/commands/_utils.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/commands/activate.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/commands/configure.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/commands/deploy.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/commands/restore.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/commands/rollback.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/commands/status.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/commands/sync_certs.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/config.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/discovery.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/hooks.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/infra/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/infra/bluegreen.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/infra/charts/django-app/Chart.yaml +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/infra/charts/django-app/templates/_helpers.tpl +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/infra/charts/django-app/templates/configmap.yaml +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/infra/charts/django-app/templates/cronjobs.yaml +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/infra/charts/django-app/templates/deployment.yaml +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/infra/charts/django-app/templates/ingress.yaml +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/infra/charts/django-app/templates/onepassword.yaml +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/infra/charts/django-app/templates/pvc.yaml +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/infra/charts/django-app/templates/service.yaml +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/infra/charts/django-app/templates/workers.yaml +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/infra/charts/django-app/values.yaml +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/infra/djaploy_hooks.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/infra/k3s.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/infra/templates.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/infra/utils.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/management/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/management/commands/__init__.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/management/commands/djaploy.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/management/commands/restore_backup.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/management/commands/sync_certs.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/management/commands/update_certs.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/management/commands/verify.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/management/utils.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/notifications.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/utils.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy/versioning.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy.egg-info/SOURCES.txt +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy.egg-info/dependency_links.txt +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy.egg-info/entry_points.txt +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy.egg-info/requires.txt +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/djaploy.egg-info/top_level.txt +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/setup.cfg +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/tests/test_bluegreen.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/tests/test_borg.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/tests/test_config.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/tests/test_deploy_scripts.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/tests/test_discovery.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/tests/test_gunicornherder.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/tests/test_hooks.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/tests/test_k3s.py +0 -0
- {djaploy-1.6.1 → djaploy-1.6.2}/tests/test_versioning.py +0 -0
|
@@ -61,7 +61,15 @@ def run_command(context: Dict[str, Any]) -> None:
|
|
|
61
61
|
call_hook(f"{command_name}:precommand", context)
|
|
62
62
|
call_hook("precommand", context)
|
|
63
63
|
|
|
64
|
-
# 2. Run pyinfra
|
|
64
|
+
# 2. Run pyinfra (unless a precommand hook already did all the work,
|
|
65
|
+
# e.g. the gitops strategy, which has no remote hooks)
|
|
66
|
+
if context.get("skip_remote"):
|
|
67
|
+
print(f"No remote execution needed ({context['skip_remote']}).", flush=True)
|
|
68
|
+
context["success"] = True
|
|
69
|
+
call_hook(f"{command_name}:postcommand", context)
|
|
70
|
+
call_hook("postcommand", context)
|
|
71
|
+
return
|
|
72
|
+
|
|
65
73
|
print("Starting remote execution...", flush=True)
|
|
66
74
|
processed_inventory = _preprocess_inventory(str(context["inventory_file"]))
|
|
67
75
|
|
|
@@ -15,8 +15,9 @@ pins its digest in the GitOps repository and syncs the Argo CD application:
|
|
|
15
15
|
5. sync that commit in Argo CD and wait until Synced and Healthy
|
|
16
16
|
|
|
17
17
|
The inventory host is the build node: its SSH connection (ssh_hostname,
|
|
18
|
-
ssh_user, ssh_key, ssh_known_hosts_file) is used for the build
|
|
19
|
-
|
|
18
|
+
ssh_user, ssh_key, ssh_known_hosts_file) is used for the build over one
|
|
19
|
+
multiplexed connection, retried if the connection drops. pyinfra is not run
|
|
20
|
+
for gitops-only inventories. Kubernetes access uses the local kubeconfig.
|
|
20
21
|
|
|
21
22
|
HostConfig fields used: app_name, manage_py_path, secret_key, data, and
|
|
22
23
|
``gitops_conf``::
|
|
@@ -50,7 +51,9 @@ import json
|
|
|
50
51
|
import os
|
|
51
52
|
import re
|
|
52
53
|
import shlex
|
|
54
|
+
import shutil
|
|
53
55
|
import subprocess
|
|
56
|
+
import tempfile
|
|
54
57
|
import time
|
|
55
58
|
import uuid
|
|
56
59
|
from pathlib import Path
|
|
@@ -203,11 +206,23 @@ def check_infra_repo(repo: Path, manifest: str) -> str:
|
|
|
203
206
|
|
|
204
207
|
|
|
205
208
|
class BuildNode:
|
|
206
|
-
"""SSH to the build node with the inventory's connection settings.
|
|
209
|
+
"""SSH to the build node with the inventory's connection settings.
|
|
210
|
+
|
|
211
|
+
All commands share one multiplexed connection, so a build makes one SSH
|
|
212
|
+
handshake. A failed connection (ssh exit status 255) is retried: every
|
|
213
|
+
build-job command is safe to repeat, and an input stream is rewound first.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
RETRIES = 3
|
|
207
217
|
|
|
208
218
|
def __init__(self, host_data):
|
|
209
219
|
self.target = f"{_get(host_data, 'ssh_user')}@{_get(host_data, 'ssh_hostname')}"
|
|
210
|
-
|
|
220
|
+
# Short private directory: Unix socket paths are limited (~104 bytes on macOS).
|
|
221
|
+
self.control_dir = tempfile.mkdtemp(prefix="djaploy-ssh-", dir="/tmp")
|
|
222
|
+
self.opts = ["-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=yes",
|
|
223
|
+
"-o", "ConnectTimeout=20", "-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=4",
|
|
224
|
+
"-o", "ControlMaster=auto", "-o", f"ControlPath={self.control_dir}/%C",
|
|
225
|
+
"-o", "ControlPersist=120"]
|
|
211
226
|
if _get(host_data, "ssh_port"):
|
|
212
227
|
self.opts += ["-p", str(_get(host_data, "ssh_port"))]
|
|
213
228
|
if _get(host_data, "ssh_key"):
|
|
@@ -216,7 +231,23 @@ class BuildNode:
|
|
|
216
231
|
self.opts += ["-o", f"UserKnownHostsFile={_get(host_data, 'ssh_known_hosts_file')}"]
|
|
217
232
|
|
|
218
233
|
def run(self, command: str, **kwargs):
|
|
219
|
-
|
|
234
|
+
stdin = kwargs.get("stdin")
|
|
235
|
+
for attempt in range(1, self.RETRIES + 1):
|
|
236
|
+
if attempt > 1 and hasattr(stdin, "seek"):
|
|
237
|
+
stdin.seek(0)
|
|
238
|
+
try:
|
|
239
|
+
return _run(["ssh", *self.opts, self.target, command], **kwargs)
|
|
240
|
+
except subprocess.CalledProcessError as exc:
|
|
241
|
+
if exc.returncode != 255 or attempt == self.RETRIES:
|
|
242
|
+
raise
|
|
243
|
+
print(f"[gitops] SSH connection to {self.target} failed; retrying ({attempt}/{self.RETRIES - 1})",
|
|
244
|
+
flush=True)
|
|
245
|
+
time.sleep(2 * attempt)
|
|
246
|
+
|
|
247
|
+
def close(self):
|
|
248
|
+
_run(["ssh", *self.opts, "-O", "exit", self.target], check=False,
|
|
249
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
250
|
+
shutil.rmtree(self.control_dir, ignore_errors=True)
|
|
220
251
|
|
|
221
252
|
|
|
222
253
|
BUILDER_DEFAULTS = {
|
|
@@ -317,6 +348,7 @@ def build_and_push(host_data, artifact_path: Path, image: str, tag: str) -> str:
|
|
|
317
348
|
result = json.loads(node.run(cmd["result"], capture_output=True, text=True).stdout)
|
|
318
349
|
finally:
|
|
319
350
|
node.run(cmd["cleanup"], check=False)
|
|
351
|
+
node.close()
|
|
320
352
|
digest = result.get("containerimage.digest", "")
|
|
321
353
|
if not DIGEST_RE.fullmatch(digest):
|
|
322
354
|
raise RuntimeError(f"Build finished without a pushed digest: {result}")
|
|
@@ -399,6 +431,9 @@ def _gitops_deploy(context):
|
|
|
399
431
|
gitops_hosts = [data for _, data in hosts if _get(data, "deployment_strategy") == GITOPS]
|
|
400
432
|
if not gitops_hosts:
|
|
401
433
|
return
|
|
434
|
+
if len(gitops_hosts) == len(hosts):
|
|
435
|
+
# No remote pyinfra hooks run for gitops hosts: don't connect at all.
|
|
436
|
+
context["skip_remote"] = "gitops deploys from this machine"
|
|
402
437
|
if len(gitops_hosts) > 1:
|
|
403
438
|
raise ValueError("gitops inventories deploy one app per environment; use one host")
|
|
404
439
|
host_data = gitops_hosts[0]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.6.2"
|
|
@@ -181,6 +181,7 @@ class SyncOperationTests(unittest.TestCase):
|
|
|
181
181
|
class BuildNodeTests(unittest.TestCase):
|
|
182
182
|
def test_ssh_options_from_inventory(self):
|
|
183
183
|
node = gitops.BuildNode(host(ssh_port=2222, ssh_known_hosts_file="/tmp/kh"))
|
|
184
|
+
self.addCleanup(lambda: __import__("shutil").rmtree(node.control_dir, ignore_errors=True))
|
|
184
185
|
self.assertEqual(node.target, "janitor@203.0.113.10")
|
|
185
186
|
self.assertIn("StrictHostKeyChecking=yes", node.opts)
|
|
186
187
|
self.assertIn("UserKnownHostsFile=/tmp/kh", node.opts)
|
|
@@ -201,6 +202,9 @@ class FakeNode:
|
|
|
201
202
|
stdout = '{"containerimage.digest": "%s"}' % NEW if command.endswith("result.json") else ""
|
|
202
203
|
return subprocess.CompletedProcess(command, 0, stdout=stdout)
|
|
203
204
|
|
|
205
|
+
def close(self):
|
|
206
|
+
self.commands.append("<close>")
|
|
207
|
+
|
|
204
208
|
|
|
205
209
|
class BuilderTests(unittest.TestCase):
|
|
206
210
|
def setUp(self):
|
|
@@ -228,7 +232,8 @@ class BuilderTests(unittest.TestCase):
|
|
|
228
232
|
def test_shared_build_sequence(self):
|
|
229
233
|
node = FakeNode()
|
|
230
234
|
self.assertEqual(self.build(node), NEW)
|
|
231
|
-
self.
|
|
235
|
+
self.assertEqual(node.commands[-1], "<close>")
|
|
236
|
+
self.assertTrue(all(c.startswith("sudo -u builder ") for c in node.commands[:-1]), node.commands)
|
|
232
237
|
self.assertFalse(any("systemd-run" in c for c in node.commands))
|
|
233
238
|
steps = ["mkdir -p", "tar --no-same-owner", "umask 077", "buildctl --addr", "/result.json", "rm -rf --"]
|
|
234
239
|
positions = [next(i for i, c in enumerate(node.commands) if step in c and (step != "/result.json" or c.endswith(step))) for step in steps]
|
|
@@ -242,14 +247,64 @@ class BuilderTests(unittest.TestCase):
|
|
|
242
247
|
node = FakeNode(fail_on=" build ")
|
|
243
248
|
with self.assertRaises(subprocess.CalledProcessError):
|
|
244
249
|
self.build(node)
|
|
245
|
-
self.assertTrue(node.commands[-
|
|
250
|
+
self.assertTrue(node.commands[-2].startswith("sudo -u builder rm -rf -- /var/lib/techco-buildkit/jobs/"))
|
|
251
|
+
self.assertEqual(node.commands[-1], "<close>")
|
|
246
252
|
|
|
247
253
|
def test_temporary_mode_starts_its_own_daemon(self):
|
|
248
254
|
node = FakeNode()
|
|
249
255
|
self.assertEqual(self.build(node, mode="temporary"), NEW)
|
|
250
256
|
self.assertTrue(any("systemd-run" in c for c in node.commands))
|
|
251
257
|
self.assertTrue(any("/data/djaploy-build/docms-prod-abc1234-" in c for c in node.commands))
|
|
252
|
-
self.assertIn("systemctl stop djaploy-buildkit-", node.commands[-
|
|
258
|
+
self.assertIn("systemctl stop djaploy-buildkit-", node.commands[-2])
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class BuildNodeConnectionTests(unittest.TestCase):
|
|
262
|
+
def setUp(self):
|
|
263
|
+
self.node = gitops.BuildNode(host())
|
|
264
|
+
self.addCleanup(lambda: __import__("shutil").rmtree(self.node.control_dir, ignore_errors=True))
|
|
265
|
+
patcher = patch.object(gitops.time, "sleep")
|
|
266
|
+
patcher.start()
|
|
267
|
+
self.addCleanup(patcher.stop)
|
|
268
|
+
|
|
269
|
+
def test_one_multiplexed_connection_with_keepalives(self):
|
|
270
|
+
self.assertTrue(self.node.control_dir.startswith("/tmp/djaploy-ssh-"))
|
|
271
|
+
self.assertIn(f"ControlPath={self.node.control_dir}/%C", self.node.opts)
|
|
272
|
+
for opt in ("ControlMaster=auto", "ServerAliveInterval=15", "ConnectTimeout=20"):
|
|
273
|
+
self.assertIn(opt, self.node.opts)
|
|
274
|
+
|
|
275
|
+
def test_connection_failure_is_retried_and_input_rewound(self):
|
|
276
|
+
failures = [subprocess.CalledProcessError(255, "ssh")]
|
|
277
|
+
seen = []
|
|
278
|
+
|
|
279
|
+
def fake_run(cmd, **kwargs):
|
|
280
|
+
seen.append(kwargs["stdin"].read())
|
|
281
|
+
if failures:
|
|
282
|
+
raise failures.pop()
|
|
283
|
+
return subprocess.CompletedProcess(cmd, 0)
|
|
284
|
+
|
|
285
|
+
with tempfile.TemporaryFile() as archive, patch.object(gitops, "_run", side_effect=fake_run):
|
|
286
|
+
archive.write(b"source")
|
|
287
|
+
archive.seek(0)
|
|
288
|
+
self.node.run("tar -xzf -", stdin=archive)
|
|
289
|
+
self.assertEqual(seen, [b"source", b"source"])
|
|
290
|
+
|
|
291
|
+
def test_command_failure_is_not_retried(self):
|
|
292
|
+
with patch.object(gitops, "_run", side_effect=subprocess.CalledProcessError(1, "ssh")) as run:
|
|
293
|
+
with self.assertRaises(subprocess.CalledProcessError):
|
|
294
|
+
self.node.run("false")
|
|
295
|
+
self.assertEqual(run.call_count, 1)
|
|
296
|
+
|
|
297
|
+
def test_gives_up_after_retries(self):
|
|
298
|
+
with patch.object(gitops, "_run", side_effect=subprocess.CalledProcessError(255, "ssh")) as run:
|
|
299
|
+
with self.assertRaises(subprocess.CalledProcessError):
|
|
300
|
+
self.node.run("true")
|
|
301
|
+
self.assertEqual(run.call_count, gitops.BuildNode.RETRIES)
|
|
302
|
+
|
|
303
|
+
def test_close_stops_master_and_removes_socket_dir(self):
|
|
304
|
+
with patch.object(gitops, "_run") as run:
|
|
305
|
+
self.node.close()
|
|
306
|
+
self.assertIn("-O", run.call_args[0][0])
|
|
307
|
+
self.assertFalse(Path(self.node.control_dir).exists())
|
|
253
308
|
|
|
254
309
|
|
|
255
310
|
def git(cwd, *args):
|
|
@@ -310,6 +365,7 @@ class HookTests(unittest.TestCase):
|
|
|
310
365
|
context = self.context([("extor", host())])
|
|
311
366
|
gitops._gitops_deploy(context)
|
|
312
367
|
self.assertEqual(calls, ["check", "build", "secret", "pin", "sync"])
|
|
368
|
+
self.assertTrue(context["skip_remote"])
|
|
313
369
|
self.assertEqual(context["gitops"], {"image": f"{IMAGE}@{NEW}", "revision": "f" * 40})
|
|
314
370
|
|
|
315
371
|
def test_build_failure_publishes_nothing(self):
|
|
@@ -326,6 +382,14 @@ class HookTests(unittest.TestCase):
|
|
|
326
382
|
gitops._gitops_deploy(self.context([("web", host(deployment_strategy="zero_downtime"))]))
|
|
327
383
|
check.assert_not_called()
|
|
328
384
|
|
|
385
|
+
def test_mixed_inventory_keeps_remote_stage(self):
|
|
386
|
+
with patch.object(gitops, "check_infra_repo", return_value="main"), \
|
|
387
|
+
patch.object(gitops, "build_and_push", return_value=NEW), patch.object(gitops, "apply_env_secret"), \
|
|
388
|
+
patch.object(gitops, "publish_pin", return_value="f" * 40), patch.object(gitops, "sync_and_wait"):
|
|
389
|
+
context = self.context([("extor", host()), ("web", host(deployment_strategy="zero_downtime"))])
|
|
390
|
+
gitops._gitops_deploy(context)
|
|
391
|
+
self.assertNotIn("skip_remote", context)
|
|
392
|
+
|
|
329
393
|
def test_one_host_only(self):
|
|
330
394
|
with self.assertRaisesRegex(ValueError, "one host"):
|
|
331
395
|
gitops._gitops_deploy(self.context([("a", host()), ("b", host())]))
|
|
@@ -333,3 +397,25 @@ class HookTests(unittest.TestCase):
|
|
|
333
397
|
|
|
334
398
|
if __name__ == "__main__":
|
|
335
399
|
unittest.main()
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class RunCommandSkipTests(unittest.TestCase):
|
|
403
|
+
def test_skip_remote_runs_no_pyinfra_but_postcommand(self):
|
|
404
|
+
from djaploy import deploy
|
|
405
|
+
called = []
|
|
406
|
+
|
|
407
|
+
def fake_call_hook(name, context):
|
|
408
|
+
called.append(name)
|
|
409
|
+
if name == "deploy:precommand":
|
|
410
|
+
context["skip_remote"] = "gitops deploys from this machine"
|
|
411
|
+
return []
|
|
412
|
+
|
|
413
|
+
context = {"command": "deploy", "env": "dev", "command_file": "x", "inventory_file": "inv.py",
|
|
414
|
+
"pyinfra_data": {}}
|
|
415
|
+
with patch("djaploy.hooks.discover_hooks"), patch("djaploy.hooks.call_hook", side_effect=fake_call_hook), \
|
|
416
|
+
patch.object(deploy, "_run_pyinfra") as pyinfra, patch.object(deploy, "_preprocess_inventory") as pre:
|
|
417
|
+
deploy.run_command(context)
|
|
418
|
+
pyinfra.assert_not_called()
|
|
419
|
+
pre.assert_not_called()
|
|
420
|
+
self.assertTrue(context["success"])
|
|
421
|
+
self.assertEqual(called, ["deploy:precommand", "precommand", "deploy:postcommand", "postcommand"])
|
djaploy-1.6.1/djaploy/version.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.6.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|