|
@@ -0,0 +1,382 @@
|
|
|
|
|
+import assert from 'node:assert/strict';
|
|
|
|
|
+import {
|
|
|
|
|
+ chmodSync,
|
|
|
|
|
+ mkdirSync,
|
|
|
|
|
+ mkdtempSync,
|
|
|
|
|
+ readFileSync,
|
|
|
|
|
+ readdirSync,
|
|
|
|
|
+ rmSync,
|
|
|
|
|
+ statSync,
|
|
|
|
|
+ writeFileSync
|
|
|
|
|
+} from 'node:fs';
|
|
|
|
|
+import os from 'node:os';
|
|
|
|
|
+import path from 'node:path';
|
|
|
|
|
+import { fileURLToPath } from 'node:url';
|
|
|
|
|
+import { spawnSync } from 'node:child_process';
|
|
|
|
|
+import { test } from 'node:test';
|
|
|
|
|
+
|
|
|
|
|
+const scriptPath = fileURLToPath(new URL('../scripts/sync-tls-certificate.sh', import.meta.url));
|
|
|
|
|
+const opensslLookup = process.platform === 'win32'
|
|
|
|
|
+ ? { status: 1, stdout: '' }
|
|
|
|
|
+ : spawnSync('sh', ['-c', 'command -v openssl'], { encoding: 'utf8' });
|
|
|
|
|
+const opensslPath = opensslLookup.status === 0 ? opensslLookup.stdout.trim() : '';
|
|
|
|
|
+const canRun = process.platform !== 'win32' && Boolean(opensslPath);
|
|
|
|
|
+
|
|
|
|
|
+test('atomically synchronizes a valid wildcard certificate and preserves safe permissions', { skip: !canRun }, (t) => {
|
|
|
|
|
+ const fixture = createFixture(t);
|
|
|
|
|
+ generateCertificate(fixture.sourceDir, 'example.test');
|
|
|
|
|
+
|
|
|
|
|
+ const first = runSync(fixture);
|
|
|
|
|
+ assert.equal(first.status, 0, first.stderr);
|
|
|
|
|
+ assert.match(first.stdout, /MailHub TLS certificate synchronized for mail\.example\.test\./);
|
|
|
|
|
+ assert.deepEqual(readFileSync(fixture.targetCert), readFileSync(fixture.sourceCert));
|
|
|
|
|
+ assert.deepEqual(readFileSync(fixture.targetKey), readFileSync(fixture.sourceKey));
|
|
|
|
|
+ assert.equal(fileMode(fixture.targetCert), 0o644);
|
|
|
|
|
+ assert.equal(fileMode(fixture.targetKey), 0o640);
|
|
|
|
|
+ assert.deepEqual(stagingFiles(fixture.certsDir), []);
|
|
|
|
|
+
|
|
|
|
|
+ const previousCert = readFileSync(fixture.targetCert);
|
|
|
|
|
+ const previousKey = readFileSync(fixture.targetKey);
|
|
|
|
|
+ chmodSync(fixture.targetCert, 0o640);
|
|
|
|
|
+ chmodSync(fixture.targetKey, 0o600);
|
|
|
|
|
+ generateCertificate(fixture.sourceDir, 'example.test');
|
|
|
|
|
+
|
|
|
|
|
+ const replacement = runSync(fixture);
|
|
|
|
|
+ assert.equal(replacement.status, 0, replacement.stderr);
|
|
|
|
|
+ assert.deepEqual(readFileSync(`${fixture.targetCert}.previous`), previousCert);
|
|
|
|
|
+ assert.deepEqual(readFileSync(`${fixture.targetKey}.previous`), previousKey);
|
|
|
|
|
+ assert.deepEqual(readFileSync(fixture.targetCert), readFileSync(fixture.sourceCert));
|
|
|
|
|
+ assert.deepEqual(readFileSync(fixture.targetKey), readFileSync(fixture.sourceKey));
|
|
|
|
|
+ assert.equal(fileMode(fixture.targetCert), 0o644);
|
|
|
|
|
+ assert.equal(fileMode(fixture.targetKey), 0o640);
|
|
|
|
|
+ assert.deepEqual(stagingFiles(fixture.certsDir), []);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+test('is idempotent when the synchronized certificate is already current', { skip: !canRun }, (t) => {
|
|
|
|
|
+ const fixture = createFixture(t);
|
|
|
|
|
+ generateCertificate(fixture.sourceDir, 'example.test');
|
|
|
|
|
+
|
|
|
|
|
+ const first = runSync(fixture);
|
|
|
|
|
+ assert.equal(first.status, 0, first.stderr);
|
|
|
|
|
+ const certBefore = statSnapshot(fixture.targetCert);
|
|
|
|
|
+ const keyBefore = statSnapshot(fixture.targetKey);
|
|
|
|
|
+
|
|
|
|
|
+ const second = runSync(fixture);
|
|
|
|
|
+ assert.equal(second.status, 0, second.stderr);
|
|
|
|
|
+ assert.match(second.stdout, /MailHub TLS certificate is already up to date\./);
|
|
|
|
|
+ assert.deepEqual(statSnapshot(fixture.targetCert), certBefore);
|
|
|
|
|
+ assert.deepEqual(statSnapshot(fixture.targetKey), keyBefore);
|
|
|
|
|
+ assert.equal(readdirSync(fixture.certsDir).some((name) => name.endsWith('.previous')), false);
|
|
|
|
|
+ assert.deepEqual(stagingFiles(fixture.certsDir), []);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+test('rejects a certificate that does not cover MAIL_HOSTNAME without touching the targets', { skip: !canRun }, (t) => {
|
|
|
|
|
+ const fixture = createFixture(t);
|
|
|
|
|
+ generateCertificate(fixture.sourceDir, 'other.test');
|
|
|
|
|
+ mkdirSync(fixture.certsDir, { recursive: true });
|
|
|
|
|
+ writeFileSync(fixture.targetCert, 'existing certificate\n');
|
|
|
|
|
+ writeFileSync(fixture.targetKey, 'existing private key\n');
|
|
|
|
|
+ chmodSync(fixture.targetCert, 0o640);
|
|
|
|
|
+ chmodSync(fixture.targetKey, 0o600);
|
|
|
|
|
+ const certBefore = readFileSync(fixture.targetCert);
|
|
|
|
|
+ const keyBefore = readFileSync(fixture.targetKey);
|
|
|
|
|
+
|
|
|
|
|
+ const result = runSync(fixture);
|
|
|
|
|
+ assert.notEqual(result.status, 0);
|
|
|
|
|
+ assert.match(result.stderr, /source certificate does not cover mail\.example\.test\./);
|
|
|
|
|
+ assert.deepEqual(readFileSync(fixture.targetCert), certBefore);
|
|
|
|
|
+ assert.deepEqual(readFileSync(fixture.targetKey), keyBefore);
|
|
|
|
|
+ assert.equal(fileMode(fixture.targetCert), 0o640);
|
|
|
|
|
+ assert.equal(fileMode(fixture.targetKey), 0o600);
|
|
|
|
|
+ assert.equal(readdirSync(fixture.certsDir).some((name) => name.endsWith('.previous')), false);
|
|
|
|
|
+ assert.deepEqual(stagingFiles(fixture.certsDir), []);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+test('rejects certificate and private key paths that resolve to the same target', { skip: !canRun }, (t) => {
|
|
|
|
|
+ const fixture = createFixture(t);
|
|
|
|
|
+ generateCertificate(fixture.sourceDir, 'example.test');
|
|
|
|
|
+ mkdirSync(fixture.certsDir, { recursive: true });
|
|
|
|
|
+ writeEnvironment(fixture, {
|
|
|
|
|
+ SUBMISSION_TLS_KEY: '/certs/fullchain.pem'
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const result = runSync(fixture);
|
|
|
|
|
+ assert.notEqual(result.status, 0);
|
|
|
|
|
+ assert.equal(readdirSync(fixture.certsDir).some((name) => name === 'fullchain.pem' || name === 'privkey.pem'), false);
|
|
|
|
|
+ assert.deepEqual(stagingFiles(fixture.certsDir), []);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+test('rejects a mismatched certificate and private key without touching the targets', { skip: !canRun }, (t) => {
|
|
|
|
|
+ const fixture = createFixture(t);
|
|
|
|
|
+ const unrelatedDir = path.join(fixture.root, 'unrelated');
|
|
|
|
|
+ mkdirSync(unrelatedDir, { recursive: true });
|
|
|
|
|
+ generateCertificate(fixture.sourceDir, 'example.test');
|
|
|
|
|
+ generateCertificate(unrelatedDir, 'example.test');
|
|
|
|
|
+ writeFileSync(fixture.sourceKey, readFileSync(path.join(unrelatedDir, 'privkey.pem')));
|
|
|
|
|
+ mkdirSync(fixture.certsDir, { recursive: true });
|
|
|
|
|
+ writeFileSync(fixture.targetCert, 'existing certificate\n');
|
|
|
|
|
+ writeFileSync(fixture.targetKey, 'existing private key\n');
|
|
|
|
|
+ const certBefore = readFileSync(fixture.targetCert);
|
|
|
|
|
+ const keyBefore = readFileSync(fixture.targetKey);
|
|
|
|
|
+
|
|
|
|
|
+ const result = runSync(fixture);
|
|
|
|
|
+ assert.notEqual(result.status, 0);
|
|
|
|
|
+ assert.match(result.stderr, /certificate and private key do not match/i);
|
|
|
|
|
+ assert.deepEqual(readFileSync(fixture.targetCert), certBefore);
|
|
|
|
|
+ assert.deepEqual(readFileSync(fixture.targetKey), keyBefore);
|
|
|
|
|
+ assert.equal(readdirSync(fixture.certsDir).some((name) => name.endsWith('.previous')), false);
|
|
|
|
|
+ assert.deepEqual(stagingFiles(fixture.certsDir), []);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+test('tightens an overly permissive existing private key to mode 0640', { skip: !canRun }, (t) => {
|
|
|
|
|
+ const fixture = createFixture(t);
|
|
|
|
|
+ generateCertificate(fixture.sourceDir, 'example.test');
|
|
|
|
|
+ const first = runSync(fixture);
|
|
|
|
|
+ assert.equal(first.status, 0, first.stderr);
|
|
|
|
|
+
|
|
|
|
|
+ chmodSync(fixture.targetKey, 0o666);
|
|
|
|
|
+ generateCertificate(fixture.sourceDir, 'example.test');
|
|
|
|
|
+ const replacement = runSync(fixture);
|
|
|
|
|
+
|
|
|
|
|
+ assert.equal(replacement.status, 0, replacement.stderr);
|
|
|
|
|
+ assert.equal(fileMode(fixture.targetKey), 0o640);
|
|
|
|
|
+ assert.deepEqual(readFileSync(fixture.targetKey), readFileSync(fixture.sourceKey));
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+test('validates and installs a staged source snapshot when managed files change mid-run', { skip: !canRun }, (t) => {
|
|
|
|
|
+ const fixture = createFixture(t);
|
|
|
|
|
+ const replacementDir = path.join(fixture.root, 'replacement');
|
|
|
|
|
+ mkdirSync(replacementDir, { recursive: true });
|
|
|
|
|
+ generateCertificate(fixture.sourceDir, 'example.test');
|
|
|
|
|
+ generateCertificate(replacementDir, 'example.test');
|
|
|
|
|
+ const expectedCert = readFileSync(fixture.sourceCert);
|
|
|
|
|
+ const expectedKey = readFileSync(fixture.sourceKey);
|
|
|
|
|
+ const marker = path.join(fixture.root, 'source-mutated');
|
|
|
|
|
+ const mutationPath = createOpenSslMutationPath(fixture.root, fixture.path);
|
|
|
|
|
+
|
|
|
|
|
+ const result = runSync(fixture, {
|
|
|
|
|
+ env: {
|
|
|
|
|
+ PATH: mutationPath,
|
|
|
|
|
+ MAILHUB_TEST_REAL_OPENSSL: opensslPath,
|
|
|
|
|
+ MAILHUB_TEST_MUTATION_MARKER: marker,
|
|
|
|
|
+ MAILHUB_TEST_SOURCE_CERT: fixture.sourceCert,
|
|
|
|
|
+ MAILHUB_TEST_SOURCE_KEY: fixture.sourceKey,
|
|
|
|
|
+ MAILHUB_TEST_REPLACEMENT_CERT: path.join(replacementDir, 'fullchain.pem'),
|
|
|
|
|
+ MAILHUB_TEST_REPLACEMENT_KEY: path.join(replacementDir, 'privkey.pem')
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ assert.equal(result.status, 0, result.stderr);
|
|
|
|
|
+ assert.equal(readFileSync(fixture.targetCert).equals(expectedCert), true, 'installed certificate must use the staged snapshot');
|
|
|
|
|
+ assert.equal(readFileSync(fixture.targetKey).equals(expectedKey), true, 'installed private key must use the staged snapshot');
|
|
|
|
|
+ assert.notDeepEqual(readFileSync(fixture.sourceCert), expectedCert);
|
|
|
|
|
+ assert.notDeepEqual(readFileSync(fixture.sourceKey), expectedKey);
|
|
|
|
|
+ assert.deepEqual(stagingFiles(fixture.certsDir), []);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+test('rolls back both certificate files when post-restart health validation fails', { skip: !canRun }, (t) => {
|
|
|
|
|
+ const fixture = createFixture(t);
|
|
|
|
|
+ generateCertificate(fixture.sourceDir, 'example.test');
|
|
|
|
|
+ const first = runSync(fixture);
|
|
|
|
|
+ assert.equal(first.status, 0, first.stderr);
|
|
|
|
|
+ const certBefore = readFileSync(fixture.targetCert);
|
|
|
|
|
+ const keyBefore = readFileSync(fixture.targetKey);
|
|
|
|
|
+ const certModeBefore = fileMode(fixture.targetCert);
|
|
|
|
|
+ const keyModeBefore = fileMode(fixture.targetKey);
|
|
|
|
|
+
|
|
|
|
|
+ generateCertificate(fixture.sourceDir, 'example.test');
|
|
|
|
|
+ const failurePath = createFailedHealthPath(fixture.root, fixture.path);
|
|
|
|
|
+ const result = runSync(fixture, {
|
|
|
|
|
+ restart: '1',
|
|
|
|
|
+ env: { PATH: failurePath }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ assert.notEqual(result.status, 0);
|
|
|
|
|
+ assert.match(result.stderr, /did not become healthy/i);
|
|
|
|
|
+ assert.equal(readFileSync(fixture.targetCert).equals(certBefore), true, 'certificate must roll back after failed health validation');
|
|
|
|
|
+ assert.equal(readFileSync(fixture.targetKey).equals(keyBefore), true, 'private key must roll back after failed health validation');
|
|
|
|
|
+ assert.equal(fileMode(fixture.targetCert), certModeBefore);
|
|
|
|
|
+ assert.equal(fileMode(fixture.targetKey), keyModeBefore);
|
|
|
|
|
+ assert.deepEqual(stagingFiles(fixture.certsDir), []);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+function createFixture(t) {
|
|
|
|
|
+ const root = mkdtempSync(path.join(os.tmpdir(), 'mailhub-cert-sync-'));
|
|
|
|
|
+ const projectDir = path.join(root, 'project');
|
|
|
|
|
+ const sourceDir = path.join(root, 'source');
|
|
|
|
|
+ const certsDir = path.join(projectDir, 'certs');
|
|
|
|
|
+ const envFile = path.join(projectDir, '.env');
|
|
|
|
|
+ mkdirSync(projectDir, { recursive: true });
|
|
|
|
|
+ mkdirSync(sourceDir, { recursive: true });
|
|
|
|
|
+ writeFileSync(envFile, [
|
|
|
|
|
+ 'MAIL_HOSTNAME=mail.example.test',
|
|
|
|
|
+ 'SUBMISSION_TLS_CERT=/certs/fullchain.pem',
|
|
|
|
|
+ 'SUBMISSION_TLS_KEY=/certs/privkey.pem',
|
|
|
|
|
+ ''
|
|
|
|
|
+ ].join('\n'));
|
|
|
|
|
+ t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
|
|
|
+ return {
|
|
|
|
|
+ root,
|
|
|
|
|
+ projectDir,
|
|
|
|
|
+ sourceDir,
|
|
|
|
|
+ certsDir,
|
|
|
|
|
+ envFile,
|
|
|
|
|
+ sourceCert: path.join(sourceDir, 'fullchain.pem'),
|
|
|
|
|
+ sourceKey: path.join(sourceDir, 'privkey.pem'),
|
|
|
|
|
+ targetCert: path.join(certsDir, 'fullchain.pem'),
|
|
|
|
|
+ targetKey: path.join(certsDir, 'privkey.pem'),
|
|
|
|
|
+ path: createCompatibilityPath(root)
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function writeEnvironment(fixture, overrides = {}) {
|
|
|
|
|
+ const values = {
|
|
|
|
|
+ MAIL_HOSTNAME: 'mail.example.test',
|
|
|
|
|
+ SUBMISSION_TLS_CERT: '/certs/fullchain.pem',
|
|
|
|
|
+ SUBMISSION_TLS_KEY: '/certs/privkey.pem',
|
|
|
|
|
+ ...overrides
|
|
|
|
|
+ };
|
|
|
|
|
+ writeFileSync(fixture.envFile, [
|
|
|
|
|
+ `MAIL_HOSTNAME=${values.MAIL_HOSTNAME}`,
|
|
|
|
|
+ `SUBMISSION_TLS_CERT=${values.SUBMISSION_TLS_CERT}`,
|
|
|
|
|
+ `SUBMISSION_TLS_KEY=${values.SUBMISSION_TLS_KEY}`,
|
|
|
|
|
+ ''
|
|
|
|
|
+ ].join('\n'));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function generateCertificate(sourceDir, domain) {
|
|
|
|
|
+ const result = spawnSync('openssl', [
|
|
|
|
|
+ 'req',
|
|
|
|
|
+ '-x509',
|
|
|
|
|
+ '-newkey', 'rsa:2048',
|
|
|
|
|
+ '-nodes',
|
|
|
|
|
+ '-sha256',
|
|
|
|
|
+ '-days', '30',
|
|
|
|
|
+ '-subj', `/CN=*.${domain}`,
|
|
|
|
|
+ '-addext', `subjectAltName=DNS:*.${domain},DNS:${domain}`,
|
|
|
|
|
+ '-keyout', path.join(sourceDir, 'privkey.pem'),
|
|
|
|
|
+ '-out', path.join(sourceDir, 'fullchain.pem')
|
|
|
|
|
+ ], { encoding: 'utf8' });
|
|
|
|
|
+ assert.equal(result.status, 0, result.stderr);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function runSync(fixture, { restart = '0', env = {} } = {}) {
|
|
|
|
|
+ const rootOwnershipEnv = typeof process.getuid === 'function' && process.getuid() === 0
|
|
|
|
|
+ ? { MAILHUB_CERT_READER_GID: String(process.getgid()) }
|
|
|
|
|
+ : {};
|
|
|
|
|
+ return spawnSync('bash', [scriptPath], {
|
|
|
|
|
+ cwd: fixture.projectDir,
|
|
|
|
|
+ encoding: 'utf8',
|
|
|
|
|
+ env: {
|
|
|
|
|
+ ...process.env,
|
|
|
|
|
+ PATH: fixture.path,
|
|
|
|
|
+ MAILHUB_CERT_PROJECT_DIR: fixture.projectDir,
|
|
|
|
|
+ MAILHUB_CERT_ENV_FILE: fixture.envFile,
|
|
|
|
|
+ MAILHUB_CERT_SOURCE_DIR: fixture.sourceDir,
|
|
|
|
|
+ MAILHUB_CERT_RESTART: restart,
|
|
|
|
|
+ ...rootOwnershipEnv,
|
|
|
|
|
+ ...env
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function statSnapshot(file) {
|
|
|
|
|
+ const stat = statSync(file);
|
|
|
|
|
+ return {
|
|
|
|
|
+ ino: stat.ino,
|
|
|
|
|
+ mode: stat.mode & 0o777,
|
|
|
|
|
+ size: stat.size,
|
|
|
|
|
+ mtimeMs: stat.mtimeMs
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function fileMode(file) {
|
|
|
|
|
+ return statSync(file).mode & 0o777;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function stagingFiles(directory) {
|
|
|
|
|
+ return readdirSync(directory).filter((name) => name.includes('.tmp.') || name.startsWith('.cert-sync.'));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function createCompatibilityPath(root) {
|
|
|
|
|
+ if (process.platform !== 'darwin') return process.env.PATH || '';
|
|
|
|
|
+ const binDir = path.join(root, 'bin');
|
|
|
|
|
+ mkdirSync(binDir, { recursive: true });
|
|
|
|
|
+ writeExecutable(path.join(binDir, 'cp'), stripDoubleDashWrapper('/bin/cp'));
|
|
|
|
|
+ writeExecutable(path.join(binDir, 'mv'), stripDoubleDashWrapper('/bin/mv'));
|
|
|
|
|
+ writeExecutable(path.join(binDir, 'rm'), stripDoubleDashWrapper('/bin/rm'));
|
|
|
|
|
+ writeExecutable(path.join(binDir, 'chmod'), `#!/usr/bin/env bash
|
|
|
|
|
+set -euo pipefail
|
|
|
|
|
+if [[ "\${1:-}" == --reference=* ]]; then
|
|
|
|
|
+ reference="\${1#*=}"
|
|
|
|
|
+ shift
|
|
|
|
|
+ mode="$(/usr/bin/stat -f '%Lp' "\${reference}")"
|
|
|
|
|
+ exec /bin/chmod "\${mode}" "$@"
|
|
|
|
|
+fi
|
|
|
|
|
+exec /bin/chmod "$@"
|
|
|
|
|
+`);
|
|
|
|
|
+ writeExecutable(path.join(binDir, 'chown'), `#!/usr/bin/env bash
|
|
|
|
|
+set -euo pipefail
|
|
|
|
|
+if [[ "\${1:-}" == --reference=* ]]; then
|
|
|
|
|
+ reference="\${1#*=}"
|
|
|
|
|
+ shift
|
|
|
|
|
+ owner="$(/usr/bin/stat -f '%u:%g' "\${reference}")"
|
|
|
|
|
+ exec /usr/sbin/chown "\${owner}" "$@"
|
|
|
|
|
+fi
|
|
|
|
|
+exec /usr/sbin/chown "$@"
|
|
|
|
|
+`);
|
|
|
|
|
+ writeExecutable(path.join(binDir, 'sha256sum'), `#!/usr/bin/env bash
|
|
|
|
|
+exec /usr/bin/shasum -a 256 "$@"
|
|
|
|
|
+`);
|
|
|
|
|
+ return `${binDir}${path.delimiter}${process.env.PATH || ''}`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function createOpenSslMutationPath(root, basePath) {
|
|
|
|
|
+ const binDir = path.join(root, 'mutating-openssl-bin');
|
|
|
|
|
+ mkdirSync(binDir, { recursive: true });
|
|
|
|
|
+ writeExecutable(path.join(binDir, 'openssl'), `#!/usr/bin/env bash
|
|
|
|
|
+set -euo pipefail
|
|
|
|
|
+if [[ ! -e "\${MAILHUB_TEST_MUTATION_MARKER}" ]]; then
|
|
|
|
|
+ : > "\${MAILHUB_TEST_MUTATION_MARKER}"
|
|
|
|
|
+ cp "\${MAILHUB_TEST_REPLACEMENT_CERT}" "\${MAILHUB_TEST_SOURCE_CERT}"
|
|
|
|
|
+ cp "\${MAILHUB_TEST_REPLACEMENT_KEY}" "\${MAILHUB_TEST_SOURCE_KEY}"
|
|
|
|
|
+fi
|
|
|
|
|
+exec "\${MAILHUB_TEST_REAL_OPENSSL}" "$@"
|
|
|
|
|
+`);
|
|
|
|
|
+ return `${binDir}${path.delimiter}${basePath}`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function createFailedHealthPath(root, basePath) {
|
|
|
|
|
+ const binDir = path.join(root, 'failed-health-bin');
|
|
|
|
|
+ mkdirSync(binDir, { recursive: true });
|
|
|
|
|
+writeExecutable(path.join(binDir, 'docker'), `#!/usr/bin/env bash
|
|
|
|
|
+set -euo pipefail
|
|
|
|
|
+if [[ "$*" == *"tls.createSecureContext"* ]]; then
|
|
|
|
|
+ exit 0
|
|
|
|
|
+fi
|
|
|
|
|
+if [[ "\${1:-}" == "compose" && "\${2:-}" == "restart" ]]; then
|
|
|
|
|
+ exit 0
|
|
|
|
|
+fi
|
|
|
|
|
+exit 1
|
|
|
|
|
+`);
|
|
|
|
|
+ writeExecutable(path.join(binDir, 'sleep'), `#!/usr/bin/env bash
|
|
|
|
|
+exit 0
|
|
|
|
|
+`);
|
|
|
|
|
+ return `${binDir}${path.delimiter}${basePath}`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function stripDoubleDashWrapper(command) {
|
|
|
|
|
+ return `#!/usr/bin/env bash
|
|
|
|
|
+set -euo pipefail
|
|
|
|
|
+args=()
|
|
|
|
|
+for argument in "$@"; do
|
|
|
|
|
+ [[ "\${argument}" == "--" ]] || args+=("\${argument}")
|
|
|
|
|
+done
|
|
|
|
|
+exec ${command} "\${args[@]}"
|
|
|
|
|
+`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function writeExecutable(file, content) {
|
|
|
|
|
+ writeFileSync(file, content);
|
|
|
|
|
+ chmodSync(file, 0o755);
|
|
|
|
|
+}
|