Handle different routers based on target path

This commit is contained in:
Owen
2025-09-11 15:30:07 -07:00
parent 2efd5c31ab
commit 2551e0c291
3 changed files with 58 additions and 23 deletions

View File

@@ -115,7 +115,9 @@ export const targets = pgTable("targets", {
method: varchar("method"), method: varchar("method"),
port: integer("port").notNull(), port: integer("port").notNull(),
internalPort: integer("internalPort"), internalPort: integer("internalPort"),
enabled: boolean("enabled").notNull().default(true) enabled: boolean("enabled").notNull().default(true),
path: text("path"),
pathMatchType: text("pathMatchType"), // exact, prefix, regex
}); });
export const exitNodes = pgTable("exitNodes", { export const exitNodes = pgTable("exitNodes", {

View File

@@ -127,7 +127,9 @@ export const targets = sqliteTable("targets", {
method: text("method"), method: text("method"),
port: integer("port").notNull(), port: integer("port").notNull(),
internalPort: integer("internalPort"), internalPort: integer("internalPort"),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true) enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
path: text("path"),
pathMatchType: text("pathMatchType"), // exact, prefix, regex
}); });
export const exitNodes = sqliteTable("exitNodes", { export const exitNodes = sqliteTable("exitNodes", {

View File

@@ -105,11 +105,9 @@ export async function getTraefikConfig(
}; };
}; };
// Get all resources with related data
const allResources = await db.transaction(async (tx) => {
// Get resources with their targets and sites in a single optimized query // Get resources with their targets and sites in a single optimized query
// Start from sites on this exit node, then join to targets and resources // Start from sites on this exit node, then join to targets and resources
const resourcesWithTargetsAndSites = await tx const resourcesWithTargetsAndSites = await db
.select({ .select({
// Resource fields // Resource fields
resourceId: resources.resourceId, resourceId: resources.resourceId,
@@ -133,6 +131,9 @@ export async function getTraefikConfig(
method: targets.method, method: targets.method,
port: targets.port, port: targets.port,
internalPort: targets.internalPort, internalPort: targets.internalPort,
path: targets.path,
pathMatchType: targets.pathMatchType,
// Site fields // Site fields
siteId: sites.siteId, siteId: sites.siteId,
siteType: sites.type, siteType: sites.type,
@@ -163,9 +164,15 @@ export async function getTraefikConfig(
resourcesWithTargetsAndSites.forEach((row) => { resourcesWithTargetsAndSites.forEach((row) => {
const resourceId = row.resourceId; const resourceId = row.resourceId;
const targetPath = sanitizePath(row.path) || ""; // Handle null/undefined paths
const pathMatchType = row.pathMatchType || "";
if (!resourcesMap.has(resourceId)) { // Create a unique key combining resourceId and path+pathMatchType
resourcesMap.set(resourceId, { const pathKey = [targetPath, pathMatchType].filter(Boolean).join("-");
const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
if (!resourcesMap.has(mapKey)) {
resourcesMap.set(mapKey, {
resourceId: row.resourceId, resourceId: row.resourceId,
fullDomain: row.fullDomain, fullDomain: row.fullDomain,
ssl: row.ssl, ssl: row.ssl,
@@ -180,12 +187,14 @@ export async function getTraefikConfig(
setHostHeader: row.setHostHeader, setHostHeader: row.setHostHeader,
enableProxy: row.enableProxy, enableProxy: row.enableProxy,
targets: [], targets: [],
headers: row.headers headers: row.headers,
path: row.path, // the targets will all have the same path
pathMatchType: row.pathMatchType // the targets will all have the same pathMatchType
}); });
} }
// Add target with its associated site data // Add target with its associated site data
resourcesMap.get(resourceId).targets.push({ resourcesMap.get(mapKey).targets.push({
resourceId: row.resourceId, resourceId: row.resourceId,
targetId: row.targetId, targetId: row.targetId,
ip: row.ip, ip: row.ip,
@@ -203,10 +212,13 @@ export async function getTraefikConfig(
}); });
}); });
return Array.from(resourcesMap.values());
}); // convert the map to an object for printing
if (!allResources.length) { logger.debug(`Resources: ${JSON.stringify(Object.fromEntries(resourcesMap), null, 2)}`);
// make sure we have at least one resource
if (resourcesMap.size === 0) {
return {}; return {};
} }
@@ -222,14 +234,15 @@ export async function getTraefikConfig(
} }
}; };
for (const resource of allResources) { // get the key and the resource
for (const [key, resource] of resourcesMap.entries()) {
const targets = resource.targets; const targets = resource.targets;
const routerName = `${resource.resourceId}-router`; const routerName = `${key}-router`;
const serviceName = `${resource.resourceId}-service`; const serviceName = `${key}-service`;
const fullDomain = `${resource.fullDomain}`; const fullDomain = `${resource.fullDomain}`;
const transportName = `${resource.resourceId}-transport`; const transportName = `${key}-transport`;
const hostHeaderMiddlewareName = `${resource.resourceId}-host-header-middleware`; const headersMiddlewareName = `${key}-headers-middleware`;
if (!resource.enabled) { if (!resource.enabled) {
continue; continue;
@@ -241,9 +254,6 @@ export async function getTraefikConfig(
} }
if (!resource.fullDomain) { if (!resource.fullDomain) {
logger.error(
`Resource ${resource.resourceId} has no fullDomain`
);
continue; continue;
} }
@@ -305,7 +315,6 @@ export async function getTraefikConfig(
]; ];
if (resource.headers && resource.headers.length > 0) { if (resource.headers && resource.headers.length > 0) {
const headersMiddlewareName = `${resource.resourceId}-headers-middleware`;
// if there are headers, parse them into an object // if there are headers, parse them into an object
let headersObj: { [key: string]: string } = {}; let headersObj: { [key: string]: string } = {};
const headersArr = resource.headers.split(","); const headersArr = resource.headers.split(",");
@@ -338,6 +347,18 @@ export async function getTraefikConfig(
} }
} }
let rule = `Host(\`${fullDomain}\`)`;
if (resource.path && resource.pathMatchType) {
// add path to rule based on match type
if (resource.pathMatchType === "exact") {
rule += ` && Path(\`${resource.path}\`)`;
} else if (resource.pathMatchType === "prefix") {
rule += ` && PathPrefix(\`${resource.path}\`)`;
} else if (resource.pathMatchType === "regex") {
rule += ` && PathRegexp(\`${resource.path}\`)`;
}
}
config_output.http.routers![routerName] = { config_output.http.routers![routerName] = {
entryPoints: [ entryPoints: [
resource.ssl resource.ssl
@@ -346,7 +367,7 @@ export async function getTraefikConfig(
], ],
middlewares: routerMiddlewares, middlewares: routerMiddlewares,
service: serviceName, service: serviceName,
rule: `Host(\`${fullDomain}\`)`, rule: rule,
priority: 100, priority: 100,
...(resource.ssl ? { tls } : {}) ...(resource.ssl ? { tls } : {})
}; };
@@ -358,7 +379,7 @@ export async function getTraefikConfig(
], ],
middlewares: [redirectHttpsMiddlewareName], middlewares: [redirectHttpsMiddlewareName],
service: serviceName, service: serviceName,
rule: `Host(\`${fullDomain}\`)`, rule: rule,
priority: 100 priority: 100
}; };
} }
@@ -550,3 +571,13 @@ export async function getTraefikConfig(
} }
return config_output; return config_output;
} }
function sanitizePath(path: string | null | undefined): string | undefined {
if (!path) return undefined;
// clean any non alphanumeric characters from the path and replace with dashes
// the path cant be too long either, so limit to 50 characters
if (path.length > 50) {
path = path.substring(0, 50);
}
return path.replace(/[^a-zA-Z0-9]/g, "");
}