From e6e7dc46e6021c3f9f95a2c095eee922cfad1be8 Mon Sep 17 00:00:00 2001 From: Su Yang Date: Mon, 5 Oct 2026 19:00:04 +0800 Subject: [PATCH 1/3] install: add Tencent Cloud package mirror Add --mirror TencentCloud using the existing DOWNLOAD_URL mechanism and https://mirrors.cloud.tencent.com/docker-ce. Update the usage text, unknown-mirror diagnostic, and README with mirror selection examples. Add offline dry-run regression tests for the default URL, existing mirrors, environment-variable compatibility and precedence, supported option combinations, help output, and invalid mirror names. Run these tests in the existing CI workflow. Signed-off-by: Su Yang --- .github/workflows/ci.yml | 2 + README.md | 44 +++++++++++++++++ install.sh | 15 ++++-- scripts/test-mirrors.sh | 104 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 5 deletions(-) create mode 100755 scripts/test-mirrors.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ae499a0..911fa6c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,8 @@ jobs: steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 + - name: Check mirrors (dry-run) + run: sh scripts/test-mirrors.sh - name: Shellcheck env: GH_TOKEN: ${{ github.token }} diff --git a/README.md b/README.md index 9135fb86..ed1be215 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,43 @@ 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` | + +For example, on a Tencent Cloud host 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 +``` + +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 +76,13 @@ To verify that the install script works amongst the supported operating systems make shellcheck ``` +To test mirror selection on a supported Linux distribution without installing +packages or contacting the mirrors: + +```shell +sh scripts/test-mirrors.sh +``` + ## 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..3cf3908c 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 } @@ -194,10 +196,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..f690bbcd --- /dev/null +++ b/scripts/test-mirrors.sh @@ -0,0 +1,104 @@ +#!/bin/sh +# Exercise mirror selection without installing packages or accessing mirrors. +# Run on a supported Linux distribution with sh scripts/test-mirrors.sh. +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" +output="$tmp_dir/output" +download_url='' + +run() { + env -i PATH="$tmp_dir/bin:$PATH" HOME="${HOME:-/tmp}" \ + DOWNLOAD_URL="$download_url" \ + "$test_shell" ./install.sh --dry-run "$@" > "$output" 2>&1 +} + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + cat "$output" >&2 + exit 1 +} + +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: 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: $mirror" +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: TencentCloud matches DOWNLOAD_URL' + +download_url='https://example.com/docker-ce' +run || fail 'custom DOWNLOAD_URL' +contains "$download_url/linux/" +echo 'PASS: custom DOWNLOAD_URL' + +run --mirror TencentCloud || fail 'mirror precedence' +contains 'https://mirrors.cloud.tencent.com/docker-ce/linux/' +excludes "$download_url" +echo 'PASS: --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: test channel' + +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: repository-only mode' + +run --mirror TencentCloud --no-autostart || fail 'no-autostart mode' +contains ' install docker-ce' +excludes 'systemctl enable --now docker.service' +echo 'PASS: 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: version option' + +run --help || fail 'help output' +contains '--mirror ' +echo 'PASS: 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: unknown mirror is rejected' From 468696018b54e7e80061c3144918effabab26a6f Mon Sep 17 00:00:00 2001 From: Su Yang Date: Mon, 5 Oct 2026 19:16:56 +0800 Subject: [PATCH 2/3] test: separate Tencent Cloud intranet checks from offline mirror tests Keep mirror argument and dry-run coverage network-free in a dedicated CI job, with guarded download/package/service commands and explicit offline result labels. Test the live check's opt-in and argument guards offline. Add an opt-in Ubuntu/Debian network smoke check for manual execution on a Tencent Cloud VM using the VPC private-network route. Skip before any network access unless explicitly enabled; fail on request/content errors. Do not install packages, follow redirects, disable TLS verification, or silently fall back to another mirror. Document VPC DNS, private-network validation, smoke-test limits and fresh-VM installation acceptance. Clarify that the chosen mirror domain is a unified public/private domain, rather than claiming it is universally intranet-only. Validated locally: 54 offline assertions across dash, bash and BusyBox ash; 27 mock-transport cases; syntax/YAML checks; swallowed-downloader mutation. No live Tencent Cloud requests or installation validation performed. Signed-off-by: Su Yang --- .github/workflows/ci.yml | 18 ++++- README.md | 82 +++++++++++++++++++++-- scripts/test-mirrors.sh | 99 ++++++++++++++++++++++------ scripts/test-tencent-cloud-mirror.sh | 87 ++++++++++++++++++++++++ 4 files changed, 261 insertions(+), 25 deletions(-) create mode 100755 scripts/test-tencent-cloud-mirror.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 911fa6c1..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 @@ -19,8 +35,6 @@ jobs: steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 - - name: Check mirrors (dry-run) - run: sh scripts/test-mirrors.sh - name: Shellcheck env: GH_TOKEN: ${{ github.token }} diff --git a/README.md b/README.md index ed1be215..044a0634 100644 --- a/README.md +++ b/README.md @@ -41,14 +41,26 @@ Use `--mirror` to select a Docker package mirror: | `AzureChinaCloud` | `https://mirror.azure.cn/docker-ce` | | `TencentCloud` | `https://mirrors.cloud.tencent.com/docker-ce` | -For example, on a Tencent Cloud host running a supported Linux distribution, -review the script and preview the installation before running it as root: +**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 @@ -76,13 +88,75 @@ To verify that the install script works amongst the supported operating systems make shellcheck ``` -To test mirror selection on a supported Linux distribution without installing -packages or contacting the mirrors: +### 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/scripts/test-mirrors.sh b/scripts/test-mirrors.sh index f690bbcd..2d9786cb 100755 --- a/scripts/test-mirrors.sh +++ b/scripts/test-mirrors.sh @@ -1,6 +1,8 @@ #!/bin/sh -# Exercise mirror selection without installing packages or accessing mirrors. -# Run on a supported Linux distribution with sh scripts/test-mirrors.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")/.." @@ -13,14 +15,22 @@ trap 'exit 1' HUP INT TERM 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='' - -run() { - env -i PATH="$tmp_dir/bin:$PATH" HOME="${HOME:-/tmp}" \ - DOWNLOAD_URL="$download_url" \ - "$test_shell" ./install.sh --dry-run "$@" > "$output" 2>&1 -} +network_test='' fail() { printf 'FAIL: %s\n' "$1" >&2 @@ -28,6 +38,22 @@ fail() { 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" } @@ -40,7 +66,7 @@ excludes() { run || fail 'default download URL' contains 'https://download.docker.com/linux/' -echo 'PASS: default download URL' +echo 'PASS (offline): default download URL' for mirror in Aliyun AzureChinaCloud TencentCloud; do case "$mirror" in @@ -51,54 +77,89 @@ for mirror in Aliyun AzureChinaCloud TencentCloud; do run --mirror "$mirror" || fail "$mirror" contains "$url/linux/" excludes 'https://download.docker.com/linux/' - echo "PASS: $mirror" + 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: TencentCloud matches DOWNLOAD_URL' +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: custom DOWNLOAD_URL' +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: --mirror overrides 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: test channel' +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: repository-only mode' +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: no-autostart mode' +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: version option' +echo 'PASS (offline): version option (not version availability)' run --help || fail 'help output' contains '--mirror ' -echo 'PASS: help lists TencentCloud' +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: unknown mirror is rejected' +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-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.' From f25407a70dfc1bbf619bc25fec0d4191b5413f8c Mon Sep 17 00:00:00 2001 From: Su Yang Date: Mon, 5 Oct 2026 21:17:22 +0800 Subject: [PATCH 3/3] install: preserve options following --setup-repo Remove the extra shift from the valueless --setup-repo option. The loop already consumes that option, so the additional shift discards the next argument. In particular, --setup-repo --dry-run loses dry-run protection and may execute prerequisite installation and repository configuration. Add 17 offline parser regression scenarios and a dedicated sh/bash CI workflow. Execute only the real argument parser and mirror/channel validation, never installation functions, including on the buggy baseline. Keep the regression fix isolated from the TencentCloud preset so it can be reviewed or cherry-picked independently. Validated locally: 17 parser and 18 existing mirror scenarios under sh, bash, and BusyBox ash on each of the TencentCloud and fork-main baselines (210 successful scenario executions). Both unpatched baselines fail the new dry-run-order regression. Shell syntax and YAML checks also passed. No live Tencent Cloud connectivity or installation validation performed. Signed-off-by: Su Yang --- .github/workflows/setup-repo.yml | 21 +++++ install.sh | 1 - scripts/test-setup-repo.sh | 150 +++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/setup-repo.yml create mode 100755 scripts/test-setup-repo.sh 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/install.sh b/install.sh index 3cf3908c..0a0b148f 100755 --- a/install.sh +++ b/install.sh @@ -171,7 +171,6 @@ while [ $# -gt 0 ]; do ;; --setup-repo) REPO_ONLY=1 - shift ;; --no-autostart) NO_AUTOSTART=1 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"