Rewrite deploy to minimize API requests (#38)
* WIP: first pass at deploy * WIP: second pass, now with more cowbell * WIP: refactor coverage handling (cleaner, more consistent, no longer has to clobber top-level bin/) * WIP: the start of more "jq" tests * WIP: add a few TODOs * Add a benchmark for `om.OrderedMap.Set` I was testing a minor memory-usage improvement to `Set`, but it turns out it doesn't actually matter (and this helped me determine that, so I might as well keep it). * Add explicit `Reference.StringWithKnownDigest` unit test * WIP: refactor EnsureManifest loop with more correct handling of child manifests vs blobs * Update to use the new `ociregistry.HTTPError` for more consistent/correct HTTP error handling * WIP: remove TODO that was implemented elsewhere (and fix error message text / comment text) * WIP: also normalize descriptor field ordering * WIP: assume pre-normalized platform (no reason to normalize more than once) * WIP: initial "deploy" data munging helpers plus tests * WIP: update Jenkinsfile.deploy to use new deploy code * WIP: remove example-commands symlink so Git detects rename better * WIP: add delay for racy registry startup * WIP: remove trap once it's no longer necessary * WIP: typo * WIP: remove unnecessary TODOs
This commit is contained in:
parent
530a1082db
commit
ab38f954b3
|
|
@ -29,11 +29,13 @@ jobs:
|
|||
chmod +x .bin/bashbrew
|
||||
.bin/bashbrew --version
|
||||
echo "$PWD/.bin" >> "$GITHUB_PATH"
|
||||
- run: .test/test.sh
|
||||
- run: .test/test.sh --deploy
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage
|
||||
path: .test/coverage**
|
||||
path: |
|
||||
.test/.coverage/coverage.*
|
||||
.test/.coverage/GOCOVERDIR/
|
||||
if-no-files-found: error
|
||||
- name: gofmt
|
||||
run: find -name '*.go' -type f -exec ./.go-env.sh gofmt -l -s -w '{}' +
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
**
|
||||
!.gitignore
|
||||
!builds.sh
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../builds.sh
|
||||
|
|
@ -0,0 +1 @@
|
|||
../builds.json
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,6 @@
|
|||
include "deploy";
|
||||
|
||||
# every single ref both "library/" and arch-specific we should push to
|
||||
tagged_manifests(true; .source.tags, .source.arches[.build.arch].archTags)
|
||||
# ... converted into a list of canonical inputs for "cmd/deploy"
|
||||
| deploy_objects
|
||||
|
|
@ -0,0 +1 @@
|
|||
../builds.json
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
[
|
||||
{
|
||||
"type": "manifest",
|
||||
"refs": [
|
||||
"amd64/docker:24.0.7-cli",
|
||||
"amd64/docker:24.0-cli",
|
||||
"amd64/docker:24-cli",
|
||||
"amd64/docker:cli",
|
||||
"amd64/docker:24.0.7-cli-alpine3.18"
|
||||
],
|
||||
"lookup": {
|
||||
"sha256:0432a4d379794811b4a2e01d0d3e67a9bcf95d6c2bf71545f03bce3f1d60f401": "oisupport/staging-amd64:4b199ac326c74b3058a147e14f553af9e8e1659abc29bd3e82c9c9807b66ee43",
|
||||
"sha256:061239943a7c3d3068527dbdac796118a9f0530d0478a48623f1a49d6aeb21e6": "oisupport/staging-amd64:4b199ac326c74b3058a147e14f553af9e8e1659abc29bd3e82c9c9807b66ee43"
|
||||
},
|
||||
"data": {
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.index.v1+json",
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:0432a4d379794811b4a2e01d0d3e67a9bcf95d6c2bf71545f03bce3f1d60f401",
|
||||
"size": 2372,
|
||||
"platform": {
|
||||
"os": "linux",
|
||||
"architecture": "amd64"
|
||||
},
|
||||
"annotations": {
|
||||
"com.docker.official-images.bashbrew.arch": "amd64",
|
||||
"org.opencontainers.image.revision": "6d541d27b5dd12639e5a33a675ebca04d3837d74",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/docker.git#6d541d27b5dd12639e5a33a675ebca04d3837d74:24/cli",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/docker",
|
||||
"org.opencontainers.image.version": "24.0.7-cli"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:061239943a7c3d3068527dbdac796118a9f0530d0478a48623f1a49d6aeb21e6",
|
||||
"size": 840,
|
||||
"platform": {
|
||||
"os": "unknown",
|
||||
"architecture": "unknown"
|
||||
},
|
||||
"annotations": {
|
||||
"com.docker.official-images.bashbrew.arch": "amd64",
|
||||
"vnd.docker.reference.digest": "sha256:0432a4d379794811b4a2e01d0d3e67a9bcf95d6c2bf71545f03bce3f1d60f401",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "manifest",
|
||||
"refs": [
|
||||
"amd64/docker:24.0.7-dind",
|
||||
"amd64/docker:24.0-dind",
|
||||
"amd64/docker:24-dind",
|
||||
"amd64/docker:dind",
|
||||
"amd64/docker:24.0.7-dind-alpine3.18",
|
||||
"amd64/docker:24.0.7",
|
||||
"amd64/docker:24.0",
|
||||
"amd64/docker:24",
|
||||
"amd64/docker:latest",
|
||||
"amd64/docker:24.0.7-alpine3.18"
|
||||
],
|
||||
"lookup": {
|
||||
"sha256:4c92bd9328191f76e8eec6592ceb2e248aa7406dfc9505870812cf8ebee9326a": "oisupport/staging-amd64:52e3bf2e5ae5606b777f60a7205b338af7ecf70bfebf714e52979dbf9a055621",
|
||||
"sha256:bd33f1033e5aa789410574b03beda7415cfc5b7826beba9148d9ca45395029a7": "oisupport/staging-amd64:52e3bf2e5ae5606b777f60a7205b338af7ecf70bfebf714e52979dbf9a055621"
|
||||
},
|
||||
"data": {
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.index.v1+json",
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:4c92bd9328191f76e8eec6592ceb2e248aa7406dfc9505870812cf8ebee9326a",
|
||||
"size": 3327,
|
||||
"platform": {
|
||||
"os": "linux",
|
||||
"architecture": "amd64"
|
||||
},
|
||||
"annotations": {
|
||||
"com.docker.official-images.bashbrew.arch": "amd64",
|
||||
"org.opencontainers.image.revision": "99073a3b6be3aa7e6b5af1e69509e8c532254500",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/docker.git#99073a3b6be3aa7e6b5af1e69509e8c532254500:24/dind",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/docker",
|
||||
"org.opencontainers.image.version": "24.0.7-dind"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:bd33f1033e5aa789410574b03beda7415cfc5b7826beba9148d9ca45395029a7",
|
||||
"size": 840,
|
||||
"platform": {
|
||||
"os": "unknown",
|
||||
"architecture": "unknown"
|
||||
},
|
||||
"annotations": {
|
||||
"com.docker.official-images.bashbrew.arch": "amd64",
|
||||
"vnd.docker.reference.digest": "sha256:4c92bd9328191f76e8eec6592ceb2e248aa7406dfc9505870812cf8ebee9326a",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "manifest",
|
||||
"refs": [
|
||||
"amd64/notary:server-0.7.0",
|
||||
"amd64/notary:server"
|
||||
],
|
||||
"lookup": {
|
||||
"sha256:4c3d07b2fed560ab0012452aa8a6f58533ddf2d4a3845fa89b74d9455816b454": "oisupport/staging-amd64:71756dd75e41c4bc5144b64d36b4834a5a960c495470915eb69f96e9f2cb6694",
|
||||
"sha256:692819af7e57efe94abadb451e05aa5eb042a540a2eae7095d37507dbd66dc94": "oisupport/staging-amd64:71756dd75e41c4bc5144b64d36b4834a5a960c495470915eb69f96e9f2cb6694"
|
||||
},
|
||||
"data": {
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.index.v1+json",
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:4c3d07b2fed560ab0012452aa8a6f58533ddf2d4a3845fa89b74d9455816b454",
|
||||
"size": 1998,
|
||||
"platform": {
|
||||
"os": "linux",
|
||||
"architecture": "amd64"
|
||||
},
|
||||
"annotations": {
|
||||
"com.docker.official-images.bashbrew.arch": "amd64",
|
||||
"org.opencontainers.image.revision": "77b9b7833f8dd6be07104b214193788795a320ff",
|
||||
"org.opencontainers.image.source": "https://github.com/docker/notary-official-images.git#77b9b7833f8dd6be07104b214193788795a320ff:notary-server",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/notary",
|
||||
"org.opencontainers.image.version": "server-0.7.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:692819af7e57efe94abadb451e05aa5eb042a540a2eae7095d37507dbd66dc94",
|
||||
"size": 839,
|
||||
"platform": {
|
||||
"os": "unknown",
|
||||
"architecture": "unknown"
|
||||
},
|
||||
"annotations": {
|
||||
"com.docker.official-images.bashbrew.arch": "amd64",
|
||||
"vnd.docker.reference.digest": "sha256:4c3d07b2fed560ab0012452aa8a6f58533ddf2d4a3845fa89b74d9455816b454",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "manifest",
|
||||
"refs": [
|
||||
"amd64/notary:signer-0.7.0",
|
||||
"amd64/notary:signer"
|
||||
],
|
||||
"lookup": {
|
||||
"sha256:a5f3cf14ec1f9dbe64f5038168764468bf8cf36023f8c1d763abd3bcbe2a5952": "oisupport/staging-amd64:57c2ee0d050ffb54c7f2b50c57b807cce8a8c478648c2eb6bbdf1604b34dd1b9",
|
||||
"sha256:9346c6fe8bf3d29a34e0e1d10f1730feff4461a2e4e2cec704a90490be890d77": "oisupport/staging-amd64:57c2ee0d050ffb54c7f2b50c57b807cce8a8c478648c2eb6bbdf1604b34dd1b9"
|
||||
},
|
||||
"data": {
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.index.v1+json",
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:a5f3cf14ec1f9dbe64f5038168764468bf8cf36023f8c1d763abd3bcbe2a5952",
|
||||
"size": 1998,
|
||||
"platform": {
|
||||
"os": "linux",
|
||||
"architecture": "amd64"
|
||||
},
|
||||
"annotations": {
|
||||
"com.docker.official-images.bashbrew.arch": "amd64",
|
||||
"org.opencontainers.image.revision": "77b9b7833f8dd6be07104b214193788795a320ff",
|
||||
"org.opencontainers.image.source": "https://github.com/docker/notary-official-images.git#77b9b7833f8dd6be07104b214193788795a320ff:notary-signer",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/notary",
|
||||
"org.opencontainers.image.version": "signer-0.7.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:9346c6fe8bf3d29a34e0e1d10f1730feff4461a2e4e2cec704a90490be890d77",
|
||||
"size": 839,
|
||||
"platform": {
|
||||
"os": "unknown",
|
||||
"architecture": "unknown"
|
||||
},
|
||||
"annotations": {
|
||||
"com.docker.official-images.bashbrew.arch": "amd64",
|
||||
"vnd.docker.reference.digest": "sha256:a5f3cf14ec1f9dbe64f5038168764468bf8cf36023f8c1d763abd3bcbe2a5952",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "manifest",
|
||||
"refs": [
|
||||
"amd64/busybox:1.36.1",
|
||||
"amd64/busybox:1.36",
|
||||
"amd64/busybox:1",
|
||||
"amd64/busybox:stable",
|
||||
"amd64/busybox:latest"
|
||||
],
|
||||
"lookup": {
|
||||
"sha256:4be429a5fbb2e71ae7958bfa558bc637cf3a61baf40a708cb8fff532b39e52d0": "oisupport/staging-amd64:191402ad0feacf03daf9d52a492207e73ef08b0bd17265043aea13aa27e2bb3f"
|
||||
},
|
||||
"data": {
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.index.v1+json",
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:4be429a5fbb2e71ae7958bfa558bc637cf3a61baf40a708cb8fff532b39e52d0",
|
||||
"size": 610,
|
||||
"platform": {
|
||||
"os": "linux",
|
||||
"architecture": "amd64"
|
||||
},
|
||||
"annotations": {
|
||||
"com.docker.official-images.bashbrew.arch": "amd64",
|
||||
"org.opencontainers.image.base.name": "scratch",
|
||||
"org.opencontainers.image.created": "2024-02-28T00:44:18Z",
|
||||
"org.opencontainers.image.revision": "d0b7d566eb4f1fa9933984e6fc04ab11f08f4592",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/busybox.git",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/busybox",
|
||||
"org.opencontainers.image.version": "1.36.1-glibc"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
include "deploy";
|
||||
|
||||
# just amd64 arch-specific manifests
|
||||
arch_tagged_manifests("amd64")
|
||||
# ... converted into a list of canonical inputs for "cmd/deploy"
|
||||
| deploy_objects
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
shopt -s nullglob # if * matches nothing, return nothing
|
||||
|
||||
dir="$(dirname "$BASH_SOURCE")"
|
||||
dir="$(readlink -ve "$dir")"
|
||||
|
||||
export SOURCE_DATE_EPOCH=0 # TODO come up with a better way for a test to specify it needs things like this (maybe a file that gets sourced/read for options/setup type things? could also provide args/swap 'out' like our "-r" hank below)
|
||||
|
||||
# TODO arguments for choosing a test? directory? name?
|
||||
for t in "$dir/"*"/test.jq"; do
|
||||
td="$(dirname "$t")"
|
||||
echo -n 'test: '
|
||||
basename "$td"
|
||||
args=( --tab -L "$dir/.." -f "$t" )
|
||||
if [ -s "$td/in.json" ]; then
|
||||
args+=( "$td/in.json" )
|
||||
else
|
||||
args+=( -n )
|
||||
fi
|
||||
out="$td/out.json"
|
||||
outs=( "$td/out."* )
|
||||
if [ "${#outs[@]}" -eq 1 ]; then
|
||||
out="${outs[0]}"
|
||||
if [[ "$out" != *.json ]]; then
|
||||
args+=( -r )
|
||||
fi
|
||||
fi
|
||||
jq "${args[@]}" > "$out"
|
||||
done
|
||||
|
|
@ -0,0 +1 @@
|
|||
../builds.json
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
include "meta";
|
||||
[
|
||||
first(.[] | select(normalized_builder == "buildkit")),
|
||||
first(.[] | select(normalized_builder == "classic")),
|
||||
first(.[] | select(normalized_builder == "oci-import")),
|
||||
empty
|
||||
]
|
||||
| map(
|
||||
. as $b
|
||||
| commands
|
||||
| to_entries
|
||||
| map("# <\(.key)>\n\(.value)\n# </\(.key)>")
|
||||
| "# \($b.source.tags[0]) [\($b.build.arch)]\n" + join("\n")
|
||||
)
|
||||
| join("\n\n")
|
||||
|
|
@ -0,0 +1,308 @@
|
|||
[
|
||||
{
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:1363c810cc39a563c6f315e26951d2ed9e93f3bf929fde8223633ecf81a4a430",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
},
|
||||
"digest": "sha256:085e87951950ac62a771af158d4d8275505088897a0e520a8a5bd582343631b0",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "unknown",
|
||||
"os": "unknown"
|
||||
},
|
||||
"size": 566
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"com.docker.official-images.bashbrew.arch": "arm32v6",
|
||||
"org.opencontainers.image.base.name": "scratch",
|
||||
"org.opencontainers.image.created": "2024-03-11T22:49:19Z",
|
||||
"org.opencontainers.image.revision": "61f3ba26fe1d027b5b443c04ac2b0690fd97561a",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#61f3ba26fe1d027b5b443c04ac2b0690fd97561a:arm32v6/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
},
|
||||
"digest": "sha256:1363c810cc39a563c6f315e26951d2ed9e93f3bf929fde8223633ecf81a4a430",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "arm",
|
||||
"os": "linux",
|
||||
"variant": "v6"
|
||||
},
|
||||
"size": 1039
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:dbbd3cf666311ad526fad9d1746177469268f32fd91b371df2ebd1c84eb22f23",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
},
|
||||
"digest": "sha256:18b1c92de36d42c75440c6fd6b25605cc91709d176faaccca8afe58b317bc33a",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "unknown",
|
||||
"os": "unknown"
|
||||
},
|
||||
"size": 566
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:2d4e459f4ecb5329407ae3e47cbc107a2fbace221354ca75960af4c047b3cb13",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
},
|
||||
"digest": "sha256:1f11fbd1720fcae3e402fc3eecb7d57c67023d2d1e11becc99ad9c7fe97d65ca",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "unknown",
|
||||
"os": "unknown"
|
||||
},
|
||||
"size": 837
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:arm32v7/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
},
|
||||
"digest": "sha256:20aea1c63c90d5e117db787c9fe1a8cd0ad98bedb5fd711273ffe05c084ff18a",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "arm",
|
||||
"os": "linux",
|
||||
"variant": "v7"
|
||||
},
|
||||
"size": 863
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:arm64v8/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
},
|
||||
"digest": "sha256:2d4e459f4ecb5329407ae3e47cbc107a2fbace221354ca75960af4c047b3cb13",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "arm64",
|
||||
"os": "linux"
|
||||
},
|
||||
"size": 863
|
||||
},
|
||||
{
|
||||
"digest": "sha256:2f19ce27632e6baf4ebb1b582960d68948e52902c8cfac10133da0058f1dab23",
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "windows",
|
||||
"os.version": "10.0.20348.2340"
|
||||
},
|
||||
"size": 946
|
||||
},
|
||||
{
|
||||
"digest": "sha256:3a0bd0fb5ad6dd6528dc78726b3df78e980b39b379e99c5a508904ec17cfafe5",
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "windows",
|
||||
"os.version": "10.0.17763.5576"
|
||||
},
|
||||
"size": 946
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:8d064a6fc27fd5e97fa8225994a1addd872396236367745bea30c92d6c032fa3",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
},
|
||||
"digest": "sha256:48147407c4594e45b7c3f0be1019bb0f44d78d7f037ce63e0e3da75b256f849e",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "unknown",
|
||||
"os": "unknown"
|
||||
},
|
||||
"size": 837
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:65f4b0d1802589b418bb6774d85de3d1a11d5bd971ee73cb8569504d928bb5d9",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
},
|
||||
"digest": "sha256:50f420e8710676da03668e446f1f51097b745e3e2c9807b018e569d26d4f65f7",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "unknown",
|
||||
"os": "unknown"
|
||||
},
|
||||
"size": 837
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:e2fc4e5012d16e7fe466f5291c476431beaa1f9b90a5c2125b493ed28e2aba57",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
},
|
||||
"digest": "sha256:579b3724a7b189f6dca599a46f16d801a43d5def185de0b7bcd5fb9d1e312c27",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "unknown",
|
||||
"os": "unknown"
|
||||
},
|
||||
"size": 837
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:s390x/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
},
|
||||
"digest": "sha256:65f4b0d1802589b418bb6774d85de3d1a11d5bd971ee73cb8569504d928bb5d9",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "s390x",
|
||||
"os": "linux"
|
||||
},
|
||||
"size": 861
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:c2d891e5c2fb4c723efb72b064be3351189f62222bd3681ce7e57f2a1527362c",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
},
|
||||
"digest": "sha256:6901d6a88eee6e90f0baa62b020bb61c4f13194cbcd9bf568ab66e8cc3f940dd",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "unknown",
|
||||
"os": "unknown"
|
||||
},
|
||||
"size": 566
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:20aea1c63c90d5e117db787c9fe1a8cd0ad98bedb5fd711273ffe05c084ff18a",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
},
|
||||
"digest": "sha256:70304c314d8a61ba1b36518624bb00bfff8d4b6016153792042de43f0453ca61",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "unknown",
|
||||
"os": "unknown"
|
||||
},
|
||||
"size": 837
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:f0c95f1ebb50c9b0b3e3416fb9dd4d1d197386a076c464cceea3d1f94c321b8f",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
},
|
||||
"digest": "sha256:838d191bca398e46cddebc48e816da83b0389d4ed2d64f408d618521b8fd1a57",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "unknown",
|
||||
"os": "unknown"
|
||||
},
|
||||
"size": 837
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:riscv64/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
},
|
||||
"digest": "sha256:8d064a6fc27fd5e97fa8225994a1addd872396236367745bea30c92d6c032fa3",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "riscv64",
|
||||
"os": "linux"
|
||||
},
|
||||
"size": 863
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:c19784034d46da48550487c5c44639f5f92d48be7b9baf4d67b5377a454d92af",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
},
|
||||
"digest": "sha256:951bcd144ddccd1ee902dc180b435faabaaa6a8747e70cbc893f2dca16badb94",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "unknown",
|
||||
"os": "unknown"
|
||||
},
|
||||
"size": 566
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:mips64le/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
},
|
||||
"digest": "sha256:c19784034d46da48550487c5c44639f5f92d48be7b9baf4d67b5377a454d92af",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "mips64le",
|
||||
"os": "linux"
|
||||
},
|
||||
"size": 864
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:arm32v5/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
},
|
||||
"digest": "sha256:c2d891e5c2fb4c723efb72b064be3351189f62222bd3681ce7e57f2a1527362c",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "arm",
|
||||
"os": "linux",
|
||||
"variant": "v5"
|
||||
},
|
||||
"size": 863
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:i386/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
},
|
||||
"digest": "sha256:dbbd3cf666311ad526fad9d1746177469268f32fd91b371df2ebd1c84eb22f23",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "386",
|
||||
"os": "linux"
|
||||
},
|
||||
"size": 860
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:amd64/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
},
|
||||
"digest": "sha256:e2fc4e5012d16e7fe466f5291c476431beaa1f9b90a5c2125b493ed28e2aba57",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux"
|
||||
},
|
||||
"size": 861
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:ppc64le/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
},
|
||||
"digest": "sha256:f0c95f1ebb50c9b0b3e3416fb9dd4d1d197386a076c464cceea3d1f94c321b8f",
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"platform": {
|
||||
"architecture": "ppc64le",
|
||||
"os": "linux"
|
||||
},
|
||||
"size": 863
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,309 @@
|
|||
[
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:e2fc4e5012d16e7fe466f5291c476431beaa1f9b90a5c2125b493ed28e2aba57",
|
||||
"size": 861,
|
||||
"platform": {
|
||||
"os": "linux",
|
||||
"architecture": "amd64"
|
||||
},
|
||||
"annotations": {
|
||||
"org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:amd64/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:579b3724a7b189f6dca599a46f16d801a43d5def185de0b7bcd5fb9d1e312c27",
|
||||
"size": 837,
|
||||
"platform": {
|
||||
"os": "unknown",
|
||||
"architecture": "unknown"
|
||||
},
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:e2fc4e5012d16e7fe466f5291c476431beaa1f9b90a5c2125b493ed28e2aba57",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:2d4e459f4ecb5329407ae3e47cbc107a2fbace221354ca75960af4c047b3cb13",
|
||||
"size": 863,
|
||||
"platform": {
|
||||
"os": "linux",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8"
|
||||
},
|
||||
"annotations": {
|
||||
"org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:arm64v8/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:1f11fbd1720fcae3e402fc3eecb7d57c67023d2d1e11becc99ad9c7fe97d65ca",
|
||||
"size": 837,
|
||||
"platform": {
|
||||
"os": "unknown",
|
||||
"architecture": "unknown"
|
||||
},
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:2d4e459f4ecb5329407ae3e47cbc107a2fbace221354ca75960af4c047b3cb13",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:dbbd3cf666311ad526fad9d1746177469268f32fd91b371df2ebd1c84eb22f23",
|
||||
"size": 860,
|
||||
"platform": {
|
||||
"os": "linux",
|
||||
"architecture": "386"
|
||||
},
|
||||
"annotations": {
|
||||
"org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:i386/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:18b1c92de36d42c75440c6fd6b25605cc91709d176faaccca8afe58b317bc33a",
|
||||
"size": 566,
|
||||
"platform": {
|
||||
"os": "unknown",
|
||||
"architecture": "unknown"
|
||||
},
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:dbbd3cf666311ad526fad9d1746177469268f32fd91b371df2ebd1c84eb22f23",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:20aea1c63c90d5e117db787c9fe1a8cd0ad98bedb5fd711273ffe05c084ff18a",
|
||||
"size": 863,
|
||||
"platform": {
|
||||
"os": "linux",
|
||||
"architecture": "arm",
|
||||
"variant": "v7"
|
||||
},
|
||||
"annotations": {
|
||||
"org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:arm32v7/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:70304c314d8a61ba1b36518624bb00bfff8d4b6016153792042de43f0453ca61",
|
||||
"size": 837,
|
||||
"platform": {
|
||||
"os": "unknown",
|
||||
"architecture": "unknown"
|
||||
},
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:20aea1c63c90d5e117db787c9fe1a8cd0ad98bedb5fd711273ffe05c084ff18a",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:1363c810cc39a563c6f315e26951d2ed9e93f3bf929fde8223633ecf81a4a430",
|
||||
"size": 1039,
|
||||
"platform": {
|
||||
"os": "linux",
|
||||
"architecture": "arm",
|
||||
"variant": "v6"
|
||||
},
|
||||
"annotations": {
|
||||
"com.docker.official-images.bashbrew.arch": "arm32v6",
|
||||
"org.opencontainers.image.base.name": "scratch",
|
||||
"org.opencontainers.image.created": "2024-03-11T22:49:19Z",
|
||||
"org.opencontainers.image.revision": "61f3ba26fe1d027b5b443c04ac2b0690fd97561a",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#61f3ba26fe1d027b5b443c04ac2b0690fd97561a:arm32v6/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:085e87951950ac62a771af158d4d8275505088897a0e520a8a5bd582343631b0",
|
||||
"size": 566,
|
||||
"platform": {
|
||||
"os": "unknown",
|
||||
"architecture": "unknown"
|
||||
},
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:1363c810cc39a563c6f315e26951d2ed9e93f3bf929fde8223633ecf81a4a430",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:c2d891e5c2fb4c723efb72b064be3351189f62222bd3681ce7e57f2a1527362c",
|
||||
"size": 863,
|
||||
"platform": {
|
||||
"os": "linux",
|
||||
"architecture": "arm",
|
||||
"variant": "v5"
|
||||
},
|
||||
"annotations": {
|
||||
"org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:arm32v5/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:6901d6a88eee6e90f0baa62b020bb61c4f13194cbcd9bf568ab66e8cc3f940dd",
|
||||
"size": 566,
|
||||
"platform": {
|
||||
"os": "unknown",
|
||||
"architecture": "unknown"
|
||||
},
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:c2d891e5c2fb4c723efb72b064be3351189f62222bd3681ce7e57f2a1527362c",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:c19784034d46da48550487c5c44639f5f92d48be7b9baf4d67b5377a454d92af",
|
||||
"size": 864,
|
||||
"platform": {
|
||||
"os": "linux",
|
||||
"architecture": "mips64le"
|
||||
},
|
||||
"annotations": {
|
||||
"org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:mips64le/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:951bcd144ddccd1ee902dc180b435faabaaa6a8747e70cbc893f2dca16badb94",
|
||||
"size": 566,
|
||||
"platform": {
|
||||
"os": "unknown",
|
||||
"architecture": "unknown"
|
||||
},
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:c19784034d46da48550487c5c44639f5f92d48be7b9baf4d67b5377a454d92af",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:f0c95f1ebb50c9b0b3e3416fb9dd4d1d197386a076c464cceea3d1f94c321b8f",
|
||||
"size": 863,
|
||||
"platform": {
|
||||
"os": "linux",
|
||||
"architecture": "ppc64le"
|
||||
},
|
||||
"annotations": {
|
||||
"org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:ppc64le/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:838d191bca398e46cddebc48e816da83b0389d4ed2d64f408d618521b8fd1a57",
|
||||
"size": 837,
|
||||
"platform": {
|
||||
"os": "unknown",
|
||||
"architecture": "unknown"
|
||||
},
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:f0c95f1ebb50c9b0b3e3416fb9dd4d1d197386a076c464cceea3d1f94c321b8f",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:8d064a6fc27fd5e97fa8225994a1addd872396236367745bea30c92d6c032fa3",
|
||||
"size": 863,
|
||||
"platform": {
|
||||
"os": "linux",
|
||||
"architecture": "riscv64"
|
||||
},
|
||||
"annotations": {
|
||||
"org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:riscv64/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:48147407c4594e45b7c3f0be1019bb0f44d78d7f037ce63e0e3da75b256f849e",
|
||||
"size": 837,
|
||||
"platform": {
|
||||
"os": "unknown",
|
||||
"architecture": "unknown"
|
||||
},
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:8d064a6fc27fd5e97fa8225994a1addd872396236367745bea30c92d6c032fa3",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:65f4b0d1802589b418bb6774d85de3d1a11d5bd971ee73cb8569504d928bb5d9",
|
||||
"size": 861,
|
||||
"platform": {
|
||||
"os": "linux",
|
||||
"architecture": "s390x"
|
||||
},
|
||||
"annotations": {
|
||||
"org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
|
||||
"org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:s390x/hello-world",
|
||||
"org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
|
||||
"org.opencontainers.image.version": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:50f420e8710676da03668e446f1f51097b745e3e2c9807b018e569d26d4f65f7",
|
||||
"size": 837,
|
||||
"platform": {
|
||||
"os": "unknown",
|
||||
"architecture": "unknown"
|
||||
},
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:65f4b0d1802589b418bb6774d85de3d1a11d5bd971ee73cb8569504d928bb5d9",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"digest": "sha256:2f19ce27632e6baf4ebb1b582960d68948e52902c8cfac10133da0058f1dab23",
|
||||
"size": 946,
|
||||
"platform": {
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"os.version": "10.0.20348.2340"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"digest": "sha256:3a0bd0fb5ad6dd6528dc78726b3df78e980b39b379e99c5a508904ec17cfafe5",
|
||||
"size": 946,
|
||||
"platform": {
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"os.version": "10.0.17763.5576"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
include "oci";
|
||||
|
||||
map(normalize_descriptor)
|
||||
| sort_manifests
|
||||
|
|
@ -0,0 +1,840 @@
|
|||
[
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "amd64",
|
||||
"variant": "v4"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "amd64",
|
||||
"variant": "v3"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "amd64",
|
||||
"variant": "v2"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "amd64",
|
||||
"variant": "v1"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "amd64"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.5"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.0"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.1"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.0"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "386"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "arm",
|
||||
"variant": "v8"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "arm",
|
||||
"variant": "v7"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "arm",
|
||||
"variant": "v6"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "arm",
|
||||
"variant": "v5"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "mips64le"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "ppc64le",
|
||||
"variant": "power10"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "ppc64le",
|
||||
"variant": "power9"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "ppc64le",
|
||||
"variant": "power8"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "ppc64le"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "riscv64",
|
||||
"variant": "rva22u64"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "riscv64",
|
||||
"variant": "rva20u64"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "riscv64"
|
||||
},
|
||||
{
|
||||
"os": "linux",
|
||||
"architecture": "s390x"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "amd64",
|
||||
"variant": "v4",
|
||||
"os.version": "13.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "amd64",
|
||||
"variant": "v4",
|
||||
"os.version": "12.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "amd64",
|
||||
"variant": "v3",
|
||||
"os.version": "13.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "amd64",
|
||||
"variant": "v3",
|
||||
"os.version": "12.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "amd64",
|
||||
"variant": "v2",
|
||||
"os.version": "13.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "amd64",
|
||||
"variant": "v2",
|
||||
"os.version": "12.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "amd64",
|
||||
"variant": "v1",
|
||||
"os.version": "13.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "amd64",
|
||||
"variant": "v1",
|
||||
"os.version": "12.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "amd64",
|
||||
"os.version": "13.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "amd64",
|
||||
"os.version": "12.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9",
|
||||
"os.version": "13.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9",
|
||||
"os.version": "12.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.5",
|
||||
"os.version": "13.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.5",
|
||||
"os.version": "12.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.0",
|
||||
"os.version": "13.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.0",
|
||||
"os.version": "12.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8",
|
||||
"os.version": "13.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8",
|
||||
"os.version": "12.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.1",
|
||||
"os.version": "13.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.1",
|
||||
"os.version": "12.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.0",
|
||||
"os.version": "13.1"
|
||||
},
|
||||
{
|
||||
"os": "freebsd",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.0",
|
||||
"os.version": "12.1"
|
||||
},
|
||||
{
|
||||
"os": "unknown",
|
||||
"architecture": "unknown"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v4",
|
||||
"os.version": "10.0.20348.2340"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v4",
|
||||
"os.version": "10.0.19042.1889"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v4",
|
||||
"os.version": "10.0.19041.1415"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v4",
|
||||
"os.version": "10.0.18363.1556"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v4",
|
||||
"os.version": "10.0.18362.1256"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v4",
|
||||
"os.version": "10.0.17763.5576"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v4",
|
||||
"os.version": "10.0.17134.1305"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v4",
|
||||
"os.version": "10.0.16299.1087"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v4",
|
||||
"os.version": "10.0.14393.6796"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v3",
|
||||
"os.version": "10.0.20348.2340"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v3",
|
||||
"os.version": "10.0.19042.1889"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v3",
|
||||
"os.version": "10.0.19041.1415"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v3",
|
||||
"os.version": "10.0.18363.1556"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v3",
|
||||
"os.version": "10.0.18362.1256"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v3",
|
||||
"os.version": "10.0.17763.5576"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v3",
|
||||
"os.version": "10.0.17134.1305"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v3",
|
||||
"os.version": "10.0.16299.1087"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v3",
|
||||
"os.version": "10.0.14393.6796"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v2",
|
||||
"os.version": "10.0.20348.2340"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v2",
|
||||
"os.version": "10.0.19042.1889"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v2",
|
||||
"os.version": "10.0.19041.1415"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v2",
|
||||
"os.version": "10.0.18363.1556"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v2",
|
||||
"os.version": "10.0.18362.1256"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v2",
|
||||
"os.version": "10.0.17763.5576"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v2",
|
||||
"os.version": "10.0.17134.1305"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v2",
|
||||
"os.version": "10.0.16299.1087"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v2",
|
||||
"os.version": "10.0.14393.6796"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v1",
|
||||
"os.version": "10.0.20348.2340"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v1",
|
||||
"os.version": "10.0.19042.1889"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v1",
|
||||
"os.version": "10.0.19041.1415"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v1",
|
||||
"os.version": "10.0.18363.1556"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v1",
|
||||
"os.version": "10.0.18362.1256"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v1",
|
||||
"os.version": "10.0.17763.5576"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v1",
|
||||
"os.version": "10.0.17134.1305"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v1",
|
||||
"os.version": "10.0.16299.1087"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"variant": "v1",
|
||||
"os.version": "10.0.14393.6796"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"os.version": "10.0.20348.2340"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"os.version": "10.0.19042.1889"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"os.version": "10.0.19041.1415"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"os.version": "10.0.18363.1556"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"os.version": "10.0.18362.1256"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"os.version": "10.0.17763.5576"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"os.version": "10.0.17134.1305"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"os.version": "10.0.16299.1087"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "amd64",
|
||||
"os.version": "10.0.14393.6796"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9",
|
||||
"os.version": "10.0.20348.2340"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9",
|
||||
"os.version": "10.0.19042.1889"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9",
|
||||
"os.version": "10.0.19041.1415"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9",
|
||||
"os.version": "10.0.18363.1556"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9",
|
||||
"os.version": "10.0.18362.1256"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9",
|
||||
"os.version": "10.0.17763.5576"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9",
|
||||
"os.version": "10.0.17134.1305"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9",
|
||||
"os.version": "10.0.16299.1087"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9",
|
||||
"os.version": "10.0.14393.6796"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.5",
|
||||
"os.version": "10.0.20348.2340"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.5",
|
||||
"os.version": "10.0.19042.1889"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.5",
|
||||
"os.version": "10.0.19041.1415"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.5",
|
||||
"os.version": "10.0.18363.1556"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.5",
|
||||
"os.version": "10.0.18362.1256"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.5",
|
||||
"os.version": "10.0.17763.5576"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.5",
|
||||
"os.version": "10.0.17134.1305"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.5",
|
||||
"os.version": "10.0.16299.1087"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.5",
|
||||
"os.version": "10.0.14393.6796"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.0",
|
||||
"os.version": "10.0.20348.2340"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.0",
|
||||
"os.version": "10.0.19042.1889"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.0",
|
||||
"os.version": "10.0.19041.1415"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.0",
|
||||
"os.version": "10.0.18363.1556"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.0",
|
||||
"os.version": "10.0.18362.1256"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.0",
|
||||
"os.version": "10.0.17763.5576"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.0",
|
||||
"os.version": "10.0.17134.1305"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.0",
|
||||
"os.version": "10.0.16299.1087"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v9.0",
|
||||
"os.version": "10.0.14393.6796"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8",
|
||||
"os.version": "10.0.20348.2340"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8",
|
||||
"os.version": "10.0.19042.1889"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8",
|
||||
"os.version": "10.0.19041.1415"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8",
|
||||
"os.version": "10.0.18363.1556"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8",
|
||||
"os.version": "10.0.18362.1256"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8",
|
||||
"os.version": "10.0.17763.5576"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8",
|
||||
"os.version": "10.0.17134.1305"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8",
|
||||
"os.version": "10.0.16299.1087"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8",
|
||||
"os.version": "10.0.14393.6796"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.1",
|
||||
"os.version": "10.0.20348.2340"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.1",
|
||||
"os.version": "10.0.19042.1889"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.1",
|
||||
"os.version": "10.0.19041.1415"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.1",
|
||||
"os.version": "10.0.18363.1556"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.1",
|
||||
"os.version": "10.0.18362.1256"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.1",
|
||||
"os.version": "10.0.17763.5576"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.1",
|
||||
"os.version": "10.0.17134.1305"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.1",
|
||||
"os.version": "10.0.16299.1087"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.1",
|
||||
"os.version": "10.0.14393.6796"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.0",
|
||||
"os.version": "10.0.20348.2340"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.0",
|
||||
"os.version": "10.0.19042.1889"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.0",
|
||||
"os.version": "10.0.19041.1415"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.0",
|
||||
"os.version": "10.0.18363.1556"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.0",
|
||||
"os.version": "10.0.18362.1256"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.0",
|
||||
"os.version": "10.0.17763.5576"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.0",
|
||||
"os.version": "10.0.17134.1305"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.0",
|
||||
"os.version": "10.0.16299.1087"
|
||||
},
|
||||
{
|
||||
"os": "windows",
|
||||
"architecture": "arm64",
|
||||
"variant": "v8.0",
|
||||
"os.version": "10.0.14393.6796"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
include "oci";
|
||||
|
||||
[
|
||||
{
|
||||
os: "linux",
|
||||
architecture: (
|
||||
"386",
|
||||
"amd64",
|
||||
"arm",
|
||||
"arm64",
|
||||
"mips64le",
|
||||
"ppc64le",
|
||||
"riscv64",
|
||||
"s390x",
|
||||
empty
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
os: "windows",
|
||||
architecture: ( "amd64", "arm64" ),
|
||||
"os.version": (
|
||||
# https://learn.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/base-image-lifecycle
|
||||
# https://oci.dag.dev/?repo=mcr.microsoft.com/windows/servercore
|
||||
# https://oci.dag.dev/?image=hell/win:core
|
||||
"10.0.14393.6796",
|
||||
"10.0.16299.1087",
|
||||
"10.0.17134.1305",
|
||||
"10.0.17763.5576",
|
||||
"10.0.18362.1256",
|
||||
"10.0.18363.1556",
|
||||
"10.0.19041.1415",
|
||||
"10.0.19042.1889",
|
||||
"10.0.20348.2340",
|
||||
empty
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
os: "freebsd",
|
||||
architecture: ( "amd64", "arm64" ),
|
||||
"os.version": ( "12.1", "13.1" ),
|
||||
},
|
||||
|
||||
# buildkit attestations
|
||||
# https://github.com/moby/buildkit/blob/5e0fe2793d529209ad52e811129f644d972ea094/docs/attestations/attestation-storage.md#attestation-manifest-descriptor
|
||||
{
|
||||
architecture: "unknown",
|
||||
os: "unknown",
|
||||
},
|
||||
|
||||
empty
|
||||
]
|
||||
|
||||
# explode out variant matricies
|
||||
| map(
|
||||
{
|
||||
# https://github.com/opencontainers/image-spec/pull/1172
|
||||
amd64: [ "v1", "v2", "v3", "v4" ],
|
||||
arm64: [ "v8", "v9", "v8.0", "v9.0", "v8.1", "v9.5" ],
|
||||
arm: [ "v5", "v6", "v7", "v8" ],
|
||||
riscv64: [ "rva20u64", "rva22u64" ],
|
||||
ppc64le: [ "power8", "power9", "power10" ],
|
||||
}[.architecture] as $variants
|
||||
| ., if $variants then
|
||||
. + { variant: $variants[] }
|
||||
else empty end
|
||||
)
|
||||
|
||||
| map(normalize_platform)
|
||||
| unique
|
||||
| sort_by(sort_split_platform)
|
||||
142
.test/test.sh
142
.test/test.sh
|
|
@ -20,6 +20,11 @@ dir="$(dirname "$BASH_SOURCE")"
|
|||
dir="$(readlink -ve "$dir")"
|
||||
export BASHBREW_LIBRARY="$dir/library"
|
||||
|
||||
doDeploy=
|
||||
if [ "${1:-}" = '--deploy' ]; then
|
||||
doDeploy=1
|
||||
fi
|
||||
|
||||
set -- docker:cli docker:dind docker:windowsservercore notary busybox:latest # a little bit of Windows, a little bit of Linux, a little bit of multi-stage, a little bit of oci-import
|
||||
# (see "library/" and ".external-pins/" for where these come from / are hard-coded for consistent testing purposes)
|
||||
# NOTE: we are explicitly *not* pinning "golang:1.19-alpine3.16" so that this also tests unpinned parent behavior (that image is deprecated so should stay unchanging)
|
||||
|
|
@ -28,18 +33,19 @@ time bashbrew fetch "$@"
|
|||
|
||||
time "$dir/../sources.sh" "$@" > "$dir/sources.json"
|
||||
|
||||
rm -rf "$dir/coverage"
|
||||
mkdir -p "$dir/coverage"
|
||||
export GOCOVERDIR="${GOCOVERDIR:-"$dir/coverage"}"
|
||||
coverage="$dir/.coverage"
|
||||
rm -rf "$coverage/GOCOVERDIR" "$coverage/bin"
|
||||
mkdir -p "$coverage/GOCOVERDIR" "$coverage/bin"
|
||||
export GOCOVERDIR="${GOCOVERDIR:-"$coverage/GOCOVERDIR"}"
|
||||
|
||||
rm -f "$dir/../bin/builds" # make sure we build with -cover for sure
|
||||
time "$dir/../builds.sh" --cache "$dir/cache-builds.json" "$dir/sources.json" > "$dir/builds.json"
|
||||
time "$coverage/builds.sh" --cache "$dir/cache-builds.json" "$dir/sources.json" > "$dir/builds.json"
|
||||
[ -s "$coverage/bin/builds" ] # just to make sure it actually did build/use an appropriate binary 🙈
|
||||
|
||||
# test again, but with "--cache=..." instead of "--cache ..." (which also lets us delete the cache and get slightly better coverage reports at the expense of speed / Hub requests)
|
||||
time "$dir/../builds.sh" --cache="$dir/cache-builds.json" "$dir/sources.json" > "$dir/builds.json"
|
||||
time "$coverage/builds.sh" --cache="$dir/cache-builds.json" "$dir/sources.json" > "$dir/builds.json"
|
||||
|
||||
# test "lookup" code for more edge cases
|
||||
"$dir/../.go-env.sh" go build -cover -trimpath -o "$dir/../bin/lookup" ./cmd/lookup
|
||||
"$dir/../.go-env.sh" go build -cover -trimpath -o "$coverage/bin/lookup" ./cmd/lookup
|
||||
lookup=(
|
||||
# force a config blob lookup for platform object creation (and top-level Docker media type!)
|
||||
'tianon/test@sha256:2f19ce27632e6baf4ebb1b582960d68948e52902c8cfac10133da0058f1dab23'
|
||||
|
|
@ -73,7 +79,7 @@ lookup=(
|
|||
--head "tianon/this-is-a-repository-that-will-never-ever-exist-$RANDOM-$RANDOM:$RANDOM-$RANDOM"
|
||||
'tianon/test@sha256:0000000000000000000000000000000000000000000000000000000000000000'
|
||||
)
|
||||
"$dir/../bin/lookup" "${lookup[@]}" | jq -s '
|
||||
"$coverage/bin/lookup" "${lookup[@]}" | jq -s '
|
||||
[
|
||||
reduce (
|
||||
$ARGS.positional[]
|
||||
|
|
@ -93,32 +99,106 @@ lookup=(
|
|||
] | transpose
|
||||
' --args -- "${lookup[@]}" > "$dir/lookup-test.json"
|
||||
|
||||
# don't leave around the "-cover" versions of these binaries
|
||||
rm -f "$dir/../bin/builds" "$dir/../bin/lookup"
|
||||
# TODO a *lot* of this could be converted to unit tests via `ocimem` (but then we have to synthesize appropriate edge-case content instead of pulling/copying it, so there's some hurdles to overcome there when we look into doing so)
|
||||
if [ -n "$doDeploy" ]; then
|
||||
# also test "deploy" (optional, disabled by default, because it's a much heavier test)
|
||||
|
||||
"$dir/../.go-env.sh" go build -cover -trimpath -o "$coverage/bin/deploy" ./cmd/deploy
|
||||
|
||||
docker rm -vf meta-scripts-test-registry &> /dev/null || :
|
||||
trap 'docker rm -vf meta-scripts-test-registry &> /dev/null || :' EXIT
|
||||
docker run --detach --name meta-scripts-test-registry --publish 5000 registry:2
|
||||
registryPort="$(DOCKER_API_VERSION=1.41 docker container inspect --format '{{ index .NetworkSettings.Ports "5000/tcp" 0 "HostPort" }}' meta-scripts-test-registry)"
|
||||
|
||||
# apparently Tianon's local system is too good and the registry spins up fast enough, but this needs a small "wait for the registry to be ready" loop for systems like GHA (adding "--cpus 0.01" to the above "docker run" seems to replicate the race reasonably well)
|
||||
tries=10
|
||||
while [ "$(( tries-- ))" -gt 0 ]; do
|
||||
if docker logs meta-scripts-test-registry |& grep -F ' listening on '; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
json="$(jq -n --arg reg "localhost:$registryPort" '
|
||||
# explicit base64 data blob
|
||||
{
|
||||
type: "blob",
|
||||
refs: [$reg+"/test@sha256:1a51828d59323e0e02522c45652b6a7a44a032b464b06d574f067d2358b0e9f1"],
|
||||
data: "YnVmZnkgdGhlIHZhbXBpcmUgc2xheWVyCg==",
|
||||
},
|
||||
|
||||
# JSON data blob
|
||||
{
|
||||
type: "blob",
|
||||
refs: [$reg+"/test@sha256:bdc1ce731138e680ada95089dded3015b8e1570d9a70216867a2a29801a747b3"],
|
||||
data: { foo: "bar", baz: [ "buzz", "buzz", "buzz" ] },
|
||||
},
|
||||
|
||||
# make sure JSON strings round-trip correctly too
|
||||
{
|
||||
type: "blob",
|
||||
refs: [$reg+"/test@sha256:680c1729a6d4a34f69123f5936cfd4f2cb82a008951241cfc499f9e52996b380"],
|
||||
data: ("json string" | @json + "\n" | @base64),
|
||||
},
|
||||
|
||||
# test blob mounting between repositories
|
||||
{
|
||||
type: "blob",
|
||||
refs: [$reg+"/test-mount"],
|
||||
lookup: { "": ($reg+"/test@sha256:1a51828d59323e0e02522c45652b6a7a44a032b464b06d574f067d2358b0e9f1") },
|
||||
},
|
||||
|
||||
# (cross-registry) copy an image from Docker Hub with a blob that is definitely larger than our "BlobSizeWorthHEAD" (and larger than our "manifestSizeLimit" cache limit, so it hits that code too)
|
||||
# https://oci.dag.dev/?image=cirros@sha256:6b2d9f5341bce2b1fb29669ff46744a145079ccc6a674849de3a4946ec3d8ffb ("cirros:latest" as of 2024-03-27)
|
||||
# https://oci.dag.dev/?image=oisupport/staging-amd64:d5093352bd93df3e9effd7a53bdd46834ac0b1766587a645d4503272597a60dc (the amd64-only index containing that build)
|
||||
# .. but first, copy one of the blob explicitly so we test both halves of the conditional
|
||||
{
|
||||
type: "blob",
|
||||
refs: [$reg+"/cirros"],
|
||||
lookup: { "": "oisupport/staging-amd64@sha256:6cef03f2716ee8ba76999750aee1a742888ccd0db923be33ff6a410d87f4277d" },
|
||||
},
|
||||
{
|
||||
type: "manifest",
|
||||
refs: [$reg+"/cirros"],
|
||||
lookup: { "": "oisupport/staging-amd64:34bb44c7d8b6fb7a337fcee0afa7c3a84148e35db6ab83041714c3e6d4c6238b" },
|
||||
},
|
||||
# and again, but with a manifest bigger than "BlobSizeWorthHEAD"
|
||||
# https://oci.dag.dev/?image=tianon/test:screaming-index (big image index, sha256:4077658bc7e39f02f81d1682fe49f66b3db2c420813e43f5db0c53046167c12f)
|
||||
{
|
||||
type: "manifest",
|
||||
refs: [$reg+"/test@sha256:4077658bc7e39f02f81d1682fe49f66b3db2c420813e43f5db0c53046167c12f"],
|
||||
lookup: { "sha256:4077658bc7e39f02f81d1682fe49f66b3db2c420813e43f5db0c53046167c12f": "tianon/test" },
|
||||
},
|
||||
# https://oci.dag.dev/?image=tianon/test:screaming (big image manifest, sha256:96a7a809d1b336011450164564154a5e1c257dc7eb9081e28638537c472ccb90)
|
||||
{
|
||||
type: "manifest",
|
||||
refs: [$reg+"/test@sha256:96a7a809d1b336011450164564154a5e1c257dc7eb9081e28638537c472ccb90"],
|
||||
lookup: { "sha256:96a7a809d1b336011450164564154a5e1c257dc7eb9081e28638537c472ccb90": "tianon/test" },
|
||||
},
|
||||
# again, but this time EVEN BIGGER, just to make sure we test right up to the limit of Docker Hub
|
||||
# https://oci.dag.dev/?image=tianon/test:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
|
||||
{
|
||||
type: "manifest",
|
||||
refs: [$reg+"/test:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"],
|
||||
lookup: { "": "tianon/test:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee@sha256:73614cc99c500aa4fa061368ed349df24a81844e3c2e6d0c31f290a7c8d73c22" },
|
||||
},
|
||||
|
||||
empty
|
||||
')" # stored in a variable for easier debugging ("bash -x")
|
||||
|
||||
"$coverage/bin/deploy" <<<"$json"
|
||||
|
||||
docker rm -vf meta-scripts-test-registry
|
||||
trap - EXIT
|
||||
fi
|
||||
|
||||
# Go tests
|
||||
"$dir/../.go-env.sh" go test -cover ./... -args -test.gocoverdir="$GOCOVERDIR"
|
||||
|
||||
# combine the coverage data into the "legacy" coverage format (understood by "go tool cover") and pre-generate HTML for easier digestion of the data
|
||||
"$dir/../.go-env.sh" go tool covdata textfmt -i "$GOCOVERDIR" -o "$dir/coverage.txt"
|
||||
"$dir/../.go-env.sh" go tool cover -html "$dir/coverage.txt" -o "$dir/coverage.html"
|
||||
"$dir/../.go-env.sh" go tool cover -func "$dir/coverage.txt"
|
||||
"$dir/../.go-env.sh" go tool covdata textfmt -i "$GOCOVERDIR" -o "$coverage/coverage.txt"
|
||||
"$dir/../.go-env.sh" go tool cover -html "$coverage/coverage.txt" -o "$coverage/coverage.html"
|
||||
"$dir/../.go-env.sh" go tool cover -func "$coverage/coverage.txt"
|
||||
|
||||
# generate an "example commands" file so that changes to generated commands are easier to review
|
||||
SOURCE_DATE_EPOCH=0 jq -r -L "$dir/.." '
|
||||
include "meta";
|
||||
[
|
||||
first(.[] | select(normalized_builder == "buildkit")),
|
||||
first(.[] | select(normalized_builder == "classic")),
|
||||
first(.[] | select(normalized_builder == "oci-import")),
|
||||
empty
|
||||
]
|
||||
| map(
|
||||
. as $b
|
||||
| commands
|
||||
| to_entries
|
||||
| map("# <\(.key)>\n\(.value)\n# </\(.key)>")
|
||||
| "# \($b.source.tags[0]) [\($b.build.arch)]\n" + join("\n")
|
||||
)
|
||||
| join("\n\n")
|
||||
' "$dir/builds.json" > "$dir/example-commands.sh"
|
||||
# also run our "jq" tests (like generating example commands from the "builds.json" we just generated)
|
||||
"$dir/jq.sh"
|
||||
|
|
|
|||
|
|
@ -3,11 +3,14 @@ properties([
|
|||
disableConcurrentBuilds(),
|
||||
disableResume(),
|
||||
durabilityHint('PERFORMANCE_OPTIMIZED'),
|
||||
rateLimitBuilds([
|
||||
count: 1,
|
||||
durationName: 'hour',
|
||||
userBoost: true,
|
||||
]),
|
||||
pipelineTriggers([
|
||||
// TODO https://github.com/docker-library/meta-scripts/issues/22
|
||||
//upstream(threshold: 'UNSTABLE', upstreamProjects: 'meta'),
|
||||
cron('H H/2 * * *'),
|
||||
// (we've dropped to only running this periodically to avoid it clogging the whole queue for a no-op, which also gives build+meta more time to cycle and get deps so they have a higher chance to all go out at once -- see the above linked issue)
|
||||
upstream('meta'),
|
||||
cron('H H/6 * * *'), // run every few hours whether we "need" it or not
|
||||
]),
|
||||
])
|
||||
|
||||
|
|
@ -34,46 +37,17 @@ node('multiarch-' + env.BASHBREW_ARCH) { ansiColor('xterm') {
|
|||
))
|
||||
}
|
||||
|
||||
dir('.bin') {
|
||||
deleteDir()
|
||||
|
||||
stage('Crane') {
|
||||
dir('meta') {
|
||||
stage('Generate') {
|
||||
sh '''#!/usr/bin/env bash
|
||||
set -Eeuo pipefail -x
|
||||
|
||||
ext=''
|
||||
if [ "$BASHBREW_ARCH" = 'windows-amd64' ]; then
|
||||
ext='.exe'
|
||||
fi
|
||||
|
||||
# https://doi-janky.infosiftr.net/job/wip/job/crane
|
||||
wget -O "crane$ext" "https://doi-janky.infosiftr.net/job/wip/job/crane/lastSuccessfulBuild/artifact/crane-$BASHBREW_ARCH$ext" --progress=dot:giga
|
||||
# TODO checksum verification ("checksums.txt")
|
||||
chmod +x "crane$ext"
|
||||
"./crane$ext" version
|
||||
jq -L.scripts '
|
||||
include "deploy";
|
||||
arch_tagged_manifests(env.BASHBREW_ARCH)
|
||||
| deploy_objects[]
|
||||
' builds.json | tee deploy.json
|
||||
'''
|
||||
if (env.BASHBREW_ARCH == 'windows-amd64') {
|
||||
env.PATH = "${workspace}/.bin;${env.PATH}"
|
||||
} else {
|
||||
env.PATH = "${workspace}/.bin:${env.PATH}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dir('meta') {
|
||||
def shell = ''
|
||||
stage('Generate') {
|
||||
shell = sh(returnStdout: true, script: '''#!/usr/bin/env bash
|
||||
set -Eeuo pipefail -x
|
||||
|
||||
jq -L.scripts -r '
|
||||
include "jenkins";
|
||||
crane_deploy_commands
|
||||
| sub("^crane "; "crane --mirror \\"$DOCKERHUB_PUBLIC_PROXY_HOST\\" ")
|
||||
' builds.json
|
||||
''').trim()
|
||||
|
||||
shell = shell.replaceAll("\r", '') // deal with Windows...
|
||||
}
|
||||
|
||||
withCredentials([
|
||||
|
|
@ -81,11 +55,18 @@ node('multiarch-' + env.BASHBREW_ARCH) { ansiColor('xterm') {
|
|||
string(credentialsId: 'dockerhub-public-proxy-host', variable: 'DOCKERHUB_PUBLIC_PROXY_HOST'),
|
||||
]) {
|
||||
stage('Deploy') {
|
||||
sh """#!/usr/bin/env bash
|
||||
sh '''#!/usr/bin/env bash
|
||||
set -Eeuo pipefail -x
|
||||
|
||||
${ shell }
|
||||
"""
|
||||
(
|
||||
cd .scripts
|
||||
# TODO make a helper to build binaries correctly/consistently 🙃
|
||||
if ./.any-go-nt.sh bin/deploy; then
|
||||
./.go-env.sh go build -trimpath -o bin/deploy ./cmd/deploy
|
||||
fi
|
||||
)
|
||||
.scripts/bin/deploy < deploy.json
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,15 @@ set -Eeuo pipefail
|
|||
: "${BASHBREW_STAGING_TEMPLATE:=oisupport/staging-ARCH:BUILD}"
|
||||
export BASHBREW_STAGING_TEMPLATE
|
||||
|
||||
# put the binary in the directory of a symlink of "builds.sh" (used for testing coverage; see GOCOVERDIR below)
|
||||
dir="$(dirname "$BASH_SOURCE")"
|
||||
dir="$(readlink -ve "$dir")"
|
||||
bin="$dir/bin/builds"
|
||||
|
||||
# but run the script/build from the directory of the *actual* "builds.sh" script
|
||||
dir="$(readlink -ve "$BASH_SOURCE")"
|
||||
dir="$(dirname "$dir")"
|
||||
|
||||
if ( cd "$dir" && ./.any-go-nt.sh "$bin" ); then
|
||||
{
|
||||
echo "building '$bin'"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,246 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker-library/meta-scripts/registry"
|
||||
|
||||
"cuelabs.dev/go/oci/ociregistry"
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
// see TestNormalizeInput for example use cases / usage (pushing images/indexes, pushing blobs, copying images/indexes/blobs)
|
||||
|
||||
// TODO should this be normalized to registry.LookupType? should that be renamed to registry.ObjectType or something more generic?
|
||||
type deployType string
|
||||
|
||||
const (
|
||||
typeManifest deployType = "manifest"
|
||||
typeBlob deployType = "blob"
|
||||
)
|
||||
|
||||
type inputRaw struct {
|
||||
// which type of thing we're pushing ("manifest" or "blob")
|
||||
Type deployType `json:"type"`
|
||||
|
||||
// where to push the thing ("jsmith/example:latest", "jsmith/example@sha256:xxx", etc)
|
||||
Refs []string `json:"refs"`
|
||||
|
||||
// a lookup table for where to find any children, if necessary (for example, pushing an index and need to be able to query/copy the child manifests, pushing a manifest and needing to copy blobs, etc), or the object we want to copy
|
||||
Lookup map[string]string `json:"lookup,omitempty"`
|
||||
|
||||
// the data to push; if this is a JSON string, it is assumed to be a "raw" base64-encoded byte stream that should be pushed as-is, otherwise it'll be formatted and pushed as JSON (great for index, manifest, config, etc)
|
||||
Data json.RawMessage `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// effectively, this is [inputRaw] but normalized in many ways (with inferred data like where to copy data from being explicit instead)
|
||||
type inputNormalized struct {
|
||||
Type deployType `json:"type"`
|
||||
Refs []registry.Reference `json:"refs"`
|
||||
Lookup map[ociregistry.Digest]registry.Reference `json:"lookup,omitempty"`
|
||||
|
||||
// Data and CopyFrom are mutually exclusive
|
||||
Data []byte `json:"data"`
|
||||
CopyFrom *registry.Reference `json:"copyFrom"`
|
||||
|
||||
Do func(ctx context.Context, dstRef registry.Reference) (ociregistry.Descriptor, error) `json:"-"`
|
||||
}
|
||||
|
||||
func NormalizeInput(raw inputRaw) (inputNormalized, error) {
|
||||
var normal inputNormalized
|
||||
|
||||
switch raw.Type {
|
||||
case "":
|
||||
// TODO is there one of the two types that I might push by hand more often than the other that could be the default when this is unspecified?
|
||||
return normal, fmt.Errorf("missing type")
|
||||
|
||||
case typeManifest, typeBlob:
|
||||
normal.Type = raw.Type
|
||||
|
||||
default:
|
||||
return normal, fmt.Errorf("unknown type: %s", raw.Type)
|
||||
}
|
||||
|
||||
if raw.Refs == nil {
|
||||
return normal, fmt.Errorf("missing refs entirely (JSON input glitch?)")
|
||||
}
|
||||
if len(raw.Refs) == 0 {
|
||||
return normal, fmt.Errorf("zero refs specified for pushing (need at least one)")
|
||||
}
|
||||
normal.Refs = make([]registry.Reference, len(raw.Refs))
|
||||
var refsDigest ociregistry.Digest // if any ref has a digest, they all have to have the same digest (and our data has to match)
|
||||
for i, refString := range raw.Refs {
|
||||
ref, err := registry.ParseRef(refString)
|
||||
if err != nil {
|
||||
return normal, fmt.Errorf("%s: failed to parse ref: %w", refString, err)
|
||||
}
|
||||
|
||||
if ref.Digest != "" {
|
||||
if refsDigest == "" {
|
||||
refsDigest = ref.Digest
|
||||
} else if ref.Digest != refsDigest {
|
||||
return normal, fmt.Errorf("refs digest mismatch in %s: %s", ref, refsDigest)
|
||||
}
|
||||
}
|
||||
|
||||
if normal.Type == typeBlob && ref.Tag != "" {
|
||||
return normal, fmt.Errorf("cannot push blobs to a tag: %s", ref)
|
||||
}
|
||||
|
||||
normal.Refs[i] = ref
|
||||
}
|
||||
debugId := normal.Refs[0]
|
||||
|
||||
normal.Lookup = make(map[ociregistry.Digest]registry.Reference, len(raw.Lookup))
|
||||
var lookupDigest ociregistry.Digest // if we store this out here, we can abuse it later to get the "last" lookup digest (for getting the single key in the case of len(lookup) == 1 without a new loop)
|
||||
for d, refString := range raw.Lookup {
|
||||
lookupDigest = ociregistry.Digest(d)
|
||||
if lookupDigest != "" {
|
||||
// normal.Lookup[""] is a special case for fallback (where to look for any child object that isn't explicitly referenced)
|
||||
if err := lookupDigest.Validate(); err != nil {
|
||||
return normal, fmt.Errorf("%s: lookup key %q invalid: %w", debugId, lookupDigest, err)
|
||||
}
|
||||
}
|
||||
if ref, err := registry.ParseRef(refString); err != nil {
|
||||
return normal, fmt.Errorf("%s: failed to parse lookup ref %q: %v", debugId, refString, err)
|
||||
} else {
|
||||
if ref.Tag != "" && lookupDigest != "" {
|
||||
//return normal, fmt.Errorf("%s: tag on by-digest lookup ref makes no sense: %s (%s)", debugId, ref, d)
|
||||
}
|
||||
|
||||
if ref.Digest == "" && lookupDigest != "" {
|
||||
ref.Digest = lookupDigest
|
||||
}
|
||||
if ref.Digest != lookupDigest && lookupDigest != "" {
|
||||
return normal, fmt.Errorf("%s: digest on lookup ref should either be omitted or match key: %s vs %s", debugId, ref, d)
|
||||
}
|
||||
|
||||
normal.Lookup[lookupDigest] = ref
|
||||
}
|
||||
}
|
||||
|
||||
if raw.Data == nil || bytes.Equal(raw.Data, []byte("null")) {
|
||||
// if we have no Data, let's see if we have enough information to infer an object to copy
|
||||
if lookupRef, ok := normal.Lookup[refsDigest]; refsDigest != "" && ok {
|
||||
// if any of our Refs had a digest, *and* we have a way to Lookup that digest, that's the one
|
||||
lookupDigest = refsDigest
|
||||
normal.CopyFrom = &lookupRef
|
||||
} else if lookupRef, ok := normal.Lookup[lookupDigest]; len(normal.Lookup) == 1 && ok {
|
||||
// if we only had one Lookup entry, that's the one
|
||||
if lookupDigest == "" {
|
||||
// if it was a fallback, it needs at least Tag or Digest (or our refs need Digest, so we can infer)
|
||||
if lookupRef.Digest == "" && lookupRef.Tag == "" {
|
||||
if refsDigest != "" {
|
||||
lookupRef.Digest = refsDigest
|
||||
} else {
|
||||
return normal, fmt.Errorf("%s: (single) fallback needs digest or tag: %s", debugId, lookupRef)
|
||||
}
|
||||
}
|
||||
}
|
||||
normal.CopyFrom = &lookupRef
|
||||
} else {
|
||||
// if Lookup has only a single entry, that's the one (but that's our last chance for inferring intent)
|
||||
return normal, fmt.Errorf("%s: missing data (and lookup is not a single item)", debugId)
|
||||
// TODO *technically* it would be fair to have lookup have two items if one of them is the fallback reference, but it doesn't really make much sense to copy an object from one namespace, but to get all its children from somewhere else
|
||||
}
|
||||
|
||||
if lookupDigest == "" && normal.CopyFrom.Digest != "" {
|
||||
lookupDigest = normal.CopyFrom.Digest
|
||||
}
|
||||
|
||||
if _, ok := normal.Lookup[""]; !ok {
|
||||
// if we don't have a fallback, add this ref as the fallback
|
||||
normal.Lookup[""] = *normal.CopyFrom
|
||||
}
|
||||
|
||||
if refsDigest == "" {
|
||||
refsDigest = lookupDigest
|
||||
} else if lookupDigest != "" && refsDigest != lookupDigest {
|
||||
return normal, fmt.Errorf("%s: copy-by-digest mismatch: %s vs %s", debugId, refsDigest, normal.CopyFrom)
|
||||
}
|
||||
} else {
|
||||
if len(raw.Data) > 0 && raw.Data[0] == '"' {
|
||||
// must be a "raw" base64-string blob, let's decode it so we're ready to push it
|
||||
if err := json.Unmarshal(raw.Data, &normal.Data); err != nil {
|
||||
return normal, fmt.Errorf("%s: failed to parse base64 data blob: %w", debugId, err)
|
||||
}
|
||||
} else {
|
||||
// otherwise it must be JSON input
|
||||
normal.Data = raw.Data
|
||||
if bytes.ContainsRune(normal.Data, '\n') && normal.Data[len(normal.Data)-1] != '\n' {
|
||||
// if it has any newlines in it, we can assume it was pretty-printed and we should ensure it has a trailing newline too (reading json.RawMessage understandably leaves off trailing whitespace)
|
||||
normal.Data = append(normal.Data, '\n')
|
||||
}
|
||||
}
|
||||
|
||||
dataDigest := godigest.FromBytes(normal.Data)
|
||||
if refsDigest == "" {
|
||||
refsDigest = dataDigest
|
||||
} else if refsDigest != dataDigest {
|
||||
return normal, fmt.Errorf("%s: push-by-digest implied by refs, but data does not match: %s vs %s", debugId, refsDigest, dataDigest)
|
||||
}
|
||||
}
|
||||
|
||||
// we already validated above that any ref with a digest was the same as refsDigest, so here we can just blindly clobber them all
|
||||
for i := range normal.Refs {
|
||||
normal.Refs[i].Digest = refsDigest
|
||||
}
|
||||
|
||||
// explicitly clear tag and digest from lookup entries (now that we've inferred any "CopyFrom" out of them, they no longer have any meaning)
|
||||
for d, ref := range normal.Lookup {
|
||||
ref.Tag = ""
|
||||
ref.Digest = ""
|
||||
normal.Lookup[d] = ref
|
||||
}
|
||||
|
||||
switch normal.Type {
|
||||
case typeManifest:
|
||||
if normal.CopyFrom == nil {
|
||||
// instead of asking for mediaType explicitly, we'll enforce that any manifest we push *must* specify mediaType in the manifest itself (which then is *not* a restriction which applies to any children we copy); see https://github.com/opencontainers/distribution-spec/security/advisories/GHSA-mc8v-mgrf-8f4m and https://github.com/opencontainers/image-spec/security/advisories/GHSA-77vh-xpmg-72qh
|
||||
var mediaTypeHaver struct {
|
||||
// https://github.com/opencontainers/image-spec/blob/v1.1.0/specs-go/v1/index.go#L25
|
||||
// https://github.com/opencontainers/image-spec/blob/v1.1.0/specs-go/v1/manifest.go#L24
|
||||
MediaType string `json:"mediaType"`
|
||||
}
|
||||
if err := json.Unmarshal(normal.Data, &mediaTypeHaver); err != nil {
|
||||
return normal, fmt.Errorf("%s: failed to parse %s data for mediaType: %w", debugId, normal.Type, err)
|
||||
}
|
||||
if mediaTypeHaver.MediaType == "" {
|
||||
// we could just leave this blank and leave it up to the registry to reject instead: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#push:~:text=Clients%20SHOULD%20set,the%20mediaType%20field.
|
||||
// however, PushManifest expects mediaType: https://github.com/cue-labs/oci/blob/f3720d0e1bec6540a9b3c8783af010f51ad5cc95/ociregistry/ociclient/writer.go#L53
|
||||
// and our logic for pushing children needs to know the mediaType (see the GHSAs referenced above)
|
||||
return normal, fmt.Errorf("%s: pushing manifest but missing 'mediaType'", debugId)
|
||||
}
|
||||
normal.Do = func(ctx context.Context, dstRef registry.Reference) (ociregistry.Descriptor, error) {
|
||||
return registry.EnsureManifest(ctx, dstRef, normal.Data, mediaTypeHaver.MediaType, normal.Lookup)
|
||||
}
|
||||
} else {
|
||||
normal.Do = func(ctx context.Context, dstRef registry.Reference) (ociregistry.Descriptor, error) {
|
||||
return registry.CopyManifest(ctx, *normal.CopyFrom, dstRef, normal.Lookup)
|
||||
}
|
||||
}
|
||||
|
||||
case typeBlob:
|
||||
if normal.CopyFrom == nil {
|
||||
normal.Do = func(ctx context.Context, dstRef registry.Reference) (ociregistry.Descriptor, error) {
|
||||
return registry.EnsureBlob(ctx, dstRef, int64(len(normal.Data)), bytes.NewReader(normal.Data))
|
||||
}
|
||||
} else {
|
||||
if normal.CopyFrom.Digest == "" {
|
||||
return normal, fmt.Errorf("%s: blobs are always by-digest, and thus need a digest: %s", debugId, normal.CopyFrom)
|
||||
}
|
||||
normal.Do = func(ctx context.Context, dstRef registry.Reference) (ociregistry.Descriptor, error) {
|
||||
return registry.CopyBlob(ctx, *normal.CopyFrom, dstRef)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
panic("unknown type: " + string(normal.Type))
|
||||
// panic instead of error because this should've already been handled/normalized above (so this is a coding error, not a runtime error)
|
||||
}
|
||||
|
||||
return normal, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeInput(t *testing.T) {
|
||||
for _, x := range []struct {
|
||||
name string
|
||||
raw string
|
||||
normal string
|
||||
}{
|
||||
{
|
||||
"manifest JSON",
|
||||
`{
|
||||
"type": "manifest",
|
||||
"refs": [ "localhost:5000/example:test" ],
|
||||
"data": {"mediaType": "application/vnd.oci.image.index.v1+json"}
|
||||
}`,
|
||||
`{"type":"manifest","refs":["localhost:5000/example:test@sha256:0ae6b7b9d0bc73ee36c1adef005deb431e94cf009c6a947718b31da3d668032d"],"data":"eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIn0=","copyFrom":null}`,
|
||||
},
|
||||
{
|
||||
"manifest raw",
|
||||
`{
|
||||
"type": "manifest",
|
||||
"refs": [ "localhost:5000/example" ],
|
||||
"data": "eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIn0="
|
||||
}`,
|
||||
`{"type":"manifest","refs":["localhost:5000/example@sha256:0ae6b7b9d0bc73ee36c1adef005deb431e94cf009c6a947718b31da3d668032d"],"data":"eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIn0=","copyFrom":null}`,
|
||||
},
|
||||
|
||||
{
|
||||
"index with children",
|
||||
`{
|
||||
"type": "manifest",
|
||||
"refs": [ "localhost:5000/example:test" ],
|
||||
"lookup": { "sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d": "tianon/true" },
|
||||
"data": {"mediaType": "application/vnd.oci.image.index.v1+json","manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d","size":1165}],"schemaVersion":2}
|
||||
}`,
|
||||
`{"type":"manifest","refs":["localhost:5000/example:test@sha256:0cb474919526d040392883b84e5babb65a149cc605b89b117781ab94e88a5e86"],"lookup":{"sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d":"tianon/true"},"data":"eyJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5pbmRleC52MStqc29uIiwibWFuaWZlc3RzIjpbeyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQub2NpLmltYWdlLm1hbmlmZXN0LnYxK2pzb24iLCJkaWdlc3QiOiJzaGEyNTY6OWVmNDJmMWQ2MDJmYjQyM2ZhZDkzNWFhYzFjYWEwY2ZkYmNlMWFkN2VkY2U2NGQwODBhNGViN2IxM2Y3Y2Q5ZCIsInNpemUiOjExNjV9XSwic2NoZW1hVmVyc2lvbiI6Mn0=","copyFrom":null}`,
|
||||
},
|
||||
{
|
||||
"image",
|
||||
`{
|
||||
"type": "manifest",
|
||||
"refs": [ "localhost:5000/example" ],
|
||||
"lookup": { "": "tianon/true" },
|
||||
"data": {"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":1471,"digest":"sha256:690912094c0165c489f874c72cee4ba208c28992c0699fa6e10d8cc59f93fec9"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":129,"digest":"sha256:4c74d744397d4bcbd3079d9c82a87b80d43da376313772978134d1288f20518c"}]}
|
||||
}`,
|
||||
`{"type":"manifest","refs":["localhost:5000/example@sha256:1c70f9d471b83100c45d5a218d45bbf7e073e11ea5043758a020379a7c78f878"],"lookup":{"":"tianon/true"},"data":"eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoxNDcxLCJkaWdlc3QiOiJzaGEyNTY6NjkwOTEyMDk0YzAxNjVjNDg5Zjg3NGM3MmNlZTRiYTIwOGMyODk5MmMwNjk5ZmE2ZTEwZDhjYzU5ZjkzZmVjOSJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoxMjksImRpZ2VzdCI6InNoYTI1Njo0Yzc0ZDc0NDM5N2Q0YmNiZDMwNzlkOWM4MmE4N2I4MGQ0M2RhMzc2MzEzNzcyOTc4MTM0ZDEyODhmMjA1MThjIn1dfQ==","copyFrom":null}`,
|
||||
},
|
||||
|
||||
{
|
||||
"blob raw",
|
||||
`{
|
||||
"type": "blob",
|
||||
"refs": [ "localhost:5000/example@sha256:1a51828d59323e0e02522c45652b6a7a44a032b464b06d574f067d2358b0e9f1" ],
|
||||
"data": "YnVmZnkgdGhlIHZhbXBpcmUgc2xheWVyCg=="
|
||||
}`,
|
||||
`{"type":"blob","refs":["localhost:5000/example@sha256:1a51828d59323e0e02522c45652b6a7a44a032b464b06d574f067d2358b0e9f1"],"data":"YnVmZnkgdGhlIHZhbXBpcmUgc2xheWVyCg==","copyFrom":null}`,
|
||||
},
|
||||
{
|
||||
"blob json",
|
||||
`{
|
||||
"type": "blob",
|
||||
"refs": [ "localhost:5000/example@sha256:d914176fd50bd7f565700006a31aa97b79d3ad17cee20c8e5ff2061d5cb74817" ],
|
||||
"data": {
|
||||
}
|
||||
}`,
|
||||
`{"type":"blob","refs":["localhost:5000/example@sha256:d914176fd50bd7f565700006a31aa97b79d3ad17cee20c8e5ff2061d5cb74817"],"data":"ewp9Cg==","copyFrom":null}`,
|
||||
},
|
||||
|
||||
{
|
||||
"copy manifest (single lookup)",
|
||||
`{
|
||||
"type": "manifest",
|
||||
"refs": [ "localhost:5000/example" ],
|
||||
"lookup": { "sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d": "tianon/true" }
|
||||
}`,
|
||||
`{"type":"manifest","refs":["localhost:5000/example@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"],"lookup":{"":"tianon/true","sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d":"tianon/true"},"data":null,"copyFrom":"tianon/true@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"}`,
|
||||
},
|
||||
{
|
||||
"copy manifest (fallback lookup)",
|
||||
`{
|
||||
"type": "manifest",
|
||||
"refs": [ "localhost:5000/example" ],
|
||||
"lookup": { "": "tianon/true@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d" }
|
||||
}`,
|
||||
`{"type":"manifest","refs":["localhost:5000/example@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"],"lookup":{"":"tianon/true"},"data":null,"copyFrom":"tianon/true@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"}`,
|
||||
},
|
||||
{
|
||||
"copy manifest (ref digest+fallback)",
|
||||
`{
|
||||
"type": "manifest",
|
||||
"refs": [ "localhost:5000/example@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d" ],
|
||||
"lookup": { "": "tianon/true" }
|
||||
}`,
|
||||
`{"type":"manifest","refs":["localhost:5000/example@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"],"lookup":{"":"tianon/true"},"data":null,"copyFrom":"tianon/true@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"}`,
|
||||
},
|
||||
{
|
||||
"copy manifest (tag)",
|
||||
`{
|
||||
"type": "manifest",
|
||||
"refs": [ "localhost:5000/example:test" ],
|
||||
"lookup": { "": "tianon/true:oci" }
|
||||
}`,
|
||||
`{"type":"manifest","refs":["localhost:5000/example:test"],"lookup":{"":"tianon/true"},"data":null,"copyFrom":"tianon/true:oci"}`,
|
||||
},
|
||||
|
||||
{
|
||||
"copy blob (single lookup)",
|
||||
`{
|
||||
"type": "blob",
|
||||
"refs": [ "localhost:5000/example" ],
|
||||
"lookup": { "sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e": "tianon/true" }
|
||||
}`,
|
||||
`{"type":"blob","refs":["localhost:5000/example@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e"],"lookup":{"":"tianon/true","sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e":"tianon/true"},"data":null,"copyFrom":"tianon/true@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e"}`,
|
||||
},
|
||||
{
|
||||
"copy blob (fallback lookup)",
|
||||
`{
|
||||
"type": "blob",
|
||||
"refs": [ "localhost:5000/example" ],
|
||||
"lookup": { "": "tianon/true@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e" }
|
||||
}`,
|
||||
`{"type":"blob","refs":["localhost:5000/example@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e"],"lookup":{"":"tianon/true"},"data":null,"copyFrom":"tianon/true@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e"}`,
|
||||
},
|
||||
{
|
||||
"copy blob (ref digest+fallback)",
|
||||
`{
|
||||
"type": "blob",
|
||||
"refs": [ "localhost:5000/example@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e" ],
|
||||
"lookup": { "": "tianon/true" }
|
||||
}`,
|
||||
`{"type":"blob","refs":["localhost:5000/example@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e"],"lookup":{"":"tianon/true"},"data":null,"copyFrom":"tianon/true@sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e"}`,
|
||||
},
|
||||
|
||||
{
|
||||
"multiple refs",
|
||||
`{
|
||||
"type": "manifest",
|
||||
"refs": [
|
||||
"localhost:5000/foo@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d",
|
||||
"localhost:5000/bar",
|
||||
"localhost:5000/baz@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"
|
||||
],
|
||||
"lookup": { "": "tianon/true" }
|
||||
}`,
|
||||
`{"type":"manifest","refs":["localhost:5000/foo@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d","localhost:5000/bar@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d","localhost:5000/baz@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"],"lookup":{"":"tianon/true"},"data":null,"copyFrom":"tianon/true@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"}`,
|
||||
},
|
||||
{
|
||||
"multiple refs + multiple lookup (copy)",
|
||||
`{
|
||||
"type": "manifest",
|
||||
"refs": [
|
||||
"localhost:5000/foo@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d",
|
||||
"localhost:5000/bar",
|
||||
"localhost:5000/baz@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"
|
||||
],
|
||||
"lookup": {
|
||||
"sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d": "tianon/true",
|
||||
"sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e": "tianon/true"
|
||||
}
|
||||
}`,
|
||||
`{"type":"manifest","refs":["localhost:5000/foo@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d","localhost:5000/bar@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d","localhost:5000/baz@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"],"lookup":{"":"tianon/true","sha256:25be82253336f0b8c4347bc4ecbbcdc85d0e0f118ccf8dc2e119c0a47a0a486e":"tianon/true","sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d":"tianon/true"},"data":null,"copyFrom":"tianon/true@sha256:9ef42f1d602fb423fad935aac1caa0cfdbce1ad7edce64d080a4eb7b13f7cd9d"}`,
|
||||
},
|
||||
} {
|
||||
x := x // https://github.com/golang/go/issues/60078
|
||||
t.Run(x.name, func(t *testing.T) {
|
||||
var raw inputRaw
|
||||
if err := json.Unmarshal([]byte(x.raw), &raw); err != nil {
|
||||
t.Fatalf("JSON parse error: %v", err)
|
||||
}
|
||||
normal, err := NormalizeInput(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("normalize error: %v", err)
|
||||
}
|
||||
if b, err := json.Marshal(normal); err != nil {
|
||||
t.Fatalf("JSON generate error: %v", err)
|
||||
} else if string(b) != x.normal {
|
||||
t.Fatalf("got:\n%s\n\nexpected:\n%s\n", string(b), x.normal)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer stop()
|
||||
|
||||
// TODO --dry-run ?
|
||||
|
||||
// TODO the best we can do on whether or not this actually updated tags is "yes, definitely (we had to copy some children)" and "maybe (we didn't have to copy any children)", but we should maybe still output those so we can trigger put-shared based on them (~immediately on "definitely" and with some medium delay on "maybe")
|
||||
|
||||
// see "input.go" and "inputRaw" for details on the expected JSON input format
|
||||
|
||||
// we pass through "jq" to pretty-print any JSON-form data fields with sane whitespace
|
||||
jq := exec.Command("jq", "del(.data), .data")
|
||||
jq.Stdin = os.Stdin
|
||||
jq.Stderr = os.Stderr
|
||||
|
||||
stdout, err := jq.StdoutPipe()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := jq.Start(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(stdout)
|
||||
for dec.More() {
|
||||
var raw inputRaw
|
||||
if err := dec.Decode(&raw); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := dec.Decode(&raw.Data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
normal, err := NormalizeInput(raw)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
refsDigest := normal.Refs[0].Digest
|
||||
|
||||
if normal.CopyFrom == nil {
|
||||
fmt.Printf("Pushing %s %s:\n", raw.Type, refsDigest)
|
||||
} else {
|
||||
fmt.Printf("Copying %s %s:\n", raw.Type, *normal.CopyFrom)
|
||||
}
|
||||
|
||||
for _, ref := range normal.Refs {
|
||||
fmt.Printf(" - %s", ref.StringWithKnownDigest(refsDigest))
|
||||
desc, err := normal.Do(ctx, ref)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, " -- ERROR: %v\n", err)
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
if ref.Digest == "" && refsDigest == "" {
|
||||
fmt.Printf("@%s", desc.Digest)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
include "oci";
|
||||
|
||||
# input: array of "build" objects (with "buildId" top level keys)
|
||||
# output: map of { "tag": [ list of OCI descriptors ], ... }
|
||||
def tagged_manifests(builds_selector; tags_extractor):
|
||||
reduce (.[] | select(.build.resolved and builds_selector)) as $i ({};
|
||||
.[
|
||||
$i
|
||||
| tags_extractor
|
||||
| ..|strings # no matter what "tags_extractor" gives us, this will flatten us to a stream of strings
|
||||
] += $i.build.resolved.manifests
|
||||
)
|
||||
;
|
||||
def arch_tagged_manifests($arch):
|
||||
tagged_manifests(.build.arch == $arch; .source.arches[.build.arch].archTags)
|
||||
;
|
||||
|
||||
# input: output of tagged_manifests (map of tag -> list of OCI descriptors)
|
||||
# output: array of input objects for "cmd/deploy" ({ "type": "manifest", "refs": [ ... ], "data": { ... } })
|
||||
def deploy_objects:
|
||||
reduce to_entries[] as $in ({};
|
||||
$in.key as $ref
|
||||
| (
|
||||
$in.value
|
||||
| map(normalize_descriptor) # normalized platforms *and* normalized field ordering
|
||||
| sort_manifests
|
||||
) as $manifests
|
||||
| ([ $manifests[].digest ] | join("\n")) as $key
|
||||
| .[$key] |= (
|
||||
if . then
|
||||
.refs += [ $ref ]
|
||||
else
|
||||
{
|
||||
type: "manifest",
|
||||
refs: [ $ref ],
|
||||
|
||||
# add appropriate "lookup" values for copying child objects properly
|
||||
lookup: (
|
||||
$manifests
|
||||
| map({
|
||||
key: .digest,
|
||||
value: (
|
||||
.digest as $dig
|
||||
| .annotations["org.opencontainers.image.ref.name"]
|
||||
| rtrimstr("@" + $dig)
|
||||
),
|
||||
})
|
||||
| from_entries
|
||||
),
|
||||
|
||||
# convert the list of "manifests" into a full (canonical!) index/manifest list for deploying
|
||||
data: {
|
||||
schemaVersion: 2,
|
||||
mediaType: (
|
||||
if $manifests[0]?.mediaType == "application/vnd.docker.distribution.manifest.v2+json" then
|
||||
"application/vnd.docker.distribution.manifest.list.v2+json"
|
||||
else
|
||||
"application/vnd.oci.image.index.v1+json"
|
||||
end
|
||||
),
|
||||
manifests: (
|
||||
$manifests
|
||||
| del(.[].annotations["org.opencontainers.image.ref.name"])
|
||||
),
|
||||
},
|
||||
}
|
||||
end
|
||||
)
|
||||
)
|
||||
| [ .[] ] # strip off our synthetic map keys to avoid leaking our implementation detail
|
||||
;
|
||||
2
go.mod
2
go.mod
|
|
@ -21,4 +21,4 @@ require (
|
|||
)
|
||||
|
||||
// https://github.com/cue-labs/oci/pull/29
|
||||
replace cuelabs.dev/go/oci/ociregistry => github.com/tianon/cuelabs-oci/ociregistry v0.0.0-20240322151419-7d3242933116
|
||||
replace cuelabs.dev/go/oci/ociregistry => github.com/tianon/cuelabs-oci/ociregistry v0.0.0-20240329232705-b652d611e4b3
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -32,8 +32,8 @@ github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
|
|||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tianon/cuelabs-oci/ociregistry v0.0.0-20240322151419-7d3242933116 h1:ZDy4uRAhzODJXRo4EoNpJTCiSeOs8wwrkfMJy3JyDps=
|
||||
github.com/tianon/cuelabs-oci/ociregistry v0.0.0-20240322151419-7d3242933116/go.mod h1:pK23AUVXuNzzTpfMCA06sxZGeVQ/75FdVtW249de9Uo=
|
||||
github.com/tianon/cuelabs-oci/ociregistry v0.0.0-20240329232705-b652d611e4b3 h1:kfAfFbiZ+2ZErqgqKtaMge1qeeE/0rnxuTl21G7fSwk=
|
||||
github.com/tianon/cuelabs-oci/ociregistry v0.0.0-20240329232705-b652d611e4b3/go.mod h1:pK23AUVXuNzzTpfMCA06sxZGeVQ/75FdVtW249de9Uo=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
include "sort";
|
||||
|
||||
# https://github.com/opencontainers/image-spec/blob/v1.1.0/image-index.md#:~:text=generate%20an%20error.-,platform%20object,-This%20OPTIONAL%20property
|
||||
|
||||
# input: OCI "platform" object (see link above)
|
||||
# output: normalized OCI "platform" object
|
||||
def normalize_platform:
|
||||
.variant = (
|
||||
{
|
||||
# https://github.com/golang/go/blob/e85968670e35fc24987944c56277d80d7884e9cc/src/cmd/dist/build.go#L145-L185
|
||||
# https://github.com/golang/go/blob/e85968670e35fc24987944c56277d80d7884e9cc/src/internal/buildcfg/cfg.go#L58-L175
|
||||
# https://github.com/containerd/platforms/blob/db76a43eaea9a004a5f240620f966b0081123884/database.go#L75-L109
|
||||
# https://github.com/opencontainers/image-spec/blob/v1.1.0/image-index.md#platform-variants
|
||||
|
||||
#"amd64/": "v1", # TODO https://github.com/opencontainers/image-spec/pull/1172
|
||||
"arm/": "v7",
|
||||
"arm64/": "v8", # TODO v8.0 ?? https://github.com/golang/go/issues/60905 -> https://go-review.googlesource.com/c/go/+/559555/comment/e2049987_1bc3a065/ (no support for vX.Y in containerd; likely nowhere else either); https://github.com/opencontainers/image-spec/pull/1172
|
||||
#"ppc64le/": "power8", # TODO https://github.com/opencontainers/image-spec/pull/1172
|
||||
#"riscv64/": "rva20u64", # TODO https://github.com/opencontainers/image-spec/pull/1172
|
||||
}["\(.architecture // "")/\(.variant // "")"]
|
||||
// .variant
|
||||
)
|
||||
| to_entries
|
||||
| sort_by(.key | sort_split_pref([
|
||||
"os",
|
||||
"architecture",
|
||||
"variant",
|
||||
"os.version",
|
||||
empty # trailing comma hack
|
||||
]))
|
||||
| map(select(.value))
|
||||
| from_entries
|
||||
;
|
||||
|
||||
# input: *normalized* OCI "platform" object (see link above)
|
||||
# output: something suitable for use in "sort_by" for sorting things based on platform
|
||||
def sort_split_platform:
|
||||
.["os", "architecture", "variant", "os.version"] //= ""
|
||||
| [
|
||||
(.os | sort_split_pref([ "linux" ])),
|
||||
(.architecture | sort_split_pref([ "amd64", "arm64" ])),
|
||||
(.variant | sort_split_natural | sort_split_desc),
|
||||
(.["os.version"] | sort_split_natural | sort_split_desc),
|
||||
empty # trailing comma hack
|
||||
]
|
||||
;
|
||||
|
||||
# https://github.com/opencontainers/image-spec/blob/v1.1.0/descriptor.md
|
||||
|
||||
def normalize_descriptor:
|
||||
if .platform then
|
||||
.platform |= normalize_platform
|
||||
else . end
|
||||
| to_entries
|
||||
| sort_by(.key | sort_split_pref([
|
||||
"mediaType",
|
||||
"artifactType",
|
||||
"digest",
|
||||
"size",
|
||||
"platform",
|
||||
"annotations",
|
||||
empty # trailing comma hack
|
||||
]; [
|
||||
"urls",
|
||||
"data",
|
||||
empty # trailing comma hack
|
||||
]))
|
||||
| from_entries
|
||||
;
|
||||
|
||||
# https://github.com/opencontainers/image-spec/blob/v1.1.0/image-index.md#:~:text=manifests%20array%20of%20objects
|
||||
|
||||
# input: list of OCI "descriptor" objects (the "manifests" array of an image index; see link above)
|
||||
# output: the same list, sorted such that attestation manifests are next to their subject
|
||||
def sort_attestations:
|
||||
[ .[].digest ] as $digs
|
||||
| sort_by(
|
||||
.digest as $dig
|
||||
| .annotations["vnd.docker.reference.digest"] as $subject
|
||||
| ($digs | index($subject // $dig) * 2)
|
||||
+ if $subject then 1 else 0 end
|
||||
)
|
||||
;
|
||||
# input: list of OCI "descriptor" objects (the "manifests" array of an image index; see link above)
|
||||
# output: the same list, sorted appropriately by platform with attestation manifests next to their subject
|
||||
def sort_manifests:
|
||||
sort_by(.platform | sort_split_platform)
|
||||
| sort_attestations
|
||||
;
|
||||
|
|
@ -2,11 +2,19 @@ package om_test
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/docker-library/meta-scripts/om"
|
||||
)
|
||||
|
||||
func BenchmarkSet(b *testing.B) {
|
||||
var m om.OrderedMap[int]
|
||||
for i := 0; i < b.N; i++ {
|
||||
m.Set(strconv.Itoa(i), i)
|
||||
}
|
||||
}
|
||||
|
||||
func assert[V comparable](t *testing.T, v V, expected V) {
|
||||
t.Helper()
|
||||
if v != expected {
|
||||
|
|
|
|||
|
|
@ -203,4 +203,76 @@ func (rc *registryCache) ResolveTag(ctx context.Context, repo string, tag string
|
|||
return desc, nil
|
||||
}
|
||||
|
||||
// TODO more methods (currently only implements what's actually necessary for SynthesizeIndex)
|
||||
func (rc *registryCache) PushManifest(ctx context.Context, repo string, tag string, contents []byte, mediaType string) (ociregistry.Descriptor, error) {
|
||||
// TODO this does *not* need to lock the entire cache during the upstream push (but it *would* be good to block pushing to this specific tag)
|
||||
rc.mu.Lock()
|
||||
defer rc.mu.Unlock()
|
||||
|
||||
desc, err := rc.registry.PushManifest(ctx, repo, tag, contents, mediaType)
|
||||
if err != nil {
|
||||
return ociregistry.Descriptor{}, err
|
||||
}
|
||||
|
||||
rc.has[cacheKeyDigest(repo, desc.Digest)] = true
|
||||
if tag != "" {
|
||||
rc.tags[cacheKeyTag(repo, tag)] = desc.Digest
|
||||
}
|
||||
if desc.Size <= manifestSizeLimit {
|
||||
desc.Data = contents
|
||||
}
|
||||
rc.data[desc.Digest] = desc
|
||||
|
||||
return desc, nil
|
||||
}
|
||||
|
||||
func (rc *registryCache) PushBlob(ctx context.Context, repo string, desc ociregistry.Descriptor, r io.Reader) (ociregistry.Descriptor, error) {
|
||||
// TODO this does *not* need to lock the entire cache during the upstream push (but it *would* be good to block pushing to this specific digest)
|
||||
rc.mu.Lock()
|
||||
defer rc.mu.Unlock()
|
||||
|
||||
// TODO if desc.Size <= manifestSizeLimit, we should technically wrap up the Reader we're given and cache the result so we can shove it directly into the cache, but we currently don't read back blobs we pushed in (and I don't think that's a common use case), so I'm taking the simpler answer of just using this event as a cache bust intead
|
||||
|
||||
desc, err := rc.registry.PushBlob(ctx, repo, desc, r)
|
||||
if err != nil {
|
||||
return ociregistry.Descriptor{}, err
|
||||
}
|
||||
|
||||
rc.has[cacheKeyDigest(repo, desc.Digest)] = true
|
||||
|
||||
// carefully copy only some fields such that any other existing fields are kept (if we resolve the TODO above about desc.Data, this matters a lot less and we should just assign directly 👀)
|
||||
if d, ok := rc.data[desc.Digest]; ok {
|
||||
d.MediaType = desc.MediaType
|
||||
d.Digest = desc.Digest
|
||||
d.Size = desc.Size
|
||||
desc = d
|
||||
}
|
||||
rc.data[desc.Digest] = desc
|
||||
|
||||
return desc, nil
|
||||
}
|
||||
|
||||
func (rc *registryCache) MountBlob(ctx context.Context, fromRepo, toRepo string, digest ociregistry.Digest) (ociregistry.Descriptor, error) {
|
||||
// TODO this does *not* need to lock the entire cache during the upstream push (but it *would* be good to block pushing to this specific digest)
|
||||
rc.mu.Lock()
|
||||
defer rc.mu.Unlock()
|
||||
|
||||
desc, err := rc.registry.MountBlob(ctx, fromRepo, toRepo, digest)
|
||||
if err != nil {
|
||||
return ociregistry.Descriptor{}, err
|
||||
}
|
||||
|
||||
rc.has[cacheKeyDigest(toRepo, desc.Digest)] = true // TODO technically we should also be able to safely imply that "fromRepo" has digest here too, but need to double check whether the contract of the MountBlob API in OCI is such that it's legal for it to return success if "toRepo" already has "digest" (even if "fromRepo" doesn't)
|
||||
|
||||
// carefully copy only some fields such that any other existing fields are kept (esp. desc.Data)
|
||||
if d, ok := rc.data[digest]; ok {
|
||||
d.MediaType = desc.MediaType
|
||||
d.Digest = desc.Digest
|
||||
d.Size = desc.Size
|
||||
desc = d
|
||||
}
|
||||
rc.data[digest] = desc
|
||||
|
||||
return desc, nil
|
||||
}
|
||||
|
||||
// TODO more methods (currently only implements what's actually necessary for SynthesizeIndex and {Ensure,Copy}{Manifest,Blob})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"cuelabs.dev/go/oci/ociregistry"
|
||||
"cuelabs.dev/go/oci/ociregistry/ocimem"
|
||||
|
|
@ -24,6 +23,9 @@ type LookupOptions struct {
|
|||
|
||||
// whether or not to do a HEAD instead of a GET (will still return an [ociregistry.BlobReader], but with an empty body / zero bytes)
|
||||
Head bool
|
||||
|
||||
// TODO allow providing a Descriptor here for more validation and/or for automatic usage of any usable/valid Data field?
|
||||
// TODO (also, if the provided Reference includes a Digest, we should probably validate it? are there cases where we don't want to / shouldn't?)
|
||||
}
|
||||
|
||||
// a wrapper around [ociregistry.Interface.GetManifest] (and `GetTag`, `GetBlob`, and the `Resolve*` versions of the above) that accepts a [Reference] and always returns a [ociregistry.BlobReader] (in the case of a HEAD request, it will be a zero-length reader with just a valid descriptor)
|
||||
|
|
@ -82,10 +84,10 @@ func Lookup(ctx context.Context, ref Reference, opts *LookupOptions) (ociregistr
|
|||
// obvious 404 cases
|
||||
return nil, nil
|
||||
}
|
||||
// https://github.com/cue-labs/oci/issues/26
|
||||
if errStr := strings.TrimPrefix(err.Error(), "error response: "); strings.HasPrefix(errStr, "404 ") ||
|
||||
// 401 often means "repository not found" (due to the nature of public/private mixing on Hub and the fact that ociauth definitely handled any possible authentication for us, so if we're still getting 401 it's unavoidable and might as well be 404)
|
||||
strings.HasPrefix(errStr, "401 ") {
|
||||
var httpErr ociregistry.HTTPError
|
||||
if errors.As(err, &httpErr) && (httpErr.StatusCode() == 404 ||
|
||||
// 401 often means "repository not found" (due to the nature of public/private mixing on Hub and the fact that ociauth definitely handled any possible authentication for us, so if we're still getting 401 it's unavoidable and might as well be 404, and 403 because getting 401 is actually a server bug that ociclient/ociauth works around for us in https://github.com/cue-labs/oci/commit/7eb5fc60a0e025038cd64d7f5df0a461136d5e9b)
|
||||
httpErr.StatusCode() == 401 || httpErr.StatusCode() == 403) {
|
||||
return nil, nil
|
||||
}
|
||||
return r, err
|
||||
|
|
|
|||
|
|
@ -0,0 +1,274 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
|
||||
"cuelabs.dev/go/oci/ociregistry"
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
var (
|
||||
// if a manifest or blob is more than this many bytes, we'll do a pre-flight HEAD request to verify whether we need to even bother pushing it before we do so (65535 is the theoretical maximum size of a single TCP packet, although MTU means it's usually closer to 1448 bytes, but this seemed like a sane place to draw a line to where a second request that might fail is worth our time)
|
||||
BlobSizeWorthHEAD = int64(65535)
|
||||
)
|
||||
|
||||
// this makes sure the given manifest (index or image) is available at the provided name (tag or digest), including copying any children (manifests or config+layers) if necessary and able (via the provided child lookup map)
|
||||
func EnsureManifest(ctx context.Context, ref Reference, manifest json.RawMessage, mediaType string, childRefs map[ociregistry.Digest]Reference) (ociregistry.Descriptor, error) {
|
||||
desc := ociregistry.Descriptor{
|
||||
MediaType: mediaType,
|
||||
Digest: godigest.FromBytes(manifest),
|
||||
Size: int64(len(manifest)),
|
||||
}
|
||||
if ref.Digest != "" {
|
||||
if ref.Digest != desc.Digest {
|
||||
return desc, fmt.Errorf("%s: digest mismatch: %s", ref, desc.Digest)
|
||||
}
|
||||
} else if ref.Tag == "" {
|
||||
ref.Digest = desc.Digest
|
||||
}
|
||||
|
||||
if _, ok := childRefs[""]; !ok {
|
||||
// empty digest is a "fallback" ref for where missing children might be found (if we don't have one, inject one)
|
||||
childRefs[""] = ref
|
||||
}
|
||||
|
||||
client, err := Client(ref.Host, nil)
|
||||
if err != nil {
|
||||
return desc, fmt.Errorf("%s: failed getting client: %w", ref, err)
|
||||
}
|
||||
|
||||
if desc.Size > BlobSizeWorthHEAD {
|
||||
r, err := Lookup(ctx, ref, &LookupOptions{Head: true})
|
||||
if err != nil {
|
||||
return desc, fmt.Errorf("%s: failed HEAD: %w", ref, err)
|
||||
}
|
||||
// TODO if we had some kind of progress interface, this would be a great place for some kind of debug log of head's contents
|
||||
if r != nil {
|
||||
head := r.Descriptor()
|
||||
r.Close()
|
||||
if head.Digest == desc.Digest && head.Size == desc.Size {
|
||||
return head, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// since we need to potentially retry this call after copying/mounting children, let's wrap it up for ease of use
|
||||
pushManifest := func() (ociregistry.Descriptor, error) {
|
||||
return client.PushManifest(ctx, ref.Repository, ref.Tag, manifest, mediaType)
|
||||
}
|
||||
rDesc, err := pushManifest()
|
||||
if err != nil {
|
||||
var httpErr ociregistry.HTTPError
|
||||
if errors.Is(err, ociregistry.ErrManifestBlobUnknown) ||
|
||||
errors.Is(err, ociregistry.ErrBlobUnknown) ||
|
||||
(errors.As(err, &httpErr) && httpErr.StatusCode() >= 400 && httpErr.StatusCode() <= 499) {
|
||||
// this probably means we need to push some child manifests and/or mount missing blobs (and then retry the manifest push)
|
||||
var manifestChildren struct {
|
||||
// *technically* this should be two separate structs chosen based on mediaType (https://github.com/opencontainers/distribution-spec/security/advisories/GHSA-mc8v-mgrf-8f4m), but that makes the code a lot more annoying when we're just collecting a list of potential children we need to copy over for the parent object to push successfully
|
||||
|
||||
// intentional subset of https://github.com/opencontainers/image-spec/blob/v1.1.0/specs-go/v1/index.go#L21 to minimize parsing
|
||||
Manifests []ocispec.Descriptor `json:"manifests"`
|
||||
|
||||
// intentional subset of https://github.com/opencontainers/image-spec/blob/v1.1.0/specs-go/v1/manifest.go#L20 to minimize parsing
|
||||
Config *ocispec.Descriptor `json:"config"` // have to turn this into a pointer so we can recognize when it's not set easier / more correctly
|
||||
Layers []ocispec.Descriptor `json:"layers"`
|
||||
}
|
||||
if err := json.Unmarshal(manifest, &manifestChildren); err != nil {
|
||||
return desc, fmt.Errorf("%s: failed parsing manifest JSON: %w", ref, err)
|
||||
}
|
||||
|
||||
childToRefs := func(child ocispec.Descriptor) (Reference, Reference) {
|
||||
childTargetRef := Reference{
|
||||
Host: ref.Host,
|
||||
Repository: ref.Repository,
|
||||
Digest: child.Digest,
|
||||
}
|
||||
childRef, ok := childRefs[child.Digest]
|
||||
if !ok {
|
||||
childRef = childRefs[""]
|
||||
}
|
||||
childRef.Tag = ""
|
||||
childRef.Digest = child.Digest
|
||||
return childRef, childTargetRef
|
||||
}
|
||||
|
||||
for _, child := range manifestChildren.Manifests {
|
||||
childRef, childTargetRef := childToRefs(child)
|
||||
r, err := Lookup(ctx, childRef, nil)
|
||||
if err != nil {
|
||||
return desc, fmt.Errorf("%s: manifest lookup failed: %w", childRef, err)
|
||||
}
|
||||
if r == nil {
|
||||
return desc, fmt.Errorf("%s: manifest not found", childRef)
|
||||
}
|
||||
//defer r.Close()
|
||||
// TODO validate r.Descriptor ?
|
||||
// TODO use readHelperRaw here (maybe a new "readHelperAll" wrapper too?)
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
r.Close()
|
||||
return desc, fmt.Errorf("%s: ReadAll of GetManifest failed: %w", childRef, err)
|
||||
}
|
||||
if err := r.Close(); err != nil {
|
||||
return desc, fmt.Errorf("%s: Close of GetManifest failed: %w", childRef, err)
|
||||
}
|
||||
grandchildRefs := maps.Clone(childRefs)
|
||||
grandchildRefs[""] = childRef // make the child's ref explicitly the "fallback" ref for any of its children
|
||||
if _, err := EnsureManifest(ctx, childTargetRef, b, child.MediaType, grandchildRefs); err != nil {
|
||||
return desc, fmt.Errorf("%s: EnsureManifest failed: %w", ref, err)
|
||||
}
|
||||
// TODO validate descriptor from EnsureManifest? (at the very least, Digest and Size)
|
||||
}
|
||||
|
||||
var childBlobs []ocispec.Descriptor
|
||||
if manifestChildren.Config != nil {
|
||||
childBlobs = append(childBlobs, *manifestChildren.Config)
|
||||
}
|
||||
childBlobs = append(childBlobs, manifestChildren.Layers...)
|
||||
for _, child := range childBlobs {
|
||||
childRef, childTargetRef := childToRefs(child)
|
||||
// TODO if blob sets URLs, don't bother (foreign layer) -- maybe check for those MediaTypes explicitly? (not a high priority as they're no longer used and officially discouraged/deprecated; would only matter if Tianon wants to use this for "hell/win" too 👀)
|
||||
if _, err := CopyBlob(ctx, childRef, childTargetRef); err != nil {
|
||||
return desc, fmt.Errorf("%s: CopyBlob(%s) failed: %w", childTargetRef, childRef, err)
|
||||
}
|
||||
// TODO validate CopyBlob returned descriptor? (at the very least, Digest and Size)
|
||||
}
|
||||
|
||||
rDesc, err = pushManifest()
|
||||
if err != nil {
|
||||
return desc, fmt.Errorf("%s: PushManifest failed: %w", ref, err)
|
||||
}
|
||||
} else {
|
||||
return desc, fmt.Errorf("%s: error pushing (does not appear to be missing manifest/blob related): %w", ref, err)
|
||||
}
|
||||
}
|
||||
// TODO validate MediaType and Size too? 🤷
|
||||
if rDesc.Digest != desc.Digest {
|
||||
return desc, fmt.Errorf("%s: pushed digest from registry (%s) does not match expected digest (%s)", ref, rDesc.Digest, desc.Digest)
|
||||
}
|
||||
return desc, nil
|
||||
}
|
||||
|
||||
// this copies a manifest (index or image) and all child objects (manifests or config+layers) from one name to another
|
||||
func CopyManifest(ctx context.Context, srcRef, dstRef Reference, childRefs map[ociregistry.Digest]Reference) (ociregistry.Descriptor, error) {
|
||||
var desc ociregistry.Descriptor
|
||||
|
||||
// wouldn't it be nice if MountBlob for manifests was a thing? 🥺
|
||||
r, err := Lookup(ctx, srcRef, nil)
|
||||
if err != nil {
|
||||
return desc, fmt.Errorf("%s: lookup failed: %w", srcRef, err)
|
||||
}
|
||||
if r == nil {
|
||||
return desc, fmt.Errorf("%s: manifest not found", srcRef)
|
||||
}
|
||||
defer r.Close()
|
||||
desc = r.Descriptor()
|
||||
|
||||
manifest, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return desc, fmt.Errorf("%s: reading manifest failed: %w", srcRef, err)
|
||||
}
|
||||
|
||||
if _, ok := childRefs[""]; !ok {
|
||||
// if we don't have a fallback, set it to src
|
||||
childRefs[""] = srcRef
|
||||
}
|
||||
|
||||
return EnsureManifest(ctx, dstRef, manifest, desc.MediaType, childRefs)
|
||||
}
|
||||
|
||||
// this takes an [io.Reader] of content and makes sure it is available as a blob in the given repository+digest (if larger than [BlobSizeWorthHEAD], this might return without consuming any of the provided [io.Reader])
|
||||
func EnsureBlob(ctx context.Context, ref Reference, size int64, content io.Reader) (ociregistry.Descriptor, error) {
|
||||
desc := ociregistry.Descriptor{
|
||||
Digest: ref.Digest,
|
||||
Size: size,
|
||||
}
|
||||
|
||||
if ref.Digest == "" {
|
||||
return desc, fmt.Errorf("%s: blobs must be pushed by digest", ref)
|
||||
}
|
||||
if ref.Tag != "" {
|
||||
return desc, fmt.Errorf("%s: blobs cannot have tags", ref)
|
||||
}
|
||||
|
||||
if desc.Size > BlobSizeWorthHEAD {
|
||||
r, err := Lookup(ctx, ref, &LookupOptions{Type: LookupTypeBlob, Head: true})
|
||||
if err != nil {
|
||||
return desc, fmt.Errorf("%s: failed HEAD: %w", ref, err)
|
||||
}
|
||||
// TODO if we had some kind of progress interface, this would be a great place for some kind of debug log of head's contents
|
||||
if r != nil {
|
||||
head := r.Descriptor()
|
||||
r.Close()
|
||||
if head.Digest == desc.Digest && head.Size == desc.Size {
|
||||
return head, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client, err := Client(ref.Host, nil)
|
||||
if err != nil {
|
||||
return desc, fmt.Errorf("%s: error getting Client: %w", ref, err)
|
||||
}
|
||||
|
||||
return client.PushBlob(ctx, ref.Repository, desc, content)
|
||||
}
|
||||
|
||||
// this copies a blob from one repository to another
|
||||
func CopyBlob(ctx context.Context, srcRef, dstRef Reference) (ociregistry.Descriptor, error) {
|
||||
var desc ociregistry.Descriptor
|
||||
|
||||
if srcRef.Digest == "" {
|
||||
return desc, fmt.Errorf("%s: missing digest (cannot copy blob without digest)", srcRef)
|
||||
} else if !(dstRef.Digest == "" || dstRef.Digest == srcRef.Digest) {
|
||||
return desc, fmt.Errorf("%s: digest mismatch in copy: %s", dstRef, srcRef)
|
||||
} else {
|
||||
dstRef.Digest = srcRef.Digest
|
||||
}
|
||||
if srcRef.Tag != "" {
|
||||
return desc, fmt.Errorf("%s: blobs cannot have tags", srcRef)
|
||||
} else if dstRef.Tag != "" {
|
||||
return desc, fmt.Errorf("%s: blobs cannot have tags", dstRef)
|
||||
}
|
||||
|
||||
if srcRef.Host == dstRef.Host {
|
||||
client, err := Client(srcRef.Host, nil)
|
||||
if err != nil {
|
||||
return desc, fmt.Errorf("%s: error getting Client: %w", srcRef, err)
|
||||
}
|
||||
return client.MountBlob(ctx, srcRef.Repository, dstRef.Repository, srcRef.Digest)
|
||||
}
|
||||
|
||||
// TODO Push/Reader progress / progresswriter concerns again 😭
|
||||
|
||||
r, err := Lookup(ctx, srcRef, &LookupOptions{Type: LookupTypeBlob})
|
||||
if err != nil {
|
||||
return desc, fmt.Errorf("%s: blob lookup failed: %w", srcRef, err)
|
||||
}
|
||||
if r == nil {
|
||||
return desc, fmt.Errorf("%s: blob not found", srcRef)
|
||||
}
|
||||
defer r.Close()
|
||||
desc = r.Descriptor()
|
||||
|
||||
if dstRef.Digest != desc.Digest {
|
||||
return desc, fmt.Errorf("%s: registry digest mismatch: %s (%s)", dstRef, desc.Digest, srcRef)
|
||||
}
|
||||
|
||||
if _, err := EnsureBlob(ctx, dstRef, desc.Size, r); err != nil {
|
||||
return desc, fmt.Errorf("%s: EnsureBlob(%s) failed: %w", dstRef, srcRef, err)
|
||||
}
|
||||
// TODO validate returned descriptor? (at least digest/size)
|
||||
|
||||
if err := r.Close(); err != nil {
|
||||
return desc, fmt.Errorf("%s: Close of GetBlob(%s) failed: %w", dstRef, srcRef, err)
|
||||
}
|
||||
|
||||
return desc, nil
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import (
|
|||
_ "crypto/sha256"
|
||||
_ "crypto/sha512"
|
||||
|
||||
"cuelabs.dev/go/oci/ociregistry"
|
||||
"cuelabs.dev/go/oci/ociregistry/ociref"
|
||||
)
|
||||
|
||||
|
|
@ -50,6 +51,14 @@ func (ref Reference) String() string {
|
|||
return ociref.Reference(ref).String()
|
||||
}
|
||||
|
||||
// like [Reference.String], but also stripping a known digest if this object's value matches
|
||||
func (ref Reference) StringWithKnownDigest(commonDigest ociregistry.Digest) string {
|
||||
if ref.Digest == commonDigest {
|
||||
ref.Digest = ""
|
||||
}
|
||||
return ref.String()
|
||||
}
|
||||
|
||||
// implements [encoding.TextMarshaler] (especially for [Reference]-in-JSON)
|
||||
func (ref Reference) MarshalText() ([]byte, error) {
|
||||
return []byte(ref.String()), nil
|
||||
|
|
|
|||
|
|
@ -85,3 +85,19 @@ func TestParseRef(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefStringWithKnownDigest(t *testing.T) {
|
||||
ref, err := registry.ParseRef("hello-world:latest@sha256:53641cd209a4fecfc68e21a99871ce8c6920b2e7502df0a20671c6fccc73a7c6")
|
||||
if err != nil {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
str := ref.String()
|
||||
|
||||
if got := ref.StringWithKnownDigest("sha256:0000000000000000000000000000000000000000000000000000000000000000"); got != str {
|
||||
t.Fatalf("expected %q, got %q", str, got)
|
||||
}
|
||||
|
||||
if got := ref.StringWithKnownDigest("sha256:53641cd209a4fecfc68e21a99871ce8c6920b2e7502df0a20671c6fccc73a7c6"); got != "hello-world:latest" {
|
||||
t.Fatalf("expected %q, got %q", "hello-world:latest", got)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
# input: string
|
||||
# output: something suitable for use in "sort_by" for sorting in "natural sort" order
|
||||
def sort_split_natural:
|
||||
# https://en.wikipedia.org/wiki/Natural_sort_order
|
||||
# similar to https://github.com/tianon/debian-bin/blob/448b5784ac63e6341d5e5762004e3d9e64331cf2/jq/dpkg-version.jq#L3 but a much smaller/simpler problem set (numbers vs non-numbers)
|
||||
[
|
||||
scan("[0-9]+|[^0-9]+|^$")
|
||||
| tonumber? // .
|
||||
]
|
||||
;
|
||||
|
||||
# input: ~anything
|
||||
# output: something suitable for use in "sort_by" for sorting in descending order (for numbers, they become negative, etc)
|
||||
def sort_split_desc:
|
||||
walk(
|
||||
if type == "number" then
|
||||
-.
|
||||
elif type == "string" then
|
||||
# https://stackoverflow.com/a/74058663/433558
|
||||
[ -explode[], 0 ] # the "0" here helps us with the empty string case; [ "a", "b", "c", "" ]
|
||||
elif type == "array" then
|
||||
. # TODO sorting an array of arrays where one is empty goes wonky here (for similar reasons to the empty string sorting); [ [1],[2],[3],[0],[] ]
|
||||
else
|
||||
error("cannot reverse sort type '\(type)': \(.)")
|
||||
end
|
||||
)
|
||||
;
|
||||
|
||||
# input: key to sort
|
||||
# output: something suitable for use in "sort_by" for sorting things based on explicit preferences
|
||||
# top: ordered list of sort preference
|
||||
# bottom: ordered list of *end* sort preference (ie, what to put at the end, in order)
|
||||
# [ 1, 2, 3, 4, 5 ] | sort_by(sort_split_pref([ 6, 5, 3 ]; [ 4, 2 ])) => [ 5, 3, 1, 4, 2 ]
|
||||
def sort_split_pref($top; $bottom):
|
||||
. as $o
|
||||
| [
|
||||
(
|
||||
$top
|
||||
| index($o) # items in $top get just their index in $top
|
||||
// (
|
||||
length
|
||||
+ (
|
||||
$bottom
|
||||
| index($o) # items in $bottom get ($top | length) + 1 + index in $bottom
|
||||
// -1 # items in neither get ($top | length)
|
||||
| . + 1
|
||||
)
|
||||
)
|
||||
),
|
||||
$o
|
||||
]
|
||||
;
|
||||
# a one-argument version of sort_split_pref for the more common usage
|
||||
def sort_split_pref(top):
|
||||
sort_split_pref(top; [])
|
||||
;
|
||||
Loading…
Reference in New Issue