osbuild-mpp: Support mpp-resolve-image for container images
This commit is contained in:
parent
66cc2900c9
commit
dbaed75b46
1 changed files with 233 additions and 0 deletions
|
|
@ -115,6 +115,47 @@ The parameters for this pre-processor, version "2", look like this:
|
|||
...
|
||||
```
|
||||
|
||||
|
||||
Container resolving:
|
||||
|
||||
This tool adjusts the `org.osbuild.skopeo` stage. It consumes the `mpp-resolve-images`
|
||||
option and produces image digests and source-entries.
|
||||
|
||||
It supports version version "2" of the manifest description format.
|
||||
|
||||
The parameters for this pre-processor, version "2", look like this:
|
||||
|
||||
```
|
||||
...
|
||||
{
|
||||
"name": "org.osbuild.skopeo",
|
||||
...
|
||||
"inputs": {
|
||||
"images": {
|
||||
"mpp-resolve-images": {
|
||||
"images": [
|
||||
{
|
||||
"source": "docker.io/library/ubuntu",
|
||||
"name": "localhost/myimagename"
|
||||
},
|
||||
{
|
||||
"source": "quay.io/centos/centos",
|
||||
"tag": "centos7",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
The "source" key is required and specifies where to get the image.
|
||||
Optional keys "tag" and "digest" allow specifying a particular version
|
||||
of the image, otherwise the "latest" tag is used. If "name" is specified
|
||||
that is used as the custom name for the container when installed.
|
||||
|
||||
|
||||
Variable expansion and substitution:
|
||||
|
||||
The variables can be set in the mpp-vars toplevel dict (which is removed from
|
||||
|
|
@ -278,6 +319,142 @@ import hawkey
|
|||
|
||||
from osbuild.util.rhsm import Subscriptions
|
||||
|
||||
# We need to resolve an image name to a resolved image manifest digest
|
||||
# and the corresponding container id (which is the digest of the config object).
|
||||
# However, it turns out that skopeo is not very useful to do this, as
|
||||
# can be seen in https://github.com/containers/skopeo/issues/1554
|
||||
# So, we have to fall back to "skopeo inspect --raw" and actually look
|
||||
# at the manifest contents.
|
||||
|
||||
|
||||
class ImageManifest:
|
||||
# We hardcode this to what skopeo/fedora does since we don't want to
|
||||
# depend on host specific cpu details for image resolving
|
||||
_arch_from_rpm = {
|
||||
"x86_64": "amd64",
|
||||
"aarch64": "arm64",
|
||||
"armhfp": "arm"
|
||||
}
|
||||
_default_variant = {
|
||||
"arm64": "v8",
|
||||
"arm": "v7",
|
||||
}
|
||||
|
||||
def arch_from_rpm(rpm_arch):
|
||||
return ImageManifest._arch_from_rpm.get(rpm_arch, rpm_arch)
|
||||
|
||||
def load(imagename, tag=None, digest=None):
|
||||
if digest:
|
||||
src = f"docker://{imagename}@{digest}"
|
||||
elif tag:
|
||||
src = f"docker://{imagename}:{tag}"
|
||||
else:
|
||||
src = f"docker://{imagename}"
|
||||
|
||||
res = subprocess.run(["skopeo", "inspect", "--raw", src],
|
||||
capture_output=True,
|
||||
check=True)
|
||||
m = ImageManifest(res.stdout)
|
||||
m.name = imagename
|
||||
m.tag = tag
|
||||
m.source_digest = digest
|
||||
|
||||
return m
|
||||
|
||||
def __init__(self, raw_manifest):
|
||||
self.name = None
|
||||
self.tag = None
|
||||
self.source_digest = None
|
||||
self.raw = raw_manifest
|
||||
self.json = json.loads(raw_manifest)
|
||||
self.schema_version = self.json.get("schemaVersion", 0)
|
||||
self.media_type = self.json.get("mediaType", "")
|
||||
|
||||
self._compute_digest()
|
||||
|
||||
# Based on joseBase64UrlDecode() from docker
|
||||
def _jose_base64url_decode(input):
|
||||
# Strip whitespace
|
||||
input.replace("\n", "")
|
||||
input.replace(" ", "")
|
||||
# Pad input with = to make it valid
|
||||
rem = len(input) % 4
|
||||
if rem > 0:
|
||||
input += "=" * (4 - rem)
|
||||
return base64.urlsafe_b64decode(input)
|
||||
|
||||
def _compute_digest(self):
|
||||
raw = self.raw
|
||||
|
||||
# If this is an old docker v1 signed manifest we need to remove the jsw signature
|
||||
if self.schema_version == 1 and "signatures" in self.json:
|
||||
formatLength = 0
|
||||
formatTail = ""
|
||||
for s in self.json["signatures"]:
|
||||
header = json.loads(ImageManifest._jose_base64url_decode(s["protected"]))
|
||||
formatLength = header["formatLength"]
|
||||
formatTail = ImageManifest._jose_base64url_decode(header["formatTail"])
|
||||
raw = raw[0:formatLength] + formatTail
|
||||
|
||||
self.digest = "sha256:" + hashlib.sha256(raw).hexdigest()
|
||||
|
||||
def is_manifest_list(self):
|
||||
return self.media_type == "application/vnd.docker.distribution.manifest.list.v2+json" or self.media_type == "application/vnd.oci.image.index.v1+json"
|
||||
|
||||
def _match_platform(self, wanted_arch, wanted_os, wanted_variant):
|
||||
for m in self.json.get("manifests", []):
|
||||
platform = m.get("platform", {})
|
||||
arch = platform.get("architecture", "")
|
||||
os = platform.get("os", "")
|
||||
variant = platform.get("variant", None)
|
||||
|
||||
if arch != wanted_arch or wanted_os != os:
|
||||
continue
|
||||
|
||||
if wanted_variant and wanted_variant != variant:
|
||||
continue
|
||||
|
||||
return m["digest"]
|
||||
|
||||
return None
|
||||
|
||||
def resolve_list(self, wanted_arch, wanted_os, wanted_variant):
|
||||
if not self.is_manifest_list():
|
||||
return self
|
||||
|
||||
manifests = self.json.get("manifests", [])
|
||||
digest = None
|
||||
|
||||
if wanted_variant:
|
||||
# Variant specify, require exact match
|
||||
digest = self._match_platform(wanted_arch, wanted_os, wanted_variant)
|
||||
else:
|
||||
# No variant specified, first try exact match with default variant for arch (if any)
|
||||
default_variant = ImageManifest._default_variant.get(wanted_arch, None)
|
||||
if default_variant:
|
||||
digest = self._match_platform(wanted_arch, wanted_os, default_variant)
|
||||
|
||||
# Else, pick first with any (or no) variant
|
||||
if not digest:
|
||||
digest = self._match_platform(wanted_arch, wanted_os, None)
|
||||
|
||||
if not digest:
|
||||
raise RuntimeError(
|
||||
f"No manifest matching architecture '{wanted_arch}', os '{wanted_os}', variant '{wanted_variant}'.")
|
||||
|
||||
return ImageManifest.load(self.name, digest=digest)
|
||||
|
||||
def get_config_digest(self):
|
||||
if self.schema_version == 1:
|
||||
# The way the image id is extracted for old v1 images is super weird, and
|
||||
# there is no easy way to get it from skopeo.
|
||||
# So, kets just not support them instead of living in the past.
|
||||
raise RuntimeError("Old docker images with schema version 1 not supported.")
|
||||
if self.is_manifest_list():
|
||||
raise RuntimeError("No config existis for manifest lists.")
|
||||
|
||||
return self.json.get("config", {}).get("digest", "")
|
||||
|
||||
|
||||
class YamlOrderedLoader(yaml.Loader):
|
||||
def construct_mapping(self, node, deep=False):
|
||||
|
|
@ -1023,6 +1200,7 @@ class ManifestFile:
|
|||
def _process_stage(self, solver_factory, stage, pipeline_name):
|
||||
self._process_depsolve(solver_factory, stage, pipeline_name)
|
||||
self._process_embed_files(stage)
|
||||
self._process_container(stage)
|
||||
|
||||
|
||||
class ManifestFileV1(ManifestFile):
|
||||
|
|
@ -1111,6 +1289,9 @@ class ManifestFileV1(ManifestFile):
|
|||
def _process_embed_files(self, stage):
|
||||
"Embedding files is not supported for v1 manifests"
|
||||
|
||||
def _process_container(self, stage):
|
||||
"Installing containers is not supported for v1 manifests"
|
||||
|
||||
|
||||
class ManifestFileV2(ManifestFile):
|
||||
def __init__(self, path, overrides, default_vars, data, searchdirs):
|
||||
|
|
@ -1247,6 +1428,58 @@ class ManifestFileV2(ManifestFile):
|
|||
|
||||
embed_data(ip, mpp)
|
||||
|
||||
def _process_container(self, stage):
|
||||
if stage.get("type", "") != "org.osbuild.skopeo":
|
||||
return
|
||||
|
||||
inputs = element_enter(stage, "inputs", {})
|
||||
inputs_images = element_enter(inputs, "images", {})
|
||||
|
||||
if inputs_images.get("type", "") != "org.osbuild.containers":
|
||||
return
|
||||
|
||||
if inputs_images.get("origin", "") != "org.osbuild.source":
|
||||
return
|
||||
|
||||
mpp = self.get_mpp_node(inputs_images, "resolve-images")
|
||||
if not mpp:
|
||||
return
|
||||
|
||||
refs = element_enter(inputs_images, "references", {})
|
||||
|
||||
for image in element_enter(mpp, "images", []):
|
||||
source = image["source"]
|
||||
name = image.get("name", source)
|
||||
digest = image.get("digest", None)
|
||||
tag = image.get("tag", None)
|
||||
|
||||
main_manifest = ImageManifest.load(source, tag=tag, digest=digest)
|
||||
|
||||
os = image.get("os", "linux")
|
||||
|
||||
default_rpm_arch = self.get_vars()["arch"]
|
||||
rpm_arch = image.get("arch", default_rpm_arch)
|
||||
oci_arch = ImageManifest.arch_from_rpm(rpm_arch)
|
||||
|
||||
variant = image.get("variant", None)
|
||||
|
||||
resolved_manifest = main_manifest.resolve_list(oci_arch, os, variant)
|
||||
|
||||
image_id = resolved_manifest.get_config_digest()
|
||||
|
||||
container_image_source = element_enter(self.sources, "org.osbuild.skopeo", {})
|
||||
items = element_enter(container_image_source, "items", {})
|
||||
items[image_id] = {
|
||||
"image": {
|
||||
"name": source,
|
||||
"digest": resolved_manifest.digest,
|
||||
}
|
||||
}
|
||||
|
||||
refs[image_id] = {
|
||||
"name": name
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Manifest pre processor")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue