gowalk-cicd 1.0.195 → 1.0.197
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.
|
@@ -50,7 +50,7 @@ def candidate(engine, old, body, name, staged, reservation):
|
|
|
50
50
|
current = engine.inspect(old['Id'])
|
|
51
51
|
unchanged(old, current)
|
|
52
52
|
if current['State']['Running']:
|
|
53
|
-
engine.
|
|
53
|
+
engine.stop(old['Id'])
|
|
54
54
|
replacement = engine.inspect(name, missing=True)
|
|
55
55
|
if replacement is None:
|
|
56
56
|
result = engine.request('POST', '/containers/create?name=' + quote(name, safe=''), body)
|
|
@@ -71,6 +71,8 @@ def confirmed(engine, method, path, proof):
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
def rollback(engine, old, name, reservation):
|
|
74
|
+
if engine.pending_stop is not None:
|
|
75
|
+
engine.stop(old['Id'])
|
|
74
76
|
replacement = engine.inspect(name, missing=True)
|
|
75
77
|
if replacement is not None and replacement['Id'] != old['Id']:
|
|
76
78
|
if (replacement['Config'].get('Labels', {}).get(MARKER) != old['Id']
|
|
@@ -13,14 +13,15 @@ SOCKET = '/var/run/docker.sock'
|
|
|
13
13
|
class Engine:
|
|
14
14
|
def __init__(self):
|
|
15
15
|
self.deadline = time.monotonic() + 90
|
|
16
|
+
self.pending_stop = None
|
|
16
17
|
|
|
17
|
-
def request(self, method, path, body=None, missing=False, raw=False):
|
|
18
|
+
def request(self, method, path, body=None, missing=False, raw=False, timeout=10):
|
|
18
19
|
remaining = self.deadline - time.monotonic()
|
|
19
20
|
if remaining <= 0:
|
|
20
21
|
raise ValueError('rollout_deadline')
|
|
21
|
-
connection = http.client.HTTPConnection('localhost', timeout=min(
|
|
22
|
+
connection = http.client.HTTPConnection('localhost', timeout=min(timeout, remaining))
|
|
22
23
|
connection.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
23
|
-
connection.sock.settimeout(min(
|
|
24
|
+
connection.sock.settimeout(min(timeout, remaining))
|
|
24
25
|
try:
|
|
25
26
|
connection.sock.connect(SOCKET)
|
|
26
27
|
data = None if body is None else json.dumps(body).encode()
|
|
@@ -38,6 +39,15 @@ class Engine:
|
|
|
38
39
|
def inspect(self, identifier, missing=False):
|
|
39
40
|
return self.request('GET', '/containers/' + quote(identifier, safe='') + '/json', missing=missing)
|
|
40
41
|
|
|
42
|
+
def stop(self, identifier):
|
|
43
|
+
# Docker continues an accepted stop after a lost HTTP reply. Keep that uncertainty
|
|
44
|
+
# until the same-ID stop completes and its stopped state is positively observed.
|
|
45
|
+
self.pending_stop = identifier
|
|
46
|
+
self.request('POST', '/containers/' + identifier + '/stop?t=10', timeout=20)
|
|
47
|
+
if self.inspect(identifier)['State']['Running']:
|
|
48
|
+
raise ValueError('container_stop_unconfirmed')
|
|
49
|
+
self.pending_stop = None
|
|
50
|
+
|
|
41
51
|
|
|
42
52
|
def unshared(engine, original):
|
|
43
53
|
identifiers = [row['Id'] for row in engine.request('GET', '/containers/json?all=1')]
|
|
@@ -67,9 +77,16 @@ def probe(engine, identifier, test):
|
|
|
67
77
|
created = engine.request('POST', '/containers/' + identifier + '/exec',
|
|
68
78
|
{'AttachStdout': False, 'AttachStderr': False, 'Cmd': command})
|
|
69
79
|
engine.request('POST', '/exec/' + created['Id'] + '/start', {'Detach': False, 'Tty': False}, raw=True)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
deadline = min(time.monotonic() + 5, engine.deadline)
|
|
81
|
+
while time.monotonic() < deadline:
|
|
82
|
+
result = engine.request('GET', '/exec/' + created['Id'] + '/json',
|
|
83
|
+
timeout=max(0.001, deadline - time.monotonic()))
|
|
84
|
+
if not result['Running']:
|
|
85
|
+
if result['ExitCode'] != 0:
|
|
86
|
+
raise ValueError('original_probe_failed')
|
|
87
|
+
return
|
|
88
|
+
time.sleep(min(0.1, max(0, deadline - time.monotonic())))
|
|
89
|
+
raise ValueError('original_probe_timeout')
|
|
73
90
|
|
|
74
91
|
|
|
75
92
|
def healthy(engine, identifier, deadline):
|
|
@@ -113,7 +113,12 @@ def unchanged(before, after, reservation_changes=False):
|
|
|
113
113
|
|
|
114
114
|
def verified(row, body, original):
|
|
115
115
|
config = {key: value for key, value in body.items() if key not in ('HostConfig', 'NetworkingConfig')}
|
|
116
|
-
|
|
116
|
+
hosts = [dict(value) for value in (row['HostConfig'], body['HostConfig'])]
|
|
117
|
+
# Docker reports the default OOM setting as null while running and false before start.
|
|
118
|
+
for host in hosts:
|
|
119
|
+
if host.get('OomKillDisable') is None:
|
|
120
|
+
host['OomKillDisable'] = False
|
|
121
|
+
if row['Config'] != config or row['Image'] != original['Image'] or hosts[0] != hosts[1]:
|
|
117
122
|
raise ValueError('replacement_config_changed')
|
|
118
123
|
actual = row['NetworkSettings']['Networks']
|
|
119
124
|
expected = body['NetworkingConfig']['EndpointsConfig']
|
|
@@ -5,6 +5,7 @@ import re
|
|
|
5
5
|
import subprocess
|
|
6
6
|
|
|
7
7
|
from runtime_healthcheck import bounded_tmpfs, cache_healthcheck
|
|
8
|
+
from runtime_model import escape_interpolation
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def eligible(service):
|
|
@@ -17,7 +18,14 @@ def eligible(service):
|
|
|
17
18
|
def image_health(command, name, service):
|
|
18
19
|
reference = service.get('image')
|
|
19
20
|
if not reference:
|
|
20
|
-
|
|
21
|
+
resolver = list(command)
|
|
22
|
+
option = next((flag for flag in ('--file', '-f') if flag in resolver), None)
|
|
23
|
+
if option:
|
|
24
|
+
resolver[resolver.index(option) + 1] = '-'
|
|
25
|
+
else:
|
|
26
|
+
resolver.extend(['--file', '-'])
|
|
27
|
+
selected = {'services': {name: {'build': '.'}}}
|
|
28
|
+
result = subprocess.run(resolver + ['config', '--images', name], input=json.dumps(selected),
|
|
21
29
|
capture_output=True, text=True, timeout=30, check=True)
|
|
22
30
|
references = result.stdout.split()
|
|
23
31
|
if len(references) != 1:
|
|
@@ -44,6 +52,7 @@ def inherit_healthchecks(model, command):
|
|
|
44
52
|
candidate = copy.deepcopy(service)
|
|
45
53
|
candidate.setdefault('healthcheck', {})['test'] = health['Test']
|
|
46
54
|
if cache_healthcheck(candidate):
|
|
55
|
+
candidate['healthcheck']['test'] = escape_interpolation(candidate['healthcheck']['test'])
|
|
47
56
|
service['healthcheck'] = candidate['healthcheck']
|
|
48
57
|
service.update(image=image['Id'], pull_policy='never')
|
|
49
58
|
changed = True
|
|
@@ -36,6 +36,10 @@ wraps only a missing Compose test, retaining explicit timing and disabled/custom
|
|
|
36
36
|
launch model pins the transformed service to that inspected image ID with pulling disabled, so a
|
|
37
37
|
moving tag cannot substitute another image's healthcheck. Explicit registry-refresh policies and
|
|
38
38
|
non-build services retain their existing image/pull behavior. No pre-build image healthcheck is reused.
|
|
39
|
+
Imported image commands escape literal dollars once at the Compose boundary, preserving Python
|
|
40
|
+
strings and container-shell expansion without interpreting them against the deployment host environment.
|
|
41
|
+
Default image names resolve from an isolated service model with the same Compose project name;
|
|
42
|
+
dependency images cannot make that selection ambiguous.
|
|
39
43
|
|
|
40
44
|
## Adopt an existing API
|
|
41
45
|
|
|
@@ -81,6 +85,12 @@ marker-bound replacement, restore/start the original and verify its original hea
|
|
|
81
85
|
by identity/state readback. `rollback_unconfirmed` means recovery needs another exact readback/replay;
|
|
82
86
|
retained containers and records stay available, and unrelated containers are never removed to clear a name.
|
|
83
87
|
|
|
88
|
+
Docker may report the default `OomKillDisable` as either null or false; these alone compare equivalently,
|
|
89
|
+
while an enabled OOM-kill override must match exactly. Stop requests retain their ten-second grace with
|
|
90
|
+
a twenty-second transport allowance inside the transaction budget. An uncertain stop must settle on
|
|
91
|
+
that exact container before rollback can restart it or certify its health. Original-probe verification
|
|
92
|
+
waits for the exec's confirmed exit status, for at most five seconds within the remaining transaction.
|
|
93
|
+
|
|
84
94
|
Replay with the same original ID resumes an interrupted retained-container transaction. After successful
|
|
85
95
|
cleanup it finds the marker-bound healthy replacement and returns `already_applied`. It refuses absent,
|
|
86
96
|
ambiguous or changed runtime evidence. Start with one canary, verify application health externally and
|