scitex-container 0.1.2__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.
- scitex_container/__init__.py +10 -0
- scitex_container/__main__.py +6 -0
- scitex_container/_cli/__init__.py +141 -0
- scitex_container/_cli/_apptainer.py +352 -0
- scitex_container/_cli/_docker.py +48 -0
- scitex_container/_cli/_env_snapshot.py +149 -0
- scitex_container/_cli/_host.py +109 -0
- scitex_container/_cli/_mcp.py +256 -0
- scitex_container/_cli/_sandbox.py +231 -0
- scitex_container/_cli/_status.py +151 -0
- scitex_container/_mcp/__init__.py +38 -0
- scitex_container/_mcp/handlers.py +337 -0
- scitex_container/_snapshot.py +249 -0
- scitex_container/apptainer/__init__.py +74 -0
- scitex_container/apptainer/_build.py +116 -0
- scitex_container/apptainer/_command_builder.py +301 -0
- scitex_container/apptainer/_freeze.py +90 -0
- scitex_container/apptainer/_sandbox.py +253 -0
- scitex_container/apptainer/_sandbox_versioning.py +298 -0
- scitex_container/apptainer/_status.py +86 -0
- scitex_container/apptainer/_utils.py +78 -0
- scitex_container/apptainer/_verify.py +249 -0
- scitex_container/apptainer/_versioning.py +328 -0
- scitex_container/docker/__init__.py +16 -0
- scitex_container/docker/_compose.py +247 -0
- scitex_container/docker/_mounts.py +67 -0
- scitex_container/host/__init__.py +18 -0
- scitex_container/host/_mounts.py +195 -0
- scitex_container/host/_packages.py +185 -0
- scitex_container/mcp_server.py +315 -0
- scitex_container-0.1.2.dist-info/METADATA +222 -0
- scitex_container-0.1.2.dist-info/RECORD +35 -0
- scitex_container-0.1.2.dist-info/WHEEL +4 -0
- scitex_container-0.1.2.dist-info/entry_points.txt +3 -0
- scitex_container-0.1.2.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-02-25"
|
|
3
|
+
# File: src/scitex_container/__init__.py
|
|
4
|
+
"""scitex-container: Unified container management for Apptainer and Docker."""
|
|
5
|
+
|
|
6
|
+
from . import apptainer, docker, host
|
|
7
|
+
from ._snapshot import env_snapshot
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.1"
|
|
10
|
+
__all__ = ["apptainer", "docker", "host", "env_snapshot"]
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-02-25"
|
|
3
|
+
# File: src/scitex_container/_cli/__init__.py
|
|
4
|
+
"""CLI package for scitex-container.
|
|
5
|
+
|
|
6
|
+
Entry point: scitex-container (maps to main() here)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import inspect
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
from ._apptainer import (
|
|
16
|
+
build,
|
|
17
|
+
cleanup,
|
|
18
|
+
deploy,
|
|
19
|
+
freeze,
|
|
20
|
+
list_containers,
|
|
21
|
+
rollback,
|
|
22
|
+
switch,
|
|
23
|
+
verify,
|
|
24
|
+
)
|
|
25
|
+
from ._docker import docker
|
|
26
|
+
from ._env_snapshot import env_snapshot_cmd
|
|
27
|
+
from ._host import host
|
|
28
|
+
from ._mcp import mcp
|
|
29
|
+
from ._sandbox import sandbox
|
|
30
|
+
from ._status import status
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _print_help_recursive(ctx, group, prefix="scitex-container"):
|
|
34
|
+
"""Recursively print help for a group and all its subcommands/subgroups."""
|
|
35
|
+
click.secho(f"━━━ {prefix} ━━━", fg="cyan", bold=True)
|
|
36
|
+
click.echo(group.get_help(ctx))
|
|
37
|
+
|
|
38
|
+
commands = group.list_commands(ctx) or []
|
|
39
|
+
for name in sorted(commands):
|
|
40
|
+
cmd = group.get_command(ctx, name)
|
|
41
|
+
if cmd is None:
|
|
42
|
+
continue
|
|
43
|
+
sub_prefix = f"{prefix} {name}"
|
|
44
|
+
with click.Context(cmd, info_name=name, parent=ctx) as sub_ctx:
|
|
45
|
+
click.echo()
|
|
46
|
+
if isinstance(cmd, click.Group):
|
|
47
|
+
_print_help_recursive(sub_ctx, cmd, prefix=sub_prefix)
|
|
48
|
+
else:
|
|
49
|
+
click.secho(f"━━━ {sub_prefix} ━━━", fg="cyan", bold=True)
|
|
50
|
+
click.echo(cmd.get_help(sub_ctx))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@click.group()
|
|
54
|
+
@click.version_option(package_name="scitex-container")
|
|
55
|
+
@click.option(
|
|
56
|
+
"--help-recursive", is_flag=True, help="Show help for all commands recursively"
|
|
57
|
+
)
|
|
58
|
+
@click.pass_context
|
|
59
|
+
def main(ctx, help_recursive):
|
|
60
|
+
"""scitex-container: Unified container management (Apptainer + Docker + host)."""
|
|
61
|
+
if help_recursive:
|
|
62
|
+
_print_help_recursive(ctx, main)
|
|
63
|
+
ctx.exit(0)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Apptainer commands (top-level)
|
|
67
|
+
main.add_command(build)
|
|
68
|
+
main.add_command(freeze)
|
|
69
|
+
main.add_command(list_containers)
|
|
70
|
+
main.add_command(switch)
|
|
71
|
+
main.add_command(rollback)
|
|
72
|
+
main.add_command(deploy)
|
|
73
|
+
main.add_command(cleanup)
|
|
74
|
+
main.add_command(verify)
|
|
75
|
+
|
|
76
|
+
# Sub-groups
|
|
77
|
+
main.add_command(sandbox)
|
|
78
|
+
main.add_command(docker)
|
|
79
|
+
main.add_command(host)
|
|
80
|
+
main.add_command(mcp)
|
|
81
|
+
|
|
82
|
+
# Unified status dashboard
|
|
83
|
+
main.add_command(status)
|
|
84
|
+
|
|
85
|
+
# Clew reproducibility snapshot
|
|
86
|
+
main.add_command(env_snapshot_cmd)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@main.command("list-python-apis")
|
|
90
|
+
@click.option(
|
|
91
|
+
"-v", "--verbose", count=True, help="Verbosity: -v with signatures, -vv +docstring"
|
|
92
|
+
)
|
|
93
|
+
def list_python_apis(verbose: int):
|
|
94
|
+
"""List scitex_container Python APIs (apptainer, docker, host modules)."""
|
|
95
|
+
import scitex_container.apptainer as apptainer_mod
|
|
96
|
+
import scitex_container.docker as docker_mod
|
|
97
|
+
import scitex_container.host as host_mod
|
|
98
|
+
|
|
99
|
+
modules = [
|
|
100
|
+
("apptainer", apptainer_mod),
|
|
101
|
+
("docker", docker_mod),
|
|
102
|
+
("host", host_mod),
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
for mod_name, mod in modules:
|
|
106
|
+
public_names = [n for n in dir(mod) if not n.startswith("_")]
|
|
107
|
+
click.secho(f"{mod_name}: {len(public_names)} APIs", fg="green", bold=True)
|
|
108
|
+
|
|
109
|
+
for name in sorted(public_names):
|
|
110
|
+
obj = getattr(mod, name, None)
|
|
111
|
+
if obj is None:
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
if callable(obj) and not isinstance(obj, type):
|
|
115
|
+
if verbose == 0:
|
|
116
|
+
click.echo(f" {name}")
|
|
117
|
+
elif verbose >= 1:
|
|
118
|
+
try:
|
|
119
|
+
sig_str = str(inspect.signature(obj))
|
|
120
|
+
except (ValueError, TypeError):
|
|
121
|
+
sig_str = "()"
|
|
122
|
+
click.echo(f" {click.style(name, fg='white', bold=True)}{sig_str}")
|
|
123
|
+
if verbose >= 2:
|
|
124
|
+
doc = inspect.getdoc(obj)
|
|
125
|
+
if doc:
|
|
126
|
+
first_line = doc.split("\n")[0].strip()
|
|
127
|
+
click.echo(f" {first_line}")
|
|
128
|
+
elif isinstance(obj, type):
|
|
129
|
+
click.echo(f" {name} [class]")
|
|
130
|
+
else:
|
|
131
|
+
if verbose >= 1:
|
|
132
|
+
click.echo(f" {name} = {obj!r}")
|
|
133
|
+
else:
|
|
134
|
+
click.echo(f" {name}")
|
|
135
|
+
|
|
136
|
+
click.echo()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
__all__ = ["main"]
|
|
140
|
+
|
|
141
|
+
# EOF
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-02-25"
|
|
3
|
+
# File: src/scitex_container/_cli/_apptainer.py
|
|
4
|
+
"""CLI commands for Apptainer container operations."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register(main: click.Group) -> None:
|
|
12
|
+
"""Register all Apptainer commands onto main."""
|
|
13
|
+
main.add_command(build)
|
|
14
|
+
main.add_command(freeze)
|
|
15
|
+
main.add_command(list_containers)
|
|
16
|
+
main.add_command(switch)
|
|
17
|
+
main.add_command(rollback)
|
|
18
|
+
main.add_command(deploy)
|
|
19
|
+
main.add_command(cleanup)
|
|
20
|
+
main.add_command(verify)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.command()
|
|
24
|
+
@click.argument("name", default="scitex-final")
|
|
25
|
+
@click.option(
|
|
26
|
+
"--sandbox", is_flag=True, help="Build a sandbox directory instead of SIF."
|
|
27
|
+
)
|
|
28
|
+
@click.option("--base", is_flag=True, help="Build the base image instead of final.")
|
|
29
|
+
@click.option("--force", "-f", is_flag=True, help="Force rebuild even if up-to-date.")
|
|
30
|
+
@click.option("--output-dir", "-o", type=click.Path(), help="Output directory.")
|
|
31
|
+
def build(name, sandbox, base, force, output_dir):
|
|
32
|
+
"""Build a SIF or sandbox from a .def file."""
|
|
33
|
+
from scitex_container.apptainer import build as do_build
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
output_path = do_build(
|
|
37
|
+
def_name=name,
|
|
38
|
+
output_dir=output_dir,
|
|
39
|
+
force=force,
|
|
40
|
+
sandbox=sandbox,
|
|
41
|
+
)
|
|
42
|
+
click.secho(f"Built: {output_path}", fg="green")
|
|
43
|
+
except FileNotFoundError as exc:
|
|
44
|
+
click.secho(str(exc), fg="red", err=True)
|
|
45
|
+
raise SystemExit(1)
|
|
46
|
+
except RuntimeError as exc:
|
|
47
|
+
click.secho(f"Build failed: {exc}", fg="red", err=True)
|
|
48
|
+
click.secho(
|
|
49
|
+
"Tip: check that apptainer is installed and the .def file exists.",
|
|
50
|
+
fg="yellow",
|
|
51
|
+
err=True,
|
|
52
|
+
)
|
|
53
|
+
raise SystemExit(1)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@click.command()
|
|
57
|
+
@click.argument("sif_path", type=click.Path(exists=True))
|
|
58
|
+
@click.option(
|
|
59
|
+
"--output-dir", "-o", type=click.Path(), help="Output directory for lock files."
|
|
60
|
+
)
|
|
61
|
+
def freeze(sif_path, output_dir):
|
|
62
|
+
"""Extract pinned package versions (pip, dpkg, npm) from a built SIF."""
|
|
63
|
+
from scitex_container.apptainer import freeze as do_freeze
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
lock_files = do_freeze(sif_path=sif_path, output_dir=output_dir)
|
|
67
|
+
click.secho("Lock files generated:", fg="green")
|
|
68
|
+
for kind, path in lock_files.items():
|
|
69
|
+
click.echo(f" {kind}: {path}")
|
|
70
|
+
except FileNotFoundError as exc:
|
|
71
|
+
click.secho(str(exc), fg="red", err=True)
|
|
72
|
+
raise SystemExit(1)
|
|
73
|
+
except RuntimeError as exc:
|
|
74
|
+
click.secho(f"Freeze failed: {exc}", fg="red", err=True)
|
|
75
|
+
raise SystemExit(1)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@click.command(name="list")
|
|
79
|
+
@click.option(
|
|
80
|
+
"--dir", "-d", "containers_dir", type=click.Path(), help="Containers directory."
|
|
81
|
+
)
|
|
82
|
+
def list_containers(containers_dir):
|
|
83
|
+
"""List available container versions."""
|
|
84
|
+
from pathlib import Path
|
|
85
|
+
|
|
86
|
+
from scitex_container.apptainer import (
|
|
87
|
+
find_containers_dir,
|
|
88
|
+
get_active_version,
|
|
89
|
+
list_versions,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
cdir = Path(containers_dir) if containers_dir else find_containers_dir()
|
|
94
|
+
except FileNotFoundError as exc:
|
|
95
|
+
click.secho(str(exc), fg="red", err=True)
|
|
96
|
+
raise SystemExit(1)
|
|
97
|
+
|
|
98
|
+
versions = list_versions(cdir)
|
|
99
|
+
if not versions:
|
|
100
|
+
click.echo(f"No versioned SIFs found in {cdir}")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
active = get_active_version(cdir)
|
|
104
|
+
click.secho(f"Container versions in {cdir}:", fg="cyan")
|
|
105
|
+
for v in versions:
|
|
106
|
+
marker = click.style(" *", fg="green") if v["active"] else " "
|
|
107
|
+
version_str = click.style(v["version"], fg="green" if v["active"] else "white")
|
|
108
|
+
click.echo(f" {marker} {version_str} {v['size']} {v['date']}")
|
|
109
|
+
|
|
110
|
+
if active:
|
|
111
|
+
click.echo()
|
|
112
|
+
click.echo(f" Active: {click.style(active, fg='green', bold=True)}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@click.command()
|
|
116
|
+
@click.argument("version")
|
|
117
|
+
@click.option(
|
|
118
|
+
"--dir", "-d", "containers_dir", type=click.Path(), help="Containers directory."
|
|
119
|
+
)
|
|
120
|
+
@click.option(
|
|
121
|
+
"--sudo", "use_sudo", is_flag=True, help="Use sudo for symlink operations."
|
|
122
|
+
)
|
|
123
|
+
def switch(version, containers_dir, use_sudo):
|
|
124
|
+
"""Switch active container to VERSION."""
|
|
125
|
+
from pathlib import Path
|
|
126
|
+
|
|
127
|
+
from scitex_container.apptainer import (
|
|
128
|
+
find_containers_dir,
|
|
129
|
+
get_active_version,
|
|
130
|
+
switch_version,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
cdir = Path(containers_dir) if containers_dir else find_containers_dir()
|
|
135
|
+
except FileNotFoundError as exc:
|
|
136
|
+
click.secho(str(exc), fg="red", err=True)
|
|
137
|
+
raise SystemExit(1)
|
|
138
|
+
|
|
139
|
+
old_version = get_active_version(cdir)
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
switch_version(version, cdir, use_sudo=use_sudo)
|
|
143
|
+
except FileNotFoundError as exc:
|
|
144
|
+
click.secho(str(exc), fg="red", err=True)
|
|
145
|
+
click.secho(
|
|
146
|
+
"Tip: run 'scitex-container list' to see available versions.",
|
|
147
|
+
fg="yellow",
|
|
148
|
+
err=True,
|
|
149
|
+
)
|
|
150
|
+
raise SystemExit(1)
|
|
151
|
+
except RuntimeError as exc:
|
|
152
|
+
click.secho(f"Switch failed: {exc}", fg="red", err=True)
|
|
153
|
+
raise SystemExit(1)
|
|
154
|
+
|
|
155
|
+
if old_version:
|
|
156
|
+
click.secho(f"Switched {old_version} -> {version}", fg="green")
|
|
157
|
+
else:
|
|
158
|
+
click.secho(f"Activated version {version}", fg="green")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@click.command()
|
|
162
|
+
@click.option(
|
|
163
|
+
"--dir", "-d", "containers_dir", type=click.Path(), help="Containers directory."
|
|
164
|
+
)
|
|
165
|
+
@click.option(
|
|
166
|
+
"--sudo", "use_sudo", is_flag=True, help="Use sudo for symlink operations."
|
|
167
|
+
)
|
|
168
|
+
def rollback(containers_dir, use_sudo):
|
|
169
|
+
"""Revert to the previous container version."""
|
|
170
|
+
from pathlib import Path
|
|
171
|
+
|
|
172
|
+
from scitex_container.apptainer import find_containers_dir, get_active_version
|
|
173
|
+
from scitex_container.apptainer import rollback as do_rollback
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
cdir = Path(containers_dir) if containers_dir else find_containers_dir()
|
|
177
|
+
except FileNotFoundError as exc:
|
|
178
|
+
click.secho(str(exc), fg="red", err=True)
|
|
179
|
+
raise SystemExit(1)
|
|
180
|
+
|
|
181
|
+
old_version = get_active_version(cdir)
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
new_version = do_rollback(cdir, use_sudo=use_sudo)
|
|
185
|
+
except RuntimeError as exc:
|
|
186
|
+
click.secho(f"Rollback failed: {exc}", fg="red", err=True)
|
|
187
|
+
click.secho(
|
|
188
|
+
"Tip: rollback requires at least two versions to be present.",
|
|
189
|
+
fg="yellow",
|
|
190
|
+
err=True,
|
|
191
|
+
)
|
|
192
|
+
raise SystemExit(1)
|
|
193
|
+
|
|
194
|
+
click.secho(f"Rolled back {old_version} -> {new_version}", fg="green")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@click.command()
|
|
198
|
+
@click.option(
|
|
199
|
+
"--target",
|
|
200
|
+
"-t",
|
|
201
|
+
"target_dir",
|
|
202
|
+
type=click.Path(),
|
|
203
|
+
default="/opt/scitex/singularity",
|
|
204
|
+
show_default=True,
|
|
205
|
+
help="Deployment target directory.",
|
|
206
|
+
)
|
|
207
|
+
@click.option(
|
|
208
|
+
"--dir",
|
|
209
|
+
"-d",
|
|
210
|
+
"containers_dir",
|
|
211
|
+
type=click.Path(),
|
|
212
|
+
help="Source containers directory.",
|
|
213
|
+
)
|
|
214
|
+
def deploy(target_dir, containers_dir):
|
|
215
|
+
"""Copy active SIF to production target directory."""
|
|
216
|
+
from pathlib import Path
|
|
217
|
+
|
|
218
|
+
from scitex_container.apptainer import find_containers_dir
|
|
219
|
+
from scitex_container.apptainer import deploy as do_deploy
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
cdir = Path(containers_dir) if containers_dir else find_containers_dir()
|
|
223
|
+
except FileNotFoundError as exc:
|
|
224
|
+
click.secho(str(exc), fg="red", err=True)
|
|
225
|
+
raise SystemExit(1)
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
do_deploy(source_dir=cdir, target_dir=Path(target_dir))
|
|
229
|
+
except (FileNotFoundError, RuntimeError) as exc:
|
|
230
|
+
click.secho(f"Deploy failed: {exc}", fg="red", err=True)
|
|
231
|
+
click.secho(
|
|
232
|
+
"Tip: ensure an active version is set (run 'scitex-container switch').",
|
|
233
|
+
fg="yellow",
|
|
234
|
+
err=True,
|
|
235
|
+
)
|
|
236
|
+
raise SystemExit(1)
|
|
237
|
+
|
|
238
|
+
click.secho(f"Deployed to {target_dir}", fg="green")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@click.command()
|
|
242
|
+
@click.option(
|
|
243
|
+
"--keep",
|
|
244
|
+
"-k",
|
|
245
|
+
type=int,
|
|
246
|
+
default=3,
|
|
247
|
+
show_default=True,
|
|
248
|
+
help="Number of recent versions to keep.",
|
|
249
|
+
)
|
|
250
|
+
@click.option(
|
|
251
|
+
"--dir", "-d", "containers_dir", type=click.Path(), help="Containers directory."
|
|
252
|
+
)
|
|
253
|
+
def cleanup(keep, containers_dir):
|
|
254
|
+
"""Remove old container versions, keeping the N most recent."""
|
|
255
|
+
from pathlib import Path
|
|
256
|
+
|
|
257
|
+
from scitex_container.apptainer import find_containers_dir
|
|
258
|
+
from scitex_container.apptainer import cleanup as do_cleanup
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
cdir = Path(containers_dir) if containers_dir else find_containers_dir()
|
|
262
|
+
except FileNotFoundError as exc:
|
|
263
|
+
click.secho(str(exc), fg="red", err=True)
|
|
264
|
+
raise SystemExit(1)
|
|
265
|
+
|
|
266
|
+
removed = do_cleanup(cdir, keep=keep)
|
|
267
|
+
|
|
268
|
+
if removed:
|
|
269
|
+
click.secho(f"Removed {len(removed)} old version(s):", fg="yellow")
|
|
270
|
+
for path in removed:
|
|
271
|
+
click.echo(f" {path.name}")
|
|
272
|
+
else:
|
|
273
|
+
click.secho("No old versions to remove.", fg="green")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@click.command()
|
|
277
|
+
@click.argument("sif_path", required=False)
|
|
278
|
+
@click.option("--def", "def_path", help="Path to .def file to verify against.")
|
|
279
|
+
@click.option("--lock-dir", help="Directory containing lock files.")
|
|
280
|
+
@click.option("--json", "as_json", is_flag=True, help="Output raw JSON.")
|
|
281
|
+
def verify(sif_path, def_path, lock_dir, as_json):
|
|
282
|
+
"""Verify SIF integrity: hash, .def origin, and lock file consistency."""
|
|
283
|
+
import json as json_mod
|
|
284
|
+
|
|
285
|
+
from scitex_container.apptainer import (
|
|
286
|
+
find_containers_dir,
|
|
287
|
+
get_active_version,
|
|
288
|
+
verify as do_verify,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if not sif_path:
|
|
292
|
+
try:
|
|
293
|
+
cdir = find_containers_dir()
|
|
294
|
+
active = get_active_version(cdir)
|
|
295
|
+
if active:
|
|
296
|
+
sif_path = str(cdir / f"scitex-v{active}.sif")
|
|
297
|
+
else:
|
|
298
|
+
click.secho(
|
|
299
|
+
"No active SIF found. Provide SIF_PATH argument.",
|
|
300
|
+
fg="red",
|
|
301
|
+
err=True,
|
|
302
|
+
)
|
|
303
|
+
raise SystemExit(1)
|
|
304
|
+
except FileNotFoundError as exc:
|
|
305
|
+
click.secho(str(exc), fg="red", err=True)
|
|
306
|
+
raise SystemExit(1)
|
|
307
|
+
|
|
308
|
+
result = do_verify(sif_path=sif_path, def_path=def_path, lock_dir=lock_dir)
|
|
309
|
+
|
|
310
|
+
if as_json:
|
|
311
|
+
click.echo(json_mod.dumps(result, indent=2))
|
|
312
|
+
else:
|
|
313
|
+
_print_verify_result(result)
|
|
314
|
+
|
|
315
|
+
raise SystemExit(0 if result["overall"] == "pass" else 1)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _print_verify_result(result: dict) -> None:
|
|
319
|
+
"""Pretty-print verification results."""
|
|
320
|
+
status_colors = {"pass": "green", "fail": "red", "skip": "yellow"}
|
|
321
|
+
|
|
322
|
+
click.secho("Container Verification Report", fg="cyan", bold=True)
|
|
323
|
+
click.echo()
|
|
324
|
+
|
|
325
|
+
# SIF
|
|
326
|
+
sif = result["sif"]
|
|
327
|
+
click.echo(f" SIF: {sif['path']}")
|
|
328
|
+
if sif["exists"]:
|
|
329
|
+
click.echo(f" SHA256: {sif['sha256']}")
|
|
330
|
+
else:
|
|
331
|
+
click.secho(" NOT FOUND", fg="red")
|
|
332
|
+
click.echo()
|
|
333
|
+
|
|
334
|
+
# Checks
|
|
335
|
+
for check_name, label in [
|
|
336
|
+
("def_origin", ".def Origin"),
|
|
337
|
+
("pip_lock", "pip Packages"),
|
|
338
|
+
("dpkg_lock", "dpkg Packages"),
|
|
339
|
+
]:
|
|
340
|
+
check = result[check_name]
|
|
341
|
+
status = check["status"]
|
|
342
|
+
color = status_colors.get(status, "white")
|
|
343
|
+
badge = click.style(f"[{status.upper()}]", fg=color, bold=True)
|
|
344
|
+
click.echo(f" {badge} {label}: {check['detail']}")
|
|
345
|
+
|
|
346
|
+
click.echo()
|
|
347
|
+
overall = result["overall"]
|
|
348
|
+
color = "green" if overall == "pass" else "red"
|
|
349
|
+
click.secho(f" Overall: {overall.upper()}", fg=color, bold=True)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# EOF
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2026-02-25"
|
|
3
|
+
# File: src/scitex_container/_cli/_docker.py
|
|
4
|
+
"""CLI docker sub-group for Docker Compose management."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
def docker():
|
|
13
|
+
"""Manage Docker Compose services."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@docker.command(name="rebuild")
|
|
17
|
+
@click.option(
|
|
18
|
+
"--env", "-e", default="dev", show_default=True, help="Environment (dev/prod)."
|
|
19
|
+
)
|
|
20
|
+
def docker_rebuild(env):
|
|
21
|
+
"""Rebuild Docker containers without cache."""
|
|
22
|
+
from scitex_container.docker import rebuild as do_rebuild
|
|
23
|
+
|
|
24
|
+
click.secho(f"Rebuilding Docker containers for env={env}...", fg="cyan")
|
|
25
|
+
rc = do_rebuild(env=env)
|
|
26
|
+
if rc != 0:
|
|
27
|
+
click.secho(f"docker compose build exited with code {rc}", fg="red", err=True)
|
|
28
|
+
raise SystemExit(rc)
|
|
29
|
+
click.secho("Rebuild complete.", fg="green")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@docker.command(name="restart")
|
|
33
|
+
@click.option(
|
|
34
|
+
"--env", "-e", default="dev", show_default=True, help="Environment (dev/prod)."
|
|
35
|
+
)
|
|
36
|
+
def docker_restart(env):
|
|
37
|
+
"""Restart Docker containers (down then up -d)."""
|
|
38
|
+
from scitex_container.docker import restart as do_restart
|
|
39
|
+
|
|
40
|
+
click.secho(f"Restarting Docker containers for env={env}...", fg="cyan")
|
|
41
|
+
rc = do_restart(env=env)
|
|
42
|
+
if rc != 0:
|
|
43
|
+
click.secho(f"docker compose restart exited with code {rc}", fg="red", err=True)
|
|
44
|
+
raise SystemExit(rc)
|
|
45
|
+
click.secho("Containers restarted.", fg="green")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# EOF
|