WIP: Traefik
This commit is contained in:
		| @@ -11,9 +11,14 @@ export const defaultProxyImage = `coolify-haproxy-alpine:latest`; | |||||||
| export const defaultProxyImageTcp = `coolify-haproxy-tcp-alpine:latest`; | export const defaultProxyImageTcp = `coolify-haproxy-tcp-alpine:latest`; | ||||||
| export const defaultProxyImageHttp = `coolify-haproxy-http-alpine:latest`; | export const defaultProxyImageHttp = `coolify-haproxy-http-alpine:latest`; | ||||||
| export const defaultTraefikImage = `traefik:v2.6`; | export const defaultTraefikImage = `traefik:v2.6`; | ||||||
| const coolifyEndpoint = dev |  | ||||||
| 	? 'http://host.docker.internal:3000/traefik.json' | const mainTraefikEndpoint = dev | ||||||
| 	: 'http://coolify:3000/traefik.json'; | 	? '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<Got> { | export async function haproxyInstance(): Promise<Got> { | ||||||
| 	const { proxyPassword } = await db.listSettings(); | 	const { proxyPassword } = await db.listSettings(); | ||||||
| @@ -154,7 +159,7 @@ export async function startTraefikTCPProxy( | |||||||
| 						image: 'traefik:v2.6', | 						image: 'traefik:v2.6', | ||||||
| 						command: [ | 						command: [ | ||||||
| 							`--entrypoints.tcp.address=:${publicPort}`, | 							`--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', | 							'--providers.http.pollTimeout=2s', | ||||||
| 							'--log.level=error' | 							'--log.level=error' | ||||||
| 						], | 						], | ||||||
| @@ -250,7 +255,7 @@ export async function startTraefikHTTPProxy( | |||||||
| 						image: 'traefik:v2.6', | 						image: 'traefik:v2.6', | ||||||
| 						command: [ | 						command: [ | ||||||
| 							`--entrypoints.http.address=:${publicPort}`, | 							`--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', | 							'--providers.http.pollTimeout=2s', | ||||||
| 							'--log.level=error' | 							'--log.level=error' | ||||||
| 						], | 						], | ||||||
| @@ -359,7 +364,7 @@ export async function startTraefikProxy(engine: string): Promise<void> { | |||||||
| 			--entrypoints.websecure.address=:443 \ | 			--entrypoints.websecure.address=:443 \ | ||||||
| 			--providers.docker=true \ | 			--providers.docker=true \ | ||||||
| 			--providers.docker.exposedbydefault=false \ | 			--providers.docker.exposedbydefault=false \ | ||||||
| 			--providers.http.endpoint=${coolifyEndpoint} \ | 			--providers.http.endpoint=${mainTraefikEndpoint} \ | ||||||
| 			--providers.http.pollTimeout=5s \ | 			--providers.http.pollTimeout=5s \ | ||||||
| 			--certificatesresolvers.letsencrypt.acme.httpchallenge=true \ | 			--certificatesresolvers.letsencrypt.acme.httpchallenge=true \ | ||||||
| 			--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json \ | 			--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json \ | ||||||
|   | |||||||
| @@ -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 |  | ||||||
| 			} |  | ||||||
| 		}; |  | ||||||
| 	} |  | ||||||
| }; |  | ||||||
							
								
								
									
										283
									
								
								src/routes/webhooks/traefik/main.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								src/routes/webhooks/traefik/main.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | }; | ||||||
							
								
								
									
										76
									
								
								src/routes/webhooks/traefik/other.json.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/routes/webhooks/traefik/other.json.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  | 	}; | ||||||
|  | }; | ||||||
		Reference in New Issue
	
	Block a user
	 Andras Bacsai
					Andras Bacsai