diff --git a/src/lib/haproxy/index.ts b/src/lib/haproxy/index.ts index c31b7740f..d0a6cb804 100644 --- a/src/lib/haproxy/index.ts +++ b/src/lib/haproxy/index.ts @@ -11,9 +11,14 @@ export const defaultProxyImage = `coolify-haproxy-alpine:latest`; export const defaultProxyImageTcp = `coolify-haproxy-tcp-alpine:latest`; export const defaultProxyImageHttp = `coolify-haproxy-http-alpine:latest`; export const defaultTraefikImage = `traefik:v2.6`; -const coolifyEndpoint = dev - ? 'http://host.docker.internal:3000/traefik.json' - : 'http://coolify:3000/traefik.json'; + +const mainTraefikEndpoint = dev + ? 'http://host.docker.internal:3000/webhooks/traefik/main.json' + : 'http://coolify:3000/webhooks/traefik/main.json'; + +const otherTraefikEndpoint = dev + ? 'http://host.docker.internal:3000/webhooks/traefik/other.json' + : 'http://coolify:3000/webhooks/traefik/other.json'; export async function haproxyInstance(): Promise { const { proxyPassword } = await db.listSettings(); @@ -154,7 +159,7 @@ export async function startTraefikTCPProxy( image: 'traefik:v2.6', command: [ `--entrypoints.tcp.address=:${publicPort}`, - `--providers.http.endpoint=${coolifyEndpoint}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=tcp`, + `--providers.http.endpoint=${otherTraefikEndpoint}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=tcp`, '--providers.http.pollTimeout=2s', '--log.level=error' ], @@ -250,7 +255,7 @@ export async function startTraefikHTTPProxy( image: 'traefik:v2.6', command: [ `--entrypoints.http.address=:${publicPort}`, - `--providers.http.endpoint=${coolifyEndpoint}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=http`, + `--providers.http.endpoint=${otherTraefikEndpoint}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=http`, '--providers.http.pollTimeout=2s', '--log.level=error' ], @@ -359,7 +364,7 @@ export async function startTraefikProxy(engine: string): Promise { --entrypoints.websecure.address=:443 \ --providers.docker=true \ --providers.docker.exposedbydefault=false \ - --providers.http.endpoint=${coolifyEndpoint} \ + --providers.http.endpoint=${mainTraefikEndpoint} \ --providers.http.pollTimeout=5s \ --certificatesresolvers.letsencrypt.acme.httpchallenge=true \ --certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json \ diff --git a/src/routes/traefik.json.ts b/src/routes/traefik.json.ts deleted file mode 100644 index 54f242109..000000000 --- a/src/routes/traefik.json.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { dev } from '$app/env'; -import { asyncExecShell, getDomain, getEngine } from '$lib/common'; -import { supportedServiceTypesAndVersions } from '$lib/components/common'; -import * as db from '$lib/database'; -import { listServicesWithIncludes } from '$lib/database'; -import { checkContainer } from '$lib/haproxy'; -import type { RequestHandler } from '@sveltejs/kit'; - -function generateMiddleware({ id, isDualCerts, isWWW, isHttps, traefik }) { - if (!isDualCerts) { - if (isWWW) { - if (isHttps) { - traefik.http.routers[id].middlewares?.length > 0 - ? traefik.http.routers[id].middlewares.push('https-redirect-non-www-to-www') - : (traefik.http.routers[id].middlewares = [ - 'https-redirect-non-www-to-www', - 'http-to-https' - ]); - } else { - traefik.http.routers[id].middlewares?.length > 0 - ? traefik.http.routers[id].middlewares.push('http-redirect-non-www-to-www') - : (traefik.http.routers[id].middlewares = [ - 'http-redirect-non-www-to-www', - 'https-to-http' - ]); - } - } else { - if (isHttps) { - traefik.http.routers[id].middlewares?.length > 0 - ? traefik.http.routers[id].middlewares.push('https-redirect-www-to-non-www') - : (traefik.http.routers[id].middlewares = [ - 'https-redirect-www-to-non-www', - 'http-to-https' - ]); - } else { - traefik.http.routers[id]?.middlewares?.length > 0 - ? traefik.http.routers[id].middlewares.push('http-redirect-www-to-non-www') - : (traefik.http.routers[id].middlewares = ['http-redirect-www-to-non-www']); - } - } - } -} -export const get: RequestHandler = async (event) => { - const id = event.url.searchParams.get('id'); - if (id) { - const privatePort = event.url.searchParams.get('privatePort'); - const publicPort = event.url.searchParams.get('publicPort'); - const type = event.url.searchParams.get('type'); - if (publicPort) { - if (type === 'tcp') { - const traefik = { - [type]: { - routers: { - [id]: { - entrypoints: [type], - rule: `HostSNI(\`*\`)`, - service: id - } - }, - services: { - [id]: { - loadbalancer: { - servers: [{ address: `${id}:${privatePort}` }] - } - } - }, - middlewares: { - ['global-compress']: { - compress: true - } - } - } - }; - return { - status: 200, - body: { - ...traefik - } - }; - } else if (type === 'http') { - const service = await db.prisma.service.findFirst({ where: { id } }); - if (service?.fqdn) { - const domain = getDomain(service.fqdn); - const isWWW = domain.startsWith('www.'); - const traefik = { - [type]: { - routers: { - [id]: { - entrypoints: [type], - rule: isWWW - ? `Host(\`${domain}\`) || Host(\`www.${domain}\`)` - : `Host(\`${domain}\`)`, - service: id - } - }, - services: { - [id]: { - loadbalancer: { - servers: [{ url: `http://${id}:${privatePort}` }] - } - } - }, - middlewares: { - ['global-compress']: { - compress: true - } - } - } - }; - return { - status: 200, - body: { - ...traefik - } - }; - } - } - } - return { - status: 500 - }; - } else { - const applications = await db.prisma.application.findMany({ - include: { destinationDocker: true, settings: true } - }); - const data = { - applications: [], - services: [], - coolify: [] - }; - for (const application of applications) { - const { - fqdn, - id, - port, - destinationDocker, - destinationDockerId, - settings: { previews, dualCerts } - } = application; - if (destinationDockerId) { - const { engine, network } = destinationDocker; - const isRunning = await checkContainer(engine, id); - if (fqdn) { - const domain = getDomain(fqdn); - const nakedDomain = domain.replace(/^www\./, ''); - const isHttps = fqdn.startsWith('https://'); - const isWWW = fqdn.includes('www.'); - if (isRunning) { - data.applications.push({ - id, - port: port || 3000, - domain, - nakedDomain, - isRunning, - isHttps, - isWWW, - isDualCerts: dualCerts - }); - } - if (previews) { - const host = getEngine(engine); - const { stdout } = await asyncExecShell( - `DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` - ); - const containers = stdout - .trim() - .split('\n') - .filter((a) => a) - .map((c) => c.replace(/"/g, '')); - if (containers.length > 0) { - for (const container of containers) { - const previewDomain = `${container.split('-')[1]}.${domain}`; - data.applications.push({ - id: container, - port: port || 3000, - domain: previewDomain, - isRunning, - isHttps, - isWWW - }); - } - } - } - } - } - } - const services = await listServicesWithIncludes(); - - for (const service of services) { - const { - fqdn, - id, - type, - dualCerts, - destinationDocker, - destinationDockerId, - plausibleAnalytics - } = service; - if (destinationDockerId) { - const { engine } = destinationDocker; - const found = supportedServiceTypesAndVersions.find((a) => a.name === type); - if (found) { - const port = found.ports.main; - const publicPort = service[type]?.publicPort; - const isRunning = await checkContainer(engine, id); - if (fqdn) { - const domain = getDomain(fqdn); - const nakedDomain = domain.replace(/^www\./, ''); - const isHttps = fqdn.startsWith('https://'); - const isWWW = fqdn.includes('www.'); - if (isRunning) { - // Plausible Analytics custom script - let scriptName = false; - if ( - type === 'plausibleanalytics' && - plausibleAnalytics.scriptName !== 'plausible.js' - ) { - scriptName = plausibleAnalytics.scriptName; - } - data.services.push({ - id, - port, - publicPort, - domain, - nakedDomain, - isRunning, - isHttps, - isWWW, - isDualCerts: dualCerts, - scriptName - }); - } - } - } - } - } - - const { fqdn, dualCerts } = await db.prisma.setting.findFirst(); - if (fqdn) { - const domain = getDomain(fqdn); - const nakedDomain = domain.replace(/^www\./, ''); - const isHttps = fqdn.startsWith('https://'); - const isWWW = fqdn.includes('www.'); - data.coolify.push({ - id: dev ? 'host.docker.internal' : 'coolify', - port: 3000, - domain, - nakedDomain, - isHttps, - isWWW, - isDualCerts: dualCerts - }); - } - const traefik = { - http: { - routers: {}, - services: {}, - middlewares: { - ['global-compress']: { - compress: true - }, - ['https-redirect-non-www-to-www']: { - redirectregex: { - regex: '^https://(?:www\\.)?(.+)', - replacement: 'https://www.${1}', - permanent: dev ? false : true - } - }, - ['http-redirect-non-www-to-www']: { - redirectregex: { - regex: '^http://(?:www\\.)?(.+)', - replacement: 'http://www.${1}', - permanent: dev ? false : true - } - }, - ['https-redirect-www-to-non-www']: { - redirectregex: { - regex: '^https?://www\\.(.+)', - replacement: 'https://${1}', - permanent: dev ? false : true - } - }, - ['http-redirect-www-to-non-www']: { - redirectregex: { - regex: '^http?://www\\.(.+)', - replacement: 'http://${1}', - permanent: dev ? false : true - } - }, - ['http-to-https']: { - redirectregex: { - regex: '^http?://(.+)', - replacement: 'https://${1}', - permanent: dev ? false : true - } - }, - ['https-to-http']: { - redirectregex: { - regex: '^https?://(.+)', - replacement: 'http://${1}', - permanent: dev ? false : true - } - }, - ['https-http']: { - redirectscheme: { - scheme: 'http', - permanent: false - } - } - } - } - }; - for (const application of data.applications) { - const { id, port, domain, nakedDomain, isHttps, isWWW, isDualCerts } = application; - if (isHttps) { - traefik.http.routers[id] = { - entrypoints: ['web'], - rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, - middlewares: ['http-to-https'], - service: id - }; - traefik.http.routers[`${id}-secure`] = { - entrypoints: ['websecure'], - rule: isWWW - ? isDualCerts - ? `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)` - : `Host(\`${nakedDomain}\`)` - : `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, - service: id - }; - } else { - traefik.http.routers[id] = { - entrypoints: ['web'], - rule: isWWW - ? `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)` - : `Host(\`${nakedDomain}\`)`, - service: id - }; - traefik.http.routers[`${id}-secure`] = { - entrypoints: ['websecure'], - rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, - middlewares: ['https-http'], - service: id - }; - } - - traefik.http.services[id] = { - loadbalancer: { - servers: [ - { - url: `http://${id}:${port}` - } - ] - } - }; - if (isHttps && !dev) { - traefik.http.routers[id].tls = { - certresolver: 'letsencrypt' - }; - } - generateMiddleware({ id, isDualCerts, isWWW, isHttps, traefik }); - } - for (const service of data.services) { - const { id, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName } = service; - - traefik.http.routers[id] = { - entrypoints: isHttps ? ['web', 'websecure'] : ['web'], - rule: isWWW - ? isDualCerts - ? `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)` - : `Host(\`${nakedDomain}\`)` - : `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, - service: id - }; - traefik.http.services[id] = { - loadbalancer: { - servers: [ - { - url: `http://${id}:${port}` - } - ] - } - }; - if (isHttps && !dev) { - traefik.http.routers[id].tls = { - certresolver: 'letsencrypt' - }; - } - if (scriptName) { - if (!traefik.http.middlewares) traefik.http.middlewares = {}; - traefik.http.middlewares[`${id}-redir`] = { - replacepathregex: { - regex: `/js/${scriptName}`, - replacement: '/js/plausible.js', - permanent: false - } - }; - traefik.http.routers[id].middlewares = [`${id}-redir`]; - } - generateMiddleware({ id, isDualCerts, isWWW, isHttps, traefik }); - } - for (const coolify of data.coolify) { - const { nakedDomain, domain, id, port, isHttps, isWWW, isDualCerts } = coolify; - traefik.http.routers['coolify'] = { - entrypoints: isHttps ? ['web', 'websecure'] : ['web'], - rule: isWWW - ? isDualCerts - ? `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)` - : `Host(\`${nakedDomain}\`)` - : `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, - service: id - }; - traefik.http.services[id] = { - loadbalancer: { - servers: [ - { - url: `http://${id}:${port}` - } - ] - } - }; - if (isHttps && !dev) { - traefik.http.routers[id].tls = { - certresolver: 'letsencrypt' - }; - } - generateMiddleware({ id, isDualCerts, isWWW, isHttps, traefik }); - } - - return { - status: 200, - body: { - ...traefik - } - }; - } -}; diff --git a/src/routes/webhooks/traefik/main.json.ts b/src/routes/webhooks/traefik/main.json.ts new file mode 100644 index 000000000..b0fee9cc7 --- /dev/null +++ b/src/routes/webhooks/traefik/main.json.ts @@ -0,0 +1,283 @@ +import { dev } from '$app/env'; +import { asyncExecShell, getDomain, getEngine } from '$lib/common'; +import { supportedServiceTypesAndVersions } from '$lib/components/common'; +import * as db from '$lib/database'; +import { listServicesWithIncludes } from '$lib/database'; +import { checkContainer } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; + +const traefik = { + http: { + routers: {}, + services: {}, + middlewares: { + 'redirect-to-https': { + redirectscheme: { + scheme: 'https' + } + }, + 'redirect-to-http': { + redirectscheme: { + scheme: 'http' + } + }, + 'redirect-to-non-www': { + redirectregex: { + regex: '^https?://www\\.(.+)', + replacement: 'http://${1}' + } + }, + 'redirect-to-www': { + redirectregex: { + regex: '^https?://(?:www\\.)?(.+)', + replacement: 'http://www.${1}' + } + } + } + } +}; + +function configureMiddleware({ id, port, nakedDomain, isHttps, isWWW, isDualCerts }) { + if (isHttps) { + traefik.http.routers[id] = { + entrypoints: ['web'], + rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, + service: `${id}`, + middlewares: ['redirect-to-https'] + }; + + traefik.http.routers[`${id}-secure`] = { + entrypoints: ['websecure'], + rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, + service: `${id}`, + tls: { + certresolver: 'letsencrypt' + }, + middlewares: [] + }; + + traefik.http.services[id] = { + loadbalancer: { + servers: [ + { + url: `http://${id}:${port}` + } + ] + } + }; + + if (!isDualCerts) { + if (isWWW) { + traefik.http.routers[`${id}`].middlewares.push('redirect-to-www'); + traefik.http.routers[`${id}-secure`].middlewares.push('redirect-to-www'); + } else { + traefik.http.routers[`${id}`].middlewares.push('redirect-to-non-www'); + traefik.http.routers[`${id}-secure`].middlewares.push('redirect-to-non-www'); + } + } + } else { + traefik.http.routers[id] = { + entrypoints: ['web'], + rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, + service: `${id}`, + middlewares: [] + }; + + traefik.http.routers[`${id}-secure`] = { + entrypoints: ['websecure'], + rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, + service: `${id}`, + tls: { + domains: { + main: `${nakedDomain}` + } + }, + middlewares: ['redirect-to-http'] + }; + + traefik.http.services[id] = { + loadbalancer: { + servers: [ + { + url: `http://${id}:${port}` + } + ] + } + }; + + if (!isDualCerts) { + if (isWWW) { + traefik.http.routers[`${id}`].middlewares.push('redirect-to-www'); + traefik.http.routers[`${id}-secure`].middlewares.push('redirect-to-www'); + } else { + traefik.http.routers[`${id}`].middlewares.push('redirect-to-non-www'); + traefik.http.routers[`${id}-secure`].middlewares.push('redirect-to-non-www'); + } + } + } +} +export const get: RequestHandler = async (event) => { + const applications = await db.prisma.application.findMany({ + include: { destinationDocker: true, settings: true } + }); + const data = { + applications: [], + services: [], + coolify: [] + }; + for (const application of applications) { + const { + fqdn, + id, + port, + destinationDocker, + destinationDockerId, + settings: { previews, dualCerts }, + updatedAt + } = application; + if (destinationDockerId) { + const { engine, network } = destinationDocker; + const isRunning = await checkContainer(engine, id); + if (fqdn) { + const domain = getDomain(fqdn); + const nakedDomain = domain.replace(/^www\./, ''); + const isHttps = fqdn.startsWith('https://'); + const isWWW = fqdn.includes('www.'); + if (isRunning) { + data.applications.push({ + id, + port: port || 3000, + domain, + nakedDomain, + isRunning, + isHttps, + isWWW, + isDualCerts: dualCerts + }); + } + if (previews) { + const host = getEngine(engine); + const { stdout } = await asyncExecShell( + `DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` + ); + const containers = stdout + .trim() + .split('\n') + .filter((a) => a) + .map((c) => c.replace(/"/g, '')); + if (containers.length > 0) { + for (const container of containers) { + const previewDomain = `${container.split('-')[1]}.${domain}`; + data.applications.push({ + id: container, + port: port || 3000, + domain: previewDomain, + isRunning, + isHttps, + redirectTo: isWWW ? previewDomain.replace('www.', '') : 'www.' + previewDomain, + updatedAt: updatedAt.getTime() + }); + } + } + } + } + } + } + const services = await listServicesWithIncludes(); + + for (const service of services) { + const { + fqdn, + id, + type, + destinationDocker, + destinationDockerId, + updatedAt, + dualCerts, + plausibleAnalytics + } = service; + if (destinationDockerId) { + const { engine } = destinationDocker; + const found = supportedServiceTypesAndVersions.find((a) => a.name === type); + if (found) { + const port = found.ports.main; + const publicPort = service[type]?.publicPort; + const isRunning = await checkContainer(engine, id); + if (fqdn) { + const domain = getDomain(fqdn); + const nakedDomain = domain.replace(/^www\./, ''); + const isHttps = fqdn.startsWith('https://'); + const isWWW = fqdn.includes('www.'); + if (isRunning) { + // Plausible Analytics custom script + let scriptName = false; + if (type === 'plausibleanalytics' && plausibleAnalytics.scriptName !== 'plausible.js') { + scriptName = plausibleAnalytics.scriptName; + } + + data.services.push({ + id, + port, + publicPort, + domain, + nakedDomain, + isRunning, + isHttps, + isWWW, + isDualCerts: dualCerts, + scriptName + }); + } + } + } + } + } + + const { fqdn, dualCerts } = await db.prisma.setting.findFirst(); + if (fqdn) { + const domain = getDomain(fqdn); + const nakedDomain = domain.replace(/^www\./, ''); + const isHttps = fqdn.startsWith('https://'); + const isWWW = fqdn.includes('www.'); + data.coolify.push({ + id: dev ? 'host.docker.internal' : 'coolify', + port: 3000, + domain, + nakedDomain, + isHttps, + isWWW, + isDualCerts: dualCerts + }); + } + for (const application of data.applications) { + configureMiddleware(application); + } + for (const service of data.services) { + const { id, scriptName } = service; + configureMiddleware(service); + + if (scriptName) { + traefik.http.middlewares[`${id}-redir`] = { + replacepathregex: { + regex: `/js/${scriptName}`, + replacement: '/js/plausible.js' + } + }; + if (traefik.http.routers[id].middlewares.length > 0) { + traefik.http.routers[id].middlewares.push(`${id}-redir`); + } else { + traefik.http.routers[id].middlewares = [`${id}-redir`]; + } + } + } + for (const coolify of data.coolify) { + configureMiddleware(coolify); + } + + return { + status: 200, + body: { + ...traefik + } + }; +}; diff --git a/src/routes/webhooks/traefik/other.json.ts b/src/routes/webhooks/traefik/other.json.ts new file mode 100644 index 000000000..f2a0a113b --- /dev/null +++ b/src/routes/webhooks/traefik/other.json.ts @@ -0,0 +1,76 @@ +import { dev } from '$app/env'; +import { asyncExecShell, getDomain, getEngine } from '$lib/common'; +import { supportedServiceTypesAndVersions } from '$lib/components/common'; +import * as db from '$lib/database'; +import { listServicesWithIncludes } from '$lib/database'; +import { checkContainer } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const get: RequestHandler = async (event) => { + const id = event.url.searchParams.get('id'); + if (id) { + const privatePort = event.url.searchParams.get('privatePort'); + const publicPort = event.url.searchParams.get('publicPort'); + const type = event.url.searchParams.get('type'); + let traefik = {}; + if (publicPort) { + if (type === 'tcp') { + traefik = { + [type]: { + routers: { + [id]: { + entrypoints: [type], + rule: `HostSNI(\`*\`)`, + service: id + } + }, + services: { + [id]: { + loadbalancer: { + servers: [] + } + } + } + } + }; + } else if (type === 'http') { + const service = await db.prisma.service.findFirst({ where: { id } }); + if (service?.fqdn) { + const domain = getDomain(service.fqdn); + traefik = { + [type]: { + routers: { + [id]: { + entrypoints: [type], + rule: `Host(\`${domain}\`)`, + service: id + } + }, + services: { + [id]: { + loadbalancer: { + servers: [] + } + } + } + } + }; + } + } + } + if (type === 'tcp') { + traefik[type].services[id].loadbalancer.servers.push({ address: `${id}:${privatePort}` }); + } else if (type === 'http') { + traefik[type].services[id].loadbalancer.servers.push({ url: `http://${id}:${privatePort}` }); + } + return { + status: 200, + body: { + ...traefik + } + }; + } + return { + status: 500 + }; +};