* New Version: 3.0.0
This commit is contained in:
Andras Bacsai
2022-07-06 11:02:36 +02:00
committed by GitHub
parent 9137e8bc32
commit 87ba4560ad
491 changed files with 16824 additions and 20459 deletions

View File

@@ -0,0 +1,222 @@
import axios from "axios";
import cuid from "cuid";
import crypto from "crypto";
import type { FastifyReply, FastifyRequest } from "fastify";
import { encrypt, errorHandler, isDev, prisma } from "../../../lib/common";
import { checkContainer, removeContainer } from "../../../lib/docker";
import { scheduler } from "../../../lib/scheduler";
import { getApplicationFromDB, getApplicationFromDBWebhook } from "../../api/v1/applications/handlers";
export async function installGithub(request: FastifyRequest, reply: FastifyReply): Promise<any> {
try {
const { gitSourceId, installation_id } = request.query;
const source = await prisma.gitSource.findUnique({
where: { id: gitSourceId },
include: { githubApp: true }
});
await prisma.githubApp.update({
where: { id: source.githubAppId },
data: { installationId: Number(installation_id) }
});
if (isDev) {
return reply.redirect(`http://localhost:3000/sources/${gitSourceId}`)
} else {
return reply.redirect(`/sources/${gitSourceId}`)
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function configureGitHubApp(request, reply) {
try {
const { code, state } = request.query;
const { apiUrl } = await prisma.gitSource.findFirst({
where: { id: state },
include: { githubApp: true, gitlabApp: true }
});
const { data }: any = await axios.post(`${apiUrl}/app-manifests/${code}/conversions`);
const { id, client_id, slug, client_secret, pem, webhook_secret } = data
const encryptedClientSecret = encrypt(client_secret);
const encryptedWebhookSecret = encrypt(webhook_secret);
const encryptedPem = encrypt(pem);
await prisma.githubApp.create({
data: {
appId: id,
name: slug,
clientId: client_id,
clientSecret: encryptedClientSecret,
webhookSecret: encryptedWebhookSecret,
privateKey: encryptedPem,
gitSource: { connect: { id: state } }
}
});
if (isDev) {
return reply.redirect(`http://localhost:3000/sources/${state}`)
} else {
return reply.redirect(`/sources/${state}`)
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function gitHubEvents(request: FastifyRequest, reply: FastifyReply): Promise<any> {
try {
const buildId = cuid();
const allowedGithubEvents = ['push', 'pull_request'];
const allowedActions = ['opened', 'reopened', 'synchronize', 'closed'];
const githubEvent = request.headers['x-github-event']?.toString().toLowerCase();
const githubSignature = request.headers['x-hub-signature-256']?.toString().toLowerCase();
if (!allowedGithubEvents.includes(githubEvent)) {
throw { status: 500, message: 'Event not allowed.' }
}
let repository, projectId, branch;
const body = request.body
if (githubEvent === 'push') {
repository = body.repository;
projectId = repository.id;
branch = body.ref.split('/')[2];
} else if (githubEvent === 'pull_request') {
repository = body.pull_request.head.repo;
projectId = repository.id;
branch = body.pull_request.head.ref.split('/')[2];
}
const applicationFound = await getApplicationFromDBWebhook(projectId, branch);
if (applicationFound) {
const webhookSecret = applicationFound.gitSource.githubApp.webhookSecret || null;
//@ts-ignore
const hmac = crypto.createHmac('sha256', webhookSecret);
const digest = Buffer.from(
'sha256=' + hmac.update(JSON.stringify(body)).digest('hex'),
'utf8'
);
if (!isDev) {
const checksum = Buffer.from(githubSignature, 'utf8');
//@ts-ignore
if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest, checksum)) {
throw { status: 500, message: 'SHA256 checksum failed. Are you doing something fishy?' }
};
}
if (githubEvent === 'push') {
if (!applicationFound.configHash) {
const configHash = crypto
//@ts-ignore
.createHash('sha256')
.update(
JSON.stringify({
buildPack: applicationFound.buildPack,
port: applicationFound.port,
exposePort: applicationFound.exposePort,
installCommand: applicationFound.installCommand,
buildCommand: applicationFound.buildCommand,
startCommand: applicationFound.startCommand
})
)
.digest('hex');
await prisma.application.updateMany({
where: { branch, projectId },
data: { configHash }
});
}
await prisma.application.update({
where: { id: applicationFound.id },
data: { updatedAt: new Date() }
});
await prisma.build.create({
data: {
id: buildId,
applicationId: applicationFound.id,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
status: 'queued',
type: 'webhook_commit'
}
});
scheduler.workers.get('deployApplication').postMessage({
build_id: buildId,
type: 'webhook_commit',
...applicationFound
});
return {
message: 'Queued. Thank you!'
};
} else if (githubEvent === 'pull_request') {
const pullmergeRequestId = body.number;
const pullmergeRequestAction = body.action;
const sourceBranch = body.pull_request.head.ref;
if (!allowedActions.includes(pullmergeRequestAction)) {
throw { status: 500, message: 'Action not allowed.' }
}
if (applicationFound.settings.previews) {
if (applicationFound.destinationDockerId) {
const isRunning = await checkContainer(
applicationFound.destinationDocker.engine,
applicationFound.id
);
if (!isRunning) {
throw { status: 500, message: 'Application not running.' }
}
}
if (
pullmergeRequestAction === 'opened' ||
pullmergeRequestAction === 'reopened' ||
pullmergeRequestAction === 'synchronize'
) {
await prisma.application.update({
where: { id: applicationFound.id },
data: { updatedAt: new Date() }
});
await prisma.build.create({
data: {
id: buildId,
applicationId: applicationFound.id,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
status: 'queued',
type: 'webhook_pr'
}
});
scheduler.workers.get('deployApplication').postMessage({
build_id: buildId,
type: 'webhook_pr',
...applicationFound,
sourceBranch,
pullmergeRequestId
});
return {
message: 'Queued. Thank you!'
};
} else if (pullmergeRequestAction === 'closed') {
if (applicationFound.destinationDockerId) {
const id = `${applicationFound.id}-${pullmergeRequestId}`;
const engine = applicationFound.destinationDocker.engine;
await removeContainer({ id, engine });
}
return {
message: 'Removed preview. Thank you!'
};
}
} else {
throw { status: 500, message: 'Pull request previews are not enabled.' }
}
}
}
throw { status: 500, message: 'Not handled event.' }
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@@ -0,0 +1,10 @@
import { FastifyPluginAsync } from 'fastify';
import { configureGitHubApp, gitHubEvents, installGithub } from './handlers';
const root: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.get('/', async (request, reply) => configureGitHubApp(request, reply));
fastify.get('/install', async (request, reply) => installGithub(request, reply));
fastify.post('/events', async (request, reply) => gitHubEvents(request, reply));
};
export default root;

View File

@@ -0,0 +1,178 @@
import axios from "axios";
import cuid from "cuid";
import crypto from "crypto";
import type { FastifyReply, FastifyRequest } from "fastify";
import { encrypt, errorHandler, isDev, listSettings, prisma } from "../../../lib/common";
import { checkContainer, removeContainer } from "../../../lib/docker";
import { scheduler } from "../../../lib/scheduler";
import { getApplicationFromDB, getApplicationFromDBWebhook } from "../../api/v1/applications/handlers";
export async function configureGitLabApp(request: FastifyRequest, reply: FastifyReply) {
try {
const { code, state } = request.query;
const { fqdn } = await listSettings();
const { gitSource: { gitlabApp: { appId, appSecret }, htmlUrl } } = await getApplicationFromDB(state, undefined);
let domain = `http://${request.hostname}`;
if (fqdn) domain = fqdn;
if (isDev) {
domain = `http://localhost:3001`;
}
const params = new URLSearchParams({
client_id: appId,
client_secret: appSecret,
code,
state,
grant_type: 'authorization_code',
redirect_uri: `${domain}/webhooks/gitlab`
});
const { data } = await axios.post(`${htmlUrl}/oauth/token`, params)
if (isDev) {
return reply.redirect(`http://localhost:3000/webhooks/success?token=${data.access_token}`)
}
return reply.redirect(`/webhooks/success?token=${data.access_token}`)
} catch ({ status, message, ...other }) {
console.log(other)
return errorHandler({ status, message })
}
}
export async function gitLabEvents(request: FastifyRequest, reply: FastifyReply) {
try {
const buildId = cuid();
const allowedActions = ['opened', 'reopen', 'close', 'open', 'update'];
const { object_kind: objectKind, ref, project_id } = request.body
const webhookToken = request.headers['x-gitlab-token'];
if (!webhookToken) {
throw { status: 500, message: 'Invalid webhookToken.' }
}
if (objectKind === 'push') {
const projectId = Number(project_id);
const branch = ref.split('/')[2];
const applicationFound = await getApplicationFromDBWebhook(projectId, branch);
if (applicationFound) {
if (!applicationFound.configHash) {
const configHash = crypto
.createHash('sha256')
.update(
JSON.stringify({
buildPack: applicationFound.buildPack,
port: applicationFound.port,
exposePort: applicationFound.exposePort,
installCommand: applicationFound.installCommand,
buildCommand: applicationFound.buildCommand,
startCommand: applicationFound.startCommand
})
)
.digest('hex');
await prisma.application.updateMany({
where: { branch, projectId },
data: { configHash }
});
}
await prisma.application.update({
where: { id: applicationFound.id },
data: { updatedAt: new Date() }
});
await prisma.build.create({
data: {
id: buildId,
applicationId: applicationFound.id,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
status: 'queued',
type: 'webhook_commit'
}
});
scheduler.workers.get('deployApplication').postMessage({
build_id: buildId,
type: 'webhook_commit',
...applicationFound
});
return {
message: 'Queued. Thank you!'
};
}
} else if (objectKind === 'merge_request') {
const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch, iid: pullmergeRequestId }, project: { id } } = request.body
const projectId = Number(id);
if (!allowedActions.includes(action)) {
throw { status: 500, message: 'Action not allowed.' }
}
if (isDraft) {
throw { status: 500, message: 'Draft MR, do nothing.' }
}
const applicationFound = await getApplicationFromDBWebhook(projectId, targetBranch);
if (applicationFound) {
if (applicationFound.settings.previews) {
if (applicationFound.destinationDockerId) {
const isRunning = await checkContainer(
applicationFound.destinationDocker.engine,
applicationFound.id
);
if (!isRunning) {
throw { status: 500, message: 'Application not running.' }
}
}
if (!isDev && applicationFound.gitSource.gitlabApp.webhookToken !== webhookToken) {
throw { status: 500, message: 'Invalid webhookToken. Are you doing something nasty?!' }
}
if (
action === 'opened' ||
action === 'reopen' ||
action === 'open' ||
action === 'update'
) {
await prisma.application.update({
where: { id: applicationFound.id },
data: { updatedAt: new Date() }
});
await prisma.build.create({
data: {
id: buildId,
applicationId: applicationFound.id,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
status: 'queued',
type: 'webhook_mr'
}
});
scheduler.workers.get('deployApplication').postMessage({
build_id: buildId,
type: 'webhook_mr',
...applicationFound,
sourceBranch,
pullmergeRequestId
});
return {
message: 'Queued. Thank you!'
};
} else if (action === 'close') {
if (applicationFound.destinationDockerId) {
const id = `${applicationFound.id}-${pullmergeRequestId}`;
const engine = applicationFound.destinationDocker.engine;
await removeContainer({ id, engine });
}
return {
message: 'Removed preview. Thank you!'
};
}
}
throw { status: 500, message: 'Merge request previews are not enabled.' }
}
}
throw { status: 500, message: 'Not handled event.' }
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@@ -0,0 +1,9 @@
import { FastifyPluginAsync } from 'fastify';
import { configureGitLabApp, gitLabEvents } from './handlers';
const root: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.get('/', async (request, reply) => configureGitLabApp(request, reply));
fastify.post('/events', async (request, reply) => gitLabEvents(request, reply));
};
export default root;

View File

@@ -0,0 +1,489 @@
import { FastifyRequest } from "fastify";
import { asyncExecShell, errorHandler, getDomain, isDev, listServicesWithIncludes, prisma, supportedServiceTypesAndVersions } from "../../../lib/common";
import { getEngine } from "../../../lib/docker";
function configureMiddleware(
{ id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type },
traefik
) {
if (isHttps) {
traefik.http.routers[id] = {
entrypoints: ['web'],
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
service: `${id}`,
middlewares: ['redirect-to-https']
};
traefik.http.services[id] = {
loadbalancer: {
servers: [
{
url: `http://${container}:${port}`
}
]
}
};
if (isDualCerts) {
traefik.http.routers[`${id}-secure`] = {
entrypoints: ['websecure'],
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
service: `${id}`,
tls: {
certresolver: 'letsencrypt'
},
middlewares: []
};
} else {
if (isWWW) {
traefik.http.routers[`${id}-secure-www`] = {
entrypoints: ['websecure'],
rule: `Host(\`www.${nakedDomain}\`)`,
service: `${id}`,
tls: {
certresolver: 'letsencrypt'
},
middlewares: []
};
traefik.http.routers[`${id}-secure`] = {
entrypoints: ['websecure'],
rule: `Host(\`${nakedDomain}\`)`,
service: `${id}`,
tls: {
domains: {
main: `${domain}`
}
},
middlewares: ['redirect-to-www']
};
traefik.http.routers[`${id}`].middlewares.push('redirect-to-www');
} else {
traefik.http.routers[`${id}-secure-www`] = {
entrypoints: ['websecure'],
rule: `Host(\`www.${nakedDomain}\`)`,
service: `${id}`,
tls: {
domains: {
main: `${domain}`
}
},
middlewares: ['redirect-to-non-www']
};
traefik.http.routers[`${id}-secure`] = {
entrypoints: ['websecure'],
rule: `Host(\`${domain}\`)`,
service: `${id}`,
tls: {
certresolver: 'letsencrypt'
},
middlewares: []
};
traefik.http.routers[`${id}`].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://${container}:${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');
}
}
}
if (type === 'plausibleanalytics' && scriptName && scriptName !== 'plausible.js') {
if (!traefik.http.routers[`${id}`].middlewares.includes(`${id}-redir`)) {
traefik.http.routers[`${id}`].middlewares.push(`${id}-redir`);
}
if (!traefik.http.routers[`${id}-secure`].middlewares.includes(`${id}-redir`)) {
traefik.http.routers[`${id}-secure`].middlewares.push(`${id}-redir`);
}
}
}
export async function traefikConfiguration(request, reply) {
try {
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}'
}
}
}
}
};
const applications = await 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 = true;
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,
container: 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}`;
const nakedDomain = previewDomain.replace(/^www\./, '');
data.applications.push({
id: container,
container,
port: port || 3000,
domain: previewDomain,
isRunning,
nakedDomain,
isHttps,
isWWW,
isDualCerts: dualCerts
});
}
}
}
}
}
}
const services = await listServicesWithIncludes();
for (const service of services) {
const {
fqdn,
id,
type,
destinationDocker,
destinationDockerId,
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 = true;
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;
}
let container = id;
let otherDomain = null;
let otherNakedDomain = null;
let otherIsHttps = null;
let otherIsWWW = null;
if (type === 'minio' && service.minio.apiFqdn) {
otherDomain = getDomain(service.minio.apiFqdn);
otherNakedDomain = otherDomain.replace(/^www\./, '');
otherIsHttps = service.minio.apiFqdn.startsWith('https://');
otherIsWWW = service.minio.apiFqdn.includes('www.');
}
data.services.push({
id,
container,
type,
otherDomain,
otherNakedDomain,
otherIsHttps,
otherIsWWW,
port,
publicPort,
domain,
nakedDomain,
isRunning,
isHttps,
isWWW,
isDualCerts: dualCerts,
scriptName
});
}
}
}
}
}
const { fqdn, dualCerts } = await 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: isDev ? 'host.docker.internal' : 'coolify',
container: isDev ? 'host.docker.internal' : 'coolify',
port: 3000,
domain,
nakedDomain,
isHttps,
isWWW,
isDualCerts: dualCerts
});
}
for (const application of data.applications) {
configureMiddleware(application, traefik);
}
for (const service of data.services) {
const { id, scriptName } = service;
configureMiddleware(service, traefik);
if (service.type === 'minio') {
service.id = id + '-minio';
service.container = id;
service.domain = service.otherDomain;
service.nakedDomain = service.otherNakedDomain;
service.isHttps = service.otherIsHttps;
service.isWWW = service.otherIsWWW;
service.port = 9000;
configureMiddleware(service, traefik);
}
if (scriptName) {
traefik.http.middlewares[`${id}-redir`] = {
replacepathregex: {
regex: `/js/${scriptName}`,
replacement: '/js/plausible.js'
}
};
}
}
for (const coolify of data.coolify) {
configureMiddleware(coolify, traefik);
}
if (Object.keys(traefik.http.routers).length === 0) {
traefik.http.routers = null;
}
if (Object.keys(traefik.http.services).length === 0) {
traefik.http.services = null;
}
return {
...traefik
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function traefikOtherConfiguration(request: FastifyRequest, reply) {
try {
const { id } = request.query
if (id) {
const { privatePort, publicPort, type, address = id } = request.query
let traefik = {};
if (publicPort && type && privatePort) {
if (type === 'tcp') {
traefik = {
[type]: {
routers: {
[id]: {
entrypoints: [type],
rule: `HostSNI(\`*\`)`,
service: id
}
},
services: {
[id]: {
loadbalancer: {
servers: [{ address: `${address}:${privatePort}` }]
}
}
}
}
};
} else if (type === 'http') {
const service = await prisma.service.findFirst({
where: { id },
include: { minio: true }
});
if (service) {
if (service.type === 'minio') {
if (service?.minio?.apiFqdn) {
const {
minio: { apiFqdn }
} = service;
const domain = getDomain(apiFqdn);
const isHttps = apiFqdn.startsWith('https://');
traefik = {
[type]: {
routers: {
[id]: {
entrypoints: [type],
rule: `Host(\`${domain}\`)`,
service: id
}
},
services: {
[id]: {
loadbalancer: {
servers: [{ url: `http://${id}:${privatePort}` }]
}
}
}
}
};
if (isHttps) {
if (isDev) {
traefik[type].routers[id].tls = {
domains: {
main: `${domain}`
}
};
} else {
traefik[type].routers[id].tls = {
certresolver: 'letsencrypt'
};
}
}
}
} else {
if (service?.fqdn) {
const domain = getDomain(service.fqdn);
const isHttps = service.fqdn.startsWith('https://');
traefik = {
[type]: {
routers: {
[id]: {
entrypoints: [type],
rule: `Host(\`${domain}:${privatePort}\`)`,
service: id
}
},
services: {
[id]: {
loadbalancer: {
servers: [{ url: `http://${id}:${privatePort}` }]
}
}
}
}
};
if (isHttps) {
if (isDev) {
traefik[type].routers[id].tls = {
domains: {
main: `${domain}`
}
};
} else {
traefik[type].routers[id].tls = {
certresolver: 'letsencrypt'
};
}
}
}
}
} else {
throw { status: 500 }
}
}
} else {
throw { status: 500 }
}
return {
...traefik
};
}
throw { status: 500 }
} catch ({ status, message }) {
console.log(status, message);
return errorHandler({ status, message })
}
}

View File

@@ -0,0 +1,9 @@
import { FastifyPluginAsync } from 'fastify';
import { traefikConfiguration, traefikOtherConfiguration } from './handlers';
const root: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.get('/main.json', async (request, reply) => traefikConfiguration(request, reply));
fastify.get('/other.json', async (request, reply) => traefikOtherConfiguration(request, reply));
};
export default root;