WIP: Traefik

This commit is contained in:
Andras Bacsai 2022-05-16 23:20:50 +02:00
parent 4f4f5b1c01
commit 8516ac671a
4 changed files with 370 additions and 443 deletions

View File

@ -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<Got> {
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<void> {
--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 \

View File

@ -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
}
};
}
};

View 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
}
};
};

View 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
};
};