diff --git a/.gitea/scripts/resolve-base-image.sh b/.gitea/scripts/resolve-base-image.sh new file mode 100755 index 0000000..1c5b074 --- /dev/null +++ b/.gitea/scripts/resolve-base-image.sh @@ -0,0 +1,35 @@ +#!/bin/bash +## resolve-base-image.sh IMAGE:TAG — CI only (not copied into images). +## Prints IMAGE@sha256: for a Docker Hub tag, looked up DIRECTLY at +## registry-1.docker.io over HTTPS (never via the pkg-cache mirror), so the +## build can say `FROM IMAGE@sha256:...` and the mirror can only serve +## content-addressed blobs that BuildKit verifies. Exits non-zero (prints +## nothing on stdout) if the lookup fails; the workflow then skips the mirror +## and pulls the tag directly from Docker Hub. HEAD requests do not count +## against Docker Hub pull rate limits. +## Canonical copy: cloud-apache-container/.gitea/scripts/resolve-base-image.sh +## (docs/pkg-cache.md there); other repos carry identical copies. +set -euo pipefail +ref="${1:?usage: resolve-base-image.sh IMAGE:TAG}" +name_re='^[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)?$' +tag_re='^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$' +name="${ref%:*}"; tag="${ref##*:}" +[ "$name" != "$ref" ] || { echo "resolve-base-image: '$ref' has no :tag" >&2; exit 2; } +[[ "$name" =~ $name_re ]] || { echo "resolve-base-image: bad image name '$name'" >&2; exit 2; } +[[ "$tag" =~ $tag_re ]] || { echo "resolve-base-image: bad tag '$tag'" >&2; exit 2; } +repo="$name"; [[ "$repo" == */* ]] || repo="library/$repo" # official images + +curl_opts=(-fsS --proto '=https' --retry 3 --retry-all-errors --max-time 20) +token=$(curl "${curl_opts[@]}" "https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repo}:pull" | + python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])') +[ -n "$token" ] || { echo "resolve-base-image: no Docker Hub token for $repo" >&2; exit 1; } +hdrs=$(curl "${curl_opts[@]}" -I \ + -H "Authorization: Bearer ${token}" \ + -H 'Accept: application/vnd.oci.image.index.v1+json' \ + -H 'Accept: application/vnd.docker.distribution.manifest.list.v2+json' \ + -H 'Accept: application/vnd.oci.image.manifest.v1+json' \ + -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \ + "https://registry-1.docker.io/v2/${repo}/manifests/${tag}") +digest=$(printf '%s\n' "$hdrs" | tr -d '\r' | awk 'tolower($1)=="docker-content-digest:" {print $2}' | tail -n1) +[[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] || { echo "resolve-base-image: no valid digest for $ref (got '${digest}')" >&2; exit 1; } +printf '%s@%s\n' "$name" "$digest" diff --git a/.gitea/workflows/build-push.yaml b/.gitea/workflows/build-push.yaml index 37afd4d..633b904 100644 --- a/.gitea/workflows/build-push.yaml +++ b/.gitea/workflows/build-push.yaml @@ -16,17 +16,40 @@ jobs: - name: Checkout uses: actions/checkout@v4 + # Base image pinned by DIGEST, looked up directly at Docker Hub (never via + # the pkg-cache mirror), so the mirror can only serve content-addressed + # blobs that BuildKit verifies. If the lookup fails this job does NOT use + # the mirror and pulls the tag straight from Docker Hub. + - name: Resolve base image digest (Docker Hub, direct) + id: base + env: + BASE_TAG: almalinux/10-base:latest + run: | + set -uo pipefail + if ref=$(.gitea/scripts/resolve-base-image.sh "$BASE_TAG") && [[ "$ref" =~ ^[a-z0-9][a-z0-9._/-]*@sha256:[0-9a-f]{64}$ ]]; then + echo "base-image: ${BASE_TAG} -> ${ref} (digest from registry-1.docker.io)" + echo "image=${ref}" >> "$GITHUB_OUTPUT" + echo "pinned=true" >> "$GITHUB_OUTPUT" + else + echo "::warning::base image ${BASE_TAG}: Docker Hub digest lookup failed - pulling the tag directly from Docker Hub, pkg-cache registry mirror NOT used" + echo "image=${BASE_TAG}" >> "$GITHUB_OUTPUT" + echo "pinned=false" >> "$GITHUB_OUTPUT" + fi + # Internal package cache (CT130 pkg-cache). Used ONLY if it answers over # TLS with our pinned CA; otherwise this job builds from the upstream repos # and Docker Hub exactly as before. Repo cache and registry mirror are # probed separately so either can fail back on its own. - name: Probe package cache id: pkgcache + # The registry mirror is only used for a digest-pinned base image. + env: + BASE_PINNED: ${{ steps.base.outputs.pinned }} run: | url=""; mirror="" if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi - if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi - echo "pkg-cache: repos=${url:-} registry-mirror=${mirror:-}" + if [ "$BASE_PINNED" = true ] && curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi + echo "pkg-cache: repos=${url:-} registry-mirror=${mirror:-}" [ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories" echo "url=${url}" >> "$GITHUB_OUTPUT" echo "mirror=${mirror}" >> "$GITHUB_OUTPUT" @@ -64,6 +87,7 @@ jobs: push: true build-args: | NODEVER=${{ matrix.nodever }} + BASE_IMAGE=${{ steps.base.outputs.image }} PKG_CACHE=${{ steps.pkgcache.outputs.url }} tags: | repo.anhonesthost.net/cloud-hosting-platform/cnoc:node${{ matrix.nodever }} diff --git a/Dockerfile b/Dockerfile index 713dda7..364489c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,9 @@ -FROM almalinux/10-base +# Base image. CI passes BASE_IMAGE=almalinux/10-base@sha256:, resolved +# DIRECTLY from Docker Hub in the same run (.gitea/scripts/resolve-base-image.sh), +# so the pkg-cache registry mirror can only serve content-addressed blobs. +# Local builds use the tag. cloud-apache-container docs/pkg-cache.md. +ARG BASE_IMAGE=almalinux/10-base +FROM ${BASE_IMAGE} ARG NODEVER=20 # OPTIONAL internal package cache, e.g. --build-arg PKG_CACHE=https://192.168.1.40