kanibako-cli 1.5.0.dev14__py3-none-any.whl
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.
- kanibako/__init__.py +3 -0
- kanibako/__main__.py +6 -0
- kanibako/auth_browser.py +296 -0
- kanibako/auth_parser.py +51 -0
- kanibako/browser_sidecar.py +183 -0
- kanibako/browser_state.py +103 -0
- kanibako/bun_sea.py +144 -0
- kanibako/cli.py +344 -0
- kanibako/commands/__init__.py +0 -0
- kanibako/commands/archive.py +228 -0
- kanibako/commands/box/__init__.py +22 -0
- kanibako/commands/box/_duplicate.py +395 -0
- kanibako/commands/box/_migrate.py +574 -0
- kanibako/commands/box/_parser.py +1178 -0
- kanibako/commands/clean.py +166 -0
- kanibako/commands/crab_cmd.py +480 -0
- kanibako/commands/diagnose.py +239 -0
- kanibako/commands/fork_cmd.py +51 -0
- kanibako/commands/helper_cmd.py +669 -0
- kanibako/commands/image.py +1300 -0
- kanibako/commands/install.py +152 -0
- kanibako/commands/refresh_credentials.py +67 -0
- kanibako/commands/restore.py +298 -0
- kanibako/commands/setup_cmd.py +89 -0
- kanibako/commands/start.py +1600 -0
- kanibako/commands/stop.py +116 -0
- kanibako/commands/system_cmd.py +224 -0
- kanibako/commands/upgrade.py +161 -0
- kanibako/commands/vault_cmd.py +199 -0
- kanibako/commands/workset_cmd.py +552 -0
- kanibako/config.py +514 -0
- kanibako/config_interface.py +573 -0
- kanibako/config_io.py +36 -0
- kanibako/container.py +607 -0
- kanibako/containerfiles.py +58 -0
- kanibako/containers/Containerfile.kanibako +99 -0
- kanibako/containers/Containerfile.template-android +55 -0
- kanibako/containers/Containerfile.template-dotnet +29 -0
- kanibako/containers/Containerfile.template-js +43 -0
- kanibako/containers/Containerfile.template-jvm +27 -0
- kanibako/containers/Containerfile.template-systems +46 -0
- kanibako/containers/__init__.py +0 -0
- kanibako/crabs.py +89 -0
- kanibako/errors.py +33 -0
- kanibako/freshness.py +67 -0
- kanibako/git.py +114 -0
- kanibako/helper_client.py +132 -0
- kanibako/helper_listener.py +538 -0
- kanibako/helpers.py +339 -0
- kanibako/hygiene.py +296 -0
- kanibako/image_sharing.py +133 -0
- kanibako/instructions.py +160 -0
- kanibako/log.py +31 -0
- kanibako/names.py +248 -0
- kanibako/paths.py +1483 -0
- kanibako/plugins/__init__.py +10 -0
- kanibako/registry.py +71 -0
- kanibako/rig_bundle.py +121 -0
- kanibako/rig_meta.py +92 -0
- kanibako/rig_registry.py +132 -0
- kanibako/rig_resolve.py +182 -0
- kanibako/rig_source.py +245 -0
- kanibako/scripts/__init__.py +0 -0
- kanibako/scripts/helper-init.sh +45 -0
- kanibako/scripts/kanibako-entry +12 -0
- kanibako/settings_resolve.py +312 -0
- kanibako/settings_seeds.py +154 -0
- kanibako/settings_shares.py +154 -0
- kanibako/shellenv.py +75 -0
- kanibako/snapshots.py +281 -0
- kanibako/targets/__init__.py +173 -0
- kanibako/targets/base.py +243 -0
- kanibako/targets/no_agent.py +58 -0
- kanibako/templates.py +60 -0
- kanibako/templates_image.py +224 -0
- kanibako/tweakcc.py +140 -0
- kanibako/tweakcc_cache.py +171 -0
- kanibako/utils.py +136 -0
- kanibako/workset.py +347 -0
- kanibako_cli-1.5.0.dev14.dist-info/METADATA +15 -0
- kanibako_cli-1.5.0.dev14.dist-info/RECORD +85 -0
- kanibako_cli-1.5.0.dev14.dist-info/WHEEL +5 -0
- kanibako_cli-1.5.0.dev14.dist-info/entry_points.txt +5 -0
- kanibako_cli-1.5.0.dev14.dist-info/licenses/LICENSE.md +594 -0
- kanibako_cli-1.5.0.dev14.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1300 @@
|
|
|
1
|
+
"""kanibako image: manage container images (list, create, info, rm, rebuild)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
import tarfile
|
|
10
|
+
import tempfile
|
|
11
|
+
import urllib.request
|
|
12
|
+
import urllib.error
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from kanibako.config import config_file_path, load_config, load_merged_config
|
|
17
|
+
from kanibako.container import ContainerRuntime
|
|
18
|
+
from kanibako.containerfiles import get_containerfile
|
|
19
|
+
from kanibako.errors import ContainerError
|
|
20
|
+
from kanibako.paths import xdg, load_std_paths
|
|
21
|
+
from kanibako.rig_bundle import (
|
|
22
|
+
BUNDLE_SUFFIX,
|
|
23
|
+
pack_bundle,
|
|
24
|
+
read_bundle_meta,
|
|
25
|
+
unpack_bundle,
|
|
26
|
+
)
|
|
27
|
+
from kanibako.rig_meta import RigMeta, write_rig_meta
|
|
28
|
+
from kanibako.rig_registry import (
|
|
29
|
+
RigRecord,
|
|
30
|
+
get as registry_get,
|
|
31
|
+
load_registry,
|
|
32
|
+
registry_path,
|
|
33
|
+
remove as registry_remove,
|
|
34
|
+
upsert,
|
|
35
|
+
)
|
|
36
|
+
from kanibako.rig_resolve import resolve_rig
|
|
37
|
+
from kanibako.rig_source import derive_name, detect_source_kind, fetch_to_temp
|
|
38
|
+
from kanibako.templates_image import (
|
|
39
|
+
list_bundled_templates,
|
|
40
|
+
read_template_checks,
|
|
41
|
+
rig_image_name,
|
|
42
|
+
template_image_name,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_TEMPLATE_PREFIX = "kanibako-template-"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _confirm(prompt: str) -> bool:
|
|
50
|
+
"""Prompt the user for yes/no confirmation. Returns True on 'y'."""
|
|
51
|
+
try:
|
|
52
|
+
answer = input(f"{prompt} [y/N] ").strip().lower()
|
|
53
|
+
except (EOFError, KeyboardInterrupt):
|
|
54
|
+
print()
|
|
55
|
+
return False
|
|
56
|
+
return answer in ("y", "yes")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _deprecated(old: str, new: str) -> None:
|
|
60
|
+
"""Print a one-line deprecation notice to stderr."""
|
|
61
|
+
print(f"note: '{old}' is deprecated; use '{new}'.", file=sys.stderr)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def add_parser(subparsers: argparse._SubParsersAction) -> None:
|
|
65
|
+
p = subparsers.add_parser(
|
|
66
|
+
"rig",
|
|
67
|
+
help="Manage box rigs (images)",
|
|
68
|
+
description="Create, list, inspect, remove, or rebuild box rigs (container images).",
|
|
69
|
+
)
|
|
70
|
+
image_sub = p.add_subparsers(dest="rig_command", metavar="COMMAND")
|
|
71
|
+
|
|
72
|
+
# kanibako image create
|
|
73
|
+
create_p = image_sub.add_parser(
|
|
74
|
+
"create",
|
|
75
|
+
help="Create a new template image from a base image",
|
|
76
|
+
)
|
|
77
|
+
create_p.add_argument("name", help="Template name (e.g. jvm, systems)")
|
|
78
|
+
create_p.add_argument(
|
|
79
|
+
"--base", default=None,
|
|
80
|
+
help="Base image to start from. With --template, defaults to the "
|
|
81
|
+
"template's declared base; without --template, defaults to "
|
|
82
|
+
"kanibako-oci.",
|
|
83
|
+
)
|
|
84
|
+
create_p.add_argument(
|
|
85
|
+
"--template",
|
|
86
|
+
help="Build a bundled template Containerfile (see 'kanibako rig list') "
|
|
87
|
+
"instead of an interactive session",
|
|
88
|
+
)
|
|
89
|
+
commit_group = create_p.add_mutually_exclusive_group()
|
|
90
|
+
commit_group.add_argument(
|
|
91
|
+
"--always-commit", action="store_true",
|
|
92
|
+
help="Commit template even if the container exits with an error",
|
|
93
|
+
)
|
|
94
|
+
commit_group.add_argument(
|
|
95
|
+
"--no-commit-on-error", action="store_true",
|
|
96
|
+
help="Skip commit if the container exits with an error",
|
|
97
|
+
)
|
|
98
|
+
create_p.set_defaults(func=run_create)
|
|
99
|
+
|
|
100
|
+
# kanibako rig prep
|
|
101
|
+
prep_p = image_sub.add_parser(
|
|
102
|
+
"prep", aliases=["prepare"],
|
|
103
|
+
help="Materialize a rig: build a template or pull a prefab",
|
|
104
|
+
description="Resolve a rig name and make it ready to use (build templates, pull prefabs).",
|
|
105
|
+
)
|
|
106
|
+
prep_p.add_argument("name", nargs="?", default=None, help="Rig name to prep (omit with --all)")
|
|
107
|
+
prep_p.add_argument("--force", action="store_true", help="Re-prep even if already prepped")
|
|
108
|
+
prep_p.add_argument("--all", action="store_true", dest="all_images", help="Prep all local kanibako rigs")
|
|
109
|
+
prep_p.set_defaults(func=run_prep)
|
|
110
|
+
|
|
111
|
+
# kanibako rig add
|
|
112
|
+
add_p = image_sub.add_parser(
|
|
113
|
+
"add",
|
|
114
|
+
help="Register a foreign rig (prefab image ref or template Containerfile)",
|
|
115
|
+
description="Add a rig by source: an image reference/tar (prefab) or a Containerfile (template). Does not pull or build; run 'rig prep <name>' afterward.",
|
|
116
|
+
)
|
|
117
|
+
add_p.add_argument("source", help="Image ref, image tar, Containerfile path, or URL")
|
|
118
|
+
add_p.add_argument("--name", default=None, help="Rig name (derived from source if omitted)")
|
|
119
|
+
add_p.add_argument("--as", dest="as_", choices=["image", "template"], default=None, help="Force the source kind (escape hatch)")
|
|
120
|
+
add_p.add_argument("--force", action="store_true", help="Overwrite an existing rig of the same name")
|
|
121
|
+
add_p.set_defaults(func=run_add)
|
|
122
|
+
|
|
123
|
+
# kanibako rig extend
|
|
124
|
+
extend_p = image_sub.add_parser(
|
|
125
|
+
"extend",
|
|
126
|
+
help="Build a custom rig interactively from a foundation rig",
|
|
127
|
+
description="Auto-prep a foundation rig, open an interactive shell to customize it, and commit the result as an extended rig (kanibako-rig-<name>).",
|
|
128
|
+
)
|
|
129
|
+
extend_p.add_argument("name", help="Name for the new extended rig")
|
|
130
|
+
extend_p.add_argument("--from", dest="from_", required=True, metavar="RIG", help="Foundation rig to build from (prefab/template/extended)")
|
|
131
|
+
extend_commit = extend_p.add_mutually_exclusive_group()
|
|
132
|
+
extend_commit.add_argument("--always-commit", action="store_true", help="Commit even if the container exits with an error")
|
|
133
|
+
extend_commit.add_argument("--no-commit-on-error", action="store_true", help="Skip commit if the container exits with an error")
|
|
134
|
+
extend_p.set_defaults(func=run_extend)
|
|
135
|
+
|
|
136
|
+
# kanibako rig export
|
|
137
|
+
export_p = image_sub.add_parser(
|
|
138
|
+
"export",
|
|
139
|
+
help="Export an extended rig to a portable .rig.tgz bundle",
|
|
140
|
+
description="Bundle an extended rig (its image + in-image rig.yaml) into a single .rig.tgz for transfer. Only extended rigs export; prefabs/templates travel by locator (use 'rig add').",
|
|
141
|
+
)
|
|
142
|
+
export_p.add_argument("name", help="Extended rig name to export")
|
|
143
|
+
export_p.add_argument("--out", default=None, help="Output path (default: <name>.rig.tgz)")
|
|
144
|
+
export_p.set_defaults(func=run_export)
|
|
145
|
+
|
|
146
|
+
# kanibako rig import
|
|
147
|
+
import_p = image_sub.add_parser(
|
|
148
|
+
"import",
|
|
149
|
+
help="Import an extended rig from a .rig.tgz bundle",
|
|
150
|
+
description="Restore an extended rig (image + registry row) from a .rig.tgz produced by 'rig export'.",
|
|
151
|
+
)
|
|
152
|
+
import_p.add_argument("file", help="Path to a .rig.tgz bundle")
|
|
153
|
+
import_p.set_defaults(func=run_import)
|
|
154
|
+
|
|
155
|
+
# kanibako image list (default behavior)
|
|
156
|
+
list_p = image_sub.add_parser(
|
|
157
|
+
"list",
|
|
158
|
+
help="List available container images (default)",
|
|
159
|
+
description="List available container images (built-in variants, local, and remote).",
|
|
160
|
+
)
|
|
161
|
+
list_p.add_argument(
|
|
162
|
+
"-q", "--quiet", action="store_true",
|
|
163
|
+
help="Print only image names, one per line",
|
|
164
|
+
)
|
|
165
|
+
list_p.add_argument(
|
|
166
|
+
"--json", action="store_true", dest="as_json",
|
|
167
|
+
help="Emit machine-readable JSON",
|
|
168
|
+
)
|
|
169
|
+
list_p.set_defaults(func=run_list)
|
|
170
|
+
|
|
171
|
+
# kanibako image info / inspect
|
|
172
|
+
info_p = image_sub.add_parser(
|
|
173
|
+
"info", aliases=["inspect"],
|
|
174
|
+
help="Show details about a container image",
|
|
175
|
+
)
|
|
176
|
+
info_p.add_argument("image", help="Image name or shorthand")
|
|
177
|
+
info_p.set_defaults(func=run_info)
|
|
178
|
+
|
|
179
|
+
# kanibako image rm / delete
|
|
180
|
+
rm_p = image_sub.add_parser(
|
|
181
|
+
"rm", aliases=["delete"],
|
|
182
|
+
help="Remove a local container image",
|
|
183
|
+
)
|
|
184
|
+
rm_p.add_argument("image", help="Image name or shorthand")
|
|
185
|
+
rm_p.add_argument(
|
|
186
|
+
"--force", "-f", action="store_true",
|
|
187
|
+
help="Remove without confirmation",
|
|
188
|
+
)
|
|
189
|
+
rm_p.set_defaults(func=run_rm)
|
|
190
|
+
|
|
191
|
+
# kanibako image rebuild
|
|
192
|
+
rebuild_p = image_sub.add_parser(
|
|
193
|
+
"rebuild",
|
|
194
|
+
help="Update container image(s) (pull or rebuild locally)",
|
|
195
|
+
description=(
|
|
196
|
+
"Pull the latest image from the registry, or rebuild locally\n"
|
|
197
|
+
"if a matching Containerfile is found."
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
rebuild_p.add_argument(
|
|
201
|
+
"image", nargs="?", default=None,
|
|
202
|
+
help="Image to update (default: current configured image)",
|
|
203
|
+
)
|
|
204
|
+
rebuild_p.add_argument(
|
|
205
|
+
"--all", action="store_true", dest="all_images",
|
|
206
|
+
help="Update all local kanibako images",
|
|
207
|
+
)
|
|
208
|
+
rebuild_p.set_defaults(func=run_rebuild)
|
|
209
|
+
|
|
210
|
+
# rig diagnose
|
|
211
|
+
from kanibako.commands.diagnose import run_rig_diagnose
|
|
212
|
+
|
|
213
|
+
diagnose_p = image_sub.add_parser(
|
|
214
|
+
"diagnose",
|
|
215
|
+
help="Check rig (image) status",
|
|
216
|
+
)
|
|
217
|
+
diagnose_p.set_defaults(func=run_rig_diagnose)
|
|
218
|
+
|
|
219
|
+
# Default to list if no subcommand given
|
|
220
|
+
p.set_defaults(func=run_list, quiet=False)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def run_create(args: argparse.Namespace) -> int:
|
|
224
|
+
"""Create a template image.
|
|
225
|
+
|
|
226
|
+
With ``--template`` builds a bundled ``Containerfile.template-<name>``;
|
|
227
|
+
otherwise runs an interactive container and commits it on exit.
|
|
228
|
+
"""
|
|
229
|
+
if getattr(args, "template", None):
|
|
230
|
+
_deprecated("rig create --template", "rig prep")
|
|
231
|
+
return _create_from_template(args)
|
|
232
|
+
|
|
233
|
+
# Interactive create is now an alias for 'rig extend'. The base becomes the
|
|
234
|
+
# foundation rig, and the result is committed as kanibako-rig-<name> with a
|
|
235
|
+
# registry row -- that IS the migration.
|
|
236
|
+
_deprecated("rig create (interactive)", "rig extend")
|
|
237
|
+
args.from_ = args.base or "kanibako-oci"
|
|
238
|
+
return run_extend(args)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def run_extend(args: argparse.Namespace) -> int:
|
|
242
|
+
"""Build a custom *extended* rig interactively from a foundation rig.
|
|
243
|
+
|
|
244
|
+
Auto-preps the ``--from`` foundation (build a template / pull-or-build a
|
|
245
|
+
prefab; an extended foundation must already exist), opens an interactive
|
|
246
|
+
container, writes in-image ``/etc/kanibako/rig.yaml`` metadata, commits the
|
|
247
|
+
result as ``kanibako-rig-<name>``, and records a registry row.
|
|
248
|
+
"""
|
|
249
|
+
config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
|
|
250
|
+
config = load_config(config_file)
|
|
251
|
+
std = load_std_paths(config)
|
|
252
|
+
merged = load_merged_config(config_file, None)
|
|
253
|
+
containers_dir = std.data_path / "containers"
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
runtime = ContainerRuntime()
|
|
257
|
+
except ContainerError as e:
|
|
258
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
259
|
+
return 1
|
|
260
|
+
|
|
261
|
+
# Validate the new name early.
|
|
262
|
+
try:
|
|
263
|
+
image = rig_image_name(args.name)
|
|
264
|
+
except ValueError as e:
|
|
265
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
266
|
+
return 1
|
|
267
|
+
|
|
268
|
+
# Resolve + auto-prep the foundation.
|
|
269
|
+
registry = load_registry(registry_path(std))
|
|
270
|
+
res = resolve_rig(args.from_, runtime, std, merged, registry=registry)
|
|
271
|
+
if res.prep_action != "none":
|
|
272
|
+
if res.kind == "extended":
|
|
273
|
+
# A missing extended foundation has no recipe to rebuild.
|
|
274
|
+
print(
|
|
275
|
+
f"Error: foundation rig '{args.from_}' is extended but its image "
|
|
276
|
+
"is missing; no recipe to re-prep it (try 'rig import').",
|
|
277
|
+
file=sys.stderr,
|
|
278
|
+
)
|
|
279
|
+
return 1
|
|
280
|
+
if res.kind == "template":
|
|
281
|
+
cf = res.containerfile or get_containerfile(
|
|
282
|
+
f"template-{args.from_}", containers_dir,
|
|
283
|
+
)
|
|
284
|
+
if cf is None:
|
|
285
|
+
print(
|
|
286
|
+
f"Error: Containerfile not found for template '{args.from_}'.",
|
|
287
|
+
file=sys.stderr,
|
|
288
|
+
)
|
|
289
|
+
return 1
|
|
290
|
+
print(f"Preparing foundation '{args.from_}' (build {cf.name})...")
|
|
291
|
+
rc = runtime.rebuild(res.image, cf, cf.parent, build_args=None)
|
|
292
|
+
if rc != 0:
|
|
293
|
+
print(
|
|
294
|
+
f"Error: failed to build foundation '{args.from_}'.",
|
|
295
|
+
file=sys.stderr,
|
|
296
|
+
)
|
|
297
|
+
return rc
|
|
298
|
+
else:
|
|
299
|
+
# prefab: build-or-pull.
|
|
300
|
+
print(f"Preparing foundation '{args.from_}'...")
|
|
301
|
+
rc = _update_one(runtime, res.image, containers_dir)
|
|
302
|
+
if rc != 0:
|
|
303
|
+
print(
|
|
304
|
+
f"Error: failed to prep foundation '{args.from_}'.",
|
|
305
|
+
file=sys.stderr,
|
|
306
|
+
)
|
|
307
|
+
return 1
|
|
308
|
+
foundation_image = res.image
|
|
309
|
+
|
|
310
|
+
# Interactive session (mirror run_create's commit-on-error logic).
|
|
311
|
+
container_name = f"kanibako-extend-{args.name}"
|
|
312
|
+
print(f"Starting interactive container from {foundation_image}...")
|
|
313
|
+
print(f"Customize it, then exit to save as extended rig '{args.name}'.")
|
|
314
|
+
rc = runtime.run_interactive(foundation_image, container_name=container_name)
|
|
315
|
+
should_commit = True
|
|
316
|
+
if rc != 0:
|
|
317
|
+
print(f"\nContainer exited with code {rc}.", file=sys.stderr)
|
|
318
|
+
if args.no_commit_on_error:
|
|
319
|
+
should_commit = False
|
|
320
|
+
elif not args.always_commit:
|
|
321
|
+
should_commit = _confirm("Commit container state anyway?")
|
|
322
|
+
if not should_commit:
|
|
323
|
+
print("Skipping commit.", file=sys.stderr)
|
|
324
|
+
runtime.rm(container_name)
|
|
325
|
+
return 1
|
|
326
|
+
|
|
327
|
+
# Write in-image metadata, then commit. Always clean up the container.
|
|
328
|
+
created = datetime.now(timezone.utc).isoformat()
|
|
329
|
+
foundation_source = res.source_ref or args.from_
|
|
330
|
+
try:
|
|
331
|
+
meta = RigMeta(
|
|
332
|
+
name=args.name,
|
|
333
|
+
kind="extended",
|
|
334
|
+
parent=foundation_image,
|
|
335
|
+
foundation_source=foundation_source,
|
|
336
|
+
reproducible=False,
|
|
337
|
+
created=created,
|
|
338
|
+
)
|
|
339
|
+
with tempfile.TemporaryDirectory() as td:
|
|
340
|
+
meta_dir = Path(td) / "kanibako"
|
|
341
|
+
write_rig_meta(meta, meta_dir / "rig.yaml")
|
|
342
|
+
# Copy the DIRECTORY into /etc/ -> lands at /etc/kanibako/rig.yaml.
|
|
343
|
+
# Do NOT cp a bare file to :/etc/kanibako/rig.yaml (fails if the dir
|
|
344
|
+
# is absent in the image).
|
|
345
|
+
if not runtime.cp(meta_dir, f"{container_name}:/etc/"):
|
|
346
|
+
print(
|
|
347
|
+
"Error: failed to write rig metadata into the container.",
|
|
348
|
+
file=sys.stderr,
|
|
349
|
+
)
|
|
350
|
+
return 1
|
|
351
|
+
try:
|
|
352
|
+
runtime.commit(container_name, image)
|
|
353
|
+
except ContainerError as e:
|
|
354
|
+
print(f"Failed to commit: {e}", file=sys.stderr)
|
|
355
|
+
return 1
|
|
356
|
+
upsert(
|
|
357
|
+
registry_path(std),
|
|
358
|
+
RigRecord(
|
|
359
|
+
name=args.name,
|
|
360
|
+
kind="extended",
|
|
361
|
+
image=image,
|
|
362
|
+
parent=foundation_image,
|
|
363
|
+
foundation_source=foundation_source,
|
|
364
|
+
reproducible=False,
|
|
365
|
+
created=created,
|
|
366
|
+
source_type="extend",
|
|
367
|
+
),
|
|
368
|
+
)
|
|
369
|
+
print(f"\nExtended rig saved as {image}")
|
|
370
|
+
finally:
|
|
371
|
+
runtime.rm(container_name)
|
|
372
|
+
|
|
373
|
+
return 0
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _create_from_template(args: argparse.Namespace) -> int:
|
|
377
|
+
"""Build a bundled template Containerfile into a local template image."""
|
|
378
|
+
template = args.template
|
|
379
|
+
|
|
380
|
+
config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
|
|
381
|
+
config = load_config(config_file)
|
|
382
|
+
std = load_std_paths(config)
|
|
383
|
+
containers_dir = std.data_path / "containers"
|
|
384
|
+
|
|
385
|
+
available = sorted(
|
|
386
|
+
t.name for t in list_bundled_templates(override_dir=containers_dir)
|
|
387
|
+
)
|
|
388
|
+
if template not in available:
|
|
389
|
+
print(
|
|
390
|
+
f"error: unknown template '{template}'. "
|
|
391
|
+
f"Available: {', '.join(available)}",
|
|
392
|
+
file=sys.stderr,
|
|
393
|
+
)
|
|
394
|
+
return 1
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
image_name = template_image_name(args.name)
|
|
398
|
+
except ValueError as e:
|
|
399
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
400
|
+
return 1
|
|
401
|
+
|
|
402
|
+
containerfile = get_containerfile(f"template-{template}", containers_dir)
|
|
403
|
+
if containerfile is None:
|
|
404
|
+
print(
|
|
405
|
+
f"Error: Containerfile not found for template: {template}",
|
|
406
|
+
file=sys.stderr,
|
|
407
|
+
)
|
|
408
|
+
return 1
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
runtime = ContainerRuntime()
|
|
412
|
+
except ContainerError as e:
|
|
413
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
414
|
+
return 1
|
|
415
|
+
|
|
416
|
+
build_args: dict[str, str] | None
|
|
417
|
+
if args.base is None:
|
|
418
|
+
# Let the Containerfile's declared ARG BASE_IMAGE default stand.
|
|
419
|
+
build_args = None
|
|
420
|
+
print(f"Building template '{template}' from its default base...")
|
|
421
|
+
else:
|
|
422
|
+
merged = load_merged_config(config_file, None)
|
|
423
|
+
base_image = resolve_image_name(args.base, merged.box_image)
|
|
424
|
+
build_args = {"BASE_IMAGE": base_image}
|
|
425
|
+
print(
|
|
426
|
+
f"Note: overriding template '{template}' default base "
|
|
427
|
+
f"with {base_image}."
|
|
428
|
+
)
|
|
429
|
+
print(f"Building template '{template}' from {base_image}...")
|
|
430
|
+
print()
|
|
431
|
+
rc = runtime.rebuild(image_name, containerfile, containerfile.parent, build_args=build_args)
|
|
432
|
+
if rc == 0:
|
|
433
|
+
print()
|
|
434
|
+
print(f"Template saved as {image_name}")
|
|
435
|
+
else:
|
|
436
|
+
print()
|
|
437
|
+
print(f"Build failed with exit code {rc}", file=sys.stderr)
|
|
438
|
+
return rc
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _bare_repo(repo: str) -> str:
|
|
442
|
+
"""Strip a trailing ``:tag`` from a ``repo:tag`` reference."""
|
|
443
|
+
return repo.split(":")[0] if ":" in repo else repo
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def run_list(args: argparse.Namespace) -> int:
|
|
447
|
+
"""List rigs grouped by kind (prefab / template / extended) with live status.
|
|
448
|
+
|
|
449
|
+
Status is derived from the local image store at call time -- never read from
|
|
450
|
+
a stored field. ``-q/--quiet`` keeps the legacy one-name-per-line behavior;
|
|
451
|
+
``--json`` emits a machine-readable document.
|
|
452
|
+
"""
|
|
453
|
+
config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
|
|
454
|
+
config = load_config(config_file)
|
|
455
|
+
std = load_std_paths(config)
|
|
456
|
+
merged = load_merged_config(config_file, None)
|
|
457
|
+
|
|
458
|
+
quiet = getattr(args, "quiet", False)
|
|
459
|
+
|
|
460
|
+
runtime: ContainerRuntime | None
|
|
461
|
+
|
|
462
|
+
if quiet:
|
|
463
|
+
# Quiet mode: just print image names (unchanged).
|
|
464
|
+
try:
|
|
465
|
+
runtime = ContainerRuntime()
|
|
466
|
+
for repo, _size in runtime.list_local_images():
|
|
467
|
+
print(repo)
|
|
468
|
+
except ContainerError:
|
|
469
|
+
pass
|
|
470
|
+
return 0
|
|
471
|
+
|
|
472
|
+
# A runtime is required to derive live status; without one, every status is
|
|
473
|
+
# reported as "unknown" but the (registry/template) catalogue still lists.
|
|
474
|
+
try:
|
|
475
|
+
runtime = ContainerRuntime()
|
|
476
|
+
except ContainerError:
|
|
477
|
+
runtime = None
|
|
478
|
+
|
|
479
|
+
containers_dir = std.data_path / "containers"
|
|
480
|
+
registry = load_registry(registry_path(std))
|
|
481
|
+
|
|
482
|
+
def _status(image: str, *, absent: str = "unprepped") -> str:
|
|
483
|
+
if runtime is None:
|
|
484
|
+
return "unknown"
|
|
485
|
+
return "prepped" if runtime.image_exists(image) else absent
|
|
486
|
+
|
|
487
|
+
# ---- Prefabs ----
|
|
488
|
+
prefabs: list[dict[str, str]] = []
|
|
489
|
+
for suffix in sorted(_KNOWN_SUFFIXES):
|
|
490
|
+
prefabs.append({
|
|
491
|
+
"name": suffix,
|
|
492
|
+
"image": f"kanibako-{suffix}",
|
|
493
|
+
"status": _status(f"kanibako-{suffix}:latest"),
|
|
494
|
+
})
|
|
495
|
+
for rec in registry.values():
|
|
496
|
+
if rec.kind != "prefab":
|
|
497
|
+
continue
|
|
498
|
+
if rec.image:
|
|
499
|
+
img = rec.image
|
|
500
|
+
elif runtime is not None:
|
|
501
|
+
img = resolve_image_reference(
|
|
502
|
+
rec.source or rec.name, runtime, merged.box_image,
|
|
503
|
+
)
|
|
504
|
+
else:
|
|
505
|
+
img = rec.source or rec.name
|
|
506
|
+
prefabs.append({"name": rec.name, "image": img, "status": _status(img)})
|
|
507
|
+
|
|
508
|
+
# ---- Templates ----
|
|
509
|
+
templates: list[dict[str, str]] = []
|
|
510
|
+
for t in list_bundled_templates(override_dir=containers_dir):
|
|
511
|
+
image = template_image_name(t.name)
|
|
512
|
+
templates.append({
|
|
513
|
+
"name": t.name,
|
|
514
|
+
"source": t.source,
|
|
515
|
+
"image": image,
|
|
516
|
+
"status": _status(image),
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
# ---- Extended ----
|
|
520
|
+
extended: list[dict[str, str]] = []
|
|
521
|
+
seen: set[str] = set()
|
|
522
|
+
if runtime is not None:
|
|
523
|
+
for repo, _size in runtime.list_local_images():
|
|
524
|
+
bare = _bare_repo(repo)
|
|
525
|
+
basename = bare.rsplit("/", 1)[-1]
|
|
526
|
+
if basename.startswith("kanibako-rig-"):
|
|
527
|
+
name = basename[len("kanibako-rig-"):]
|
|
528
|
+
seen.add(name)
|
|
529
|
+
extended.append({"name": name, "image": bare, "status": "prepped"})
|
|
530
|
+
for rec in registry.values():
|
|
531
|
+
if rec.kind != "extended" or rec.name in seen:
|
|
532
|
+
continue
|
|
533
|
+
if rec.image:
|
|
534
|
+
img = rec.image
|
|
535
|
+
else:
|
|
536
|
+
try:
|
|
537
|
+
img = rig_image_name(rec.name)
|
|
538
|
+
except ValueError:
|
|
539
|
+
img = rec.image or rec.name
|
|
540
|
+
extended.append({
|
|
541
|
+
"name": rec.name,
|
|
542
|
+
"image": img,
|
|
543
|
+
"status": _status(img, absent="missing"),
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
if getattr(args, "as_json", False):
|
|
547
|
+
data = {
|
|
548
|
+
"prefabs": prefabs,
|
|
549
|
+
"templates": templates,
|
|
550
|
+
"extended": extended,
|
|
551
|
+
"current": merged.box_image,
|
|
552
|
+
}
|
|
553
|
+
print(json.dumps(data, indent=2))
|
|
554
|
+
return 0
|
|
555
|
+
|
|
556
|
+
print("Prefabs (pull to prep):")
|
|
557
|
+
if prefabs:
|
|
558
|
+
for p in prefabs:
|
|
559
|
+
print(f" {p['name']:<32} {p['status']}")
|
|
560
|
+
else:
|
|
561
|
+
print(" (none)")
|
|
562
|
+
|
|
563
|
+
print()
|
|
564
|
+
print("Templates (build to prep):")
|
|
565
|
+
if templates:
|
|
566
|
+
for t_row in templates:
|
|
567
|
+
tag = f"[{t_row['source']}]"
|
|
568
|
+
print(f" {t_row['name']:<16} {tag:<11} {t_row['status']}")
|
|
569
|
+
else:
|
|
570
|
+
print(" (none)")
|
|
571
|
+
|
|
572
|
+
print()
|
|
573
|
+
print("Extended (interactive; export/import to move):")
|
|
574
|
+
if extended:
|
|
575
|
+
for e in extended:
|
|
576
|
+
print(f" {e['name']:<32} {e['status']}")
|
|
577
|
+
else:
|
|
578
|
+
print(" (none)")
|
|
579
|
+
|
|
580
|
+
print()
|
|
581
|
+
print(f"Current rig: {merged.box_image}")
|
|
582
|
+
return 0
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
_PREP_STATUS = {"none": "prepped", "pull": "unprepped", "build": "unprepped", "missing": "missing"}
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def run_info(args: argparse.Namespace) -> int:
|
|
589
|
+
"""Show details about a rig: kind, live status, image, and provenance."""
|
|
590
|
+
config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
|
|
591
|
+
config = load_config(config_file)
|
|
592
|
+
std = load_std_paths(config)
|
|
593
|
+
merged = load_merged_config(config_file, None)
|
|
594
|
+
|
|
595
|
+
try:
|
|
596
|
+
runtime = ContainerRuntime()
|
|
597
|
+
except ContainerError as e:
|
|
598
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
599
|
+
return 1
|
|
600
|
+
|
|
601
|
+
registry = load_registry(registry_path(std))
|
|
602
|
+
res = resolve_rig(args.image, runtime, std, merged, registry=registry)
|
|
603
|
+
|
|
604
|
+
# A speculative prefab guess for an unknown name is "not found": only a known
|
|
605
|
+
# base, a registered rig, or a discovered template is shown when unprepped.
|
|
606
|
+
if (
|
|
607
|
+
res.kind == "prefab"
|
|
608
|
+
and res.prep_action != "none"
|
|
609
|
+
and args.image not in _KNOWN_SUFFIXES
|
|
610
|
+
and args.image not in registry
|
|
611
|
+
):
|
|
612
|
+
print(f"Error: rig not found: {args.image}", file=sys.stderr)
|
|
613
|
+
return 1
|
|
614
|
+
|
|
615
|
+
status = _PREP_STATUS.get(res.prep_action, res.prep_action)
|
|
616
|
+
|
|
617
|
+
print(f"Name: {args.image}")
|
|
618
|
+
print(f"Kind: {res.kind}")
|
|
619
|
+
print(f"Status: {status}")
|
|
620
|
+
print(f"Image: {res.image}")
|
|
621
|
+
if res.source_ref:
|
|
622
|
+
print(f"Source: {res.source_ref}")
|
|
623
|
+
|
|
624
|
+
# Live image details (only available once the rig is prepped).
|
|
625
|
+
data = runtime.image_inspect(res.image)
|
|
626
|
+
if data is not None:
|
|
627
|
+
image_id = data.get("Id", "")
|
|
628
|
+
if image_id:
|
|
629
|
+
short_id = image_id[:19] if len(image_id) > 19 else image_id
|
|
630
|
+
print(f"ID: {short_id}")
|
|
631
|
+
created = data.get("Created", "")
|
|
632
|
+
if created:
|
|
633
|
+
print(f"Created: {created}")
|
|
634
|
+
size = data.get("Size")
|
|
635
|
+
if size is not None:
|
|
636
|
+
if isinstance(size, (int, float)):
|
|
637
|
+
if size >= 1_000_000_000:
|
|
638
|
+
print(f"Size: {size / 1_000_000_000:.1f} GB")
|
|
639
|
+
elif size >= 1_000_000:
|
|
640
|
+
print(f"Size: {size / 1_000_000:.1f} MB")
|
|
641
|
+
else:
|
|
642
|
+
print(f"Size: {size} bytes")
|
|
643
|
+
else:
|
|
644
|
+
print(f"Size: {size}")
|
|
645
|
+
labels = data.get("Labels") or data.get("Config", {}).get("Labels")
|
|
646
|
+
if labels:
|
|
647
|
+
print("Labels:")
|
|
648
|
+
for k, v in sorted(labels.items()):
|
|
649
|
+
print(f" {k}={v}")
|
|
650
|
+
|
|
651
|
+
# Provenance.
|
|
652
|
+
record = registry.get(args.image)
|
|
653
|
+
if record is not None:
|
|
654
|
+
if record.parent:
|
|
655
|
+
print(f"Parent: {record.parent}")
|
|
656
|
+
if record.foundation_source:
|
|
657
|
+
print(f"Foundation: {record.foundation_source}")
|
|
658
|
+
|
|
659
|
+
if res.kind == "template":
|
|
660
|
+
cf = get_containerfile(f"template-{args.image}", std.data_path / "containers")
|
|
661
|
+
if cf is not None:
|
|
662
|
+
print(f"Containerfile: {cf}")
|
|
663
|
+
checks = read_template_checks(cf)
|
|
664
|
+
if checks:
|
|
665
|
+
print("Checks:")
|
|
666
|
+
for check in checks:
|
|
667
|
+
print(f" {check}")
|
|
668
|
+
|
|
669
|
+
return 0
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def run_rm(args: argparse.Namespace) -> int:
|
|
673
|
+
"""Remove a local container image, or un-add a registered/template rig."""
|
|
674
|
+
try:
|
|
675
|
+
runtime = ContainerRuntime()
|
|
676
|
+
except ContainerError as e:
|
|
677
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
678
|
+
return 1
|
|
679
|
+
|
|
680
|
+
config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
|
|
681
|
+
config = load_config(config_file)
|
|
682
|
+
std = load_std_paths(config)
|
|
683
|
+
|
|
684
|
+
# --- Un-add: a registered rig (rigs.yaml) wins over image removal. ---
|
|
685
|
+
record = registry_get(registry_path(std), args.image)
|
|
686
|
+
if record is not None:
|
|
687
|
+
registry_remove(registry_path(std), args.image)
|
|
688
|
+
print(f"Removed rig '{args.image}' from the registry.")
|
|
689
|
+
# A loaded (file-sourced) prefab owns its local image; clean it up.
|
|
690
|
+
if record.source_type == "file" and record.image:
|
|
691
|
+
try:
|
|
692
|
+
runtime.remove_image(record.image)
|
|
693
|
+
except ContainerError:
|
|
694
|
+
pass
|
|
695
|
+
return 0
|
|
696
|
+
|
|
697
|
+
# --- Un-add: an installed user template removes its Containerfile. ---
|
|
698
|
+
override = std.data_path / "containers" / f"Containerfile.template-{args.image}"
|
|
699
|
+
if override.is_file():
|
|
700
|
+
override.unlink()
|
|
701
|
+
print(f"Removed user template '{args.image}'.")
|
|
702
|
+
return 0
|
|
703
|
+
|
|
704
|
+
merged = load_merged_config(config_file, None)
|
|
705
|
+
image = resolve_image_name(args.image, merged.box_image)
|
|
706
|
+
|
|
707
|
+
if not args.force:
|
|
708
|
+
# Check if local-only (template images are local-only)
|
|
709
|
+
# Extract just the image name (strip registry prefix and tag)
|
|
710
|
+
bare = image.split(":")[0] if ":" in image else image
|
|
711
|
+
image_basename = bare.rsplit("/", 1)[-1]
|
|
712
|
+
if image_basename.startswith(_TEMPLATE_PREFIX):
|
|
713
|
+
print(f"Rig '{image}' is a local template (not recoverable from registry).")
|
|
714
|
+
else:
|
|
715
|
+
print(f"Rig '{image}' may be recoverable via 'kanibako rig rebuild'.")
|
|
716
|
+
|
|
717
|
+
if not _confirm(f"Remove image '{image}'?"):
|
|
718
|
+
print("Cancelled.")
|
|
719
|
+
return 0
|
|
720
|
+
|
|
721
|
+
try:
|
|
722
|
+
runtime.remove_image(image)
|
|
723
|
+
print(f"Removed rig '{image}'.")
|
|
724
|
+
except ContainerError as e:
|
|
725
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
726
|
+
return 1
|
|
727
|
+
return 0
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def _extract_ghcr_owner(image: str) -> str | None:
|
|
731
|
+
"""Extract GitHub owner from ghcr.io/<owner>/... image path."""
|
|
732
|
+
if not image.startswith("ghcr.io/"):
|
|
733
|
+
return None
|
|
734
|
+
remainder = image[len("ghcr.io/"):]
|
|
735
|
+
return remainder.split("/")[0] if "/" in remainder else None
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def _list_remote_packages(owner: str) -> None:
|
|
739
|
+
"""Query GitHub API for the owner's kanibako container packages."""
|
|
740
|
+
url = f"https://api.github.com/users/{owner}/packages?package_type=container"
|
|
741
|
+
try:
|
|
742
|
+
req = urllib.request.Request(url, headers={"User-Agent": "kanibako"})
|
|
743
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
744
|
+
data = json.loads(resp.read())
|
|
745
|
+
except (urllib.error.URLError, OSError, json.JSONDecodeError):
|
|
746
|
+
print(" (could not reach GitHub API)")
|
|
747
|
+
return
|
|
748
|
+
|
|
749
|
+
packages = [pkg["name"] for pkg in data if "kanibako" in pkg.get("name", "").lower()]
|
|
750
|
+
if packages:
|
|
751
|
+
for pkg in packages:
|
|
752
|
+
print(f" ghcr.io/{owner}/{pkg}")
|
|
753
|
+
else:
|
|
754
|
+
print(f" (no kanibako packages found for {owner})")
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _extract_registry_prefix(image: str) -> str | None:
|
|
758
|
+
"""Extract ``registry/owner`` prefix from a fully qualified image name.
|
|
759
|
+
|
|
760
|
+
>>> _extract_registry_prefix("ghcr.io/doctorjei/kanibako-oci:latest")
|
|
761
|
+
'ghcr.io/doctorjei'
|
|
762
|
+
"""
|
|
763
|
+
# Expect at least registry/owner/name
|
|
764
|
+
parts = image.split("/")
|
|
765
|
+
if len(parts) >= 3:
|
|
766
|
+
return "/".join(parts[:-1])
|
|
767
|
+
return None
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
# Known shorthand suffixes that map to kanibako-<suffix> images.
|
|
771
|
+
_KNOWN_SUFFIXES = {"min", "oci", "lxc", "vm"}
|
|
772
|
+
|
|
773
|
+
# Last-resort registry/owner when none can be derived from configuration.
|
|
774
|
+
_FALLBACK_REGISTRY = "ghcr.io/doctorjei"
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def resolve_image_reference(
|
|
778
|
+
name: str, runtime: ContainerRuntime, configured_image: str,
|
|
779
|
+
) -> str:
|
|
780
|
+
"""Resolve a kanibako image name to a runtime-usable reference.
|
|
781
|
+
|
|
782
|
+
Resolution order:
|
|
783
|
+
|
|
784
|
+
1. Already qualified (*name* contains ``/``) -> returned unchanged.
|
|
785
|
+
2. Non-kanibako bare names (e.g. ``busybox``, ``ubuntu``) -> returned
|
|
786
|
+
unchanged, so the runtime's own ``unqualified-search-registries``
|
|
787
|
+
resolve them as before. Only kanibako-branded names — a known suffix
|
|
788
|
+
(``min``/``oci``/``lxc``/``vm``) or a ``kanibako-`` prefix — are
|
|
789
|
+
expanded and prefixed.
|
|
790
|
+
3. Local-first: if the local image store already has the (suffix-expanded)
|
|
791
|
+
bare reference, use it as-is — lets a locally built or bare-tagged
|
|
792
|
+
kanibako image win without contacting the registry.
|
|
793
|
+
4. Otherwise prefix it with the ``registry/owner`` derived from
|
|
794
|
+
*configured_image* (falling back to :data:`_FALLBACK_REGISTRY`) so the
|
|
795
|
+
runtime can pull it without relying on ``unqualified-search-registries``.
|
|
796
|
+
|
|
797
|
+
Unlike :func:`resolve_image_name`, this consults the local store first.
|
|
798
|
+
The branding restriction is deliberate: we cannot safely assume an
|
|
799
|
+
arbitrary bare name (a public Docker Hub image) belongs to the kanibako
|
|
800
|
+
registry, so only names we actually publish are rewritten.
|
|
801
|
+
"""
|
|
802
|
+
if "/" in name:
|
|
803
|
+
return name
|
|
804
|
+
|
|
805
|
+
if name in _KNOWN_SUFFIXES:
|
|
806
|
+
candidate = f"kanibako-{name}"
|
|
807
|
+
elif name.startswith("kanibako-"):
|
|
808
|
+
candidate = name
|
|
809
|
+
else:
|
|
810
|
+
return name
|
|
811
|
+
|
|
812
|
+
bare = candidate if ":" in candidate else f"{candidate}:latest"
|
|
813
|
+
|
|
814
|
+
if runtime.image_exists(bare):
|
|
815
|
+
return bare
|
|
816
|
+
|
|
817
|
+
prefix = _extract_registry_prefix(configured_image) or _FALLBACK_REGISTRY
|
|
818
|
+
return f"{prefix}/{bare}"
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def resolve_image_name(name: str, configured_image: str) -> str:
|
|
822
|
+
"""Expand a shorthand image name to a fully qualified image reference.
|
|
823
|
+
|
|
824
|
+
- If *name* contains ``/``, it is already qualified -- returned as-is.
|
|
825
|
+
- If *name* is a known suffix (``min``, ``oci``, ``lxc``, ``vm``), expand
|
|
826
|
+
to ``{prefix}/kanibako-{name}:latest``.
|
|
827
|
+
- If *name* starts with ``kanibako-``, expand to ``{prefix}/{name}:latest``.
|
|
828
|
+
- Otherwise return *name* unchanged.
|
|
829
|
+
"""
|
|
830
|
+
if "/" in name:
|
|
831
|
+
return name
|
|
832
|
+
|
|
833
|
+
prefix = _extract_registry_prefix(configured_image)
|
|
834
|
+
if prefix is None:
|
|
835
|
+
return name
|
|
836
|
+
|
|
837
|
+
if name in _KNOWN_SUFFIXES:
|
|
838
|
+
return f"{prefix}/kanibako-{name}:latest"
|
|
839
|
+
|
|
840
|
+
if name.startswith("kanibako-"):
|
|
841
|
+
return f"{prefix}/{name}:latest"
|
|
842
|
+
|
|
843
|
+
return name
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def run_prep(args: argparse.Namespace) -> int:
|
|
847
|
+
"""Materialize a rig: build a template or pull/build a prefab.
|
|
848
|
+
|
|
849
|
+
Resolves *name* via :func:`resolve_rig` (pure) and then performs the
|
|
850
|
+
side effect it implies. ``--force`` re-preps even if already prepped;
|
|
851
|
+
``--all`` build-or-pulls every local kanibako rig.
|
|
852
|
+
"""
|
|
853
|
+
config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
|
|
854
|
+
config = load_config(config_file)
|
|
855
|
+
std = load_std_paths(config)
|
|
856
|
+
containers_dir = std.data_path / "containers"
|
|
857
|
+
|
|
858
|
+
try:
|
|
859
|
+
runtime = ContainerRuntime()
|
|
860
|
+
except ContainerError as e:
|
|
861
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
862
|
+
return 1
|
|
863
|
+
|
|
864
|
+
if args.all_images:
|
|
865
|
+
return _update_all(runtime, containers_dir)
|
|
866
|
+
|
|
867
|
+
if args.name is None:
|
|
868
|
+
print("error: rig name required (or use --all)", file=sys.stderr)
|
|
869
|
+
return 1
|
|
870
|
+
|
|
871
|
+
merged = load_merged_config(config_file, None)
|
|
872
|
+
registry = load_registry(registry_path(std))
|
|
873
|
+
res = resolve_rig(args.name, runtime, std, merged, registry=registry)
|
|
874
|
+
force = getattr(args, "force", False)
|
|
875
|
+
|
|
876
|
+
if res.kind == "extended":
|
|
877
|
+
if force:
|
|
878
|
+
print(
|
|
879
|
+
f"error: extended rig '{args.name}' has no recipe to re-prep "
|
|
880
|
+
f"(use 'rig export'/'rig import' or 'rig extend').",
|
|
881
|
+
file=sys.stderr,
|
|
882
|
+
)
|
|
883
|
+
return 1
|
|
884
|
+
if res.prep_action == "none":
|
|
885
|
+
print(f"Rig '{args.name}' is already prepped.")
|
|
886
|
+
return 0
|
|
887
|
+
print(
|
|
888
|
+
f"error: extended rig '{args.name}' image is missing.",
|
|
889
|
+
file=sys.stderr,
|
|
890
|
+
)
|
|
891
|
+
return 1
|
|
892
|
+
|
|
893
|
+
if res.prep_action == "none" and not force:
|
|
894
|
+
print(f"Rig '{args.name}' is already prepped.")
|
|
895
|
+
return 0
|
|
896
|
+
|
|
897
|
+
if res.kind == "template":
|
|
898
|
+
cf = res.containerfile or get_containerfile(
|
|
899
|
+
f"template-{args.name}", containers_dir,
|
|
900
|
+
)
|
|
901
|
+
if cf is None:
|
|
902
|
+
print(
|
|
903
|
+
f"error: Containerfile not found for template '{args.name}'.",
|
|
904
|
+
file=sys.stderr,
|
|
905
|
+
)
|
|
906
|
+
return 1
|
|
907
|
+
print(f"Building rig '{args.name}' from {cf.name}...")
|
|
908
|
+
rc = runtime.rebuild(res.image, cf, cf.parent, build_args=None)
|
|
909
|
+
if rc == 0:
|
|
910
|
+
print(f"Rig '{args.name}' prepped as {res.image}")
|
|
911
|
+
else:
|
|
912
|
+
print(f"Build failed with exit code {rc}", file=sys.stderr)
|
|
913
|
+
return rc
|
|
914
|
+
|
|
915
|
+
# prefab: build-if-Containerfile-else-pull (same as rebuild).
|
|
916
|
+
return _update_one(runtime, res.image, containers_dir)
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def run_add(args: argparse.Namespace) -> int:
|
|
920
|
+
"""Register a foreign rig from a source. Never pulls or builds.
|
|
921
|
+
|
|
922
|
+
The *source* is classified (an image ref/tar -> prefab; a Containerfile ->
|
|
923
|
+
template) and recorded. Templates install their Containerfile under the
|
|
924
|
+
user-override dir (the file IS the source of truth -- no registry row);
|
|
925
|
+
prefabs get a ``rigs.yaml`` row (a tar is loaded via ``runtime.load`` first,
|
|
926
|
+
a ref is recorded as-is). Run ``rig prep <name>`` afterward to materialize.
|
|
927
|
+
"""
|
|
928
|
+
config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
|
|
929
|
+
config = load_config(config_file)
|
|
930
|
+
std = load_std_paths(config)
|
|
931
|
+
|
|
932
|
+
try:
|
|
933
|
+
runtime = ContainerRuntime()
|
|
934
|
+
except ContainerError as e:
|
|
935
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
936
|
+
return 1
|
|
937
|
+
|
|
938
|
+
source = args.source
|
|
939
|
+
|
|
940
|
+
# A raw URL is undecidable on its own; fetch it first, then classify the
|
|
941
|
+
# downloaded file. The temp file doubles as the source for tar/template.
|
|
942
|
+
if source.lower().startswith(("http://", "https://")):
|
|
943
|
+
local_path: Path | None = fetch_to_temp(source)
|
|
944
|
+
detect_target = str(local_path)
|
|
945
|
+
else:
|
|
946
|
+
local_path = None
|
|
947
|
+
detect_target = source
|
|
948
|
+
|
|
949
|
+
try:
|
|
950
|
+
kind = detect_source_kind(detect_target, force=args.as_)
|
|
951
|
+
except ValueError as e:
|
|
952
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
953
|
+
return 1
|
|
954
|
+
|
|
955
|
+
name = args.name or derive_name(source, kind)
|
|
956
|
+
if name is None:
|
|
957
|
+
print(
|
|
958
|
+
"Error: could not derive a rig name from source; pass --name NAME.",
|
|
959
|
+
file=sys.stderr,
|
|
960
|
+
)
|
|
961
|
+
return 1
|
|
962
|
+
|
|
963
|
+
# Collision: a registry row OR an installed user template of the same name.
|
|
964
|
+
containers_dir = std.data_path / "containers"
|
|
965
|
+
exists = registry_get(registry_path(std), name) is not None or (
|
|
966
|
+
get_containerfile(f"template-{name}", containers_dir) is not None
|
|
967
|
+
)
|
|
968
|
+
if exists and not args.force:
|
|
969
|
+
print(
|
|
970
|
+
f"Error: rig '{name}' already exists; use --force to overwrite.",
|
|
971
|
+
file=sys.stderr,
|
|
972
|
+
)
|
|
973
|
+
return 1
|
|
974
|
+
|
|
975
|
+
if kind == "template":
|
|
976
|
+
# Install the Containerfile under the user-override dir; that file is the
|
|
977
|
+
# source of truth for templates, so no registry row is written.
|
|
978
|
+
dest_dir = std.data_path / "containers"
|
|
979
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
980
|
+
dest = dest_dir / f"Containerfile.template-{name}"
|
|
981
|
+
src_file = local_path if local_path is not None else Path(source)
|
|
982
|
+
shutil.copyfile(src_file, dest)
|
|
983
|
+
print(
|
|
984
|
+
f"Added template '{name}' ({dest}). "
|
|
985
|
+
f"Run 'kanibako rig prep {name}' to build it."
|
|
986
|
+
)
|
|
987
|
+
return 0
|
|
988
|
+
|
|
989
|
+
# kind == "image": a local tar (or fetched tar) is loaded now; a bare
|
|
990
|
+
# reference is recorded without pulling.
|
|
991
|
+
is_tar = local_path is not None or Path(source).is_file()
|
|
992
|
+
if is_tar:
|
|
993
|
+
archive = local_path if local_path is not None else Path(source)
|
|
994
|
+
loaded_ref = runtime.load(archive)
|
|
995
|
+
if loaded_ref is None:
|
|
996
|
+
print(
|
|
997
|
+
f"Error: failed to load image archive '{archive}'.",
|
|
998
|
+
file=sys.stderr,
|
|
999
|
+
)
|
|
1000
|
+
return 1
|
|
1001
|
+
if not loaded_ref:
|
|
1002
|
+
# Loaded, but the archive carries no RepoTag, so there is no stable
|
|
1003
|
+
# reference to run the rig by. Don't record a guessed/wrong image.
|
|
1004
|
+
print(
|
|
1005
|
+
f"Error: loaded '{archive}' but it has no image tag; re-save "
|
|
1006
|
+
"the image with a tag, or add it by reference instead.",
|
|
1007
|
+
file=sys.stderr,
|
|
1008
|
+
)
|
|
1009
|
+
return 1
|
|
1010
|
+
upsert(
|
|
1011
|
+
registry_path(std),
|
|
1012
|
+
RigRecord(
|
|
1013
|
+
name=name,
|
|
1014
|
+
kind="prefab",
|
|
1015
|
+
source=str(Path(source).resolve()) if local_path is None else source,
|
|
1016
|
+
source_type="file",
|
|
1017
|
+
image=loaded_ref,
|
|
1018
|
+
),
|
|
1019
|
+
)
|
|
1020
|
+
print(f"Added prefab '{name}' from archive.")
|
|
1021
|
+
return 0
|
|
1022
|
+
|
|
1023
|
+
upsert(
|
|
1024
|
+
registry_path(std),
|
|
1025
|
+
RigRecord(name=name, kind="prefab", source=source, source_type="ref"),
|
|
1026
|
+
)
|
|
1027
|
+
print(
|
|
1028
|
+
f"Added prefab '{name}' -> {source}. "
|
|
1029
|
+
f"Run 'kanibako rig prep {name}' to pull it."
|
|
1030
|
+
)
|
|
1031
|
+
return 0
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def run_rebuild(args: argparse.Namespace) -> int:
|
|
1035
|
+
"""Update container image(s): auto-detect local build vs registry pull."""
|
|
1036
|
+
_deprecated("rig rebuild", "rig prep --force")
|
|
1037
|
+
config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
|
|
1038
|
+
config = load_config(config_file)
|
|
1039
|
+
std = load_std_paths(config)
|
|
1040
|
+
containers_dir = std.data_path / "containers"
|
|
1041
|
+
|
|
1042
|
+
try:
|
|
1043
|
+
runtime = ContainerRuntime()
|
|
1044
|
+
except ContainerError as e:
|
|
1045
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
1046
|
+
return 1
|
|
1047
|
+
|
|
1048
|
+
if args.all_images:
|
|
1049
|
+
return _update_all(runtime, containers_dir)
|
|
1050
|
+
|
|
1051
|
+
# Determine which image to update
|
|
1052
|
+
merged = load_merged_config(config_file, None)
|
|
1053
|
+
image = args.image
|
|
1054
|
+
if image is None:
|
|
1055
|
+
image = merged.box_image
|
|
1056
|
+
else:
|
|
1057
|
+
image = resolve_image_name(image, merged.box_image)
|
|
1058
|
+
|
|
1059
|
+
return _update_one(runtime, image, containers_dir)
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
def _pull_one(runtime: ContainerRuntime, image: str) -> int:
|
|
1063
|
+
"""Pull a single image from the registry."""
|
|
1064
|
+
print(f"Pulling {image}...")
|
|
1065
|
+
print()
|
|
1066
|
+
if runtime.pull(image, quiet=False):
|
|
1067
|
+
print()
|
|
1068
|
+
print(f"Successfully pulled {image}")
|
|
1069
|
+
return 0
|
|
1070
|
+
else:
|
|
1071
|
+
print()
|
|
1072
|
+
print(f"Failed to pull {image}", file=sys.stderr)
|
|
1073
|
+
return 1
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
def _build_one(runtime: ContainerRuntime, image: str, containers_dir: Path) -> int:
|
|
1077
|
+
"""Build a single image locally from its Containerfile."""
|
|
1078
|
+
suffix = runtime.guess_containerfile(image)
|
|
1079
|
+
if suffix is None:
|
|
1080
|
+
print(f"Error: cannot determine Containerfile for rig: {image}", file=sys.stderr)
|
|
1081
|
+
print("Known patterns: " + ", ".join(
|
|
1082
|
+
f"{p} -> Containerfile.{s}"
|
|
1083
|
+
for p, s in sorted(set(
|
|
1084
|
+
(p, runtime.guess_containerfile(p))
|
|
1085
|
+
for p in ["kanibako-min", "kanibako-oci", "kanibako-lxc",
|
|
1086
|
+
"kanibako-vm"]
|
|
1087
|
+
))
|
|
1088
|
+
), file=sys.stderr)
|
|
1089
|
+
return 1
|
|
1090
|
+
|
|
1091
|
+
containerfile = get_containerfile(suffix, containers_dir)
|
|
1092
|
+
if containerfile is None:
|
|
1093
|
+
print(f"Error: Containerfile not found for variant: {suffix}", file=sys.stderr)
|
|
1094
|
+
return 1
|
|
1095
|
+
|
|
1096
|
+
build_args: dict[str, str] = {}
|
|
1097
|
+
base = runtime.get_base_image(image)
|
|
1098
|
+
if base:
|
|
1099
|
+
build_args["BASE_IMAGE"] = base
|
|
1100
|
+
variant = runtime.get_variant(image)
|
|
1101
|
+
if variant:
|
|
1102
|
+
build_args["VARIANT"] = variant
|
|
1103
|
+
|
|
1104
|
+
print(f"Building {image} from Containerfile.{suffix}...")
|
|
1105
|
+
print()
|
|
1106
|
+
rc = runtime.rebuild(image, containerfile, containerfile.parent, build_args=build_args)
|
|
1107
|
+
if rc == 0:
|
|
1108
|
+
print()
|
|
1109
|
+
print(f"Successfully built {image}")
|
|
1110
|
+
else:
|
|
1111
|
+
print()
|
|
1112
|
+
print(f"Build failed with exit code {rc}", file=sys.stderr)
|
|
1113
|
+
return rc
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
def _update_one(
|
|
1117
|
+
runtime: ContainerRuntime, image: str, containers_dir: Path,
|
|
1118
|
+
) -> int:
|
|
1119
|
+
"""Update a single image: build locally if Containerfile exists, else pull."""
|
|
1120
|
+
suffix = runtime.guess_containerfile(image)
|
|
1121
|
+
if suffix is not None and containers_dir.is_dir():
|
|
1122
|
+
containerfile = get_containerfile(suffix, containers_dir)
|
|
1123
|
+
if containerfile is not None:
|
|
1124
|
+
return _build_one(runtime, image, containers_dir)
|
|
1125
|
+
return _pull_one(runtime, image)
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
def _update_all(
|
|
1129
|
+
runtime: ContainerRuntime, containers_dir: Path,
|
|
1130
|
+
) -> int:
|
|
1131
|
+
"""Update all local kanibako images."""
|
|
1132
|
+
images = runtime.list_local_images()
|
|
1133
|
+
if not images:
|
|
1134
|
+
print("No local kanibako rigs to update.")
|
|
1135
|
+
return 0
|
|
1136
|
+
|
|
1137
|
+
failed = 0
|
|
1138
|
+
for repo, _size in images:
|
|
1139
|
+
print(f"\n{'=' * 60}")
|
|
1140
|
+
print(f"Updating {repo}")
|
|
1141
|
+
print('=' * 60)
|
|
1142
|
+
rc = _update_one(runtime, repo, containers_dir)
|
|
1143
|
+
if rc != 0:
|
|
1144
|
+
failed += 1
|
|
1145
|
+
|
|
1146
|
+
print()
|
|
1147
|
+
if failed:
|
|
1148
|
+
print(f"Updated {len(images) - failed}/{len(images)} rigs ({failed} failed)")
|
|
1149
|
+
return 1
|
|
1150
|
+
else:
|
|
1151
|
+
print(f"Updated {len(images)} rig(s) successfully.")
|
|
1152
|
+
return 0
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
def run_export(args: argparse.Namespace) -> int:
|
|
1156
|
+
"""Export an *extended* rig to a portable ``.rig.tgz`` bundle.
|
|
1157
|
+
|
|
1158
|
+
Only extended rigs export: prefabs/templates have an external source and
|
|
1159
|
+
travel by locator ('rig add'). Saves ``kanibako-rig-<name>`` to an
|
|
1160
|
+
``image.tar``, reconstructs the sidecar ``rig.yaml`` from the registry row
|
|
1161
|
+
(the authoritative copy still rides inside the image), and packs both.
|
|
1162
|
+
"""
|
|
1163
|
+
config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
|
|
1164
|
+
config = load_config(config_file)
|
|
1165
|
+
std = load_std_paths(config)
|
|
1166
|
+
|
|
1167
|
+
try:
|
|
1168
|
+
runtime = ContainerRuntime()
|
|
1169
|
+
except ContainerError as e:
|
|
1170
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
1171
|
+
return 1
|
|
1172
|
+
|
|
1173
|
+
try:
|
|
1174
|
+
image = rig_image_name(args.name)
|
|
1175
|
+
except ValueError as e:
|
|
1176
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
1177
|
+
return 1
|
|
1178
|
+
|
|
1179
|
+
registry = load_registry(registry_path(std))
|
|
1180
|
+
record = registry.get(args.name)
|
|
1181
|
+
|
|
1182
|
+
is_extended = (
|
|
1183
|
+
record is not None and record.kind == "extended"
|
|
1184
|
+
) or runtime.image_exists(image)
|
|
1185
|
+
if not is_extended:
|
|
1186
|
+
print(
|
|
1187
|
+
f"Error: '{args.name}' is not an extended rig; export is only for "
|
|
1188
|
+
"extended rigs. Prefabs and templates travel by locator -- use "
|
|
1189
|
+
"'rig add'.",
|
|
1190
|
+
file=sys.stderr,
|
|
1191
|
+
)
|
|
1192
|
+
return 1
|
|
1193
|
+
|
|
1194
|
+
if not runtime.image_exists(image):
|
|
1195
|
+
print(
|
|
1196
|
+
f"Error: rig image '{image}' is not present locally; nothing to "
|
|
1197
|
+
"export (prep or import it first).",
|
|
1198
|
+
file=sys.stderr,
|
|
1199
|
+
)
|
|
1200
|
+
return 1
|
|
1201
|
+
|
|
1202
|
+
# Reconstruct the sidecar metadata from the registry row. The authoritative
|
|
1203
|
+
# copy still rides inside image.tar at /etc/kanibako/rig.yaml.
|
|
1204
|
+
if record is not None:
|
|
1205
|
+
meta = RigMeta(
|
|
1206
|
+
name=args.name,
|
|
1207
|
+
kind="extended",
|
|
1208
|
+
parent=record.parent,
|
|
1209
|
+
foundation_source=record.foundation_source,
|
|
1210
|
+
reproducible=bool(record.reproducible),
|
|
1211
|
+
created=record.created,
|
|
1212
|
+
)
|
|
1213
|
+
else:
|
|
1214
|
+
meta = RigMeta(name=args.name)
|
|
1215
|
+
|
|
1216
|
+
out = Path(args.out) if args.out else Path(f"{args.name}{BUNDLE_SUFFIX}")
|
|
1217
|
+
|
|
1218
|
+
with tempfile.TemporaryDirectory() as td:
|
|
1219
|
+
rig_yaml = Path(td) / "rig.yaml"
|
|
1220
|
+
write_rig_meta(meta, rig_yaml)
|
|
1221
|
+
image_tar = Path(td) / "image.tar"
|
|
1222
|
+
if not runtime.save(image, image_tar):
|
|
1223
|
+
print(f"Error: failed to save image '{image}'.", file=sys.stderr)
|
|
1224
|
+
return 1
|
|
1225
|
+
pack_bundle(out, rig_yaml, image_tar)
|
|
1226
|
+
print(f"Exported '{args.name}' -> {out}")
|
|
1227
|
+
return 0
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
def run_import(args: argparse.Namespace) -> int:
|
|
1231
|
+
"""Import an *extended* rig from a ``.rig.tgz`` bundle.
|
|
1232
|
+
|
|
1233
|
+
Loads the bundle's ``image.tar`` and records an extended registry row from
|
|
1234
|
+
the bundle's ``rig.yaml`` metadata.
|
|
1235
|
+
"""
|
|
1236
|
+
config_file = config_file_path(xdg("XDG_CONFIG_HOME", ".config"))
|
|
1237
|
+
config = load_config(config_file)
|
|
1238
|
+
std = load_std_paths(config)
|
|
1239
|
+
|
|
1240
|
+
try:
|
|
1241
|
+
runtime = ContainerRuntime()
|
|
1242
|
+
except ContainerError as e:
|
|
1243
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
1244
|
+
return 1
|
|
1245
|
+
|
|
1246
|
+
tgz = Path(args.file)
|
|
1247
|
+
if not tgz.is_file():
|
|
1248
|
+
print(f"Error: bundle not found: {tgz}", file=sys.stderr)
|
|
1249
|
+
return 1
|
|
1250
|
+
try:
|
|
1251
|
+
meta = read_bundle_meta(tgz)
|
|
1252
|
+
except (ValueError, OSError, tarfile.ReadError) as e:
|
|
1253
|
+
print(f"Error: not a valid rig bundle: {e}", file=sys.stderr)
|
|
1254
|
+
return 1
|
|
1255
|
+
|
|
1256
|
+
try:
|
|
1257
|
+
target = rig_image_name(meta.name)
|
|
1258
|
+
except ValueError as e:
|
|
1259
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
1260
|
+
return 1
|
|
1261
|
+
|
|
1262
|
+
with tempfile.TemporaryDirectory() as td:
|
|
1263
|
+
try:
|
|
1264
|
+
members = unpack_bundle(tgz, Path(td))
|
|
1265
|
+
except ValueError as e:
|
|
1266
|
+
print(f"Error: invalid rig bundle: {e}", file=sys.stderr)
|
|
1267
|
+
return 1
|
|
1268
|
+
loaded_ref = runtime.load(members["image_tar"])
|
|
1269
|
+
if loaded_ref is None:
|
|
1270
|
+
print("Error: failed to load the bundle's image.", file=sys.stderr)
|
|
1271
|
+
return 1
|
|
1272
|
+
# Our own bundles save kanibako-rig-<name>, so the loaded image already
|
|
1273
|
+
# carries the right repo name (image_exists(target) resolves :latest).
|
|
1274
|
+
# Auto-retag of a foreign/mistagged bundle is a deferred refinement; we
|
|
1275
|
+
# deliberately do NOT add a 'podman tag' wrapper here (Task 4.1 wrappers
|
|
1276
|
+
# are frozen). Such a bundle is rejected with a clear error instead.
|
|
1277
|
+
if not runtime.image_exists(target):
|
|
1278
|
+
print(
|
|
1279
|
+
f"Error: loaded image '{loaded_ref or '<untagged>'}' does not "
|
|
1280
|
+
f"match expected '{target}'; the bundle may be foreign or "
|
|
1281
|
+
"mistagged.",
|
|
1282
|
+
file=sys.stderr,
|
|
1283
|
+
)
|
|
1284
|
+
return 1
|
|
1285
|
+
|
|
1286
|
+
upsert(
|
|
1287
|
+
registry_path(std),
|
|
1288
|
+
RigRecord(
|
|
1289
|
+
name=meta.name,
|
|
1290
|
+
kind="extended",
|
|
1291
|
+
image=target,
|
|
1292
|
+
parent=meta.parent,
|
|
1293
|
+
foundation_source=meta.foundation_source,
|
|
1294
|
+
reproducible=bool(meta.reproducible),
|
|
1295
|
+
created=meta.created,
|
|
1296
|
+
source_type="import",
|
|
1297
|
+
),
|
|
1298
|
+
)
|
|
1299
|
+
print(f"Imported extended rig '{meta.name}' as {target}.")
|
|
1300
|
+
return 0
|