doover-cli 0.6.4__tar.gz → 0.6.6__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.
- {doover_cli-0.6.4 → doover_cli-0.6.6}/PKG-INFO +2 -2
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/__init__.py +1 -1
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/apps/apps.py +249 -0
- doover_cli-0.6.6/src/doover_cli/apps/migrate.py +956 -0
- doover_cli-0.6.6/src/doover_cli/apps/widget_plugin.py +182 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/channel.py +7 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/config_schema.py +13 -3
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/ui_schema.py +16 -7
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/utils/apps.py +102 -2
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/conftest.py +18 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_apps.py +372 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_discover.py +21 -0
- doover_cli-0.6.6/tests/test_migrate.py +137 -0
- doover_cli-0.6.6/tests/test_schema_export.py +174 -0
- doover_cli-0.6.6/tests/test_widget_plugin.py +70 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/.github/CONTRIBUTING.md +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/.github/ISSUE_TEMPLATE/feature-request.yml +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/.github/workflows/lint-test.yml +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/.github/workflows/release-brew.yml +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/.github/workflows/release-pypi.yml +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/.github/workflows/release.yml +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/.gitignore +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/.pre-commit-config.yaml +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/AGENT.md +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/LICENSE +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/README.md +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/debian/changelog +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/debian/control +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/debian/rules +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/docs/user_stories.md +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/pyproject.toml +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/agent.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/api/__init__.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/api/auth.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/api/errors.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/api/session.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/apps/app_install.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/apps/device.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/apps/device_type.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/apps/tunnel.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/colours.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/dda_logs.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/doover_config.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/grpc.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/login.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/registry.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/renderer/__init__.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/renderer/_base.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/renderer/_basic.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/renderer/_default.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/renderer/_json.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/report.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/simulator.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/user.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/utils/__init__.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/utils/api.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/utils/context.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/utils/crud/__init__.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/utils/crud/commands.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/utils/crud/lookup.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/utils/crud/prompting.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/utils/crud/schema.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/utils/crud/values.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/utils/errors.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/utils/formatters.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/utils/misc.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/utils/parsers.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/utils/prompt.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/utils/sentry.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/utils/shell_commands.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/src/doover_cli/utils/state.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/__init__.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_agent.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_app_install.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_auth_integration.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_auth_unit.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_basic.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_crud_commands.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_crud_lookup.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_crud_prompting.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_crud_schema.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_crud_values.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_default_renderer.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_device.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_device_cli_integration.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_device_type.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_device_type_cli_integration.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_prompt.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_registry.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_sentry.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_user.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/tests/test_utils_apps.py +0 -0
- {doover_cli-0.6.4 → doover_cli-0.6.6}/uv.lock +0 -0
|
@@ -49,6 +49,7 @@ from ..utils.shell_commands import run as shell_run
|
|
|
49
49
|
from ..utils.sentry import capture_handled_exception
|
|
50
50
|
from ..utils.state import state
|
|
51
51
|
from .app_install import application_app as installs_app
|
|
52
|
+
from .migrate import migrate as migrate_command
|
|
52
53
|
|
|
53
54
|
if TYPE_CHECKING:
|
|
54
55
|
from pydoover.api import ControlClient
|
|
@@ -67,6 +68,7 @@ app.add_typer(
|
|
|
67
68
|
name="installs",
|
|
68
69
|
help="Manage installations for an application.",
|
|
69
70
|
)
|
|
71
|
+
app.command(name="migrate")(migrate_command)
|
|
70
72
|
|
|
71
73
|
|
|
72
74
|
class AppType(Enum):
|
|
@@ -194,6 +196,150 @@ def _get_image_digest(image_name: str) -> str | None:
|
|
|
194
196
|
return None
|
|
195
197
|
|
|
196
198
|
|
|
199
|
+
def _repository_of(image_name: str) -> str:
|
|
200
|
+
"""`image_name` without its tag, so a digest can be appended to it."""
|
|
201
|
+
repository, sep, tail = image_name.rpartition(":")
|
|
202
|
+
# A colon in the last path segment is a tag; one before a `/` is a port.
|
|
203
|
+
if sep and "/" not in tail:
|
|
204
|
+
return repository
|
|
205
|
+
return image_name
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# What the registry says when a manifest genuinely is not there, as opposed to
|
|
209
|
+
# the many other reasons a lookup can fail -- an expired token being the likely
|
|
210
|
+
# one, since the credential minted for a push is short-lived.
|
|
211
|
+
_NOT_FOUND_MARKERS = ("not found", "manifest unknown", "manifest_unknown")
|
|
212
|
+
|
|
213
|
+
PRESENT, MISSING, UNKNOWN = "present", "missing", "unknown"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _read_manifest(ref: str) -> tuple[str, dict | None]:
|
|
217
|
+
"""Look `ref` up in the registry: (PRESENT | MISSING | UNKNOWN, manifest).
|
|
218
|
+
|
|
219
|
+
Goes through `imagetools`, not a direct registry request, so it reuses
|
|
220
|
+
whatever `docker login` already established -- including credential helpers,
|
|
221
|
+
which is what the doover registry uses.
|
|
222
|
+
|
|
223
|
+
MISSING and UNKNOWN have to be told apart: only the first is evidence of a
|
|
224
|
+
bad push. Reporting a lapsed credential as a missing manifest would fail
|
|
225
|
+
publishes for a reason that has nothing to do with the image.
|
|
226
|
+
"""
|
|
227
|
+
try:
|
|
228
|
+
result = subprocess.run(
|
|
229
|
+
["docker", "buildx", "imagetools", "inspect", "--raw", ref],
|
|
230
|
+
capture_output=True,
|
|
231
|
+
text=True,
|
|
232
|
+
check=False,
|
|
233
|
+
)
|
|
234
|
+
except OSError:
|
|
235
|
+
return UNKNOWN, None
|
|
236
|
+
|
|
237
|
+
if result.returncode != 0:
|
|
238
|
+
stderr = (result.stderr or "").lower()
|
|
239
|
+
if any(marker in stderr for marker in _NOT_FOUND_MARKERS):
|
|
240
|
+
return MISSING, None
|
|
241
|
+
return UNKNOWN, None
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
return PRESENT, json.loads(result.stdout)
|
|
245
|
+
except ValueError:
|
|
246
|
+
return UNKNOWN, None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _is_attestation(child: dict) -> bool:
|
|
250
|
+
"""Whether an index entry is buildx provenance rather than a runnable image.
|
|
251
|
+
|
|
252
|
+
buildx records attestations as extra children with an `unknown/unknown`
|
|
253
|
+
platform, which is how a pull knows to skip them.
|
|
254
|
+
"""
|
|
255
|
+
if child.get("annotations", {}).get("vnd.docker.reference.type"):
|
|
256
|
+
return True
|
|
257
|
+
platform = child.get("platform") or {}
|
|
258
|
+
return platform.get("architecture") == "unknown" and platform.get("os") == "unknown"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
_INDEX_MEDIA_TYPES = frozenset(
|
|
262
|
+
{
|
|
263
|
+
"application/vnd.oci.image.index.v1+json",
|
|
264
|
+
"application/vnd.docker.distribution.manifest.list.v2+json",
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _verify_pushed_image(image_name: str, digest: str | None) -> None:
|
|
270
|
+
"""Fail unless the registry can serve every manifest the push referenced.
|
|
271
|
+
|
|
272
|
+
A multi-platform push uploads the per-platform manifests and then the index
|
|
273
|
+
that points at them. If a registry keeps the index but not its children, the
|
|
274
|
+
tag still resolves for whoever pushed it -- their build cache has the
|
|
275
|
+
children -- and every device fails to pull it with "manifest unknown". Docker
|
|
276
|
+
does not report that as a failed push, and a release would then pin a digest
|
|
277
|
+
nothing can pull, so the check belongs here: at publish time, where it is
|
|
278
|
+
still one command to re-run.
|
|
279
|
+
"""
|
|
280
|
+
reference = f"{_repository_of(image_name)}@{digest}" if digest else image_name
|
|
281
|
+
|
|
282
|
+
state_, index = _read_manifest(reference)
|
|
283
|
+
if state_ == MISSING:
|
|
284
|
+
rich.print(
|
|
285
|
+
f"[bold red]Pushed image {reference} is not in the registry.[/bold red] "
|
|
286
|
+
"The push reported success, so this is a registry-side rejection "
|
|
287
|
+
"rather than a build failure."
|
|
288
|
+
)
|
|
289
|
+
raise typer.Exit(1)
|
|
290
|
+
if state_ == UNKNOWN:
|
|
291
|
+
rich.print(
|
|
292
|
+
f"Warning: could not read {reference} back from the registry, so the "
|
|
293
|
+
"push was not verified."
|
|
294
|
+
)
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
if index.get("mediaType") not in _INDEX_MEDIA_TYPES:
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
repository = _repository_of(image_name)
|
|
301
|
+
missing = [
|
|
302
|
+
child
|
|
303
|
+
for child in index.get("manifests", [])
|
|
304
|
+
if child.get("digest")
|
|
305
|
+
and _read_manifest(f"{repository}@{child['digest']}")[0] == MISSING
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
# A missing attestation is not a broken image: a pull resolves the index by
|
|
309
|
+
# platform and never asks for the `unknown/unknown` entries buildx adds for
|
|
310
|
+
# provenance. Say so, but do not fail a publish over it.
|
|
311
|
+
runnable = [child for child in missing if not _is_attestation(child)]
|
|
312
|
+
if missing and not runnable:
|
|
313
|
+
rich.print(
|
|
314
|
+
f"Warning: {len(missing)} attestation manifest(s) referenced by "
|
|
315
|
+
f"{reference} are not in the registry. Pulls are unaffected."
|
|
316
|
+
)
|
|
317
|
+
return
|
|
318
|
+
if not runnable:
|
|
319
|
+
return
|
|
320
|
+
missing = runnable
|
|
321
|
+
|
|
322
|
+
rich.print(
|
|
323
|
+
f"[bold red]Incomplete push: the registry has the image index for "
|
|
324
|
+
f"{reference} but not {len(missing)} of the platform manifests it "
|
|
325
|
+
"references.[/bold red]\nAnything pulling this image will fail with "
|
|
326
|
+
"'manifest unknown'. Missing:"
|
|
327
|
+
)
|
|
328
|
+
for child in missing:
|
|
329
|
+
platform = child.get("platform") or {}
|
|
330
|
+
described = (
|
|
331
|
+
f"{platform.get('os', '?')}/{platform.get('architecture', '?')}"
|
|
332
|
+
if platform
|
|
333
|
+
else "no platform"
|
|
334
|
+
)
|
|
335
|
+
rich.print(f" {child['digest']} ({described})")
|
|
336
|
+
rich.print(
|
|
337
|
+
"The registry accepted the index but not everything it points at. "
|
|
338
|
+
"Nothing is released against this digest."
|
|
339
|
+
)
|
|
340
|
+
raise typer.Exit(1)
|
|
341
|
+
|
|
342
|
+
|
|
197
343
|
def _require_publish_value(
|
|
198
344
|
name: str, value, *, allow_empty_list: bool = False, allow_none: bool = False
|
|
199
345
|
):
|
|
@@ -1129,6 +1275,17 @@ def publish(
|
|
|
1129
1275
|
if publish_org_id is not None:
|
|
1130
1276
|
client.organisation_id = int(publish_org_id)
|
|
1131
1277
|
|
|
1278
|
+
# The deployment folder is published verbatim and is what a device pulls, so
|
|
1279
|
+
# an image pinned there wins over image_name in practice. Warn rather than
|
|
1280
|
+
# abort: a deliberate pin (an older tag, a fork) is legitimate, and this
|
|
1281
|
+
# can't tell one from a reference left behind by a registry change.
|
|
1282
|
+
for path, reference in app_config.stale_deployment_images():
|
|
1283
|
+
rich.print(
|
|
1284
|
+
f"[yellow]{path.name} deploys '{reference}', but this app is "
|
|
1285
|
+
f"registered as '{app_config.image_name}'. The deployment folder is "
|
|
1286
|
+
f"what the device pulls, so it will run the former.[/yellow]"
|
|
1287
|
+
)
|
|
1288
|
+
|
|
1132
1289
|
rich.print(
|
|
1133
1290
|
f"Updating application on doover site ({_control_base_url() or 'unknown base URL'})...\n"
|
|
1134
1291
|
)
|
|
@@ -1282,6 +1439,8 @@ def publish(
|
|
|
1282
1439
|
"Warning: could not determine the pushed image digest. "
|
|
1283
1440
|
"Pass --digest to release against it explicitly."
|
|
1284
1441
|
)
|
|
1442
|
+
|
|
1443
|
+
_verify_pushed_image(image_name, detected_digest)
|
|
1285
1444
|
else:
|
|
1286
1445
|
print("App requested to not build. Skipping build step.")
|
|
1287
1446
|
|
|
@@ -1389,6 +1548,96 @@ def registry_login(
|
|
|
1389
1548
|
print(f"Logged in to push {target}")
|
|
1390
1549
|
|
|
1391
1550
|
|
|
1551
|
+
@app.command(name="verify-releases")
|
|
1552
|
+
def verify_releases(
|
|
1553
|
+
app_fp: Annotated[
|
|
1554
|
+
Path, typer.Argument(help="Path to the application directory.")
|
|
1555
|
+
] = Path(),
|
|
1556
|
+
app_name: Annotated[
|
|
1557
|
+
str | None,
|
|
1558
|
+
typer.Option(help="Which app in doover_config.json, if it defines several."),
|
|
1559
|
+
] = None,
|
|
1560
|
+
staging: Annotated[
|
|
1561
|
+
bool | None,
|
|
1562
|
+
typer.Option(help="Force staging mode. Defaults to matching the API URL."),
|
|
1563
|
+
] = None,
|
|
1564
|
+
_profile: ProfileAnnotation = None,
|
|
1565
|
+
):
|
|
1566
|
+
"""Check that every released version of an app can still be pulled.
|
|
1567
|
+
|
|
1568
|
+
Reads only. A release records a digest, and until the registry was fixed a
|
|
1569
|
+
digest stopped resolving as soon as its tag moved on, so releases from before
|
|
1570
|
+
then may already be gone with nothing else reporting it.
|
|
1571
|
+
|
|
1572
|
+
A release whose platform manifests have gone cannot be recovered: their bytes
|
|
1573
|
+
went with them, so it has to be rebuilt and published again.
|
|
1574
|
+
"""
|
|
1575
|
+
_ = _profile
|
|
1576
|
+
client, _ = get_state()
|
|
1577
|
+
root_fp = get_app_directory(app_fp)
|
|
1578
|
+
app_config = get_app_config(root_fp, app_name=app_name)
|
|
1579
|
+
|
|
1580
|
+
application_id = _resolve_application_id(
|
|
1581
|
+
client, app_config, staging=_resolve_staging(staging)
|
|
1582
|
+
)
|
|
1583
|
+
if application_id is None:
|
|
1584
|
+
raise typer.BadParameter(f"'{app_config.name}' has not been published yet.")
|
|
1585
|
+
|
|
1586
|
+
versions = client.list_application_versions(application_id)
|
|
1587
|
+
rows = versions.get("results", versions) if isinstance(versions, dict) else versions
|
|
1588
|
+
|
|
1589
|
+
broken = checked = 0
|
|
1590
|
+
for version in sorted(rows, key=lambda v: v.get("number") or 0):
|
|
1591
|
+
image = version.get("image_name") or ""
|
|
1592
|
+
if "@" not in image or not is_doover_registry(image, _control_base_url()):
|
|
1593
|
+
continue
|
|
1594
|
+
checked += 1
|
|
1595
|
+
digest_ = image.split("@", 1)[1]
|
|
1596
|
+
state_, verdict = _release_state(image)
|
|
1597
|
+
rich.print(f" v{version.get('number'):<4} {digest_[:19]} {verdict}")
|
|
1598
|
+
if state_ == MISSING:
|
|
1599
|
+
broken += 1
|
|
1600
|
+
|
|
1601
|
+
if not checked:
|
|
1602
|
+
rich.print("No releases on the doover registry to check.")
|
|
1603
|
+
return
|
|
1604
|
+
if broken:
|
|
1605
|
+
rich.print(
|
|
1606
|
+
f"\n[bold red]{broken} of {checked} releases cannot be pulled.[/bold red] "
|
|
1607
|
+
"Rebuild and republish them; the images cannot be recovered."
|
|
1608
|
+
)
|
|
1609
|
+
raise typer.Exit(1)
|
|
1610
|
+
rich.print(f"\n[green]All {checked} releases are intact.[/green]")
|
|
1611
|
+
|
|
1612
|
+
|
|
1613
|
+
def _release_state(image: str) -> tuple[str, str]:
|
|
1614
|
+
"""Whether a released image is still pullable, and a line describing it."""
|
|
1615
|
+
repository, digest = image.split("@", 1)
|
|
1616
|
+
state_, index = _read_manifest(image)
|
|
1617
|
+
if state_ == MISSING:
|
|
1618
|
+
return MISSING, "[red]GONE[/red] -- no longer in the registry"
|
|
1619
|
+
if state_ == UNKNOWN:
|
|
1620
|
+
return UNKNOWN, "could not be read (credentials?)"
|
|
1621
|
+
|
|
1622
|
+
if index.get("mediaType") not in _INDEX_MEDIA_TYPES:
|
|
1623
|
+
return PRESENT, "[green]pullable[/green]"
|
|
1624
|
+
|
|
1625
|
+
platforms = [c for c in index.get("manifests", []) if not _is_attestation(c)]
|
|
1626
|
+
gone = [
|
|
1627
|
+
c
|
|
1628
|
+
for c in platforms
|
|
1629
|
+
if _read_manifest(f"{repository}@{c['digest']}")[0] == MISSING
|
|
1630
|
+
]
|
|
1631
|
+
if gone:
|
|
1632
|
+
missing = ", ".join(
|
|
1633
|
+
f"{(c.get('platform') or {}).get('os', '?')}/"
|
|
1634
|
+
f"{(c.get('platform') or {}).get('architecture', '?')}"
|
|
1635
|
+
for c in gone
|
|
1636
|
+
)
|
|
1637
|
+
return MISSING, f"[red]GONE[/red] -- missing platform manifests: {missing}"
|
|
1638
|
+
return PRESENT, f"[green]pullable[/green] ({len(platforms)} platform(s))"
|
|
1639
|
+
|
|
1640
|
+
|
|
1392
1641
|
@app.command(name="release")
|
|
1393
1642
|
def release_command(
|
|
1394
1643
|
app_fp: Annotated[
|