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:
Tianon Gravi 2024-04-18 11:53:23 -07:00 committed by GitHub
parent 530a1082db
commit ab38f954b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 5362 additions and 85 deletions

View File

@ -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 '{}' +

3
.test/.coverage/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
**
!.gitignore
!builds.sh

1
.test/.coverage/builds.sh Symbolic link
View File

@ -0,0 +1 @@
../../builds.sh

1
.test/deploy-all/in.json Symbolic link
View File

@ -0,0 +1 @@
../builds.json

2270
.test/deploy-all/out.json Normal file

File diff suppressed because it is too large Load Diff

6
.test/deploy-all/test.jq Normal file
View File

@ -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

1
.test/deploy-amd64/in.json Symbolic link
View File

@ -0,0 +1 @@
../builds.json

238
.test/deploy-amd64/out.json Normal file
View File

@ -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"
}
}
]
}
}
]

View File

@ -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

31
.test/jq.sh Executable file
View File

@ -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

1
.test/meta-commands/in.json Symbolic link
View File

@ -0,0 +1 @@
../builds.json

View File

@ -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")

View File

@ -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
}
]

View File

@ -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"
}
}
]

View File

@ -0,0 +1,4 @@
include "oci";
map(normalize_descriptor)
| sort_manifests

View File

@ -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"
}
]

View File

@ -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)

View File

@ -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"

View File

@ -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
'''
}
}
}

View File

@ -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'"

246
cmd/deploy/input.go Normal file
View File

@ -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
}

186
cmd/deploy/input_test.go Normal file
View File

@ -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)
}
})
}
}

73
cmd/deploy/main.go Normal file
View File

@ -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()
}
}

71
deploy.jq Normal file
View File

@ -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
View File

@ -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
View File

@ -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=

89
oci.jq Normal file
View File

@ -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
;

View File

@ -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 {

View File

@ -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})

View File

@ -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

274
registry/push.go Normal file
View File

@ -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
}

View File

@ -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

View File

@ -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)
}
}

56
sort.jq Normal file
View File

@ -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; [])
;