Skip to content

Commit 3c0417a

Browse files
committed
Add support for converting to OCI artifacts
Add Podman >= 5.7.0 version requirement for artifact tests Artifact support requires Podman 5.7.0 or later. This commit adds version checking to skip artifact tests on systems with older Podman versions. Changes: - Add get_podman_version() and skip_if_podman_too_old() functions to test/system/helpers.bash - Add skip_if_podman_too_old decorator to test/conftest.py for e2e tests - Apply version check to all artifact-related tests in: - test/system/056-artifact.bats (BATS tests) - test/e2e/test_artifact.py (pytest e2e tests) - Tests will be skipped with clear message on systems with Podman < 5.7.0 - Docker and nocontainer tests are also skipped as they don't support artifacts The version check extracts the Podman version and compares it numerically, handling development versions like "5.7.0-dev" correctly. Cursor-AI-Generated Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>
1 parent 17c2a46 commit 3c0417a

25 files changed

+1580
-146
lines changed

docs/ramalama-convert.1.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,15 @@ Image to use when converting to GGUF format (when then `--gguf` option has been
3939
executable and available in the `PATH`. The script is available from the `llama.cpp` GitHub repo. Defaults to the current
4040
`quay.io/ramalama/ramalama-rag` image.
4141

42-
#### **--type**=*raw* | *car*
42+
#### **--type**="artifact" | *raw* | *car*
4343

44-
type of OCI Model Image to convert.
44+
Convert the MODEL to the specified OCI Object
4545

46-
| Type | Description |
47-
| ---- | ------------------------------------------------------------- |
48-
| car | Includes base image with the model stored in a /models subdir |
49-
| raw | Only the model and a link file model.file to it stored at / |
46+
| Type | Description |
47+
| -------- | ------------------------------------------------------------- |
48+
| artifact | Store AI Models as artifacts |
49+
| car | Traditional OCI image including base image with the model stored in a /models subdir |
50+
| raw | Traditional OCI image including only the model and a link file `model.file` pointed at it stored at / |
5051

5152
## EXAMPLE
5253

docs/ramalama.conf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@
3232
#
3333
#carimage = "registry.access.redhat.com/ubi10-micro:latest"
3434

35+
# Convert the MODEL to the specified OCI Object
36+
# Options: artifact, car, raw
37+
#
38+
# artifact: Store AI Models as artifacts
39+
# car: Traditional OCI image including base image with the model stored in a /models subdir
40+
# raw: Traditional OCI image including only the model and a link file `model.file` pointed at it stored at /
41+
#convert_type = "raw"
42+
3543
# Run RamaLama in the default container.
3644
#
3745
#container = true

docs/ramalama.conf.5.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,18 @@ Min chunk size to attempt reusing from the cache via KV shifting
8484
Run RamaLama in the default container.
8585
RAMALAMA_IN_CONTAINER environment variable overrides this field.
8686

87+
**convert_type**="raw"
88+
89+
Convert the MODEL to the specified OCI Object
90+
Options: artifact, car, raw
91+
92+
| Type | Description |
93+
| -------- | ------------------------------------------------------------- |
94+
| artifact | Store AI Models as artifacts |
95+
| car | Traditional OCI image including base image with the model stored in a /models subdir |
96+
| raw | Traditional OCI image including only the model and a link file `model.file` pointed at it stored at / |
97+
98+
8799
**ctx_size**=0
88100

89101
Size of the prompt context (0 = loaded from model)

ramalama/cli.py

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -714,11 +714,12 @@ def convert_parser(subparsers):
714714
)
715715
parser.add_argument(
716716
"--type",
717-
default="raw",
718-
choices=["car", "raw"],
717+
default=CONFIG.convert_type,
718+
choices=["artifact", "car", "raw"],
719719
help="""\
720720
type of OCI Model Image to push.
721721
722+
Model "artifact" stores the AI Model as an OCI Artifact.
722723
Model "car" includes base image with the model stored in a /models subdir.
723724
Model "raw" contains the model and a link file model.file to it stored at /.""",
724725
)
@@ -755,11 +756,12 @@ def push_parser(subparsers):
755756
add_network_argument(parser)
756757
parser.add_argument(
757758
"--type",
758-
default="raw",
759-
choices=["car", "raw"],
759+
default=CONFIG.convert_type,
760+
choices=["artifact", "car", "raw"],
760761
help="""\
761762
type of OCI Model Image to push.
762763
764+
Model "artifact" stores the AI Model as an OCI Artifact.
763765
Model "car" includes base image with the model stored in a /models subdir.
764766
Model "raw" contains the model and a link file model.file to it stored at /.""",
765767
)
@@ -774,20 +776,28 @@ def push_parser(subparsers):
774776
parser.set_defaults(func=push_cli)
775777

776778

777-
def _get_source_model(args):
779+
def _get_source_model(args, transport=None):
778780
src = shortnames.resolve(args.SOURCE)
779-
smodel = New(src, args)
781+
smodel = New(src, args, transport=transport)
780782
if smodel.type == "OCI":
783+
if not args.TARGET:
784+
return smodel
781785
raise ValueError(f"converting from an OCI based image {src} is not supported")
782786
if not smodel.exists() and not args.dryrun:
783787
smodel.pull(args)
784788
return smodel
785789

786790

787791
def push_cli(args):
788-
source_model = _get_source_model(args)
789792
target = args.SOURCE
793+
transport = None
794+
if not args.TARGET:
795+
transport = "oci"
796+
source_model = _get_source_model(args, transport=transport)
797+
790798
if args.TARGET:
799+
if source_model.type == "OCI":
800+
raise ValueError(f"converting from an OCI based image {args.SOURCE} is not supported")
791801
target = shortnames.resolve(args.TARGET)
792802
target_model = New(target, args)
793803

@@ -1169,9 +1179,14 @@ def serve_cli(args):
11691179
model.ensure_model_exists(args)
11701180
except KeyError as e:
11711181
try:
1182+
if "://" in args.MODEL:
1183+
raise e
11721184
args.quiet = True
11731185
model = TransportFactory(args.MODEL, args, ignore_stderr=True).create_oci()
11741186
model.ensure_model_exists(args)
1187+
# Since this is a OCI model, prepend oci://
1188+
args.MODEL = f"oci://{args.MODEL}"
1189+
11751190
except Exception:
11761191
raise e
11771192

@@ -1412,27 +1427,42 @@ def rm_parser(subparsers):
14121427
parser.set_defaults(func=rm_cli)
14131428

14141429

1430+
def _rm_oci_model(model, args) -> bool:
1431+
# attempt to remove as a container image
1432+
try:
1433+
m = TransportFactory(model, args, ignore_stderr=True).create_oci()
1434+
return m.remove(args)
1435+
except Exception:
1436+
return False
1437+
1438+
14151439
def _rm_model(models, args):
1440+
exceptions = []
14161441
for model in models:
14171442
model = shortnames.resolve(model)
14181443

14191444
try:
14201445
m = New(model, args)
1421-
m.remove(args)
1422-
except KeyError as e:
1446+
if m.remove(args):
1447+
continue
1448+
# Failed to remove and might be OCI so attempt to remove OCI
1449+
if args.ignore:
1450+
_rm_oci_model(model, args)
1451+
continue
1452+
except (KeyError, subprocess.CalledProcessError) as e:
14231453
for prefix in MODEL_TYPES:
14241454
if model.startswith(prefix + "://"):
14251455
if not args.ignore:
14261456
raise e
1427-
try:
1428-
# attempt to remove as a container image
1429-
m = TransportFactory(model, args, ignore_stderr=True).create_oci()
1430-
m.remove(args)
1431-
return
1432-
except Exception:
1433-
pass
1434-
if not args.ignore:
1435-
raise e
1457+
# attempt to remove as a container image
1458+
if _rm_oci_model(model, args) or args.ignore:
1459+
continue
1460+
exceptions.append(e)
1461+
1462+
if len(exceptions) > 0:
1463+
for exception in exceptions[:1]:
1464+
perror(exception)
1465+
raise exceptions[0]
14361466

14371467

14381468
def rm_cli(args):
@@ -1524,9 +1554,11 @@ def eprint(e, exit_code):
15241554
args.func(args)
15251555
except urllib.error.HTTPError as e:
15261556
eprint(f"pulling {e.geturl()} failed: {e}", errno.EINVAL)
1557+
except FileNotFoundError as e:
1558+
eprint(e, errno.ENOENT)
15271559
except HelpException:
15281560
parser.print_help()
1529-
except (ConnectionError, IndexError, KeyError, ValueError, NoRefFileFound) as e:
1561+
except (IsADirectoryError, ConnectionError, IndexError, KeyError, ValueError, NoRefFileFound) as e:
15301562
eprint(e, errno.EINVAL)
15311563
except NotImplementedError as e:
15321564
eprint(e, errno.ENOSYS)

ramalama/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ def verify_checksum(filename: str) -> bool:
283283

284284

285285
def genname():
286-
return "ramalama_" + "".join(random.choices(string.ascii_letters + string.digits, k=10))
286+
return "ramalama-" + "".join(random.choices(string.ascii_letters + string.digits, k=10))
287287

288288

289289
def engine_version(engine: SUPPORTED_ENGINES) -> str:

ramalama/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ class BaseConfig:
221221
carimage: str = "registry.access.redhat.com/ubi10-micro:latest"
222222
container: bool = None # type: ignore
223223
ctx_size: int = 0
224+
convert_type: Literal["artifact", "car", "raw"] = "raw"
224225
default_image: str = DEFAULT_IMAGE
225226
default_rag_image: str = DEFAULT_RAG_IMAGE
226227
dryrun: bool = False

ramalama/kube.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22
from typing import Optional, Tuple
33

4-
from ramalama.common import MNT_DIR, RAG_DIR, genname, get_accel_env_vars
4+
from ramalama.common import MNT_DIR, RAG_DIR, get_accel_env_vars
55
from ramalama.file import PlainFile
66
from ramalama.version import version
77

@@ -15,6 +15,7 @@ def __init__(
1515
mmproj_paths: Optional[Tuple[str, str]],
1616
args,
1717
exec_args,
18+
artifact: bool,
1819
):
1920
self.src_model_path, self.dest_model_path = model_paths
2021
self.src_chat_template_path, self.dest_chat_template_path = (
@@ -27,27 +28,30 @@ def __init__(
2728
if getattr(args, "name", None):
2829
self.name = args.name
2930
else:
30-
self.name = genname()
31+
self.name = "ramalama"
3132

3233
self.args = args
3334
self.exec_args = exec_args
3435
self.image = args.image
36+
self.artifact = artifact
3537

3638
def _gen_volumes(self):
3739
mounts = """\
3840
volumeMounts:"""
3941

4042
volumes = """
4143
volumes:"""
42-
4344
if os.path.exists(self.src_model_path):
4445
m, v = self._gen_path_volume()
4546
mounts += m
4647
volumes += v
4748
else:
49+
subPath = ""
50+
if not self.artifact:
51+
subPath = """
52+
subPath: /models"""
4853
mounts += f"""
49-
- mountPath: {MNT_DIR}
50-
subPath: /models
54+
- mountPath: {MNT_DIR}{subPath}
5155
name: model"""
5256
volumes += self._gen_oci_volume()
5357

@@ -98,7 +102,7 @@ def _gen_path_volume(self):
98102
def _gen_oci_volume(self):
99103
return f"""
100104
- image:
101-
reference: {self.ai_image}
105+
reference: {self.src_model_path}
102106
pullPolicy: IfNotPresent
103107
name: model"""
104108

@@ -162,7 +166,7 @@ def __gen_env_vars():
162166
for k, v in env_vars.items():
163167
env_spec += f"""
164168
- name: {k}
165-
value: {v}"""
169+
value: \"{v}\""""
166170

167171
return env_spec
168172

@@ -177,7 +181,7 @@ def generate(self) -> PlainFile:
177181
# it into Kubernetes.
178182
#
179183
# Created with ramalama-{_version}
180-
apiVersion: v1
184+
apiVersion: apps/v1
181185
kind: Deployment
182186
metadata:
183187
name: {self.name}

0 commit comments

Comments
 (0)