diff --git a/COVERAGE.md b/COVERAGE.md index 0895e226..da242480 100644 --- a/COVERAGE.md +++ b/COVERAGE.md @@ -4,13 +4,58 @@ Measured on 2026-09-24 against upstream 19.x (b60dd23), following the project decision 0003 (90 percent floor per repository, 95 percent for every file our changes touch). -## Measured baseline on the default branch: 100 percent (2026-09-26) +## Measured baseline on the default branch: 100 percent (2026-09-28) Pull request #2 merged on 2026-09-26 (merge commit 5a0a381) and brought -`tests/coverage.sh` with it: conf/turnkey.d/postfix-local 17 of 17 lines under kcov, 100 percent, 7 bats. The gate in -`.github/workflows/tests.yml` is set to 100, the measured number rounded -down, and is only ever raised. The sections that follow record the state -before the merge. +`tests/coverage.sh` with it. Every file it measures is at 100 percent: + +| File | What it is | Measured | +| --- | --- | --- | +| `conf/turnkey.d/postfix-local` | the build-time postfix configuration | 100 percent, 17 of 17 lines, 7 bats | +| `conf/turnkey.d/dpkg-vendor` | points the dpkg vendor at Keel, and removes an inherited TurnKey origin | 100 percent, 7 of 7 lines, 16 bats | +| `conf/turnkey.d/apt-identity` | keeps the shipped apt User-Agent the one in force | 100 percent, 4 of 4 lines, 11 bats | + +34 bats, measured on 2026-09-29 with kcov 43 and bats 1.11. The gate in +`.github/workflows/tests.yml` is set to 100, the measured number, and is +only ever raised. The sections that follow record the state before the +first merge. + +## The apt and vendor identity of an image + +Three things used to tell an archive, or a bug reporting tool, that this +machine is a TurnKey appliance (Keel-Linux/common#6). None of the +assertions below reads back a file the code under test wrote: + +- **the vendor.** `conf/turnkey.d/dpkg-vendor` points the origins `default` + entry at the `Keel` file the matching overlay ships. Every verdict in + `tests/dpkg-vendor.bats` is an answer from the real `dpkg-vendor`, pointed + at the tree the script produced through dpkg's own `DPKG_ORIGINS_DIR`: + `--query Vendor`, `--query Bugs`, `--is`, `--derives-from`. The `Keel` + file keeps `Parent: Debian`, so `dpkg-dev` resolves the same vendor object + it did before and package building is unaffected. A `TurnKey` origin file + inherited from a parent layer is removed, so dpkg no longer knows that + vendor by name. +- **the apt User-Agent.** The header is a fixed file the overlay ships, + `/etc/apt/apt.conf.d/01keel`; `mk/turnkey.mk` and `mk/turnkey-desktop.mk` + no longer write a per-appliance `01turnkey`. Every verdict in + `tests/apt-identity.bats` is read off the wire: `tests/ua-recorder.py` + records the header a real `apt-get update` sent, over http and over TLS. + One test measures the hazard the conf script exists for: with a stale + `01turnkey` beside `01keel`, apt sends TurnKey's header, because + `apt.conf.d` is read in lexical order and the last assignment wins. +- **the source URIs.** The stanzas are extracted from `conf/bootstrap_apt` + itself, rendered with a build's variables and handed to apt, which is + asked with `apt-get indextargets` what it would fetch. No network. + +Refutations in both suites are written `run ! cmd`, never a bare `! cmd`. +bash does not apply errexit to a negated command, so a bare one that is +not the last command of its test passes whatever happens; as the last +command it does decide the test, because bats takes the last status as the +verdict. `run !` asserts wherever it stands, which is why it is the +convention. shellcheck grades the two cases differently: SC2314 is an error +for the inert one and a note otherwise. Of the three bare negations in +`tests/postfix-local.bats`, only line 96 is inert; lines 66 and 97 are last +in their tests. It is left for the pull request that owns that file. ## Baseline before the merge: 0 percent, nothing measured diff --git a/changes/turnkey.changelog b/changes/turnkey.changelog index 20fcef6f..1cdb7da9 100644 --- a/changes/turnkey.changelog +++ b/changes/turnkey.changelog @@ -1,5 +1,22 @@ turnkey-core-19.0 (1) turnkey; urgency=low + * apt no longer announces the appliance to every archive it contacts. The + per-appliance '/etc/apt/apt.conf.d/01turnkey', which carried a User-Agent + of 'TurnKey APT-HTTP/1.3 (turnkey----)', is + replaced by a fixed '/etc/apt/apt.conf.d/01keel' naming the distribution + and nothing else. A stale 01turnkey inherited from a parent layer would + win on lexical order, so conf/turnkey.d/apt-identity removes it. + + * 'dpkg-vendor' answers Keel, and bug reporting tools address + Keel-Linux/tracker rather than turnkeylinux/tracker. The TurnKey origin + file is no longer shipped, and one inherited from a parent layer is + removed; the new Keel one keeps 'Parent: Debian', so dpkg-dev behaves + exactly as before. + + * The TurnKey archive is fetched over https rather than plain http, in all + three suites and in the legacy sources.list written for pre-Trixie + releases. The Debian sources are unchanged. + * Upgraded base distribution to Debian 13.x/Trixie. * Replace TurnKey custom Debian-Installer based 'di-live' with new custom diff --git a/conf/bootstrap_apt b/conf/bootstrap_apt index 60c571d9..bd161f2b 100755 --- a/conf/bootstrap_apt +++ b/conf/bootstrap_apt @@ -205,7 +205,7 @@ if [[ $deb_ver -ge 13 ]]; then # Main repos cat > $SOURCES_LIST/sources.sources < $SOURCES_LIST/security.sources.sources < $SOURCES_LIST/turnkey-testing.sources < $SOURCES_LIST/sources.list < $SOURCES_LIST/security.sources.list < $SOURCES_LIST/$TKL_TESTING_LIST <&2; exit 1; } + +conf_dir="${APT_CONF_DIR:-/etc/apt/apt.conf.d}" + +[ -d "$conf_dir" ] || fatal "the apt configuration directory '$conf_dir' does not exist" +[ -f "$conf_dir/01keel" ] || fatal "'$conf_dir/01keel' is missing - it is shipped by overlays/turnkey.d/apt-identity" + +rm -f "$conf_dir/01turnkey" diff --git a/conf/turnkey.d/dpkg-vendor b/conf/turnkey.d/dpkg-vendor index 559fb87d..8065c047 100755 --- a/conf/turnkey.d/dpkg-vendor +++ b/conf/turnkey.d/dpkg-vendor @@ -1,7 +1,38 @@ #!/bin/bash -e # -# Creates required symlink so dpkg-vendor --query vendor -# returns the correct string +# Makes a vendor query answer Keel. +# +# dpkg reads the vendor of the running system from the 'default' entry of the +# origins directory (Dpkg::Vendor), which has to resolve to one of the files +# beside it. The Keel file itself is shipped by the matching overlay, +# overlays/turnkey.d/dpkg-vendor. Everything a vendor query answers comes out +# of the file this points at: the name dpkg-vendor reports, and the tracker +# that bug reporting tools address. It used to point at TurnKey, so both +# named TurnKey (Keel-Linux/common#6). +# +# The Keel file keeps "Parent: Debian", and that line is not decoration. +# There is no Dpkg::Vendor::Keel perl module, so Dpkg::Vendor falls back to +# the parent's object, Dpkg::Vendor::Debian, the one TurnKey resolved to: +# Debian's hardening defaults, update-buildflags hook and changelog +# handling. Without Parent it would fall to Dpkg::Vendor::Default instead. +# +# DPKG_ORIGINS_DIR is dpkg's own override of that directory, honoured here so +# tests/dpkg-vendor.bats can arrange a tree and then ask the real dpkg-vendor +# what it makes of it, rather than reading back the link this wrote. A build +# leaves it unset and the directory is /etc/dpkg/origins. + +fatal() { echo "'$(basename "$0")' Error: $*" >&2; exit 1; } + +origins="${DPKG_ORIGINS_DIR:-/etc/dpkg/origins}" +vendor="Keel" + +[ -d "$origins" ] || fatal "the dpkg origins directory '$origins' does not exist" +[ -f "$origins/$vendor" ] || fatal "'$origins/$vendor' is missing - it is shipped by overlays/turnkey.d/dpkg-vendor" + +rm -rf "$origins/default" +ln -s "$origins/$vendor" "$origins/default" -rm -rf /etc/dpkg/origins/default -ln -s /etc/dpkg/origins/TurnKey /etc/dpkg/origins/default +# An overlay only adds files, so a parent layer built before this change +# leaves its TurnKey origin file behind, and dpkg keeps knowing that vendor +# by name. Remove it, as apt-identity removes a stale 01turnkey. +rm -f "$origins/TurnKey" diff --git a/mk/turnkey-desktop.mk b/mk/turnkey-desktop.mk index 7e97153e..2de6b7a9 100644 --- a/mk/turnkey-desktop.mk +++ b/mk/turnkey-desktop.mk @@ -47,12 +47,15 @@ endef bootstrap/post += $(_bootstrap/post) # tag package management system with release package -# set /etc/turnkey_version and apt user-agent +# set /etc/turnkey_version +# +# The apt User-Agent is no longer written here, for the reason given in +# mk/turnkey.mk: overlays/turnkey.d/apt-identity ships it (Keel-Linux/common#6). define _root.patched/post - - # + + # # tagging package management system with release package - # setting /etc/turnkey_version and apt user-agent + # setting /etc/turnkey_version # @if [ -f $(FAB_PATH)/products/core/changelog ]; then \ echo $(FAB_SHARE_PATH)/make-release-deb.py $(FAB_PATH)/products/core/changelog $O/root.patched; \ @@ -62,9 +65,7 @@ define _root.patched/post echo $(FAB_SHARE_PATH)/make-release-deb.py ./changelog $O/root.patched; \ $(FAB_SHARE_PATH)/make-release-deb.py ./changelog $O/root.patched; \ turnkey_version=$$($(FAB_SHARE_PATH)/turnkey-version.py --dist=$(CODENAME) --tag=$(VERSION_TAG) ./changelog $(FAB_ARCH)); \ - turnkey_aptconf="Acquire::http::User-Agent \"TurnKey APT-HTTP/1.3 ($$turnkey_version)\";"; \ echo $$turnkey_version > $O/root.patched/etc/turnkey_version; \ - echo $$turnkey_aptconf > $O/root.patched/etc/apt/apt.conf.d/01turnkey; \ else \ echo; \ echo "WARNING: can't tag local release (./changelog doesn't exist)"; \ diff --git a/mk/turnkey.mk b/mk/turnkey.mk index 75baac19..1a483329 100644 --- a/mk/turnkey.mk +++ b/mk/turnkey.mk @@ -44,20 +44,24 @@ endef bootstrap/post += $(_bootstrap/post) # tag package management system with release package -# set /etc/turnkey_version and apt user-agent +# set /etc/turnkey_version +# +# The apt User-Agent is no longer written here. It used to carry the appliance +# and its version to every archive the machine ever contacted; it is now a +# fixed header naming the distribution and nothing else, shipped by +# overlays/turnkey.d/apt-identity as /etc/apt/apt.conf.d/01keel +# (Keel-Linux/common#6). define _root.patched/post - - # + + # # tagging package management system with release package - # setting /etc/turnkey_version and apt user-agent + # setting /etc/turnkey_version # @if [ -f ./changelog ]; then \ echo $(FAB_SHARE_PATH)/make-release-deb.py ./changelog $O/root.patched; \ $(FAB_SHARE_PATH)/make-release-deb.py ./changelog $O/root.patched; \ turnkey_version=$$($(FAB_SHARE_PATH)/turnkey-version.py --dist=$(CODENAME) --tag=$(VERSION_TAG) ./changelog $(FAB_ARCH)); \ - turnkey_aptconf="Acquire::http::User-Agent \"TurnKey APT-HTTP/1.3 ($$turnkey_version)\";"; \ echo $$turnkey_version > $O/root.patched/etc/turnkey_version; \ - echo $$turnkey_aptconf > $O/root.patched/etc/apt/apt.conf.d/01turnkey; \ else \ echo; \ echo "WARNING: can't tag local release (./changelog doesn't exist)"; \ diff --git a/overlays/turnkey.d/apt-identity/etc/apt/apt.conf.d/01keel b/overlays/turnkey.d/apt-identity/etc/apt/apt.conf.d/01keel new file mode 100644 index 00000000..153e221a --- /dev/null +++ b/overlays/turnkey.d/apt-identity/etc/apt/apt.conf.d/01keel @@ -0,0 +1,13 @@ +// The User-Agent apt announces to every archive it contacts. +// +// It names the distribution and nothing else. What used to stand here was +// /etc/apt/apt.conf.d/01turnkey, written per appliance by mk/turnkey.mk: +// +// Acquire::http::User-Agent "TurnKey APT-HTTP/1.3 (turnkey-wordpress-19.0-trixie-amd64)"; +// +// so deb.debian.org, security.debian.org, every mirror in between and anyone +// watching the connection were told which appliance this machine is and which +// version it runs, on every apt run it ever made. Keel-Linux/common#6. +// +// The https method of apt reads this same setting, so one line covers both. +Acquire::http::User-Agent "Keel APT-HTTP/1.3"; diff --git a/overlays/turnkey.d/dpkg-vendor/etc/dpkg/origins/Keel b/overlays/turnkey.d/dpkg-vendor/etc/dpkg/origins/Keel new file mode 100644 index 00000000..2536efd5 --- /dev/null +++ b/overlays/turnkey.d/dpkg-vendor/etc/dpkg/origins/Keel @@ -0,0 +1,4 @@ +Vendor: Keel +Vendor-URL: https://keellinux.org/ +Bugs: https://github.com/Keel-Linux/tracker/issues +Parent: Debian diff --git a/overlays/turnkey.d/dpkg-vendor/etc/dpkg/origins/TurnKey b/overlays/turnkey.d/dpkg-vendor/etc/dpkg/origins/TurnKey deleted file mode 100644 index 67c4695f..00000000 --- a/overlays/turnkey.d/dpkg-vendor/etc/dpkg/origins/TurnKey +++ /dev/null @@ -1,4 +0,0 @@ -Vendor: TurnKey -Vendor-URL: https://www.turnkeylinux.org/ -Bugs: https://github.com/turnkeylinux/tracker/issues -Parent: Debian diff --git a/tests/apt-identity.bats b/tests/apt-identity.bats new file mode 100644 index 00000000..22970667 --- /dev/null +++ b/tests/apt-identity.bats @@ -0,0 +1,258 @@ +#!/usr/bin/env bats +# Tests for the apt identity of an image: +# +# overlays/turnkey.d/apt-identity/etc/apt/apt.conf.d/01keel the User-Agent +# conf/turnkey.d/apt-identity keeps it the one in force +# conf/bootstrap_apt the source URIs +# +# Every User-Agent verdict is taken off the wire: a local server records the +# header a real apt-get update sent it, so what is asserted is what apt +# announces, not what the file says it should announce (docs/traps.md, +# "Asserting the configuration is not asserting the behaviour"). The source +# URIs are likewise read back from apt, which is asked what it would fetch +# from the stanzas conf/bootstrap_apt generates. +# +# Every refutation is written "run ! cmd", never a bare "! cmd": bash does not +# apply errexit to a negated command, so a bare one passes whatever happens +# and asserts nothing (measured; shellcheck SC2314 names it). + +bats_require_minimum_version 1.5.0 + +setup() { + TESTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")" && pwd)" + REPO="$(cd "$TESTS_DIR/.." && pwd)" + SCRIPT="$REPO/conf/turnkey.d/apt-identity" + SHIPPED="$REPO/overlays/turnkey.d/apt-identity/etc/apt/apt.conf.d" + BOOTSTRAP="$REPO/conf/bootstrap_apt" + RECORDER="$TESTS_DIR/ua-recorder.py" + + # a scratch apt root: everything apt reads and writes is under here + APTROOT="$BATS_TEST_TMPDIR/aptroot" + export APT_CONF_DIR="$APTROOT/etc/apt/apt.conf.d" + mkdir -p "$APT_CONF_DIR" "$APTROOT/etc/apt/sources.list.d" \ + "$APTROOT/var/lib/apt/lists/partial" \ + "$APTROOT/var/cache/apt/archives/partial" "$APTROOT/var/lib/dpkg" + : > "$APTROOT/var/lib/dpkg/status" + APT_CONFIG="$BATS_TEST_TMPDIR/apt.conf" + export APT_CONFIG + cat > "$APT_CONFIG" < "$UA_LOG" + RECORDER_PID= +} + +teardown() { + [ -n "${RECORDER_PID:-}" ] && kill "$RECORDER_PID" 2>/dev/null + return 0 +} + +# the file the overlay ships, in the scratch tree +ship_01keel() { + cp "$SHIPPED/01keel" "$APT_CONF_DIR/01keel" +} + +# start_recorder [CERTFILE]; sets PORT +start_recorder() { + local portfile="$BATS_TEST_TMPDIR/port" + rm -f "$portfile" + python3 "$RECORDER" "$UA_LOG" "$@" > "$portfile" & + RECORDER_PID=$! + local waited + for waited in $(seq 1 100); do + [ -s "$portfile" ] && break + sleep 0.1 + done + [ -n "$waited" ] || return 1 + [ -s "$portfile" ] || { + echo "the recorder never printed a port" >&2 + return 1 + } + PORT="$(cat "$portfile")" +} + +# the distinct User-Agent strings the recorder was sent +sent_user_agents() { + cut -f3 "$UA_LOG" | sort -u +} + +# --------------------------------------------------- what apt puts on the wire + +@test "apt announces Keel over http, and no appliance identity" { + ship_01keel + start_recorder + printf 'deb [trusted=yes] http://127.0.0.1:%s/debian trixie main\n' "$PORT" \ + > "$APTROOT/etc/apt/sources.list" + apt-get update >/dev/null 2>&1 || true + + [ -s "$UA_LOG" ] + run sent_user_agents + [ "$output" = "Keel APT-HTTP/1.3" ] +} + +@test "apt announces the same over https" { + ship_01keel + openssl req -x509 -newkey rsa:2048 -days 1 -nodes -subj /CN=localhost \ + -keyout "$BATS_TEST_TMPDIR/key.pem" -out "$BATS_TEST_TMPDIR/cert.pem" 2>/dev/null + cat "$BATS_TEST_TMPDIR/key.pem" "$BATS_TEST_TMPDIR/cert.pem" > "$BATS_TEST_TMPDIR/pair.pem" + start_recorder "$BATS_TEST_TMPDIR/pair.pem" + printf 'deb [trusted=yes] https://127.0.0.1:%s/debian trixie main\n' "$PORT" \ + > "$APTROOT/etc/apt/sources.list" + apt-get update -o Acquire::https::Verify-Peer=false \ + -o Acquire::https::Verify-Host=false >/dev/null 2>&1 || true + + [ -s "$UA_LOG" ] + run sent_user_agents + [ "$output" = "Keel APT-HTTP/1.3" ] +} + +@test "what apt announces names no appliance, no version and no codename" { + ship_01keel + start_recorder + printf 'deb [trusted=yes] http://127.0.0.1:%s/debian trixie main\n' "$PORT" \ + > "$APTROOT/etc/apt/sources.list" + apt-get update >/dev/null 2>&1 || true + + [ -s "$UA_LOG" ] + local ua + ua="$(sent_user_agents)" + # the leak was everything inside the parentheses of the old header: + # "TurnKey APT-HTTP/1.3 (turnkey-wordpress-19.0-trixie-amd64)" + [[ "$ua" != *"("* ]] + [[ "$ua" != *TurnKey* ]] + [[ "$ua" != *turnkey* ]] + [[ "$ua" != *wordpress* ]] + [[ "$ua" != *trixie* ]] + [[ "$ua" != *amd64* ]] + [[ "$ua" != *19.0* ]] +} + +@test "a stale 01turnkey from a parent layer beats 01keel until it is removed" { + ship_01keel + # what an appliance layer built on a core layer from before the change has + printf 'Acquire::http::User-Agent "TurnKey APT-HTTP/1.3 (turnkey-wordpress-19.0-trixie-amd64)";\n' \ + > "$APT_CONF_DIR/01turnkey" + start_recorder + printf 'deb [trusted=yes] http://127.0.0.1:%s/debian trixie main\n' "$PORT" \ + > "$APTROOT/etc/apt/sources.list" + apt-get update >/dev/null 2>&1 || true + run sent_user_agents + [ "$output" = "TurnKey APT-HTTP/1.3 (turnkey-wordpress-19.0-trixie-amd64)" ] + + # the conf script is what stops that + run "$SCRIPT" + [ "$status" -eq 0 ] + : > "$UA_LOG" + rm -rf "$APTROOT/var/lib/apt/lists" + mkdir -p "$APTROOT/var/lib/apt/lists/partial" + apt-get update >/dev/null 2>&1 || true + run sent_user_agents + [ "$output" = "Keel APT-HTTP/1.3" ] +} + +# ---------------------------------------------------------------- the script + +@test "the conf script is a no-op when only 01keel is there" { + ship_01keel + run "$SCRIPT" + [ "$status" -eq 0 ] + [ -f "$APT_CONF_DIR/01keel" ] + run ls "$APT_CONF_DIR" + [ "$output" = 01keel ] +} + +@test "the conf script refuses when the overlay file is missing" { + run "$SCRIPT" + [ "$status" -eq 1 ] + [[ "$output" == *01keel* ]] +} + +@test "the conf script refuses when there is no apt configuration directory" { + rm -rf "$APT_CONF_DIR" + run "$SCRIPT" + [ "$status" -eq 1 ] + [[ "$output" == *apt.conf.d* ]] +} + +# ------------------------------------------- the URIs the bootstrap generates + +# the body of the heredoc conf/bootstrap_apt writes to the named source file +stanza() { + sed -n "/^ *cat > \$SOURCES_LIST\/$1 <&2 + return 1 + } + local KEY_CODENAME=trixie CODENAME=trixie + local MIRROR_URL=http://deb.debian.org/debian + local SEC_MIRROR=http://security.debian.org/debian-security + local sec_repo=trixie-security + local tkl_apt_repo_enabled=yes tkl_apt_testing_enabled=yes + local debian_backports_enabled=yes + local SUPPORTED_ARCH=(amd64 arm64) + local debian_components=(main non-free-firmware) + eval "cat < "$APTROOT/etc/apt/sources.list.d/$f" + done +} + +@test "no source the bootstrap writes fetches the turnkey archive over plain http" { + write_all_sources + fetch_uris > "$BATS_TEST_TMPDIR/uris" + [ -s "$BATS_TEST_TMPDIR/uris" ] + run ! grep -q '^http://archive\.turnkeylinux\.org' "$BATS_TEST_TMPDIR/uris" +} + +@test "the turnkey archive is fetched over https, in all three suites" { + write_all_sources + run fetch_uris + [ "$status" -eq 0 ] + local suite + for suite in trixie trixie-security trixie-testing; do + grep -q "^https://archive\.turnkeylinux\.org/debian/dists/$suite/" <<< "$output" + done +} + +@test "the debian sources are left as Debian ships them" { + write_all_sources + run fetch_uris + [ "$status" -eq 0 ] + grep -q '^http://deb\.debian\.org/debian/dists/trixie/' <<< "$output" + grep -q '^http://security\.debian\.org/debian-security/dists/trixie-security/' <<< "$output" + grep -q '^http://deb\.debian\.org/debian/dists/trixie-backports/' <<< "$output" +} + +@test "the legacy sources.list the bootstrap writes for older releases is https too" { + # the pre deb822 branch of conf/bootstrap_apt, still reached for bookworm + grep -n 'deb .*archive\.turnkeylinux\.org' "$BOOTSTRAP" > "$BATS_TEST_TMPDIR/legacy" + [ -s "$BATS_TEST_TMPDIR/legacy" ] + run ! grep -q 'http://archive\.turnkeylinux\.org' "$BATS_TEST_TMPDIR/legacy" +} diff --git a/tests/coverage.sh b/tests/coverage.sh index e6a880b2..67ced292 100755 --- a/tests/coverage.sh +++ b/tests/coverage.sh @@ -1,18 +1,27 @@ #!/bin/bash -# Line coverage of conf/turnkey.d/postfix-local under the bats suite -# in this directory, measured with kcov. Exits 1 when the covered share of -# the script is below the threshold (default 95), 2 when a tool is missing. +# Line coverage of the shell this repository is measured on, with kcov +# (decision 0004). Exits 1 when any measured file is below the threshold +# (default 95, the bar for project-authored code), 2 when a tool is missing. # # tests/coverage.sh [THRESHOLD] (or COVERAGE_THRESHOLD in the environment) # -# COVERAGE_DIR keeps the kcov report (default: a temporary directory). -# Needs the Debian packages kcov and bats. +# COVERAGE_DIR keeps the kcov reports, one directory per measured file +# (default: a temporary directory). Needs the Debian packages kcov and bats; +# tests/apt-identity.bats also needs apt, openssl and python3, and +# tests/dpkg-vendor.bats needs dpkg-vendor from dpkg-dev. set -euo pipefail here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -target="$(cd "$here/.." && pwd)/conf/turnkey.d/postfix-local" +root="$(cd "$here/.." && pwd)" threshold="${1:-${COVERAGE_THRESHOLD:-95}}" +# Each entry is a measured file and the bats file that exercises it. +targets=( + "conf/turnkey.d/postfix-local:tests/postfix-local.bats" + "conf/turnkey.d/dpkg-vendor:tests/dpkg-vendor.bats" + "conf/turnkey.d/apt-identity:tests/apt-identity.bats" +) + for tool in kcov bats; do if ! command -v "$tool" >/dev/null; then echo "$tool not found (apt-get install $tool)" >&2 @@ -20,18 +29,30 @@ for tool in kcov bats; do fi done -report="${COVERAGE_DIR:-$(mktemp -d)}" -kcov --include-path="$target" "$report" bats "$here" +reports="${COVERAGE_DIR:-$(mktemp -d)}" +failed=0 + +for target in "${targets[@]}"; do + measured="${target%%:*}" + suite="${target#*:}" + name="$(basename "$measured")" + report="$reports/$name" + mkdir -p "$report" + kcov --include-path="$root/$measured" "$report" bats "$root/$suite" + + # with --include-path the report holds one file, so its first entry is ours + json="$(find "$report" -name coverage.json -not -path '*/kcov-merged/*' | head -1)" + percent="$(grep -o '"percent_covered": "[0-9.]*"' "$json" | head -1 | grep -o '[0-9.]*')" + covered="$(grep -o '"covered_lines": "[0-9]*"' "$json" | head -1 | grep -o '[0-9]*')" + total="$(grep -o '"total_lines": "[0-9]*"' "$json" | head -1 | grep -o '[0-9]*')" -# kcov names the sub directory after the command; with --include-path the -# report holds one file, so its first entry is ours -json="$(ls -t "$report"/*/coverage.json | grep -v /kcov-merged/ | head -1)" -percent="$(grep -o '"percent_covered": "[0-9.]*"' "$json" | head -1 | grep -o '[0-9.]*')" -covered="$(grep -o '"covered_lines": "[0-9]*"' "$json" | head -1 | grep -o '[0-9]*')" -total="$(grep -o '"total_lines": "[0-9]*"' "$json" | head -1 | grep -o '[0-9]*')" + echo "$name: $percent percent ($covered of $total lines) covered, threshold $threshold" + if ! awk -v p="$percent" -v t="$threshold" 'BEGIN { exit !(p + 0 >= t + 0) }'; then + echo "$name: coverage below threshold (report: $report)" >&2 + failed=1 + fi +done -echo "postfix-local: $percent percent ($covered of $total lines) covered, threshold $threshold" -if ! awk -v p="$percent" -v t="$threshold" 'BEGIN { exit !(p + 0 >= t + 0) }'; then - echo "coverage below threshold (report: $report)" >&2 +if [ "$failed" -ne 0 ]; then exit 1 fi diff --git a/tests/dpkg-vendor.bats b/tests/dpkg-vendor.bats new file mode 100644 index 00000000..af597a89 --- /dev/null +++ b/tests/dpkg-vendor.bats @@ -0,0 +1,178 @@ +#!/usr/bin/env bats +# Tests for conf/turnkey.d/dpkg-vendor and the origin file it selects, +# overlays/turnkey.d/dpkg-vendor/etc/dpkg/origins/Keel. +# +# No verdict here reads back the file the conf script wrote (docs/traps.md, +# "Asserting the configuration is not asserting the behaviour"). Every one is +# the answer the real dpkg-vendor gives when it is pointed at the tree the +# script produced, through dpkg's own DPKG_ORIGINS_DIR (Dpkg::Vendor). What is +# asserted is therefore what a bug reporting tool, dpkg-buildpackage or +# anything else asking "who is the vendor of this machine" is told. +# +# Refutations are written "run ! cmd", never a bare "! cmd": bash does not +# apply errexit to a negated command, so a bare one asserts nothing unless +# it happens to be the last command of its test. + +bats_require_minimum_version 1.5.0 + +setup() { + TESTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")" && pwd)" + REPO="$(cd "$TESTS_DIR/.." && pwd)" + SCRIPT="$REPO/conf/turnkey.d/dpkg-vendor" + SHIPPED="$REPO/overlays/turnkey.d/dpkg-vendor/etc/dpkg/origins" + + # the origins directory of an image being built: what the overlay ships, + # plus the Debian file dpkg itself installs, which Parent: resolves to + export DPKG_ORIGINS_DIR="$BATS_TEST_TMPDIR/origins" + mkdir -p "$DPKG_ORIGINS_DIR" + cp -a "$SHIPPED"/. "$DPKG_ORIGINS_DIR/" + cat > "$DPKG_ORIGINS_DIR/debian" <<'DEBIAN' +Vendor: Debian +Vendor-URL: https://www.debian.org/ +Bugs: debbugs://bugs.debian.org +DEBIAN + + # DEB_VENDOR would override the default symlink, which is the thing under + # test; dpkg-vendor is the real one from dpkg-dev + unset DEB_VENDOR + command -v dpkg-vendor >/dev/null || { + echo "dpkg-vendor not found (apt-get install dpkg-dev)" >&2 + return 1 + } +} + +# the real dpkg-vendor, reading the tree the conf script just arranged +vendor() { + env -u DEB_VENDOR DPKG_ORIGINS_DIR="$DPKG_ORIGINS_DIR" dpkg-vendor "$@" +} + +# --------------------------------------------------------- what it answers + +@test "a vendor query answers Keel" { + run "$SCRIPT" + [ "$status" -eq 0 ] + run vendor --query Vendor + [ "$status" -eq 0 ] + [ "$output" = Keel ] +} + +@test "a vendor query does not answer TurnKey" { + "$SCRIPT" + run vendor --query Vendor + [ "$output" != TurnKey ] + run vendor --is TurnKey + [ "$status" -ne 0 ] +} + +@test "the vendor is Keel by dpkg's own --is test" { + "$SCRIPT" + run vendor --is Keel + [ "$status" -eq 0 ] +} + +@test "bug reports are addressed to our own tracker" { + "$SCRIPT" + run vendor --query Bugs + [ "$status" -eq 0 ] + [ "$output" = "https://github.com/Keel-Linux/tracker/issues" ] +} + +@test "nothing a vendor query answers names a turnkeylinux host" { + "$SCRIPT" + for field in Vendor Vendor-URL Bugs Parent; do + run vendor --query "$field" + [[ "$output" != *turnkeylinux* ]] + done +} + +@test "the vendor URL is our own site" { + "$SCRIPT" + run vendor --query Vendor-URL + [ "$output" = "https://keellinux.org/" ] +} + +@test "the vendor still derives from Debian, so dpkg-dev behaves as before" { + "$SCRIPT" + run vendor --derives-from Debian + [ "$status" -eq 0 ] +} + +@test "the vendor does not claim to derive from Ubuntu" { + "$SCRIPT" + run vendor --derives-from Ubuntu + [ "$status" -ne 0 ] +} + +# ------------------------------------------------------------- the script + +@test "the TurnKey origin file is not shipped at all" { + [ ! -e "$SHIPPED/TurnKey" ] + [ -f "$SHIPPED/Keel" ] +} + +@test "running it twice leaves the same answer" { + "$SCRIPT" + "$SCRIPT" + run vendor --query Vendor + [ "$status" -eq 0 ] + [ "$output" = Keel ] +} + +@test "an inherited default pointing at TurnKey is replaced" { + # the upgrade path: a parent layer built before this change + printf 'Vendor: TurnKey\nVendor-URL: https://www.turnkeylinux.org/\nBugs: https://github.com/turnkeylinux/tracker/issues\nParent: Debian\n' \ + > "$DPKG_ORIGINS_DIR/TurnKey" + ln -sf "$DPKG_ORIGINS_DIR/TurnKey" "$DPKG_ORIGINS_DIR/default" + run vendor --query Vendor + [ "$output" = TurnKey ] + "$SCRIPT" + run vendor --query Vendor + [ "$output" = Keel ] +} + +@test "an inherited TurnKey origin file is removed, not only unselected" { + # an overlay only adds, so a parent layer built before this change + # leaves its TurnKey file on the image; dpkg still knows that vendor by + # name until the file is gone + printf 'Vendor: TurnKey\nVendor-URL: https://www.turnkeylinux.org/\nBugs: https://github.com/turnkeylinux/tracker/issues\nParent: Debian\n' \ + > "$DPKG_ORIGINS_DIR/TurnKey" + ln -sf "$DPKG_ORIGINS_DIR/TurnKey" "$DPKG_ORIGINS_DIR/default" + run dpkg-vendor --vendor TurnKey --query Bugs + [ "$output" = https://github.com/turnkeylinux/tracker/issues ] + "$SCRIPT" + run dpkg-vendor --vendor TurnKey --query Bugs + [ "$status" -ne 0 ] + [[ "$output" == *"vendor TurnKey doesn't exist"* ]] + run vendor --query Vendor + [ "$output" = Keel ] +} + +@test "a default that is a regular file rather than a symlink is replaced" { + cp "$DPKG_ORIGINS_DIR/Keel" "$DPKG_ORIGINS_DIR/default" + printf 'Vendor: Whoever\n' > "$DPKG_ORIGINS_DIR/default" + "$SCRIPT" + run vendor --query Vendor + [ "$output" = Keel ] +} + +@test "a default that is a directory is replaced" { + mkdir -p "$DPKG_ORIGINS_DIR/default" + "$SCRIPT" + run vendor --query Vendor + [ "$output" = Keel ] +} + +@test "it refuses when the origin file it points at is missing" { + rm -f "$DPKG_ORIGINS_DIR/Keel" + run "$SCRIPT" + [ "$status" -eq 1 ] + [[ "$output" == *Keel* ]] + [ ! -e "$DPKG_ORIGINS_DIR/default" ] +} + +@test "it refuses when the origins directory is missing" { + rm -rf "$DPKG_ORIGINS_DIR" + run "$SCRIPT" + [ "$status" -eq 1 ] + [[ "$output" == *origins* ]] +} diff --git a/tests/ua-recorder.py b/tests/ua-recorder.py new file mode 100755 index 00000000..6a9859a7 --- /dev/null +++ b/tests/ua-recorder.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Records the User-Agent of every request it is sent, and answers 404. + +Used by tests/apt-identity.bats to read off the wire what apt announces to an +archive it contacts, rather than reading back the configuration file that was +supposed to make it announce that (docs/traps.md, "Asserting the +configuration is not asserting the behaviour"). + + ua-recorder.py LOGFILE [CERTFILE] + +Listens on 127.0.0.1 on a port the kernel picks, prints that port on stdout as +one line, and then serves until it is killed. With CERTFILE (a PEM holding +both the key and the certificate) it speaks HTTPS instead of HTTP, which is +how the same verdict is taken for the https method of apt. + +Every request appends one line to LOGFILE: + + METHODPATHUSER-AGENT + +404 is a perfectly good answer here: apt only has to send the request for its +User-Agent to be on the wire, and a server with nothing in it keeps the test +free of fixtures. +""" + +import http.server +import socketserver +import ssl +import sys + + +def main() -> int: + if not 2 <= len(sys.argv) <= 3: + print(__doc__, file=sys.stderr) + return 2 + + log = open(sys.argv[1], "a", buffering=1, encoding="utf-8") + + class Handler(http.server.BaseHTTPRequestHandler): + # HTTP/1.0 so every request stands alone and nothing is held open + protocol_version = "HTTP/1.0" + + def _record(self) -> None: + log.write( + "%s\t%s\t%s\n" + % (self.command, self.path, self.headers.get("User-Agent", "-")) + ) + self.send_response(404) + self.send_header("Content-Length", "0") + self.end_headers() + + do_GET = _record + do_HEAD = _record + + def log_message(self, *args) -> None: + pass # the access log is the file above, not stderr + + socketserver.TCPServer.allow_reuse_address = True + server = socketserver.TCPServer(("127.0.0.1", 0), Handler) + if len(sys.argv) == 3: + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.load_cert_chain(sys.argv[2]) + server.socket = context.wrap_socket(server.socket, server_side=True) + + print(server.server_address[1], flush=True) + try: + server.serve_forever() + except KeyboardInterrupt: + pass + return 0 + + +if __name__ == "__main__": + sys.exit(main())