arcsecond 4.3.0__tar.gz → 4.3.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.
- {arcsecond-4.3.0 → arcsecond-4.3.2}/PKG-INFO +1 -1
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/hosting/stack.py +28 -2
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/hosting/token.py +58 -10
- {arcsecond-4.3.0 → arcsecond-4.3.2}/pyproject.toml +1 -1
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_hosting_token.py +108 -1
- {arcsecond-4.3.0 → arcsecond-4.3.2}/uv.lock +1 -1
- {arcsecond-4.3.0 → arcsecond-4.3.2}/.docker/Dockerfile_postgres +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/.docker/Dockerfile_redis +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/.flake8 +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/.gitattributes +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/.github/dependabot.yml +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/.github/workflows/pythonpublish.yml +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/.github/workflows/tests.yml +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/.gitignore +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/LICENSE +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/Makefile +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/README.md +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/__init__.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/__version__.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/alpaca/__init__.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/alpaca/commands.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/alpaca/dome_probe.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/alpaca/probe.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/alpaca/telescope_probe.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/api/__init__.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/api/config.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/api/constants.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/api/endpoint.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/api/main.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/api/resources.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/cli.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/cloud/__init__.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/cloud/auth.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/cloud/resources.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/cloud/uploader/__init__.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/cloud/uploader/constants.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/cloud/uploader/context.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/cloud/uploader/datafiles/__init__.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/cloud/uploader/datafiles/context.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/cloud/uploader/datafiles/errors.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/cloud/uploader/datafiles/uploader.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/cloud/uploader/datafiles/utils.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/cloud/uploader/errors.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/cloud/uploader/logger.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/cloud/uploader/uploader.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/cloud/uploader/utils.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/cloud/uploader/walker.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/cloud/uploads.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/docgen.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/errors.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/hosting/__init__.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/hosting/backups.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/hosting/check.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/hosting/composedoc.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/hosting/database.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/hosting/docker/__init__.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/hosting/docker/docker-compose.yml +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/hosting/lifecycle.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/hosting/local.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/hosting/utils.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/imagesources/__init__.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/imagesources/autostart.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/imagesources/commands.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/imagesources/detection.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/imagesources/proxy.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/imagesources/registry.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/imagesources/runtime.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/imagesources/sources/__init__.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/imagesources/sources/base.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/imagesources/sources/filewatch.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/imagesources/sources/mdns.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/imagesources/sources/network.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/imagesources/sources/opencv.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/imagesources/store.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/options.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/arcsecond/targets.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/examples/example_upload_files.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/requirements.txt +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/setup.cfg +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/setup.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/sonar-project.properties +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/__init__.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/api/__init__.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/api/test_api.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/api/test_api_endpoint.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/api/test_config.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/api/test_config_pointer.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/api/test_targets.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/cloud/__init__.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/cloud/uploader/__init__.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/cloud/uploader/datafiles/__init__.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/cloud/uploader/datafiles/test_uploader_errors.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/cloud/uploader/datafiles/test_uploader_full_process.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/cloud/uploader/datafiles/test_uploader_init.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/cloud/uploader/datafiles/test_uploader_prepare.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/cloud/uploader/datafiles/test_uploader_upload.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/conftest.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/fixtures/file1.fits +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_alpaca_probe.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_cli.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_cli_api_commands.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_docgen.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_hosting_backups_install_dir.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_hosting_backups_restore.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_hosting_check.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_hosting_composedoc.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_hosting_database.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_hosting_lifecycle.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_hosting_local.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_hosting_stack.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_hosting_utils_secret_key.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_imagesources_autostart.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_imagesources_commands.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_imagesources_detection.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_imagesources_mdns.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_imagesources_network.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_imagesources_proxy.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_imagesources_store.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_imagesources_webcam.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/test_targets_planning.py +0 -0
- {arcsecond-4.3.0 → arcsecond-4.3.2}/tests/utils.py +0 -0
|
@@ -229,6 +229,22 @@ def compose_streaming(
|
|
|
229
229
|
)
|
|
230
230
|
|
|
231
231
|
|
|
232
|
+
REGISTRY_REFUSED = (
|
|
233
|
+
"Docker could not download an Arcsecond image: the registry refused.\n"
|
|
234
|
+
"Enter the token Arcsecond gave your observatory: arcsecond token set"
|
|
235
|
+
)
|
|
236
|
+
REGISTRY_REFUSED_NO_TOKEN = (
|
|
237
|
+
"Docker could not download an Arcsecond image: this machine has no token "
|
|
238
|
+
"saved, so it asked without one.\n"
|
|
239
|
+
"Enter the token Arcsecond gave your observatory: arcsecond token set\n"
|
|
240
|
+
'It must answer "Token accepted." before Arcsecond.local can be downloaded.'
|
|
241
|
+
)
|
|
242
|
+
REGISTRY_REFUSED_WITH_TOKEN = (
|
|
243
|
+
"Docker could not download an Arcsecond image: the token this machine holds "
|
|
244
|
+
"was refused. It may have expired or been replaced — enter the current one "
|
|
245
|
+
"with arcsecond token set, or ask team@arcsecond.io for a new one."
|
|
246
|
+
)
|
|
247
|
+
|
|
232
248
|
# The compose failures the troubleshooting page used to explain, matched on
|
|
233
249
|
# the daemon's own words so `start` can answer them on the spot.
|
|
234
250
|
_KNOWN_FAILURES = (
|
|
@@ -267,8 +283,7 @@ _KNOWN_FAILURES = (
|
|
|
267
283
|
),
|
|
268
284
|
(
|
|
269
285
|
("denied", "unauthorized", "pull access denied"),
|
|
270
|
-
|
|
271
|
-
"Enter the token Arcsecond gave your observatory: arcsecond token set",
|
|
286
|
+
REGISTRY_REFUSED,
|
|
272
287
|
),
|
|
273
288
|
)
|
|
274
289
|
|
|
@@ -284,6 +299,17 @@ def explain_compose_failure(stderr: str) -> Optional[str]:
|
|
|
284
299
|
|
|
285
300
|
def raise_compose_failure(what: str, result: subprocess.CompletedProcess) -> None:
|
|
286
301
|
explanation = explain_compose_failure(result.stderr)
|
|
302
|
+
if explanation == REGISTRY_REFUSED:
|
|
303
|
+
# "Unauthorized" reads the same whether the token is wrong or was never
|
|
304
|
+
# saved (a password store that could not write it, a first `start`
|
|
305
|
+
# before `token set`); the two call for different words.
|
|
306
|
+
from . import token # token imports this module
|
|
307
|
+
|
|
308
|
+
explanation = (
|
|
309
|
+
REGISTRY_REFUSED_WITH_TOKEN
|
|
310
|
+
if token.has_token()
|
|
311
|
+
else REGISTRY_REFUSED_NO_TOKEN
|
|
312
|
+
)
|
|
287
313
|
lines = [f"`{what}` failed (exit code {result.returncode})."]
|
|
288
314
|
if explanation:
|
|
289
315
|
lines += ["", explanation]
|
|
@@ -4,7 +4,8 @@ From the operator's side there is one fact: Arcsecond sent a token, and it
|
|
|
4
4
|
has to be entered once so the machine can download Arcsecond.local. What it
|
|
5
5
|
is underneath — a login to the container registry the images are pulled
|
|
6
6
|
from, ghcr.io, as the organisation user — is this module's business, not
|
|
7
|
-
theirs, and the words "registry" and "docker" stay out of what they read
|
|
7
|
+
theirs, and the words "registry" and "docker" stay out of what they read —
|
|
8
|
+
except when Docker's own password store is what failed, which only they can fix.
|
|
8
9
|
|
|
9
10
|
The token is asked for without echo, or read from standard input for a
|
|
10
11
|
script, and handed to Docker over a pipe. There is deliberately no
|
|
@@ -52,21 +53,23 @@ def docker_config_path() -> Path:
|
|
|
52
53
|
def has_token() -> bool:
|
|
53
54
|
"""Whether Docker on this machine holds a credential for the images.
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
When the config names a credential store (Docker Desktop's, `pass`, the OS
|
|
57
|
+
keychain), the store is the truth and is asked: the empty entry Docker
|
|
58
|
+
leaves under `auths` survives a store being emptied or replaced, and a
|
|
59
|
+
machine whose `pass` was never initialised can carry one from an older
|
|
60
|
+
login. Without a store, the secret sits inline under `auths`. Only when
|
|
61
|
+
the store cannot answer — its helper missing, or waiting on a passphrase
|
|
62
|
+
prompt nobody sees — does the entry's presence stand in for it.
|
|
59
63
|
"""
|
|
60
64
|
try:
|
|
61
65
|
config = json.loads(docker_config_path().read_text(encoding="utf-8") or "{}")
|
|
62
66
|
except (OSError, ValueError):
|
|
63
67
|
return False
|
|
64
68
|
auths = config.get("auths") or {}
|
|
65
|
-
|
|
66
|
-
return True
|
|
69
|
+
entries = [value for key, value in auths.items() if REGISTRY in key]
|
|
67
70
|
store = (config.get("credHelpers") or {}).get(REGISTRY) or config.get("credsStore")
|
|
68
71
|
if not store:
|
|
69
|
-
return
|
|
72
|
+
return any(isinstance(entry, dict) and entry.get("auth") for entry in entries)
|
|
70
73
|
try:
|
|
71
74
|
probe = subprocess.run(
|
|
72
75
|
[f"docker-credential-{store}", "get"],
|
|
@@ -77,8 +80,13 @@ def has_token() -> bool:
|
|
|
77
80
|
timeout=15,
|
|
78
81
|
)
|
|
79
82
|
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
|
83
|
+
return bool(entries)
|
|
84
|
+
if probe.returncode != 0:
|
|
85
|
+
return False
|
|
86
|
+
try:
|
|
87
|
+
return bool(json.loads(probe.stdout or "{}").get("Secret"))
|
|
88
|
+
except ValueError:
|
|
80
89
|
return False
|
|
81
|
-
return probe.returncode == 0 and REGISTRY_USER in (probe.stdout or "")
|
|
82
90
|
|
|
83
91
|
|
|
84
92
|
def store_token(token: str) -> None:
|
|
@@ -96,7 +104,10 @@ def store_token(token: str) -> None:
|
|
|
96
104
|
token=token + "\n",
|
|
97
105
|
)
|
|
98
106
|
if result.returncode != 0:
|
|
99
|
-
|
|
107
|
+
output = (result.stderr or result.stdout or "").strip()
|
|
108
|
+
if _is_credential_store_failure(output):
|
|
109
|
+
raise ArcsecondError(_credential_store_advice(output))
|
|
110
|
+
detail = output.splitlines()
|
|
100
111
|
reason = detail[-1] if detail else "Docker gave no reason"
|
|
101
112
|
raise ArcsecondError(
|
|
102
113
|
f"The token was refused: {reason}\n"
|
|
@@ -105,6 +116,43 @@ def store_token(token: str) -> None:
|
|
|
105
116
|
)
|
|
106
117
|
|
|
107
118
|
|
|
119
|
+
# Docker only saves the credential once the registry has accepted it, so these
|
|
120
|
+
# words mean the token was good and the machine's own keychain failed. Docker
|
|
121
|
+
# Desktop on Linux keeps credentials in `pass`, which is empty until someone
|
|
122
|
+
# runs `pass init` — it says so in its install notes, and nobody reads them.
|
|
123
|
+
_CREDENTIAL_STORE_FAILURES = ("error saving credentials", "error storing credentials")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _is_credential_store_failure(output: str) -> bool:
|
|
127
|
+
lowered = output.lower()
|
|
128
|
+
return any(words in lowered for words in _CREDENTIAL_STORE_FAILURES)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _credential_store_advice(output: str) -> str:
|
|
132
|
+
lowered = output.lower()
|
|
133
|
+
head = (
|
|
134
|
+
"The token is good — Arcsecond accepted it — but this machine could not "
|
|
135
|
+
"save it: Docker keeps it in a password store that refused to write.\n"
|
|
136
|
+
)
|
|
137
|
+
if "pass init" in lowered or "pass not initialized" in lowered:
|
|
138
|
+
return head + (
|
|
139
|
+
"Docker Desktop on Linux uses `pass`, and it has not been set up yet. "
|
|
140
|
+
"Set it up once, then run `arcsecond token set` again:\n"
|
|
141
|
+
" gpg --generate-key (any name and e-mail; note the key id it prints)\n"
|
|
142
|
+
" pass init <that key id>\n"
|
|
143
|
+
"Or, if this machine runs Docker Engine without Docker Desktop, remove the "
|
|
144
|
+
f'"credsStore" line from {docker_config_path()} and Docker will keep the '
|
|
145
|
+
"token in that file instead."
|
|
146
|
+
)
|
|
147
|
+
detail = output.splitlines()[-1] if output else ""
|
|
148
|
+
return head + (
|
|
149
|
+
f"It said: {detail}\n"
|
|
150
|
+
'Unlock or repair that store, or remove the "credsStore" line from '
|
|
151
|
+
f"{docker_config_path()} so Docker keeps the token in that file, then run "
|
|
152
|
+
"`arcsecond token set` again."
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
108
156
|
def prompt_for_token() -> str:
|
|
109
157
|
return click.prompt(
|
|
110
158
|
"Access token from Arcsecond (nothing is shown while you type)", hide_input=True
|
|
@@ -7,6 +7,7 @@ import pytest
|
|
|
7
7
|
from click.testing import CliRunner
|
|
8
8
|
|
|
9
9
|
from arcsecond import cli
|
|
10
|
+
from arcsecond.errors import ArcsecondError
|
|
10
11
|
from arcsecond.hosting import local, stack, token
|
|
11
12
|
|
|
12
13
|
|
|
@@ -65,6 +66,42 @@ def test_a_refused_token_is_explained_without_docker_words(docker):
|
|
|
65
66
|
assert "registry" not in result.output.lower()
|
|
66
67
|
|
|
67
68
|
|
|
69
|
+
def _login_fails_with(monkeypatch, stderr):
|
|
70
|
+
def fake_run(cmd, input=None, **kwargs):
|
|
71
|
+
return subprocess.CompletedProcess(cmd, 1, stdout="", stderr=stderr)
|
|
72
|
+
|
|
73
|
+
monkeypatch.setattr(token.subprocess, "run", fake_run)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_an_uninitialised_pass_store_is_not_a_refused_token(docker, monkeypatch):
|
|
77
|
+
_login_fails_with(
|
|
78
|
+
monkeypatch,
|
|
79
|
+
"Error saving credentials: error storing credentials - err: exit status 1, "
|
|
80
|
+
'out: `pass not initialized: exit status 1: Error: password store is empty. Try "pass init".`',
|
|
81
|
+
)
|
|
82
|
+
result = _invoke("token", "set", input="ghp_good\n")
|
|
83
|
+
assert result.exit_code == 1
|
|
84
|
+
assert (
|
|
85
|
+
"token is good" in result.output
|
|
86
|
+
and "refused" not in result.output.split("\n")[0]
|
|
87
|
+
)
|
|
88
|
+
assert "pass init" in result.output and "gpg --generate-key" in result.output
|
|
89
|
+
assert "team@arcsecond.io" not in result.output
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_any_other_credential_store_failure_names_the_config(
|
|
93
|
+
docker, monkeypatch, tmp_path
|
|
94
|
+
):
|
|
95
|
+
_login_fails_with(
|
|
96
|
+
monkeypatch,
|
|
97
|
+
"Error saving credentials: error storing credentials - err: exit status 1, out: `keychain locked`",
|
|
98
|
+
)
|
|
99
|
+
result = _invoke("token", "set", input="ghp_good\n")
|
|
100
|
+
assert result.exit_code == 1
|
|
101
|
+
assert "token is good" in result.output and "keychain locked" in result.output
|
|
102
|
+
assert "credsStore" in result.output and "gpg" not in result.output
|
|
103
|
+
|
|
104
|
+
|
|
68
105
|
def test_chevrons_and_empty_input_never_reach_docker(docker):
|
|
69
106
|
assert "placeholder" in _invoke("token", "set", input="<ghp_good>\n").output
|
|
70
107
|
assert _invoke("token", "set", "--stdin", input="\n").exit_code == 1
|
|
@@ -81,12 +118,61 @@ def test_status_reads_dockers_config(docker, tmp_path):
|
|
|
81
118
|
result = _invoke("token")
|
|
82
119
|
assert result.exit_code == 1 and "no token yet" in result.output
|
|
83
120
|
(tmp_path / "config.json").write_text(
|
|
84
|
-
json.dumps({"auths": {"ghcr.io": {
|
|
121
|
+
json.dumps({"auths": {"ghcr.io": {"auth": "YXJjc2Vjb25kLWlvOng="}}})
|
|
85
122
|
)
|
|
86
123
|
result = _invoke("token")
|
|
87
124
|
assert result.exit_code == 0 and "has the token" in result.output
|
|
88
125
|
|
|
89
126
|
|
|
127
|
+
def _store_answers(monkeypatch, tmp_path, config, returncode, stdout=""):
|
|
128
|
+
monkeypatch.setattr(token, "docker_config_path", lambda: tmp_path / "config.json")
|
|
129
|
+
(tmp_path / "config.json").write_text(json.dumps(config))
|
|
130
|
+
|
|
131
|
+
def fake_run(cmd, input=None, **kwargs):
|
|
132
|
+
return subprocess.CompletedProcess(cmd, returncode, stdout=stdout)
|
|
133
|
+
|
|
134
|
+
monkeypatch.setattr(token.subprocess, "run", fake_run)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_an_empty_entry_is_not_a_token_when_the_store_holds_nothing(
|
|
138
|
+
monkeypatch, tmp_path
|
|
139
|
+
):
|
|
140
|
+
# JB's machine: `pass` never initialised, an `auths` entry left behind.
|
|
141
|
+
_store_answers(
|
|
142
|
+
monkeypatch,
|
|
143
|
+
tmp_path,
|
|
144
|
+
{"auths": {"ghcr.io": {}}, "credsStore": "desktop"},
|
|
145
|
+
returncode=1,
|
|
146
|
+
stdout="credentials not found in native keychain",
|
|
147
|
+
)
|
|
148
|
+
assert token.has_token() is False
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_a_token_saved_under_another_username_still_counts(monkeypatch, tmp_path):
|
|
152
|
+
# Before 4.2 the documentation had people log in under their own name.
|
|
153
|
+
_store_answers(
|
|
154
|
+
monkeypatch,
|
|
155
|
+
tmp_path,
|
|
156
|
+
{"credsStore": "pass"},
|
|
157
|
+
returncode=0,
|
|
158
|
+
stdout=json.dumps({"Username": "jb", "Secret": "x"}),
|
|
159
|
+
)
|
|
160
|
+
assert token.has_token() is True
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_a_store_that_cannot_answer_falls_back_on_the_entry(monkeypatch, tmp_path):
|
|
164
|
+
monkeypatch.setattr(token, "docker_config_path", lambda: tmp_path / "config.json")
|
|
165
|
+
(tmp_path / "config.json").write_text(
|
|
166
|
+
json.dumps({"auths": {"ghcr.io": {}}, "credsStore": "pass"})
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def hangs(cmd, **kwargs):
|
|
170
|
+
raise subprocess.TimeoutExpired(cmd, 15)
|
|
171
|
+
|
|
172
|
+
monkeypatch.setattr(token.subprocess, "run", hangs)
|
|
173
|
+
assert token.has_token() is True
|
|
174
|
+
|
|
175
|
+
|
|
90
176
|
def test_has_token_asks_the_credential_store_when_the_config_lists_nothing(
|
|
91
177
|
monkeypatch, tmp_path
|
|
92
178
|
):
|
|
@@ -114,6 +200,27 @@ def test_start_points_at_the_token_when_the_pull_is_denied():
|
|
|
114
200
|
)
|
|
115
201
|
|
|
116
202
|
|
|
203
|
+
@pytest.mark.parametrize(
|
|
204
|
+
"saved, says, never",
|
|
205
|
+
[
|
|
206
|
+
(False, "no token saved", "expired"),
|
|
207
|
+
(True, "was refused", "no token saved"),
|
|
208
|
+
],
|
|
209
|
+
)
|
|
210
|
+
def test_start_tells_a_missing_token_from_a_refused_one(
|
|
211
|
+
monkeypatch, saved, says, never
|
|
212
|
+
):
|
|
213
|
+
monkeypatch.setattr(token, "has_token", lambda: saved)
|
|
214
|
+
result = subprocess.CompletedProcess(
|
|
215
|
+
[], 1, stderr="Error response from daemon: error from registry: unauthorized"
|
|
216
|
+
)
|
|
217
|
+
with pytest.raises(ArcsecondError) as excinfo:
|
|
218
|
+
stack.raise_compose_failure("docker compose up", result)
|
|
219
|
+
message = str(excinfo.value)
|
|
220
|
+
assert says in message and never not in message
|
|
221
|
+
assert "arcsecond token set" in message
|
|
222
|
+
|
|
223
|
+
|
|
117
224
|
# --- setup asks for it -----------------------------------------------------------
|
|
118
225
|
|
|
119
226
|
|
|
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
|
{arcsecond-4.3.0 → arcsecond-4.3.2}/tests/cloud/uploader/datafiles/test_uploader_full_process.py
RENAMED
|
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
|