Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ name: docker-install CI
on: [push, pull_request]

jobs:
# Fixture-driven installer execution; no repository access or package changes.
test-apt-version-selection:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- name: Check APT version test syntax
run: sh -n scripts/test-apt-version-selection.sh
- name: Check APT package selection (offline)
run: |
sh scripts/test-apt-version-selection.sh
TEST_SHELL=bash sh scripts/test-apt-version-selection.sh

test:
runs-on: ubuntu-latest
timeout-minutes: 5
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,23 @@ To verify that the install script works amongst the supported operating systems
make shellcheck
```

### Offline APT version-selection regression checks

```shell
sh scripts/test-apt-version-selection.sh
TEST_SHELL=bash sh scripts/test-apt-version-selection.sh
```

These checks execute the installer's APT installation flow against package-list
fixtures and assert the selected Engine and CLI installation arguments. Platform
detection is fixed to Ubuntu noble and privileged commands are intercepted by a
strict command allowlist, so the checks do not access repositories, change the
host, or install packages. They cover version-field boundaries, literal matching,
epochs, partial versions, historical `-ce` releases, pre-releases, and unavailable
versions. CI runs the fixtures under both `sh` and `bash`. This does not establish
live repository availability or installation success; `--dry-run` still does not
resolve version pins.

## Legal
*Brought to you courtesy of our legal counsel. For more context,
please see the [NOTICE](NOTICE) document in this repo.*
Expand Down
27 changes: 21 additions & 6 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,23 @@ version_compare() (
return 0
)

# Select the first APT package version matching a literal Docker version prefix.
# Restrict matching to madison's version field, ignoring the epoch and distro
# suffix. Require a component boundary so 24.0.1 cannot select 24.0.10.
# Debian packages use "~" for Docker's "-ce" and pre-release separators.
# Repository ordering is preserved, including the existing test-channel behavior.
select_apt_package_version() (
# An embedded newline would give grep multiple expressions and could turn a
# later fragment into an unanchored match. Invalid pins must select nothing.
case "$1" in
*'
'*) return ;;
esac
pkg_pattern="$(printf '%s\n' "$1" | sed 's/-/~/g; s/[][\\.^$*+?(){}|]/\\&/g')"
awk -F '|' '{gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}' |
grep -E "^([0-9]+:)?$pkg_pattern([.~+-]|$)" | head -1
)

is_dry_run() {
if [ -z "$DRY_RUN" ]; then
return 1
Expand Down Expand Up @@ -617,12 +634,10 @@ do_install() {
if is_dry_run; then
echo "# WARNING: VERSION pinning is not supported in DRY_RUN"
else
# Will work for incomplete versions IE (17.12), but may not actually grab the "latest" if in the test channel
pkg_pattern="$(echo "$VERSION" | sed 's/-ce-/~ce~.*/g' | sed 's/-/.*/g')"
search_command="apt-cache madison docker-ce | grep '$pkg_pattern' | head -1 | awk '{\$1=\$1};1' | cut -d' ' -f 3"
search_command="apt-cache madison docker-ce"
echo "INFO: Searching repository for VERSION '$VERSION'"
echo "INFO: $search_command"
pkg_version="$($sh_c "$search_command")"
pkg_version="$($sh_c "$search_command" | select_apt_package_version "$VERSION")"
if [ -z "$pkg_version" ]; then
echo
echo "ERROR: '$VERSION' not found amongst apt-cache madison results"
Expand All @@ -632,9 +647,9 @@ do_install() {
pkg_version="=$pkg_version"

if version_gte "18.09"; then
search_command="apt-cache madison docker-ce-cli | grep '$pkg_pattern' | head -1 | awk '{\$1=\$1};1' | cut -d' ' -f 3"
search_command="apt-cache madison docker-ce-cli"
echo "INFO: $search_command"
cli_pkg_version="$($sh_c "$search_command")"
cli_pkg_version="$($sh_c "$search_command" | select_apt_package_version "$VERSION")"
if [ -n "$cli_pkg_version" ]; then
cli_pkg_version="=$cli_pkg_version"
fi
Expand Down
219 changes: 219 additions & 0 deletions scripts/test-apt-version-selection.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
#!/bin/sh
# Offline APT selection regression checks. Execute the installer's real install
# flow against package-list fixtures, recording its installation command. Platform
# detection is fixed to Ubuntu noble; every privileged command is intercepted.
# This is neither dry-run output nor a live package installation test.
set -eu

cd "$(dirname "$0")/.."
install_script=${INSTALL_SCRIPT:-./install.sh}
test_shell=$(command -v "${TEST_SHELL:-sh}")
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' 0
trap 'exit 1' HUP INT TERM
mkdir "$tmp_dir/bin"
output="$tmp_dir/output"
test_channel=stable

fail() {
printf 'FAIL: %s\n' "$1" >&2
cat "$output" >&2
[ ! -f "$tmp_dir/install-command" ] || cat "$tmp_dir/install-command" >&2
exit 1
}

# Load the entire installer, replacing only its final invocation with fixed
# platform detection. None of the version lookup or package-list code is copied.
[ "$(tail -n 1 "$install_script")" = 'do_install' ] || fail 'unexpected installer entry point'
sed '$d' "$install_script" > "$tmp_dir/install.sh"
cat >> "$tmp_dir/install.sh" <<'PLATFORM'
get_distribution() { echo ubuntu; }
check_forked() { :; }
is_wsl() { return 1; }
command_exists() { [ "$1" = lsb_release ]; }
do_install
PLATFORM

cat > "$tmp_dir/bin/id" <<'MOCK'
#!/bin/sh
[ "$*" = '-un' ] || exit 99
echo root
MOCK
cat > "$tmp_dir/bin/lsb_release" <<'MOCK'
#!/bin/sh
[ "$*" = '--codename' ] || exit 99
printf 'Codename:\tnoble\n'
MOCK
cat > "$tmp_dir/bin/dpkg" <<'MOCK'
#!/bin/sh
[ "$*" = '--print-architecture' ] || exit 99
echo amd64
MOCK
cat > "$tmp_dir/bin/apt-cache" <<'MOCK'
#!/bin/sh
case "$*" in
'madison docker-ce') cat "$TEST_FIXTURES/engine" ;;
'madison docker-ce-cli') cat "$TEST_FIXTURES/cli" ;;
*) echo "unexpected apt-cache arguments: $*" >> "$FORBIDDEN_COMMAND_LOG"; exit 99 ;;
esac
MOCK
# Intercept sh -c before commands or redirects can change the host. Only package
# lookups run through a real shell, with apt-cache replaced by the fixture reader.
cat > "$tmp_dir/bin/sh" <<'MOCK'
#!/bin/sh
if [ "$#" = 2 ] && [ "$1" = '-c' ]; then
case "$2" in
'apt-cache madison docker-ce'|'apt-cache madison docker-ce | '*|\
'apt-cache madison docker-ce-cli'|'apt-cache madison docker-ce-cli | '*)
exec /bin/sh "$@"
;;
'DEBIAN_FRONTEND=noninteractive apt-get -y -qq --allow-downgrades install docker-ce='*|\
'DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=60 -y -qq --allow-downgrades install docker-ce='*)
printf '%s\n' "$2" >> "$INSTALL_COMMAND_LOG"
exit 0
;;
'apt-get -qq update >/dev/null'|\
'DEBIAN_FRONTEND=noninteractive apt-get -y -qq install ca-certificates curl >/dev/null'|\
'DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=60 -y -qq install ca-certificates curl >/dev/null'|\
'install -m 0755 -d /etc/apt/keyrings'|\
'curl -fsSL "https://download.docker.com/linux/ubuntu/gpg" -o /etc/apt/keyrings/docker.asc'|\
'chmod a+r /etc/apt/keyrings/docker.asc'|\
'echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable" > /etc/apt/sources.list.d/docker.list'|\
'echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble test" > /etc/apt/sources.list.d/docker.list')
exit 0
;;
esac
fi
printf 'unexpected sh command: %s\n' "$*" >> "$FORBIDDEN_COMMAND_LOG"
exit 99
MOCK
cat > "$tmp_dir/bin/guard" <<'MOCK'
#!/bin/sh
printf 'unexpected command: %s %s\n' "${0##*/}" "$*" >> "$FORBIDDEN_COMMAND_LOG"
exit 99
MOCK
chmod +x "$tmp_dir/bin/"*
for tool in curl wget apt-get dnf dnf5 yum yum-config-manager systemctl sudo su docker install chmod; do
ln -s guard "$tmp_dir/bin/$tool"
done

fixture() {
: > "$tmp_dir/engine"
for version in "$@"; do
# Include both normal spaces and tabs around fields, as madison can use
# either; distro metadata contains the known false-positive version.
printf ' docker-ce\t| %s \t| https://download.docker.com/linux/ubuntu noble/stable amd64 Packages\n' "$version" >> "$tmp_dir/engine"
done
cp "$tmp_dir/engine" "$tmp_dir/cli"
}

run() {
status=0
: > "$tmp_dir/forbidden"
: > "$tmp_dir/install-command"
env -i PATH="$tmp_dir/bin:$PATH" HOME="${HOME:-/tmp}" \
TEST_FIXTURES="$tmp_dir" FORBIDDEN_COMMAND_LOG="$tmp_dir/forbidden" \
INSTALL_COMMAND_LOG="$tmp_dir/install-command" \
"$test_shell" "$tmp_dir/install.sh" --version "$1" --channel "$test_channel" --no-autostart > "$output" 2>&1 || status=$?
[ ! -s "$tmp_dir/forbidden" ] || fail 'installer executed an unexpected command'
}

expect_version() {
requested=$1
expected=$2
expected_cli=${3:-$expected}
run "$requested"
[ "$status" -eq 0 ] || fail "version $requested exited with $status"
[ "$(wc -l < "$tmp_dir/install-command" | tr -d ' ')" -eq 1 ] || fail 'expected one package installation command'
grep -F -- " install docker-ce=$expected " "$tmp_dir/install-command" > /dev/null || fail "version $requested selected the wrong Engine package"
# Legacy releases did not provide a separate CLI package.
case "$requested" in
17.*)
if grep -F -- 'docker-ce-cli' "$tmp_dir/install-command" > /dev/null; then
fail 'legacy version unexpectedly installs a separate CLI package'
fi
;;
*)
grep -F -- " docker-ce-cli=$expected_cli " "$tmp_dir/install-command" > /dev/null || fail "version $requested selected the wrong CLI package"
;;
esac
printf 'PASS (offline): %s selects %s\n' "$requested" "$expected"
}

expect_missing() {
run "$1"
[ "$status" -eq 1 ] || fail "unavailable version $1 should exit with status 1"
grep -F -- "ERROR: '$1' not found amongst apt-cache madison results" "$output" > /dev/null || fail "missing error for version $1"
[ ! -s "$tmp_dir/install-command" ] || fail 'unavailable version must not install packages'
printf 'PASS (offline): unavailable %s fails before installation\n' "$1"
}

fixture '5:28.4.0-1~ubuntu.24.04~noble' '5:24.0.9-1~ubuntu.24.04~noble'
expect_version 24.0 '5:24.0.9-1~ubuntu.24.04~noble'

fixture '5:24.0.10-1~ubuntu.24.04~noble' '5:24.0.1-1~ubuntu.24.04~noble'
expect_version 24.0.1 '5:24.0.1-1~ubuntu.24.04~noble'

fixture '5:22.02.1-1~ubuntu.24.04~noble' '5:22.0.1-1~ubuntu.24.04~noble'
expect_version 22.0 '5:22.0.1-1~ubuntu.24.04~noble'

fixture '5:24x0x1-1~ubuntu.24.04~noble' '5:24.0.1-1~ubuntu.24.04~noble'
expect_version 24.0.1 '5:24.0.1-1~ubuntu.24.04~noble'

# Check CLI independently: an Engine-only fix must still fail this fixture.
fixture '5:24.0.1-1~ubuntu.24.04~noble'
printf '%s\n' 'docker-ce-cli | 5:28.4.0-1~ubuntu.24.04~noble | noble/stable' > "$tmp_dir/cli"
cat "$tmp_dir/engine" >> "$tmp_dir/cli"
expect_version 24.0.1 '5:24.0.1-1~ubuntu.24.04~noble'

fixture '5:24.0.1-1~ubuntu.24.04~noble'
printf '%s\n' 'docker-ce-cli | 5:24.0.10-1~ubuntu.24.04~noble | noble/stable' > "$tmp_dir/cli"
cat "$tmp_dir/engine" >> "$tmp_dir/cli"
expect_version 24.0.1 '5:24.0.1-1~ubuntu.24.04~noble'

fixture '24.0.1-1~ubuntu.24.04~noble'
expect_version 24.0.1 '24.0.1-1~ubuntu.24.04~noble'
fixture '2:24.0.1-1~ubuntu.24.04~noble'
expect_version 24.0.1 '2:24.0.1-1~ubuntu.24.04~noble'
fixture '5:24.1.7-1~ubuntu.24.04~noble' '5:24.0.9-1~ubuntu.24.04~noble'
expect_version 24 '5:24.1.7-1~ubuntu.24.04~noble'
fixture '5:24.0.9-1~ubuntu.24.04~noble' '5:24.0.1-1~ubuntu.24.04~noble'
expect_version 24.0 '5:24.0.9-1~ubuntu.24.04~noble'
expect_version v24.0.1 '5:24.0.1-1~ubuntu.24.04~noble'

fixture '17.03.2~ce-0~ubuntu-xenial'
expect_version 17.03 '17.03.2~ce-0~ubuntu-xenial'
expect_version 17.03.2-ce '17.03.2~ce-0~ubuntu-xenial'
test_channel='test'
fixture '17.12.0~ce~rc10-0~ubuntu' '17.12.0~ce~rc1-0~ubuntu'
expect_version 17.12.0-ce-rc1 '17.12.0~ce~rc1-0~ubuntu'

fixture '5:29.0.0~rc.10-1~ubuntu.24.04~noble' '5:29.0.0~rc.1-1~ubuntu.24.04~noble'
expect_version 29.0.0-rc.1 '5:29.0.0~rc.1-1~ubuntu.24.04~noble'
expect_version 29.0.0~rc.1 '5:29.0.0~rc.1-1~ubuntu.24.04~noble'
fixture '5:29.0.0~alpha.1-1~ubuntu.24.04~noble'
expect_version 29.0.0-alpha.1 '5:29.0.0~alpha.1-1~ubuntu.24.04~noble'

test_channel=stable
fixture '5:28.4.0-1~ubuntu.24.04~noble'
expect_missing 24.0
fixture '5:24.0.10-1~ubuntu.24.04~noble'
expect_missing 24.0.1
fixture '5:22.02.1-1~ubuntu.24.04~noble'
expect_missing 22.0
test_channel='test'
fixture '5:29.0.0~rc.10-1~ubuntu.24.04~noble'
expect_missing 29.0.0-rc.1
test_channel=stable
fixture '5:24.01-1~ubuntu.24.04~noble'
expect_missing '24.0[1]'
# VERSION is never interpolated into the privileged shell command.
expect_missing "24.0'; echo unexpected; #"

# grep treats embedded newlines as separate expressions. A later fragment must
# not match distribution metadata and broaden the requested version.
fixture '5:29.4.0-1~ubuntu.24.04~noble'
expect_missing 'bogus
24'

echo 'Offline APT selection checks complete. Repository availability and real installation were NOT tested.'