Files
harbor-net/src/server/features/routing/routeRulesService.ts
T
dokril daec12e013
Build and Deploy Gateway / build-and-push (push) Failing after 14s
Build and Deploy Gateway / deploy (push) Has been skipped
Update Harbor client and gateway integration workflows
2026-08-19 18:16:10 +03:00

151 lines
5.3 KiB
TypeScript

import { isDeepStrictEqual } from 'node:util';
import {
appliedProfile,
desiredProfile,
type RouteRule,
type StoredState,
} from '../../../shared/contracts/state.js';
import { HarborError } from '../../../shared/errors.js';
import {
normalizeRouteRules,
ROUTE_RULES_CONTRACT_VERSION,
} from '../../../shared/routingRules.js';
import type { RuntimeCommandResult } from '../connection/index.js';
import { finishRollback } from '../../services/rollback.js';
interface RouteRulesDependencies {
state: {
read(): StoredState;
update(mutator: (state: StoredState) => Record<string, unknown>): StoredState;
};
subscription: { readConfig(profileId: string): unknown | null };
config: {
build(subscriptionConfig: unknown, selectedServerId: string, routeRules: RouteRule[]): unknown;
read(): string | null;
write(value: unknown): void;
restore(value: string): void;
remove(): void;
};
runtime: {
isRunning(): Promise<boolean>;
applyCommand(): Promise<RuntimeCommandResult>;
restoreRunning(): Promise<unknown>;
};
route?: { isGatewayDirect(): boolean };
serialize<T>(operation: () => Promise<T>): Promise<T>;
runOperation<T>(operation: () => Promise<T>): Promise<T>;
afterApply?: () => Promise<unknown>;
restoreAppliedActivation?: (state: StoredState) => Promise<unknown>;
}
export function createRouteRulesService(dependencies: RouteRulesDependencies) {
const applyRules = async (previousState: StoredState, routeRules: RouteRule[]) => {
const wasRunning = await dependencies.runtime.isRunning();
const bypassed = dependencies.route?.isGatewayDirect() === true;
const targetProfile = wasRunning
? appliedProfile(previousState)
: desiredProfile(previousState);
const targetServerId = wasRunning
? previousState.appliedServerId
: targetProfile?.desiredServerId || '';
const subscriptionConfig = targetProfile
? dependencies.subscription.readConfig(targetProfile.id)
: null;
if (bypassed || !targetServerId || !subscriptionConfig) {
let stateCommitStarted = false;
try {
stateCommitStarted = true;
dependencies.state.update((state) => ({
...state,
routeRules,
...(bypassed ? { appliedRouteRules: [] } : {}),
routeRulesRevision: state.routeRulesRevision + 1,
}));
} catch (error) {
await finishRollback(error, [
...(stateCommitStarted ? [{ run: () => dependencies.state.update(() => previousState) }] : []),
], 'Route rules rollback failed');
}
return;
}
const candidateConfig = dependencies.config.build(
subscriptionConfig,
targetServerId,
routeRules,
);
const previousConfig = dependencies.config.read();
const configChanged = previousConfig !== JSON.stringify(candidateConfig, null, 2);
let configMutationStarted = false;
let runtimeMutationStarted = false;
let stateCommitStarted = false;
try {
if (configChanged) {
configMutationStarted = true;
dependencies.config.write(candidateConfig);
}
if (wasRunning && configChanged) {
const command = await dependencies.runtime.applyCommand();
runtimeMutationStarted = command.mutationStarted;
if (!command.ok) throw command.error;
}
stateCommitStarted = true;
dependencies.state.update((state) => ({
...state,
routeRules,
...(wasRunning ? { appliedRouteRules: routeRules } : {}),
routeRulesRevision: state.routeRulesRevision + 1,
}));
await dependencies.afterApply?.();
} catch (error) {
await finishRollback(error, [
...(configMutationStarted ? [{
run: () => previousConfig === null
? dependencies.config.remove()
: dependencies.config.restore(previousConfig),
}] : []),
...(wasRunning && runtimeMutationStarted ? [{
run: async () => {
await dependencies.runtime.restoreRunning();
await dependencies.restoreAppliedActivation?.(previousState);
},
runtime: true,
}] : []),
...(stateCommitStarted ? [{ run: () => dependencies.state.update(() => previousState) }] : []),
], 'Route rules rollback failed');
}
};
const update = (
rules: unknown,
expectedRulesRevision: unknown,
rulesContractVersion: unknown,
) => {
if (rulesContractVersion !== ROUTE_RULES_CONTRACT_VERSION) {
throw new HarborError('REQUEST_INVALID');
}
let routeRules: RouteRule[];
try {
routeRules = normalizeRouteRules(rules, { strict: true }) as RouteRule[];
} catch (cause) {
throw new HarborError('REQUEST_INVALID', { cause });
}
if (!Number.isSafeInteger(expectedRulesRevision) || Number(expectedRulesRevision) < 0) {
throw new HarborError('REQUEST_INVALID');
}
return dependencies.serialize(async () => {
const current = dependencies.state.read();
if (current.routeRulesRevision !== expectedRulesRevision) throw new HarborError('STATE_CONFLICT');
if (isDeepStrictEqual(current.routeRules, routeRules)) return;
await dependencies.runOperation(() => applyRules(current, routeRules));
});
};
return { update };
}
export type RouteRulesService = ReturnType<typeof createRouteRulesService>;