mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: initial commit
This commit is contained in:
140
server/utils/backups/index.ts
Normal file
140
server/utils/backups/index.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
40
server/utils/backups/mariadb.ts
Normal file
40
server/utils/backups/mariadb.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
37
server/utils/backups/mongo.ts
Normal file
37
server/utils/backups/mongo.ts
Normal 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
|
||||
38
server/utils/backups/mysql.ts
Normal file
38
server/utils/backups/mysql.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
41
server/utils/backups/postgres.ts
Normal file
41
server/utils/backups/postgres.ts
Normal 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"
|
||||
59
server/utils/backups/utils.ts
Normal file
59
server/utils/backups/utils.ts
Normal 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();
|
||||
};
|
||||
40
server/utils/builders/docker-file.ts
Normal file
40
server/utils/builders/docker-file.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
40
server/utils/builders/heroku.ts
Normal file
40
server/utils/builders/heroku.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
138
server/utils/builders/index.ts
Normal file
138
server/utils/builders/index.ts
Normal 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();
|
||||
};
|
||||
30
server/utils/builders/nixpacks.ts
Normal file
30
server/utils/builders/nixpacks.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
38
server/utils/builders/paketo.ts
Normal file
38
server/utils/builders/paketo.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
96
server/utils/databases/mariadb.ts
Normal file
96
server/utils/databases/mariadb.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
96
server/utils/databases/mongo.ts
Normal file
96
server/utils/databases/mongo.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
102
server/utils/databases/mysql.ts
Normal file
102
server/utils/databases/mysql.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
97
server/utils/databases/postgres.ts
Normal file
97
server/utils/databases/postgres.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
95
server/utils/databases/redis.ts
Normal file
95
server/utils/databases/redis.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
278
server/utils/docker/utils.ts
Normal file
278
server/utils/docker/utils.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
59
server/utils/filesystem/directory.ts
Normal file
59
server/utils/filesystem/directory.ts
Normal 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 ?? "");
|
||||
};
|
||||
62
server/utils/filesystem/ssh.ts
Normal file
62
server/utils/filesystem/ssh.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
3
server/utils/process/execAsync.ts
Normal file
3
server/utils/process/execAsync.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { exec } from "node:child_process";
|
||||
import util from "node:util";
|
||||
export const execAsync = util.promisify(exec);
|
||||
58
server/utils/process/spawnAsync.ts
Normal file
58
server/utils/process/spawnAsync.ts
Normal 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;
|
||||
};
|
||||
49
server/utils/providers/docker.ts
Normal file
49
server/utils/providers/docker.ts
Normal 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();
|
||||
}
|
||||
};
|
||||
120
server/utils/providers/git.ts
Normal file
120
server/utils/providers/git.ts
Normal 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`;
|
||||
},
|
||||
};
|
||||
};
|
||||
119
server/utils/providers/github.ts
Normal file
119
server/utils/providers/github.ts
Normal 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();
|
||||
}
|
||||
};
|
||||
132
server/utils/traefik/application.ts
Normal file
132
server/utils/traefik/application.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
82
server/utils/traefik/domain.ts
Normal file
82
server/utils/traefik/domain.ts
Normal 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;
|
||||
};
|
||||
1274
server/utils/traefik/file-types.ts
Normal file
1274
server/utils/traefik/file-types.ts
Normal file
File diff suppressed because it is too large
Load Diff
79
server/utils/traefik/middleware.ts
Normal file
79
server/utils/traefik/middleware.ts
Normal 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");
|
||||
};
|
||||
69
server/utils/traefik/redirect.ts
Normal file
69
server/utils/traefik/redirect.ts
Normal 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);
|
||||
};
|
||||
82
server/utils/traefik/security.ts
Normal file
82
server/utils/traefik/security.ts
Normal 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;
|
||||
};
|
||||
574
server/utils/traefik/types.ts
Normal file
574
server/utils/traefik/types.ts
Normal 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;
|
||||
};
|
||||
};
|
||||
}
|
||||
70
server/utils/traefik/web-server.ts
Normal file
70
server/utils/traefik/web-server.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user