Add digest-aware mirrored image resolver across CI workflows
Some checks failed
CICD / Build and Publish CICD Base Image (push) Failing after 24s
CICD / Build and Push CICD Image (push) Has been skipped
CICD / Source Checks (push) Has been skipped
CICD / Frontend Dependency Audit (push) Has been skipped
CICD / Backend Dependency Audit (push) Has been skipped
CICD / Build CICD Image Failure Postmortem (push) Has been skipped
CICD / Build Backend Base Image (push) Has been skipped
CICD / Build Frontend Base Image (push) Has been skipped
CICD / Build Integration Tester Image (push) Has been skipped
CICD / Build E2E Tester Image (push) Has been skipped
CICD / CICD Tests Complete (push) Failing after 8s
CICD / Build Backend Main Image (push) Has been skipped
CICD / Build Frontend Main Image (push) Has been skipped
CICD / Runtime Black-Box Integration Tests (push) Has been cancelled
CICD / Integration Tests Failure Postmortem (push) Has been cancelled
CICD / End-to-End Tests (push) Has been cancelled
CICD / E2E Tests Failure Postmortem (push) Has been cancelled
CICD / Build and Publish CICD Base Image (pull_request) Failing after 52s
CICD / Production Image Failures Postmortem (push) Has been cancelled
CICD / Source Lanes Failure Postmortem (push) Has been cancelled
CICD / Production Images Complete (push) Has been cancelled
CICD / Build and Push CICD Image (pull_request) Has been skipped
CICD / Source Checks (pull_request) Has been skipped
CICD / Frontend Dependency Audit (pull_request) Has been skipped
CICD / Backend Dependency Audit (pull_request) Has been skipped
CICD / Build Backend Base Image (pull_request) Has been skipped
CICD / Build Frontend Base Image (pull_request) Has been skipped
CICD / Build CICD Image Failure Postmortem (pull_request) Has been skipped
CICD / Build Integration Tester Image (pull_request) Has been skipped
CICD / Build E2E Tester Image (pull_request) Has been skipped
CICD / Build Backend Main Image (pull_request) Has been skipped
CICD / Build Frontend Main Image (pull_request) Has been skipped
CICD / CICD Tests Complete (pull_request) Failing after 9s
CICD / Production Images Complete (pull_request) Failing after 11s
CICD / Production Image Failures Postmortem (pull_request) Has been skipped
CICD / End-to-End Tests (pull_request) Has been skipped
CICD / Runtime Black-Box Integration Tests (pull_request) Has been skipped
CICD / Integration Tests Failure Postmortem (pull_request) Has been skipped
CICD / E2E Tests Failure Postmortem (pull_request) Has been skipped
CICD / Source Lanes Failure Postmortem (pull_request) Has been skipped
Some checks failed
CICD / Build and Publish CICD Base Image (push) Failing after 24s
CICD / Build and Push CICD Image (push) Has been skipped
CICD / Source Checks (push) Has been skipped
CICD / Frontend Dependency Audit (push) Has been skipped
CICD / Backend Dependency Audit (push) Has been skipped
CICD / Build CICD Image Failure Postmortem (push) Has been skipped
CICD / Build Backend Base Image (push) Has been skipped
CICD / Build Frontend Base Image (push) Has been skipped
CICD / Build Integration Tester Image (push) Has been skipped
CICD / Build E2E Tester Image (push) Has been skipped
CICD / CICD Tests Complete (push) Failing after 8s
CICD / Build Backend Main Image (push) Has been skipped
CICD / Build Frontend Main Image (push) Has been skipped
CICD / Runtime Black-Box Integration Tests (push) Has been cancelled
CICD / Integration Tests Failure Postmortem (push) Has been cancelled
CICD / End-to-End Tests (push) Has been cancelled
CICD / E2E Tests Failure Postmortem (push) Has been cancelled
CICD / Build and Publish CICD Base Image (pull_request) Failing after 52s
CICD / Production Image Failures Postmortem (push) Has been cancelled
CICD / Source Lanes Failure Postmortem (push) Has been cancelled
CICD / Production Images Complete (push) Has been cancelled
CICD / Build and Push CICD Image (pull_request) Has been skipped
CICD / Source Checks (pull_request) Has been skipped
CICD / Frontend Dependency Audit (pull_request) Has been skipped
CICD / Backend Dependency Audit (pull_request) Has been skipped
CICD / Build Backend Base Image (pull_request) Has been skipped
CICD / Build Frontend Base Image (pull_request) Has been skipped
CICD / Build CICD Image Failure Postmortem (pull_request) Has been skipped
CICD / Build Integration Tester Image (pull_request) Has been skipped
CICD / Build E2E Tester Image (pull_request) Has been skipped
CICD / Build Backend Main Image (pull_request) Has been skipped
CICD / Build Frontend Main Image (pull_request) Has been skipped
CICD / CICD Tests Complete (pull_request) Failing after 9s
CICD / Production Images Complete (pull_request) Failing after 11s
CICD / Production Image Failures Postmortem (pull_request) Has been skipped
CICD / End-to-End Tests (pull_request) Has been skipped
CICD / Runtime Black-Box Integration Tests (pull_request) Has been skipped
CICD / Integration Tests Failure Postmortem (pull_request) Has been skipped
CICD / E2E Tests Failure Postmortem (pull_request) Has been skipped
CICD / Source Lanes Failure Postmortem (pull_request) Has been skipped
This commit is contained in:
@@ -132,12 +132,13 @@ jobs:
|
||||
|
||||
if retry_cmd 5 3 env GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" \
|
||||
git fetch --depth 1 origin "${HEAD_SHA}" >/dev/null 2>&1; then
|
||||
git checkout FETCH_HEAD -- Dockerfile.cicd-base .dockerignore scripts/compute-cicd-base-hash.sh
|
||||
git checkout FETCH_HEAD -- Dockerfile.cicd-base .dockerignore scripts/compute-cicd-base-hash.sh scripts/resolve-mirrored-image.sh
|
||||
else
|
||||
git checkout HEAD -- Dockerfile.cicd-base .dockerignore scripts/compute-cicd-base-hash.sh
|
||||
git checkout HEAD -- Dockerfile.cicd-base .dockerignore scripts/compute-cicd-base-hash.sh scripts/resolve-mirrored-image.sh
|
||||
fi
|
||||
|
||||
chmod +x scripts/compute-cicd-base-hash.sh
|
||||
chmod +x scripts/resolve-mirrored-image.sh
|
||||
|
||||
- name: Compute base hash and inspect registry
|
||||
id: base-state
|
||||
@@ -241,9 +242,19 @@ jobs:
|
||||
echo "${PACKAGE_ACCESS_TOKEN}" | docker login "http://${GITEA_REGISTRY}" -u "${REGISTRY_USER}" --password-stdin
|
||||
|
||||
PLAYWRIGHT_BROWSERS_MIRROR_TAG="${GITEA_REGISTRY}/darkhelm.org/playwright-browsers:v1.56.1-jammy"
|
||||
retry_registry_op pull "${PLAYWRIGHT_BROWSERS_MIRROR_TAG}" 5 3
|
||||
PLAYWRIGHT_BROWSERS_UPSTREAM_TAG="mcr.microsoft.com/playwright:v1.56.1-jammy"
|
||||
PLAYWRIGHT_BROWSERS_LOCAL_TAG="$(./scripts/resolve-mirrored-image.sh \
|
||||
--image "${PLAYWRIGHT_BROWSERS_MIRROR_TAG}" \
|
||||
--mirror-image "${PLAYWRIGHT_BROWSERS_MIRROR_TAG}" \
|
||||
--upstream-image "${PLAYWRIGHT_BROWSERS_UPSTREAM_TAG}" \
|
||||
--registry-user "${REGISTRY_USER}" \
|
||||
--registry-password-env PACKAGE_ACCESS_TOKEN)"
|
||||
|
||||
PLAYWRIGHT_BROWSERS_IMAGE=$(docker image inspect --format='{{index .RepoDigests 0}}' "${PLAYWRIGHT_BROWSERS_MIRROR_TAG}")
|
||||
PLAYWRIGHT_BROWSERS_IMAGE="$({ docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "${PLAYWRIGHT_BROWSERS_LOCAL_TAG}" | head -n 1; } || true)"
|
||||
if [ -z "${PLAYWRIGHT_BROWSERS_IMAGE}" ]; then
|
||||
echo "❌ Unable to resolve Playwright browsers digest reference"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker build --progress=plain -f Dockerfile.cicd-base \
|
||||
--build-arg BASE_IMAGE_VERSION="v1.0.0-${BASE_HASH}" \
|
||||
@@ -340,11 +351,12 @@ jobs:
|
||||
|
||||
if retry_cmd 5 3 env GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=no" \
|
||||
git fetch --depth 1 origin "${HEAD_SHA}" >/dev/null 2>&1; then
|
||||
git checkout FETCH_HEAD -- .dockerignore Dockerfile.cicd Dockerfile.cicd-base scripts/compute-cicd-base-hash.sh
|
||||
git checkout FETCH_HEAD -- .dockerignore Dockerfile.cicd Dockerfile.cicd-base scripts/compute-cicd-base-hash.sh scripts/resolve-mirrored-image.sh
|
||||
else
|
||||
git checkout HEAD -- .dockerignore Dockerfile.cicd Dockerfile.cicd-base scripts/compute-cicd-base-hash.sh
|
||||
git checkout HEAD -- .dockerignore Dockerfile.cicd Dockerfile.cicd-base scripts/compute-cicd-base-hash.sh scripts/resolve-mirrored-image.sh
|
||||
fi
|
||||
chmod +x scripts/compute-cicd-base-hash.sh
|
||||
chmod +x scripts/resolve-mirrored-image.sh
|
||||
|
||||
- name: Build and push complete CICD image
|
||||
env:
|
||||
@@ -1061,7 +1073,8 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
PLAYWRIGHT_BASE_IMAGE="${GITEA_REGISTRY}/darkhelm.org/playwright-browsers:v1.56.1-jammy"
|
||||
PLAYWRIGHT_BASE_IMAGE_MIRROR="${GITEA_REGISTRY}/darkhelm.org/playwright-browsers:v1.56.1-jammy"
|
||||
PLAYWRIGHT_BASE_IMAGE_UPSTREAM="mcr.microsoft.com/playwright:v1.56.1-jammy"
|
||||
|
||||
retry_base_pull() {
|
||||
image_ref="$1"
|
||||
@@ -1130,6 +1143,13 @@ jobs:
|
||||
E2E_TESTER_TAG_REF="${E2E_TESTER_REPO}:${HEAD_SHA}"
|
||||
E2E_TESTER_CACHE_REF="${E2E_TESTER_REPO}:cache"
|
||||
|
||||
PLAYWRIGHT_BASE_IMAGE="$(./scripts/resolve-mirrored-image.sh \
|
||||
--image "${PLAYWRIGHT_BASE_IMAGE_MIRROR}" \
|
||||
--mirror-image "${PLAYWRIGHT_BASE_IMAGE_MIRROR}" \
|
||||
--upstream-image "${PLAYWRIGHT_BASE_IMAGE_UPSTREAM}" \
|
||||
--registry-user "${REGISTRY_USER}" \
|
||||
--registry-password-env PACKAGE_ACCESS_TOKEN)"
|
||||
|
||||
retry_base_pull "${PLAYWRIGHT_BASE_IMAGE}" 6 5
|
||||
retry_registry_op pull "${E2E_TESTER_CACHE_REF}" 5 3 || true
|
||||
DOCKER_BUILDKIT=1 docker build --pull=false -f Dockerfile.e2e-tester \
|
||||
@@ -1574,6 +1594,9 @@ jobs:
|
||||
COMPOSE_PROJECT_NAME="plex-int-${RUN_ID}-${RUN_ATTEMPT}"
|
||||
DB_PASSWORD="plex_password"
|
||||
DB_URL="postgresql://plex_user:${DB_PASSWORD}@database:5432/plex_playlist"
|
||||
POSTGRES_IMAGE_MIRROR="${GITEA_REGISTRY}/darkhelm.org/postgres:16-alpine"
|
||||
POSTGRES_IMAGE_UPSTREAM="docker.io/library/postgres:16-alpine"
|
||||
RESOLVER_PATH="/tmp/resolve-mirrored-image.sh"
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "❌ Docker is required"
|
||||
@@ -1584,10 +1607,25 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "${{ secrets.PACKAGE_ACCESS_TOKEN }}" | docker login "http://${GITEA_REGISTRY}" -u "${{ github.actor }}" --password-stdin
|
||||
|
||||
RESOLVER_SOURCE_IMAGE="${GITEA_REGISTRY}/darkhelm.org/plex-playlist-cicd:${HEAD_SHA}"
|
||||
RESOLVER_CONTAINER="$(docker create "${RESOLVER_SOURCE_IMAGE}")"
|
||||
docker cp "${RESOLVER_CONTAINER}:/workspace/scripts/resolve-mirrored-image.sh" "${RESOLVER_PATH}"
|
||||
docker rm -f "${RESOLVER_CONTAINER}" >/dev/null 2>&1 || true
|
||||
chmod +x "${RESOLVER_PATH}"
|
||||
|
||||
POSTGRES_IMAGE="$(${RESOLVER_PATH} \
|
||||
--image "${POSTGRES_IMAGE_MIRROR}" \
|
||||
--mirror-image "${POSTGRES_IMAGE_MIRROR}" \
|
||||
--upstream-image "${POSTGRES_IMAGE_UPSTREAM}" \
|
||||
--registry-user "${{ github.actor }}" \
|
||||
--registry-password-env PACKAGE_ACCESS_TOKEN)"
|
||||
|
||||
cat >"${COMPOSE_FILE}" <<'EOF'
|
||||
services:
|
||||
database:
|
||||
image: postgres:16-alpine
|
||||
image: ${POSTGRES_IMAGE}
|
||||
environment:
|
||||
POSTGRES_DB: plex_playlist
|
||||
POSTGRES_USER: plex_user
|
||||
@@ -1694,8 +1732,6 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "${{ secrets.PACKAGE_ACCESS_TOKEN }}" | docker login "http://${GITEA_REGISTRY}" -u "${{ github.actor }}" --password-stdin
|
||||
|
||||
retry_registry_op pull "${DEPLOYABLE_BACKEND_TAG_REF}" 5 3
|
||||
retry_registry_op pull "${DEPLOYABLE_BACKEND_DIGEST_REF}" 5 3
|
||||
|
||||
@@ -1722,7 +1758,7 @@ jobs:
|
||||
echo "Resolved deployable backend tag: ${DEPLOYABLE_BACKEND_TAG_REF}"
|
||||
echo "Resolved deployable backend digest: ${EXPECTED_DIGEST_REF}"
|
||||
|
||||
export DB_PASSWORD DB_URL DEPLOYABLE_BACKEND_IMAGE="${EXPECTED_DIGEST_REF}"
|
||||
export DB_PASSWORD DB_URL POSTGRES_IMAGE DEPLOYABLE_BACKEND_IMAGE="${EXPECTED_DIGEST_REF}"
|
||||
|
||||
docker compose -p "${COMPOSE_PROJECT_NAME}" -f "${COMPOSE_FILE}" up -d database backend
|
||||
|
||||
@@ -1852,6 +1888,9 @@ jobs:
|
||||
DB_PASSWORD="plex_password"
|
||||
DB_URL="postgresql://plex_user:${DB_PASSWORD}@database:5432/plex_playlist"
|
||||
E2E_REPO_DIR="$(mktemp -d)"
|
||||
POSTGRES_IMAGE_MIRROR="${GITEA_REGISTRY}/darkhelm.org/postgres:16-alpine"
|
||||
POSTGRES_IMAGE_UPSTREAM="docker.io/library/postgres:16-alpine"
|
||||
RESOLVER_PATH="/tmp/resolve-mirrored-image.sh"
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "❌ Docker is required"
|
||||
@@ -1862,10 +1901,25 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "${{ secrets.PACKAGE_ACCESS_TOKEN }}" | docker login "http://${GITEA_REGISTRY}" -u "${{ github.actor }}" --password-stdin
|
||||
|
||||
RESOLVER_SOURCE_IMAGE="${GITEA_REGISTRY}/darkhelm.org/plex-playlist-cicd:${HEAD_SHA}"
|
||||
RESOLVER_CONTAINER="$(docker create "${RESOLVER_SOURCE_IMAGE}")"
|
||||
docker cp "${RESOLVER_CONTAINER}:/workspace/scripts/resolve-mirrored-image.sh" "${RESOLVER_PATH}"
|
||||
docker rm -f "${RESOLVER_CONTAINER}" >/dev/null 2>&1 || true
|
||||
chmod +x "${RESOLVER_PATH}"
|
||||
|
||||
POSTGRES_IMAGE="$(${RESOLVER_PATH} \
|
||||
--image "${POSTGRES_IMAGE_MIRROR}" \
|
||||
--mirror-image "${POSTGRES_IMAGE_MIRROR}" \
|
||||
--upstream-image "${POSTGRES_IMAGE_UPSTREAM}" \
|
||||
--registry-user "${{ github.actor }}" \
|
||||
--registry-password-env PACKAGE_ACCESS_TOKEN)"
|
||||
|
||||
cat >"${COMPOSE_FILE}" <<'EOF'
|
||||
services:
|
||||
database:
|
||||
image: postgres:16-alpine
|
||||
image: ${POSTGRES_IMAGE}
|
||||
environment:
|
||||
POSTGRES_DB: plex_playlist
|
||||
POSTGRES_USER: plex_user
|
||||
@@ -2028,7 +2082,6 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "${{ secrets.PACKAGE_ACCESS_TOKEN }}" | docker login "http://${GITEA_REGISTRY}" -u "${{ github.actor }}" --password-stdin
|
||||
retry_registry_op pull "${DEPLOYABLE_BACKEND_TAG_REF}" 5 3
|
||||
retry_registry_op pull "${DEPLOYABLE_BACKEND_DIGEST_REF}" 5 3
|
||||
retry_registry_op pull "${DEPLOYABLE_FRONTEND_TAG_REF}" 5 3
|
||||
@@ -2077,7 +2130,7 @@ jobs:
|
||||
echo "Resolved deployable backend digest: ${EXPECTED_BACKEND_DIGEST_REF}"
|
||||
echo "Resolved deployable frontend digest: ${EXPECTED_FRONTEND_DIGEST_REF}"
|
||||
|
||||
export DB_PASSWORD DB_URL E2E_ARTIFACT_DIR="${ARTIFACT_DIR}"
|
||||
export DB_PASSWORD DB_URL POSTGRES_IMAGE E2E_ARTIFACT_DIR="${ARTIFACT_DIR}"
|
||||
export DEPLOYABLE_BACKEND_IMAGE="${EXPECTED_BACKEND_DIGEST_REF}"
|
||||
export DEPLOYABLE_FRONTEND_IMAGE="${EXPECTED_FRONTEND_DIGEST_REF}"
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ jobs:
|
||||
RENOVATE_IMAGE_FALLBACK: ghcr.io/renovatebot/renovate:41
|
||||
GITEA_REGISTRY_HOST: kankali.darkhelm.lan
|
||||
GITEA_REGISTRY_IP: 10.18.75.2
|
||||
PACKAGE_ACCESS_TOKEN: ${{ secrets.PACKAGE_ACCESS_TOKEN }}
|
||||
REGISTRY_USER: ${{ github.actor }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -60,17 +62,78 @@ jobs:
|
||||
echo "${GITEA_REGISTRY_IP} ${GITEA_REGISTRY_HOST}" >> /etc/hosts
|
||||
fi
|
||||
|
||||
if [ -n "${PACKAGE_ACCESS_TOKEN:-}" ] && [ -n "${REGISTRY_USER:-}" ]; then
|
||||
echo "${PACKAGE_ACCESS_TOKEN}" | docker login "http://${GITEA_REGISTRY_HOST}:3001" -u "${REGISTRY_USER}" --password-stdin >/dev/null || true
|
||||
fi
|
||||
|
||||
extract_remote_digest_buildx() {
|
||||
image_ref="$1"
|
||||
digest="$(docker buildx imagetools inspect "${image_ref}" --format '{{json .Manifest.Digest}}' 2>/dev/null || true)"
|
||||
digest="${digest//\"/}"
|
||||
digest="${digest//$'\n'/}"
|
||||
digest="${digest//$'\r'/}"
|
||||
digest="${digest// /}"
|
||||
if [[ "${digest}" == sha256:* ]]; then
|
||||
printf '%s\n' "${digest}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
extract_remote_digest_manifest() {
|
||||
image_ref="$1"
|
||||
digest="$(docker manifest inspect "${image_ref}" 2>/dev/null | sed -n 's/.*"digest"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1 || true)"
|
||||
if [[ "${digest}" == sha256:* ]]; then
|
||||
printf '%s\n' "${digest}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
remote_digest() {
|
||||
image_ref="$1"
|
||||
if [[ "${image_ref}" == *@sha256:* ]]; then
|
||||
printf '%s\n' "${image_ref##*@}"
|
||||
return 0
|
||||
fi
|
||||
extract_remote_digest_buildx "${image_ref}" || extract_remote_digest_manifest "${image_ref}"
|
||||
}
|
||||
|
||||
local_digest_matches() {
|
||||
image_ref="$1"
|
||||
digest="$2"
|
||||
docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "${image_ref}" 2>/dev/null | grep -q "@${digest}$"
|
||||
}
|
||||
|
||||
resolve_candidate() {
|
||||
image_ref="$1"
|
||||
|
||||
if docker image inspect "${image_ref}" >/dev/null 2>&1; then
|
||||
if digest="$(remote_digest "${image_ref}")"; then
|
||||
if local_digest_matches "${image_ref}" "${digest}"; then
|
||||
echo "Using cached current image: ${image_ref}" >&2
|
||||
printf '%s\n' "${image_ref}"
|
||||
return 0
|
||||
fi
|
||||
echo "Cached image is stale for ${image_ref}; refreshing" >&2
|
||||
else
|
||||
echo "Remote digest unavailable for ${image_ref}; attempting refresh pull" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
if retry_cmd 3 15 docker pull "${image_ref}" >/dev/null; then
|
||||
printf '%s\n' "${image_ref}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
pick_renovate_image() {
|
||||
for candidate in "${RENOVATE_IMAGE_PRIMARY}" "${RENOVATE_IMAGE_FALLBACK}"; do
|
||||
echo "Trying Renovate image candidate: ${candidate}" >&2
|
||||
|
||||
if docker image inspect "${candidate}" >/dev/null 2>&1; then
|
||||
echo "Using cached Renovate image: ${candidate}" >&2
|
||||
printf '%s\n' "${candidate}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if retry_cmd 3 15 docker pull "${candidate}" >/dev/null; then
|
||||
if resolve_candidate "${candidate}" >/dev/null; then
|
||||
printf '%s\n' "${candidate}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
313
scripts/resolve-mirrored-image.sh
Executable file
313
scripts/resolve-mirrored-image.sh
Executable file
@@ -0,0 +1,313 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: resolve-mirrored-image.sh --image <upstream-or-primary-image> [options]
|
||||
|
||||
Required:
|
||||
--image IMAGE Desired image reference to use locally.
|
||||
|
||||
Optional:
|
||||
--mirror-image IMAGE Mirror image reference to check/pull/push.
|
||||
--upstream-image IMAGE Upstream source image to import into mirror when mirror is missing.
|
||||
--registry-user USER Registry username for mirror auth.
|
||||
--registry-password-env VAR Environment variable name containing mirror registry password/token.
|
||||
--disable-remote-digest-check Skip remote digest freshness check for local images.
|
||||
--local-only Only verify local presence; do not pull.
|
||||
--quiet Suppress informational output except final image ref.
|
||||
|
||||
Behavior:
|
||||
1. If IMAGE exists locally and matches remote digest, use it.
|
||||
2. Otherwise try to pull IMAGE.
|
||||
3. If IMAGE pull fails and MIRROR_IMAGE is set, resolve via MIRROR_IMAGE.
|
||||
4. If MIRROR_IMAGE is missing and UPSTREAM_IMAGE is set, pull upstream, push mirror, then use mirror.
|
||||
5. Prints the resolved local image reference on stdout.
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
if [[ "${QUIET}" != "true" ]]; then
|
||||
echo "$*" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
require_arg() {
|
||||
local value="$1"
|
||||
local name="$2"
|
||||
if [[ -z "${value}" ]]; then
|
||||
echo "Missing required argument: ${name}" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
login_if_needed() {
|
||||
local image_ref="$1"
|
||||
local registry password_var password
|
||||
|
||||
registry="${image_ref%%/*}"
|
||||
if [[ -z "${registry}" || "${registry}" == "${image_ref}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -z "${REGISTRY_USER}" || -z "${REGISTRY_PASSWORD_ENV}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
password_var="${REGISTRY_PASSWORD_ENV}"
|
||||
password="${!password_var:-}"
|
||||
if [[ -z "${password}" ]]; then
|
||||
log "Skipping docker login for ${registry}: env ${password_var} is empty"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n "${LOGGED_IN_REGISTRIES[${registry}]:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Logging into ${registry}"
|
||||
printf '%s' "${password}" | docker login "http://${registry}" -u "${REGISTRY_USER}" --password-stdin >/dev/null
|
||||
LOGGED_IN_REGISTRIES["${registry}"]=1
|
||||
}
|
||||
|
||||
image_present_locally() {
|
||||
local image_ref="$1"
|
||||
docker image inspect "${image_ref}" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
is_digest_pinned_ref() {
|
||||
local image_ref="$1"
|
||||
[[ "${image_ref}" == *@sha256:* ]]
|
||||
}
|
||||
|
||||
extract_remote_digest_buildx() {
|
||||
local image_ref="$1"
|
||||
local digest
|
||||
|
||||
if ! digest="$(docker buildx imagetools inspect "${image_ref}" --format '{{json .Manifest.Digest}}' 2>/dev/null)"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
digest="${digest//\"/}"
|
||||
digest="${digest//$'\n'/}"
|
||||
digest="${digest//$'\r'/}"
|
||||
digest="${digest// /}"
|
||||
|
||||
if [[ "${digest}" == sha256:* ]]; then
|
||||
printf '%s\n' "${digest}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
extract_remote_digest_manifest() {
|
||||
local image_ref="$1"
|
||||
local digest
|
||||
|
||||
if ! digest="$(docker manifest inspect "${image_ref}" 2>/dev/null | sed -n 's/.*"digest"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "${digest}" == sha256:* ]]; then
|
||||
printf '%s\n' "${digest}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
get_remote_digest() {
|
||||
local image_ref="$1"
|
||||
|
||||
if is_digest_pinned_ref "${image_ref}"; then
|
||||
printf '%s\n' "${image_ref##*@}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if extract_remote_digest_buildx "${image_ref}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if extract_remote_digest_manifest "${image_ref}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
local_image_matches_digest() {
|
||||
local image_ref="$1"
|
||||
local digest="$2"
|
||||
|
||||
docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "${image_ref}" 2>/dev/null | grep -q "@${digest}$"
|
||||
}
|
||||
|
||||
pull_image() {
|
||||
local image_ref="$1"
|
||||
login_if_needed "${image_ref}"
|
||||
docker pull "${image_ref}" >/dev/null
|
||||
}
|
||||
|
||||
resolve_reference() {
|
||||
local image_ref="$1"
|
||||
local remote_digest
|
||||
|
||||
if image_present_locally "${image_ref}"; then
|
||||
if [[ "${DISABLE_REMOTE_DIGEST_CHECK}" == "true" ]] || [[ "${LOCAL_ONLY}" == "true" ]] || is_digest_pinned_ref "${image_ref}"; then
|
||||
log "Using locally cached image ${image_ref}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
login_if_needed "${image_ref}"
|
||||
if remote_digest="$(get_remote_digest "${image_ref}")"; then
|
||||
if local_image_matches_digest "${image_ref}" "${remote_digest}"; then
|
||||
log "Using local image ${image_ref}; remote digest matches (${remote_digest})"
|
||||
return 0
|
||||
fi
|
||||
log "Local image ${image_ref} is stale; expected remote digest ${remote_digest}"
|
||||
else
|
||||
log "Unable to determine remote digest for ${image_ref}; attempting pull refresh"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${LOCAL_ONLY}" == "true" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if pull_image "${image_ref}"; then
|
||||
log "Pulled image ${image_ref}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
mirror_available() {
|
||||
local image_ref="$1"
|
||||
if pull_image "${image_ref}"; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
promote_upstream_to_mirror() {
|
||||
require_arg "${UPSTREAM_IMAGE}" "--upstream-image"
|
||||
require_arg "${MIRROR_IMAGE}" "--mirror-image"
|
||||
|
||||
log "Pulling upstream image ${UPSTREAM_IMAGE}"
|
||||
docker pull "${UPSTREAM_IMAGE}" >/dev/null
|
||||
|
||||
login_if_needed "${MIRROR_IMAGE}"
|
||||
log "Tagging ${UPSTREAM_IMAGE} as ${MIRROR_IMAGE}"
|
||||
docker tag "${UPSTREAM_IMAGE}" "${MIRROR_IMAGE}"
|
||||
log "Pushing ${MIRROR_IMAGE}"
|
||||
docker push "${MIRROR_IMAGE}" >/dev/null
|
||||
}
|
||||
|
||||
IMAGE=""
|
||||
MIRROR_IMAGE=""
|
||||
UPSTREAM_IMAGE=""
|
||||
REGISTRY_USER=""
|
||||
REGISTRY_PASSWORD_ENV=""
|
||||
DISABLE_REMOTE_DIGEST_CHECK="false"
|
||||
LOCAL_ONLY="false"
|
||||
QUIET="false"
|
||||
declare -A LOGGED_IN_REGISTRIES=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--image)
|
||||
IMAGE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--mirror-image)
|
||||
MIRROR_IMAGE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--upstream-image)
|
||||
UPSTREAM_IMAGE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--registry-user)
|
||||
REGISTRY_USER="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--registry-password-env)
|
||||
REGISTRY_PASSWORD_ENV="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--disable-remote-digest-check)
|
||||
DISABLE_REMOTE_DIGEST_CHECK="true"
|
||||
shift
|
||||
;;
|
||||
--local-only)
|
||||
LOCAL_ONLY="true"
|
||||
shift
|
||||
;;
|
||||
--quiet)
|
||||
QUIET="true"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
require_arg "${IMAGE}" "--image"
|
||||
|
||||
if resolve_reference "${IMAGE}"; then
|
||||
printf '%s\n' "${IMAGE}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${LOCAL_ONLY}" == "true" ]]; then
|
||||
echo "Image unavailable via local-only mode: ${IMAGE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${MIRROR_IMAGE}" ]]; then
|
||||
log "Primary image unavailable; checking mirror ${MIRROR_IMAGE}"
|
||||
if resolve_reference "${MIRROR_IMAGE}"; then
|
||||
printf '%s\n' "${MIRROR_IMAGE}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -n "${UPSTREAM_IMAGE}" ]]; then
|
||||
log "Mirror image ${MIRROR_IMAGE} missing; promoting from upstream ${UPSTREAM_IMAGE}"
|
||||
promote_upstream_to_mirror
|
||||
if ! pull_image "${MIRROR_IMAGE}"; then
|
||||
echo "Failed to pull promoted mirror image: ${MIRROR_IMAGE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "${MIRROR_IMAGE}"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${UPSTREAM_IMAGE}" ]]; then
|
||||
log "Falling back to upstream image ${UPSTREAM_IMAGE}"
|
||||
if resolve_reference "${UPSTREAM_IMAGE}"; then
|
||||
printf '%s\n' "${UPSTREAM_IMAGE}"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Failed to resolve image via primary/mirror/upstream path" >&2
|
||||
echo "primary=${IMAGE}" >&2
|
||||
if [[ -n "${MIRROR_IMAGE}" ]]; then
|
||||
echo "mirror=${MIRROR_IMAGE}" >&2
|
||||
fi
|
||||
if [[ -n "${UPSTREAM_IMAGE}" ]]; then
|
||||
echo "upstream=${UPSTREAM_IMAGE}" >&2
|
||||
fi
|
||||
exit 1
|
||||
Reference in New Issue
Block a user