mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
feat(self-host)!: rework the compose stack and demand secrets (#2486)
This commit is contained in:
10 files changed
+387
-79
No files matched your search
@@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {readFileSync} from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
const SELF_HOSTING = path.join(fileURLToPath(new URL('../../../../', import.meta.url)), 'deploy/self-hosting');
|
||||
|
||||
const INTERPOLATION_PATTERN = /\$\{([A-Z][A-Z0-9_]*)[:?}-]/g;
|
||||
const CADDY_PLACEHOLDER_PATTERN = /\{\$([A-Z][A-Z0-9_]*)[:}]/g;
|
||||
const DECLARATION_PATTERN = /^#?([A-Z][A-Z0-9_]*)=/gm;
|
||||
const ACTIVE_DECLARATION_PATTERN = /^([A-Z][A-Z0-9_]*)=/gm;
|
||||
|
||||
const read = (name: string) => readFileSync(path.join(SELF_HOSTING, name), 'utf8');
|
||||
|
||||
const namesMatching = (source: string, pattern: RegExp) =>
|
||||
new Set([...source.matchAll(pattern)].map(([, name]) => name));
|
||||
|
||||
const interpolatedNames = (source: string) => namesMatching(source, INTERPOLATION_PATTERN);
|
||||
|
||||
const example = read('.env.example');
|
||||
const declared = new Set([...example.matchAll(DECLARATION_PATTERN)].map(([, name]) => name));
|
||||
|
||||
describe('.env.example covers every name the compose files interpolate', () => {
|
||||
for (const file of ['docker-compose.yml', 'docker-compose.proxy.yml']) {
|
||||
test(`every \${NAME} in ${file} has a line in .env.example`, () => {
|
||||
const missing = [...interpolatedNames(read(file))].filter((name) => !declared.has(name)).sort();
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
}
|
||||
|
||||
test('every {$NAME} in the Caddyfile has a line in .env.example', () => {
|
||||
const missing = [...namesMatching(read('Caddyfile'), CADDY_PLACEHOLDER_PATTERN)]
|
||||
.filter((name) => !declared.has(name))
|
||||
.sort();
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
test('no name is assigned twice', () => {
|
||||
const seen = new Set<string>();
|
||||
const repeated = new Set<string>();
|
||||
for (const [, name] of example.matchAll(ACTIVE_DECLARATION_PATTERN)) {
|
||||
if (seen.has(name)) {
|
||||
repeated.add(name);
|
||||
}
|
||||
seen.add(name);
|
||||
}
|
||||
expect([...repeated].sort()).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {readFileSync} from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
const SELF_HOSTING = path.join(fileURLToPath(new URL('../../../../', import.meta.url)), 'deploy/self-hosting');
|
||||
|
||||
const compose = readFileSync(path.join(SELF_HOSTING, 'docker-compose.yml'), 'utf8');
|
||||
|
||||
const serviceBlock = (name: string): string => {
|
||||
const start = compose.indexOf(`\n ${name}:\n`);
|
||||
if (start === -1) {
|
||||
throw new Error(`docker-compose.yml has no ${name} service`);
|
||||
}
|
||||
const rest = compose.slice(start + 1);
|
||||
const next = rest.slice(1).search(/\n {2}[a-z][a-z0-9_-]*:\n/u);
|
||||
return next === -1 ? rest : rest.slice(0, next + 1);
|
||||
};
|
||||
|
||||
const sharedEnv = compose.slice(compose.indexOf('x-fluxer-env: &fluxer-env'), compose.indexOf('\nx-fluxer-service:'));
|
||||
|
||||
describe('the shipped compose stack wires every service it starts', () => {
|
||||
test('no service sizes a pool for a connection it cannot make', () => {
|
||||
for (const [, name] of compose.matchAll(/\n {2}([a-z][a-z0-9_-]*):\n/gu)) {
|
||||
const block = serviceBlock(name);
|
||||
if (block.includes('FLUXER_POSTGRES_MAX_CONNECTIONS')) {
|
||||
expect(block).toMatch(/<<: \*fluxer-(postgres-)?env/u);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('the shared block sets the client-IP trust the merged services read', () => {
|
||||
expect(sharedEnv).toContain('FLUXER_TRUST_CLIENT_IP_HEADER: "true"');
|
||||
expect(sharedEnv).toContain('FLUXER_CLIENT_IP_HEADER_NAME: x-forwarded-for');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {readFileSync} from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
const SELF_HOSTING = path.join(fileURLToPath(new URL('../../../../', import.meta.url)), 'deploy/self-hosting');
|
||||
|
||||
const compose = readFileSync(path.join(SELF_HOSTING, 'docker-compose.yml'), 'utf8');
|
||||
|
||||
const serviceBlock = (name: string): string => {
|
||||
const start = compose.indexOf(`\n ${name}:\n`);
|
||||
if (start === -1) {
|
||||
throw new Error(`docker-compose.yml has no ${name} service`);
|
||||
}
|
||||
const rest = compose.slice(start + 1);
|
||||
const next = rest.slice(1).search(/\n {2}[a-z][a-z0-9_-]*:\n/u);
|
||||
return next === -1 ? rest : rest.slice(0, next + 1);
|
||||
};
|
||||
|
||||
describe('the shipped object store checks the credentials the stack sends', () => {
|
||||
const init = serviceBlock('seaweedfs-init');
|
||||
|
||||
test('seaweedfs-init applies an S3 identity built from the .env credentials', () => {
|
||||
expect(init).toContain(
|
||||
's3.configure -user=fluxer -access_key=$$FLUXER_S3_ACCESS_KEY -secret_key=$$FLUXER_S3_SECRET_KEY',
|
||||
);
|
||||
expect(init).toContain('-apply');
|
||||
});
|
||||
|
||||
test('seaweedfs-init is handed the same credentials the api requires', () => {
|
||||
for (const name of ['FLUXER_S3_ACCESS_KEY', 'FLUXER_S3_SECRET_KEY']) {
|
||||
expect(init).toMatch(new RegExp(`${name}: \\$\\{${name}:\\?set ${name} in \\.env\\}`, 'u'));
|
||||
}
|
||||
});
|
||||
|
||||
test('a failed identity write fails the init instead of leaving the store open', () => {
|
||||
const configure = init.indexOf('s3.configure');
|
||||
const ready = init.indexOf('echo "buckets ready"');
|
||||
expect(configure).toBeGreaterThan(-1);
|
||||
expect(ready).toBeGreaterThan(configure);
|
||||
expect(init).toContain('echo "seaweedfs-init could not configure the S3 identity" >&2');
|
||||
expect(init).toContain('exit 1');
|
||||
});
|
||||
|
||||
test('media-proxy signs its reads, which the store now refuses to serve unsigned', () => {
|
||||
expect(serviceBlock('media-proxy')).toContain('FLUXER_S3_READ_SIGNED: "true"');
|
||||
});
|
||||
|
||||
test('every service that reaches the store waits for the identity to exist', () => {
|
||||
for (const name of ['api', 'worker', 'media-proxy']) {
|
||||
expect(serviceBlock(name)).toContain('seaweedfs-init: {condition: service_completed_successfully}');
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user