mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(docker): move start-up into a script so an edited container still boots
The image carried its whole start-up sequence as one inline `sh -c` string containing four single-quoted echo messages. Container management UIs render Config.Cmd back into a text field and re-split it on submit with a quote-aware, escape-unaware tokenizer, so editing a container in place — the usual way to change an environment variable — shredded the quoting and left dash half an if-block to parse. The container then died before Node started, with only `TREK: 1: Syntax error: end of file unexpected (expecting "fi")` in the log: the word TREK is the stranded fragment of the first echo, reported as $0. The logic moves to server/scripts/entrypoint.sh unchanged, and CMD becomes a single bare token, which has nothing left to mangle. A unit test pins that shape — both exec-form lines parse, and every argv element stays one token free of quotes and spaces — and the Dockerfile joins the test workflow's path filter so that guard actually runs when someone edits it. Closes #2374
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
#!/bin/sh
|
||||
# Container start-up: check the image layout, correct ownership of the mounted
|
||||
# data dirs while still root, then drop to the node user.
|
||||
#
|
||||
# This has to stay a file rather than an inline `sh -c` string in CMD. Container
|
||||
# management UIs render Config.Cmd back into a text field and re-split it with a
|
||||
# quote-aware tokenizer when you edit a container in place; a command carrying
|
||||
# embedded quotes comes back mangled and the container dies with a shell syntax
|
||||
# error before Node starts. A CMD of one bare token survives that round-trip.
|
||||
set -e
|
||||
|
||||
# If the app code is missing, a volume was almost certainly mounted over /app
|
||||
# (it hides the image's node_modules + dist). Fail with actionable guidance
|
||||
# instead of a cryptic "Cannot find module 'tsconfig-paths/register'".
|
||||
if [ ! -f /app/server/dist/index.js ] || [ ! -d /app/node_modules/tsconfig-paths ]; then
|
||||
echo 'FATAL: TREK application files are missing from the image.'
|
||||
echo 'A volume is likely mounted over /app, which hides the app code.'
|
||||
echo 'Mount ONLY your data and uploads dirs: -v ./data:/app/data -v ./uploads:/app/uploads'
|
||||
echo 'Do NOT mount a volume at /app. See https://github.com/liketrek/TREK/wiki/Troubleshooting'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Best effort: a read-only host filesystem or a locked-down bind mount must keep
|
||||
# booting, exactly as it did before.
|
||||
chown -R node:node /app/data /app/uploads 2>/dev/null || true
|
||||
|
||||
# cd into server/ so tsconfig-paths/register finds tsconfig.json and
|
||||
# ../node_modules resolves correctly.
|
||||
cd /app/server
|
||||
exec gosu node node --require tsconfig-paths/register dist/index.js
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
// Container management UIs let you edit a running container's command, and they
|
||||
// do it by rendering Config.Cmd back into a text field and re-splitting it with a
|
||||
// quote-aware tokenizer on submit. Embedded quotes never survive that round-trip:
|
||||
// an inline `sh -c "… echo 'FATAL: …' …"` comes back truncated and the container
|
||||
// dies with a shell syntax error before Node starts (#2374). The image therefore
|
||||
// keeps its start-up logic in server/scripts/entrypoint.sh and every ENTRYPOINT
|
||||
// and CMD element is a single bare token. Pin that shape so it cannot drift back.
|
||||
const repoRoot = path.resolve(__dirname, '../../..');
|
||||
|
||||
const dockerfile = fs.readFileSync(path.join(repoRoot, 'Dockerfile'), 'utf8');
|
||||
const entrypointPath = path.join(repoRoot, 'server', 'scripts', 'entrypoint.sh');
|
||||
|
||||
// Anchored at column 0 and requiring the JSON exec form, so the HEALTHCHECK's
|
||||
// indented shell-form `CMD wget -qO- … || exit 1` is not picked up.
|
||||
const readExecForms = (): string[][] =>
|
||||
[...dockerfile.matchAll(/^(?:ENTRYPOINT|CMD)[ \t]+(\[[^\n]*\])$/gm)].map((m) => JSON.parse(m[1]));
|
||||
|
||||
describe('container start-up shape', () => {
|
||||
const execForms = readExecForms();
|
||||
|
||||
it('finds both exec-form lines (guards against matching nothing or the healthcheck)', () => {
|
||||
expect(execForms).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('keeps every ENTRYPOINT and CMD element a single token free of quotes and spaces', () => {
|
||||
for (const argv of execForms) {
|
||||
expect(argv.length).toBeGreaterThan(0);
|
||||
for (const element of argv) expect(element).toMatch(/^[^\s'"]+$/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('entrypoint script', () => {
|
||||
const source = fs.readFileSync(entrypointPath, 'utf8');
|
||||
|
||||
it('exists and is copied into the image by the Dockerfile', () => {
|
||||
expect(fs.existsSync(entrypointPath)).toBe(true);
|
||||
expect(dockerfile).toContain('COPY server/scripts/entrypoint.sh /usr/local/bin/trek-entrypoint');
|
||||
});
|
||||
|
||||
it('has LF line endings (a CRLF shebang kills the container with "no such file or directory")', () => {
|
||||
expect(source).not.toContain('\r');
|
||||
});
|
||||
|
||||
it('keeps the volume-over-/app preflight wording the wiki quotes', () => {
|
||||
expect(source).toContain('FATAL: TREK application files are missing from the image.');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user