Add local routing rules to Harbor
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 17s
Build and Deploy Gateway / deploy (push) Successful in 6s

This commit is contained in:
2026-07-11 21:52:03 +03:00
parent 306a9b8ced
commit a0c66edb02
17 changed files with 823 additions and 26 deletions

View File

@@ -1,3 +1,5 @@
import { BUILT_IN_DIRECT_RULES, normalizeRouteRules } from '../routingRules.js';
const MODES = new Set(['client', 'gateway']);
const CONNECTION_STATES = new Set(['running', 'stopped']);
const OPERATION_STATES = new Set(['idle', 'running', 'failed']);
@@ -17,6 +19,7 @@ export function normalizeStoredState(value) {
selectedTag,
appliedTag: Object.hasOwn(state, 'appliedTag') ? text(state.appliedTag) : selectedTag,
servers: Array.isArray(state.servers) ? state.servers : [],
routeRules: normalizeRouteRules(state.routeRules),
};
}
@@ -80,6 +83,10 @@ export function createStateSnapshot({
gatewayAddress: mode === 'client' ? gatewayAuto?.gateway?.gateway || null : null,
lastVerifiedAt: null,
reason: mode === 'client' && stored.gatewayAutoEnabled !== false ? 'auto' : 'manual',
localRules: {
builtIn: BUILT_IN_DIRECT_RULES.map((rule) => ({ ...rule })),
custom: stored.routeRules,
},
},
operation: {
kind: nullableText(operation.kind),
@@ -136,6 +143,12 @@ export function assertStateSnapshot(snapshot) {
server.port >= 0 &&
typeof server.protocol === 'string'
);
const validRouteRule = (rule) => (
rule &&
['domain', 'domain_suffix', 'domain_keyword'].includes(rule.type) &&
typeof rule.value === 'string' &&
Boolean(rule.value)
);
if (
!snapshot ||
@@ -164,6 +177,11 @@ export function assertStateSnapshot(snapshot) {
!nullableString(snapshot.route.gatewayAddress) ||
!nullableDate(snapshot.route.lastVerifiedAt) ||
typeof snapshot.route.reason !== 'string' ||
!snapshot.route.localRules ||
!Array.isArray(snapshot.route.localRules.builtIn) ||
!snapshot.route.localRules.builtIn.every(validRouteRule) ||
!Array.isArray(snapshot.route.localRules.custom) ||
!snapshot.route.localRules.custom.every(validRouteRule) ||
!snapshot.operation ||
!nullableString(snapshot.operation.kind) ||
!OPERATION_STATES.has(snapshot.operation.status) ||

View File

@@ -0,0 +1,52 @@
export const BUILT_IN_DIRECT_RULES = Object.freeze([
Object.freeze({ type: 'domain_suffix', value: 'ru' }),
]);
const RULE_TYPES = new Set(['domain', 'domain_suffix', 'domain_keyword']);
const MAX_RULES = 200;
function hostname(value) {
const input = String(value || '').trim().replace(/^\*\./, '').replace(/^\./, '');
if (!input) throw new TypeError('Domain rule value is required');
const url = new URL(/^[a-z][a-z\d+.-]*:\/\//i.test(input) ? input : `https://${input}`);
const normalized = url.hostname.replace(/\.$/, '').toLowerCase();
if (!normalized || normalized.length > 253) throw new TypeError('Invalid domain rule value');
return normalized;
}
function normalizeRule(rule) {
const type = String(rule?.type || '').trim();
if (!RULE_TYPES.has(type)) throw new TypeError('Invalid domain rule type');
const value = type === 'domain_keyword'
? String(rule?.value || '').trim().toLowerCase()
: hostname(rule?.value);
if (!value || value.length > 253 || /[\s/:?#]/.test(value)) {
throw new TypeError('Invalid domain rule value');
}
return { type, value };
}
export function normalizeRouteRules(value, { strict = false } = {}) {
if (!Array.isArray(value)) {
if (strict) throw new TypeError('Route rules must be an array');
return [];
}
if (strict && value.length > MAX_RULES) throw new TypeError(`Route rules limit is ${MAX_RULES}`);
const seen = new Set(BUILT_IN_DIRECT_RULES.map(({ type, value: builtInValue }) => (
`${type}:${builtInValue}`
)));
const normalized = [];
for (const candidate of value.slice(0, MAX_RULES)) {
try {
const rule = normalizeRule(candidate);
const key = `${rule.type}:${rule.value}`;
if (seen.has(key)) continue;
seen.add(key);
normalized.push(rule);
} catch (error) {
if (strict) throw error;
}
}
return normalized;
}

View File

@@ -1,7 +1,7 @@
export const HARBOR_VERSIONS = Object.freeze({
macClient: '0.2.0',
gatewayClient: '0.2.0',
gatewayBackend: '0.2.0',
macClient: '0.3.0',
gatewayClient: '0.3.0',
gatewayBackend: '0.3.0',
});
export function parseVersion(value) {