From ccea4e48eca40b4bfe682b8de7a614e8a1c36309 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Mon, 17 Aug 2026 13:45:14 +0200 Subject: [PATCH] test: enforce production docker network invariants --- package.json | 3 ++ pnpm-lock.yaml | 42 +++++++++++++++++---------- scripts/verify-compose-invariants.mjs | 18 ++++++++++++ tests/compose-invariants.test.mjs | 20 +++++++++++++ 4 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 scripts/verify-compose-invariants.mjs create mode 100644 tests/compose-invariants.test.mjs diff --git a/package.json b/package.json index 7914608..f87a5d9 100644 --- a/package.json +++ b/package.json @@ -21,5 +21,8 @@ "msgpackr-extract", "unrs-resolver" ] + }, + "devDependencies": { + "yaml": "^2.9.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc0c36f..6c9e1c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,11 @@ settings: importers: - .: {} + .: + devDependencies: + yaml: + specifier: ^2.9.0 + version: 2.9.0 backend: dependencies: @@ -137,7 +141,7 @@ importers: devDependencies: '@angular/build': specifier: ^21.2.6 - version: 21.2.21(@angular/compiler-cli@21.2.20(@angular/compiler@21.2.20)(typescript@5.9.3))(@angular/compiler@21.2.20)(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(@angular/platform-browser@21.2.20(@angular/common@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)))(@angular/service-worker@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(rxjs@7.8.2))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.13.3)(chokidar@5.0.0)(postcss@8.5.26)(terser@5.50.0)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0))) + version: 21.2.21(@angular/compiler-cli@21.2.20(@angular/compiler@21.2.20)(typescript@5.9.3))(@angular/compiler@21.2.20)(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(@angular/platform-browser@21.2.20(@angular/common@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)))(@angular/service-worker@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(rxjs@7.8.2))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.13.3)(chokidar@5.0.0)(postcss@8.5.26)(terser@5.50.0)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)))(yaml@2.9.0) '@angular/cli': specifier: ^21.2.6 version: 21.2.21(@types/node@24.13.3)(chokidar@5.0.0) @@ -155,7 +159,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.8 - version: 4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)) + version: 4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)) packages: @@ -4887,6 +4891,11 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -5106,7 +5115,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular/build@21.2.21(@angular/compiler-cli@21.2.20(@angular/compiler@21.2.20)(typescript@5.9.3))(@angular/compiler@21.2.20)(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(@angular/platform-browser@21.2.20(@angular/common@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)))(@angular/service-worker@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(rxjs@7.8.2))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.13.3)(chokidar@5.0.0)(postcss@8.5.26)(terser@5.50.0)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)))': + '@angular/build@21.2.21(@angular/compiler-cli@21.2.20(@angular/compiler@21.2.20)(typescript@5.9.3))(@angular/compiler@21.2.20)(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(@angular/platform-browser@21.2.20(@angular/common@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2)))(@angular/service-worker@21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(rxjs@7.8.2))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.13.3)(chokidar@5.0.0)(postcss@8.5.26)(terser@5.50.0)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)))(yaml@2.9.0)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.21(chokidar@5.0.0) @@ -5116,7 +5125,7 @@ snapshots: '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-split-export-declaration': 7.24.7 '@inquirer/confirm': 5.1.21(@types/node@24.13.3) - '@vitejs/plugin-basic-ssl': 2.1.4(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)) + '@vitejs/plugin-basic-ssl': 2.1.4(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)) beasties: 0.4.1 browserslist: 4.28.8 esbuild: 0.28.1 @@ -5137,7 +5146,7 @@ snapshots: tslib: 2.8.1 typescript: 5.9.3 undici: 7.29.0 - vite: 7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0) + vite: 7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0) watchpack: 2.5.1 optionalDependencies: '@angular/core': 21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2) @@ -5145,7 +5154,7 @@ snapshots: '@angular/service-worker': 21.2.20(@angular/core@21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2))(rxjs@7.8.2) lmdb: 3.5.1 postcss: 8.5.26 - vitest: 4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)) + vitest: 4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -6910,9 +6919,9 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.12.2': optional: true - '@vitejs/plugin-basic-ssl@2.1.4(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0))': + '@vitejs/plugin-basic-ssl@2.1.4(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0))': dependencies: - vite: 7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0) + vite: 7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0) '@vitest/expect@4.1.10': dependencies: @@ -6923,13 +6932,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.10(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0))': + '@vitest/mocker@4.1.10(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0) + vite: 7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0) '@vitest/pretty-format@4.1.10': dependencies: @@ -9850,7 +9859,7 @@ snapshots: vary@1.1.2: {} - vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0): + vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0): dependencies: esbuild: 0.28.1 fdir: 6.5.0(picomatch@4.0.4) @@ -9863,11 +9872,12 @@ snapshots: fsevents: 2.3.3 sass: 1.97.3 terser: 5.50.0 + yaml: 2.9.0 - vitest@4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)): + vitest@4.1.10(@types/node@24.13.3)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)) + '@vitest/mocker': 4.1.10(vite@7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -9884,7 +9894,7 @@ snapshots: tinyexec: 1.3.0 tinyglobby: 0.2.17 tinyrainbow: 3.1.1 - vite: 7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0) + vite: 7.3.6(@types/node@24.13.3)(sass@1.97.3)(terser@5.50.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.13.3 @@ -10034,6 +10044,8 @@ snapshots: yallist@5.0.0: {} + yaml@2.9.0: {} + yargs-parser@21.1.1: {} yargs-parser@22.0.0: {} diff --git a/scripts/verify-compose-invariants.mjs b/scripts/verify-compose-invariants.mjs new file mode 100644 index 0000000..d937a7e --- /dev/null +++ b/scripts/verify-compose-invariants.mjs @@ -0,0 +1,18 @@ +export function verifyComposeInvariants(compose) { + const services = compose?.services ?? {}; + + for (const [name, service] of Object.entries(services)) { + if (name !== 'edge' && Array.isArray(service.ports) && service.ports.length > 0) { + throw new Error(`${name} must not define ports`); + } + if (service.network_mode === 'host') { + throw new Error(`${name} must not use network_mode: host`); + } + } + + const edge = services.edge; + const edgePorts = Array.isArray(edge?.ports) ? edge.ports : []; + if (edgePorts.length !== 1) { + throw new Error(`edge must publish exactly one port; found ${edgePorts.length}`); + } +} diff --git a/tests/compose-invariants.test.mjs b/tests/compose-invariants.test.mjs new file mode 100644 index 0000000..c6f47b4 --- /dev/null +++ b/tests/compose-invariants.test.mjs @@ -0,0 +1,20 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import { parse } from 'yaml'; +import { verifyComposeInvariants } from '../scripts/verify-compose-invariants.mjs'; + +test('production compose publishes exactly one edge port', () => { + const compose = parse(fs.readFileSync(new URL('../compose.yml', import.meta.url), 'utf8')); + assert.doesNotThrow(() => verifyComposeInvariants(compose)); +}); + +test('invariant rejects a host port on postgres', () => { + const invalid = { + services: { + edge: { ports: ['443:443'] }, + postgres: { ports: ['5432:5432'] }, + }, + }; + assert.throws(() => verifyComposeInvariants(invalid), /postgres.*ports/i); +});