diff --git a/.gitea/workflows/cicd.yaml b/.gitea/workflows/cicd.yaml index f9d0b79..9354252 100644 --- a/.gitea/workflows/cicd.yaml +++ b/.gitea/workflows/cicd.yaml @@ -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}" diff --git a/.gitea/workflows/renovate.yml b/.gitea/workflows/renovate.yml index 4ee14cb..014db65 100644 --- a/.gitea/workflows/renovate.yml +++ b/.gitea/workflows/renovate.yml @@ -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 diff --git a/scripts/resolve-mirrored-image.sh b/scripts/resolve-mirrored-image.sh new file mode 100755 index 0000000..18ce851 --- /dev/null +++ b/scripts/resolve-mirrored-image.sh @@ -0,0 +1,313 @@ +#!/bin/bash + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: resolve-mirrored-image.sh --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