This adds a stage called org.osbuild.skopeo that installs docker and oci archive files into the container storage of the tree being constructed. The source can either be a file from another pipeline, for example one created with the existing org.osbuild.oci-archive stage, or it can be using the new org.osbuild.skopeo source and org.osbuild.containers input, which will download an image from a registry and install that. There is an optional option in the install stage that lets you configure a custom storage location, which allows the use of the additionalimagestores option in the container storage.conf to use a read-only image stores (instead of /var/lib/container). Note: skopeo fails to start if /etc/containers/policy.json is not available, so we bind mount it from the build tree to the buildroot if available.
134 lines
4.1 KiB
Python
Executable file
134 lines
4.1 KiB
Python
Executable file
#!/usr/bin/python3
|
|
"""Fetch container image from a registry using skopeo
|
|
|
|
Buildhost commands used: `skopeo`.
|
|
"""
|
|
|
|
import errno
|
|
import os
|
|
import sys
|
|
import subprocess
|
|
import tempfile
|
|
import hashlib
|
|
|
|
from osbuild import sources
|
|
from osbuild.util import ctx
|
|
|
|
SCHEMA = """
|
|
"additionalProperties": false,
|
|
"definitions": {
|
|
"item": {
|
|
"description": "The container image to fetch indexed by the container image id",
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"patternProperties": {
|
|
"sha256:[0-9a-f]{64}": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"required": ["image"],
|
|
"properties": {
|
|
"image": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"required": ["name", "digest"],
|
|
"properties": {
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Name of the image (including registry)."
|
|
},
|
|
"digest": {
|
|
"type": "string",
|
|
"description": "Digest of image in registry.",
|
|
"pattern": "sha256:[0-9a-f]{64}"
|
|
},
|
|
"tls-verify": {
|
|
"type": "boolean",
|
|
"description": "Require https (default true)."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"properties": {
|
|
"items": {"$ref": "#/definitions/item"},
|
|
"digests": {"$ref": "#/definitions/item"}
|
|
},
|
|
"oneOf": [{
|
|
"required": ["items"]
|
|
}, {
|
|
"required": ["digests"]
|
|
}]
|
|
"""
|
|
|
|
|
|
def download(items, cache):
|
|
for image_id, item in items.items():
|
|
image = item["image"]
|
|
imagename = image["name"]
|
|
digest = image["digest"]
|
|
tls_verify = image.get("tls-verify", True)
|
|
|
|
path = f"{cache}/{image_id}"
|
|
if os.path.isdir(path):
|
|
continue
|
|
|
|
with tempfile.TemporaryDirectory(prefix="tmp-download-", dir=cache) as tmpdir:
|
|
archive_path = os.path.join(tmpdir, "container-image.tar")
|
|
|
|
source = f"docker://{imagename}@{digest}"
|
|
|
|
# We use the docker format, not oci, because that is the
|
|
# default return image type of real world registries,
|
|
# allowing the image to get the same image id as if you
|
|
# did "podman pull" (rather than converting the image to
|
|
# oci format, changing the id)
|
|
destination = f"docker-archive:{archive_path}"
|
|
|
|
extra_args = []
|
|
|
|
# The archive format can't store signatures, but we still verify them during download
|
|
extra_args.append("--remove-signatures")
|
|
|
|
if not tls_verify:
|
|
extra_args.append("--src-tls-verify=false")
|
|
|
|
subprocess.run(["skopeo", "copy"] + extra_args + [source, destination],
|
|
encoding="utf-8",
|
|
check=True)
|
|
|
|
# Verify that the digest supplied downloaded the correct container image id.
|
|
# The image id is the digest of the config, but skopeo can' currently
|
|
# get the config id, only the full config, so we checksum it ourselves.
|
|
res = subprocess.run(["skopeo", "inspect", "--raw", "--config", destination],
|
|
capture_output=True,
|
|
check=True)
|
|
downloaded_id = "sha256:" + hashlib.sha256(res.stdout).hexdigest()
|
|
if downloaded_id != image_id:
|
|
raise RuntimeError(
|
|
f"Downloaded image {imagename}@{digest} has a id of {downloaded_id}, but expected {image_id}")
|
|
|
|
# Atomically move download dir into place on successful download
|
|
os.chmod(tmpdir, 0o755)
|
|
with ctx.suppress_oserror(errno.ENOTEMPTY, errno.EEXIST):
|
|
os.rename(tmpdir, path)
|
|
|
|
|
|
class SkopeoSource(sources.SourceService):
|
|
|
|
def download(self, items, cache, _options):
|
|
cache = os.path.join(cache, "org.osbuild.containers")
|
|
os.makedirs(cache, exist_ok=True)
|
|
|
|
download(items, cache)
|
|
|
|
|
|
def main():
|
|
service = SkopeoSource.from_args(sys.argv[1:])
|
|
service.main()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|