feat(admin-ui): add configurable Admin UI domain routing
This commit is contained in:
@@ -173,6 +173,47 @@ Deno.test('ExternalGatewayManager syncs service routes to dcrouter gatewayClient
|
||||
assertEquals(syncRequest.requestData.enabled, true);
|
||||
});
|
||||
|
||||
Deno.test('ExternalGatewayManager syncs Admin UI route to dcrouter gatewayClient API', async () => {
|
||||
const oneboxRef = makeOneboxRef();
|
||||
oneboxRef.database.settings.set('adminUiDomain', 'Onebox.Example.com');
|
||||
oneboxRef.database.settings.set('serverIP', '203.0.113.10');
|
||||
oneboxRef.database.settings.set('httpPort', '8080');
|
||||
|
||||
const requests: Array<{ method: string; requestData: Record<string, unknown> }> = [];
|
||||
const manager = new ExternalGatewayManager(oneboxRef as any);
|
||||
(manager as any).fireDcRouterRequest = async (
|
||||
method: string,
|
||||
requestData: Record<string, unknown>,
|
||||
) => {
|
||||
if (method === 'getGatewayClientContext') {
|
||||
return {
|
||||
context: { role: 'gatewayClient', gatewayClient: { type: 'onebox', id: 'onebox-token' } },
|
||||
};
|
||||
}
|
||||
requests.push({ method, requestData });
|
||||
if (method === 'exportCertificate') {
|
||||
return { success: false };
|
||||
}
|
||||
return { success: true, action: 'created', routeId: 'admin-route' };
|
||||
};
|
||||
|
||||
await manager.syncAdminUiRoute();
|
||||
|
||||
const syncRequest = requests.find((request) => request.method === 'syncGatewayClientRoute')!;
|
||||
const route = syncRequest.requestData.route as any;
|
||||
const ownership = syncRequest.requestData.ownership as any;
|
||||
|
||||
assertEquals(ownership, {
|
||||
gatewayClientType: 'onebox',
|
||||
gatewayClientId: 'onebox-token',
|
||||
appId: 'onebox-admin-ui',
|
||||
hostname: 'onebox.example.com',
|
||||
});
|
||||
assertEquals(route.match, { ports: [443], domains: ['onebox.example.com'] });
|
||||
assertEquals(route.action.targets, [{ host: '203.0.113.10', port: 8080 }]);
|
||||
assertEquals(syncRequest.requestData.enabled, true);
|
||||
});
|
||||
|
||||
Deno.test('ExternalGatewayManager uses managed dcrouter local target in managed mode', async () => {
|
||||
const oneboxRef = makeOneboxRef();
|
||||
(oneboxRef as any).managedDcRouter = {
|
||||
@@ -322,6 +363,206 @@ Deno.test('ExternalGatewayManager removes stale gateway routes during reconcilia
|
||||
assertEquals((deletes[0].ownership as any).hostname, 'stale.example.com');
|
||||
});
|
||||
|
||||
Deno.test('ExternalGatewayManager preserves configured Admin UI route during reconciliation', async () => {
|
||||
const oneboxRef = makeOneboxRef();
|
||||
oneboxRef.database.settings.set('adminUiDomain', 'onebox.example.com');
|
||||
oneboxRef.database.settings.set('serverIP', '203.0.113.10');
|
||||
oneboxRef.database.services.push({
|
||||
id: 1,
|
||||
name: 'active',
|
||||
image: 'nginx:latest',
|
||||
envVars: {},
|
||||
port: 3000,
|
||||
domain: 'active.example.com',
|
||||
status: 'running',
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
});
|
||||
|
||||
const deletes: Record<string, unknown>[] = [];
|
||||
const manager = new ExternalGatewayManager(oneboxRef as any);
|
||||
(manager as any).fireDcRouterRequest = async (method: string, requestData: Record<string, unknown>) => {
|
||||
if (method === 'getGatewayClientContext') {
|
||||
return { context: { role: 'gatewayClient', gatewayClient: { type: 'onebox', id: 'onebox-token' } } };
|
||||
}
|
||||
if (method === 'syncGatewayClientRoute') {
|
||||
if (requestData.delete) {
|
||||
deletes.push(requestData);
|
||||
return { success: true, action: 'deleted' };
|
||||
}
|
||||
return { success: true, action: 'updated' };
|
||||
}
|
||||
if (method === 'exportCertificate') {
|
||||
return { success: false };
|
||||
}
|
||||
if (method === 'getGatewayClientDnsRecords') {
|
||||
return {
|
||||
records: [
|
||||
{
|
||||
id: 'admin-record',
|
||||
domainId: 'domain-1',
|
||||
name: 'onebox',
|
||||
type: 'A',
|
||||
value: '203.0.113.10',
|
||||
ttl: 300,
|
||||
source: 'route',
|
||||
status: 'active',
|
||||
gatewayClientType: 'onebox',
|
||||
gatewayClientId: 'onebox-token',
|
||||
appId: 'onebox-admin-ui',
|
||||
hostname: 'onebox.example.com',
|
||||
routeId: 'admin-route',
|
||||
},
|
||||
{
|
||||
id: 'stale-record',
|
||||
domainId: 'domain-1',
|
||||
name: 'stale',
|
||||
type: 'A',
|
||||
value: '203.0.113.10',
|
||||
ttl: 300,
|
||||
source: 'route',
|
||||
status: 'active',
|
||||
gatewayClientType: 'onebox',
|
||||
gatewayClientId: 'onebox-token',
|
||||
appId: 'stale',
|
||||
hostname: 'stale.example.com',
|
||||
routeId: 'stale-route',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected method: ${method}`);
|
||||
};
|
||||
|
||||
await manager.syncServiceRoutes();
|
||||
|
||||
assertEquals(deletes.length, 1);
|
||||
assertEquals((deletes[0].ownership as any).hostname, 'stale.example.com');
|
||||
});
|
||||
|
||||
Deno.test('ExternalGatewayManager preserves legacy Admin UI route when setting is absent', async () => {
|
||||
const oneboxRef = makeOneboxRef();
|
||||
oneboxRef.database.settings.set('serverIP', '203.0.113.10');
|
||||
|
||||
const deletes: Record<string, unknown>[] = [];
|
||||
const manager = new ExternalGatewayManager(oneboxRef as any);
|
||||
(manager as any).fireDcRouterRequest = async (
|
||||
method: string,
|
||||
requestData: Record<string, unknown>,
|
||||
) => {
|
||||
if (method === 'getGatewayClientContext') {
|
||||
return {
|
||||
context: { role: 'gatewayClient', gatewayClient: { type: 'onebox', id: 'onebox-token' } },
|
||||
};
|
||||
}
|
||||
if (method === 'syncGatewayClientRoute') {
|
||||
if (requestData.delete) {
|
||||
deletes.push(requestData);
|
||||
return { success: true, action: 'deleted' };
|
||||
}
|
||||
return { success: true, action: 'updated' };
|
||||
}
|
||||
if (method === 'getGatewayClientDnsRecords') {
|
||||
return {
|
||||
records: [
|
||||
{
|
||||
id: 'legacy-admin-record',
|
||||
domainId: 'domain-1',
|
||||
name: 'onebox',
|
||||
type: 'A',
|
||||
value: '203.0.113.10',
|
||||
ttl: 300,
|
||||
source: 'route',
|
||||
status: 'active',
|
||||
gatewayClientType: 'onebox',
|
||||
gatewayClientId: 'onebox-token',
|
||||
appId: 'onebox',
|
||||
hostname: 'onebox.example.com',
|
||||
routeId: 'legacy-admin-route',
|
||||
},
|
||||
{
|
||||
id: 'stale-record',
|
||||
domainId: 'domain-1',
|
||||
name: 'stale',
|
||||
type: 'A',
|
||||
value: '203.0.113.10',
|
||||
ttl: 300,
|
||||
source: 'route',
|
||||
status: 'active',
|
||||
gatewayClientType: 'onebox',
|
||||
gatewayClientId: 'onebox-token',
|
||||
appId: 'stale',
|
||||
hostname: 'stale.example.com',
|
||||
routeId: 'stale-route',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected method: ${method}`);
|
||||
};
|
||||
|
||||
await manager.syncServiceRoutes();
|
||||
|
||||
assertEquals(deletes.length, 1);
|
||||
assertEquals((deletes[0].ownership as any).hostname, 'stale.example.com');
|
||||
});
|
||||
|
||||
Deno.test('ExternalGatewayManager deletes old Admin UI route after domain change', async () => {
|
||||
const oneboxRef = makeOneboxRef();
|
||||
oneboxRef.database.settings.set('adminUiDomain', 'new.example.com');
|
||||
oneboxRef.database.settings.set('serverIP', '203.0.113.10');
|
||||
|
||||
const deletes: Record<string, unknown>[] = [];
|
||||
const manager = new ExternalGatewayManager(oneboxRef as any);
|
||||
(manager as any).fireDcRouterRequest = async (
|
||||
method: string,
|
||||
requestData: Record<string, unknown>,
|
||||
) => {
|
||||
if (method === 'getGatewayClientContext') {
|
||||
return {
|
||||
context: { role: 'gatewayClient', gatewayClient: { type: 'onebox', id: 'onebox-token' } },
|
||||
};
|
||||
}
|
||||
if (method === 'syncGatewayClientRoute') {
|
||||
if (requestData.delete) {
|
||||
deletes.push(requestData);
|
||||
return { success: true, action: 'deleted' };
|
||||
}
|
||||
return { success: true, action: 'updated' };
|
||||
}
|
||||
if (method === 'exportCertificate') {
|
||||
return { success: false };
|
||||
}
|
||||
if (method === 'getGatewayClientDnsRecords') {
|
||||
return {
|
||||
records: [
|
||||
{
|
||||
id: 'old-admin-record',
|
||||
domainId: 'domain-1',
|
||||
name: 'onebox',
|
||||
type: 'A',
|
||||
value: '203.0.113.10',
|
||||
ttl: 300,
|
||||
source: 'route',
|
||||
status: 'active',
|
||||
gatewayClientType: 'onebox',
|
||||
gatewayClientId: 'onebox-token',
|
||||
appId: 'onebox-admin-ui',
|
||||
hostname: 'old.example.com',
|
||||
routeId: 'old-admin-route',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected method: ${method}`);
|
||||
};
|
||||
|
||||
await manager.syncServiceRoutes();
|
||||
|
||||
assertEquals(deletes.length, 1);
|
||||
assertEquals((deletes[0].ownership as any).hostname, 'old.example.com');
|
||||
});
|
||||
|
||||
Deno.test('ExternalGatewayManager imports exported dcrouter certificates into Onebox', async () => {
|
||||
const oneboxRef = makeOneboxRef();
|
||||
const manager = new ExternalGatewayManager(oneboxRef as any);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { assertEquals } from '@std/assert';
|
||||
|
||||
import { OneboxReverseProxy } from '../ts/classes/reverseproxy.ts';
|
||||
import type { IService } from '../ts/types.ts';
|
||||
|
||||
class FakeDatabase {
|
||||
public settings = new Map<string, string>();
|
||||
public services: IService[] = [];
|
||||
|
||||
getSetting(key: string): string | null {
|
||||
return this.settings.get(key) ?? null;
|
||||
}
|
||||
|
||||
getAllServices(): IService[] {
|
||||
return this.services;
|
||||
}
|
||||
|
||||
getServiceByID(id: number): IService | null {
|
||||
return this.services.find((service) => service.id === id) ?? null;
|
||||
}
|
||||
|
||||
getAllSSLCertificates(): [] {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Deno.test('OneboxReverseProxy loads Admin UI domain as local SmartProxy route', async () => {
|
||||
const database = new FakeDatabase();
|
||||
database.settings.set('adminUiDomain', 'onebox.example.com');
|
||||
database.settings.set('serverIP', '203.0.113.10');
|
||||
|
||||
const reverseProxy = new OneboxReverseProxy({ database } as any);
|
||||
const routes: Array<{ domain: string; upstream: string }> = [];
|
||||
(reverseProxy as any).smartProxy = {
|
||||
clear: () => routes.splice(0, routes.length),
|
||||
addRoute: async (domain: string, upstream: string) => {
|
||||
routes.push({ domain, upstream });
|
||||
},
|
||||
getCertificates: () => [],
|
||||
};
|
||||
|
||||
await reverseProxy.reloadRoutes();
|
||||
|
||||
assertEquals(routes, [
|
||||
{
|
||||
domain: 'onebox.example.com',
|
||||
upstream: '203.0.113.10:3000',
|
||||
},
|
||||
]);
|
||||
});
|
||||
Reference in New Issue
Block a user