feat: initial commit

This commit is contained in:
Mauricio Siu
2024-04-28 23:57:52 -06:00
parent 8857a20344
commit be56ba046c
412 changed files with 60777 additions and 1 deletions

View File

@@ -0,0 +1,140 @@
import { scheduleJob } from "node-schedule";
import { db } from "../../db/index";
import { runMariadbBackup } from "./mariadb";
import { runMongoBackup } from "./mongo";
import { runMySqlBackup } from "./mysql";
import { runPostgresBackup } from "./postgres";
import {
cleanUpDockerBuilder,
cleanUpSystemPrune,
cleanUpUnusedImages,
} from "../docker/utils";
import { findAdmin } from "@/server/api/services/admin";
export const initCronJobs = async () => {
console.log("Setting up cron jobs....");
const admin = await findAdmin();
if (admin?.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
);
await cleanUpUnusedImages();
await cleanUpDockerBuilder();
await cleanUpSystemPrune();
});
}
const pgs = await db.query.postgres.findMany({
with: {
backups: {
with: {
destination: true,
postgres: true,
mariadb: true,
mysql: true,
mongo: true,
},
},
},
});
for (const pg of pgs) {
for (const backup of pg.backups) {
const { schedule, backupId, enabled } = backup;
if (enabled) {
scheduleJob(backupId, schedule, async () => {
console.log(
`PG-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
);
runPostgresBackup(pg, backup);
});
}
}
}
const mariadbs = await db.query.mariadb.findMany({
with: {
backups: {
with: {
destination: true,
postgres: true,
mariadb: true,
mysql: true,
mongo: true,
},
},
},
});
for (const maria of mariadbs) {
for (const backup of maria.backups) {
const { schedule, backupId, enabled } = backup;
if (enabled) {
scheduleJob(backupId, schedule, async () => {
console.log(
`MARIADB-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
);
await runMariadbBackup(maria, backup);
});
}
}
}
const mongodbs = await db.query.mongo.findMany({
with: {
backups: {
with: {
destination: true,
postgres: true,
mariadb: true,
mysql: true,
mongo: true,
},
},
},
});
for (const mongo of mongodbs) {
for (const backup of mongo.backups) {
const { schedule, backupId, enabled } = backup;
if (enabled) {
scheduleJob(backupId, schedule, async () => {
console.log(
`MONGO-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
);
await runMongoBackup(mongo, backup);
});
}
}
}
const mysqls = await db.query.mysql.findMany({
with: {
backups: {
with: {
destination: true,
postgres: true,
mariadb: true,
mysql: true,
mongo: true,
},
},
},
});
for (const mysql of mysqls) {
for (const backup of mysql.backups) {
const { schedule, backupId, enabled } = backup;
if (enabled) {
scheduleJob(backupId, schedule, async () => {
console.log(
`MYSQL-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
);
await runMySqlBackup(mysql, backup);
});
}
}
}
};

View File

@@ -0,0 +1,40 @@
import { unlink } from "node:fs/promises";
import path from "node:path";
import { execAsync } from "../process/execAsync";
import { uploadToS3 } from "./utils";
import type { BackupSchedule } from "@/server/api/services/backup";
import type { Mariadb } from "@/server/api/services/mariadb";
import { getServiceContainer } from "../docker/utils";
export const runMariadbBackup = async (
mariadb: Mariadb,
backup: BackupSchedule,
) => {
const { appName, databasePassword, databaseUser } = mariadb;
const { prefix, database } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = path.join(prefix, backupFileName);
const containerPath = `/backup/${backupFileName}`;
const hostPath = `./${backupFileName}`;
try {
const { Id: containerId } = await getServiceContainer(appName);
await execAsync(
`docker exec ${containerId} sh -c "rm -rf /backup && mkdir -p /backup"`,
);
await execAsync(
`docker exec ${containerId} sh -c "mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --databases ${database} | gzip > ${containerPath}"`,
);
await execAsync(
`docker cp ${containerId}:/backup/${backupFileName} ${hostPath}`,
);
await uploadToS3(destination, bucketDestination, hostPath);
} catch (error) {
console.log(error);
throw error;
} finally {
await unlink(hostPath);
}
};

View File

@@ -0,0 +1,37 @@
import { unlink } from "node:fs/promises";
import path from "node:path";
import { execAsync } from "../process/execAsync";
import { uploadToS3 } from "./utils";
import type { BackupSchedule } from "@/server/api/services/backup";
import type { Mongo } from "@/server/api/services/mongo";
import { getServiceContainer } from "../docker/utils";
// mongodb://mongo:Bqh7AQl-PRbnBu@localhost:27017/?tls=false&directConnection=true
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
const { appName, databasePassword } = mongo;
const { prefix, database } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.dump.gz`;
const bucketDestination = path.join(prefix, backupFileName);
const containerPath = `/backup/${backupFileName}`;
const hostPath = `./${backupFileName}`;
try {
const { Id: containerId } = await getServiceContainer(appName);
await execAsync(
`docker exec ${containerId} sh -c "rm -rf /backup && mkdir -p /backup"`,
);
await execAsync(
`docker exec ${containerId} sh -c "mongodump -d '${database}' -u 'mongo' -p '${databasePassword}' --authenticationDatabase=admin --archive=${containerPath} --gzip"`,
);
await execAsync(`docker cp ${containerId}:${containerPath} ${hostPath}`);
await uploadToS3(destination, bucketDestination, hostPath);
} catch (error) {
console.log(error);
throw error;
} finally {
await unlink(hostPath);
}
};
// mongorestore -d monguito -u mongo -p Bqh7AQl-PRbnBu --authenticationDatabase admin --gzip --archive=2024-04-13T05:03:58.937Z.dump.gz

View File

@@ -0,0 +1,38 @@
import path from "node:path";
import { execAsync } from "../process/execAsync";
import { unlink } from "node:fs/promises";
import { uploadToS3 } from "./utils";
import type { BackupSchedule } from "@/server/api/services/backup";
import type { MySql } from "@/server/api/services/mysql";
import { getServiceContainer } from "../docker/utils";
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
const { appName, databaseRootPassword } = mysql;
const { prefix, database } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = path.join(prefix, backupFileName);
const containerPath = `/backup/${backupFileName}`;
const hostPath = `./${backupFileName}`;
try {
const { Id: containerId } = await getServiceContainer(appName);
await execAsync(
`docker exec ${containerId} sh -c "rm -rf /backup && mkdir -p /backup"`,
);
await execAsync(
`docker exec ${containerId} sh -c "mysqldump --default-character-set=utf8mb4 -u 'root' --password='${databaseRootPassword}' --single-transaction --no-tablespaces --quick '${database}' | gzip > ${containerPath}"`,
);
await execAsync(
`docker cp ${containerId}:/backup/${backupFileName} ${hostPath}`,
);
await uploadToS3(destination, bucketDestination, hostPath);
} catch (error) {
console.log(error);
throw error;
} finally {
await unlink(hostPath);
}
};

View File

@@ -0,0 +1,41 @@
import path from "node:path";
import { execAsync } from "../process/execAsync";
import { unlink } from "node:fs/promises";
import { uploadToS3 } from "./utils";
import type { BackupSchedule } from "@/server/api/services/backup";
import type { Postgres } from "@/server/api/services/postgres";
import { getServiceContainer } from "../docker/utils";
export const runPostgresBackup = async (
postgres: Postgres,
backup: BackupSchedule,
) => {
const { appName, databaseUser } = postgres;
const { prefix, database } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = path.join(prefix, backupFileName);
const containerPath = `/backup/${backupFileName}`;
const hostPath = `./${backupFileName}`;
try {
const { Id: containerId } = await getServiceContainer(appName);
await execAsync(
`docker exec ${containerId} /bin/bash -c "rm -rf /backup && mkdir -p /backup"`,
);
await execAsync(
`docker exec ${containerId} sh -c "pg_dump -Fc --no-acl --no-owner -h localhost -U ${databaseUser} --no-password '${database}' | gzip > ${containerPath}"`,
);
await execAsync(`docker cp ${containerId}:${containerPath} ${hostPath}`);
await uploadToS3(destination, bucketDestination, hostPath);
} catch (error) {
console.log(error);
throw error;
} finally {
await unlink(hostPath);
}
};
// Restore
// /Applications/pgAdmin 4.app/Contents/SharedSupport/pg_restore --host "localhost" --port "5432" --username "mauricio" --no-password --dbname "postgres" --verbose "/Users/mauricio/Downloads/_databases_2024-04-12T07_02_05.234Z.sql"

View File

@@ -0,0 +1,59 @@
import type { BackupSchedule } from "@/server/api/services/backup";
import type { Destination } from "@/server/api/services/destination";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { scheduleJob, scheduledJobs } from "node-schedule";
import { readFile } from "node:fs/promises";
import { runPostgresBackup } from "./postgres";
import { runMySqlBackup } from "./mysql";
import { runMongoBackup } from "./mongo";
import { runMariadbBackup } from "./mariadb";
export const uploadToS3 = async (
destination: Destination,
destinationBucketPath: string,
filePath: string,
) => {
const { accessKey, secretAccessKey, bucket, region, endpoint } = destination;
const s3Client = new S3Client({
region: region,
...(endpoint && {
endpoint: endpoint,
}),
credentials: {
accessKeyId: accessKey,
secretAccessKey: secretAccessKey,
},
forcePathStyle: true,
});
const fileContent = await readFile(filePath);
const command = new PutObjectCommand({
Bucket: bucket,
Key: destinationBucketPath,
Body: fileContent,
});
await s3Client.send(command);
};
export const scheduleBackup = (backup: BackupSchedule) => {
const { schedule, backupId, databaseType, postgres, mysql, mongo, mariadb } =
backup;
scheduleJob(backupId, schedule, async () => {
if (databaseType === "postgres" && postgres) {
await runPostgresBackup(postgres, backup);
} else if (databaseType === "mysql" && mysql) {
await runMySqlBackup(mysql, backup);
} else if (databaseType === "mongo" && mongo) {
await runMongoBackup(mongo, backup);
} else if (databaseType === "mariadb" && mariadb) {
await runMariadbBackup(mariadb, backup);
}
});
};
export const removeScheduleBackup = (backupId: string) => {
const currentJob = scheduledJobs[backupId];
currentJob?.cancel();
};

View File

@@ -0,0 +1,40 @@
import { docker } from "@/server/constants";
import type { WriteStream } from "node:fs";
import * as tar from "tar-fs";
import type { ApplicationNested } from ".";
import { getBuildAppDirectory } from "../filesystem/directory";
export const buildCustomDocker = async (
application: ApplicationNested,
writeStream: WriteStream,
) => {
const { appName } = application;
const dockerFilePath = getBuildAppDirectory(application);
try {
const image = `${appName}`;
const contextPath =
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
const tarStream = tar.pack(contextPath);
const stream = await docker.buildImage(tarStream, {
t: image,
dockerfile: dockerFilePath.substring(dockerFilePath.lastIndexOf("/") + 1),
// TODO: maybe use or not forcerm
// forcerm: true,
});
await new Promise((resolve, reject) => {
docker.modem.followProgress(
stream,
(err, res) => (err ? reject(err) : resolve(res)),
(event) => {
if (event.stream) {
writeStream.write(event.stream);
}
},
);
});
} catch (error) {
throw error;
}
};

View File

@@ -0,0 +1,40 @@
import type { ApplicationNested } from ".";
import { prepareEnvironmentVariables } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import type { WriteStream } from "node:fs";
// TODO: integrate in the vps sudo chown -R $(whoami) ~/.docker
export const buildHeroku = async (
application: ApplicationNested,
writeStream: WriteStream,
) => {
const { env, appName } = application;
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(env);
try {
const args = [
"build",
appName,
"--path",
buildAppDirectory,
"--builder",
"heroku/builder:22",
];
for (const env in envVariables) {
args.push("--env", env);
}
await spawnAsync("pack", args, (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
// Stream the data
console.log(data);
});
return true;
} catch (e) {
throw e;
}
};

View File

@@ -0,0 +1,138 @@
import { createWriteStream } from "node:fs";
import { docker } from "@/server/constants";
import type { InferResultType } from "@/server/types/with";
import type { CreateServiceOptions } from "dockerode";
import {
calculateResources,
generateBindMounts,
generateFileMounts,
generateVolumeMounts,
prepareEnvironmentVariables,
} from "../docker/utils";
import { buildCustomDocker } from "./docker-file";
import { buildHeroku } from "./heroku";
import { buildNixpacks } from "./nixpacks";
import { buildPaketo } from "./paketo";
// NIXPACKS codeDirectory = where is the path of the code directory
// HEROKU codeDirectory = where is the path of the code directory
// PAKETO codeDirectory = where is the path of the code directory
// DOKERFILE codeDirectory = where is the exact path of the (Dockerfile)
export type ApplicationNested = InferResultType<
"applications",
{ mounts: true; security: true; redirects: true; ports: true }
>;
export const buildApplication = async (
application: ApplicationNested,
logPath: string,
) => {
const writeStream = createWriteStream(logPath, { flags: "a" });
const { buildType, sourceType } = application;
try {
writeStream.write(
`\nBuild ${buildType}: ✅\nSource Type: ${sourceType}: ✅\n`,
);
console.log(`Build ${buildType}: ✅`);
if (buildType === "nixpacks") {
await buildNixpacks(application, writeStream);
} else if (buildType === "heroku_buildpacks") {
await buildHeroku(application, writeStream);
} else if (buildType === "paketo_buildpacks") {
await buildPaketo(application, writeStream);
} else if (buildType === "dockerfile") {
await buildCustomDocker(application, writeStream);
}
await mechanizeDockerContainer(application);
writeStream.write("Docker Deployed: ✅");
} catch (error) {
writeStream.write(`ERROR: ${error}: ❌`);
throw error;
} finally {
writeStream.end();
}
};
export const mechanizeDockerContainer = async (
application: ApplicationNested,
) => {
const {
appName,
env,
mounts,
sourceType,
dockerImage,
cpuLimit,
memoryLimit,
memoryReservation,
cpuReservation,
command,
ports,
} = application;
const resources = calculateResources({
memoryLimit,
memoryReservation,
cpuLimit,
cpuReservation,
});
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);
const filesMount = generateFileMounts(appName, mounts);
const envVariables = prepareEnvironmentVariables(env);
const settings: CreateServiceOptions = {
Name: appName,
TaskTemplate: {
ContainerSpec: {
Image: sourceType === "docker" ? dockerImage! : `${appName}:latest`,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(command
? {
Command: ["/bin/sh"],
Args: ["-c", command],
}
: {}),
},
Networks: [{ Target: "dokploy-network" }],
RestartPolicy: {
Condition: "on-failure",
},
Resources: {
...resources,
},
},
Mode: {
Replicated: {
Replicas: 1,
},
},
EndpointSpec: {
Ports: ports.map((port) => ({
Protocol: port.protocol,
TargetPort: port.targetPort,
PublishedPort: port.publishedPort,
})),
},
UpdateConfig: {
Parallelism: 1,
Order: "start-first",
},
};
try {
const service = docker.getService(appName);
const inspect = await service.inspect();
await service.update({
version: Number.parseInt(inspect.Version.Index),
...settings,
TaskTemplate: {
...settings.TaskTemplate,
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
});
} catch (error) {
await docker.createService(settings);
}
// await cleanUpUnusedImages();
};

View File

@@ -0,0 +1,30 @@
import type { ApplicationNested } from ".";
import { prepareEnvironmentVariables } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import type { WriteStream } from "node:fs";
// TODO: integrate in the vps sudo chown -R $(whoami) ~/.docker
export const buildNixpacks = async (
application: ApplicationNested,
writeStream: WriteStream,
) => {
const { env, appName } = application;
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(env);
try {
const args = ["build", buildAppDirectory, "--name", appName];
for (const env in envVariables) {
args.push("--env", env);
}
await spawnAsync("nixpacks", args, (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
});
return true;
} catch (e) {
throw e;
}
};

View File

@@ -0,0 +1,38 @@
import type { WriteStream } from "node:fs";
import { spawnAsync } from "../process/spawnAsync";
import type { ApplicationNested } from ".";
import { getBuildAppDirectory } from "../filesystem/directory";
import { prepareEnvironmentVariables } from "../docker/utils";
// TODO: integrate in the vps sudo chown -R $(whoami) ~/.docker
export const buildPaketo = async (
application: ApplicationNested,
writeStream: WriteStream,
) => {
const { env, appName } = application;
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(env);
try {
const args = [
"build",
appName,
"--path",
buildAppDirectory,
"--builder",
"paketobuildpacks/builder-jammy-full",
];
for (const env in envVariables) {
args.push("--env", env);
}
await spawnAsync("pack", args, (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
});
return true;
} catch (e) {
throw e;
}
};

View File

@@ -0,0 +1,96 @@
import type { Mariadb } from "@/server/api/services/mariadb";
import { docker } from "@/server/constants";
import type { CreateServiceOptions } from "dockerode";
import {
calculateResources,
generateBindMounts,
generateFileMounts,
generateVolumeMounts,
prepareEnvironmentVariables,
} from "../docker/utils";
import type { Mount } from "@/server/api/services/mount";
type MariadbWithMounts = Mariadb & {
mounts: Mount[];
};
export const buildMariadb = async (mariadb: MariadbWithMounts) => {
const {
appName,
env,
externalPort,
dockerImage,
memoryLimit,
memoryReservation,
databaseName,
databaseUser,
databasePassword,
databaseRootPassword,
cpuLimit,
cpuReservation,
command,
mounts,
} = mariadb;
const defaultMariadbEnv = `MARIADB_DATABASE=${databaseName}\nMARIADB_USER=${databaseUser}\nMARIADB_PASSWORD=${databasePassword}\nMARIADB_ROOT_PASSWORD=${databaseRootPassword}${
env ? `\n${env}` : ""
}`;
const resources = calculateResources({
memoryLimit,
memoryReservation,
cpuLimit,
cpuReservation,
});
const envVariables = prepareEnvironmentVariables(defaultMariadbEnv);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);
const filesMount = generateFileMounts(appName, mounts);
const settings: CreateServiceOptions = {
Name: appName,
TaskTemplate: {
ContainerSpec: {
Image: dockerImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(command
? {
Command: ["/bin/sh"],
Args: ["-c", command],
}
: {}),
},
Networks: [{ Target: "dokploy-network" }],
Resources: {
...resources,
},
},
Mode: {
Replicated: {
Replicas: 1,
},
},
EndpointSpec: {
Mode: "dnsrr",
Ports: externalPort
? [
{
Protocol: "tcp",
TargetPort: 3306,
PublishedPort: externalPort,
PublishMode: "host",
},
]
: [],
},
};
try {
const service = docker.getService(appName);
const inspect = await service.inspect();
await service.update({
version: Number.parseInt(inspect.Version.Index),
...settings,
});
} catch (error) {
await docker.createService(settings);
}
};

View File

@@ -0,0 +1,96 @@
import type { Mongo } from "@/server/api/services/mongo";
import { docker } from "@/server/constants";
import type { CreateServiceOptions } from "dockerode";
import {
calculateResources,
generateBindMounts,
generateFileMounts,
generateVolumeMounts,
prepareEnvironmentVariables,
} from "../docker/utils";
import type { Postgres } from "@/server/api/services/postgres";
import type { Mount } from "@/server/api/services/mount";
type MongoWithMounts = Mongo & {
mounts: Mount[];
};
export const buildMongo = async (mongo: MongoWithMounts) => {
const {
appName,
env,
externalPort,
dockerImage,
memoryLimit,
memoryReservation,
cpuLimit,
cpuReservation,
databaseUser,
databasePassword,
command,
mounts,
} = mongo;
const defaultMongoEnv = `MONGO_INITDB_ROOT_USERNAME=${databaseUser}\nMONGO_INITDB_ROOT_PASSWORD=${databasePassword}${
env ? `\n${env}` : ""
}`;
const resources = calculateResources({
memoryLimit,
memoryReservation,
cpuLimit,
cpuReservation,
});
const envVariables = prepareEnvironmentVariables(defaultMongoEnv);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);
const filesMount = generateFileMounts(appName, mounts);
const settings: CreateServiceOptions = {
Name: appName,
TaskTemplate: {
ContainerSpec: {
Image: dockerImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(command
? {
Command: ["/bin/sh"],
Args: ["-c", command],
}
: {}),
},
Networks: [{ Target: "dokploy-network" }],
Resources: {
...resources,
},
},
Mode: {
Replicated: {
Replicas: 1,
},
},
EndpointSpec: {
Mode: "dnsrr",
Ports: externalPort
? [
{
Protocol: "tcp",
TargetPort: 27017,
PublishedPort: externalPort,
PublishMode: "host",
},
]
: [],
},
};
try {
const service = docker.getService(appName);
const inspect = await service.inspect();
await service.update({
version: Number.parseInt(inspect.Version.Index),
...settings,
});
} catch (error) {
await docker.createService(settings);
}
};

View File

@@ -0,0 +1,102 @@
import type { MySql } from "@/server/api/services/mysql";
import {
calculateResources,
generateBindMounts,
generateFileMounts,
generateVolumeMounts,
prepareEnvironmentVariables,
} from "../docker/utils";
import { docker } from "@/server/constants";
import type { CreateServiceOptions } from "dockerode";
import type { Mount } from "@/server/api/services/mount";
type MysqlWithMounts = MySql & {
mounts: Mount[];
};
export const buildMysql = async (mysql: MysqlWithMounts) => {
const {
appName,
env,
externalPort,
dockerImage,
memoryLimit,
memoryReservation,
databaseName,
databaseUser,
databasePassword,
databaseRootPassword,
cpuLimit,
cpuReservation,
command,
mounts,
} = mysql;
const defaultMysqlEnv =
databaseUser !== "root"
? `MYSQL_USER=${databaseUser}\nMYSQL_DATABASE=${databaseName}\nMYSQL_PASSWORD=${databasePassword}\nMYSQL_ROOT_PASSWORD=${databaseRootPassword}${
env ? `\n${env}` : ""
}`
: `MYSQL_DATABASE=${databaseName}\nMYSQL_ROOT_PASSWORD=${databaseRootPassword}${
env ? `\n${env}` : ""
}`;
const resources = calculateResources({
memoryLimit,
memoryReservation,
cpuLimit,
cpuReservation,
});
const envVariables = prepareEnvironmentVariables(defaultMysqlEnv);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);
const filesMount = generateFileMounts(appName, mounts);
const settings: CreateServiceOptions = {
Name: appName,
TaskTemplate: {
ContainerSpec: {
Image: dockerImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(command
? {
Command: ["/bin/sh"],
Args: ["-c", command],
}
: {}),
},
Networks: [{ Target: "dokploy-network" }],
Resources: {
...resources,
},
},
Mode: {
Replicated: {
Replicas: 1,
},
},
EndpointSpec: {
Mode: "dnsrr",
Ports: externalPort
? [
{
Protocol: "tcp",
TargetPort: 3306,
PublishedPort: externalPort,
PublishMode: "host",
},
]
: [],
},
};
try {
const service = docker.getService(appName);
const inspect = await service.inspect();
await service.update({
version: Number.parseInt(inspect.Version.Index),
...settings,
});
} catch (error) {
await docker.createService(settings);
}
};

View File

@@ -0,0 +1,97 @@
import type { Postgres } from "@/server/api/services/postgres";
import {
calculateResources,
generateBindMounts,
generateFileMounts,
generateVolumeMounts,
prepareEnvironmentVariables,
} from "../docker/utils";
import { docker } from "@/server/constants";
import type { CreateServiceOptions } from "dockerode";
import type { Mount } from "@/server/api/services/mount";
type PostgresWithMounts = Postgres & {
mounts: Mount[];
};
export const buildPostgres = async (postgres: PostgresWithMounts) => {
const {
appName,
env,
externalPort,
dockerImage,
memoryLimit,
memoryReservation,
cpuLimit,
cpuReservation,
databaseName,
databaseUser,
databasePassword,
command,
mounts,
} = postgres;
const defaultPostgresEnv = `POSTGRES_DB=${databaseName}\nPOSTGRES_USER=${databaseUser}\nPOSTGRES_PASSWORD=${databasePassword}${
env ? `\n${env}` : ""
}`;
const resources = calculateResources({
memoryLimit,
memoryReservation,
cpuLimit,
cpuReservation,
});
const envVariables = prepareEnvironmentVariables(defaultPostgresEnv);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);
const filesMount = generateFileMounts(appName, mounts);
const settings: CreateServiceOptions = {
Name: appName,
TaskTemplate: {
ContainerSpec: {
Image: dockerImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(command
? {
Command: ["/bin/sh"],
Args: ["-c", command],
}
: {}),
},
Networks: [{ Target: "dokploy-network" }],
Resources: {
...resources,
},
},
Mode: {
Replicated: {
Replicas: 1,
},
},
EndpointSpec: {
Mode: "dnsrr",
Ports: externalPort
? [
{
Protocol: "tcp",
TargetPort: 5432,
PublishedPort: externalPort,
PublishMode: "host",
},
]
: [],
},
};
try {
const service = docker.getService(appName);
const inspect = await service.inspect();
await service.update({
version: Number.parseInt(inspect.Version.Index),
...settings,
});
} catch (error) {
console.log("error", error);
await docker.createService(settings);
}
};

View File

@@ -0,0 +1,95 @@
import type { Redis } from "@/server/api/services/redis";
import {
calculateResources,
generateBindMounts,
generateFileMounts,
generateVolumeMounts,
prepareEnvironmentVariables,
} from "../docker/utils";
import { docker } from "@/server/constants";
import type { CreateServiceOptions } from "dockerode";
import type { Mount } from "@/server/api/services/mount";
type RedisWithMounts = Redis & {
mounts: Mount[];
};
export const buildRedis = async (redis: RedisWithMounts) => {
const {
appName,
env,
externalPort,
dockerImage,
memoryLimit,
memoryReservation,
databasePassword,
cpuLimit,
cpuReservation,
command,
mounts,
} = redis;
const defaultRedisEnv = `REDIS_PASSWORD=${databasePassword}${
env ? `\n${env}` : ""
}`;
const resources = calculateResources({
memoryLimit,
memoryReservation,
cpuLimit,
cpuReservation,
});
const envVariables = prepareEnvironmentVariables(defaultRedisEnv);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);
const filesMount = generateFileMounts(appName, mounts);
const settings: CreateServiceOptions = {
Name: appName,
TaskTemplate: {
ContainerSpec: {
Image: dockerImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(command
? {
Command: ["/bin/sh"],
Args: ["-c", command],
}
: {}),
},
Networks: [{ Target: "dokploy-network" }],
Resources: {
...resources,
},
},
Mode: {
Replicated: {
Replicas: 1,
},
},
EndpointSpec: {
Mode: "dnsrr",
Ports: externalPort
? [
{
Protocol: "tcp",
TargetPort: 6379,
PublishedPort: externalPort,
PublishMode: "host",
},
]
: [],
},
};
try {
const service = docker.getService(appName);
const inspect = await service.inspect();
await service.update({
version: Number.parseInt(inspect.Version.Index),
...settings,
});
} catch (error) {
await docker.createService(settings);
}
};

View File

@@ -0,0 +1,278 @@
import fs from "node:fs";
import path from "node:path";
import type { Readable } from "node:stream";
import { APPLICATIONS_PATH, docker } from "@/server/constants";
import type { ContainerInfo, ResourceRequirements } from "dockerode";
import type { ApplicationNested } from "../builders";
import { execAsync } from "../process/execAsync";
interface RegistryAuth {
username: string;
password: string;
serveraddress: string;
}
export const pullImage = async (
dockerImage: string,
onData?: (data: any) => void,
authConfig?: Partial<RegistryAuth>,
): Promise<void> => {
try {
if (!dockerImage) {
throw new Error("Docker image not found");
}
await new Promise((resolve, reject) => {
docker.pull(dockerImage, { authconfig: authConfig }, (err, stream) => {
if (err) {
reject(err);
return;
}
docker.modem.followProgress(
stream as Readable,
(err: Error | null, res) => {
if (!err) {
resolve(res);
}
if (err) {
reject(err);
}
},
(event) => {
onData?.(event);
},
);
});
});
} catch (error) {
throw error;
}
};
export const containerExists = async (containerName: string) => {
const container = docker.getContainer(containerName);
try {
await container.inspect();
return true;
} catch (error) {
return false;
}
};
export const stopService = async (appName: string) => {
try {
await execAsync(`docker service scale ${appName}=0 `);
console.log("Service stopped ✅");
} catch (error) {
return error;
}
};
export const getContainerByName = (name: string): Promise<ContainerInfo> => {
const opts = {
limit: 1,
filters: {
name: [name],
},
};
return new Promise((resolve, reject) => {
docker.listContainers(opts, (err, containers) => {
if (err) {
reject(err);
} else if (containers?.length === 0) {
reject(new Error(`No container found with name: ${name}`));
} else if (containers && containers?.length > 0 && containers[0]) {
resolve(containers[0]);
}
});
});
};
export const cleanUpUnusedImages = async () => {
try {
await execAsync("docker image prune --all --force");
} catch (error) {
throw error;
}
};
export const cleanStoppedContainers = async () => {
try {
await execAsync("docker container prune --force");
} catch (error) {
throw error;
}
};
export const cleanUpUnusedVolumes = async () => {
try {
await execAsync("docker volume prune --force");
} catch (error) {
throw error;
}
};
export const cleanUpInactiveContainers = async () => {
try {
const containers = await docker.listContainers({ all: true });
const inactiveContainers = containers.filter(
(container) => container.State !== "running",
);
for (const container of inactiveContainers) {
await docker.getContainer(container.Id).remove({ force: true });
console.log(`Contenedor eliminado: ${container.Id}`);
}
} catch (error) {
console.error("Error al limpiar contenedores inactivos:", error);
throw error;
}
};
export const cleanUpDockerBuilder = async () => {
await execAsync("docker builder prune --all --force");
};
export const cleanUpSystemPrune = async () => {
await execAsync("docker system prune --all --force --volumes");
};
export const startService = async (appName: string) => {
try {
await execAsync(`docker service scale ${appName}=1 `);
} catch (error) {
throw error;
}
};
export const removeService = async (appName: string) => {
try {
await execAsync(`docker service rm ${appName}`);
} catch (error) {
return error;
}
};
export const prepareEnvironmentVariables = (env: string | null) =>
env
?.split("\n")
.map((line) => line.trim()) // Trim whitespace
.filter((line) => line && !line.startsWith("#")) // Exclude empty lines and comments
.map((envVar) => {
let [key, value] = envVar.split("=", 2);
value = value?.replace(/^"(.*)"$/, "$1"); // Remove surrounding double quotes
return `${key}=${value}`;
}) || [];
export const generateVolumeMounts = (mounts: ApplicationNested["mounts"]) => {
if (!mounts || mounts.length === 0) {
return [];
}
return mounts
.filter((mount) => mount.type === "volume")
.map((mount) => ({
Type: "volume" as const,
Source: mount.volumeName || "",
Target: mount.mountPath,
}));
};
type Resources = {
memoryLimit: number | null;
memoryReservation: number | null;
cpuLimit: number | null;
cpuReservation: number | null;
};
export const calculateResources = ({
memoryLimit,
memoryReservation,
cpuLimit,
cpuReservation,
}: Resources): ResourceRequirements => {
return {
Limits: {
MemoryBytes: memoryLimit ? memoryLimit * 1024 * 1024 : undefined,
NanoCPUs: memoryLimit ? (cpuLimit || 1) * 1000 * 1000 * 1000 : undefined,
},
Reservations: {
MemoryBytes: memoryLimit
? (memoryReservation || 1) * 1024 * 1024
: undefined,
NanoCPUs: memoryLimit
? (cpuReservation || 1) * 1000 * 1000 * 1000
: undefined,
},
};
};
export const generateBindMounts = (mounts: ApplicationNested["mounts"]) => {
if (!mounts || mounts.length === 0) {
return [];
}
return mounts
.filter((mount) => mount.type === "bind")
.map((mount) => ({
Type: "bind" as const,
Source: mount.hostPath || "",
Target: mount.mountPath,
}));
};
export const generateFileMounts = (
appName: string,
mounts: ApplicationNested["mounts"],
) => {
if (!mounts || mounts.length === 0) {
return [];
}
return mounts
.filter((mount) => mount.type === "file")
.map((mount) => {
const fileName = mount.mountPath.split("/").pop();
if (!fileName) {
throw new Error("File name not found");
}
const absoluteBasePath = path.resolve(APPLICATIONS_PATH);
const directory = path.join(absoluteBasePath, appName, "files");
const filePath = path.join(directory, fileName);
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true });
}
fs.writeFileSync(filePath, mount.content || "");
return {
Type: "bind" as const,
Source: filePath,
Target: mount.mountPath,
};
});
};
export const getServiceContainer = async (appName: string) => {
try {
const filter = {
status: ["running"],
label: [`com.docker.swarm.service.name=${appName}`],
};
const containers = await docker.listContainers({
filters: JSON.stringify(filter),
});
if (containers.length === 0 || !containers[0]) {
throw new Error(`No container found with name: ${appName}`);
}
const container = containers[0];
return container;
} catch (error) {
throw error;
}
};

View File

@@ -0,0 +1,59 @@
import fs, { promises as fsPromises } from "node:fs";
import path from "node:path";
import type { Application } from "@/server/api/services/application";
import { APPLICATIONS_PATH, MONITORING_PATH } from "@/server/constants";
import { execAsync } from "../process/execAsync";
export const recreateDirectory = async (pathFolder: string): Promise<void> => {
try {
await removeDirectoryIfExistsContent(pathFolder);
await fsPromises.mkdir(pathFolder, { recursive: true });
} catch (error) {
console.error(`Error recreating directory '${pathFolder}':`, error);
}
};
export const removeDirectoryIfExistsContent = async (
path: string,
): Promise<void> => {
if (fs.existsSync(path) && fs.readdirSync(path).length !== 0) {
await execAsync(`rm -rf ${path}`);
}
};
export const removeDirectoryCode = async (appName: string) => {
const directoryPath = path.join(APPLICATIONS_PATH, appName);
try {
await execAsync(`rm -rf ${directoryPath}`);
} catch (error) {
console.error(`Error to remove ${directoryPath}: ${error}`);
throw error;
}
};
export const removeMonitoringDirectory = async (appName: string) => {
const directoryPath = path.join(MONITORING_PATH, appName);
try {
await execAsync(`rm -rf ${directoryPath}`);
} catch (error) {
console.error(`Error to remove ${directoryPath}: ${error}`);
throw error;
}
};
export const getBuildAppDirectory = (application: Application) => {
const { appName, buildType, sourceType, customGitBuildPath, dockerfile } =
application;
const buildPath =
sourceType === "github" ? application?.buildPath : customGitBuildPath;
if (buildType === "dockerfile") {
return path.join(
APPLICATIONS_PATH,
appName,
buildPath ?? "",
dockerfile || "",
);
}
return path.join(APPLICATIONS_PATH, appName, buildPath ?? "");
};

View File

@@ -0,0 +1,62 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { SSH_PATH } from "@/server/constants";
import { spawnAsync } from "../process/spawnAsync";
export const generateSSHKey = async (appName: string) => {
const applicationDirectory = SSH_PATH;
if (!fs.existsSync(applicationDirectory)) {
fs.mkdirSync(applicationDirectory, { recursive: true });
}
const keyPath = path.join(applicationDirectory, `${appName}_rsa`);
if (fs.existsSync(`${keyPath}`)) {
fs.unlinkSync(`${keyPath}`);
}
if (fs.existsSync(`${keyPath}.pub`)) {
fs.unlinkSync(`${keyPath}.pub`);
}
const args = [
"-t",
"rsa",
"-b",
"4096",
"-C",
"dokploy",
"-f",
keyPath,
"-N",
"",
];
try {
await spawnAsync("ssh-keygen", args);
return keyPath;
} catch (error) {
throw error;
}
};
export const readRSAFile = async (appName: string) => {
try {
if (!fs.existsSync(SSH_PATH)) {
fs.mkdirSync(SSH_PATH, { recursive: true });
}
const keyPath = path.join(SSH_PATH, `${appName}_rsa.pub`);
const data = fs.readFileSync(keyPath, { encoding: "utf-8" });
return data;
} catch (error) {
throw error;
}
};
export const removeRSAFiles = async (appName: string) => {
try {
const publicKeyPath = path.join(SSH_PATH, `${appName}_rsa.pub`);
const privateKeyPath = path.join(SSH_PATH, `${appName}_rsa`);
await fs.promises.unlink(publicKeyPath);
await fs.promises.unlink(privateKeyPath);
} catch (error) {
throw error;
}
};

View File

@@ -0,0 +1,3 @@
import { exec } from "node:child_process";
import util from "node:util";
export const execAsync = util.promisify(exec);

View File

@@ -0,0 +1,58 @@
import {
type ChildProcess,
type SpawnOptions,
spawn,
} from "node:child_process";
import BufferList from "bl";
export const spawnAsync = (
command: string,
args?: string[] | undefined,
onData?: (data: string) => void, // Callback opcional para manejar datos en tiempo real
options?: SpawnOptions,
): Promise<BufferList> & { child: ChildProcess } => {
const child = spawn(command, args ?? [], options ?? {});
const stdout = child.stdout ? new BufferList() : new BufferList();
const stderr = child.stderr ? new BufferList() : new BufferList();
if (child.stdout) {
child.stdout.on("data", (data) => {
stdout.append(data);
if (onData) {
onData(data.toString());
}
});
}
if (child.stderr) {
child.stderr.on("data", (data) => {
stderr.append(data);
if (onData) {
onData(data.toString());
}
});
}
const promise = new Promise<BufferList>((resolve, reject) => {
child.on("error", reject);
child.on("close", (code) => {
if (code === 0) {
resolve(stdout);
} else {
const err = new Error(`child exited with code ${code}`) as Error & {
code: number;
stderr: BufferList;
stdout: BufferList;
};
err.code = code || -1;
err.stderr = stderr;
err.stdout = stdout;
reject(err);
}
});
}) as Promise<BufferList> & { child: ChildProcess };
promise.child = child;
return promise;
};

View File

@@ -0,0 +1,49 @@
import { createWriteStream } from "node:fs";
import { type ApplicationNested, mechanizeDockerContainer } from "../builders";
import { pullImage } from "../docker/utils";
interface RegistryAuth {
username: string;
password: string;
serveraddress: string;
}
export const buildDocker = async (
application: ApplicationNested,
logPath: string,
): Promise<void> => {
const { buildType, dockerImage, username, password } = application;
const authConfig: Partial<RegistryAuth> = {
username: username || "",
password: password || "",
};
const writeStream = createWriteStream(logPath, { flags: "a" });
writeStream.write(`\nBuild ${buildType}\n`);
writeStream.write(`Pulling ${dockerImage}: ✅\n`);
try {
if (!dockerImage) {
throw new Error("Docker image not found");
}
await pullImage(
dockerImage,
(data) => {
if (writeStream.writable) {
writeStream.write(`${data.status}\n`);
}
},
authConfig,
);
await mechanizeDockerContainer(application);
writeStream.write("\nDocker Deployed: ✅\n");
} catch (error) {
writeStream.write(`ERROR: ${error}: ❌`);
throw error;
} finally {
writeStream.end();
}
};

View File

@@ -0,0 +1,120 @@
import { createWriteStream } from "node:fs";
import path from "node:path";
import type { Application } from "@/server/api/services/application";
import { APPLICATIONS_PATH, SSH_PATH } from "@/server/constants";
import { TRPCError } from "@trpc/server";
import { recreateDirectory } from "../filesystem/directory";
import { execAsync } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
export const cloneGitRepository = async (
application: Application,
logPath: string,
) => {
const { appName, customGitUrl, customGitBranch, customGitSSHKey } =
application;
if (!customGitUrl || !customGitBranch) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error: Repository not found",
});
}
const writeStream = createWriteStream(logPath, { flags: "a" });
const keyPath = path.join(SSH_PATH, `${appName}_rsa`);
const outputPath = path.join(APPLICATIONS_PATH, appName);
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
try {
await addHostToKnownHosts(customGitUrl);
await recreateDirectory(outputPath);
// const command = `GIT_SSH_COMMAND="ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}" git clone --branch ${customGitBranch} --depth 1 ${customGitUrl} ${gitCopyPath} --progress`;
// const { stdout, stderr } = await execAsync(command);
writeStream.write(
`\nCloning Repo Custom ${customGitUrl} to ${outputPath}: ✅\n`,
);
await spawnAsync(
"git",
[
"clone",
"--branch",
customGitBranch,
"--depth",
"1",
customGitUrl,
outputPath,
"--progress",
],
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
{
env: {
...process.env,
...(customGitSSHKey && {
GIT_SSH_COMMAND: `ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}`,
}),
},
},
);
writeStream.write(`\nCloned Custom Git ${customGitUrl}: ✅\n`);
} catch (error) {
writeStream.write(`\nERROR Cloning Custom Git: ${error}: ❌\n`);
throw error;
} finally {
writeStream.end();
}
};
const addHostToKnownHosts = async (repositoryURL: string) => {
const { domain, port } = sanitizeRepoPathSSH(repositoryURL);
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
const command = `ssh-keyscan -p ${port} ${domain} >> ${knownHostsPath}`;
try {
await execAsync(command);
} catch (error) {
console.error(`Error adding host to known_hosts: ${error}`);
throw error;
}
};
const sanitizeRepoPathSSH = (input: string) => {
const SSH_PATH_RE = new RegExp(
[
/^\s*/,
/(?:(?<proto>[a-z]+):\/\/)?/,
/(?:(?<user>[a-z_][a-z0-9_-]+)@)?/,
/(?<domain>[^\s\/\?#:]+)/,
/(?::(?<port>[0-9]{1,5}))?/,
/(?:[\/:](?<owner>[^\s\/\?#:]+))?/,
/(?:[\/:](?<repo>(?:[^\s\?#:.]|\.(?!git\/?\s*$))+))/,
/(?:.git)?\/?\s*$/,
]
.map((r) => r.source)
.join(""),
"i",
);
const found = input.match(SSH_PATH_RE);
if (!found) {
throw new Error(`Malformatted SSH path: ${input}`);
}
return {
user: found.groups?.user ?? "git",
domain: found.groups?.domain,
port: Number(found.groups?.port ?? 22),
owner: found.groups?.owner ?? "",
repo: found.groups?.repo,
get repoPath() {
return `ssh://${this.user}@${this.domain}:${this.port}/${this.owner}${
this.owner && "/"
}${this.repo}.git`;
},
};
};

View File

@@ -0,0 +1,119 @@
import { createWriteStream } from "node:fs";
import path from "node:path";
import type { Application } from "@/server/api/services/application";
import { APPLICATIONS_PATH } from "@/server/constants";
import { createAppAuth } from "@octokit/auth-app";
import { TRPCError } from "@trpc/server";
import { Octokit } from "octokit";
import { recreateDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import type { Admin } from "@/server/api/services/admin";
export const authGithub = (admin: Admin) => {
if (!haveGithubRequirements(admin)) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Github Account not configured correctly",
});
}
const octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: admin?.githubAppId || 0,
privateKey: admin?.githubPrivateKey || "",
installationId: admin?.githubInstallationId,
},
});
return octokit;
};
export const getGithubToken = async (
octokit: ReturnType<typeof authGithub>,
) => {
const installation = (await octokit.auth({
type: "installation",
})) as {
token: string;
};
return installation.token;
};
export const haveGithubRequirements = (admin: Admin) => {
return !!(
admin?.githubAppId &&
admin?.githubPrivateKey &&
admin?.githubInstallationId
);
};
const getErrorCloneRequirements = (application: Application) => {
const reasons: string[] = [];
const { repository, owner, branch } = application;
if (!repository) reasons.push("1. Repository not assigned.");
if (!owner) reasons.push("2. Owner not specified .");
if (!branch) reasons.push("3. Branch not defined.");
return reasons;
};
export const cloneGithubRepository = async (
admin: Admin,
application: Application,
logPath: string,
) => {
const writeStream = createWriteStream(logPath, { flags: "a" });
const { appName, repository, owner, branch } = application;
const requirements = getErrorCloneRequirements(application);
// Check if requirements are met
if (requirements.length > 0) {
writeStream.write(
`\nGitHub Repository configuration failed for application: ${appName}\n`,
);
writeStream.write("Reasons:\n");
writeStream.write(requirements.join("\n"));
writeStream.end();
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error: GitHub repository information is incomplete.",
});
}
const outputPath = path.join(APPLICATIONS_PATH, appName);
const octokit = authGithub(admin);
const token = await getGithubToken(octokit);
const repoclone = `github.com/${owner}/${repository}.git`;
await recreateDirectory(outputPath);
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
try {
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
await spawnAsync(
"git",
[
"clone",
"--branch",
branch!,
"--depth",
"1",
cloneUrl,
outputPath,
"--progress",
],
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
);
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
} catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`);
throw error;
} finally {
writeStream.end();
}
};

View File

@@ -0,0 +1,132 @@
import fs, { writeFileSync } from "node:fs";
import path from "node:path";
import type { Domain } from "@/server/api/services/domain";
import { DYNAMIC_TRAEFIK_PATH } from "@/server/constants";
import { dump, load } from "js-yaml";
import type { FileConfig, HttpLoadBalancerService } from "./file-types";
export const createTraefikConfig = (appName: string) => {
const defaultPort = 3000;
const serviceURLDefault = `http://${appName}:${defaultPort}`;
const domainDefault = `Host(\`${appName}.docker.localhost\`)`;
const config: FileConfig = {
http: {
routers: {
...(process.env.NODE_ENV === "production"
? {}
: {
[`${appName}-router-1`]: {
rule: domainDefault,
service: `${appName}-service-1`,
entryPoints: ["web"],
},
}),
},
services: {
...(process.env.NODE_ENV === "production"
? {}
: {
[`${appName}-service-1`]: {
loadBalancer: {
servers: [{ url: serviceURLDefault }],
passHostHeader: true,
},
},
}),
},
},
};
const yamlStr = dump(config);
fs.mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true });
writeFileSync(
path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`),
yamlStr,
"utf8",
);
};
export const removeTraefikConfig = async (appName: string) => {
try {
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
if (fs.existsSync(configPath)) {
await fs.promises.unlink(configPath);
}
} catch (error) {}
};
export const loadOrCreateConfig = (appName: string): FileConfig => {
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
if (fs.existsSync(configPath)) {
const yamlStr = fs.readFileSync(configPath, "utf8");
const parsedConfig = (load(yamlStr) as FileConfig) || {
http: { routers: {}, services: {} },
};
return parsedConfig;
}
return { http: { routers: {}, services: {} } };
};
export const readConfig = (appName: string) => {
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
if (fs.existsSync(configPath)) {
const yamlStr = fs.readFileSync(configPath, "utf8");
return yamlStr;
}
return null;
};
export const readConfigInPath = (pathFile: string) => {
const configPath = path.join(pathFile);
if (fs.existsSync(configPath)) {
const yamlStr = fs.readFileSync(configPath, "utf8");
return yamlStr;
}
return null;
};
export const writeConfig = (appName: string, traefikConfig: string) => {
try {
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
fs.writeFileSync(configPath, traefikConfig, "utf8");
} catch (e) {
console.error("Error saving the YAML config file:", e);
}
};
export const writeTraefikConfigInPath = (
pathFile: string,
traefikConfig: string,
) => {
try {
const configPath = path.join(pathFile);
fs.writeFileSync(configPath, traefikConfig, "utf8");
} catch (e) {
console.error("Error saving the YAML config file:", e);
}
};
export const writeTraefikConfig = (
traefikConfig: FileConfig,
appName: string,
) => {
try {
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
const yamlStr = dump(traefikConfig);
fs.writeFileSync(configPath, yamlStr, "utf8");
} catch (e) {
console.error("Error saving the YAML config file:", e);
}
};
export const createServiceConfig = (
appName: string,
domain: Domain,
): {
loadBalancer: HttpLoadBalancerService;
} => ({
loadBalancer: {
servers: [{ url: `http://${appName}:${domain.port || 80}` }],
passHostHeader: true,
},
});

View File

@@ -0,0 +1,82 @@
import {
createServiceConfig,
loadOrCreateConfig,
writeTraefikConfig,
} from "./application";
import type { ApplicationNested } from "../builders";
import type { Domain } from "@/server/api/services/domain";
import type { FileConfig, HttpRouter } from "./file-types";
export const manageDomain = async (app: ApplicationNested, domain: Domain) => {
const { appName } = app;
const config: FileConfig = loadOrCreateConfig(appName);
const serviceName = `${appName}-service-${domain.uniqueConfigKey}`;
const routerName = `${appName}-router-${domain.uniqueConfigKey}`;
config.http = config.http || { routers: {}, services: {} };
config.http.routers = config.http.routers || {};
config.http.services = config.http.services || {};
config.http.routers[routerName] = await createRouterConfig(app, domain);
config.http.services[serviceName] = createServiceConfig(appName, domain);
writeTraefikConfig(config, appName);
};
export const removeDomain = (appName: string, uniqueKey: number) => {
const config: FileConfig = loadOrCreateConfig(appName);
const routerKey = `${appName}-router-${uniqueKey}`;
const serviceKey = `${appName}-service-${uniqueKey}`;
if (config.http?.routers?.[routerKey]) {
delete config.http.routers[routerKey];
}
if (config.http?.services?.[serviceKey]) {
delete config.http.services[serviceKey];
}
writeTraefikConfig(config, appName);
};
export const createRouterConfig = async (
app: ApplicationNested,
domain: Domain,
) => {
const { appName, redirects, security } = app;
const { certificateType } = domain;
const { host, path, https, uniqueConfigKey } = domain;
const routerConfig: HttpRouter = {
rule: `Host(\`${host}\`)${path ? ` && PathPrefix(\`${path}\`)` : ""}`,
service: `${appName}-service-${uniqueConfigKey}`,
middlewares: [],
entryPoints: https
? ["web", ...(process.env.NODE_ENV === "production" ? ["websecure"] : [])]
: ["web"],
tls: {},
};
if (https) {
routerConfig.middlewares = ["redirect-to-https"];
}
// redirects
for (const redirect of redirects) {
const middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`;
routerConfig.middlewares?.push(middlewareName);
}
// security
if (security.length > 0) {
const middlewareName = `auth-${appName}`;
routerConfig.middlewares?.push(middlewareName);
}
if (certificateType === "letsencrypt") {
routerConfig.tls = { certResolver: "letsencrypt" };
} else if (certificateType === "none") {
routerConfig.tls = undefined;
}
return routerConfig;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,79 @@
import { dump, load } from "js-yaml";
import { join } from "node:path";
import { DYNAMIC_TRAEFIK_PATH } from "@/server/constants";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import type { ApplicationNested } from "../builders";
import type { FileConfig } from "./file-types";
export const addMiddleware = (config: FileConfig, middlewareName: string) => {
if (config.http?.routers) {
const values = Object.keys(config.http.routers);
for (const routerName of values) {
const router = config.http.routers[routerName];
if (router) {
if (!router.middlewares) {
router.middlewares = [];
}
if (!router.middlewares.includes(middlewareName)) {
router.middlewares.push(middlewareName);
}
}
}
}
};
export const deleteMiddleware = (
config: FileConfig,
middlewareName: string,
) => {
if (config.http?.routers) {
const values = Object.keys(config?.http?.routers);
for (const routerName of values) {
const router = config.http.routers[routerName];
if (router?.middlewares) {
router.middlewares = router.middlewares.filter(
(m) => m !== middlewareName,
);
}
}
}
};
export const deleteAllMiddlewares = (application: ApplicationNested) => {
const config = loadMiddlewares<FileConfig>();
const { security, appName, redirects } = application;
if (config.http?.middlewares) {
if (security.length > 0) {
const middlewareName = `auth-${appName}`;
delete config.http.middlewares[middlewareName];
}
for (const redirect of redirects) {
const middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`;
delete config.http.middlewares[middlewareName];
}
}
writeMiddleware(config);
};
export const loadMiddlewares = <T>() => {
const configPath = join(DYNAMIC_TRAEFIK_PATH, "middlewares.yml");
if (!existsSync(configPath)) {
throw new Error(`File not found: ${configPath}`);
}
const yamlStr = readFileSync(configPath, "utf8");
const config = load(yamlStr) as T;
return config;
};
export const writeMiddleware = <T>(config: T) => {
const configPath = join(DYNAMIC_TRAEFIK_PATH, "middlewares.yml");
const newYamlContent = dump(config);
writeFileSync(configPath, newYamlContent, "utf8");
};

View File

@@ -0,0 +1,69 @@
import type { Redirect } from "@/server/api/services/redirect";
import { loadOrCreateConfig, writeTraefikConfig } from "./application";
import {
addMiddleware,
deleteMiddleware,
loadMiddlewares,
writeMiddleware,
} from "./middleware";
import type { FileConfig } from "./file-types";
export const updateRedirectMiddleware = (appName: string, data: Redirect) => {
const config = loadMiddlewares<FileConfig>();
if (config?.http?.middlewares?.[appName]) {
const middlewareName = `${appName}-${data.uniqueConfigKey}`;
config.http.middlewares[middlewareName] = {
redirectRegex: {
regex: data.regex,
replacement: data.replacement,
permanent: data.permanent,
},
};
}
writeMiddleware(config);
};
export const createRedirectMiddleware = (appName: string, data: Redirect) => {
const config = loadMiddlewares<FileConfig>();
const middlewareName = `redirect-${appName}-${data.uniqueConfigKey}`;
const newMiddleware = {
[middlewareName]: {
redirectRegex: {
regex: data.regex,
replacement: data.replacement,
permanent: data.permanent,
},
},
};
if (config?.http) {
config.http.middlewares = {
...config.http.middlewares,
...newMiddleware,
};
}
const appConfig = loadOrCreateConfig(appName);
addMiddleware(appConfig, middlewareName);
writeTraefikConfig(appConfig, appName);
writeMiddleware(config);
};
export const removeRedirectMiddleware = (appName: string, data: Redirect) => {
const config = loadMiddlewares<FileConfig>();
const middlewareName = `redirect-${appName}-${data.uniqueConfigKey}`;
if (config?.http?.middlewares?.[middlewareName]) {
delete config.http.middlewares[middlewareName];
}
const appConfig = loadOrCreateConfig(appName);
deleteMiddleware(appConfig, middlewareName);
writeTraefikConfig(appConfig, appName);
writeMiddleware(config);
};

View File

@@ -0,0 +1,82 @@
import { loadOrCreateConfig, writeTraefikConfig } from "./application";
import type { Security } from "@/server/api/services/security";
import * as bcrypt from "bcrypt";
import {
addMiddleware,
deleteMiddleware,
loadMiddlewares,
writeMiddleware,
} from "./middleware";
import type {
BasicAuthMiddleware,
FileConfig,
HttpMiddleware,
} from "./file-types";
export const createSecurityMiddleware = async (
appName: string,
data: Security,
) => {
const config = loadMiddlewares<FileConfig>();
const middlewareName = `auth-${appName}`;
const user = `${data.username}:${await bcrypt.hash(data.password, 10)}`;
if (config.http?.middlewares) {
const currentMiddleware = config.http.middlewares[middlewareName];
if (isBasicAuthMiddleware(currentMiddleware)) {
currentMiddleware.basicAuth.users = [
...(currentMiddleware.basicAuth.users || []),
user,
];
} else {
config.http.middlewares[middlewareName] = {
basicAuth: {
removeHeader: true,
users: [user],
},
};
}
}
const appConfig = loadOrCreateConfig(appName);
addMiddleware(appConfig, middlewareName);
writeTraefikConfig(appConfig, appName);
writeMiddleware(config);
};
export const removeSecurityMiddleware = (appName: string, data: Security) => {
const config = loadMiddlewares<FileConfig>();
const appConfig = loadOrCreateConfig(appName);
const middlewareName = `auth-${appName}`;
if (config.http?.middlewares) {
const currentMiddleware = config.http.middlewares[middlewareName];
if (isBasicAuthMiddleware(currentMiddleware)) {
const users = currentMiddleware.basicAuth.users;
const filteredUsers =
users?.filter((user) => {
const [username] = user.split(":");
return username !== data.username;
}) || [];
currentMiddleware.basicAuth.users = filteredUsers;
if (filteredUsers.length === 0) {
if (config?.http?.middlewares?.[middlewareName]) {
delete config.http.middlewares[middlewareName];
}
deleteMiddleware(appConfig, middlewareName);
writeTraefikConfig(appConfig, appName);
}
}
}
writeMiddleware(config);
};
const isBasicAuthMiddleware = (
middleware: HttpMiddleware | undefined,
): middleware is { basicAuth: BasicAuthMiddleware } => {
return !!middleware && "basicAuth" in middleware;
};

View File

@@ -0,0 +1,574 @@
/* eslint-disable */
export interface MainTraefikConfig {
accessLog?: {
filePath?: string;
format?: string;
filters?: {
statusCodes?: string[];
retryAttempts?: boolean;
minDuration?: string;
[k: string]: unknown;
};
fields?: {
defaultMode?: string;
names?: {
/**
* This interface was referenced by `undefined`'s JSON-Schema definition
* via the `patternProperty` "[a-zA-Z0-9-_]+".
*/
[k: string]: string;
};
headers?: {
defaultMode?: string;
names?: {
/**
* This interface was referenced by `undefined`'s JSON-Schema definition
* via the `patternProperty` "[a-zA-Z0-9-_]+".
*/
[k: string]: string;
};
[k: string]: unknown;
};
[k: string]: unknown;
};
bufferingSize?: number;
};
api?: {
insecure?: boolean;
dashboard?: boolean;
debug?: boolean;
};
certificatesResolvers?: {
/**
* This interface was referenced by `undefined`'s JSON-Schema definition
* via the `patternProperty` "[a-zA-Z0-9-_]+".
*/
[k: string]: {
acme?: {
email?: string;
caServer?: string;
certificatesDuration?: number;
preferredChain?: string;
storage?: string;
keyType?: string;
eab?: {
kid?: string;
hmacEncoded?: string;
[k: string]: unknown;
};
dnsChallenge?: {
provider?: string;
delayBeforeCheck?: string;
resolvers?: string[];
disablePropagationCheck?: boolean;
[k: string]: unknown;
};
httpChallenge?: {
entryPoint?: string;
[k: string]: unknown;
};
tlsChallenge?: {
[k: string]: unknown;
};
[k: string]: unknown;
};
};
};
entryPoints?: {
/**
* This interface was referenced by `undefined`'s JSON-Schema definition
* via the `patternProperty` "[a-zA-Z0-9-_]+".
*/
[k: string]: {
address?: string;
transport?: {
lifeCycle?: {
requestAcceptGraceTimeout?: string;
graceTimeOut?: string;
[k: string]: unknown;
};
respondingTimeouts?: {
readTimeout?: string;
writeTimeout?: string;
idleTimeout?: string;
[k: string]: unknown;
};
[k: string]: unknown;
};
proxyProtocol?: {
insecure?: boolean;
trustedIPs?: string[];
[k: string]: unknown;
};
forwardedHeaders?: {
insecure?: boolean;
trustedIPs?: string[];
[k: string]: unknown;
};
http?: {
redirections?: {
entryPoint?: {
to?: string;
scheme?: string;
permanent?: boolean;
priority?: number;
[k: string]: unknown;
};
[k: string]: unknown;
};
middlewares?: string[];
tls?: {
options?: string;
certResolver?: string;
domains?: {
main?: string;
sans?: string[];
[k: string]: unknown;
}[];
[k: string]: unknown;
};
[k: string]: unknown;
};
http2?: {
maxConcurrentStreams?: number;
[k: string]: unknown;
};
http3?: {
advertisedPort?: number;
[k: string]: unknown;
};
udp?: {
timeout?: string;
[k: string]: unknown;
};
};
};
experimental?: {
kubernetesGateway?: boolean;
http3?: boolean;
hub?: boolean;
plugins?: {
/**
* This interface was referenced by `undefined`'s JSON-Schema definition
* via the `patternProperty` "[a-zA-Z0-9-_]+".
*/
[k: string]: {
moduleName?: string;
version?: string;
};
};
localPlugins?: {
/**
* This interface was referenced by `undefined`'s JSON-Schema definition
* via the `patternProperty` "[a-zA-Z0-9-_]+".
*/
[k: string]: {
moduleName?: string;
};
};
};
global?: {
checkNewVersion?: boolean;
sendAnonymousUsage?: boolean;
};
hostResolver?: {
cnameFlattening?: boolean;
resolvConfig?: string;
resolvDepth?: number;
};
hub?: {
tls?: {
insecure?: boolean;
ca?: string;
cert?: string;
key?: string;
[k: string]: unknown;
};
};
log?: {
level?: string;
filePath?: string;
format?: string;
};
metrics?: {
prometheus?: {
buckets?: number[];
addEntryPointsLabels?: boolean;
addRoutersLabels?: boolean;
addServicesLabels?: boolean;
entryPoint?: string;
manualRouting?: boolean;
};
datadog?: {
address?: string;
pushInterval?: string;
addEntryPointsLabels?: boolean;
addRoutersLabels?: boolean;
addServicesLabels?: boolean;
prefix?: string;
};
statsD?: {
address?: string;
pushInterval?: string;
addEntryPointsLabels?: boolean;
addRoutersLabels?: boolean;
addServicesLabels?: boolean;
prefix?: string;
};
influxDB?: {
address?: string;
protocol?: string;
pushInterval?: string;
database?: string;
retentionPolicy?: string;
username?: string;
password?: string;
addEntryPointsLabels?: boolean;
addRoutersLabels?: boolean;
addServicesLabels?: boolean;
additionalLabels?: {
[k: string]: unknown;
};
};
influxDB2?: {
address?: string;
token?: string;
pushInterval?: string;
org?: string;
bucket?: string;
addEntryPointsLabels?: boolean;
addRoutersLabels?: boolean;
addServicesLabels?: boolean;
additionalLabels?: {
[k: string]: unknown;
};
};
};
pilot?: {
token?: string;
dashboard?: boolean;
};
ping?: {
entryPoint?: string;
manualRouting?: boolean;
terminatingStatusCode?: number;
};
providers?: {
providersThrottleDuration?: string;
docker?: {
allowEmptyServices?: boolean;
constraints?: string;
defaultRule?: string;
endpoint?: string;
exposedByDefault?: boolean;
httpClientTimeout?: number;
network?: string;
swarmMode?: boolean;
swarmModeRefreshSeconds?: string;
tls?: {
ca?: string;
caOptional?: boolean;
cert?: string;
key?: string;
insecureSkipVerify?: boolean;
};
useBindPortIP?: boolean;
watch?: boolean;
};
file?: {
directory?: string;
watch?: boolean;
filename?: string;
debugLogGeneratedTemplate?: boolean;
};
marathon?: {
constraints?: string;
trace?: boolean;
watch?: boolean;
endpoint?: string;
defaultRule?: string;
exposedByDefault?: boolean;
dcosToken?: string;
tls?: {
ca?: string;
caOptional?: boolean;
cert?: string;
key?: string;
insecureSkipVerify?: boolean;
};
dialerTimeout?: string;
responseHeaderTimeout?: string;
tlsHandshakeTimeout?: string;
keepAlive?: string;
forceTaskHostname?: boolean;
basic?: {
httpBasicAuthUser?: string;
httpBasicPassword?: string;
};
respectReadinessChecks?: boolean;
};
kubernetesIngress?: {
endpoint?: string;
token?: string;
certAuthFilePath?: string;
namespaces?: string[];
labelSelector?: string;
ingressClass?: string;
throttleDuration?: string;
allowEmptyServices?: boolean;
allowExternalNameServices?: boolean;
ingressEndpoint?: {
ip?: string;
hostname?: string;
publishedService?: string;
};
};
kubernetesCRD?: {
endpoint?: string;
token?: string;
certAuthFilePath?: string;
namespaces?: string[];
allowCrossNamespace?: boolean;
allowExternalNameServices?: boolean;
labelSelector?: string;
ingressClass?: string;
throttleDuration?: string;
allowEmptyServices?: boolean;
};
kubernetesGateway?: {
endpoint?: string;
token?: string;
certAuthFilePath?: string;
namespaces?: string[];
labelSelector?: string;
throttleDuration?: string;
};
rest?: {
insecure?: boolean;
};
rancher?: {
constraints?: string;
watch?: boolean;
defaultRule?: string;
exposedByDefault?: boolean;
enableServiceHealthFilter?: boolean;
refreshSeconds?: number;
intervalPoll?: boolean;
prefix?: string;
};
consulCatalog?: {
constraints?: string;
prefix?: string;
refreshInterval?: string;
requireConsistent?: boolean;
stale?: boolean;
cache?: boolean;
exposedByDefault?: boolean;
defaultRule?: string;
connectAware?: boolean;
connectByDefault?: boolean;
serviceName?: string;
namespace?: string;
namespaces?: string[];
watch?: boolean;
endpoint?: {
address?: string;
scheme?: string;
datacenter?: string;
token?: string;
endpointWaitTime?: string;
tls?: {
ca?: string;
caOptional?: boolean;
cert?: string;
key?: string;
insecureSkipVerify?: boolean;
};
httpAuth?: {
username?: string;
password?: string;
};
};
[k: string]: unknown;
};
nomad?: {
constraints?: string;
prefix?: string;
refreshInterval?: string;
stale?: boolean;
exposedByDefault?: boolean;
defaultRule?: string;
namespace?: string;
endpoint?: {
address?: string;
region?: string;
token?: string;
endpointWaitTime?: string;
tls?: {
ca?: string;
caOptional?: boolean;
cert?: string;
key?: string;
insecureSkipVerify?: boolean;
};
};
};
ecs?: {
constraints?: string;
exposedByDefault?: boolean;
ecsAnywhere?: boolean;
refreshSeconds?: number;
defaultRule?: string;
clusters?: string[];
autoDiscoverClusters?: boolean;
region?: string;
accessKeyID?: string;
secretAccessKey?: string;
};
consul?: {
rootKey?: string;
endpoints?: string[];
token?: string;
namespace?: string;
namespaces?: string[];
tls?: {
ca?: string;
caOptional?: boolean;
cert?: string;
key?: string;
insecureSkipVerify?: boolean;
};
};
etcd?: {
rootKey?: string;
endpoints?: string[];
username?: string;
password?: string;
tls?: {
ca?: string;
caOptional?: boolean;
cert?: string;
key?: string;
insecureSkipVerify?: boolean;
};
};
zooKeeper?: {
rootKey?: string;
endpoints?: string[];
username?: string;
password?: string;
};
redis?: {
rootKey?: string;
endpoints?: string[];
username?: string;
password?: string;
db?: number;
tls?: {
ca?: string;
caOptional?: boolean;
cert?: string;
key?: string;
insecureSkipVerify?: boolean;
};
};
http?: {
endpoint?: string;
pollInterval?: string;
pollTimeout?: string;
tls?: {
ca?: string;
caOptional?: boolean;
cert?: string;
key?: string;
insecureSkipVerify?: boolean;
};
};
plugin?: {
/**
* This interface was referenced by `undefined`'s JSON-Schema definition
* via the `patternProperty` "[a-zA-Z0-9-_]+".
*/
[k: string]: {
[k: string]: unknown;
};
};
[k: string]: unknown;
};
serversTransport?: {
insecureSkipVerify?: boolean;
rootCAs?: string[];
maxIdleConnsPerHost?: number;
forwardingTimeouts?: {
dialTimeout?: string;
responseHeaderTimeout?: string;
idleConnTimeout?: string;
};
};
tracing?: {
serviceName?: string;
spanNameLimit?: number;
jaeger?: {
samplingServerURL?: string;
samplingType?: string;
samplingParam?: number;
localAgentHostPort?: string;
gen128Bit?: boolean;
propagation?: string;
traceContextHeaderName?: string;
disableAttemptReconnecting?: boolean;
collector?: {
endpoint?: string;
user?: string;
password?: string;
};
};
zipkin?: {
httpEndpoint?: string;
sameSpan?: boolean;
id128Bit?: boolean;
sampleRate?: number;
};
datadog?: {
localAgentHostPort?: string;
globalTag?: string;
/**
* Sets a list of key:value tags on all spans.
*/
globalTags?: {
/**
* This interface was referenced by `undefined`'s JSON-Schema definition
* via the `patternProperty` "[a-zA-Z0-9-_]+".
*/
[k: string]: string;
};
debug?: boolean;
prioritySampling?: boolean;
traceIDHeaderName?: string;
parentIDHeaderName?: string;
samplingPriorityHeaderName?: string;
bagagePrefixHeaderName?: string;
};
instana?: {
localAgentHost?: string;
localAgentPort?: number;
logLevel?: string;
enableAutoProfile?: boolean;
};
haystack?: {
localAgentHost?: string;
localAgentPort?: number;
globalTag?: string;
traceIDHeaderName?: string;
parentIDHeaderName?: string;
spanIDHeaderName?: string;
baggagePrefixHeaderName?: string;
};
elastic?: {
serverURL?: string;
secretToken?: string;
serviceEnvironment?: string;
};
};
}

View File

@@ -0,0 +1,70 @@
import { join } from "node:path";
import type { MainTraefikConfig } from "./types";
import { loadOrCreateConfig, writeTraefikConfig } from "./application";
import { MAIN_TRAEFIK_PATH } from "@/server/constants";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { dump, load } from "js-yaml";
import type { FileConfig } from "./file-types";
import type { Admin } from "@/server/api/services/admin";
export const updateServerTraefik = (
admin: Admin | null,
newHost: string | null,
) => {
const appName = "dokploy";
const config: FileConfig = loadOrCreateConfig(appName);
config.http = config.http || { routers: {}, services: {} };
config.http.routers = config.http.routers || {};
const currentRouterConfig = config.http.routers[`${appName}-router-app`];
if (currentRouterConfig) {
if (newHost) {
currentRouterConfig.rule = `Host(\`${newHost}\`)`;
}
if (admin?.certificateType === "letsencrypt") {
currentRouterConfig.tls = { certResolver: "letsencrypt" };
} else if (admin?.certificateType === "none") {
currentRouterConfig.tls = undefined;
}
}
writeTraefikConfig(config, appName);
};
export const updateLetsEncryptEmail = (newEmail: string | null) => {
try {
if (!newEmail) return;
const configPath = join(MAIN_TRAEFIK_PATH, "traefik.yml");
const configContent = readFileSync(configPath, "utf8");
const config = load(configContent) as MainTraefikConfig;
if (config?.certificatesResolvers?.letsencrypt?.acme) {
config.certificatesResolvers.letsencrypt.acme.email = newEmail;
} else {
throw new Error("Invalid Let's Encrypt configuration structure.");
}
const newYamlContent = dump(config);
writeFileSync(configPath, newYamlContent, "utf8");
} catch (error) {
throw error;
}
};
export const readMainConfig = () => {
const configPath = join(MAIN_TRAEFIK_PATH, "traefik.yml");
if (existsSync(configPath)) {
const yamlStr = readFileSync(configPath, "utf8");
return yamlStr;
}
return null;
};
export const writeMainConfig = (traefikConfig: string) => {
try {
const configPath = join(MAIN_TRAEFIK_PATH, "traefik.yml");
writeFileSync(configPath, traefikConfig, "utf8");
} catch (e) {
console.error("Error saving the YAML config file:", e);
}
};