diff --git a/__test__/traefik/traefik.test.ts b/__test__/traefik/traefik.test.ts new file mode 100644 index 00000000..f10eefb3 --- /dev/null +++ b/__test__/traefik/traefik.test.ts @@ -0,0 +1,187 @@ +import type { Domain } from "@/server/api/services/domain"; +import type { Redirect } from "@/server/api/services/redirect"; +import type { ApplicationNested } from "@/server/utils/builders"; +import { createRouterConfig } from "@/server/utils/traefik/domain"; +import { expect, test } from "vitest"; + +const baseApp: ApplicationNested = { + applicationId: "", + applicationStatus: "done", + appName: "", + autoDeploy: true, + branch: null, + buildArgs: null, + buildPath: "/", + buildType: "nixpacks", + command: null, + cpuLimit: null, + cpuReservation: null, + createdAt: "", + customGitBranch: "", + customGitBuildPath: "", + customGitSSHKey: "", + customGitUrl: "", + description: "", + dockerfile: null, + dockerImage: null, + dropBuildPath: null, + enabled: null, + env: null, + healthCheckSwarm: null, + labelsSwarm: null, + memoryLimit: null, + memoryReservation: null, + modeSwarm: null, + mounts: [], + name: "", + networkSwarm: null, + owner: null, + password: null, + placementSwarm: null, + ports: [], + projectId: "", + redirects: [], + refreshToken: "", + registry: null, + registryId: null, + replicas: 1, + repository: null, + restartPolicySwarm: null, + rollbackConfigSwarm: null, + security: [], + sourceType: "git", + subtitle: null, + title: null, + updateConfigSwarm: null, + username: null, +}; + +const baseDomain: Domain = { + applicationId: "", + certificateType: "none", + createdAt: "", + domainId: "", + host: "", + https: false, + path: null, + port: null, + uniqueConfigKey: 1, +}; + +const baseRedirect: Redirect = { + redirectId: "", + regex: "", + replacement: "", + permanent: false, + uniqueConfigKey: 1, + createdAt: "", + applicationId: "", +}; + +/** Middlewares */ + +test("Web entrypoint on http domain", async () => { + const router = await createRouterConfig( + baseApp, + { ...baseDomain, https: false }, + "web", + ); + + expect(router.middlewares).not.toContain("redirect-to-https"); +}); + +test("Web entrypoint on http domain with redirect", async () => { + const router = await createRouterConfig( + { + ...baseApp, + appName: "test", + redirects: [{ ...baseRedirect, uniqueConfigKey: 1 }], + }, + { ...baseDomain, https: false }, + "web", + ); + + expect(router.middlewares).not.toContain("redirect-to-https"); + expect(router.middlewares).toContain("redirect-test-1"); +}); + +test("Web entrypoint on http domain with multiple redirect", async () => { + const router = await createRouterConfig( + { + ...baseApp, + appName: "test", + redirects: [ + { ...baseRedirect, uniqueConfigKey: 1 }, + { ...baseRedirect, uniqueConfigKey: 2 }, + ], + }, + { ...baseDomain, https: false }, + "web", + ); + + expect(router.middlewares).not.toContain("redirect-to-https"); + expect(router.middlewares).toContain("redirect-test-1"); + expect(router.middlewares).toContain("redirect-test-2"); +}); + +test("Web entrypoint on https domain", async () => { + const router = await createRouterConfig( + baseApp, + { ...baseDomain, https: true }, + "web", + ); + + expect(router.middlewares).toContain("redirect-to-https"); +}); + +test("Web entrypoint on https domain with redirect", async () => { + const router = await createRouterConfig( + { + ...baseApp, + appName: "test", + redirects: [{ ...baseRedirect, uniqueConfigKey: 1 }], + }, + { ...baseDomain, https: true }, + "web", + ); + + expect(router.middlewares).toContain("redirect-to-https"); + expect(router.middlewares).not.toContain("redirect-test-1"); +}); + +test("Websecure entrypoint on https domain", async () => { + const router = await createRouterConfig( + baseApp, + { ...baseDomain, https: true }, + "websecure", + ); + + expect(router.middlewares).not.toContain("redirect-to-https"); +}); + +test("Websecure entrypoint on https domain with redirect", async () => { + const router = await createRouterConfig( + { + ...baseApp, + appName: "test", + redirects: [{ ...baseRedirect, uniqueConfigKey: 1 }], + }, + { ...baseDomain, https: true }, + "websecure", + ); + + expect(router.middlewares).not.toContain("redirect-to-https"); + expect(router.middlewares).toContain("redirect-test-1"); +}); + +/** Certificates */ + +test("CertificateType on websecure entrypoint", async () => { + const router = await createRouterConfig( + baseApp, + { ...baseDomain, certificateType: "letsencrypt" }, + "websecure", + ); + + expect(router.tls?.certResolver).toBe("letsencrypt"); +}); diff --git a/server/utils/traefik/domain.ts b/server/utils/traefik/domain.ts index be7a7fac..401c7a8e 100644 --- a/server/utils/traefik/domain.ts +++ b/server/utils/traefik/domain.ts @@ -13,12 +13,27 @@ export const manageDomain = async (app: ApplicationNested, domain: Domain) => { const config: FileConfig = loadOrCreateConfig(appName); const serviceName = `${appName}-service-${domain.uniqueConfigKey}`; const routerName = `${appName}-router-${domain.uniqueConfigKey}`; + const routerNameSecure = `${appName}-router-websecure-${domain.uniqueConfigKey}`; config.http = config.http || { routers: {}, services: {} }; config.http.routers = config.http.routers || {}; config.http.services = config.http.services || {}; - config.http.routers[routerName] = await createRouterConfig(app, domain); + config.http.routers[routerName] = await createRouterConfig( + app, + domain, + "web", + ); + + if (domain.https) { + config.http.routers[routerNameSecure] = await createRouterConfig( + app, + domain, + "websecure", + ); + } else { + delete config.http.routers[routerNameSecure]; + } config.http.services[serviceName] = createServiceConfig(appName, domain); writeTraefikConfig(config, appName); @@ -28,10 +43,15 @@ export const removeDomain = async (appName: string, uniqueKey: number) => { const config: FileConfig = loadOrCreateConfig(appName); const routerKey = `${appName}-router-${uniqueKey}`; + const routerSecureKey = `${appName}-router-websecure-${uniqueKey}`; + const serviceKey = `${appName}-service-${uniqueKey}`; if (config.http?.routers?.[routerKey]) { delete config.http.routers[routerKey]; } + if (config.http?.routers?.[routerSecureKey]) { + delete config.http.routers[routerSecureKey]; + } if (config.http?.services?.[serviceKey]) { delete config.http.services[serviceKey]; } @@ -47,7 +67,11 @@ export const removeDomain = async (appName: string, uniqueKey: number) => { } }; -const createRouterConfig = async (app: ApplicationNested, domain: Domain) => { +export const createRouterConfig = async ( + app: ApplicationNested, + domain: Domain, + entryPoint: "web" | "websecure", +) => { const { appName, redirects, security } = app; const { certificateType } = domain; @@ -56,32 +80,33 @@ const createRouterConfig = async (app: ApplicationNested, domain: Domain) => { rule: `Host(\`${host}\`)${path ? ` && PathPrefix(\`${path}\`)` : ""}`, service: `${appName}-service-${uniqueConfigKey}`, middlewares: [], - entryPoints: https - ? ["web", ...(process.env.NODE_ENV === "production" ? ["websecure"] : [])] - : ["web"], - tls: {}, + entryPoints: [entryPoint], }; - if (https) { + if (entryPoint === "web" && https) { routerConfig.middlewares = ["redirect-to-https"]; } - // redirects - for (const redirect of redirects) { - const middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`; - routerConfig.middlewares?.push(middlewareName); + if ((entryPoint === "websecure" && https) || !https) { + // redirects + for (const redirect of redirects) { + const middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`; + routerConfig.middlewares?.push(middlewareName); + } + + // security + if (security.length > 0) { + const middlewareName = `auth-${appName}`; + routerConfig.middlewares?.push(middlewareName); + } } - // security - if (security.length > 0) { - const middlewareName = `auth-${appName}`; - routerConfig.middlewares?.push(middlewareName); - } - - if (certificateType === "letsencrypt") { - routerConfig.tls = { certResolver: "letsencrypt" }; - } else if (certificateType === "none") { - routerConfig.tls = undefined; + if (entryPoint === "websecure") { + if (certificateType === "letsencrypt") { + routerConfig.tls = { certResolver: "letsencrypt" }; + } else if (certificateType === "none") { + routerConfig.tls = undefined; + } } return routerConfig;