feat(admin-ui): add configurable Admin UI domain routing
This commit is contained in:
+102
-17
@@ -1,11 +1,17 @@
|
||||
import * as plugins from '../plugins.ts';
|
||||
import { logger } from '../logging.ts';
|
||||
import { getErrorMessage } from '../utils/error.ts';
|
||||
import { normalizeHostname } from '../utils/domain.ts';
|
||||
import { OneboxDatabase } from './database.ts';
|
||||
import type { IDomain, IService } from '../types.ts';
|
||||
import type { TDcRouterMode } from './managed-dcrouter.ts';
|
||||
|
||||
const adminUiRouteName = 'onebox-admin-ui';
|
||||
|
||||
type TWorkHosterType = 'onebox';
|
||||
type TExternalGatewayRoute = Pick<IService, 'id' | 'name' | 'domain' | 'status'> & {
|
||||
domain: string;
|
||||
};
|
||||
|
||||
interface IExternalGatewayConfig {
|
||||
url: string;
|
||||
@@ -137,15 +143,34 @@ export class ExternalGatewayManager {
|
||||
}
|
||||
|
||||
public async syncServiceRoutes(): Promise<void> {
|
||||
const adminUiRoute = this.getAdminUiRoute();
|
||||
const adminUiDomain = adminUiRoute?.domain;
|
||||
const services = this.database.getAllServices()
|
||||
.filter((service) => service.domain && service.status === 'running');
|
||||
.filter((service) =>
|
||||
service.domain && service.status === 'running' && service.domain !== adminUiDomain
|
||||
);
|
||||
const activeHostnames = new Set(services.map((service) => service.domain!));
|
||||
|
||||
if (adminUiRoute) {
|
||||
activeHostnames.add(adminUiRoute.domain);
|
||||
try {
|
||||
await this.syncGatewayRoute(adminUiRoute);
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`Failed to sync external gateway route for ${adminUiRoute.domain}: ${
|
||||
getErrorMessage(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const service of services) {
|
||||
try {
|
||||
await this.syncServiceRoute(service);
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to sync external gateway route for ${service.domain}: ${getErrorMessage(error)}`);
|
||||
logger.warn(
|
||||
`Failed to sync external gateway route for ${service.domain}: ${getErrorMessage(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +183,7 @@ export class ExternalGatewayManager {
|
||||
|
||||
for (const record of records) {
|
||||
if (!record.hostname || activeHostnamesArg.has(record.hostname)) continue;
|
||||
if (this.shouldPreserveUnconfiguredAdminUiRecord(record)) continue;
|
||||
if (!record.routeId && !record.appId && !record.serviceName) continue;
|
||||
staleRecordsByHostname.set(record.hostname, record);
|
||||
}
|
||||
@@ -169,7 +195,11 @@ export class ExternalGatewayManager {
|
||||
domain: record.hostname,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to delete stale external gateway route for ${record.hostname}: ${getErrorMessage(error)}`);
|
||||
logger.warn(
|
||||
`Failed to delete stale external gateway route for ${record.hostname}: ${
|
||||
getErrorMessage(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,40 +319,72 @@ export class ExternalGatewayManager {
|
||||
public async syncServiceRoute(service: IService): Promise<void> {
|
||||
if (!service.domain) return;
|
||||
|
||||
await this.syncGatewayRoute({
|
||||
id: service.id,
|
||||
name: service.name,
|
||||
domain: service.domain,
|
||||
status: service.status,
|
||||
});
|
||||
}
|
||||
|
||||
public async syncAdminUiRoute(): Promise<void> {
|
||||
const route = this.getAdminUiRoute();
|
||||
if (!route) return;
|
||||
await this.syncGatewayRoute(route);
|
||||
}
|
||||
|
||||
public async deleteAdminUiRoute(domain: string): Promise<void> {
|
||||
const normalizedDomain = normalizeHostname(domain);
|
||||
if (!normalizedDomain) return;
|
||||
await this.deleteServiceRoute({
|
||||
name: adminUiRouteName,
|
||||
domain: normalizedDomain,
|
||||
});
|
||||
}
|
||||
|
||||
private async syncGatewayRoute(route: TExternalGatewayRoute): Promise<void> {
|
||||
if (!route.domain) return;
|
||||
|
||||
const config = await this.getConfig({ requireTarget: true });
|
||||
if (!config) return;
|
||||
|
||||
const result = await this.fireDcRouterRequest<IWorkAppRouteSyncResult>(
|
||||
'syncGatewayClientRoute',
|
||||
{
|
||||
ownership: this.buildGatewayClientOwnership(service, service.domain, config),
|
||||
route: this.buildRoute(service, config),
|
||||
enabled: service.status === 'running',
|
||||
ownership: this.buildGatewayClientOwnership(route, route.domain, config),
|
||||
route: this.buildRoute(route, config),
|
||||
enabled: route.status === 'running',
|
||||
},
|
||||
config,
|
||||
).catch(async () => {
|
||||
return await this.fireDcRouterRequest<IWorkAppRouteSyncResult>(
|
||||
'syncWorkAppRoute',
|
||||
{
|
||||
ownership: this.buildOwnership(service, service.domain!, config),
|
||||
route: this.buildRoute(service, config),
|
||||
enabled: service.status === 'running',
|
||||
ownership: this.buildOwnership(route, route.domain, config),
|
||||
route: this.buildRoute(route, config),
|
||||
enabled: route.status === 'running',
|
||||
},
|
||||
config,
|
||||
);
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || `dcrouter route sync failed for ${service.domain}`);
|
||||
throw new Error(result.message || `dcrouter route sync failed for ${route.domain}`);
|
||||
}
|
||||
|
||||
logger.success(`External gateway route ${result.action || 'synced'} for ${service.domain}`);
|
||||
await this.importCertificateForDomain(service.domain).catch((error) => {
|
||||
logger.debug(`External gateway certificate import skipped for ${service.domain}: ${getErrorMessage(error)}`);
|
||||
logger.success(`External gateway route ${result.action || 'synced'} for ${route.domain}`);
|
||||
await this.importCertificateForDomain(route.domain).catch((error) => {
|
||||
logger.debug(
|
||||
`External gateway certificate import skipped for ${route.domain}: ${
|
||||
getErrorMessage(error)
|
||||
}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteServiceRoute(service: Pick<IService, 'id' | 'name' | 'domain'>): Promise<void> {
|
||||
public async deleteServiceRoute(
|
||||
service: Pick<IService, 'id' | 'name' | 'domain'>,
|
||||
): Promise<void> {
|
||||
if (!service.domain) return;
|
||||
|
||||
const config = await this.getConfig({ requireTarget: false });
|
||||
@@ -536,12 +598,35 @@ export class ExternalGatewayManager {
|
||||
return ownership;
|
||||
}
|
||||
|
||||
private buildRoute(service: IService, config: IExternalGatewayConfig): IDcRouterRouteConfig {
|
||||
private getAdminUiRoute(): TExternalGatewayRoute | null {
|
||||
const domain = normalizeHostname(this.database.getSetting('adminUiDomain') || '');
|
||||
if (!domain) return null;
|
||||
return {
|
||||
name: this.routeName(service.domain!),
|
||||
id: 0,
|
||||
name: adminUiRouteName,
|
||||
domain,
|
||||
status: 'running',
|
||||
};
|
||||
}
|
||||
|
||||
private isAdminUiRecord(record: IGatewayDnsRecord): boolean {
|
||||
const ownerName = record.serviceName || record.appId;
|
||||
return ownerName === adminUiRouteName || ownerName === 'onebox';
|
||||
}
|
||||
|
||||
private shouldPreserveUnconfiguredAdminUiRecord(record: IGatewayDnsRecord): boolean {
|
||||
return this.database.getSetting('adminUiDomain') === null && this.isAdminUiRecord(record);
|
||||
}
|
||||
|
||||
private buildRoute(
|
||||
route: TExternalGatewayRoute,
|
||||
config: IExternalGatewayConfig,
|
||||
): IDcRouterRouteConfig {
|
||||
return {
|
||||
name: this.routeName(route.domain),
|
||||
match: {
|
||||
ports: [443],
|
||||
domains: [service.domain!],
|
||||
domains: [route.domain],
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
|
||||
Reference in New Issue
Block a user