diff --git a/docs/recipes.md b/docs/recipes.md index 1e62d48..57c2678 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -154,6 +154,20 @@ without `git clean`. The hostname line uses `__PACKER_RECIPE_NAME__`, replaced b the same script, so the preseed files stay identical across Debian releases (enforced by `tests/recipe-consistency.test.ts`). +An answer that only one release needs goes on that recipe's `boot_command` +instead, where the installer reads it as a preseed value, but only for a key +`preseed.cfg` does not set. The installer loads the file after applying boot +parameters, so the file wins any key both name. `debian-11` passes +`apt-setup/services-select=updates` there: the bullseye-security index still +lists packages whose binaries security.debian.org no longer serves, so +selecting the security suite fails the package step. + +The package mirror is a key the preseed does set, so it is a placeholder +instead: `__PACKER_DEBIAN_MIRROR__`, filled by `scripts/inject-placeholders.sh` +from the recipe's `# mirror:` header and defaulting to `deb.debian.org`. A +release Debian has archived sets `# mirror: archive.debian.org`, which keeps +serving it after `deb.debian.org` drops it. + ## AlmaLinux and Rocky Linux kickstart Copy the nearest `ks.cfg` and update repository and release details. These builds diff --git a/recipes/debian-11.pkr.hcl b/recipes/debian-11.pkr.hcl index 8c42f0b..6dc42a0 100644 --- a/recipes/debian-11.pkr.hcl +++ b/recipes/debian-11.pkr.hcl @@ -1,5 +1,6 @@ # display: Debian 11 (Bullseye) # group: debian +# mirror: archive.debian.org # build_vmid: 4000 # min_cores: 1 # min_memory: 1024 @@ -173,6 +174,17 @@ source "proxmox-iso" "debian-11" { "console-setup/ask_detect=false ", "console-keymaps-at/keymap=us ", "grub-installer/bootdev=/dev/sda ", + # Install from main and bullseye-updates only. Debian has removed every + # bullseye binary from security.debian.org while the bullseye-security + # index still lists them, so selecting that suite makes pkgsel fetch 404s + # and the install stops at "Select and install software". Passed here + # rather than in preseed.cfg, which the Debian recipes share byte for + # byte; this works only because preseed.cfg never sets the key, since the + # installer loads that file after the boot parameters and it would win. + # The mirror is archive.debian.org (`# mirror:` above), where bullseye + # stays after deb.debian.org drops it. Bullseye's cloud.cfg sets + # apt_preserve_sources_list, so clones keep this sources.list. + "apt-setup/services-select=updates ", "", ] @@ -208,6 +220,17 @@ build { ] } + # A mirror that silently fell back to deb.debian.org would build fine today + # and break the day Debian drops bullseye from it, so check what landed. + provisioner "shell" { + inline = [ + "echo '==> Verifying apt sources'", + "cat /etc/apt/sources.list", + "grep -Eq '^deb http://archive.debian.org/debian/? bullseye main' /etc/apt/sources.list || { echo 'sources.list does not use archive.debian.org' >&2; exit 1; }", + "! grep -Eq '^deb .*bullseye-security' /etc/apt/sources.list || { echo 'sources.list still enables bullseye-security' >&2; exit 1; }", + ] + } + provisioner "file" { source = "${path.root}/_shared/cloud-init-cleanup.sh" destination = "/tmp/cloud-init-cleanup.sh" diff --git a/recipes/debian-11/http/preseed.cfg b/recipes/debian-11/http/preseed.cfg index e1d9bc0..9fca11f 100644 --- a/recipes/debian-11/http/preseed.cfg +++ b/recipes/debian-11/http/preseed.cfg @@ -10,7 +10,7 @@ d-i netcfg/get_hostname string packer-__PACKER_RECIPE_NAME__ d-i netcfg/wireless_wep string d-i hw-detect/load_firmware boolean false d-i mirror/country string manual -d-i mirror/http/hostname string deb.debian.org +d-i mirror/http/hostname string __PACKER_DEBIAN_MIRROR__ d-i mirror/http/directory string /debian d-i mirror/http/proxy string d-i passwd/root-login boolean false diff --git a/recipes/debian-12/http/preseed.cfg b/recipes/debian-12/http/preseed.cfg index e1d9bc0..9fca11f 100644 --- a/recipes/debian-12/http/preseed.cfg +++ b/recipes/debian-12/http/preseed.cfg @@ -10,7 +10,7 @@ d-i netcfg/get_hostname string packer-__PACKER_RECIPE_NAME__ d-i netcfg/wireless_wep string d-i hw-detect/load_firmware boolean false d-i mirror/country string manual -d-i mirror/http/hostname string deb.debian.org +d-i mirror/http/hostname string __PACKER_DEBIAN_MIRROR__ d-i mirror/http/directory string /debian d-i mirror/http/proxy string d-i passwd/root-login boolean false diff --git a/recipes/debian-13/http/preseed.cfg b/recipes/debian-13/http/preseed.cfg index e1d9bc0..9fca11f 100644 --- a/recipes/debian-13/http/preseed.cfg +++ b/recipes/debian-13/http/preseed.cfg @@ -10,7 +10,7 @@ d-i netcfg/get_hostname string packer-__PACKER_RECIPE_NAME__ d-i netcfg/wireless_wep string d-i hw-detect/load_firmware boolean false d-i mirror/country string manual -d-i mirror/http/hostname string deb.debian.org +d-i mirror/http/hostname string __PACKER_DEBIAN_MIRROR__ d-i mirror/http/directory string /debian d-i mirror/http/proxy string d-i passwd/root-login boolean false diff --git a/scripts/inject-placeholders.sh b/scripts/inject-placeholders.sh index 2ddb019..73696bf 100755 --- a/scripts/inject-placeholders.sh +++ b/scripts/inject-placeholders.sh @@ -21,6 +21,13 @@ rm -f "$VARS_FILE" RECIPE_DIR="recipes/${RECIPE}" +# Debian package mirror, from the recipe's `# mirror:` header. It has to reach +# the preseed file rather than the boot command: the installer loads the file +# with debconf-set-selections after applying boot parameters, so a key the +# shared preseed sets would silently override a boot-command value. +DEBIAN_MIRROR="$(sed -nE 's/^#[[:space:]]*mirror:[[:space:]]*([^[:space:]]+).*/\1/p' "recipes/${RECIPE}.pkr.hcl" 2>/dev/null | head -n1 || true)" +DEBIAN_MIRROR="${DEBIAN_MIRROR:-deb.debian.org}" + # ── Detect installer files and generate ephemeral SSH keypair ──────────────── PRESEED="${RECIPE_DIR}/http/preseed.cfg" USER_DATA="${RECIPE_DIR}/http/user-data" @@ -47,6 +54,7 @@ if [ "$NEEDS_KEY" = "1" ]; then "s|__PACKER_SSH_PUBLIC_KEY__|${PUB_KEY}|g; \ s|ssh-ed25519 AAAA[^ ]+ packer-${RECIPE}-[^ '\"]*|${PUB_KEY}|g; \ s|__PACKER_RECIPE_NAME__|${RECIPE}|g; \ + s|__PACKER_DEBIAN_MIRROR__|${DEBIAN_MIRROR}|g; \ s|__PACKER_BUILD_IP__|${CF_BUILD_IP:-}|g; \ s|__PACKER_BUILD_GW__|${CF_BUILD_GW:-}|g; \ s|__PACKER_BUILD_DNS__|${CF_BUILD_DNS:-1.1.1.1}|g" \ diff --git a/tests/inject-placeholders.test.ts b/tests/inject-placeholders.test.ts index 69a570b..f78b19e 100644 --- a/tests/inject-placeholders.test.ts +++ b/tests/inject-placeholders.test.ts @@ -33,6 +33,7 @@ describe('inject-placeholders.sh', () => { preseed, [ 'd-i netcfg/get_hostname string packer-__PACKER_RECIPE_NAME__', + 'd-i mirror/http/hostname string __PACKER_DEBIAN_MIRROR__', 'd-i netcfg/get_ipaddress string __PACKER_BUILD_IP__', 'd-i netcfg/get_gateway string __PACKER_BUILD_GW__', 'd-i netcfg/get_nameservers string __PACKER_BUILD_DNS__', @@ -57,6 +58,10 @@ describe('inject-placeholders.sh', () => { expect(injected).toContain( 'd-i netcfg/get_hostname string packer-debian-test' ) + // No recipe file, so no `# mirror:` header: the default mirror. + expect(injected).toContain( + 'd-i mirror/http/hostname string deb.debian.org' + ) expect(injected).toContain('d-i netcfg/get_ipaddress string 10.99.0.5') expect(injected).toContain('d-i netcfg/get_gateway string 10.99.0.1') expect(injected).toContain('d-i netcfg/get_nameservers string 9.9.9.9') @@ -69,6 +74,35 @@ describe('inject-placeholders.sh', () => { ) }) + // A release Debian has archived installs from archive.debian.org. This has + // to reach the preseed file: a boot-command value loses to it. + test("takes the Debian mirror from the recipe's # mirror: header", () => { + const root = mkdtempSync(join(tmpdir(), 'cf-inject-')) + tempDirs.push(root) + const httpDir = join(root, 'recipes', 'debian-old', 'http') + const runnerTemp = join(root, 'runner-temp') + mkdirSync(httpDir, { recursive: true }) + mkdirSync(runnerTemp) + writeFileSync( + join(root, 'recipes', 'debian-old.pkr.hcl'), + '# display: Debian Old\n# mirror: archive.debian.org\n' + ) + const preseed = join(httpDir, 'preseed.cfg') + writeFileSync( + preseed, + 'd-i mirror/http/hostname string __PACKER_DEBIAN_MIRROR__\n' + ) + + const result = Bun.spawnSync(['bash', script, 'debian-old'], { + cwd: root, + env: { ...process.env, RUNNER_TEMP: runnerTemp }, + }) + expect(result.exitCode).toBe(0) + expect(readFileSync(preseed, 'utf8')).toBe( + 'd-i mirror/http/hostname string archive.debian.org\n' + ) + }) + test('protects generated Windows credentials with mode 0600', () => { const root = mkdtempSync(join(tmpdir(), 'cf-inject-')) tempDirs.push(root)