test: enforce production docker network invariants

This commit is contained in:
Bastian Wagner
2026-08-17 13:45:14 +02:00
parent 5edb16de73
commit ccea4e48ec
4 changed files with 68 additions and 15 deletions

View File

@@ -21,5 +21,8 @@
"msgpackr-extract", "msgpackr-extract",
"unrs-resolver" "unrs-resolver"
] ]
},
"devDependencies": {
"yaml": "^2.9.0"
} }
} }

42
pnpm-lock.yaml generated
View File

@@ -6,7 +6,11 @@ settings:
importers: importers:
.: {} .:
devDependencies:
yaml:
specifier: ^2.9.0
version: 2.9.0
backend: backend:
dependencies: dependencies:
@@ -137,7 +141,7 @@ importers:
devDependencies: devDependencies:
'@angular/build': '@angular/build':
specifier: ^21.2.6 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': '@angular/cli':
specifier: ^21.2.6 specifier: ^21.2.6
version: 21.2.21(@types/node@24.13.3)(chokidar@5.0.0) version: 21.2.21(@types/node@24.13.3)(chokidar@5.0.0)
@@ -155,7 +159,7 @@ importers:
version: 5.9.3 version: 5.9.3
vitest: vitest:
specifier: ^4.0.8 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: packages:
@@ -4887,6 +4891,11 @@ packages:
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
engines: {node: '>=18'} engines: {node: '>=18'}
yaml@2.9.0:
resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
engines: {node: '>= 14.6'}
hasBin: true
yargs-parser@21.1.1: yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -5106,7 +5115,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- chokidar - 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: dependencies:
'@ampproject/remapping': 2.3.0 '@ampproject/remapping': 2.3.0
'@angular-devkit/architect': 0.2102.21(chokidar@5.0.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-annotate-as-pure': 7.27.3
'@babel/helper-split-export-declaration': 7.24.7 '@babel/helper-split-export-declaration': 7.24.7
'@inquirer/confirm': 5.1.21(@types/node@24.13.3) '@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 beasties: 0.4.1
browserslist: 4.28.8 browserslist: 4.28.8
esbuild: 0.28.1 esbuild: 0.28.1
@@ -5137,7 +5146,7 @@ snapshots:
tslib: 2.8.1 tslib: 2.8.1
typescript: 5.9.3 typescript: 5.9.3
undici: 7.29.0 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 watchpack: 2.5.1
optionalDependencies: optionalDependencies:
'@angular/core': 21.2.20(@angular/compiler@21.2.20)(rxjs@7.8.2) '@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) '@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 lmdb: 3.5.1
postcss: 8.5.26 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: transitivePeerDependencies:
- '@emnapi/core' - '@emnapi/core'
- '@emnapi/runtime' - '@emnapi/runtime'
@@ -6910,9 +6919,9 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.12.2': '@unrs/resolver-binding-win32-x64-msvc@1.12.2':
optional: true 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: 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': '@vitest/expect@4.1.10':
dependencies: dependencies:
@@ -6923,13 +6932,13 @@ snapshots:
chai: 6.2.2 chai: 6.2.2
tinyrainbow: 3.1.1 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: dependencies:
'@vitest/spy': 4.1.10 '@vitest/spy': 4.1.10
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 0.30.21 magic-string: 0.30.21
optionalDependencies: 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': '@vitest/pretty-format@4.1.10':
dependencies: dependencies:
@@ -9850,7 +9859,7 @@ snapshots:
vary@1.1.2: {} 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: dependencies:
esbuild: 0.28.1 esbuild: 0.28.1
fdir: 6.5.0(picomatch@4.0.4) fdir: 6.5.0(picomatch@4.0.4)
@@ -9863,11 +9872,12 @@ snapshots:
fsevents: 2.3.3 fsevents: 2.3.3
sass: 1.97.3 sass: 1.97.3
terser: 5.50.0 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: dependencies:
'@vitest/expect': 4.1.10 '@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/pretty-format': 4.1.10
'@vitest/runner': 4.1.10 '@vitest/runner': 4.1.10
'@vitest/snapshot': 4.1.10 '@vitest/snapshot': 4.1.10
@@ -9884,7 +9894,7 @@ snapshots:
tinyexec: 1.3.0 tinyexec: 1.3.0
tinyglobby: 0.2.17 tinyglobby: 0.2.17
tinyrainbow: 3.1.1 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 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@types/node': 24.13.3 '@types/node': 24.13.3
@@ -10034,6 +10044,8 @@ snapshots:
yallist@5.0.0: {} yallist@5.0.0: {}
yaml@2.9.0: {}
yargs-parser@21.1.1: {} yargs-parser@21.1.1: {}
yargs-parser@22.0.0: {} yargs-parser@22.0.0: {}

View File

@@ -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}`);
}
}

View File

@@ -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);
});