socaity-cli 0.1.3__tar.gz → 0.1.4__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.
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/PKG-INFO +1 -1
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/pyproject.toml +1 -1
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/deployment.py +13 -3
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/registry_push.py +15 -1
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli.egg-info/PKG-INFO +1 -1
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli.egg-info/SOURCES.txt +2 -1
- socaity_cli-0.1.4/test/test_registry_push.py +12 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/README.md +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/setup.cfg +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/__init__.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/__main__.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/apipod_commands.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/auth.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/backend_client.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/cli.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/clients/__init__.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/clients/analytics.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/clients/catalog.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/clients/deployment.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/clients/files.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/clients/jobs.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/clients/projects.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/clients/sdk_install.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/credentials.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/errors.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/http.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/prompts.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/requires.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli/sdk_commands.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli.egg-info/dependency_links.txt +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli.egg-info/entry_points.txt +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli.egg-info/requires.txt +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/socaity_cli.egg-info/top_level.txt +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/test/test_cli.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/test/test_credentials.py +0 -0
- {socaity_cli-0.1.3 → socaity_cli-0.1.4}/test/test_private_slot_error.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "socaity-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "Socaity platform CLI: login, catalog browsing, SDK installs and APIPod deployment commands."
|
|
9
9
|
requires-python = ">=3.8"
|
|
10
10
|
authors = [
|
|
@@ -17,6 +17,7 @@ from socaity_cli.registry_push import (
|
|
|
17
17
|
DockerNotAvailable,
|
|
18
18
|
PushFailed,
|
|
19
19
|
image_size_bytes,
|
|
20
|
+
is_transient_push_error,
|
|
20
21
|
push_image,
|
|
21
22
|
)
|
|
22
23
|
|
|
@@ -160,7 +161,9 @@ def push_image_to_registry(
|
|
|
160
161
|
print("Could not obtain registry push credentials.")
|
|
161
162
|
return None
|
|
162
163
|
|
|
163
|
-
|
|
164
|
+
refreshed = False
|
|
165
|
+
transient_attempts = 0
|
|
166
|
+
while True:
|
|
164
167
|
try:
|
|
165
168
|
digest, _size = push_image(
|
|
166
169
|
registry=creds["registry"],
|
|
@@ -172,12 +175,13 @@ def push_image_to_registry(
|
|
|
172
175
|
)
|
|
173
176
|
return digest
|
|
174
177
|
except CredentialsExpired:
|
|
175
|
-
if
|
|
178
|
+
if not refreshed:
|
|
176
179
|
print("Push credentials expired; requesting fresh credentials...")
|
|
177
180
|
creds = client.get_push_credentials(deployment_id, estimated_size_bytes=estimated_size)
|
|
178
181
|
if creds is None:
|
|
179
182
|
print("Could not refresh registry push credentials.")
|
|
180
183
|
return None
|
|
184
|
+
refreshed = True
|
|
181
185
|
continue
|
|
182
186
|
print("Registry rejected the push credentials again.")
|
|
183
187
|
return None
|
|
@@ -185,9 +189,15 @@ def push_image_to_registry(
|
|
|
185
189
|
print(str(exc))
|
|
186
190
|
return None
|
|
187
191
|
except PushFailed as exc:
|
|
192
|
+
if is_transient_push_error(str(exc)) and transient_attempts < 2:
|
|
193
|
+
transient_attempts += 1
|
|
194
|
+
print(
|
|
195
|
+
f"Push interrupted ({exc}); retrying the same repository "
|
|
196
|
+
f"({transient_attempts}/3)..."
|
|
197
|
+
)
|
|
198
|
+
continue
|
|
188
199
|
print(str(exc))
|
|
189
200
|
return None
|
|
190
|
-
return None
|
|
191
201
|
|
|
192
202
|
|
|
193
203
|
def poll_deployment_until_terminal(
|
|
@@ -13,6 +13,14 @@ import tempfile
|
|
|
13
13
|
from typing import Optional, Tuple
|
|
14
14
|
|
|
15
15
|
_DIGEST_RE = re.compile(r"digest:\s*(sha256:[0-9a-f]{64})")
|
|
16
|
+
_TRANSIENT_PUSH_MARKERS = (
|
|
17
|
+
"timeout",
|
|
18
|
+
"eof",
|
|
19
|
+
"disconnected",
|
|
20
|
+
"connection reset",
|
|
21
|
+
"tls handshake",
|
|
22
|
+
"unexpected eof",
|
|
23
|
+
)
|
|
16
24
|
|
|
17
25
|
|
|
18
26
|
class DockerNotAvailable(RuntimeError):
|
|
@@ -27,6 +35,12 @@ class PushFailed(RuntimeError):
|
|
|
27
35
|
"""Any other push failure; message is safe to print."""
|
|
28
36
|
|
|
29
37
|
|
|
38
|
+
def is_transient_push_error(message: str) -> bool:
|
|
39
|
+
"""True when a docker push failed due to a drop or header timeout, not auth."""
|
|
40
|
+
lower = (message or "").lower()
|
|
41
|
+
return any(marker in lower for marker in _TRANSIENT_PUSH_MARKERS)
|
|
42
|
+
|
|
43
|
+
|
|
30
44
|
def _docker(args: list, *, env: Optional[dict] = None, input_bytes: Optional[bytes] = None) -> subprocess.CompletedProcess:
|
|
31
45
|
try:
|
|
32
46
|
return subprocess.run(["docker", *args], capture_output=True, env=env, input=input_bytes)
|
|
@@ -82,7 +96,7 @@ def push_image(
|
|
|
82
96
|
raise PushFailed(f"docker login failed: {stderr[:300]}")
|
|
83
97
|
|
|
84
98
|
push = subprocess.Popen(
|
|
85
|
-
["docker", "push", ref],
|
|
99
|
+
["docker", "push", "--max-concurrent-uploads=1", ref],
|
|
86
100
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env, text=True,
|
|
87
101
|
)
|
|
88
102
|
digest, last_lines = None, []
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from socaity_cli.registry_push import is_transient_push_error
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_is_transient_push_error_matches_harbor_timeouts():
|
|
5
|
+
assert is_transient_push_error(
|
|
6
|
+
'docker push failed: timeout awaiting response headers'
|
|
7
|
+
)
|
|
8
|
+
assert is_transient_push_error("client disconnected during blob PUT")
|
|
9
|
+
assert is_transient_push_error("unexpected EOF")
|
|
10
|
+
assert is_transient_push_error("connection reset by peer")
|
|
11
|
+
assert not is_transient_push_error("denied: requested access to the resource is denied")
|
|
12
|
+
assert not is_transient_push_error("")
|
|
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
|