Split gateway control and dataplane into separate services
This commit is contained in:
23
test/server/dataplane-client.test.js
Normal file
23
test/server/dataplane-client.test.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createDataplaneClient } from '../../src/server/dataplaneClient.js';
|
||||
|
||||
test('control uses the dataplane socket protocol', async () => {
|
||||
const requests = [];
|
||||
const send = async (socketPath, pathname, method) => {
|
||||
requests.push(`${method} ${pathname} ${socketPath}`);
|
||||
return { running: pathname !== '/stop', startedAt: 'now' };
|
||||
};
|
||||
|
||||
const client = createDataplaneClient('/run/dataplane.sock', send);
|
||||
assert.equal((await client.refresh()).running, true);
|
||||
await client.apply();
|
||||
await client.restart();
|
||||
assert.equal((await client.stop()).running, false);
|
||||
assert.deepEqual(requests, [
|
||||
'GET /status /run/dataplane.sock',
|
||||
'POST /apply /run/dataplane.sock',
|
||||
'POST /restart /run/dataplane.sock',
|
||||
'POST /stop /run/dataplane.sock',
|
||||
]);
|
||||
});
|
||||
17
test/server/deploy-split.test.js
Normal file
17
test/server/deploy-split.test.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
const root = path.resolve(import.meta.dirname, '../..');
|
||||
const compose = fs.readFileSync(path.join(root, 'docker-compose.gateway.yml'), 'utf8');
|
||||
const deploy = fs.readFileSync(path.join(root, 'scripts/deploy-gateway.sh'), 'utf8');
|
||||
const workflow = fs.readFileSync(path.join(root, '.gitea/workflows/gateway-build.yml'), 'utf8');
|
||||
|
||||
test('gateway deploy updates control without recreating dataplane', () => {
|
||||
assert.match(compose, /vpn-proxy-control:/);
|
||||
assert.match(compose, /vpn-proxy-dataplane:/);
|
||||
assert.match(compose, /DATAPLANE_SOCKET: \/run\/vpn-proxy\/dataplane\.sock/);
|
||||
assert.match(deploy, /up -d --no-deps --wait[^\n]+vpn-proxy-control/);
|
||||
assert.match(workflow, /UPDATE_DATAPLANE="\$\{UPDATE_DATAPLANE\}"/);
|
||||
});
|
||||
@@ -16,3 +16,10 @@ test('gateway keeps direct forwarding active while TProxy interception is switch
|
||||
assert.doesNotMatch(entrypoint, /-A PREROUTING -j "\$TPROXY_CHAIN"/);
|
||||
assert.doesNotMatch(entrypoint, /TPROXY_BYPASS_SOURCE_CIDRS|DIRECT_BYPASS_CACHE|ipset/);
|
||||
});
|
||||
|
||||
test('control bypasses host routing while dataplane owns it', () => {
|
||||
assert.match(entrypoint, /APP_COMPONENT.*control/);
|
||||
assert.match(entrypoint, /exec node \/app\/src\/server\/index\.js/);
|
||||
assert.match(entrypoint, /APP_COMPONENT.*dataplane/);
|
||||
assert.match(entrypoint, /node \/app\/src\/server\/dataplane\.js/);
|
||||
});
|
||||
|
||||
51
test/server/singbox-runtime.test.js
Normal file
51
test/server/singbox-runtime.test.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { createSingboxRuntime } from '../../src/server/singboxRuntime.js';
|
||||
|
||||
async function waitForStarts(filePath, count) {
|
||||
for (let attempt = 0; attempt < 100; attempt += 1) {
|
||||
if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8').length >= count) return;
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
throw new Error(`sing-box did not start ${count} time(s)`);
|
||||
}
|
||||
|
||||
test('dataplane keeps sing-box running when the applied config is unchanged', async (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-dataplane-'));
|
||||
const binDir = path.join(dir, 'bin');
|
||||
const configPath = path.join(dir, 'config.json');
|
||||
const startsPath = path.join(dir, 'starts');
|
||||
fs.mkdirSync(binDir);
|
||||
fs.writeFileSync(configPath, '{}');
|
||||
fs.writeFileSync(path.join(binDir, 'sing-box'), `#!/usr/bin/env node
|
||||
if (process.argv[2] === 'check') process.exit(0);
|
||||
require('node:fs').appendFileSync(process.env.SINGBOX_TEST_STARTS, 'x');
|
||||
process.on('SIGTERM', () => process.exit(0));
|
||||
setInterval(() => {}, 60_000);
|
||||
`);
|
||||
fs.chmodSync(path.join(binDir, 'sing-box'), 0o755);
|
||||
|
||||
const previousPath = process.env.PATH;
|
||||
process.env.PATH = `${binDir}:${previousPath}`;
|
||||
process.env.SINGBOX_TEST_STARTS = startsPath;
|
||||
const runtime = createSingboxRuntime({ configPath });
|
||||
t.after(async () => {
|
||||
await runtime.stop();
|
||||
process.env.PATH = previousPath;
|
||||
delete process.env.SINGBOX_TEST_STARTS;
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
await runtime.apply();
|
||||
await waitForStarts(startsPath, 1);
|
||||
await runtime.apply();
|
||||
assert.equal(fs.readFileSync(startsPath, 'utf8'), 'x');
|
||||
|
||||
fs.writeFileSync(configPath, '{"changed":true}');
|
||||
await runtime.apply();
|
||||
await waitForStarts(startsPath, 2);
|
||||
assert.equal(fs.readFileSync(startsPath, 'utf8'), 'xx');
|
||||
});
|
||||
Reference in New Issue
Block a user