diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ae499a0..a0bdde40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,22 @@ name: docker-install CI on: [push, pull_request] jobs: + # Test mirror-option handling, not mirror reachability. Live Tencent Cloud + # validation must be run explicitly from a Tencent Cloud VPC private network. + test-mirror-options: + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 + - name: Check mirror test syntax + run: | + sh -n scripts/test-mirrors.sh + sh -n scripts/test-tencent-cloud-mirror.sh + - name: Check mirror options (offline only) + run: | + sh scripts/test-mirrors.sh + TEST_SHELL=bash sh scripts/test-mirrors.sh + test: runs-on: ubuntu-latest timeout-minutes: 5 diff --git a/.github/workflows/setup-repo.yml b/.github/workflows/setup-repo.yml new file mode 100644 index 00000000..7dc56ff6 --- /dev/null +++ b/.github/workflows/setup-repo.yml @@ -0,0 +1,21 @@ +name: setup-repo option regression + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + test-setup-repo-options: + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 + - name: Check shell syntax + run: | + sh -n install.sh + sh -n scripts/test-setup-repo.sh + - name: Check setup-repo options (offline only) + run: | + sh scripts/test-setup-repo.sh + TEST_SHELL=bash sh scripts/test-setup-repo.sh diff --git a/README.md b/README.md index 9135fb86..044a0634 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,55 @@ From the source repo (This will install latest from the `stable` channel): sh install.sh ``` +### Package mirrors + +Use `--mirror` to select a Docker package mirror: + +| Mirror | Download URL | +| --- | --- | +| `Aliyun` | `https://mirrors.aliyun.com/docker-ce` | +| `AzureChinaCloud` | `https://mirror.azure.cn/docker-ce` | +| `TencentCloud` | `https://mirrors.cloud.tencent.com/docker-ce` | + +**Tencent Cloud environment:** use the `TencentCloud` preset on a Tencent Cloud +VM through its VPC private-network route. Keep the default Tencent Cloud VPC DNS +configuration and avoid external proxies for private-network validation. +According to [Tencent Cloud's software mirror documentation](https://cloud.tencent.com/document/product/213/8623), +`mirrors.cloud.tencent.com` is a unified public/private domain that preferentially +resolves to the internal route with the default VPC DNS. This is not a claim that +the domain can never be reached publicly. Public CI results are not evidence of +Tencent Cloud intranet connectivity; live validation must run in that environment. + +On a fresh Tencent Cloud VM running a supported Linux distribution, review the +script and preview the installation before running it as root: + +```shell +sh install.sh --mirror TencentCloud --dry-run +sudo sh install.sh --mirror TencentCloud +``` + +`--dry-run` only prints commands; it does not verify DNS, mirror reachability, +package availability, or installation success, even when run on a Tencent Cloud VM. + +To configure the package repository without installing Docker packages: + +```shell +sudo sh install.sh --mirror TencentCloud --setup-repo +``` + +The `DOWNLOAD_URL` environment variable remains available for custom mirrors. +For example, the following previews the same mirror selection: + +```shell +sudo env DOWNLOAD_URL=https://mirrors.cloud.tencent.com/docker-ce \ + sh install.sh --dry-run +``` + +An explicit `--mirror` takes precedence over `DOWNLOAD_URL`. These options select +Docker package repositories, not Docker Hub registry mirrors. Mirror selection +does not change the supported distributions, and package availability depends on +the selected mirror. + ## Testing: To verify that the install script works amongst the supported operating systems run: @@ -39,6 +88,75 @@ To verify that the install script works amongst the supported operating systems make shellcheck ``` +### Offline mirror-option regression checks + +Run on a supported Linux distribution, including ordinary GitHub-hosted runners: + +```shell +sh scripts/test-mirrors.sh +``` + +This suite checks argument parsing and dry-run output for all mirror presets, +including `TencentCloud`. It does not contact any mirror or install packages. +Downloader, package-manager and service commands are guarded against accidental +execution. Output is labelled `PASS (offline)` and must not be reported as live +mirror validation. The separate CI job runs only these offline checks and syntax +checks; it does not add Tencent Cloud network access to the installation matrix. + +### Opt-in Tencent Cloud intranet smoke check + +**Run live checks only from a Tencent Cloud VM using the VPC private network.** +Check the VM's DNS and routing first; do not run this on ordinary GitHub-hosted +runners or enable it automatically for untrusted pull requests on private runners. +For example, inspect resolution from the VM: + +```shell +getent ahostsv4 mirrors.cloud.tencent.com +``` + +The network script skips by default, before DNS lookup or any network request: + +```shell +sh scripts/test-tencent-cloud-mirror.sh ubuntu resolute +# SKIP: Tencent Cloud network smoke check is disabled; no network requests made. +``` + +After confirming the Tencent Cloud private-network environment, explicitly enable +it. The following checks the Ubuntu 26.04 (`resolute`) repository: + +```shell +TENCENT_CLOUD_INTRANET=1 \ + sh scripts/test-tencent-cloud-mirror.sh ubuntu resolute +``` + +The switch is an operator confirmation, not automatic cloud or route detection. +The script covers Ubuntu and Debian APT repositories; pass the target distribution +and codename, for example `debian trixie`. It downloads only the GPG key and Release +metadata to temporary files, with HTTPS verification, proxies disabled and bounded +timeouts. It neither follows redirects nor silently falls back to another endpoint. +It needs `curl`, not root privileges, and makes no package or service changes. + +A disabled check reports `SKIP` and exits 0. Invalid arguments or opt-in values +exit 2. After opting in, DNS/TLS/HTTP errors, timeouts and unexpected content +fail with exit 1; they are never converted to a successful skip. + +`PASS (network smoke only)` means the two endpoints returned expected content +markers. It does **not** verify cryptographic signatures, architecture/channel +package availability, package downloads, installation, or Docker service startup. +For installation acceptance, use a fresh disposable Tencent Cloud VM in the same +private-network environment, review the script, and run: + +```shell +sudo sh install.sh --mirror TencentCloud +sudo docker version +sudo docker info +sudo docker compose version +``` + +Record the OS, architecture, commit, network environment and results separately +from the offline and smoke checks. Docker Hub access is a separate service and is +not part of this package-mirror test. + ## Legal *Brought to you courtesy of our legal counsel. For more context, please see the [NOTICE](NOTICE) document in this repo.* diff --git a/install.sh b/install.sh index 82ec27f4..0a0b148f 100755 --- a/install.sh +++ b/install.sh @@ -67,13 +67,15 @@ set -e # Alternatively, use the script at https://test.docker.com, which uses the test # channel as default. # -# --mirror +# --mirror # # Use the --mirror option to install from a mirror supported by this script. -# Available mirrors are "Aliyun" (https://mirrors.aliyun.com/docker-ce), and -# "AzureChinaCloud" (https://mirror.azure.cn/docker-ce), for example: +# Available mirrors are "Aliyun" (https://mirrors.aliyun.com/docker-ce), +# "AzureChinaCloud" (https://mirror.azure.cn/docker-ce), and +# "TencentCloud" (https://mirrors.cloud.tencent.com/docker-ce), for example: # # $ sudo sh install-docker.sh --mirror AzureChinaCloud +# $ sudo sh install-docker.sh --mirror TencentCloud # # --setup-repo # @@ -146,7 +148,7 @@ SBX=${SBX:-0} usage() { echo echo "USAGE: " - echo " ${0} [--channel ] [--mirror ] [--version ] [--setup-repo] [--no-autostart] [--dry-run] [--help]" + echo " ${0} [--channel ] [--mirror ] [--version ] [--setup-repo] [--no-autostart] [--dry-run] [--help]" echo } @@ -169,7 +171,6 @@ while [ $# -gt 0 ]; do ;; --setup-repo) REPO_ONLY=1 - shift ;; --no-autostart) NO_AUTOSTART=1 @@ -194,10 +195,13 @@ case "$mirror" in AzureChinaCloud) DOWNLOAD_URL="https://mirror.azure.cn/docker-ce" ;; + TencentCloud) + DOWNLOAD_URL="https://mirrors.cloud.tencent.com/docker-ce" + ;; "") ;; *) - >&2 echo "unknown mirror '$mirror': use either 'Aliyun', or 'AzureChinaCloud'." + >&2 echo "unknown mirror '$mirror': use 'Aliyun', 'AzureChinaCloud', or 'TencentCloud'." exit 1 ;; esac diff --git a/scripts/test-mirrors.sh b/scripts/test-mirrors.sh new file mode 100755 index 00000000..2d9786cb --- /dev/null +++ b/scripts/test-mirrors.sh @@ -0,0 +1,165 @@ +#!/bin/sh +# Offline mirror-option/dry-run regression checks, not mirror availability tests. +# TencentCloud must stay covered here even on non-Tencent CI runners: these +# assertions never contact a mirror. See test-tencent-cloud-mirror.sh for the +# separate, opt-in smoke check that requires Tencent Cloud's private network. +set -eu + +cd "$(dirname "$0")/.." +test_shell=${TEST_SHELL:-sh} +tmp_dir=$(mktemp -d) +trap 'rm -rf "$tmp_dir"' 0 +trap 'exit 1' HUP INT TERM + +# Avoid the existing-installation warning delay on CI hosts with Docker installed. +mkdir "$tmp_dir/bin" +printf '#!/bin/sh\nexit 0\n' > "$tmp_dir/bin/sleep" +chmod +x "$tmp_dir/bin/sleep" + +# Fail if dry-run accidentally invokes a downloader, package manager or service +# command. Preserve command-existence detection by only shadowing installed tools. +for tool in curl wget apt-get apt-cache dnf dnf5 yum yum-config-manager systemctl sudo su docker; do + command -v "$tool" > /dev/null 2>&1 || continue + cat > "$tmp_dir/bin/$tool" <<-'GUARD' + #!/bin/sh + printf '%s\n' "${0##*/}" >> "$FORBIDDEN_COMMAND_LOG" + echo "ERROR: ${0##*/} must not execute in an offline test." >&2 + exit 99 + GUARD + chmod +x "$tmp_dir/bin/$tool" +done +output="$tmp_dir/output" +download_url='' +network_test='' + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + cat "$output" >&2 + exit 1 +} + +run_script() { + status=0 + : > "$tmp_dir/forbidden" + # Discard inherited installer options and any network-test opt-in. + env -i PATH="$tmp_dir/bin:$PATH" HOME="${HOME:-/tmp}" \ + DOWNLOAD_URL="$download_url" TENCENT_CLOUD_INTRANET="$network_test" \ + FORBIDDEN_COMMAND_LOG="$tmp_dir/forbidden" \ + "$test_shell" "$@" > "$output" 2>&1 || status=$? + [ ! -s "$tmp_dir/forbidden" ] || fail 'offline test executed a forbidden command' + return "$status" +} + +run() { + run_script ./install.sh --dry-run "$@" +} + +contains() { + grep -F -- "$1" "$output" > /dev/null || fail "missing: $1" +} + +excludes() { + if grep -F -- "$1" "$output" > /dev/null; then + fail "unexpected: $1" + fi +} + +run || fail 'default download URL' +contains 'https://download.docker.com/linux/' +echo 'PASS (offline): default download URL' + +for mirror in Aliyun AzureChinaCloud TencentCloud; do + case "$mirror" in + Aliyun) url='https://mirrors.aliyun.com/docker-ce' ;; + AzureChinaCloud) url='https://mirror.azure.cn/docker-ce' ;; + TencentCloud) url='https://mirrors.cloud.tencent.com/docker-ce' ;; + esac + run --mirror "$mirror" || fail "$mirror" + contains "$url/linux/" + excludes 'https://download.docker.com/linux/' + echo "PASS (offline): $mirror option selects expected URL" +done +cp "$output" "$tmp_dir/tencent-cloud" + +download_url='https://mirrors.cloud.tencent.com/docker-ce' +run || fail 'DOWNLOAD_URL compatibility' +cmp -s "$output" "$tmp_dir/tencent-cloud" || fail 'mirror and DOWNLOAD_URL differ' +echo 'PASS (offline): TencentCloud matches DOWNLOAD_URL' + +download_url='https://example.com/docker-ce' +run || fail 'custom DOWNLOAD_URL' +contains "$download_url/linux/" +echo 'PASS (offline): custom DOWNLOAD_URL' + +run --mirror TencentCloud || fail 'mirror precedence' +contains 'https://mirrors.cloud.tencent.com/docker-ce/linux/' +excludes "$download_url" +echo 'PASS (offline): --mirror overrides DOWNLOAD_URL' +download_url='' + +run --mirror TencentCloud --channel test || fail 'test channel' +contains 'https://mirrors.cloud.tencent.com/docker-ce/linux/' +grep -E ' test"|docker-ce-test' "$output" > /dev/null || fail 'test channel missing' +echo 'PASS (offline): test channel argument (not package availability)' + +run --mirror TencentCloud --setup-repo || fail 'repository-only mode' +contains 'https://mirrors.cloud.tencent.com/docker-ce/linux/' +excludes ' install docker-ce' +excludes 'systemctl enable --now docker.service' +echo 'PASS (offline): repository-only mode' + +run --mirror TencentCloud --no-autostart || fail 'no-autostart mode' +contains ' install docker-ce' +excludes 'systemctl enable --now docker.service' +echo 'PASS (offline): no-autostart mode' + +run --mirror TencentCloud --version 27.5 || fail 'version option' +contains 'VERSION pinning is not supported in DRY_RUN' +contains 'https://mirrors.cloud.tencent.com/docker-ce/linux/' +echo 'PASS (offline): version option (not version availability)' + +run --help || fail 'help output' +contains '--mirror ' +echo 'PASS (offline): help lists TencentCloud' + +status=0 +run --mirror UnknownMirror || status=$? +[ "$status" -eq 1 ] || fail 'unknown mirror should exit with status 1' +contains "unknown mirror 'UnknownMirror'" +contains "'Aliyun', 'AzureChinaCloud', or 'TencentCloud'" +echo 'PASS (offline): unknown mirror is rejected' + +# Test only the network check's entry guards here, never its live requests. +run_script ./scripts/test-tencent-cloud-mirror.sh ubuntu resolute || fail 'network check default' +contains 'SKIP:' +contains 'Tencent Cloud private network' +excludes 'PASS' +echo 'PASS (offline): network smoke check is skipped by default' + +network_test=yes +status=0 +run_script ./scripts/test-tencent-cloud-mirror.sh ubuntu resolute || status=$? +[ "$status" -eq 2 ] || fail 'invalid opt-in must fail before network access' +contains 'must be 0 or 1' +echo 'PASS (offline): invalid network opt-in is rejected' + +network_test=1 +status=0 +run_script ./scripts/test-tencent-cloud-mirror.sh || status=$? +[ "$status" -eq 2 ] || fail 'missing network-check arguments must be rejected' +contains 'Usage:' +echo 'PASS (offline): missing network-check arguments are rejected' + +status=0 +run_script ./scripts/test-tencent-cloud-mirror.sh centos 9 || status=$? +[ "$status" -eq 2 ] || fail 'unsupported smoke-check repository must be rejected' +contains 'Ubuntu and Debian APT repositories only' +echo 'PASS (offline): unsupported smoke-check repository is rejected' + +status=0 +run_script ./scripts/test-tencent-cloud-mirror.sh ubuntu '../resolute' || status=$? +[ "$status" -eq 2 ] || fail 'invalid repository codename must be rejected' +contains 'lowercase letters and digits' +echo 'PASS (offline): invalid repository codename is rejected' + +echo 'Offline checks complete. Tencent Cloud intranet connectivity and installation were NOT tested.' diff --git a/scripts/test-setup-repo.sh b/scripts/test-setup-repo.sh new file mode 100755 index 00000000..c999227b --- /dev/null +++ b/scripts/test-setup-repo.sh @@ -0,0 +1,150 @@ +#!/bin/sh +# Exercise the real option parser without entering the installation code. +# In particular, a broken --setup-repo must never turn this test into an install. +set -eu + +cd "$(dirname "$0")/.." +test_shell=${TEST_SHELL:-sh} +tmp_dir=$(mktemp -d) +trap 'rm -rf "$tmp_dir"' 0 +trap 'exit 1' HUP INT TERM + +# Keep the argument parser and mirror/channel validation verbatim. Stop before +# function definitions and do_install, so even a swallowed --dry-run is harmless. +# Fail closed if the boundary changes instead of executing the complete script. +awk ' + /^command_exists\(\) \{$/ { found = 1; exit } + { print } + END { if (!found) exit 1 } +' install.sh > "$tmp_dir/parser.sh" +cat >> "$tmp_dir/parser.sh" <<'PROBE' +printf 'REPO_ONLY=%s\nDRY_RUN=%s\nNO_AUTOSTART=%s\nCHANNEL=%s\nVERSION=%s\nDOWNLOAD_URL=%s\n' \ + "$REPO_ONLY" "$DRY_RUN" "$NO_AUTOSTART" "$CHANNEL" "$VERSION" "$DOWNLOAD_URL" +PROBE + +output="$tmp_dir/output" +dry_run='' +count=0 + +fail() { + printf 'FAIL (offline): %s\n' "$1" >&2 + cat "$output" >&2 + exit 1 +} + +run() { + expected_status=$1 + shift + status=0 + env -i PATH="$PATH" HOME="${HOME:-/tmp}" DRY_RUN="$dry_run" \ + "$test_shell" "$tmp_dir/parser.sh" "$@" > "$output" 2>&1 || status=$? + [ "$status" -eq "$expected_status" ] || fail "expected status $expected_status, got $status" +} + +contains() { + grep -F -- "$1" "$output" > /dev/null || fail "missing: $1" +} + +equals() { + grep -Fx -- "$1" "$output" > /dev/null || fail "missing exact value: $1" +} + +pass() { + count=$((count + 1)) + printf 'PASS (offline): %s\n' "$1" +} + +run 0 +equals 'REPO_ONLY=0' +equals 'DRY_RUN=' +pass 'defaults' + +run 0 --setup-repo +equals 'REPO_ONLY=1' +pass '--setup-repo as the only argument' + +run 0 --setup-repo --dry-run +equals 'REPO_ONLY=1' +equals 'DRY_RUN=1' +pass '--setup-repo preserves following --dry-run' + +run 0 --dry-run --setup-repo +equals 'REPO_ONLY=1' +equals 'DRY_RUN=1' +pass '--setup-repo as the last argument' + +run 0 --setup-repo --mirror Aliyun +equals 'REPO_ONLY=1' +equals 'DOWNLOAD_URL=https://mirrors.aliyun.com/docker-ce' +pass '--setup-repo preserves following --mirror and value' + +run 0 --mirror Aliyun --setup-repo +equals 'REPO_ONLY=1' +equals 'DOWNLOAD_URL=https://mirrors.aliyun.com/docker-ce' +pass 'mirror before --setup-repo' + +run 0 --setup-repo --channel test +equals 'REPO_ONLY=1' +equals 'CHANNEL=test' +pass '--setup-repo preserves following --channel and value' + +run 0 --setup-repo --version v27.5 +equals 'REPO_ONLY=1' +equals 'VERSION=27.5' +pass '--setup-repo preserves following --version and value' + +run 0 --setup-repo --no-autostart +equals 'REPO_ONLY=1' +equals 'NO_AUTOSTART=1' +pass '--setup-repo preserves following --no-autostart' + +run 0 --setup-repo --help +contains 'USAGE:' +if grep -F 'REPO_ONLY=' "$output" > /dev/null; then + fail '--help must exit before the parser probe' +fi +pass '--setup-repo preserves following --help' + +run 1 --setup-repo --unknown-option +contains 'Illegal option --unknown-option' +pass '--setup-repo does not hide an invalid option' + +run 0 --setup-repo --setup-repo --dry-run +equals 'REPO_ONLY=1' +equals 'DRY_RUN=1' +pass 'repeated --setup-repo remains idempotent' + +run 0 --setup-repo --dry-run --mirror Aliyun --channel test --version v27.5 --no-autostart +equals 'REPO_ONLY=1' +equals 'DRY_RUN=1' +equals 'DOWNLOAD_URL=https://mirrors.aliyun.com/docker-ce' +equals 'CHANNEL=test' +equals 'VERSION=27.5' +equals 'NO_AUTOSTART=1' +pass 'all options after --setup-repo' + +run 0 --dry-run --mirror Aliyun --channel test --version v27.5 --no-autostart --setup-repo +equals 'REPO_ONLY=1' +equals 'DRY_RUN=1' +equals 'DOWNLOAD_URL=https://mirrors.aliyun.com/docker-ce' +equals 'CHANNEL=test' +equals 'VERSION=27.5' +equals 'NO_AUTOSTART=1' +pass 'all options before --setup-repo' + +dry_run=1 +run 0 --setup-repo +equals 'REPO_ONLY=1' +equals 'DRY_RUN=1' +pass 'DRY_RUN environment variable remains supported' +dry_run='' + +run 1 --setup-repo --mirror UnknownMirror +contains "unknown mirror 'UnknownMirror'" +pass '--setup-repo does not hide mirror validation' + +run 1 --setup-repo --channel invalid +contains "unknown CHANNEL 'invalid'" +pass '--setup-repo does not hide channel validation' + +printf '%s offline parser scenarios passed; no network or installation was attempted.\n' "$count" diff --git a/scripts/test-tencent-cloud-mirror.sh b/scripts/test-tencent-cloud-mirror.sh new file mode 100755 index 00000000..d78a8313 --- /dev/null +++ b/scripts/test-tencent-cloud-mirror.sh @@ -0,0 +1,87 @@ +#!/bin/sh +# Opt-in network smoke check. Run only from a Tencent Cloud VM using its VPC +# private-network route. This is not a package installation or signature test. +set -eu + +usage() { + echo "Usage: TENCENT_CLOUD_INTRANET=1 sh $0 " + echo "Run only inside Tencent Cloud's private network, with the default VPC DNS." + echo "The environment variable is an operator confirmation, not cloud detection." +} + +if [ "${1:-}" = "--help" ]; then + usage + exit 0 +fi + +# Check the opt-in before looking up DNS, invoking curl, or creating files. +case "${TENCENT_CLOUD_INTRANET:-0}" in + 0|"") + echo 'SKIP: Tencent Cloud network smoke check is disabled; no network requests made.' + echo 'Run in the Tencent Cloud private network and set TENCENT_CLOUD_INTRANET=1.' + exit 0 + ;; + 1) ;; + *) + >&2 echo 'ERROR: TENCENT_CLOUD_INTRANET must be 0 or 1.' + exit 2 + ;; +esac + +if [ "$#" -ne 2 ]; then + usage >&2 + exit 2 +fi + +distro=$1 +codename=$2 +case "$distro" in + ubuntu|debian) ;; + *) + >&2 echo 'ERROR: this smoke check covers Ubuntu and Debian APT repositories only.' + exit 2 + ;; +esac +case "$codename" in + ""|*[!a-z0-9]*) + >&2 echo 'ERROR: use a distribution codename containing only lowercase letters and digits.' + exit 2 + ;; +esac + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + echo 'Check the Tencent Cloud VPC DNS/private-network route and repository availability.' >&2 + exit 1 +} + +command -v curl > /dev/null 2>&1 || fail 'curl is required' +echo 'NOTICE: run only inside the Tencent Cloud private network; no cloud or route detection is performed.' +echo 'NOTICE: proxies are bypassed; ensure the default Tencent Cloud VPC DNS is in use.' +umask 077 +tmp_dir=$(mktemp -d) +trap 'rm -rf "$tmp_dir"' 0 +trap 'exit 1' HUP INT TERM +base_url="https://mirrors.cloud.tencent.com/docker-ce/linux/$distro" + +fetch() { + # Ignore .curlrc and proxies; retain TLS verification. Do not follow redirects + # or silently fall back to a public endpoint. Bound each request's duration. + if ! http_code=$(curl -q --fail --silent --show-error --proto '=https' \ + --noproxy '*' --connect-timeout 5 --max-time 30 \ + --output "$tmp_dir/$2" --write-out '%{http_code}' "$base_url/$1"); then + fail "cannot download $base_url/$1" + fi + [ "$http_code" = '200' ] || fail "unexpected HTTP $http_code from $base_url/$1" + [ -s "$tmp_dir/$2" ] || fail "empty response from $base_url/$1" +} + +fetch gpg gpg +grep -Fqx -- '-----BEGIN PGP PUBLIC KEY BLOCK-----' "$tmp_dir/gpg" || fail 'unexpected GPG key response' +grep -Fqx -- '-----END PGP PUBLIC KEY BLOCK-----' "$tmp_dir/gpg" || fail 'incomplete GPG key response' +fetch "dists/$codename/Release" Release +grep -Fqx -- 'Origin: Docker' "$tmp_dir/Release" || fail 'unexpected repository metadata' +grep -Fqx -- "Codename: $codename" "$tmp_dir/Release" || fail 'repository codename mismatch' + +echo "PASS (network smoke only): $distro/$codename key and Release endpoints returned expected content markers." +echo 'NOT TESTED: cryptographic signatures, package downloads, installation, or Docker service startup.'