kub-cli 0.2.0__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.
- kub_cli/__init__.py +17 -0
- kub_cli/__main__.py +12 -0
- kub_cli/cli.py +292 -0
- kub_cli/commands.py +164 -0
- kub_cli/config.py +555 -0
- kub_cli/errors.py +32 -0
- kub_cli/img_cli.py +306 -0
- kub_cli/img_integration.py +265 -0
- kub_cli/img_tools.py +434 -0
- kub_cli/logging_utils.py +31 -0
- kub_cli/runtime.py +463 -0
- kub_cli/versioning.py +175 -0
- kub_cli-0.2.0.dist-info/METADATA +344 -0
- kub_cli-0.2.0.dist-info/RECORD +17 -0
- kub_cli-0.2.0.dist-info/WHEEL +4 -0
- kub_cli-0.2.0.dist-info/entry_points.txt +6 -0
- kub_cli-0.2.0.dist-info/licenses/LICENSE +206 -0
kub_cli/img_cli.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 University of Strasbourg
|
|
2
|
+
# SPDX-FileContributor: Christophe Prud'homme
|
|
3
|
+
# SPDX-FileContributor: Cemosis
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""Typer CLI for kub-img image management."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
from . import __version__
|
|
15
|
+
from .errors import KubCliError
|
|
16
|
+
from .logging_utils import configureLogging
|
|
17
|
+
from .img_tools import KubImgManager, resolveImgConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
imgApp = typer.Typer(
|
|
21
|
+
add_completion=False,
|
|
22
|
+
help="Manage kub runtime images for Apptainer and Docker.",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def buildImgManager(
|
|
27
|
+
*,
|
|
28
|
+
runtime: str | None,
|
|
29
|
+
image: str | None,
|
|
30
|
+
runner: str | None,
|
|
31
|
+
verbose: bool | None,
|
|
32
|
+
showConfig: bool,
|
|
33
|
+
) -> KubImgManager:
|
|
34
|
+
config = resolveImgConfig(
|
|
35
|
+
runtime=runtime,
|
|
36
|
+
image=image,
|
|
37
|
+
runner=runner,
|
|
38
|
+
verbose=verbose,
|
|
39
|
+
)
|
|
40
|
+
configureLogging(config.verbose)
|
|
41
|
+
|
|
42
|
+
if showConfig:
|
|
43
|
+
print(json.dumps(config.toDict(), indent=2, sort_keys=True))
|
|
44
|
+
|
|
45
|
+
return KubImgManager(config=config)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def exitOnError(error: KubCliError) -> None:
|
|
49
|
+
typer.secho(str(error), fg=typer.colors.RED, err=True)
|
|
50
|
+
raise typer.Exit(code=error.exit_code) from error
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@imgApp.callback(invoke_without_command=True)
|
|
54
|
+
def root(
|
|
55
|
+
ctx: typer.Context,
|
|
56
|
+
version: bool = typer.Option(
|
|
57
|
+
False,
|
|
58
|
+
"--version",
|
|
59
|
+
is_eager=True,
|
|
60
|
+
help="Show kub-cli version and exit.",
|
|
61
|
+
),
|
|
62
|
+
) -> None:
|
|
63
|
+
if version:
|
|
64
|
+
typer.echo(f"kub-cli {__version__}")
|
|
65
|
+
raise typer.Exit(code=0)
|
|
66
|
+
|
|
67
|
+
if ctx.invoked_subcommand is None:
|
|
68
|
+
typer.echo(ctx.get_help())
|
|
69
|
+
raise typer.Exit(code=0)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@imgApp.command("pull")
|
|
73
|
+
def pullImageCommand(
|
|
74
|
+
source: str | None = typer.Argument(
|
|
75
|
+
None,
|
|
76
|
+
metavar="SOURCE",
|
|
77
|
+
help=(
|
|
78
|
+
"Optional pull source. "
|
|
79
|
+
"For Apptainer use oras://... (never docker://). "
|
|
80
|
+
"If omitted, source is derived from configuration."
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
runtime: str | None = typer.Option(
|
|
84
|
+
None,
|
|
85
|
+
"--runtime",
|
|
86
|
+
metavar="{auto,apptainer,docker}",
|
|
87
|
+
help="Container runtime selection.",
|
|
88
|
+
),
|
|
89
|
+
image: str | None = typer.Option(
|
|
90
|
+
None,
|
|
91
|
+
"--image",
|
|
92
|
+
metavar="IMAGE",
|
|
93
|
+
help="Runtime image path/reference (destination for pull).",
|
|
94
|
+
),
|
|
95
|
+
runner: str | None = typer.Option(
|
|
96
|
+
None,
|
|
97
|
+
"--runner",
|
|
98
|
+
metavar="PATH",
|
|
99
|
+
help="Runtime executable path or command name.",
|
|
100
|
+
),
|
|
101
|
+
force: bool = typer.Option(
|
|
102
|
+
False,
|
|
103
|
+
"--force",
|
|
104
|
+
help="Overwrite destination image when runtime is Apptainer.",
|
|
105
|
+
),
|
|
106
|
+
disableCache: bool = typer.Option(
|
|
107
|
+
False,
|
|
108
|
+
"--disable-cache",
|
|
109
|
+
help="Disable cache when runtime is Apptainer.",
|
|
110
|
+
),
|
|
111
|
+
apptainerFlags: list[str] | None = typer.Option(
|
|
112
|
+
None,
|
|
113
|
+
"--apptainer-flag",
|
|
114
|
+
metavar="FLAG",
|
|
115
|
+
help="Additional apptainer pull flags (repeatable).",
|
|
116
|
+
),
|
|
117
|
+
dockerFlags: list[str] | None = typer.Option(
|
|
118
|
+
None,
|
|
119
|
+
"--docker-flag",
|
|
120
|
+
metavar="FLAG",
|
|
121
|
+
help="Additional docker pull flags (repeatable).",
|
|
122
|
+
),
|
|
123
|
+
dryRun: bool = typer.Option(
|
|
124
|
+
False,
|
|
125
|
+
"--dry-run",
|
|
126
|
+
help="Print the pull command without executing it.",
|
|
127
|
+
),
|
|
128
|
+
verbose: bool | None = typer.Option(
|
|
129
|
+
None,
|
|
130
|
+
"--verbose/--no-verbose",
|
|
131
|
+
help="Enable or disable verbose logging.",
|
|
132
|
+
),
|
|
133
|
+
showConfig: bool = typer.Option(
|
|
134
|
+
False,
|
|
135
|
+
"--show-config",
|
|
136
|
+
help="Print effective kub-cli configuration as JSON.",
|
|
137
|
+
),
|
|
138
|
+
) -> None:
|
|
139
|
+
try:
|
|
140
|
+
manager = buildImgManager(
|
|
141
|
+
runtime=runtime,
|
|
142
|
+
image=image,
|
|
143
|
+
runner=runner,
|
|
144
|
+
verbose=verbose,
|
|
145
|
+
showConfig=showConfig,
|
|
146
|
+
)
|
|
147
|
+
exitCode = manager.pullImage(
|
|
148
|
+
runtime=runtime,
|
|
149
|
+
source=source,
|
|
150
|
+
force=force,
|
|
151
|
+
disableCache=disableCache,
|
|
152
|
+
apptainerFlags=apptainerFlags or [],
|
|
153
|
+
dockerFlags=dockerFlags or [],
|
|
154
|
+
dryRun=dryRun,
|
|
155
|
+
)
|
|
156
|
+
except KubCliError as error:
|
|
157
|
+
exitOnError(error)
|
|
158
|
+
|
|
159
|
+
raise typer.Exit(code=exitCode)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@imgApp.command("info")
|
|
163
|
+
def infoCommand(
|
|
164
|
+
runtime: str | None = typer.Option(
|
|
165
|
+
None,
|
|
166
|
+
"--runtime",
|
|
167
|
+
metavar="{auto,apptainer,docker}",
|
|
168
|
+
help="Container runtime selection.",
|
|
169
|
+
),
|
|
170
|
+
image: str | None = typer.Option(
|
|
171
|
+
None,
|
|
172
|
+
"--image",
|
|
173
|
+
metavar="IMAGE",
|
|
174
|
+
help="Runtime image path/reference.",
|
|
175
|
+
),
|
|
176
|
+
runner: str | None = typer.Option(
|
|
177
|
+
None,
|
|
178
|
+
"--runner",
|
|
179
|
+
metavar="PATH",
|
|
180
|
+
help="Runtime executable path or command name.",
|
|
181
|
+
),
|
|
182
|
+
jsonOutput: bool = typer.Option(
|
|
183
|
+
False,
|
|
184
|
+
"--json",
|
|
185
|
+
help="Emit image info as JSON.",
|
|
186
|
+
),
|
|
187
|
+
verbose: bool | None = typer.Option(
|
|
188
|
+
None,
|
|
189
|
+
"--verbose/--no-verbose",
|
|
190
|
+
help="Enable or disable verbose logging.",
|
|
191
|
+
),
|
|
192
|
+
showConfig: bool = typer.Option(
|
|
193
|
+
False,
|
|
194
|
+
"--show-config",
|
|
195
|
+
help="Print effective kub-cli configuration as JSON.",
|
|
196
|
+
),
|
|
197
|
+
) -> None:
|
|
198
|
+
try:
|
|
199
|
+
manager = buildImgManager(
|
|
200
|
+
runtime=runtime,
|
|
201
|
+
image=image,
|
|
202
|
+
runner=runner,
|
|
203
|
+
verbose=verbose,
|
|
204
|
+
showConfig=showConfig,
|
|
205
|
+
)
|
|
206
|
+
exitCode = manager.printInfo(runtime=runtime, jsonOutput=jsonOutput)
|
|
207
|
+
except KubCliError as error:
|
|
208
|
+
exitOnError(error)
|
|
209
|
+
|
|
210
|
+
raise typer.Exit(code=exitCode)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@imgApp.command("apps")
|
|
214
|
+
def appsCommand(
|
|
215
|
+
runtime: str | None = typer.Option(
|
|
216
|
+
"apptainer",
|
|
217
|
+
"--runtime",
|
|
218
|
+
metavar="{apptainer}",
|
|
219
|
+
help="Runtime selection for app listing (Apptainer only).",
|
|
220
|
+
),
|
|
221
|
+
image: str | None = typer.Option(
|
|
222
|
+
None,
|
|
223
|
+
"--image",
|
|
224
|
+
metavar="IMAGE",
|
|
225
|
+
help="Apptainer image path.",
|
|
226
|
+
),
|
|
227
|
+
runner: str | None = typer.Option(
|
|
228
|
+
None,
|
|
229
|
+
"--runner",
|
|
230
|
+
metavar="PATH",
|
|
231
|
+
help="Apptainer executable path or command name.",
|
|
232
|
+
),
|
|
233
|
+
verbose: bool | None = typer.Option(
|
|
234
|
+
None,
|
|
235
|
+
"--verbose/--no-verbose",
|
|
236
|
+
help="Enable or disable verbose logging.",
|
|
237
|
+
),
|
|
238
|
+
showConfig: bool = typer.Option(
|
|
239
|
+
False,
|
|
240
|
+
"--show-config",
|
|
241
|
+
help="Print effective kub-cli configuration as JSON.",
|
|
242
|
+
),
|
|
243
|
+
) -> None:
|
|
244
|
+
try:
|
|
245
|
+
manager = buildImgManager(
|
|
246
|
+
runtime=runtime,
|
|
247
|
+
image=image,
|
|
248
|
+
runner=runner,
|
|
249
|
+
verbose=verbose,
|
|
250
|
+
showConfig=showConfig,
|
|
251
|
+
)
|
|
252
|
+
exitCode = manager.printApps(runtime=runtime)
|
|
253
|
+
except KubCliError as error:
|
|
254
|
+
exitOnError(error)
|
|
255
|
+
|
|
256
|
+
raise typer.Exit(code=exitCode)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@imgApp.command("path")
|
|
260
|
+
def pathCommand(
|
|
261
|
+
runtime: str | None = typer.Option(
|
|
262
|
+
None,
|
|
263
|
+
"--runtime",
|
|
264
|
+
metavar="{auto,apptainer,docker}",
|
|
265
|
+
help="Runtime selection for resolved image path/reference.",
|
|
266
|
+
),
|
|
267
|
+
image: str | None = typer.Option(
|
|
268
|
+
None,
|
|
269
|
+
"--image",
|
|
270
|
+
metavar="IMAGE",
|
|
271
|
+
help="Image path/reference override.",
|
|
272
|
+
),
|
|
273
|
+
runner: str | None = typer.Option(
|
|
274
|
+
None,
|
|
275
|
+
"--runner",
|
|
276
|
+
metavar="PATH",
|
|
277
|
+
help="Runtime executable path or command name.",
|
|
278
|
+
),
|
|
279
|
+
verbose: bool | None = typer.Option(
|
|
280
|
+
None,
|
|
281
|
+
"--verbose/--no-verbose",
|
|
282
|
+
help="Enable or disable verbose logging.",
|
|
283
|
+
),
|
|
284
|
+
showConfig: bool = typer.Option(
|
|
285
|
+
False,
|
|
286
|
+
"--show-config",
|
|
287
|
+
help="Print effective kub-cli configuration as JSON.",
|
|
288
|
+
),
|
|
289
|
+
) -> None:
|
|
290
|
+
try:
|
|
291
|
+
manager = buildImgManager(
|
|
292
|
+
runtime=runtime,
|
|
293
|
+
image=image,
|
|
294
|
+
runner=runner,
|
|
295
|
+
verbose=verbose,
|
|
296
|
+
showConfig=showConfig,
|
|
297
|
+
)
|
|
298
|
+
exitCode = manager.printImagePath(runtime=runtime)
|
|
299
|
+
except KubCliError as error:
|
|
300
|
+
exitOnError(error)
|
|
301
|
+
|
|
302
|
+
raise typer.Exit(code=exitCode)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def imgMain() -> None:
|
|
306
|
+
imgApp(prog_name="kub-img")
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 University of Strasbourg
|
|
2
|
+
# SPDX-FileContributor: Christophe Prud'homme
|
|
3
|
+
# SPDX-FileContributor: Cemosis
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""Internal helpers to invoke the kub-img utility via subprocess."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
from typing import Any, Literal
|
|
17
|
+
|
|
18
|
+
from .config import KubConfig, SUPPORTED_RUNTIMES
|
|
19
|
+
from .errors import KubCliError, RuntimeSelectionError
|
|
20
|
+
from .logging_utils import LOGGER, formatCommand
|
|
21
|
+
from .runtime import (
|
|
22
|
+
deriveApptainerOrasReference,
|
|
23
|
+
getRuntimeCandidateImage,
|
|
24
|
+
getRunnerValue,
|
|
25
|
+
tryResolveRunnerExecutable,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
ImageRuntime = Literal["apptainer", "docker"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class KubImgPullRequest:
|
|
34
|
+
"""Resolved arguments for a kub-img pull operation."""
|
|
35
|
+
|
|
36
|
+
runtime: ImageRuntime
|
|
37
|
+
image: str
|
|
38
|
+
source: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class KubImgInfoRequest:
|
|
43
|
+
"""Resolved arguments for a kub-img info operation."""
|
|
44
|
+
|
|
45
|
+
runtime: ImageRuntime
|
|
46
|
+
image: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class KubImgCommandRunner:
|
|
51
|
+
"""Invoke `kub-img` as a subprocess safely."""
|
|
52
|
+
|
|
53
|
+
executable: str = "kub-img"
|
|
54
|
+
verbose: bool = False
|
|
55
|
+
|
|
56
|
+
def resolveExecutable(self) -> str:
|
|
57
|
+
executablePath = Path(self.executable).expanduser()
|
|
58
|
+
hasPathSeparator = executablePath.parent != Path(".")
|
|
59
|
+
|
|
60
|
+
if executablePath.is_absolute() or hasPathSeparator:
|
|
61
|
+
if executablePath.exists() and os.access(executablePath, os.X_OK):
|
|
62
|
+
return str(executablePath)
|
|
63
|
+
raise KubCliError(f"kub-img executable not found or not executable: '{executablePath}'")
|
|
64
|
+
|
|
65
|
+
resolved = shutil.which(self.executable)
|
|
66
|
+
if resolved is None:
|
|
67
|
+
raise KubCliError(
|
|
68
|
+
"kub-img executable not found in PATH. Install kub-cli with the kub-img entrypoint."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return resolved
|
|
72
|
+
|
|
73
|
+
def pullImage(self, request: KubImgPullRequest, *, dryRun: bool = False) -> int:
|
|
74
|
+
executable = self.resolveExecutable()
|
|
75
|
+
command = [
|
|
76
|
+
executable,
|
|
77
|
+
"pull",
|
|
78
|
+
"--runtime",
|
|
79
|
+
request.runtime,
|
|
80
|
+
"--image",
|
|
81
|
+
request.image,
|
|
82
|
+
request.source,
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
if self.verbose:
|
|
86
|
+
command.append("--verbose")
|
|
87
|
+
LOGGER.debug("Running kub-img pull command: %s", formatCommand(command))
|
|
88
|
+
|
|
89
|
+
if dryRun:
|
|
90
|
+
print(formatCommand(command))
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
completed = subprocess.run(command, check=False)
|
|
95
|
+
except OSError as error:
|
|
96
|
+
raise KubCliError(f"Unable to execute kub-img pull command: {error}") from error
|
|
97
|
+
|
|
98
|
+
if completed.returncode != 0:
|
|
99
|
+
raise KubCliError(
|
|
100
|
+
f"kub-img pull failed with exit code {completed.returncode}.",
|
|
101
|
+
exit_code=completed.returncode,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
def inspectImageInfo(self, request: KubImgInfoRequest) -> dict[str, Any]:
|
|
107
|
+
executable = self.resolveExecutable()
|
|
108
|
+
command = [
|
|
109
|
+
executable,
|
|
110
|
+
"info",
|
|
111
|
+
"--runtime",
|
|
112
|
+
request.runtime,
|
|
113
|
+
"--image",
|
|
114
|
+
request.image,
|
|
115
|
+
"--json",
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
if self.verbose:
|
|
119
|
+
command.append("--verbose")
|
|
120
|
+
LOGGER.debug("Running kub-img info command: %s", formatCommand(command))
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
completed = subprocess.run(
|
|
124
|
+
command,
|
|
125
|
+
check=False,
|
|
126
|
+
capture_output=True,
|
|
127
|
+
text=True,
|
|
128
|
+
)
|
|
129
|
+
except OSError as error:
|
|
130
|
+
raise KubCliError(f"Unable to execute kub-img info command: {error}") from error
|
|
131
|
+
|
|
132
|
+
if completed.returncode != 0:
|
|
133
|
+
stderrText = completed.stderr.strip()
|
|
134
|
+
message = (
|
|
135
|
+
f"kub-img info failed with exit code {completed.returncode}. "
|
|
136
|
+
f"{stderrText}".strip()
|
|
137
|
+
)
|
|
138
|
+
raise KubCliError(message, exit_code=completed.returncode)
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
payload = json.loads(completed.stdout)
|
|
142
|
+
except json.JSONDecodeError as error:
|
|
143
|
+
raise KubCliError(
|
|
144
|
+
"kub-img info returned non-JSON output while --json was requested."
|
|
145
|
+
) from error
|
|
146
|
+
|
|
147
|
+
if not isinstance(payload, dict):
|
|
148
|
+
raise KubCliError("kub-img info JSON output must be an object.")
|
|
149
|
+
|
|
150
|
+
return payload
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def resolveImageRuntime(config: KubConfig) -> ImageRuntime:
|
|
154
|
+
"""Resolve runtime for image pull/info operations."""
|
|
155
|
+
|
|
156
|
+
configured = config.runtime.strip().lower()
|
|
157
|
+
if configured not in SUPPORTED_RUNTIMES:
|
|
158
|
+
supported = ", ".join(sorted(SUPPORTED_RUNTIMES))
|
|
159
|
+
raise RuntimeSelectionError(
|
|
160
|
+
f"Invalid runtime value '{config.runtime}'. Use one of: {supported}."
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if configured in {"apptainer", "docker"}:
|
|
164
|
+
return configured
|
|
165
|
+
|
|
166
|
+
apptainerRunnerValue = getRunnerValue(config, "apptainer")
|
|
167
|
+
apptainerRunner = tryResolveRunnerExecutable(apptainerRunnerValue)
|
|
168
|
+
if apptainerRunner is not None:
|
|
169
|
+
try:
|
|
170
|
+
resolveApptainerLocalImageReference(config)
|
|
171
|
+
return "apptainer"
|
|
172
|
+
except KubCliError:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
dockerRunnerValue = getRunnerValue(config, "docker")
|
|
176
|
+
dockerRunner = tryResolveRunnerExecutable(dockerRunnerValue)
|
|
177
|
+
dockerImage = getRuntimeCandidateImage(config, "docker")
|
|
178
|
+
if dockerRunner is not None and dockerImage is not None:
|
|
179
|
+
return "docker"
|
|
180
|
+
|
|
181
|
+
raise RuntimeSelectionError(
|
|
182
|
+
"Unable to resolve runtime in auto mode for image operations. "
|
|
183
|
+
"Neither Apptainer nor Docker runner is available."
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def buildKubImgInfoRequest(config: KubConfig) -> KubImgInfoRequest:
|
|
188
|
+
runtime = resolveImageRuntime(config)
|
|
189
|
+
|
|
190
|
+
if runtime == "docker":
|
|
191
|
+
dockerImage = getRuntimeCandidateImage(config, "docker")
|
|
192
|
+
if dockerImage is None:
|
|
193
|
+
raise KubCliError(
|
|
194
|
+
"No Docker image configured for image info. "
|
|
195
|
+
"Set --image, KUB_IMAGE_DOCKER, or KUB_IMAGE."
|
|
196
|
+
)
|
|
197
|
+
return KubImgInfoRequest(runtime="docker", image=dockerImage)
|
|
198
|
+
|
|
199
|
+
apptainerImage = resolveApptainerLocalImageReference(config)
|
|
200
|
+
return KubImgInfoRequest(runtime="apptainer", image=apptainerImage)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def buildKubImgPullRequest(config: KubConfig) -> KubImgPullRequest:
|
|
204
|
+
runtime = resolveImageRuntime(config)
|
|
205
|
+
|
|
206
|
+
if runtime == "docker":
|
|
207
|
+
dockerImage = getRuntimeCandidateImage(config, "docker")
|
|
208
|
+
if dockerImage is None:
|
|
209
|
+
raise KubCliError(
|
|
210
|
+
"No Docker image configured for pull. "
|
|
211
|
+
"Set --image, KUB_IMAGE_DOCKER, or KUB_IMAGE."
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return KubImgPullRequest(
|
|
215
|
+
runtime="docker",
|
|
216
|
+
image=dockerImage,
|
|
217
|
+
source=dockerImage,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
destinationImage = resolveApptainerLocalImageReference(config)
|
|
221
|
+
|
|
222
|
+
explicitApptainerReference = config.imageApptainer
|
|
223
|
+
if explicitApptainerReference is not None and explicitApptainerReference.startswith("oras://"):
|
|
224
|
+
source = explicitApptainerReference
|
|
225
|
+
else:
|
|
226
|
+
dockerImage = getRuntimeCandidateImage(config, "docker")
|
|
227
|
+
if dockerImage is None:
|
|
228
|
+
raise KubCliError(
|
|
229
|
+
"Unable to derive Apptainer ORAS source: no Docker image is configured. "
|
|
230
|
+
"Set KUB_IMAGE_DOCKER (or runtime image config)."
|
|
231
|
+
)
|
|
232
|
+
source = deriveApptainerOrasReference(dockerImage)
|
|
233
|
+
|
|
234
|
+
if source.startswith("docker://"):
|
|
235
|
+
raise KubCliError(
|
|
236
|
+
"Apptainer image pull source must use oras://, not docker://."
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
return KubImgPullRequest(
|
|
240
|
+
runtime="apptainer",
|
|
241
|
+
image=destinationImage,
|
|
242
|
+
source=source,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def resolveApptainerLocalImageReference(config: KubConfig) -> str:
|
|
247
|
+
candidates = [config.imageOverride, config.imageApptainer, config.image]
|
|
248
|
+
|
|
249
|
+
for candidate in candidates:
|
|
250
|
+
if candidate is None:
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
normalized = candidate.strip()
|
|
254
|
+
if not normalized:
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
if "://" in normalized:
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
return normalized
|
|
261
|
+
|
|
262
|
+
raise KubCliError(
|
|
263
|
+
"No local Apptainer image path configured. "
|
|
264
|
+
"Set --image or KUB_IMAGE_APPTAINER to a .sif file path."
|
|
265
|
+
)
|