Implement fetch_all() and fetch_one() as calls to exists() to make sure we check that the containers are available every time they are needed.
102 lines
3.6 KiB
Python
Executable file
102 lines
3.6 KiB
Python
Executable file
#!/usr/bin/python3
|
|
"""Provide access to an image in the host system's container storage.
|
|
|
|
This stage differs from other sources in that the source checks to see if the
|
|
container is available in the host's container storage. This puts a requirement
|
|
on the user to ensure that the container is copied into local storage before
|
|
trying to build an image. The starts by reading the host's
|
|
`/etc/containers/storage.conf` file and then using the config to check if the
|
|
container has been imported.
|
|
|
|
Unlike all other sources, this source relies on external storage not controlled
|
|
by osbuild itself.
|
|
|
|
Buildhost commands used: `skopeo`.
|
|
"""
|
|
|
|
import concurrent.futures
|
|
import hashlib
|
|
import subprocess as sp
|
|
import sys
|
|
|
|
from osbuild import sources
|
|
from osbuild.util import containers
|
|
|
|
SCHEMA = """
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"items": {
|
|
"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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
|
|
|
|
class ContainersStorageSource(sources.SourceService):
|
|
|
|
content_type = "org.osbuild.containers-storage"
|
|
|
|
storage_conf = None
|
|
|
|
def local_image_name(self, imagename):
|
|
"""
|
|
Construct the full image name that references an image with a given checksum in the local storage.
|
|
"""
|
|
conf = containers.get_host_storage(self.storage_conf)
|
|
driver = conf["storage"]["driver"]
|
|
graphroot = conf["storage"]["graphroot"]
|
|
runroot = conf["storage"]["runroot"]
|
|
|
|
return f"containers-storage:[{driver}@{graphroot}+{runroot}]{imagename}"
|
|
|
|
def fetch_one(self, checksum, desc) -> None:
|
|
# Instead of fetching anything, just check that it exists.
|
|
#
|
|
# Note that there's an obvious TOCTOU issue here, but it's unavoidable without copying the storage or a
|
|
# container out of it, which is exactly what we want to avoid with this source.
|
|
# Unlike all other sources, this source relies on external storage not controlled by osbuild itself.
|
|
self.exists(checksum, desc)
|
|
|
|
def fetch_all(self, items) -> None:
|
|
# prepare each item as a (checksum, desc) tuple (where desc=None)
|
|
transformed = map(lambda i: self.transform(i, None), items)
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
|
for _ in executor.map(self.fetch_one, *zip(*transformed)):
|
|
pass
|
|
|
|
def exists(self, checksum, _) -> bool:
|
|
image_id = checksum.split(":")[1]
|
|
source = self.local_image_name(image_id)
|
|
res = sp.run(["skopeo", "inspect", "--raw", "--config", source],
|
|
check=False, capture_output=True, universal_newlines=True)
|
|
|
|
# fail early if the user hasn't imported the container into
|
|
# containers-storage
|
|
if res.returncode != 0:
|
|
raise RuntimeError(f"Container does not exist in local containers storage: {res.stderr}")
|
|
|
|
# NOTE: this is a bit redundant because we're checking the content digest of the thing we retrieved via its
|
|
# id (which is the content digest), but let's keep it in case we change to retrieving local containers by name
|
|
local_id = "sha256:" + hashlib.sha256(res.stdout.encode()).hexdigest()
|
|
if local_id != checksum:
|
|
raise RuntimeError(
|
|
f"Local container image id of {local_id}, but expected {checksum}")
|
|
|
|
return True
|
|
|
|
|
|
def main():
|
|
service = ContainersStorageSource.from_args(sys.argv[1:])
|
|
service.main()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|