debian-forge/sources/org.osbuild.skopeo
Alexander Larsson 46a228df38 Add support for installing containers in images
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.
2022-02-10 14:43:17 +01:00

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()