Handle connectivity diagnostics failures
Build and Deploy Gateway / build-and-push (push) Successful in 13s
Build and Deploy Gateway / deploy (push) Successful in 13s

This commit is contained in:
2026-08-07 20:58:18 +03:00
parent 12375006d3
commit 0c376c72aa
6 changed files with 30 additions and 11 deletions
+1 -1
View File
@@ -111,7 +111,7 @@ jobs:
DATAPLANE_IMAGE="${IMAGE}-dataplane:${{ gitea.sha }}" DATAPLANE_IMAGE="${IMAGE}-dataplane:${{ gitea.sha }}"
UPDATE_DATAPLANE=false UPDATE_DATAPLANE=false
if git diff-tree --no-commit-id --name-only -r -m HEAD | grep -Eq \ if git diff-tree --no-commit-id --name-only -r -m HEAD | grep -Eq \
'^(Dockerfile|entrypoint\.sh|package(-lock)?\.json|scripts/build-runtime-base\.sh|\.gitea/workflows/gateway-build\.yml|src/server/(config|dataplane|gatewayRouting|singboxRuntime|version)\.js|src/server/(adapters/neighbors|services/(deviceTrafficService|devicePolicyService))\.js|src/shared/errors\.js)$'; then '^(Dockerfile|entrypoint\.sh|package(-lock)?\.json|scripts/build-runtime-base\.sh|\.gitea/workflows/gateway-build\.yml|src/server/(config|dataplane|gatewayRouting|singbox|singboxRuntime|version)\.js|src/server/(adapters/neighbors|services/(connectivityDiagnosticsService|deviceTrafficService|devicePolicyService))\.js|src/shared/errors\.js)$'; then
UPDATE_DATAPLANE=true UPDATE_DATAPLANE=true
fi fi
+9 -3
View File
@@ -1,7 +1,7 @@
import http from 'node:http'; import http from 'node:http';
import { HarborError } from '../shared/errors.js'; import { HarborError } from '../shared/errors.js';
function request(socketPath, pathname, method = 'GET', body = null) { function request(socketPath, pathname, method = 'GET', body = null, timeoutMs = 6000) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const encoded = body == null ? null : JSON.stringify(body); const encoded = body == null ? null : JSON.stringify(body);
const req = http.request({ const req = http.request({
@@ -29,7 +29,7 @@ function request(socketPath, pathname, method = 'GET', body = null) {
}); });
}); });
req.on('error', reject); req.on('error', reject);
req.setTimeout(6000, () => req.destroy(new Error('Dataplane не ответил за 6 секунд'))); req.setTimeout(timeoutMs, () => req.destroy(new Error(`Dataplane не ответил за ${Math.ceil(timeoutMs / 1000)} секунд`)));
req.end(encoded); req.end(encoded);
}); });
} }
@@ -56,7 +56,13 @@ export function createDataplaneClient(socketPath, send = request) {
observeTraffic: () => send(socketPath, '/device-traffic', 'GET'), observeTraffic: () => send(socketPath, '/device-traffic', 'GET'),
observeDevicePolicy: () => send(socketPath, '/device-policy', 'GET'), observeDevicePolicy: () => send(socketPath, '/device-policy', 'GET'),
applyDevicePolicies: (devices) => send(socketPath, '/device-policy', 'PUT', { devices }), applyDevicePolicies: (devices) => send(socketPath, '/device-policy', 'PUT', { devices }),
runConnectivityDiagnostics: () => send(socketPath, '/diagnostics/connectivity', 'POST'), runConnectivityDiagnostics: async () => {
try {
return await send(socketPath, '/diagnostics/connectivity', 'POST', null, 15_000);
} catch (cause) {
throw new HarborError('DIAGNOSTICS_FAILED', { cause });
}
},
apply: () => update('/apply', 'POST'), apply: () => update('/apply', 'POST'),
restart: () => update('/restart', 'POST'), restart: () => update('/restart', 'POST'),
stop: () => update('/stop', 'POST'), stop: () => update('/stop', 'POST'),
+1
View File
@@ -13,6 +13,7 @@ export const ERROR_DEFINITIONS = Object.freeze({
DEVICE_NOT_FOUND: { status: 404, message: 'Устройство больше недоступно.', retryable: false }, DEVICE_NOT_FOUND: { status: 404, message: 'Устройство больше недоступно.', retryable: false },
DEVICE_IDENTITY_AMBIGUOUS: { status: 409, message: 'Gateway не может безопасно применить маршрут к этому устройству.', retryable: true }, DEVICE_IDENTITY_AMBIGUOUS: { status: 409, message: 'Gateway не может безопасно применить маршрут к этому устройству.', retryable: true },
DEVICE_POLICY_APPLY_FAILED: { status: 503, message: 'Не удалось применить маршрут устройства.', retryable: true }, DEVICE_POLICY_APPLY_FAILED: { status: 503, message: 'Не удалось применить маршрут устройства.', retryable: true },
DIAGNOSTICS_FAILED: { status: 503, message: 'Не удалось проверить маршруты. Попробуйте ещё раз.', retryable: true },
CONFIG_INVALID: { status: 422, message: 'Конфигурация VPN недействительна.', retryable: false }, CONFIG_INVALID: { status: 422, message: 'Конфигурация VPN недействительна.', retryable: false },
PROCESS_START_FAILED: { status: 503, message: 'Не удалось запустить VPN-процесс.', retryable: true }, PROCESS_START_FAILED: { status: 503, message: 'Не удалось запустить VPN-процесс.', retryable: true },
OPERATION_IN_PROGRESS: { status: 409, message: 'Другая операция ещё выполняется.', retryable: true }, OPERATION_IN_PROGRESS: { status: 409, message: 'Другая операция ещё выполняется.', retryable: true },
+3 -3
View File
@@ -1,7 +1,7 @@
export const HARBOR_VERSIONS = Object.freeze({ export const HARBOR_VERSIONS = Object.freeze({
macClient: '0.15.0', macClient: '0.15.1',
gatewayClient: '0.16.0', gatewayClient: '0.16.1',
gatewayBackend: '0.16.0', gatewayBackend: '0.16.1',
}); });
export function parseVersion(value) { export function parseVersion(value) {
+14 -2
View File
@@ -4,8 +4,8 @@ import { createDataplaneClient } from '../../src/server/dataplaneClient.js';
test('control uses the dataplane socket protocol', async () => { test('control uses the dataplane socket protocol', async () => {
const requests = []; const requests = [];
const send = async (socketPath, pathname, method, body) => { const send = async (socketPath, pathname, method, body, timeoutMs) => {
requests.push({ method, pathname, socketPath, body }); requests.push({ method, pathname, socketPath, body, timeoutMs });
return { return {
running: pathname !== '/stop', running: pathname !== '/stop',
startedAt: 'now', startedAt: 'now',
@@ -42,4 +42,16 @@ test('control uses the dataplane socket protocol', async () => {
'POST /stop /run/dataplane.sock', 'POST /stop /run/dataplane.sock',
]); ]);
assert.deepEqual(requests[5].body, { devices: [{ id: 'dev_0011223344556677' }] }); assert.deepEqual(requests[5].body, { devices: [{ id: 'dev_0011223344556677' }] });
assert.equal(requests[6].timeoutMs, 15_000);
});
test('connectivity diagnostics expose a retryable domain error', async () => {
const client = createDataplaneClient('/run/dataplane.sock', async () => {
throw new Error('Dataplane не ответил');
});
await assert.rejects(client.runConnectivityDiagnostics(), {
code: 'DIAGNOSTICS_FAILED',
retryable: true,
});
}); });
+2 -2
View File
@@ -16,8 +16,8 @@ test('gateway deploy updates control without recreating dataplane', () => {
assert.match(compose, /DATAPLANE_SOCKET: \/run\/vpn-proxy\/dataplane\.sock/); assert.match(compose, /DATAPLANE_SOCKET: \/run\/vpn-proxy\/dataplane\.sock/);
assert.match(deploy, /up -d --no-deps --wait[^\n]+vpn-proxy-control/); assert.match(deploy, /up -d --no-deps --wait[^\n]+vpn-proxy-control/);
assert.match(workflow, /UPDATE_DATAPLANE="\$\{UPDATE_DATAPLANE\}"/); assert.match(workflow, /UPDATE_DATAPLANE="\$\{UPDATE_DATAPLANE\}"/);
assert.match(workflow, /src\/server\/\(config\|dataplane\|gatewayRouting\|singboxRuntime\|version\)/); assert.match(workflow, /src\/server\/\(config\|dataplane\|gatewayRouting\|singbox\|singboxRuntime\|version\)/);
assert.match(workflow, /src\/server\/\(adapters\/neighbors\|services\/\(deviceTrafficService\|devicePolicyService\)\)/); assert.match(workflow, /src\/server\/\(adapters\/neighbors\|services\/\(connectivityDiagnosticsService\|deviceTrafficService\|devicePolicyService\)\)/);
assert.match(workflow, /src\/shared\/errors/); assert.match(workflow, /src\/shared\/errors/);
assert.doesNotMatch(workflow, /dataplaneClient/); assert.doesNotMatch(workflow, /dataplaneClient/);
}); });