fix(ci): verify registry upload against exported OCI config
This commit is contained in:
1 parent
e617ed176a
commit
8dc187483c
4 files changed
+19
-18
No files matched your search
@@ -248,7 +248,7 @@ Publication uses checksum-pinned regctl v0.11.6 with 8 MiB blob requests to
|
|||||||
avoid monolithic layer uploads exceeding reverse-proxy limits. Both images are
|
avoid monolithic layer uploads exceeding reverse-proxy limits. Both images are
|
||||||
exported and uploaded sequentially; temporary archives and credentials are removed
|
exported and uploaded sequentially; temporary archives and credentials are removed
|
||||||
on exit. The runner needs curl, sha256sum and temporary disk space for one Docker
|
on exit. The runner needs curl, sha256sum and temporary disk space for one Docker
|
||||||
image archive. The remote image config digest is checked against the local image
|
image archive plus its extracted OCI layout. The remote image config digest is checked against the locally normalized archive
|
||||||
after each upload. A proxy must still allow the OCI registry PATCH/PUT endpoints.
|
after each upload. A proxy must still allow the OCI registry PATCH/PUT endpoints.
|
||||||
|
|
||||||
For this repository the image base is
|
For this repository the image base is
|
||||||
|
|||||||
@@ -58,12 +58,16 @@ regctl registry set "$registry" --blob-chunk 8388608 --blob-max 8388608
|
|||||||
for target in "$image-migrations" "$image"; do
|
for target in "$image-migrations" "$image"; do
|
||||||
echo "Publishing $target with blob requests up to 8 MiB"
|
echo "Publishing $target with blob requests up to 8 MiB"
|
||||||
docker image save --output "$context/image.tar" "$target"
|
docker image save --output "$context/image.tar" "$target"
|
||||||
regctl image import "$target" "$context/image.tar"
|
# Normalize the saved archive locally before copying it unchanged to Gitea.
|
||||||
# Import may change compression/manifest representation, but the immutable
|
# Docker engine IDs and OCI index IDs are not interchangeable with config IDs.
|
||||||
# image config digest must still match the exact local image we verified.
|
local_ref="ocidir://$context/oci:verified"
|
||||||
expected_config="$(docker image inspect --format '{{.Id}}' "$target")"
|
regctl image import "$local_ref" "$context/image.tar"
|
||||||
|
expected_config="$(regctl manifest get "$local_ref" --platform "linux/$arch" --format '{{.GetConfig.Digest}}')"
|
||||||
|
[[ "$expected_config" =~ ^sha256:[0-9a-f]{64}$ ]] || { echo "Invalid local image config digest" >&2; exit 1; }
|
||||||
|
regctl image copy "$local_ref" "$target"
|
||||||
remote_config="$(regctl manifest get "$target" --platform "linux/$arch" --format '{{.GetConfig.Digest}}')"
|
remote_config="$(regctl manifest get "$target" --platform "linux/$arch" --format '{{.GetConfig.Digest}}')"
|
||||||
[[ "$remote_config" = "$expected_config" ]] || { echo "Published image config does not match verified local image" >&2; exit 1; }
|
[[ "$remote_config" = "$expected_config" ]] || { echo "Published image config does not match verified local image" >&2; exit 1; }
|
||||||
rm -f -- "$context/image.tar"
|
rm -f -- "$context/image.tar"
|
||||||
|
rm -rf -- "$context/oci"
|
||||||
done
|
done
|
||||||
echo "Published application and migrations: $image"
|
echo "Published application and migrations: $image"
|
||||||
@@ -57,7 +57,7 @@ describe("verified application image reuse", () => {
|
|||||||
expect(calls).not.toContain("docker build --network=host --build-arg");
|
expect(calls).not.toContain("docker build --network=host --build-arg");
|
||||||
expect(
|
expect(
|
||||||
calls.indexOf("verify scripts/verify-portable-image.mjs"),
|
calls.indexOf("verify scripts/verify-portable-image.mjs"),
|
||||||
).toBeLessThan(calls.indexOf("regctl image import"));
|
).toBeLessThan(calls.indexOf("regctl image copy"));
|
||||||
});
|
});
|
||||||
it.each(["missing", "mismatch"])(
|
it.each(["missing", "mismatch"])(
|
||||||
"builds committed source when verification marker is %s",
|
"builds committed source when verification marker is %s",
|
||||||
@@ -71,16 +71,12 @@ describe("verified application image reuse", () => {
|
|||||||
|
|
||||||
it("uses the token account namespace instead of the repository owner", () => {
|
it("uses the token account namespace instead of the repository owner", () => {
|
||||||
const calls = simulate("verified");
|
const calls = simulate("verified");
|
||||||
expect(calls).toContain(
|
expect(calls).toContain(`registry.invalid/simo/cms:${sha}\n`);
|
||||||
`regctl image import registry.invalid/simo/cms:${sha}`,
|
|
||||||
);
|
|
||||||
expect(calls).not.toContain("registry.invalid/owner/cms");
|
expect(calls).not.toContain("registry.invalid/owner/cms");
|
||||||
});
|
});
|
||||||
it("supports an explicit organization namespace", () => {
|
it("supports an explicit organization namespace", () => {
|
||||||
const calls = simulate("verified", "My-Org");
|
const calls = simulate("verified", "My-Org");
|
||||||
expect(calls).toContain(
|
expect(calls).toContain(`registry.invalid/my-org/cms:${sha}\n`);
|
||||||
`regctl image import registry.invalid/my-org/cms:${sha}`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("bounds uploads and verifies both published image configs", () => {
|
it("bounds uploads and verifies both published image configs", () => {
|
||||||
@@ -88,15 +84,16 @@ it("bounds uploads and verifies both published image configs", () => {
|
|||||||
expect(calls).toContain("--blob-chunk 8388608 --blob-max 8388608");
|
expect(calls).toContain("--blob-chunk 8388608 --blob-max 8388608");
|
||||||
expect(calls).not.toContain("docker push");
|
expect(calls).not.toContain("docker push");
|
||||||
expect(calls.match(/regctl image import/g)).toHaveLength(2);
|
expect(calls.match(/regctl image import/g)).toHaveLength(2);
|
||||||
expect(calls.match(/regctl manifest get/g)).toHaveLength(2);
|
expect(calls.match(/regctl image copy/g)).toHaveLength(2);
|
||||||
expect(calls.match(/--platform linux\/amd64/g)).toHaveLength(2);
|
expect(calls.match(/regctl manifest get/g)).toHaveLength(4);
|
||||||
|
expect(calls.match(/--platform linux\/amd64/g)).toHaveLength(4);
|
||||||
});
|
});
|
||||||
it.each(["upload-fails", "wrong-config", "bad-checksum"])(
|
it.each(["upload-fails", "wrong-config", "bad-checksum"])(
|
||||||
"stops publication on %s",
|
"stops publication on %s",
|
||||||
(scenario) => {
|
(scenario) => {
|
||||||
const calls = simulate(scenario, "", 1);
|
const calls = simulate(scenario, "", 1);
|
||||||
expect(calls).not.toContain(
|
expect(calls.match(/regctl image copy/g)?.length ?? 0).toBeLessThanOrEqual(
|
||||||
`regctl image import registry.invalid/simo/cms:${sha} `,
|
1,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -18,9 +18,9 @@ curl() {
|
|||||||
cat > "$output" <<'MOCK'
|
cat > "$output" <<'MOCK'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
echo "regctl $*" >> "$TEST_DIR/calls"
|
echo "regctl $*" >> "$TEST_DIR/calls"
|
||||||
if [[ "$1 $2" = "image import" && "$SCENARIO" = upload-fails ]]; then exit 1; fi
|
if [[ "$1 $2" = "image copy" && "$SCENARIO" = upload-fails ]]; then exit 1; fi
|
||||||
if [[ "$1 $2" = "manifest get" ]]; then
|
if [[ "$1 $2" = "manifest get" ]]; then
|
||||||
if [[ "$SCENARIO" = wrong-config ]]; then echo sha256:wrong; else echo sha256:candidate; fi
|
if [[ "$SCENARIO" = wrong-config && "$3" != ocidir:* ]]; then echo sha256:wrong; else printf "sha256:%064d\n" 0; fi
|
||||||
fi
|
fi
|
||||||
MOCK
|
MOCK
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user