stages/org.osbuild.skopeo: support for dir and oci-archive
This commit is contained in:
parent
a3f86a0736
commit
7de7838534
3 changed files with 139 additions and 2 deletions
|
|
@ -1,5 +1,6 @@
|
|||
#!/usr/bin/python3
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
|
@ -19,9 +20,10 @@ def main(inputs, output, options):
|
|||
storage_root = destination.get("storage-path", "/var/lib/containers/storage")
|
||||
storage_driver = destination.get("storage-driver", "overlay")
|
||||
dest = f"containers-storage:[{storage_driver}@{output}{storage_root}+/run/containers/storage]{image_name}"
|
||||
elif dest_type == "oci":
|
||||
elif dest_type in ("oci", "oci-archive", "dir"):
|
||||
path = destination["path"]
|
||||
dest = f"oci:{output}{path}"
|
||||
dest = f"{dest_type}:{output}{path}"
|
||||
pathlib.Path(f"{output}{path}").parent.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
raise ValueError(f"Unknown destination type '{dest_type}'")
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,44 @@
|
|||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"destination-oci-archive": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"type",
|
||||
"path"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"oci-archive"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"description": "Location of a tar archive compliant with 'Open Container Image Layout Specification'",
|
||||
"type": "string"
|
||||
},
|
||||
}
|
||||
},
|
||||
"destination-dir": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"type",
|
||||
"path"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"dir"
|
||||
]
|
||||
},
|
||||
"path": {
|
||||
"description": "Location of a directory storing the manifest, layer tarballs and signatures as individual files. This is a non-standardized format, primarily useful for debugging or noninvasive container inspection.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
|
|
@ -80,6 +118,12 @@
|
|||
},
|
||||
{
|
||||
"$ref": "#/definitions/destination-oci"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/destination-oci-archive"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/destination-dir"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
#!/usr/bin/python3
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from osbuild import testutil
|
||||
from osbuild.testutil import has_executable, make_container
|
||||
|
||||
STAGE_NAME = "org.osbuild.skopeo"
|
||||
|
||||
|
|
@ -13,8 +17,14 @@ STAGE_NAME = "org.osbuild.skopeo"
|
|||
({"destination": {}}, "is not valid under any of the given schemas"),
|
||||
({"destination": {"type": "foo"}}, "is not valid under any of the given schemas"),
|
||||
({"destination": {"type": "oci"}}, "is not valid under any of the given schemas"),
|
||||
({"destination": {"type": "dir"}}, "is not valid under any of the given schemas"),
|
||||
({"destination": {"type": "os-archive"}}, "is not valid under any of the given schemas"),
|
||||
({"destination": {"type": "dir", "path": "/foo"}, "remove-signatures": "YesPlease"},
|
||||
"'YesPlease' is not of type 'boolean'"),
|
||||
# good
|
||||
({"destination": {"type": "oci", "path": "/foo"}}, ""),
|
||||
({"destination": {"type": "oci-archive", "path": "/foo"}}, ""),
|
||||
({"destination": {"type": "dir", "path": "/foo"}}, ""),
|
||||
|
||||
# this one might not be expected but it's valid because we don't require any
|
||||
# *inputs* and it'll be a no-op in the stage
|
||||
|
|
@ -33,3 +43,84 @@ def test_schema_validation_skopeo(stage_schema, test_data, expected_err):
|
|||
else:
|
||||
assert res.valid is False
|
||||
testutil.assert_jsonschema_error_contains(res, expected_err, expected_num_errs=1)
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.getuid() != 0, reason="needs root")
|
||||
@pytest.mark.skipif(not has_executable("podman"), reason="no podman executable")
|
||||
@pytest.mark.parametrize("dest_type,local_output_path", [
|
||||
("dir", "/tmp/test-output-skopeo-dir"),
|
||||
("oci-archive", "/tmp/test-output-skopeo.tar"),
|
||||
("oci", "/tmp/test-output-skopeo-dir"),
|
||||
])
|
||||
def test_skopeo_copy(tmp_path, stage_module, dest_type, local_output_path):
|
||||
with make_container(tmp_path, {"file1": "file1 from final layer"}) as cont_tag:
|
||||
# export for the container-deploy stage
|
||||
fake_container_dst = tmp_path / "fake-container"
|
||||
subprocess.check_call([
|
||||
"podman", "save",
|
||||
"--format=oci-archive",
|
||||
f"--output={fake_container_dst}",
|
||||
cont_tag,
|
||||
])
|
||||
|
||||
inputs = {
|
||||
"images": {
|
||||
# seems to be unused with fake_container_path?
|
||||
"path": fake_container_dst,
|
||||
"data": {
|
||||
"archives": {
|
||||
fake_container_dst: {
|
||||
"format": "oci-archive",
|
||||
"name": cont_tag,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
output_dir = tmp_path / "output"
|
||||
options = {
|
||||
"destination": {
|
||||
"type": dest_type,
|
||||
"path": local_output_path
|
||||
}
|
||||
}
|
||||
|
||||
stage_module.main(inputs, output_dir, options)
|
||||
|
||||
assert output_dir.exists()
|
||||
|
||||
result = output_dir / local_output_path.lstrip("/")
|
||||
assert result.exists()
|
||||
|
||||
if dest_type == "dir":
|
||||
assert (result / "version").exists()
|
||||
|
||||
manifest_file = (result / "manifest.json")
|
||||
elif dest_type == "oci-archive":
|
||||
# TBD extract TAR and check content
|
||||
assert result.exists()
|
||||
elif dest_type == "oci":
|
||||
assert (result / "oci-layout").exists()
|
||||
|
||||
index_file = (result / "index.json")
|
||||
assert index_file.exists()
|
||||
|
||||
data = json.loads(index_file.read_bytes())
|
||||
|
||||
assert data.get("manifests") is not None, "'index.json' seems corrupt - no 'manifests' section found"
|
||||
assert data["manifests"][0].get("digest") is not None, \
|
||||
"'manifest.json' seems corrupt - no 'manifests[0].digest' section found"
|
||||
data_digest = data["manifests"][0]["digest"].split(':')
|
||||
|
||||
manifest_file = result / "blobs" / data_digest[0] / data_digest[1]
|
||||
|
||||
if dest_type in ["dir", "oci"]:
|
||||
assert manifest_file.exists()
|
||||
|
||||
data = json.loads(manifest_file.read_bytes())
|
||||
|
||||
assert data.get("config") is not None, "'manifest.json' seems corrupt - no 'config' section found"
|
||||
|
||||
assert data["config"].get("digest") is not None, \
|
||||
"'manifest.json' seems corrupt - no 'config.digest' section found"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue