From 07bf2059c67f05bf2d201caa92f249756ef0cb35 Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Sat, 28 Sep 2024 22:50:10 -0400 Subject: [PATCH] added traefik config provider endpoint --- drizzle.config.ts | 4 +- scripts/migrate.ts | 5 +- server/db/schema.ts | 6 +- server/migrations/.gitkeep | 0 .../0000_wealthy_captain_midlands.sql | 59 --- server/migrations/meta/0000_snapshot.json | 394 ------------------ server/migrations/meta/_journal.json | 13 - server/routers/internal.ts | 3 + .../traefik-config-provider/configSchema.ts | 52 +++ server/traefik-config-provider/index.ts | 70 ++++ 10 files changed, 134 insertions(+), 472 deletions(-) create mode 100644 server/migrations/.gitkeep delete mode 100644 server/migrations/0000_wealthy_captain_midlands.sql delete mode 100644 server/migrations/meta/0000_snapshot.json delete mode 100644 server/migrations/meta/_journal.json create mode 100644 server/traefik-config-provider/configSchema.ts create mode 100644 server/traefik-config-provider/index.ts diff --git a/drizzle.config.ts b/drizzle.config.ts index a0df5c5..782e1c9 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -4,8 +4,8 @@ import path from "path"; export default defineConfig({ dialect: "sqlite", - schema: "server/db/schema.ts", - out: "server/migrations", + schema: path.join(__dirname, "server", "db", "schema.ts"), + out: path.join(__dirname, "server", "migrations"), verbose: true, dbCredentials: { url: path.join(environment.CONFIG_PATH, "db", "db.sqlite"), diff --git a/scripts/migrate.ts b/scripts/migrate.ts index 9ac1a75..a0d2659 100644 --- a/scripts/migrate.ts +++ b/scripts/migrate.ts @@ -1,10 +1,13 @@ import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import db from "@server/db"; +import path from "path"; const runMigrations = async () => { console.log("Running migrations..."); try { - migrate(db, { migrationsFolder: "./server/migrations" }); + migrate(db, { + migrationsFolder: path.join(__dirname, "server/migrations"), + }); console.log("Migrations completed successfully."); } catch (error) { console.error("Error running migrations:", error); diff --git a/server/db/schema.ts b/server/db/schema.ts index 0c29459..0822223 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -41,8 +41,8 @@ export const targets = sqliteTable("targets", { targetId: integer("targetId").primaryKey({ autoIncrement: true }), resourceId: text("resourceId").references(() => resources.resourceId, { onDelete: "cascade" }), ip: text("ip").notNull(), - method: text("method"), - port: integer("port"), + method: text("method").notNull(), + port: integer("port").notNull(), protocol: text("protocol"), }); @@ -69,4 +69,4 @@ export type Site = InferSelectModel; export type Resource = InferSelectModel; export type ExitNode = InferSelectModel; export type Route = InferSelectModel; -export type Target = InferSelectModel; \ No newline at end of file +export type Target = InferSelectModel; diff --git a/server/migrations/.gitkeep b/server/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/server/migrations/0000_wealthy_captain_midlands.sql b/server/migrations/0000_wealthy_captain_midlands.sql deleted file mode 100644 index 0643a3d..0000000 --- a/server/migrations/0000_wealthy_captain_midlands.sql +++ /dev/null @@ -1,59 +0,0 @@ -CREATE TABLE `exitNodes` ( - `exitNodeId` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `name` text NOT NULL, - `address` text NOT NULL, - `privateKey` text, - `listenPort` integer -); ---> statement-breakpoint -CREATE TABLE `orgs` ( - `orgId` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `name` text NOT NULL, - `domain` text NOT NULL -); ---> statement-breakpoint -CREATE TABLE `resources` ( - `resourceId` text(2048) PRIMARY KEY NOT NULL, - `siteId` integer, - `name` text NOT NULL, - `subdomain` text, - FOREIGN KEY (`siteId`) REFERENCES `sites`(`siteId`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE TABLE `routes` ( - `routeId` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `exitNodeId` integer, - `subnet` text NOT NULL, - FOREIGN KEY (`exitNodeId`) REFERENCES `exitNodes`(`exitNodeId`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE TABLE `sites` ( - `siteId` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `orgId` integer, - `exitNode` integer, - `name` text NOT NULL, - `subdomain` text, - `pubKey` text, - `subnet` text, - FOREIGN KEY (`orgId`) REFERENCES `orgs`(`orgId`) ON UPDATE no action ON DELETE cascade, - FOREIGN KEY (`exitNode`) REFERENCES `exitNodes`(`exitNodeId`) ON UPDATE no action ON DELETE set null -); ---> statement-breakpoint -CREATE TABLE `targets` ( - `targetId` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `resourceId` text, - `ip` text NOT NULL, - `method` text, - `port` integer, - `protocol` text, - FOREIGN KEY (`resourceId`) REFERENCES `resources`(`resourceId`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE TABLE `users` ( - `userId` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `orgId` integer, - `name` text NOT NULL, - `email` text NOT NULL, - `groups` text, - FOREIGN KEY (`orgId`) REFERENCES `orgs`(`orgId`) ON UPDATE no action ON DELETE cascade -); diff --git a/server/migrations/meta/0000_snapshot.json b/server/migrations/meta/0000_snapshot.json deleted file mode 100644 index 99da50f..0000000 --- a/server/migrations/meta/0000_snapshot.json +++ /dev/null @@ -1,394 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "379ca2f9-068a-4289-8a23-001e7dc269b1", - "prevId": "00000000-0000-0000-0000-000000000000", - "tables": { - "exitNodes": { - "name": "exitNodes", - "columns": { - "exitNodeId": { - "name": "exitNodeId", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "address": { - "name": "address", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "privateKey": { - "name": "privateKey", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "listenPort": { - "name": "listenPort", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "orgs": { - "name": "orgs", - "columns": { - "orgId": { - "name": "orgId", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "domain": { - "name": "domain", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "resources": { - "name": "resources", - "columns": { - "resourceId": { - "name": "resourceId", - "type": "text(2048)", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "siteId": { - "name": "siteId", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "subdomain": { - "name": "subdomain", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "resources_siteId_sites_siteId_fk": { - "name": "resources_siteId_sites_siteId_fk", - "tableFrom": "resources", - "tableTo": "sites", - "columnsFrom": [ - "siteId" - ], - "columnsTo": [ - "siteId" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "routes": { - "name": "routes", - "columns": { - "routeId": { - "name": "routeId", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "exitNodeId": { - "name": "exitNodeId", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "subnet": { - "name": "subnet", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "routes_exitNodeId_exitNodes_exitNodeId_fk": { - "name": "routes_exitNodeId_exitNodes_exitNodeId_fk", - "tableFrom": "routes", - "tableTo": "exitNodes", - "columnsFrom": [ - "exitNodeId" - ], - "columnsTo": [ - "exitNodeId" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "sites": { - "name": "sites", - "columns": { - "siteId": { - "name": "siteId", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "orgId": { - "name": "orgId", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "exitNode": { - "name": "exitNode", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "subdomain": { - "name": "subdomain", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "pubKey": { - "name": "pubKey", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "subnet": { - "name": "subnet", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "sites_orgId_orgs_orgId_fk": { - "name": "sites_orgId_orgs_orgId_fk", - "tableFrom": "sites", - "tableTo": "orgs", - "columnsFrom": [ - "orgId" - ], - "columnsTo": [ - "orgId" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "sites_exitNode_exitNodes_exitNodeId_fk": { - "name": "sites_exitNode_exitNodes_exitNodeId_fk", - "tableFrom": "sites", - "tableTo": "exitNodes", - "columnsFrom": [ - "exitNode" - ], - "columnsTo": [ - "exitNodeId" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "targets": { - "name": "targets", - "columns": { - "targetId": { - "name": "targetId", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "resourceId": { - "name": "resourceId", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "ip": { - "name": "ip", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "method": { - "name": "method", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "port": { - "name": "port", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protocol": { - "name": "protocol", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "targets_resourceId_resources_resourceId_fk": { - "name": "targets_resourceId_resources_resourceId_fk", - "tableFrom": "targets", - "tableTo": "resources", - "columnsFrom": [ - "resourceId" - ], - "columnsTo": [ - "resourceId" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "users": { - "name": "users", - "columns": { - "userId": { - "name": "userId", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "orgId": { - "name": "orgId", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "groups": { - "name": "groups", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "users_orgId_orgs_orgId_fk": { - "name": "users_orgId_orgs_orgId_fk", - "tableFrom": "users", - "tableTo": "orgs", - "columnsFrom": [ - "orgId" - ], - "columnsTo": [ - "orgId" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - } - }, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/server/migrations/meta/_journal.json b/server/migrations/meta/_journal.json deleted file mode 100644 index 4eb8fe0..0000000 --- a/server/migrations/meta/_journal.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "entries": [ - { - "idx": 0, - "version": "6", - "when": 1727557783608, - "tag": "0000_wealthy_captain_midlands", - "breakpoints": true - } - ] -} \ No newline at end of file diff --git a/server/routers/internal.ts b/server/routers/internal.ts index 2467f93..06e38eb 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -1,6 +1,7 @@ import { Router } from "express"; import badger from "./badger/badger"; import gerbil from "./gerbil/gerbil"; +import { traefikConfigProvider } from "@server/traefik-config-provider"; const unauth = Router(); @@ -11,4 +12,6 @@ unauth.get("/", (_, res) => { unauth.use("/badger", badger); unauth.use("/gerbil", gerbil); +unauth.get("/traefik-config-provider", traefikConfigProvider); + export default unauth; diff --git a/server/traefik-config-provider/configSchema.ts b/server/traefik-config-provider/configSchema.ts new file mode 100644 index 0000000..d23630b --- /dev/null +++ b/server/traefik-config-provider/configSchema.ts @@ -0,0 +1,52 @@ +export type DynamicTraefikConfig = { + http: Http; +}; + +export type Http = { + routers: Routers; + services: Services; + middlewares: Middlewares; +}; + +export type Routers = { + [key: string]: Router; +}; + +export type Router = { + entryPoints: string[]; + middlewares: string[]; + service: string; + rule: string; +}; + +export type Services = { + [key: string]: Service; +}; + +export type Service = { + loadBalancer: LoadBalancer; +}; + +export type LoadBalancer = { + servers: Server[]; +}; + +export type Server = { + url: string; +}; + +export type Middlewares = { + [key: string]: MiddlewarePlugin; +}; + +export type MiddlewarePlugin = { + plugin: Plugin; +}; + +export type Plugin = { + [key: string]: MiddlewarePluginConfig; +}; + +export type MiddlewarePluginConfig = { + [key: string]: any; +}; diff --git a/server/traefik-config-provider/index.ts b/server/traefik-config-provider/index.ts new file mode 100644 index 0000000..1de0a3e --- /dev/null +++ b/server/traefik-config-provider/index.ts @@ -0,0 +1,70 @@ +import { Request, Response } from "express"; +import db from "@server/db"; +import * as schema from "@server/db/schema"; +import { DynamicTraefikConfig } from "./configSchema"; +import { like } from "drizzle-orm"; +import logger from "@server/logger"; + +export async function traefikConfigProvider(_: Request, res: Response) { + try { + const targets = await getAllTargets(); + const traefikConfig = buildTraefikConfig(targets); + res.status(200).send(traefikConfig); + } catch (e) { + logger.error(`Failed to build traefik config: ${e}`); + res.status(500).send({ message: "Failed to build traefik config" }); + } +} + +export function buildTraefikConfig( + targets: schema.Target[], +): DynamicTraefikConfig { + const middlewareName = "gerbil"; + + const http: DynamicTraefikConfig["http"] = { + routers: {}, + services: {}, + middlewares: { + [middlewareName]: { + plugin: { + [middlewareName]: { + // These are temporary values + APIEndpoint: + "http://host.docker.internal:3001/api/v1/gerbil", + ValidToken: "abc123", + }, + }, + }, + }, + }; + + for (const target of targets) { + const routerName = `router-${target.targetId}`; + const serviceName = `service-${target.targetId}`; + + http.routers[routerName] = { + entryPoints: [target.method], + middlewares: [middlewareName], + service: serviceName, + rule: `Host(\`${target.resourceId}\`)`, // assuming resourceId is a valid full hostname + }; + + http.services[serviceName] = { + loadBalancer: { + servers: [ + { url: `${target.method}://${target.ip}:${target.port}` }, + ], + }, + }; + } + + return { http } as DynamicTraefikConfig; +} + +export async function getAllTargets(): Promise { + const all = await db + .select() + .from(schema.targets) + .where(like(schema.targets.resourceId, "%.%")); // any resourceId with a dot is a valid hostname; otherwise it's a UUID placeholder + return all; +}