Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions scripts/build-core-distribution.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ esac
# Build one selected Linux architecture image and write the ID the local image store gives it to
# $stage/NAME.id. BuildKit's --iidfile reports the config digest, which is the
# image ID only in Docker's classic store; the containerd store (Docker 29's
# default) uses the manifest digest and cannot resolve the config digest. The
# build metadata carries both, and built-image keeps the one the store resolves.
# default) uses the manifest digest and cannot resolve the config digest.
# built-image verifies the local identity from the digests the exporter reports.
# Without provenance attestations each image is one platform manifest in both
# stores, as the archive verifier requires.
build_image() {
Expand Down
8 changes: 8 additions & 0 deletions scripts/core-distribution-manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ def built_image(metadata_file, architecture="amd64"):
# by its manifest digest; the other value never resolves to itself there.
config = metadata.get("containerimage.config.digest")
manifest = metadata.get("containerimage.digest", config)
# The Docker/OCI exporter may omit the config digest. Its manifest must
# itself identify the loaded image; archive verification still binds both.
if "containerimage.config.digest" not in metadata:
if not isinstance(manifest, str) or not DIGEST.fullmatch(manifest):
raise ValueError("Build metadata lacks valid image digests")
verify_image(manifest, architecture)
print(manifest)
return
print(resolve_image(config, manifest, architecture))


Expand Down
53 changes: 50 additions & 3 deletions scripts/core-distribution-manifest.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,12 +250,15 @@ def test_built_image_records_the_id_each_docker_store_resolves(self):
config, manifest, other = ("sha256:" + digit * 64 for digit in "123")
metadata = self.stage / "build.json"
both = {"containerimage.config.digest": config, "containerimage.digest": manifest}
# Classic stores resolve the config digest, containerd stores the manifest digest. A digest
# resolving to another image, or metadata without a config digest, identifies nothing.
# Classic stores resolve the config digest, containerd stores the manifest digest.
# A digest resolving to another image identifies nothing.
cases = ((dict(both, **{"containerimage.digest": config}), {config: config}, config),
({"containerimage.config.digest": config}, {config: config}, config),
(both, {config: config}, config),
(both, {manifest: manifest}, manifest),
(both, {config: config, manifest: manifest}, "does not identify"),
(both, {config: other}, "does not identify"),
({"containerimage.digest": manifest}, {manifest: manifest}, "lacks valid image digests"))
({"containerimage.digest": manifest}, {manifest: manifest}, manifest))
for build, store, expected in cases:
with self.subTest(build=build, store=store):
metadata.write_text(json.dumps(build))
Expand All @@ -273,6 +276,50 @@ def test_built_image_records_the_id_each_docker_store_resolves(self):
distribution.built_image(metadata)
verify.assert_not_called()

def test_manifest_only_metadata_requires_the_exact_loaded_linux_image(self):
manifest, other = ("sha256:" + digit * 64 for digit in "12")
metadata = self.stage / "build.json"
metadata.write_text(json.dumps({"containerimage.digest": manifest}))
correct = {"Id": manifest, "Os": "linux", "Architecture": "amd64"}
for details in (correct, dict(correct, Id=other), dict(correct, Os="windows"),
dict(correct, Architecture="arm64")):
with self.subTest(details=details), \
mock.patch.object(distribution.subprocess, "check_output", return_value=json.dumps([details])) as inspect, \
mock.patch("builtins.print") as output:
if details == correct:
distribution.built_image(metadata)
output.assert_called_once_with(manifest)
else:
with self.assertRaisesRegex(ValueError, "selected Linux architecture"):
distribution.built_image(metadata)
output.assert_not_called()
inspect.assert_called_once_with(["docker", "image", "inspect", manifest], text=True)
with mock.patch.object(distribution.subprocess, "check_output",
side_effect=subprocess.CalledProcessError(1, "docker")), \
mock.patch("builtins.print") as output:
with self.assertRaises(subprocess.CalledProcessError):
distribution.built_image(metadata)
output.assert_not_called()

def test_missing_config_does_not_accept_invalid_digests(self):
metadata = self.stage / "build.json"
manifest = "sha256:" + "1" * 64
cases = [{}]
for invalid in (None, "", "image:latest", "sha256:123", 123, {}):
cases.append({"containerimage.digest": invalid})
cases.append({"containerimage.digest": manifest, "containerimage.config.digest": invalid})
for build in cases:
with self.subTest(build=build), \
mock.patch.object(distribution.subprocess, "run") as resolve, \
mock.patch.object(distribution.subprocess, "check_output") as inspect, \
mock.patch("builtins.print") as output:
metadata.write_text(json.dumps(build))
with self.assertRaisesRegex(ValueError, "lacks valid image digests"):
distribution.built_image(metadata)
resolve.assert_not_called()
inspect.assert_not_called()
output.assert_not_called()

def test_containerd_build_ids_still_publish_archive_config_ids(self):
for name, identity in self.identities.items():
(self.stage / (name + ".id")).write_text(identity[1])
Expand Down