deploy: compare portable image config digests
This commit is contained in:
parent
be3b5f9cee
commit
440eee42e1
@ -60,14 +60,12 @@ DOCKER_BUILDKIT=1 docker build \
|
||||
-t "youyidian-3d-migrator:$TARGET_SHA" .
|
||||
docker image inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' \
|
||||
"youyidian-3d-migrator:$TARGET_SHA"
|
||||
MIGRATOR_IMAGE_ID="$(docker image inspect --format '{{.Id}}' "youyidian-3d-migrator:$TARGET_SHA")"
|
||||
MIGRATOR_BAKED_SHA="$(docker run --rm --network none --read-only \
|
||||
--entrypoint cat "youyidian-3d-migrator:$TARGET_SHA" /app/DEPLOYMENT_COMMIT)"
|
||||
test "$MIGRATOR_BAKED_SHA" = "$TARGET_SHA"
|
||||
printf '%s\n' "$MIGRATOR_IMAGE_ID"
|
||||
```
|
||||
|
||||
revision label 和 baked SHA 必须精确等于目标 SHA。`MIGRATOR_IMAGE_ID` 是独立于 runner 的值,二者不得混用。
|
||||
revision label 和 baked SHA 必须精确等于目标 SHA。构建机和生产机可能运行不同版本的 Docker,`docker image inspect .Id` 对带 provenance 的镜像可能分别显示 manifest-list digest 与 config digest,因此不能直接把构建机 `.Id` 当成跨主机身份凭证。下一步必须从 `docker save` 的可信归档中提取该镜像唯一的 config digest,再与生产装载后的 `.Id` 比较。
|
||||
|
||||
### 1.3 通过已固定主机指纹的 SSH 传输 migrator
|
||||
|
||||
@ -81,6 +79,17 @@ trap 'rm -f -- "$MIGRATOR_ARCHIVE"' EXIT
|
||||
docker save --output "$MIGRATOR_ARCHIVE" "$MIGRATOR_IMAGE"
|
||||
MIGRATOR_ARCHIVE_SHA256="$(sha256sum "$MIGRATOR_ARCHIVE" | awk '{print $1}')"
|
||||
test -n "$MIGRATOR_ARCHIVE_SHA256"
|
||||
MIGRATOR_IMAGE_ID="$(tar -xOf "$MIGRATOR_ARCHIVE" manifest.json | node -e '
|
||||
const fs = require("node:fs")
|
||||
const image = process.argv[1]
|
||||
const entries = JSON.parse(fs.readFileSync(0, "utf8"))
|
||||
const matches = entries.filter((entry) => Array.isArray(entry.RepoTags) && entry.RepoTags.includes(image))
|
||||
if (matches.length !== 1 || typeof matches[0].Config !== "string") process.exit(2)
|
||||
const configMatch = /^blobs\/sha256\/([0-9a-f]{64})$/.exec(matches[0].Config)
|
||||
if (!configMatch) process.exit(3)
|
||||
process.stdout.write(`sha256:${configMatch[1]}`)
|
||||
' "$MIGRATOR_IMAGE")"
|
||||
test -n "$MIGRATOR_IMAGE_ID"
|
||||
|
||||
SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=yes \
|
||||
-o UserKnownHostsFile=/root/.ssh/gongxiangp2-production-known_hosts)
|
||||
|
||||
@ -439,19 +439,24 @@ describe('production video transcode drain gate', () => {
|
||||
expect(script).not.toContain('git remote set-url origin "$REPO_URL"')
|
||||
})
|
||||
|
||||
test('生产按不可变 manifest digest 拉取并二次核对构建机 image ID', () => {
|
||||
const captureId = script.indexOf('EXPECTED_IMAGE_ID="$(docker image inspect')
|
||||
const captureDigest = script.indexOf('EXPECTED_MANIFEST_DIGEST="$(curl -fsSI', captureId)
|
||||
const immutableReference = script.indexOf('REGISTRY_COMMIT="${BUILD_SERVER_IP}:${REGISTRY_PORT}/${IMAGE_NAME}@${EXPECTED_MANIFEST_DIGEST}"', captureDigest)
|
||||
test('生产按不可变 manifest 拉取并用平台 config digest 跨 Docker 版本核对镜像', () => {
|
||||
const captureDigest = script.indexOf('EXPECTED_MANIFEST_DIGEST="$(curl -fsSI')
|
||||
const capturePlatform = script.indexOf('EXPECTED_PLATFORM_MANIFEST_DIGEST="$(printf', captureDigest)
|
||||
const captureId = script.indexOf('EXPECTED_IMAGE_ID="$(printf', capturePlatform)
|
||||
const immutableReference = script.indexOf('REGISTRY_COMMIT="${BUILD_SERVER_IP}:${REGISTRY_PORT}/${IMAGE_NAME}@${EXPECTED_MANIFEST_DIGEST}"', captureId)
|
||||
const remoteCompare = script.indexOf('ACTUAL_PROD_IMAGE_ID="$(prod_ssh', immutableReference)
|
||||
const payloadCompare = script.indexOf('ACTUAL_IMAGE_ID="$(docker image inspect', remoteCompare)
|
||||
const createCandidate = script.indexOf('create_container "$NEXT_CONTAINER" "$REGISTRY_COMMIT"', payloadCompare)
|
||||
|
||||
expect(captureId).toBeGreaterThan(0)
|
||||
expect(captureDigest).toBeGreaterThan(captureId)
|
||||
expect(captureDigest).toBeGreaterThan(0)
|
||||
expect(capturePlatform).toBeGreaterThan(captureDigest)
|
||||
expect(captureId).toBeGreaterThan(capturePlatform)
|
||||
expect(script).toContain('application/vnd.oci.image.index.v1+json')
|
||||
expect(script).toContain('tolower($1) == "docker-content-digest:" { print $2 }')
|
||||
expect(script).not.toContain("awk -F ': *' 'tolower($1) == \"docker-content-digest\"")
|
||||
expect(script).toContain('entry.platform.os === "linux"')
|
||||
expect(script).toContain('entry.platform.architecture === "amd64"')
|
||||
expect(script).toContain('document.config && document.config.digest')
|
||||
expect(immutableReference).toBeGreaterThan(captureDigest)
|
||||
expect(remoteCompare).toBeGreaterThan(immutableReference)
|
||||
expect(payloadCompare).toBeGreaterThan(remoteCompare)
|
||||
|
||||
@ -912,8 +912,6 @@ if [ "$SKIP_PUSH" != "1" ]; then
|
||||
start_remote_step "Push Image"
|
||||
docker push "$LOCAL_REGISTRY_COMMIT"
|
||||
docker push "$LOCAL_REGISTRY_MOVING"
|
||||
EXPECTED_IMAGE_ID="$(docker image inspect --format '{{.Id}}' "$REGISTRY_TAG" 2>/dev/null || true)"
|
||||
[[ "$EXPECTED_IMAGE_ID" =~ ^sha256:[0-9a-f]{64}$ ]] || fail "Built image ID is not one exact sha256 digest"
|
||||
EXPECTED_MANIFEST_DIGEST="$(curl -fsSI \
|
||||
-H 'Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' \
|
||||
"http://localhost:${REGISTRY_PORT}/v2/${IMAGE_NAME}/manifests/${COMMIT_TAG}" \
|
||||
@ -921,6 +919,37 @@ if [ "$SKIP_PUSH" != "1" ]; then
|
||||
| awk 'tolower($1) == "docker-content-digest:" { print $2 }' \
|
||||
| tail -n 1)"
|
||||
[[ "$EXPECTED_MANIFEST_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]] || fail "Registry did not return one immutable manifest digest for the exact commit tag"
|
||||
REGISTRY_MANIFEST_JSON="$(curl -fsS \
|
||||
-H 'Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' \
|
||||
"http://localhost:${REGISTRY_PORT}/v2/${IMAGE_NAME}/manifests/${COMMIT_TAG}")"
|
||||
EXPECTED_PLATFORM_MANIFEST_DIGEST="$(printf '%s' "$REGISTRY_MANIFEST_JSON" | node -e '
|
||||
const document = JSON.parse(require("node:fs").readFileSync(0, "utf8"))
|
||||
if (!Array.isArray(document.manifests)) {
|
||||
if (!document.config || typeof document.config.digest !== "string") process.exit(2)
|
||||
process.stdout.write("self")
|
||||
process.exit(0)
|
||||
}
|
||||
const matches = document.manifests.filter((entry) =>
|
||||
entry && entry.platform && entry.platform.os === "linux" && entry.platform.architecture === "amd64"
|
||||
)
|
||||
if (matches.length !== 1 || typeof matches[0].digest !== "string") process.exit(3)
|
||||
process.stdout.write(matches[0].digest)
|
||||
')"
|
||||
if [ "$EXPECTED_PLATFORM_MANIFEST_DIGEST" = "self" ]; then
|
||||
REGISTRY_PLATFORM_MANIFEST_JSON="$REGISTRY_MANIFEST_JSON"
|
||||
else
|
||||
[[ "$EXPECTED_PLATFORM_MANIFEST_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]] || fail "Registry index did not contain exactly one linux/amd64 manifest"
|
||||
REGISTRY_PLATFORM_MANIFEST_JSON="$(curl -fsS \
|
||||
-H 'Accept: application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' \
|
||||
"http://localhost:${REGISTRY_PORT}/v2/${IMAGE_NAME}/manifests/${EXPECTED_PLATFORM_MANIFEST_DIGEST}")"
|
||||
fi
|
||||
EXPECTED_IMAGE_ID="$(printf '%s' "$REGISTRY_PLATFORM_MANIFEST_JSON" | node -e '
|
||||
const document = JSON.parse(require("node:fs").readFileSync(0, "utf8"))
|
||||
const digest = document && document.config && document.config.digest
|
||||
if (typeof digest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(digest)) process.exit(2)
|
||||
process.stdout.write(digest)
|
||||
')"
|
||||
[[ "$EXPECTED_IMAGE_ID" =~ ^sha256:[0-9a-f]{64}$ ]] || fail "Registry platform manifest did not contain one exact config digest"
|
||||
REGISTRY_COMMIT="${BUILD_SERVER_IP}:${REGISTRY_PORT}/${IMAGE_NAME}@${EXPECTED_MANIFEST_DIGEST}"
|
||||
ok "Pushed image tags: $COMMIT_TAG, $MOVING_TAG"
|
||||
ok "Pinned production image: $REGISTRY_COMMIT ($EXPECTED_IMAGE_ID)"
|
||||
|
||||
@ -440,6 +440,9 @@ test('container supply chain and migrator image are immutable while SOP keeps co
|
||||
assert.match(migrator, /command -v pg_restore >\/dev\/null/)
|
||||
assert.match(migrator, /command -v psql >\/dev\/null/)
|
||||
assert.match(migrator, /COPY --from=builder \/app\/scripts\/create-verified-production-backup\.mjs/)
|
||||
assert.match(sop, /tar -xOf "\$MIGRATOR_ARCHIVE" manifest\.json/)
|
||||
assert.match(sop, /entry\.RepoTags\.includes\(image\)/)
|
||||
assert.match(sop, /\^blobs\\\/sha256\\\/\(\[0-9a-f\]\{64\}\)\$/)
|
||||
const scriptsExcludeIndex = dockerIgnore.lastIndexOf('scripts/**')
|
||||
assert.ok(scriptsExcludeIndex >= 0)
|
||||
assert.ok(dockerIgnore.lastIndexOf('!scripts/safe-production-migrate.mjs') > scriptsExcludeIndex)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user