diff --git a/CHANGELOG.md b/CHANGELOG.md index 69c78da12..83a4bc8c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,11 +58,14 @@ password hashing and the Prisma engine run their glibc builds. The published arm64 image renders PDF pages on a Raspberry Pi 4 again, where the Alpine build of the renderer needs CPU features an ARMv8.0 core - lacks. Node security releases now reach the image with the official - Node image, usually within days. The image carries no package manager: - npm, npx, corepack and yarn are removed. `pg_dump` and `pg_restore` are - PostgreSQL 18.6, the bundled database's version; `psql` is no longer in - the app image, so run it in the `db` container. A health check that + lacks. Node security releases now come with the official Node image, + which HealthLog pins and moves forward with its releases. The image + carries no package manager: npm, npx, corepack and yarn are removed. + `pg_dump` and `pg_restore` are PostgreSQL 18.6, the bundled database's + version. A few tools the Alpine image happened to carry are gone too: + `psql`, `pg_isready`, `pg_dumpall`, `ps`, `top`, `nc`, `unzip` and the + full `wget`. HealthLog uses none of them in the app container; run + `psql` in the `db` container. A health check that still calls `wget`, from a compose file copied before this release or a platform template, keeps working: the image carries a small `wget` that answers health checks and nothing else. The current `docker-compose.yml` diff --git a/Dockerfile b/Dockerfile index 8736ac053..e5adabd18 100644 --- a/Dockerfile +++ b/Dockerfile @@ -392,8 +392,9 @@ COPY --from=builder /app/src/generated ./src/generated # pending migration count, the second runs `migrate deploy` with its # lock-timeout retry. `db-dump.mjs` takes the dump before migrating, and # `db-restore.mjs` is the `db-restore` service; both share -# `src/lib/database-backup/dump-core.mjs` with the app. Root-owned on purpose: -# the app user can run them but not rewrite them. +# `src/lib/database-backup/dump-core.mjs` with the app. The files are +# root-owned, so the app user can run them but not edit them in place. The +# directory around them stays the app user's. COPY --from=builder /app/scripts/reset-password.mjs ./scripts/reset-password.mjs COPY --from=builder /app/scripts/pending-migrations.mjs ./scripts/pending-migrations.mjs COPY --from=builder /app/scripts/migrate-deploy.mjs ./scripts/migrate-deploy.mjs @@ -421,8 +422,8 @@ RUN node scripts/db-dump.mjs 2>&1 | grep -q "db-dump: usage:" && \ pg_restore --version | grep -q "(PostgreSQL) 18\." # The migration CLI from the `prisma-cli` stage. The Prisma config loads -# dotenv from /app, so expose the isolated copy there. Root-owned: the app -# user can run it but not rewrite it. +# dotenv from /app, so expose the isolated copy there. /opt/prisma-cli is +# root-owned: the app user can run it but not change it. COPY --from=prisma-cli /opt/prisma-cli /opt/prisma-cli RUN ln -sfn /opt/prisma-cli/node_modules/.bin/tsx /usr/local/bin/healthlog-tsx && \ ln -sfn /opt/prisma-cli/node_modules/dotenv /app/node_modules/dotenv && \ diff --git a/scripts/wget-shim.mjs b/scripts/wget-shim.mjs index fde4d75e8..212cde065 100644 --- a/scripts/wget-shim.mjs +++ b/scripts/wget-shim.mjs @@ -62,6 +62,9 @@ function parseArgs(argv) { }; if (/^https?:\/\//i.test(arg)) { opts.url ??= arg; + } else if (/^[a-z0-9.[\]:-]+(:\d+)?\//i.test(arg)) { + // busybox wget takes `127.0.0.1:3000/api/health` without a scheme. + opts.url ??= `http://${arg}`; } else if (arg === "--spider") { opts.spider = true; } else if (arg === "-q" || arg === "--quiet") { diff --git a/src/__tests__/runtime-package-pins.test.ts b/src/__tests__/runtime-package-pins.test.ts index 24b0610d7..62c8467dc 100644 --- a/src/__tests__/runtime-package-pins.test.ts +++ b/src/__tests__/runtime-package-pins.test.ts @@ -92,7 +92,12 @@ describe("runtime package pins", () => { pg!.image, ); expect(client, `a pinned major 18 image, not ${pg!.image}`).not.toBeNull(); - const db = /^\s*image:\s*postgres:(\d+\.\d+)-alpine$/m.exec(COMPOSE); + // The `db` service's own image line, not the first postgres image in the + // file: `db-upgrade` carries the previous major. + const db = + /^ {2}db:\n(?:(?! {2}\S).*\n)*? {4}image:\s*postgres:(\d+\.\d+)-alpine$/m.exec( + COMPOSE, + ); expect(db, "the compose file pins the database").not.toBeNull(); // One decision: a database minor bump moves the client with it. expect(client![1]).toBe(db![1]); diff --git a/src/__tests__/wget-shim.test.ts b/src/__tests__/wget-shim.test.ts index 3df905ec2..ee3f71f40 100644 --- a/src/__tests__/wget-shim.test.ts +++ b/src/__tests__/wget-shim.test.ts @@ -175,6 +175,12 @@ describe("wget shim", () => { expect((await wget(["--spider"])).code).toBe(2); }); + it("takes a URL without a scheme, like busybox wget", async () => { + const bare = base.replace(/^http:\/\//, ""); + expect((await wget(["-qO-", `${bare}/ok`])).code).toBe(0); + expect((await wget(["-qO-", `${bare}/down`])).code).toBe(1); + }); + it("is what the image installs as wget", () => { const dockerfile = readSource("Dockerfile", "AS runner"); const runner = dockerfile.slice(dockerfile.search(/^FROM .* AS runner$/m));