mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
The package cache creates every Nexus proxy with strictContentTypeValidation. A raw proxy has no type contract, and Nexus cannot type a GPG keybox: on a fresh cache it answers the Node.js release keyring (raw.githubusercontent.com/nodejs/release-keys/.../pubring.kbx) with 404 "Content type could not be determined". The Node.js installer from #1108 fetches that keyring, so on main (run 39668792538) Workspace debian:13 fails in `make install-lint`, and Workspace ubuntu, centos and manjaro fail in `test-external`, whose URL check reaches the same file through the cache. The four raw proxies (raw-githubusercontent, raw-codeload-github, raw-packagist, raw-alpine) now share the storage block with validation switched off. Every typed proxy (apt, pypi, npm, helm, rubygems, go, yum) keeps strict validation. The installer's integrity check does not depend on Nexus: it verifies the SHASUMS file with gpgv and the tarball with SHA-256. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
209 lines
13 KiB
Bash
Executable File
209 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# shellcheck shell=bash
|
||
# Idempotent Nexus 3 OSS proxy bootstrap.
|
||
# See docs/contributing/environment/cache.md.
|
||
|
||
set -euo pipefail
|
||
|
||
: "${INFINITO_CACHE_PACKAGE_HOST_PATH:?Source scripts/meta/env/load.sh first}"
|
||
: "${INFINITO_CACHE_PACKAGE_ADMIN_PASSWORD:?Source scripts/meta/env/load.sh first}"
|
||
: "${INFINITO_CACHE_PACKAGE_BLOBSTORE_MAX:?Source scripts/meta/env/load.sh first}"
|
||
: "${INFINITO_CACHE_PACKAGE_MAX_AGE_MIN:?Source scripts/meta/env/load.sh first}"
|
||
: "${INFINITO_CACHE_PACKAGE_METADATA_MAX_AGE_MIN:?Source scripts/meta/env/load.sh first}"
|
||
: "${INFINITO_APT_UBUNTU_MIRRORS:?Source scripts/meta/env/load.sh first}"
|
||
|
||
CACHE_CONTENT_MAX_AGE_MIN="${INFINITO_CACHE_PACKAGE_MAX_AGE_MIN}"
|
||
CACHE_METADATA_MAX_AGE_MIN="${INFINITO_CACHE_PACKAGE_METADATA_MAX_AGE_MIN}"
|
||
|
||
read -r -a UBUNTU_MIRRORS <<<"${INFINITO_APT_UBUNTU_MIRRORS}"
|
||
UBUNTU_PRIMARY="${UBUNTU_MIRRORS[0]%/}"
|
||
UBUNTU_FALLBACK="${UBUNTU_MIRRORS[1]:?INFINITO_APT_UBUNTU_MIRRORS needs a primary and a fallback mirror}"
|
||
UBUNTU_FALLBACK="${UBUNTU_FALLBACK%/}"
|
||
|
||
PKGCACHE_CONTAINER="infinito-package-cache"
|
||
NEXUS_REST="http://127.0.0.1:8081/service/rest"
|
||
|
||
BOOTSTRAP_DONE_FILE="/nexus-data/.infinito-bootstrap-done"
|
||
NEXUS_ADMIN_PW_FILE="/nexus-data/.infinito-admin-password"
|
||
NEXUS_DEFAULT_PASSWORD="admin123"
|
||
|
||
log() { printf '[package-cache-bootstrap] %s\n' "$*" >&2; }
|
||
|
||
nexus_curl() { docker exec "${PKGCACHE_CONTAINER}" curl "$@"; }
|
||
|
||
wait_for_nexus() {
|
||
local _attempt
|
||
for _attempt in $(seq 1 120); do
|
||
if nexus_curl -fsS "${NEXUS_REST}/v1/status" >/dev/null 2>&1; then
|
||
return 0
|
||
fi
|
||
sleep 2
|
||
done
|
||
log "Nexus REST not reachable at ${NEXUS_REST}/v1/status after 4 minutes"
|
||
return 1
|
||
}
|
||
|
||
store_admin_password() {
|
||
printf %s "${INFINITO_CACHE_PACKAGE_ADMIN_PASSWORD}" |
|
||
docker exec -i "${PKGCACHE_CONTAINER}" sh -c "umask 077; cat > '${NEXUS_ADMIN_PW_FILE}'"
|
||
}
|
||
|
||
rotate_admin_password() {
|
||
ADMIN_USER="admin"
|
||
if docker exec "${PKGCACHE_CONTAINER}" test -f "${BOOTSTRAP_DONE_FILE}"; then
|
||
log "Already bootstrapped; using stored admin password"
|
||
ADMIN_PASS="$(docker exec "${PKGCACHE_CONTAINER}" cat "${NEXUS_ADMIN_PW_FILE}" 2>/dev/null || true)"
|
||
if [ -z "${ADMIN_PASS}" ]; then
|
||
ADMIN_PASS="${INFINITO_CACHE_PACKAGE_ADMIN_PASSWORD}"
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
log "Rotating admin password from default '${NEXUS_DEFAULT_PASSWORD}'"
|
||
if ! nexus_curl -fsS -u "admin:${NEXUS_DEFAULT_PASSWORD}" \
|
||
-H "Content-Type: text/plain" \
|
||
-X PUT "${NEXUS_REST}/v1/security/users/admin/change-password" \
|
||
--data-binary "${INFINITO_CACHE_PACKAGE_ADMIN_PASSWORD}"; then
|
||
if nexus_curl -fsS -o /dev/null -u "admin:${INFINITO_CACHE_PACKAGE_ADMIN_PASSWORD}" \
|
||
"${NEXUS_REST}/v1/repositories" >/dev/null 2>&1; then
|
||
log "Default rotation rejected but configured password authenticates; assuming previous bootstrap, marking done"
|
||
else
|
||
log "Cannot authenticate with default '${NEXUS_DEFAULT_PASSWORD}' nor with configured INFINITO_CACHE_PACKAGE_ADMIN_PASSWORD."
|
||
log "Wipe ${INFINITO_CACHE_PACKAGE_HOST_PATH} (e.g. via 'make clean-cache') and retry."
|
||
return 1
|
||
fi
|
||
fi
|
||
store_admin_password
|
||
docker exec "${PKGCACHE_CONTAINER}" touch "${BOOTSTRAP_DONE_FILE}"
|
||
ADMIN_PASS="${INFINITO_CACHE_PACKAGE_ADMIN_PASSWORD}"
|
||
}
|
||
|
||
ensure_blobstore() {
|
||
local quota_gb="${INFINITO_CACHE_PACKAGE_BLOBSTORE_MAX%g}"
|
||
local quota_mb=$((quota_gb * 1024))
|
||
local payload
|
||
payload="$(printf '{"name":"default","path":"default","softQuota":{"type":"spaceUsedQuota","limit":%d}}' "${quota_mb}")"
|
||
local code
|
||
code="$(nexus_curl -sS -o /dev/null -w '%{http_code}' \
|
||
-u "${ADMIN_USER}:${ADMIN_PASS}" \
|
||
-H "Content-Type: application/json" \
|
||
-X POST "${NEXUS_REST}/v1/blobstores/file" \
|
||
--data "${payload}" || true)"
|
||
case "${code}" in
|
||
201 | 204) log "blobstore default created (HTTP ${code})" ;;
|
||
400 | 409) log "blobstore default already exists (HTTP ${code})" ;;
|
||
*)
|
||
log "unexpected HTTP ${code} creating blobstore"
|
||
return 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
ensure_repo() {
|
||
local format="$1" kind="$2" name="$3" payload="$4"
|
||
local code
|
||
code="$(nexus_curl -sS -o /dev/null -w '%{http_code}' \
|
||
-u "${ADMIN_USER}:${ADMIN_PASS}" \
|
||
-H "Content-Type: application/json" \
|
||
-X POST "${NEXUS_REST}/v1/repositories/${format}/${kind}" \
|
||
--data "${payload}" || true)"
|
||
case "${code}" in
|
||
201 | 204) log "${format} ${kind} ${name} created (HTTP ${code})" ;;
|
||
400 | 409)
|
||
code="$(nexus_curl -sS -o /dev/null -w '%{http_code}' \
|
||
-u "${ADMIN_USER}:${ADMIN_PASS}" \
|
||
-H "Content-Type: application/json" \
|
||
-X PUT "${NEXUS_REST}/v1/repositories/${format}/${kind}/${name}" \
|
||
--data "${payload}" || true)"
|
||
case "${code}" in
|
||
200 | 204) log "${format} ${kind} ${name} updated (HTTP ${code})" ;;
|
||
*)
|
||
log "unexpected HTTP ${code} updating ${format} ${kind} ${name}"
|
||
return 1
|
||
;;
|
||
esac
|
||
;;
|
||
*)
|
||
log "unexpected HTTP ${code} creating ${format} ${kind} ${name}"
|
||
return 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
ensure_proxy_repo() { ensure_repo "$1" proxy "$2" "$3"; }
|
||
|
||
# autoBlock stays off: once Nexus blocks a remote it answers every client
|
||
# with `404 Remote Auto Blocked` for the whole block window, so a single
|
||
# upstream hiccup fails every concurrent job instead of just the request
|
||
# that hit it.
|
||
ensure_all_proxies() {
|
||
local storage='"storage":{"blobStoreName":"default","strictContentTypeValidation":true},"proxy":{"contentMaxAge":'"${CACHE_CONTENT_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'},"negativeCache":{"enabled":true,"timeToLive":'"${CACHE_METADATA_MAX_AGE_MIN}"'},"httpClient":{"blocked":false,"autoBlock":false}'
|
||
# Exception: raw content has no type contract; strict validation answers a GPG keybox (.kbx) with 404 "Content type could not be determined".
|
||
local raw_storage="${storage/'"strictContentTypeValidation":true'/'"strictContentTypeValidation":false'}"
|
||
|
||
ensure_proxy_repo apt apt-debian "$(printf '{"name":"apt-debian","online":true,%s,"proxy":{"remoteUrl":"http://deb.debian.org/debian","contentMaxAge":'"${CACHE_CONTENT_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'},"apt":{"distribution":"bookworm","flat":false}}' "${storage}")"
|
||
ensure_proxy_repo apt apt-ubuntu "$(printf '{"name":"apt-ubuntu","online":true,%s,"proxy":{"remoteUrl":"'"${UBUNTU_PRIMARY}"'","contentMaxAge":'"${CACHE_CONTENT_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'},"apt":{"distribution":"jammy","flat":false}}' "${storage}")"
|
||
ensure_proxy_repo apt apt-debian-security "$(printf '{"name":"apt-debian-security","online":true,%s,"proxy":{"remoteUrl":"http://deb.debian.org/debian-security","contentMaxAge":'"${CACHE_CONTENT_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'},"apt":{"distribution":"bookworm-security","flat":false}}' "${storage}")" # nocheck: url (bare GET on the deb.debian.org redirector returns 404; apt-client paths underneath dists/bookworm-security/ resolve correctly)
|
||
ensure_proxy_repo apt apt-ubuntu-security "$(printf '{"name":"apt-ubuntu-security","online":true,%s,"proxy":{"remoteUrl":"'"${UBUNTU_PRIMARY}"'","contentMaxAge":'"${CACHE_CONTENT_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'},"apt":{"distribution":"jammy-security","flat":false}}' "${storage}")"
|
||
ensure_proxy_repo apt apt-debian-mirror "$(printf '{"name":"apt-debian-mirror","online":true,%s,"proxy":{"remoteUrl":"http://ftp.debian.org/debian","contentMaxAge":'"${CACHE_CONTENT_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'},"apt":{"distribution":"bookworm","flat":false}}' "${storage}")"
|
||
ensure_proxy_repo apt apt-ubuntu-mirror "$(printf '{"name":"apt-ubuntu-mirror","online":true,%s,"proxy":{"remoteUrl":"'"${UBUNTU_FALLBACK}"'","contentMaxAge":'"${CACHE_CONTENT_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'},"apt":{"distribution":"jammy","flat":false}}' "${storage}")"
|
||
ensure_proxy_repo apt apt-debian-security-mirror "$(printf '{"name":"apt-debian-security-mirror","online":true,%s,"proxy":{"remoteUrl":"http://security.debian.org/debian-security","contentMaxAge":'"${CACHE_CONTENT_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'},"apt":{"distribution":"bookworm-security","flat":false}}' "${storage}")"
|
||
ensure_proxy_repo apt apt-ubuntu-security-mirror "$(printf '{"name":"apt-ubuntu-security-mirror","online":true,%s,"proxy":{"remoteUrl":"'"${UBUNTU_FALLBACK}"'","contentMaxAge":'"${CACHE_CONTENT_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'},"apt":{"distribution":"jammy-security","flat":false}}' "${storage}")"
|
||
ensure_proxy_repo pypi pypi-proxy "$(printf '{"name":"pypi-proxy","online":true,%s,"proxy":{"remoteUrl":"https://pypi.org/","contentMaxAge":'"${CACHE_CONTENT_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'}}' "${storage}")"
|
||
ensure_proxy_repo npm npm-proxy "$(printf '{"name":"npm-proxy","online":true,%s,"proxy":{"remoteUrl":"https://registry.npmjs.org/","contentMaxAge":'"${CACHE_CONTENT_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'}}' "${storage}")"
|
||
ensure_proxy_repo helm helm-bitnami "$(printf '{"name":"helm-bitnami","online":true,%s,"proxy":{"remoteUrl":"https://charts.bitnami.com/bitnami","contentMaxAge":'"${CACHE_CONTENT_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'}}' "${storage}")"
|
||
ensure_proxy_repo raw raw-githubusercontent "$(printf '{"name":"raw-githubusercontent","online":true,%s,"proxy":{"remoteUrl":"https://raw.githubusercontent.com/","contentMaxAge":'"${CACHE_CONTENT_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'},"raw":{"contentDisposition":"ATTACHMENT"}}' "${raw_storage}")"
|
||
ensure_proxy_repo raw raw-codeload-github "$(printf '{"name":"raw-codeload-github","online":true,%s,"proxy":{"remoteUrl":"https://codeload.github.com/","contentMaxAge":'"${CACHE_CONTENT_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'},"raw":{"contentDisposition":"ATTACHMENT"}}' "${raw_storage}")"
|
||
ensure_proxy_repo rubygems gem-proxy "$(printf '{"name":"gem-proxy","online":true,%s,"proxy":{"remoteUrl":"https://rubygems.org/","contentMaxAge":'"${CACHE_CONTENT_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'}}' "${storage}")"
|
||
ensure_proxy_repo go go-proxy "$(printf '{"name":"go-proxy","online":true,%s,"proxy":{"remoteUrl":"https://proxy.golang.org/","contentMaxAge":'"${CACHE_CONTENT_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'}}' "${storage}")"
|
||
ensure_proxy_repo yum yum-rocky "$(printf '{"name":"yum-rocky","online":true,%s,"proxy":{"remoteUrl":"https://download.rockylinux.org/pub/rocky/","contentMaxAge":'"${CACHE_CONTENT_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'},"yum":{"repodataDepth":5}}' "${storage}")"
|
||
ensure_proxy_repo yum yum-fedora "$(printf '{"name":"yum-fedora","online":true,%s,"proxy":{"remoteUrl":"https://download.fedoraproject.org/pub/fedora/linux/","contentMaxAge":'"${CACHE_CONTENT_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'},"yum":{"repodataDepth":5}}' "${storage}")"
|
||
ensure_proxy_repo raw raw-packagist "$(printf '{"name":"raw-packagist","online":true,%s,"proxy":{"remoteUrl":"https://repo.packagist.org/","contentMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'},"raw":{"contentDisposition":"ATTACHMENT"}}' "${raw_storage}")" # nocheck: url
|
||
ensure_proxy_repo raw raw-alpine "$(printf '{"name":"raw-alpine","online":true,%s,"proxy":{"remoteUrl":"https://dl-cdn.alpinelinux.org/alpine/","contentMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"',"metadataMaxAge":'"${CACHE_METADATA_MAX_AGE_MIN}"'},"raw":{"contentDisposition":"ATTACHMENT"}}' "${raw_storage}")"
|
||
}
|
||
|
||
ensure_eula_accepted() {
|
||
local code
|
||
# shellcheck disable=SC1112
|
||
code="$(nexus_curl -sS -o /dev/null -w '%{http_code}' \
|
||
-u "${ADMIN_USER}:${ADMIN_PASS}" \
|
||
-H "Content-Type: application/json" \
|
||
-X POST "${NEXUS_REST}/v1/system/eula" \
|
||
--data '{"accepted":true,"disclaimer":"Use of Sonatype Nexus Repository - Community Edition is governed by the End User License Agreement at https://links.sonatype.com/products/nxrm/ce-eula. By returning the value from ‘accepted:false’ to ‘accepted:true’, you acknowledge that you have read and agree to the End User License Agreement at https://links.sonatype.com/products/nxrm/ce-eula."}' || true)"
|
||
case "${code}" in
|
||
200 | 204) log "EULA accepted (HTTP ${code})" ;;
|
||
*)
|
||
log "unexpected HTTP ${code} accepting EULA"
|
||
return 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
ensure_anonymous_access() {
|
||
local code
|
||
code="$(nexus_curl -sS -o /dev/null -w '%{http_code}' \
|
||
-u "${ADMIN_USER}:${ADMIN_PASS}" \
|
||
-H "Content-Type: application/json" \
|
||
-X PUT "${NEXUS_REST}/v1/security/anonymous" \
|
||
--data '{"enabled":true,"userId":"anonymous","realmName":"NexusAuthorizingRealm"}' || true)"
|
||
case "${code}" in
|
||
200 | 204) log "anonymous access enabled (HTTP ${code})" ;;
|
||
*)
|
||
log "unexpected HTTP ${code} enabling anonymous access"
|
||
return 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
main() {
|
||
wait_for_nexus
|
||
rotate_admin_password
|
||
ensure_eula_accepted
|
||
ensure_anonymous_access
|
||
ensure_blobstore
|
||
ensure_all_proxies
|
||
log "bootstrap done"
|
||
}
|
||
|
||
main "$@"
|