From ae5d90eb474a3381d60fd6962c1096256367b0e6 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Thu, 12 May 2022 16:53:22 +0200 Subject: [PATCH] WIP: Traefik --- data/traefik/docker-compose-tcp.yaml | 23 + docker-compose-traefik.yaml | 1 + .../migration.sql | 24 - prisma/schema.prisma | 2 +- src/lib/database/destinations.ts | 17 +- src/lib/haproxy/index.ts | 190 +++++++- src/lib/queues/proxy.ts | 7 +- src/lib/queues/proxyTcpHttp.ts | 53 ++- src/lib/store.ts | 2 + src/routes/__layout.svelte | 30 +- src/routes/dashboard.json.ts | 4 +- src/routes/databases/[id]/settings.json.ts | 9 +- src/routes/destinations/[id]/index.json.ts | 6 +- src/routes/destinations/[id]/restart.json.ts | 18 +- src/routes/destinations/[id]/start.json.ts | 22 +- src/routes/destinations/[id]/stop.json.ts | 11 +- .../[id]/plausibleanalytics/start.json.ts | 1 - .../services/[id]/wordpress/ftp.json.ts | 15 +- src/routes/settings/index.json.ts | 3 +- src/routes/settings/index.svelte | 41 +- src/routes/traefik.json.ts | 416 ++++++++++++------ src/routes/update.json.ts | 42 +- 22 files changed, 721 insertions(+), 216 deletions(-) create mode 100644 data/traefik/docker-compose-tcp.yaml delete mode 100644 prisma/migrations/20220512110234_disable_haproxy/migration.sql diff --git a/data/traefik/docker-compose-tcp.yaml b/data/traefik/docker-compose-tcp.yaml new file mode 100644 index 000000000..110630d2e --- /dev/null +++ b/data/traefik/docker-compose-tcp.yaml @@ -0,0 +1,23 @@ +version: '3.5' + +services: + ${ID}: + container_name: proxy-for-${PORT} + image: traefik:v2.6 + command: + - --api.insecure=true + - --entrypoints.web.address=:${PORT} + - --providers.docker=false + - --providers.docker.exposedbydefault=false + - --providers.http.endpoint=http://host.docker.internal:3000/traefik.json?id=${ID} + - --providers.http.pollTimeout=5s + - --log.level=error + ports: + - '${PORT}:${PORT}' + networks: + - ${NETWORK} + +networks: + net: + external: false + name: ${NETWORK} diff --git a/docker-compose-traefik.yaml b/docker-compose-traefik.yaml index 6d7ca2487..09fd32525 100644 --- a/docker-compose-traefik.yaml +++ b/docker-compose-traefik.yaml @@ -6,6 +6,7 @@ services: command: - --api.insecure=true - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 - --providers.docker=false - --providers.docker.exposedbydefault=false - --providers.http.endpoint=http://host.docker.internal:3000/traefik.json diff --git a/prisma/migrations/20220512110234_disable_haproxy/migration.sql b/prisma/migrations/20220512110234_disable_haproxy/migration.sql deleted file mode 100644 index afb5dd97b..000000000 --- a/prisma/migrations/20220512110234_disable_haproxy/migration.sql +++ /dev/null @@ -1,24 +0,0 @@ --- RedefineTables -PRAGMA foreign_keys=OFF; -CREATE TABLE "new_Setting" ( - "id" TEXT NOT NULL PRIMARY KEY, - "fqdn" TEXT, - "isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false, - "dualCerts" BOOLEAN NOT NULL DEFAULT false, - "minPort" INTEGER NOT NULL DEFAULT 9000, - "maxPort" INTEGER NOT NULL DEFAULT 9100, - "proxyPassword" TEXT NOT NULL, - "proxyUser" TEXT NOT NULL, - "proxyHash" TEXT, - "isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false, - "isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true, - "disableHaproxy" BOOLEAN NOT NULL DEFAULT false, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL -); -INSERT INTO "new_Setting" ("createdAt", "dualCerts", "fqdn", "id", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt") SELECT "createdAt", "dualCerts", "fqdn", "id", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt" FROM "Setting"; -DROP TABLE "Setting"; -ALTER TABLE "new_Setting" RENAME TO "Setting"; -CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn"); -PRAGMA foreign_key_check; -PRAGMA foreign_keys=ON; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8196a6e51..a20b3455c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,7 +20,7 @@ model Setting { proxyHash String? isAutoUpdateEnabled Boolean @default(false) isDNSCheckEnabled Boolean @default(true) - disableHaproxy Boolean @default(false) + isTraefikUsed Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } diff --git a/src/lib/database/destinations.ts b/src/lib/database/destinations.ts index 2f6cdb488..f378874b0 100644 --- a/src/lib/database/destinations.ts +++ b/src/lib/database/destinations.ts @@ -1,6 +1,6 @@ import { asyncExecShell, getEngine } from '$lib/common'; import { dockerInstance } from '$lib/docker'; -import { startCoolifyProxy } from '$lib/haproxy'; +import { startCoolifyProxy, startTraefikProxy } from '$lib/haproxy'; import { getDatabaseImage } from '.'; import { prisma } from './common'; import type { DestinationDocker, Service, Application, Prisma } from '@prisma/client'; @@ -125,7 +125,14 @@ export async function newLocalDestination({ } await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } }); } - if (isCoolifyProxyUsed) await startCoolifyProxy(engine); + if (isCoolifyProxyUsed) { + const settings = await prisma.setting.findFirst(); + if (settings?.isTraefikUsed) { + await startTraefikProxy(engine); + } else { + await startCoolifyProxy(engine); + } + } return destination.id; } export async function removeDestination({ id }: Pick): Promise { @@ -133,12 +140,14 @@ export async function removeDestination({ id }: Pick): if (destination.isCoolifyProxyUsed) { const host = getEngine(destination.engine); const { network } = destination; + const settings = await prisma.setting.findFirst(); + const containerName = settings.isTraefikUsed ? 'coolify-proxy' : 'coolify-haproxy'; const { stdout: found } = await asyncExecShell( - `DOCKER_HOST=${host} docker ps -a --filter network=${network} --filter name=coolify-haproxy --format '{{.}}'` + `DOCKER_HOST=${host} docker ps -a --filter network=${network} --filter name=${containerName} --format '{{.}}'` ); if (found) { await asyncExecShell( - `DOCKER_HOST="${host}" docker network disconnect ${network} coolify-haproxy` + `DOCKER_HOST="${host}" docker network disconnect ${network} ${containerName}` ); await asyncExecShell(`DOCKER_HOST="${host}" docker network rm ${network}`); } diff --git a/src/lib/haproxy/index.ts b/src/lib/haproxy/index.ts index 5b713e343..361015fb3 100644 --- a/src/lib/haproxy/index.ts +++ b/src/lib/haproxy/index.ts @@ -3,12 +3,17 @@ import { asyncExecShell, getEngine } from '$lib/common'; import got, { type Got, type Response } from 'got'; import * as db from '$lib/database'; import type { DestinationDocker } from '@prisma/client'; - +import fs from 'fs/promises'; +import yaml from 'js-yaml'; const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555'; 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'; export async function haproxyInstance(): Promise { const { proxyPassword } = await db.listSettings(); @@ -99,11 +104,17 @@ export async function checkHAProxy(haproxy?: Got): Promise { export async function stopTcpHttpProxy( destinationDocker: DestinationDocker, - publicPort: number + publicPort: number, + forceName: string = null ): Promise<{ stdout: string; stderr: string } | Error> { const { engine } = destinationDocker; const host = getEngine(engine); - const containerName = `haproxy-for-${publicPort}`; + const settings = await db.listSettings(); + let containerName = `proxy-for-${publicPort}`; + if (!settings.isTraefikUsed) { + containerName = `haproxy-for-${publicPort}`; + } + if (forceName) containerName = forceName; const found = await checkContainer(engine, containerName); try { if (found) { @@ -115,6 +126,67 @@ export async function stopTcpHttpProxy( return error; } } +export async function startTraefikTCPProxy( + destinationDocker: DestinationDocker, + id: string, + publicPort: number, + privatePort: number, + volume?: string +): Promise<{ stdout: string; stderr: string } | Error> { + const { network, engine } = destinationDocker; + const host = getEngine(engine); + + const containerName = `proxy-for-${publicPort}`; + const found = await checkContainer(engine, containerName, true); + const foundDependentContainer = await checkContainer(engine, id, true); + + try { + if (foundDependentContainer && !found) { + const { stdout: Config } = await asyncExecShell( + `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` + ); + const ip = JSON.parse(Config)[0].Gateway; + const tcpProxy = { + version: '3.5', + services: { + [id]: { + container_name: `proxy-for-${publicPort}`, + image: 'traefik:v2.6', + command: [ + `--entrypoints.tcp.address=:${publicPort}`, + `--providers.http.endpoint=${coolifyEndpoint}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=tcp`, + '--providers.http.pollTimeout=2s', + '--log.level=debug' + ], + ports: [`${publicPort}:${publicPort}`], + extra_hosts: ['host.docker.internal:host-gateway', `host.docker.internal:${ip}`], + volumes: ['/var/run/docker.sock:/var/run/docker.sock'], + networks: [network] + } + }, + networks: { + [network]: { + external: false, + name: network + } + } + }; + await fs.writeFile(`/tmp/docker-compose-${id}.yaml`, yaml.dump(tcpProxy)); + await asyncExecShell( + `DOCKER_HOST=${host} docker compose -f /tmp/docker-compose-${id}.yaml up -d` + ); + await fs.rm(`/tmp/docker-compose-${id}.yaml`); + } + if (!foundDependentContainer && found) { + return await asyncExecShell( + `DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}` + ); + } + } catch (error) { + console.log(error); + return error; + } +} export async function startTcpProxy( destinationDocker: DestinationDocker, id: string, @@ -151,6 +223,65 @@ export async function startTcpProxy( } } +export async function startTraefikHTTPProxy( + destinationDocker: DestinationDocker, + id: string, + publicPort: number, + privatePort: number +): Promise<{ stdout: string; stderr: string } | Error> { + const { network, engine } = destinationDocker; + const host = getEngine(engine); + + const containerName = `proxy-for-${publicPort}`; + const found = await checkContainer(engine, containerName, true); + const foundDependentContainer = await checkContainer(engine, id, true); + + try { + if (foundDependentContainer && !found) { + const { stdout: Config } = await asyncExecShell( + `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` + ); + const ip = JSON.parse(Config)[0].Gateway; + const tcpProxy = { + version: '3.5', + services: { + [id]: { + container_name: `proxy-for-${publicPort}`, + image: 'traefik:v2.6', + command: [ + `--entrypoints.http.address=:${publicPort}`, + `--providers.http.endpoint=${coolifyEndpoint}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=http`, + '--providers.http.pollTimeout=2s', + '--log.level=debug' + ], + ports: [`${publicPort}:${publicPort}`], + extra_hosts: ['host.docker.internal:host-gateway', `host.docker.internal:${ip}`], + volumes: ['/var/run/docker.sock:/var/run/docker.sock'], + networks: [network] + } + }, + networks: { + [network]: { + external: false, + name: network + } + } + }; + await fs.writeFile(`/tmp/docker-compose-${id}.yaml`, yaml.dump(tcpProxy)); + await asyncExecShell( + `DOCKER_HOST=${host} docker compose -f /tmp/docker-compose-${id}.yaml up -d` + ); + await fs.rm(`/tmp/docker-compose-${id}.yaml`); + } + if (!foundDependentContainer && found) { + return await asyncExecShell( + `DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}` + ); + } + } catch (error) { + return error; + } +} export async function startHttpProxy( destinationDocker: DestinationDocker, id: string, @@ -197,10 +328,29 @@ export async function startCoolifyProxy(engine: string): Promise { `DOCKER_HOST="${host}" docker run -e HAPROXY_USERNAME=${proxyUser} -e HAPROXY_PASSWORD=${proxyPassword} --restart always --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' -v coolify-ssl-certs:/usr/local/etc/haproxy/ssl --network coolify-infra -p "80:80" -p "443:443" -p "8404:8404" -p "5555:5555" -p "5000:5000" --name coolify-haproxy -d coollabsio/${defaultProxyImage}` ); await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } }); + await db.setDestinationSettings({ engine, isCoolifyProxyUsed: true }); } await configureNetworkCoolifyProxy(engine); } +export async function startTraefikProxy(engine: string): Promise { + const host = getEngine(engine); + const found = await checkContainer(engine, 'coolify-proxy', true); + const { id } = await db.listSettings(); + if (!found) { + const { stdout: Config } = await asyncExecShell( + `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` + ); + const ip = JSON.parse(Config)[0].Gateway; + await asyncExecShell( + `DOCKER_HOST="${host}" docker run --restart always --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' -v coolify-ssl-certs:/usr/local/etc/haproxy/ssl -v /var/run/docker.sock:/var/run/docker.sock --network coolify-infra -p "80:80" -p "443:443" -p "8080:8080" --name coolify-proxy -d ${defaultTraefikImage} --api.insecure=true --entrypoints.web.address=:80 --entrypoints.websecure.address=:443 --providers.docker=false --providers.docker.exposedbydefault=false --providers.http.endpoint=${coolifyEndpoint} --providers.http.pollTimeout=5s --log.level=error` + ); + await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } }); + await db.setDestinationSettings({ engine, isCoolifyProxyUsed: true }); + } + await configureNetworkTraefikProxy(engine); +} + export async function isContainerExited(engine: string, containerName: string): Promise { let isExited = false; const host = getEngine(engine); @@ -263,6 +413,24 @@ export async function stopCoolifyProxy( return error; } } +export async function stopTraefikProxy( + engine: string +): Promise<{ stdout: string; stderr: string } | Error> { + const host = getEngine(engine); + const found = await checkContainer(engine, 'coolify-proxy'); + await db.setDestinationSettings({ engine, isCoolifyProxyUsed: false }); + const { id } = await db.prisma.setting.findFirst({}); + await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } }); + try { + if (found) { + await asyncExecShell( + `DOCKER_HOST="${host}" docker stop -t 0 coolify-proxy && docker rm coolify-proxy` + ); + } + } catch (error) { + return error; + } +} export async function configureNetworkCoolifyProxy(engine: string): Promise { const host = getEngine(engine); @@ -279,3 +447,19 @@ export async function configureNetworkCoolifyProxy(engine: string): Promise { + const host = getEngine(engine); + const destinations = await db.prisma.destinationDocker.findMany({ where: { engine } }); + const { stdout: networks } = await asyncExecShell( + `DOCKER_HOST="${host}" docker ps -a --filter name=coolify-proxy --format '{{json .Networks}}'` + ); + const configuredNetworks = networks.replace(/"/g, '').replace('\n', '').split(','); + for (const destination of destinations) { + if (!configuredNetworks.includes(destination.network)) { + await asyncExecShell( + `DOCKER_HOST="${host}" docker network connect ${destination.network} coolify-proxy` + ); + } + } +} diff --git a/src/lib/queues/proxy.ts b/src/lib/queues/proxy.ts index db8742e74..245fb3b02 100644 --- a/src/lib/queues/proxy.ts +++ b/src/lib/queues/proxy.ts @@ -1,4 +1,4 @@ -import { ErrorHandler } from '$lib/database'; +import { ErrorHandler, prisma } from '$lib/database'; import { configureHAProxy } from '$lib/haproxy/configuration'; export default async function (): Promise { try { - return await configureHAProxy(); + const settings = await prisma.setting.findFirst(); + if (!settings.isTraefikUsed) { + return await configureHAProxy(); + } } catch (error) { return ErrorHandler(error.response?.body || error); } diff --git a/src/lib/queues/proxyTcpHttp.ts b/src/lib/queues/proxyTcpHttp.ts index 3d48c5c1d..b23b1119f 100644 --- a/src/lib/queues/proxyTcpHttp.ts +++ b/src/lib/queues/proxyTcpHttp.ts @@ -1,5 +1,13 @@ import { ErrorHandler, generateDatabaseConfiguration, prisma } from '$lib/database'; -import { startCoolifyProxy, startHttpProxy, startTcpProxy } from '$lib/haproxy'; +import { + startCoolifyProxy, + startHttpProxy, + startTcpProxy, + startTraefikHTTPProxy, + startTraefikProxy, + startTraefikTCPProxy, + stopTcpHttpProxy +} from '$lib/haproxy'; export default async function (): Promise = read beta: browser && window.localStorage.getItem('beta') === 'true', latestVersion: browser && window.localStorage.getItem('latestVersion') }); + +export const isTraefikUsed: Writable = writable(false); diff --git a/src/routes/__layout.svelte b/src/routes/__layout.svelte index 0f5b481f5..0f0843f8f 100644 --- a/src/routes/__layout.svelte +++ b/src/routes/__layout.svelte @@ -34,6 +34,7 @@ @@ -522,13 +518,15 @@ >Powered by Coolify {/if} - - - + {#if !$isTraefikUsed} + + + + {/if} {/if}
diff --git a/src/routes/dashboard.json.ts b/src/routes/dashboard.json.ts index 297ce1a14..1b9528861 100644 --- a/src/routes/dashboard.json.ts +++ b/src/routes/dashboard.json.ts @@ -49,6 +49,7 @@ export const get: RequestHandler = async (event) => { where: { userId }, include: { team: { include: { _count: { select: { users: true } } } } } }); + const settings = await db.prisma.setting.findFirst(); return { body: { teams, @@ -57,7 +58,8 @@ export const get: RequestHandler = async (event) => { destinationsCount, teamsCount, databasesCount, - servicesCount + servicesCount, + settings } }; } catch (error) { diff --git a/src/routes/databases/[id]/settings.json.ts b/src/routes/databases/[id]/settings.json.ts index 1f09946c6..e4263ce9a 100644 --- a/src/routes/databases/[id]/settings.json.ts +++ b/src/routes/databases/[id]/settings.json.ts @@ -1,7 +1,7 @@ import { getUserDetails } from '$lib/common'; import * as db from '$lib/database'; import { generateDatabaseConfiguration, ErrorHandler, getFreePort } from '$lib/database'; -import { startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy'; +import { startTcpProxy, startTraefikTCPProxy, stopTcpHttpProxy } from '$lib/haproxy'; import type { RequestHandler } from '@sveltejs/kit'; export const post: RequestHandler = async (event) => { @@ -13,6 +13,7 @@ export const post: RequestHandler = async (event) => { const publicPort = await getFreePort(); try { + const settings = await db.listSettings(); await db.setDatabase({ id, isPublic, appendOnly }); const database = await db.getDatabase({ id, teamId }); const { destinationDockerId, destinationDocker, publicPort: oldPublicPort } = database; @@ -21,7 +22,11 @@ export const post: RequestHandler = async (event) => { if (destinationDockerId) { if (isPublic) { await db.prisma.database.update({ where: { id }, data: { publicPort } }); - await startTcpProxy(destinationDocker, id, publicPort, privatePort); + if (settings.isTraefikUsed) { + await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort); + } else { + await startTcpProxy(destinationDocker, id, publicPort, privatePort); + } } else { await db.prisma.database.update({ where: { id }, data: { publicPort: null } }); await stopTcpHttpProxy(destinationDocker, oldPublicPort); diff --git a/src/routes/destinations/[id]/index.json.ts b/src/routes/destinations/[id]/index.json.ts index aa9be6aea..8c4c67cee 100644 --- a/src/routes/destinations/[id]/index.json.ts +++ b/src/routes/destinations/[id]/index.json.ts @@ -26,8 +26,12 @@ export const get: RequestHandler = async (event) => { // // await saveSshKey(destination); // payload.state = await checkContainer(engine, 'coolify-haproxy'); } else { + let containerName = 'coolify-proxy'; + if (!settings.isTraefikUsed) { + containerName = 'coolify-haproxy'; + } payload.state = - destination?.engine && (await checkContainer(destination.engine, 'coolify-haproxy')); + destination?.engine && (await checkContainer(destination.engine, containerName)); } return { status: 200, diff --git a/src/routes/destinations/[id]/restart.json.ts b/src/routes/destinations/[id]/restart.json.ts index 9d2da459a..c2cf88fa4 100644 --- a/src/routes/destinations/[id]/restart.json.ts +++ b/src/routes/destinations/[id]/restart.json.ts @@ -1,7 +1,12 @@ import { getUserDetails } from '$lib/common'; import { ErrorHandler } from '$lib/database'; import * as db from '$lib/database'; -import { startCoolifyProxy, stopCoolifyProxy } from '$lib/haproxy'; +import { + startCoolifyProxy, + startTraefikProxy, + stopCoolifyProxy, + stopTraefikProxy +} from '$lib/haproxy'; import type { RequestHandler } from '@sveltejs/kit'; export const post: RequestHandler = async (event) => { @@ -11,9 +16,16 @@ export const post: RequestHandler = async (event) => { const { engine } = await event.request.json(); try { - await stopCoolifyProxy(engine); - await startCoolifyProxy(engine); + const settings = await db.prisma.setting.findFirst({}); + if (settings?.isTraefikUsed) { + await stopTraefikProxy(engine); + await startTraefikProxy(engine); + } else { + await stopCoolifyProxy(engine); + await startCoolifyProxy(engine); + } await db.setDestinationSettings({ engine, isCoolifyProxyUsed: true }); + return { status: 200 }; diff --git a/src/routes/destinations/[id]/start.json.ts b/src/routes/destinations/[id]/start.json.ts index f5272418a..865187542 100644 --- a/src/routes/destinations/[id]/start.json.ts +++ b/src/routes/destinations/[id]/start.json.ts @@ -1,6 +1,12 @@ import { getUserDetails } from '$lib/common'; import { ErrorHandler } from '$lib/database'; -import { startCoolifyProxy, stopCoolifyProxy } from '$lib/haproxy'; +import * as db from '$lib/database'; +import { + startCoolifyProxy, + startTraefikProxy, + stopCoolifyProxy, + stopTraefikProxy +} from '$lib/haproxy'; import type { RequestHandler } from '@sveltejs/kit'; export const post: RequestHandler = async (event) => { @@ -8,14 +14,24 @@ export const post: RequestHandler = async (event) => { if (status === 401) return { status, body }; const { engine } = await event.request.json(); + const settings = await db.prisma.setting.findFirst({}); try { - await startCoolifyProxy(engine); + if (settings?.isTraefikUsed) { + await startTraefikProxy(engine); + } else { + await startCoolifyProxy(engine); + } + return { status: 200 }; } catch (error) { - await stopCoolifyProxy(engine); + if (settings?.isTraefikUsed) { + await stopTraefikProxy(engine); + } else { + await stopCoolifyProxy(engine); + } return ErrorHandler(error); } }; diff --git a/src/routes/destinations/[id]/stop.json.ts b/src/routes/destinations/[id]/stop.json.ts index 495c7612e..a1e01d218 100644 --- a/src/routes/destinations/[id]/stop.json.ts +++ b/src/routes/destinations/[id]/stop.json.ts @@ -1,6 +1,7 @@ import { getUserDetails } from '$lib/common'; import { ErrorHandler } from '$lib/database'; -import { stopCoolifyProxy } from '$lib/haproxy'; +import * as db from '$lib/database'; +import { stopCoolifyProxy, stopTraefikProxy } from '$lib/haproxy'; import type { RequestHandler } from '@sveltejs/kit'; export const post: RequestHandler = async (event) => { @@ -9,7 +10,13 @@ export const post: RequestHandler = async (event) => { const { engine } = await event.request.json(); try { - await stopCoolifyProxy(engine); + const settings = await db.prisma.setting.findFirst({}); + if (settings?.isTraefikUsed) { + await stopTraefikProxy(engine); + } else { + await stopCoolifyProxy(engine); + } + return { status: 200 }; diff --git a/src/routes/services/[id]/plausibleanalytics/start.json.ts b/src/routes/services/[id]/plausibleanalytics/start.json.ts index 81c8f2bd8..e98e7093a 100644 --- a/src/routes/services/[id]/plausibleanalytics/start.json.ts +++ b/src/routes/services/[id]/plausibleanalytics/start.json.ts @@ -195,7 +195,6 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`; } }; const composeFileDestination = `${workdir}/docker-compose.yaml`; - console.log(JSON.stringify(composeFile, null, 2)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); await asyncExecShell( diff --git a/src/routes/services/[id]/wordpress/ftp.json.ts b/src/routes/services/[id]/wordpress/ftp.json.ts index cee85bc41..8267e1807 100644 --- a/src/routes/services/[id]/wordpress/ftp.json.ts +++ b/src/routes/services/[id]/wordpress/ftp.json.ts @@ -3,7 +3,12 @@ import { asyncExecShell, getEngine, getUserDetails } from '$lib/common'; import { decrypt, encrypt } from '$lib/crypto'; import * as db from '$lib/database'; import { ErrorHandler, generatePassword, getFreePort } from '$lib/database'; -import { checkContainer, startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy'; +import { + checkContainer, + startTcpProxy, + startTraefikTCPProxy, + stopTcpHttpProxy +} from '$lib/haproxy'; import type { ComposeFile } from '$lib/types/composeFile'; import type { RequestHandler } from '@sveltejs/kit'; import cuid from 'cuid'; @@ -142,8 +147,12 @@ export const post: RequestHandler = async (event) => { await asyncExecShell( `DOCKER_HOST=${host} docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d` ); - - await startTcpProxy(destinationDocker, `${id}-ftp`, publicPort, 22); + const settings = await db.prisma.setting.findFirst(); + if (settings.isTraefikUsed) { + await startTraefikTCPProxy(destinationDocker, `${id}-ftp`, publicPort, 22); + } else { + await startTcpProxy(destinationDocker, `${id}-ftp`, publicPort, 22); + } } else { await db.prisma.wordpress.update({ where: { serviceId: id }, diff --git a/src/routes/settings/index.json.ts b/src/routes/settings/index.json.ts index 21f4907f2..e57b159c6 100644 --- a/src/routes/settings/index.json.ts +++ b/src/routes/settings/index.json.ts @@ -72,8 +72,7 @@ export const post: RequestHandler = async (event) => { minPort, maxPort, isAutoUpdateEnabled, - isDNSCheckEnabled, - forceSave + isDNSCheckEnabled } = await event.request.json(); try { const { id } = await db.listSettings(); diff --git a/src/routes/settings/index.svelte b/src/routes/settings/index.svelte index c601db4ed..9b97570a0 100644 --- a/src/routes/settings/index.svelte +++ b/src/routes/settings/index.svelte @@ -37,12 +37,13 @@ import { getDomain } from '$lib/components/common'; import { toast } from '@zerodevx/svelte-toast'; import { t } from '$lib/translations'; - import { features } from '$lib/store'; + import { features, isTraefikUsed } from '$lib/store'; let isRegistrationEnabled = settings.isRegistrationEnabled; let dualCerts = settings.dualCerts; let isAutoUpdateEnabled = settings.isAutoUpdateEnabled; let isDNSCheckEnabled = settings.isDNSCheckEnabled; + $isTraefikUsed = settings.isTraefikUsed; let minPort = settings.minPort; let maxPort = settings.maxPort; @@ -55,7 +56,8 @@ let isFqdnSet = !!settings.fqdn; let loading = { save: false, - remove: false + remove: false, + proxyMigration: false }; async function removeFqdn() { @@ -86,6 +88,7 @@ if (name === 'isDNSCheckEnabled') { isDNSCheckEnabled = !isDNSCheckEnabled; } + await post(`/settings.json`, { isRegistrationEnabled, dualCerts, @@ -156,6 +159,20 @@ function resetView() { forceSave = false; } + async function migrateProxy(to) { + if (loading.proxyMigration) return; + try { + loading.proxyMigration = true; + await post(`/update.json`, { type: to }); + const data = await get(`/settings.json`); + $isTraefikUsed = data.settings.isTraefikUsed; + return toast.push('Proxy migration completed.'); + } catch ({ error }) { + return errorNotification(error); + } finally { + loading.proxyMigration = false; + } + }
@@ -192,6 +209,26 @@
+
+
+
+
New Proxy Available!
+ +
+
+ +
diff --git a/src/routes/traefik.json.ts b/src/routes/traefik.json.ts index ee7745c9c..d14438d9e 100644 --- a/src/routes/traefik.json.ts +++ b/src/routes/traefik.json.ts @@ -1,152 +1,308 @@ +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 = async () => { - 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 }, - updatedAt - } = application; - if (destinationDockerId) { - const { engine, network } = destinationDocker; - const isRunning = await checkContainer(engine, id); - if (fqdn) { - const domain = getDomain(fqdn); - const isHttps = fqdn.startsWith('https://'); - const isWWW = fqdn.includes('www.'); - const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`; - if (isRunning) { - data.applications.push({ - id, - port: port || 3000, - domain, - isRunning, - isHttps, - redirectValue, - redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain, - updatedAt: updatedAt.getTime() - }); +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) { + traefik = { + [type]: { + routers: { + [id]: { + entrypoints: [type], + rule: `HostSNI(\`*\`)`, + service: id + } + }, + services: { + [id]: { + loadbalancer: { + servers: [] + } + } + } } - 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, + }; + } + if (type === 'tcp') { + traefik[type].services[id].loadbalancer.servers.push({ address: `${id}:${privatePort}` }); + } else { + traefik[type].services[id].loadbalancer.servers.push({ url: `http://${id}:${privatePort}` }); + } + return { + status: 200, + body: { + ...traefik + } + }; + } 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 }, + updatedAt + } = application; + if (destinationDockerId) { + const { engine, network } = destinationDocker; + const isRunning = await checkContainer(engine, id); + if (fqdn) { + const domain = getDomain(fqdn); + const isHttps = fqdn.startsWith('https://'); + const isWWW = fqdn.includes('www.'); + const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`; + if (isRunning) { + data.applications.push({ + id, + port: port || 3000, + domain, + isRunning, + isHttps, + redirectValue, + redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain, + updatedAt: updatedAt.getTime() + }); + } + 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, + redirectValue, + 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, + 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 isHttps = fqdn.startsWith('https://'); + const isWWW = fqdn.includes('www.'); + const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`; + 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, isRunning, isHttps, redirectValue, - redirectTo: isWWW ? previewDomain.replace('www.', '') : 'www.' + previewDomain, - updatedAt: updatedAt.getTime() + redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain, + updatedAt: updatedAt.getTime(), + scriptName }); } } } } } - } - const traefik = { - http: { - routers: {}, - services: {} + const { fqdn } = await db.prisma.setting.findFirst(); + if (fqdn) { + const domain = getDomain(fqdn); + const isHttps = fqdn.startsWith('https://'); + const isWWW = fqdn.includes('www.'); + const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`; + data.coolify.push({ + id: dev ? 'host.docker.internal' : 'coolify', + port: 3000, + domain, + isHttps, + redirectValue, + redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain + }); } - }; - for (const application of data.applications) { - const { id, port, domain, isHttps, redirectValue, redirectTo, updatedAt } = application; - traefik.http.routers[id] = { - entrypoints: ['web'], - rule: `Host(\`${domain}\`)`, - service: id + const traefik = { + http: { + routers: {}, + services: {} + } }; - traefik.http.services[id] = { - loadbalancer: { - servers: [ - { - url: `http://${id}:${port}` + for (const application of data.applications) { + const { id, port, domain, isHttps, redirectValue, redirectTo, updatedAt } = application; + traefik.http.routers[id] = { + entrypoints: ['web'], + rule: `Host(\`${domain}\`)`, + service: id + }; + traefik.http.services[id] = { + loadbalancer: { + servers: [ + { + url: `http://${id}:${port}` + } + ] + } + }; + } + for (const application of data.services) { + const { id, port, domain, isHttps, redirectValue, redirectTo, updatedAt, scriptName } = + application; + + traefik.http.routers[id] = { + entrypoints: ['web'], + rule: `Host(\`${domain}\`)`, + service: id + }; + traefik.http.services[id] = { + loadbalancer: { + servers: [ + { + url: `http://${id}:${port}` + } + ] + } + }; + if (scriptName) { + if (!traefik.http.middlewares) traefik.http.middlewares = {}; + traefik.http.middlewares[`${id}-redir`] = { + replacepathregex: { + regex: `/js/${scriptName}`, + replacement: '/js/plausible.js' } - ] + }; + traefik.http.routers[id].middlewares = [`${id}-redir`]; + } + } + for (const application of data.coolify) { + const { domain, id, port } = application; + traefik.http.routers['coolify'] = { + entrypoints: ['web'], + rule: `Host(\`${domain}\`)`, + service: id + }; + traefik.http.services[id] = { + loadbalancer: { + servers: [ + { + url: `http://${id}:${port}` + } + ] + } + }; + } + + return { + status: 200, + body: { + ...traefik + // "http": { + // "routers": { + // "coolify": { + // "entrypoints": [ + // "web" + // ], + // "middlewares": [ + // "coolify-hc" + // ], + // "rule": "Host(`staging.coolify.io`)", + // "service": "coolify" + // }, + // "static.example.coolify.io": { + // "entrypoints": [ + // "web" + // ], + // "rule": "Host(`static.example.coolify.io`)", + // "service": "static.example.coolify.io" + // } + // }, + // "services": { + // "coolify": { + // "loadbalancer": { + // "servers": [ + // { + // "url": "http://coolify:3000" + // } + // ] + // } + // }, + // "static.example.coolify.io": { + // "loadbalancer": { + // "servers": [ + // { + // "url": "http://cl32p06f58068518cs3thg6vbc7:80" + // } + // ] + // } + // } + // }, + // "middlewares": { + // "coolify-hc": { + // "replacepathregex": { + // "regex": "/dead.json", + // "replacement": "/undead.json" + // } + // } + // } + // } } }; } - return { - status: 200, - body: { - ...traefik - // "http": { - // "routers": { - // "coolify": { - // "entrypoints": [ - // "web" - // ], - // "middlewares": [ - // "coolify-hc" - // ], - // "rule": "Host(`staging.coolify.io`)", - // "service": "coolify" - // }, - // "static.example.coolify.io": { - // "entrypoints": [ - // "web" - // ], - // "rule": "Host(`static.example.coolify.io`)", - // "service": "static.example.coolify.io" - // } - // }, - // "services": { - // "coolify": { - // "loadbalancer": { - // "servers": [ - // { - // "url": "http://coolify:3000" - // } - // ] - // } - // }, - // "static.example.coolify.io": { - // "loadbalancer": { - // "servers": [ - // { - // "url": "http://cl32p06f58068518cs3thg6vbc7:80" - // } - // ] - // } - // } - // }, - // "middlewares": { - // "coolify-hc": { - // "replacepathregex": { - // "regex": "/dead.json", - // "replacement": "/undead.json" - // } - // } - // } - // } - } - }; }; diff --git a/src/routes/update.json.ts b/src/routes/update.json.ts index d584cde7b..429030db1 100644 --- a/src/routes/update.json.ts +++ b/src/routes/update.json.ts @@ -6,6 +6,12 @@ import * as db from '$lib/database'; import type { RequestHandler } from '@sveltejs/kit'; import compare from 'compare-versions'; import got from 'got'; +import { + checkContainer, + configureNetworkTraefikProxy, + startCoolifyProxy, + startTraefikProxy +} from '$lib/haproxy'; export const get: RequestHandler = async (request) => { try { @@ -34,14 +40,14 @@ export const get: RequestHandler = async (request) => { export const post: RequestHandler = async (event) => { const { type, latestVersion } = await event.request.json(); + const settings = await db.prisma.setting.findFirst(); if (type === 'update') { try { if (!dev) { - const { isAutoUpdateEnabled } = await db.prisma.setting.findFirst(); await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); await asyncExecShell(`env | grep COOLIFY > .env`); await asyncExecShell( - `sed -i '/COOLIFY_AUTO_UPDATE=/c\COOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` + `sed -i '/COOLIFY_AUTO_UPDATE=/c\COOLIFY_AUTO_UPDATE=${settings.isAutoUpdateEnabled}' .env` ); await asyncExecShell( `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-redis && docker rm coolify coolify-redis && docker compose up -d --force-recreate"` @@ -61,15 +67,37 @@ export const post: RequestHandler = async (event) => { } catch (error) { return ErrorHandler(error); } - } else if (type === 'migrateToTraefik') { + } else if (type === 'traefik') { try { - const settings = await db.prisma.setting.findFirst({}); + const found = await checkContainer('/var/run/docker.sock', 'coolify-haproxy'); + if (found) { + await asyncExecShell(`docker stop -t 0 coolify-haproxy`); + await asyncExecShell(`docker rm coolify-haproxy`); + } + await startTraefikProxy('/var/run/docker.sock'); await db.prisma.setting.update({ where: { id: settings.id }, - data: { disableHaproxy: true } + data: { isTraefikUsed: true } + }); + return { + status: 200, + body: {} + }; + } catch (error) { + return ErrorHandler(error); + } + } else if (type === 'haproxy') { + try { + const found = await checkContainer('/var/run/docker.sock', 'coolify-proxy'); + if (found) { + await asyncExecShell(`docker stop -t 0 coolify-proxy`); + await asyncExecShell(`docker rm coolify-proxy`); + } + await startCoolifyProxy('/var/run/docker.sock'); + await db.prisma.setting.update({ + where: { id: settings.id }, + data: { isTraefikUsed: false } }); - await asyncExecShell(`docker stop -t 0 coolify-haproxy`); - await asyncExecShell(`docker rm coolify-haproxy`); return { status: 200, body: {}