mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-25 09:05:21 +02:00
refactor(server): remove files
This commit is contained in:
@@ -1,157 +0,0 @@
|
||||
import { findAdmin } from "@/server/api/services/admin";
|
||||
import { getAllServers } from "@/server/api/services/server";
|
||||
import { scheduleJob } from "node-schedule";
|
||||
import { db } from "../../db/index";
|
||||
import {
|
||||
cleanUpDockerBuilder,
|
||||
cleanUpSystemPrune,
|
||||
cleanUpUnusedImages,
|
||||
} from "../docker/utils";
|
||||
import { runMariadbBackup } from "./mariadb";
|
||||
import { runMongoBackup } from "./mongo";
|
||||
import { runMySqlBackup } from "./mysql";
|
||||
import { runPostgresBackup } from "./postgres";
|
||||
|
||||
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 servers = await getAllServers();
|
||||
|
||||
for (const server of servers) {
|
||||
const { appName, serverId } = server;
|
||||
if (serverId) {
|
||||
scheduleJob(serverId, "0 0 * * *", async () => {
|
||||
console.log(
|
||||
`SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${appName}`,
|
||||
);
|
||||
await cleanUpUnusedImages(serverId);
|
||||
await cleanUpDockerBuilder(serverId);
|
||||
await cleanUpSystemPrune(serverId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
import path from "node:path";
|
||||
import type { BackupSchedule } from "@/server/api/services/backup";
|
||||
import type { Mariadb } from "@/server/api/services/mariadb";
|
||||
import { findProjectById } from "@/server/api/services/project";
|
||||
import {
|
||||
getRemoteServiceContainer,
|
||||
getServiceContainer,
|
||||
} from "../docker/utils";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getS3Credentials } from "./utils";
|
||||
|
||||
export const runMariadbBackup = async (
|
||||
mariadb: Mariadb,
|
||||
backup: BackupSchedule,
|
||||
) => {
|
||||
const { appName, databasePassword, databaseUser, projectId, name } = mariadb;
|
||||
const project = await findProjectById(projectId);
|
||||
const { prefix, database } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const bucketDestination = path.join(prefix, backupFileName);
|
||||
|
||||
try {
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||
|
||||
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
if (mariadb.serverId) {
|
||||
const { Id: containerId } = await getRemoteServiceContainer(
|
||||
mariadb.serverId,
|
||||
appName,
|
||||
);
|
||||
const mariadbDumpCommand = `docker exec ${containerId} sh -c "mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --databases ${database} | gzip"`;
|
||||
|
||||
await execAsyncRemote(
|
||||
mariadb.serverId,
|
||||
`${mariadbDumpCommand} | ${rcloneCommand}`,
|
||||
);
|
||||
} else {
|
||||
const { Id: containerId } = await getServiceContainer(appName);
|
||||
const mariadbDumpCommand = `docker exec ${containerId} sh -c "mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --databases ${database} | gzip"`;
|
||||
|
||||
await execAsync(`${mariadbDumpCommand} | ${rcloneCommand}`);
|
||||
}
|
||||
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "mariadb",
|
||||
type: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "mariadb",
|
||||
type: "error",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -1,63 +0,0 @@
|
||||
import path from "node:path";
|
||||
import type { BackupSchedule } from "@/server/api/services/backup";
|
||||
import type { Mongo } from "@/server/api/services/mongo";
|
||||
import { findProjectById } from "@/server/api/services/project";
|
||||
import {
|
||||
getRemoteServiceContainer,
|
||||
getServiceContainer,
|
||||
} from "../docker/utils";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getS3Credentials } from "./utils";
|
||||
|
||||
// mongodb://mongo:Bqh7AQl-PRbnBu@localhost:27017/?tls=false&directConnection=true
|
||||
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||
const { appName, databasePassword, databaseUser, projectId, name } = mongo;
|
||||
const project = await findProjectById(projectId);
|
||||
const { prefix, database } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.dump.gz`;
|
||||
const bucketDestination = path.join(prefix, backupFileName);
|
||||
|
||||
try {
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||
|
||||
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
if (mongo.serverId) {
|
||||
const { Id: containerId } = await getRemoteServiceContainer(
|
||||
mongo.serverId,
|
||||
appName,
|
||||
);
|
||||
const mongoDumpCommand = `docker exec ${containerId} sh -c "mongodump -d '${database}' -u '${databaseUser}' -p '${databasePassword}' --authenticationDatabase=admin --gzip"`;
|
||||
|
||||
await execAsyncRemote(
|
||||
mongo.serverId,
|
||||
`${mongoDumpCommand} | ${rcloneCommand}`,
|
||||
);
|
||||
} else {
|
||||
const { Id: containerId } = await getServiceContainer(appName);
|
||||
const mongoDumpCommand = `docker exec ${containerId} sh -c "mongodump -d '${database}' -u '${databaseUser}' -p '${databasePassword}' --authenticationDatabase=admin --gzip"`;
|
||||
await execAsync(`${mongoDumpCommand} | ${rcloneCommand}`);
|
||||
}
|
||||
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "mongodb",
|
||||
type: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "mongodb",
|
||||
type: "error",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
// mongorestore -d monguito -u mongo -p Bqh7AQl-PRbnBu --authenticationDatabase admin --gzip --archive=2024-04-13T05:03:58.937Z.dump.gz
|
||||
@@ -1,62 +0,0 @@
|
||||
import { unlink } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { BackupSchedule } from "@/server/api/services/backup";
|
||||
import type { MySql } from "@/server/api/services/mysql";
|
||||
import { findProjectById } from "@/server/api/services/project";
|
||||
import {
|
||||
getRemoteServiceContainer,
|
||||
getServiceContainer,
|
||||
} from "../docker/utils";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getS3Credentials } from "./utils";
|
||||
|
||||
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||
const { appName, databaseRootPassword, projectId, name } = mysql;
|
||||
const project = await findProjectById(projectId);
|
||||
const { prefix, database } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const bucketDestination = path.join(prefix, backupFileName);
|
||||
|
||||
try {
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||
|
||||
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
if (mysql.serverId) {
|
||||
const { Id: containerId } = await getRemoteServiceContainer(
|
||||
mysql.serverId,
|
||||
appName,
|
||||
);
|
||||
const mysqlDumpCommand = `docker exec ${containerId} sh -c "mysqldump --default-character-set=utf8mb4 -u 'root' --password='${databaseRootPassword}' --single-transaction --no-tablespaces --quick '${database}' | gzip"`;
|
||||
|
||||
await execAsyncRemote(
|
||||
mysql.serverId,
|
||||
`${mysqlDumpCommand} | ${rcloneCommand}`,
|
||||
);
|
||||
} else {
|
||||
const { Id: containerId } = await getServiceContainer(appName);
|
||||
const mysqlDumpCommand = `docker exec ${containerId} sh -c "mysqldump --default-character-set=utf8mb4 -u 'root' --password='${databaseRootPassword}' --single-transaction --no-tablespaces --quick '${database}' | gzip"`;
|
||||
|
||||
await execAsync(`${mysqlDumpCommand} | ${rcloneCommand}`);
|
||||
}
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "mysql",
|
||||
type: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "mysql",
|
||||
type: "error",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
import path from "node:path";
|
||||
import type { BackupSchedule } from "@/server/api/services/backup";
|
||||
import type { Postgres } from "@/server/api/services/postgres";
|
||||
import { findProjectById } from "@/server/api/services/project";
|
||||
import {
|
||||
getRemoteServiceContainer,
|
||||
getServiceContainer,
|
||||
} from "../docker/utils";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getS3Credentials } from "./utils";
|
||||
|
||||
export const runPostgresBackup = async (
|
||||
postgres: Postgres,
|
||||
backup: BackupSchedule,
|
||||
) => {
|
||||
const { appName, databaseUser, name, projectId } = postgres;
|
||||
const project = await findProjectById(projectId);
|
||||
|
||||
const { prefix, database } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const bucketDestination = path.join(prefix, backupFileName);
|
||||
try {
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||
|
||||
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
if (postgres.serverId) {
|
||||
const { Id: containerId } = await getRemoteServiceContainer(
|
||||
postgres.serverId,
|
||||
appName,
|
||||
);
|
||||
const pgDumpCommand = `docker exec ${containerId} sh -c "pg_dump -Fc --no-acl --no-owner -h localhost -U ${databaseUser} --no-password '${database}' | gzip"`;
|
||||
|
||||
await execAsyncRemote(
|
||||
postgres.serverId,
|
||||
`${pgDumpCommand} | ${rcloneCommand}`,
|
||||
);
|
||||
} else {
|
||||
const { Id: containerId } = await getServiceContainer(appName);
|
||||
|
||||
const pgDumpCommand = `docker exec ${containerId} sh -c "pg_dump -Fc --no-acl --no-owner -h localhost -U ${databaseUser} --no-password '${database}' | gzip"`;
|
||||
await execAsync(`${pgDumpCommand} | ${rcloneCommand}`);
|
||||
}
|
||||
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "postgres",
|
||||
type: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "postgres",
|
||||
type: "error",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
});
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
}
|
||||
};
|
||||
|
||||
// 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"
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { BackupSchedule } from "@/server/api/services/backup";
|
||||
import type { Destination } from "@/server/api/services/destination";
|
||||
import { scheduleJob, scheduledJobs } from "node-schedule";
|
||||
import { runMariadbBackup } from "./mariadb";
|
||||
import { runMongoBackup } from "./mongo";
|
||||
import { runMySqlBackup } from "./mysql";
|
||||
import { runPostgresBackup } from "./postgres";
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
export const getS3Credentials = (destination: Destination) => {
|
||||
const { accessKey, secretAccessKey, bucket, region, endpoint } = destination;
|
||||
const rcloneFlags = [
|
||||
// `--s3-provider=Cloudflare`,
|
||||
`--s3-access-key-id=${accessKey}`,
|
||||
`--s3-secret-access-key=${secretAccessKey}`,
|
||||
`--s3-region=${region}`,
|
||||
`--s3-endpoint=${endpoint}`,
|
||||
"--s3-no-check-bucket",
|
||||
"--s3-force-path-style",
|
||||
];
|
||||
|
||||
return rcloneFlags;
|
||||
};
|
||||
@@ -1,216 +0,0 @@
|
||||
import {
|
||||
createWriteStream,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { paths } from "@/server/constants";
|
||||
import type { InferResultType } from "@/server/types/with";
|
||||
import boxen from "boxen";
|
||||
import {
|
||||
writeDomainsToCompose,
|
||||
writeDomainsToComposeRemote,
|
||||
} from "../docker/domain";
|
||||
import { encodeBase64, prepareEnvironmentVariables } from "../docker/utils";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export type ComposeNested = InferResultType<
|
||||
"compose",
|
||||
{ project: true; mounts: true; domains: true }
|
||||
>;
|
||||
export const buildCompose = async (compose: ComposeNested, logPath: string) => {
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const { sourceType, appName, mounts, composeType, domains } = compose;
|
||||
try {
|
||||
const { COMPOSE_PATH } = paths();
|
||||
const command = createCommand(compose);
|
||||
await writeDomainsToCompose(compose, domains);
|
||||
createEnvFile(compose);
|
||||
|
||||
const logContent = `
|
||||
App Name: ${appName}
|
||||
Build Compose 🐳
|
||||
Detected: ${mounts.length} mounts 📂
|
||||
Command: docker ${command}
|
||||
Source Type: docker ${sourceType} ✅
|
||||
Compose Type: ${composeType} ✅`;
|
||||
const logBox = boxen(logContent, {
|
||||
padding: {
|
||||
left: 1,
|
||||
right: 1,
|
||||
bottom: 1,
|
||||
},
|
||||
width: 80,
|
||||
borderStyle: "double",
|
||||
});
|
||||
writeStream.write(`\n${logBox}\n`);
|
||||
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
[...command.split(" ")],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data.toString());
|
||||
}
|
||||
},
|
||||
{
|
||||
cwd: projectPath,
|
||||
env: {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
PATH: process.env.PATH,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
writeStream.write("Docker Compose Deployed: ✅");
|
||||
} catch (error) {
|
||||
writeStream.write("Error ❌");
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const getBuildComposeCommand = async (
|
||||
compose: ComposeNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
const { sourceType, appName, mounts, composeType, domains, composePath } =
|
||||
compose;
|
||||
const command = createCommand(compose);
|
||||
const envCommand = getCreateEnvFileCommand(compose);
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||
|
||||
const newCompose = await writeDomainsToComposeRemote(
|
||||
compose,
|
||||
domains,
|
||||
logPath,
|
||||
);
|
||||
const logContent = `
|
||||
App Name: ${appName}
|
||||
Build Compose 🐳
|
||||
Detected: ${mounts.length} mounts 📂
|
||||
Command: docker ${command}
|
||||
Source Type: docker ${sourceType} ✅
|
||||
Compose Type: ${composeType} ✅`;
|
||||
|
||||
const logBox = boxen(logContent, {
|
||||
padding: {
|
||||
left: 1,
|
||||
right: 1,
|
||||
bottom: 1,
|
||||
},
|
||||
width: 80,
|
||||
borderStyle: "double",
|
||||
});
|
||||
|
||||
const bashCommand = `
|
||||
set -e
|
||||
{
|
||||
echo "${logBox}" >> "${logPath}"
|
||||
|
||||
${newCompose}
|
||||
|
||||
${envCommand}
|
||||
|
||||
cd "${projectPath}";
|
||||
|
||||
docker ${command.split(" ").join(" ")} >> "${logPath}" 2>&1 || { echo "Error: ❌ Docker command failed" >> "${logPath}"; exit 1; }
|
||||
|
||||
echo "Docker Compose Deployed: ✅" >> "${logPath}"
|
||||
} || {
|
||||
echo "Error: ❌ Script execution failed" >> "${logPath}"
|
||||
exit 1
|
||||
}
|
||||
`;
|
||||
|
||||
return await execAsyncRemote(compose.serverId, bashCommand);
|
||||
};
|
||||
|
||||
const sanitizeCommand = (command: string) => {
|
||||
const sanitizedCommand = command.trim();
|
||||
|
||||
const parts = sanitizedCommand.split(/\s+/);
|
||||
|
||||
const restCommand = parts.map((arg) => arg.replace(/^"(.*)"$/, "$1"));
|
||||
|
||||
return restCommand.join(" ");
|
||||
};
|
||||
|
||||
export const createCommand = (compose: ComposeNested) => {
|
||||
const { composeType, appName, sourceType } = compose;
|
||||
|
||||
const path =
|
||||
sourceType === "raw" ? "docker-compose.yml" : compose.composePath;
|
||||
let command = "";
|
||||
|
||||
if (composeType === "docker-compose") {
|
||||
command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`;
|
||||
} else if (composeType === "stack") {
|
||||
command = `stack deploy -c ${path} ${appName} --prune`;
|
||||
}
|
||||
|
||||
const customCommand = sanitizeCommand(compose.command);
|
||||
|
||||
if (customCommand) {
|
||||
command = `${command} ${customCommand}`;
|
||||
}
|
||||
|
||||
return command;
|
||||
};
|
||||
|
||||
const createEnvFile = (compose: ComposeNested) => {
|
||||
const { COMPOSE_PATH } = paths();
|
||||
const { env, composePath, appName } = compose;
|
||||
const composeFilePath =
|
||||
join(COMPOSE_PATH, appName, "code", composePath) ||
|
||||
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
|
||||
|
||||
const envFilePath = join(dirname(composeFilePath), ".env");
|
||||
let envContent = env || "";
|
||||
if (!envContent.includes("DOCKER_CONFIG")) {
|
||||
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
||||
}
|
||||
|
||||
if (compose.randomize) {
|
||||
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
|
||||
}
|
||||
|
||||
const envFileContent = prepareEnvironmentVariables(envContent).join("\n");
|
||||
|
||||
if (!existsSync(dirname(envFilePath))) {
|
||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
||||
}
|
||||
writeFileSync(envFilePath, envFileContent);
|
||||
};
|
||||
|
||||
export const getCreateEnvFileCommand = (compose: ComposeNested) => {
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
const { env, composePath, appName } = compose;
|
||||
const composeFilePath =
|
||||
join(COMPOSE_PATH, appName, "code", composePath) ||
|
||||
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
|
||||
|
||||
const envFilePath = join(dirname(composeFilePath), ".env");
|
||||
|
||||
let envContent = env || "";
|
||||
if (!envContent.includes("DOCKER_CONFIG")) {
|
||||
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
||||
}
|
||||
|
||||
if (compose.randomize) {
|
||||
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
|
||||
}
|
||||
|
||||
const envFileContent = prepareEnvironmentVariables(envContent).join("\n");
|
||||
|
||||
const encodedContent = encodeBase64(envFileContent);
|
||||
return `
|
||||
touch ${envFilePath};
|
||||
echo "${encodedContent}" | base64 -d > "${envFilePath}";
|
||||
`;
|
||||
};
|
||||
@@ -1,116 +0,0 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import { prepareEnvironmentVariables } from "@/server/utils/docker/utils";
|
||||
import type { ApplicationNested } from ".";
|
||||
import {
|
||||
getBuildAppDirectory,
|
||||
getDockerContextPath,
|
||||
} from "../filesystem/directory";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import { createEnvFile, createEnvFileCommand } from "./utils";
|
||||
|
||||
export const buildCustomDocker = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } =
|
||||
application;
|
||||
const dockerFilePath = getBuildAppDirectory(application);
|
||||
try {
|
||||
const image = `${appName}`;
|
||||
|
||||
const defaultContextPath =
|
||||
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
||||
const args = prepareEnvironmentVariables(buildArgs);
|
||||
|
||||
const dockerContextPath = getDockerContextPath(application);
|
||||
|
||||
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
|
||||
|
||||
if (dockerBuildStage) {
|
||||
commandArgs.push("--target", dockerBuildStage);
|
||||
}
|
||||
|
||||
for (const arg of args) {
|
||||
commandArgs.push("--build-arg", arg);
|
||||
}
|
||||
/*
|
||||
Do not generate an environment file when publishDirectory is specified,
|
||||
as it could be publicly exposed.
|
||||
*/
|
||||
if (!publishDirectory) {
|
||||
createEnvFile(dockerFilePath, env);
|
||||
}
|
||||
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
commandArgs,
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
{
|
||||
cwd: dockerContextPath || defaultContextPath,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getDockerCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } =
|
||||
application;
|
||||
const dockerFilePath = getBuildAppDirectory(application);
|
||||
|
||||
try {
|
||||
const image = `${appName}`;
|
||||
|
||||
const defaultContextPath =
|
||||
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
||||
const args = prepareEnvironmentVariables(buildArgs);
|
||||
|
||||
const dockerContextPath =
|
||||
getDockerContextPath(application) || defaultContextPath;
|
||||
|
||||
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
|
||||
|
||||
if (dockerBuildStage) {
|
||||
commandArgs.push("--target", dockerBuildStage);
|
||||
}
|
||||
|
||||
for (const arg of args) {
|
||||
commandArgs.push("--build-arg", arg);
|
||||
}
|
||||
|
||||
/*
|
||||
Do not generate an environment file when publishDirectory is specified,
|
||||
as it could be publicly exposed.
|
||||
*/
|
||||
let command = "";
|
||||
if (!publishDirectory) {
|
||||
command += createEnvFileCommand(dockerFilePath, env);
|
||||
}
|
||||
|
||||
command = `
|
||||
echo "Building ${appName}" >> ${logPath};
|
||||
cd ${dockerContextPath} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ The path ${dockerContextPath} does not exist" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
|
||||
docker ${commandArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Docker build failed" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Docker build completed." >> ${logPath};
|
||||
`;
|
||||
|
||||
return command;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -1,124 +0,0 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path, { join } from "node:path";
|
||||
import type { Application } from "@/server/api/services/application";
|
||||
import { findServerById } from "@/server/api/services/server";
|
||||
import { paths } from "@/server/constants";
|
||||
import AdmZip from "adm-zip";
|
||||
import { Client, type SFTPWrapper } from "ssh2";
|
||||
import {
|
||||
recreateDirectory,
|
||||
recreateDirectoryRemote,
|
||||
} from "../filesystem/directory";
|
||||
import { readSSHKey } from "../filesystem/ssh";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
|
||||
export const unzipDrop = async (zipFile: File, application: Application) => {
|
||||
let sftp: SFTPWrapper | null = null;
|
||||
|
||||
try {
|
||||
const { appName } = application;
|
||||
const { APPLICATIONS_PATH } = paths(!!application.serverId);
|
||||
const outputPath = join(APPLICATIONS_PATH, appName, "code");
|
||||
if (application.serverId) {
|
||||
await recreateDirectoryRemote(outputPath, application.serverId);
|
||||
} else {
|
||||
await recreateDirectory(outputPath);
|
||||
}
|
||||
const arrayBuffer = await zipFile.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const zip = new AdmZip(buffer);
|
||||
const zipEntries = zip.getEntries();
|
||||
|
||||
const rootEntries = zipEntries.filter(
|
||||
(entry) =>
|
||||
entry.entryName.split("/").length === 1 ||
|
||||
(entry.entryName.split("/").length === 2 &&
|
||||
entry.entryName.endsWith("/")),
|
||||
);
|
||||
|
||||
const hasSingleRootFolder = !!(
|
||||
rootEntries.length === 1 && rootEntries[0]?.isDirectory
|
||||
);
|
||||
const rootFolderName = hasSingleRootFolder
|
||||
? rootEntries[0]?.entryName.split("/")[0]
|
||||
: "";
|
||||
|
||||
if (application.serverId) {
|
||||
sftp = await getSFTPConnection(application.serverId);
|
||||
}
|
||||
for (const entry of zipEntries) {
|
||||
let filePath = entry.entryName;
|
||||
|
||||
if (
|
||||
hasSingleRootFolder &&
|
||||
rootFolderName &&
|
||||
filePath.startsWith(`${rootFolderName}/`)
|
||||
) {
|
||||
filePath = filePath.slice(rootFolderName?.length + 1);
|
||||
}
|
||||
|
||||
if (!filePath) continue;
|
||||
|
||||
const fullPath = path.join(outputPath, filePath);
|
||||
|
||||
if (application.serverId) {
|
||||
if (entry.isDirectory) {
|
||||
await execAsyncRemote(application.serverId, `mkdir -p ${fullPath}`);
|
||||
} else {
|
||||
if (sftp === null) throw new Error("No SFTP connection available");
|
||||
await uploadFileToServer(sftp, entry.getData(), fullPath);
|
||||
}
|
||||
} else {
|
||||
if (entry.isDirectory) {
|
||||
await fs.mkdir(fullPath, { recursive: true });
|
||||
} else {
|
||||
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
||||
await fs.writeFile(fullPath, entry.getData());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error processing ZIP file:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
sftp?.end();
|
||||
}
|
||||
};
|
||||
|
||||
const getSFTPConnection = async (serverId: string): Promise<SFTPWrapper> => {
|
||||
const server = await findServerById(serverId);
|
||||
if (!server.sshKeyId) throw new Error("No SSH key available for this server");
|
||||
|
||||
const keys = await readSSHKey(server.sshKeyId);
|
||||
return new Promise((resolve, reject) => {
|
||||
const conn = new Client();
|
||||
conn
|
||||
.on("ready", () => {
|
||||
conn.sftp((err, sftp) => {
|
||||
if (err) return reject(err);
|
||||
resolve(sftp);
|
||||
});
|
||||
})
|
||||
.connect({
|
||||
host: server.ipAddress,
|
||||
port: server.port,
|
||||
username: server.username,
|
||||
privateKey: keys.privateKey,
|
||||
timeout: 99999,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const uploadFileToServer = (
|
||||
sftp: SFTPWrapper,
|
||||
data: Buffer,
|
||||
remotePath: string,
|
||||
): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
sftp.writeFile(remotePath, data, (err) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import type { ApplicationNested } from ".";
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
// 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:24",
|
||||
];
|
||||
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
await spawnAsync("pack", args, (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getHerokuCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { env, appName } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
|
||||
const args = [
|
||||
"build",
|
||||
appName,
|
||||
"--path",
|
||||
buildAppDirectory,
|
||||
"--builder",
|
||||
"heroku/builder:24",
|
||||
];
|
||||
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
const command = `pack ${args.join(" ")}`;
|
||||
const bashCommand = `
|
||||
echo "Starting heroku build..." >> ${logPath};
|
||||
${command} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Heroku build failed" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Heroku build completed." >> ${logPath};
|
||||
`;
|
||||
|
||||
return bashCommand;
|
||||
};
|
||||
@@ -1,216 +0,0 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import type { InferResultType } from "@/server/types/with";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import { uploadImage } from "../cluster/upload";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
generateConfigContainer,
|
||||
generateFileMounts,
|
||||
generateVolumeMounts,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
import { buildCustomDocker, getDockerCommand } from "./docker-file";
|
||||
import { buildHeroku, getHerokuCommand } from "./heroku";
|
||||
import { buildNixpacks, getNixpacksCommand } from "./nixpacks";
|
||||
import { buildPaketo, getPaketoCommand } from "./paketo";
|
||||
import { buildStatic, getStaticCommand } from "./static";
|
||||
|
||||
// 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
|
||||
// DOCKERFILE codeDirectory = where is the exact path of the (Dockerfile)
|
||||
export type ApplicationNested = InferResultType<
|
||||
"applications",
|
||||
{ mounts: true; security: true; redirects: true; ports: true; registry: 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);
|
||||
} else if (buildType === "static") {
|
||||
await buildStatic(application, writeStream);
|
||||
}
|
||||
|
||||
if (application.registryId) {
|
||||
await uploadImage(application, writeStream);
|
||||
}
|
||||
await mechanizeDockerContainer(application);
|
||||
writeStream.write("Docker Deployed: ✅");
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
writeStream.write(`Error ❌\n${error?.message}`);
|
||||
} else {
|
||||
writeStream.write("Error ❌");
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const getBuildCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { buildType } = application;
|
||||
switch (buildType) {
|
||||
case "nixpacks":
|
||||
return getNixpacksCommand(application, logPath);
|
||||
case "heroku_buildpacks":
|
||||
return getHerokuCommand(application, logPath);
|
||||
case "paketo_buildpacks":
|
||||
return getPaketoCommand(application, logPath);
|
||||
case "static":
|
||||
return getStaticCommand(application, logPath);
|
||||
case "dockerfile":
|
||||
return getDockerCommand(application, logPath);
|
||||
}
|
||||
};
|
||||
|
||||
export const mechanizeDockerContainer = async (
|
||||
application: ApplicationNested,
|
||||
) => {
|
||||
const {
|
||||
appName,
|
||||
env,
|
||||
mounts,
|
||||
cpuLimit,
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuReservation,
|
||||
command,
|
||||
ports,
|
||||
} = application;
|
||||
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
|
||||
const {
|
||||
HealthCheck,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Labels,
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
UpdateConfig,
|
||||
Networks,
|
||||
} = generateConfigContainer(application);
|
||||
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, application);
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
|
||||
const image = getImageName(application);
|
||||
const authConfig = getAuthConfig(application);
|
||||
const docker = await getRemoteDocker(application.serverId);
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
authconfig: authConfig,
|
||||
Name: appName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
HealthCheck,
|
||||
Image: image,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(command
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", command],
|
||||
}
|
||||
: {}),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
},
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
EndpointSpec: {
|
||||
Ports: ports.map((port) => ({
|
||||
Protocol: port.protocol,
|
||||
TargetPort: port.targetPort,
|
||||
PublishedPort: port.publishedPort,
|
||||
})),
|
||||
},
|
||||
UpdateConfig,
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const getImageName = (application: ApplicationNested) => {
|
||||
const { appName, sourceType, dockerImage, registry } = application;
|
||||
|
||||
if (sourceType === "docker") {
|
||||
return dockerImage || "ERROR-NO-IMAGE-PROVIDED";
|
||||
}
|
||||
|
||||
const registryUrl = registry?.registryUrl || "";
|
||||
const imagePrefix = registry?.imagePrefix ? `${registry.imagePrefix}/` : "";
|
||||
return registry
|
||||
? `${registryUrl}/${imagePrefix}${appName}`
|
||||
: `${appName}:latest`;
|
||||
};
|
||||
|
||||
const getAuthConfig = (application: ApplicationNested) => {
|
||||
const { registry, username, password, sourceType } = application;
|
||||
|
||||
if (sourceType === "docker") {
|
||||
if (username && password) {
|
||||
return {
|
||||
password,
|
||||
username,
|
||||
serveraddress: "https://index.docker.io/v1/",
|
||||
};
|
||||
}
|
||||
} else if (registry) {
|
||||
return {
|
||||
password: registry.password,
|
||||
username: registry.username,
|
||||
serveraddress: registry.registryUrl,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
@@ -1,124 +0,0 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { buildStatic, getStaticCommand } from "@/server/utils/builders/static";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { ApplicationNested } from ".";
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export const buildNixpacks = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { env, appName, publishDirectory, serverId } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
|
||||
const writeToStream = (data: string) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const args = ["build", buildAppDirectory, "--name", appName];
|
||||
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
if (publishDirectory) {
|
||||
/* No need for any start command, since we'll use nginx later on */
|
||||
args.push("--no-error-without-start");
|
||||
}
|
||||
|
||||
await spawnAsync("nixpacks", args, writeToStream);
|
||||
|
||||
/*
|
||||
Run the container with the image created by nixpacks,
|
||||
and copy the artifacts on the host filesystem.
|
||||
Then, remove the container and create a static build.
|
||||
*/
|
||||
|
||||
if (publishDirectory) {
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
["create", "--name", buildContainerId, appName],
|
||||
writeToStream,
|
||||
);
|
||||
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
[
|
||||
"cp",
|
||||
`${buildContainerId}:/app/${publishDirectory}`,
|
||||
path.join(buildAppDirectory, publishDirectory),
|
||||
],
|
||||
writeToStream,
|
||||
);
|
||||
|
||||
await spawnAsync("docker", ["rm", buildContainerId], writeToStream);
|
||||
|
||||
await buildStatic(application, writeStream);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
await spawnAsync("docker", ["rm", buildContainerId], writeToStream);
|
||||
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getNixpacksCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { env, appName, publishDirectory, serverId } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
|
||||
const args = ["build", buildAppDirectory, "--name", appName];
|
||||
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
if (publishDirectory) {
|
||||
/* No need for any start command, since we'll use nginx later on */
|
||||
args.push("--no-error-without-start");
|
||||
}
|
||||
const command = `nixpacks ${args.join(" ")}`;
|
||||
let bashCommand = `
|
||||
echo "Starting nixpacks build..." >> ${logPath};
|
||||
${command} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Nixpacks build failed" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Nixpacks build completed." >> ${logPath};
|
||||
`;
|
||||
|
||||
/*
|
||||
Run the container with the image created by nixpacks,
|
||||
and copy the artifacts on the host filesystem.
|
||||
Then, remove the container and create a static build.
|
||||
*/
|
||||
if (publishDirectory) {
|
||||
bashCommand += `
|
||||
docker create --name ${buildContainerId} ${appName}
|
||||
docker cp ${buildContainerId}:/app/${publishDirectory} ${path.join(buildAppDirectory, publishDirectory)} >> ${logPath} 2>> ${logPath} || {
|
||||
docker rm ${buildContainerId}
|
||||
echo "❌ Copying ${publishDirectory} to ${path.join(buildAppDirectory, publishDirectory)} failed" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
docker rm ${buildContainerId}
|
||||
${getStaticCommand(application, logPath)}
|
||||
`;
|
||||
}
|
||||
|
||||
return bashCommand;
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import type { ApplicationNested } from ".";
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
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 of envVariables) {
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
await spawnAsync("pack", args, (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getPaketoCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { env, appName } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
|
||||
const args = [
|
||||
"build",
|
||||
appName,
|
||||
"--path",
|
||||
buildAppDirectory,
|
||||
"--builder",
|
||||
"paketobuildpacks/builder-jammy-full",
|
||||
];
|
||||
|
||||
for (const env of envVariables) {
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
const command = `pack ${args.join(" ")}`;
|
||||
const bashCommand = `
|
||||
echo "Starting Paketo build..." >> ${logPath};
|
||||
${command} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Paketo build failed" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Paketo build completed." >> ${logPath};
|
||||
`;
|
||||
|
||||
return bashCommand;
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import {
|
||||
buildCustomDocker,
|
||||
getDockerCommand,
|
||||
} from "@/server/utils/builders/docker-file";
|
||||
import type { ApplicationNested } from ".";
|
||||
import { createFile, getCreateFileCommand } from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
|
||||
export const buildStatic = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { publishDirectory } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
|
||||
try {
|
||||
createFile(
|
||||
buildAppDirectory,
|
||||
"Dockerfile",
|
||||
[
|
||||
"FROM nginx:alpine",
|
||||
"WORKDIR /usr/share/nginx/html/",
|
||||
`COPY ${publishDirectory || "."} .`,
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
await buildCustomDocker(
|
||||
{
|
||||
...application,
|
||||
buildType: "dockerfile",
|
||||
dockerfile: "Dockerfile",
|
||||
},
|
||||
writeStream,
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getStaticCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { publishDirectory } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
|
||||
let command = getCreateFileCommand(
|
||||
buildAppDirectory,
|
||||
"Dockerfile",
|
||||
[
|
||||
"FROM nginx:alpine",
|
||||
"WORKDIR /usr/share/nginx/html/",
|
||||
`COPY ${publishDirectory || "."} .`,
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
command += getDockerCommand(
|
||||
{
|
||||
...application,
|
||||
buildType: "dockerfile",
|
||||
dockerfile: "Dockerfile",
|
||||
},
|
||||
logPath,
|
||||
);
|
||||
return command;
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
|
||||
export const createEnvFile = (directory: string, env: string | null) => {
|
||||
const envFilePath = join(dirname(directory), ".env");
|
||||
if (!existsSync(dirname(envFilePath))) {
|
||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
||||
}
|
||||
const envFileContent = prepareEnvironmentVariables(env).join("\n");
|
||||
writeFileSync(envFilePath, envFileContent);
|
||||
};
|
||||
|
||||
export const createEnvFileCommand = (directory: string, env: string | null) => {
|
||||
const envFilePath = join(dirname(directory), ".env");
|
||||
if (!existsSync(dirname(envFilePath))) {
|
||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
||||
}
|
||||
const envFileContent = prepareEnvironmentVariables(env).join("\n");
|
||||
return `echo "${envFileContent}" > ${envFilePath}`;
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export const uploadImage = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const registry = application.registry;
|
||||
|
||||
if (!registry) {
|
||||
throw new Error("Registry not found");
|
||||
}
|
||||
|
||||
const { registryUrl, imagePrefix, registryType } = registry;
|
||||
const { appName } = application;
|
||||
const imageName = `${appName}:latest`;
|
||||
|
||||
const finalURL =
|
||||
registryType === "selfHosted"
|
||||
? process.env.NODE_ENV === "development"
|
||||
? "localhost:5000"
|
||||
: registryUrl
|
||||
: registryUrl;
|
||||
|
||||
const registryTag = imagePrefix
|
||||
? `${finalURL}/${imagePrefix}/${imageName}`
|
||||
: `${finalURL}/${imageName}`;
|
||||
|
||||
try {
|
||||
console.log(finalURL, registryTag);
|
||||
writeStream.write(
|
||||
`📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${registryTag} | ${finalURL}\n`,
|
||||
);
|
||||
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
["login", finalURL, "-u", registry.username, "-p", registry.password],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await spawnAsync("docker", ["tag", imageName, registryTag], (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
|
||||
await spawnAsync("docker", ["push", registryTag], (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
// docker:
|
||||
// endpoint: "unix:///var/run/docker.sock"
|
||||
// exposedByDefault: false
|
||||
// swarmMode: true
|
||||
@@ -1,98 +0,0 @@
|
||||
import type { InferResultType } from "@/server/types/with";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
generateFileMounts,
|
||||
generateVolumeMounts,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
export type MariadbNested = InferResultType<"mariadb", { mounts: true }>;
|
||||
export const buildMariadb = async (mariadb: MariadbNested) => {
|
||||
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, mariadb);
|
||||
|
||||
const docker = await getRemoteDocker(mariadb.serverId);
|
||||
|
||||
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,
|
||||
},
|
||||
Placement: {
|
||||
Constraints: ["node.role==manager"],
|
||||
},
|
||||
},
|
||||
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);
|
||||
}
|
||||
};
|
||||
@@ -1,97 +0,0 @@
|
||||
import type { InferResultType } from "@/server/types/with";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
generateFileMounts,
|
||||
generateVolumeMounts,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
export type MongoNested = InferResultType<"mongo", { mounts: true }>;
|
||||
|
||||
export const buildMongo = async (mongo: MongoNested) => {
|
||||
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, mongo);
|
||||
|
||||
const docker = await getRemoteDocker(mongo.serverId);
|
||||
|
||||
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,
|
||||
},
|
||||
Placement: {
|
||||
Constraints: ["node.role==manager"],
|
||||
},
|
||||
},
|
||||
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);
|
||||
}
|
||||
};
|
||||
@@ -1,104 +0,0 @@
|
||||
import type { InferResultType } from "@/server/types/with";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
generateFileMounts,
|
||||
generateVolumeMounts,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
export type MysqlNested = InferResultType<"mysql", { mounts: true }>;
|
||||
|
||||
export const buildMysql = async (mysql: MysqlNested) => {
|
||||
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, mysql);
|
||||
|
||||
const docker = await getRemoteDocker(mysql.serverId);
|
||||
|
||||
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,
|
||||
},
|
||||
Placement: {
|
||||
Constraints: ["node.role==manager"],
|
||||
},
|
||||
},
|
||||
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);
|
||||
}
|
||||
};
|
||||
@@ -1,98 +0,0 @@
|
||||
import type { InferResultType } from "@/server/types/with";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
generateFileMounts,
|
||||
generateVolumeMounts,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
export type PostgresNested = InferResultType<"postgres", { mounts: true }>;
|
||||
export const buildPostgres = async (postgres: PostgresNested) => {
|
||||
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, postgres);
|
||||
|
||||
const docker = await getRemoteDocker(postgres.serverId);
|
||||
|
||||
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,
|
||||
},
|
||||
Placement: {
|
||||
Constraints: ["node.role==manager"],
|
||||
},
|
||||
},
|
||||
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);
|
||||
}
|
||||
};
|
||||
@@ -1,95 +0,0 @@
|
||||
import type { InferResultType } from "@/server/types/with";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
generateFileMounts,
|
||||
generateVolumeMounts,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
export type RedisNested = InferResultType<"redis", { mounts: true }>;
|
||||
export const buildRedis = async (redis: RedisNested) => {
|
||||
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, redis);
|
||||
|
||||
const docker = await getRemoteDocker(redis.serverId);
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
Name: appName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
Image: dockerImage,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
Command: ["/bin/sh"],
|
||||
Args: [
|
||||
"-c",
|
||||
command ? command : `redis-server --requirepass ${databasePassword}`,
|
||||
],
|
||||
},
|
||||
Networks: [{ Target: "dokploy-network" }],
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
Placement: {
|
||||
Constraints: ["node.role==manager"],
|
||||
},
|
||||
},
|
||||
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);
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { exec } from "node:child_process";
|
||||
import util from "node:util";
|
||||
import { findServerById } from "@/server/api/services/server";
|
||||
import { findServerById } from "@dokploy/builders";
|
||||
import { Client } from "ssh2";
|
||||
import { readSSHKey } from "../filesystem/ssh";
|
||||
export const execAsync = util.promisify(exec);
|
||||
|
||||
@@ -1,361 +0,0 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { findBitbucketById } from "@/server/api/services/bitbucket";
|
||||
import type { Compose } from "@/server/api/services/compose";
|
||||
import { paths } from "@/server/constants";
|
||||
import type {
|
||||
apiBitbucketTestConnection,
|
||||
apiFindBitbucketBranches,
|
||||
} from "@/server/db/schema";
|
||||
import type { InferResultType } from "@/server/types/with";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export type ApplicationWithBitbucket = InferResultType<
|
||||
"applications",
|
||||
{ bitbucket: true }
|
||||
>;
|
||||
|
||||
export type ComposeWithBitbucket = InferResultType<
|
||||
"compose",
|
||||
{ bitbucket: true }
|
||||
>;
|
||||
|
||||
export const cloneBitbucketRepository = async (
|
||||
entity: ApplicationWithBitbucket | ComposeWithBitbucket,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths();
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const {
|
||||
appName,
|
||||
bitbucketRepository,
|
||||
bitbucketOwner,
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
bitbucket,
|
||||
} = entity;
|
||||
|
||||
if (!bitbucketId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Bitbucket Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const cloneUrl = `https://${bitbucket?.bitbucketUsername}:${bitbucket?.appPassword}@${repoclone}`;
|
||||
try {
|
||||
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
bitbucketBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawBitbucketRepository = async (entity: Compose) => {
|
||||
const { COMPOSE_PATH } = paths();
|
||||
const {
|
||||
appName,
|
||||
bitbucketRepository,
|
||||
bitbucketOwner,
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
} = entity;
|
||||
|
||||
if (!bitbucketId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Bitbucket Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const bitbucketProvider = await findBitbucketById(bitbucketId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
||||
|
||||
try {
|
||||
await spawnAsync("git", [
|
||||
"clone",
|
||||
"--branch",
|
||||
bitbucketBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
]);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
const {
|
||||
appName,
|
||||
bitbucketRepository,
|
||||
bitbucketOwner,
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
serverId,
|
||||
} = compose;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
if (!bitbucketId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Bitbucket Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const bitbucketProvider = await findBitbucketById(bitbucketId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
||||
|
||||
try {
|
||||
await execAsyncRemote(
|
||||
serverId,
|
||||
`git clone --branch ${bitbucketBranch} --depth 1 ${cloneUrl} ${outputPath}`,
|
||||
);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getBitbucketCloneCommand = async (
|
||||
entity: ApplicationWithBitbucket | ComposeWithBitbucket,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
||||
const {
|
||||
appName,
|
||||
bitbucketRepository,
|
||||
bitbucketOwner,
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
serverId,
|
||||
bitbucket,
|
||||
} = entity;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!bitbucketId) {
|
||||
const command = `
|
||||
echo "Error: ❌ Bitbucket Provider not found" >> ${logPath};
|
||||
exit 1;
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Bitbucket Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const bitbucketProvider = await findBitbucketById(bitbucketId);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
||||
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
if ! git clone --branch ${bitbucketBranch} --depth 1 --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
|
||||
`;
|
||||
|
||||
return cloneCommand;
|
||||
};
|
||||
|
||||
export const getBitbucketRepositories = async (bitbucketId?: string) => {
|
||||
if (!bitbucketId) {
|
||||
return [];
|
||||
}
|
||||
const bitbucketProvider = await findBitbucketById(bitbucketId);
|
||||
|
||||
const username =
|
||||
bitbucketProvider.bitbucketWorkspaceName ||
|
||||
bitbucketProvider.bitbucketUsername;
|
||||
const url = `https://api.bitbucket.org/2.0/repositories/${username}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const mappedData = data.values.map((repo: any) => {
|
||||
return {
|
||||
name: repo.name,
|
||||
url: repo.links.html.href,
|
||||
owner: {
|
||||
username: repo.workspace.slug,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return mappedData as {
|
||||
name: string;
|
||||
url: string;
|
||||
owner: {
|
||||
username: string;
|
||||
};
|
||||
}[];
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getBitbucketBranches = async (
|
||||
input: typeof apiFindBitbucketBranches._type,
|
||||
) => {
|
||||
if (!input.bitbucketId) {
|
||||
return [];
|
||||
}
|
||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||
const { owner, repo } = input;
|
||||
const url = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `HTTP error! status: ${response.status}`,
|
||||
});
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const mappedData = data.values.map((branch: any) => {
|
||||
return {
|
||||
name: branch.name,
|
||||
commit: {
|
||||
sha: branch.target.hash,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return mappedData as {
|
||||
name: string;
|
||||
commit: {
|
||||
sha: string;
|
||||
};
|
||||
}[];
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const testBitbucketConnection = async (
|
||||
input: typeof apiBitbucketTestConnection._type,
|
||||
) => {
|
||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||
|
||||
if (!bitbucketProvider) {
|
||||
throw new Error("Bitbucket provider not found");
|
||||
}
|
||||
|
||||
const { bitbucketUsername, workspaceName } = input;
|
||||
|
||||
const username = workspaceName || bitbucketUsername;
|
||||
|
||||
const url = `https://api.bitbucket.org/2.0/repositories/${username}`;
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const mappedData = data.values.map((repo: any) => {
|
||||
return {
|
||||
name: repo.name,
|
||||
url: repo.links.html.href,
|
||||
owner: {
|
||||
username: repo.workspace.slug,
|
||||
},
|
||||
};
|
||||
}) as [];
|
||||
|
||||
return mappedData.length;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
export const buildRemoteDocker = async (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { sourceType, dockerImage, username, password } = application;
|
||||
|
||||
try {
|
||||
if (!dockerImage) {
|
||||
throw new Error("Docker image not found");
|
||||
}
|
||||
let command = `
|
||||
echo "Pulling ${dockerImage}" >> ${logPath};
|
||||
`;
|
||||
|
||||
if (username && password) {
|
||||
command += `
|
||||
if ! docker login --username ${username} --password ${password} https://index.docker.io/v1/ >> ${logPath} 2>&1; then
|
||||
echo "❌ Login failed" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
`;
|
||||
}
|
||||
|
||||
command += `
|
||||
docker pull ${dockerImage} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Pulling image failed" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
|
||||
echo "✅ Pulling image completed." >> ${logPath};
|
||||
`;
|
||||
return command;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -1,347 +0,0 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import path, { join } from "node:path";
|
||||
import type { Compose } from "@/server/api/services/compose";
|
||||
import { updateSSHKeyById } from "@/server/api/services/ssh-key";
|
||||
import { paths } from "@/server/constants";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export const cloneGitRepository = async (
|
||||
entity: {
|
||||
appName: string;
|
||||
customGitUrl?: string | null;
|
||||
customGitBranch?: string | null;
|
||||
customGitSSHKeyId?: string | null;
|
||||
},
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths();
|
||||
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity;
|
||||
|
||||
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, `${customGitSSHKeyId}_rsa`);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
|
||||
try {
|
||||
if (!isHttpOrHttps(customGitUrl)) {
|
||||
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`,
|
||||
);
|
||||
|
||||
if (customGitSSHKeyId) {
|
||||
await updateSSHKeyById({
|
||||
sshKeyId: customGitSSHKeyId,
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
customGitBranch,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
customGitUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
...(customGitSSHKeyId && {
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
export const getCustomGitCloneCommand = async (
|
||||
entity: {
|
||||
appName: string;
|
||||
customGitUrl?: string | null;
|
||||
customGitBranch?: string | null;
|
||||
customGitSSHKeyId?: string | null;
|
||||
serverId: string | null;
|
||||
},
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
||||
const {
|
||||
appName,
|
||||
customGitUrl,
|
||||
customGitBranch,
|
||||
customGitSSHKeyId,
|
||||
serverId,
|
||||
} = entity;
|
||||
|
||||
if (!customGitUrl || !customGitBranch) {
|
||||
const command = `
|
||||
echo "Error: ❌ Repository not found" >> ${logPath};
|
||||
exit 1;
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error: Repository not found",
|
||||
});
|
||||
}
|
||||
|
||||
const keyPath = path.join(SSH_PATH, `${customGitSSHKeyId}_rsa`);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
|
||||
if (customGitSSHKeyId) {
|
||||
await updateSSHKeyById({
|
||||
sshKeyId: customGitSSHKeyId,
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
try {
|
||||
const command = [];
|
||||
if (!isHttpOrHttps(customGitUrl)) {
|
||||
command.push(addHostToKnownHostsCommand(customGitUrl));
|
||||
}
|
||||
command.push(`rm -rf ${outputPath};`);
|
||||
command.push(`mkdir -p ${outputPath};`);
|
||||
command.push(
|
||||
`echo "Cloning Custom Git ${customGitUrl}" to ${outputPath}: ✅ >> ${logPath};`,
|
||||
);
|
||||
if (customGitSSHKeyId) {
|
||||
command.push(
|
||||
`GIT_SSH_COMMAND="ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}"`,
|
||||
);
|
||||
}
|
||||
|
||||
command.push(
|
||||
`if ! git clone --branch ${customGitBranch} --depth 1 --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
`,
|
||||
);
|
||||
command.push(`echo "Cloned Custom Git ${customGitUrl}: ✅" >> ${logPath};`);
|
||||
return command.join("\n");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const isHttpOrHttps = (url: string): boolean => {
|
||||
const regex = /^https?:\/\//;
|
||||
return regex.test(url);
|
||||
};
|
||||
|
||||
const addHostToKnownHosts = async (repositoryURL: string) => {
|
||||
const { SSH_PATH } = paths();
|
||||
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 addHostToKnownHostsCommand = (repositoryURL: string) => {
|
||||
const { SSH_PATH } = paths();
|
||||
const { domain, port } = sanitizeRepoPathSSH(repositoryURL);
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
|
||||
return `ssh-keyscan -p ${port} ${domain} >> ${knownHostsPath};`;
|
||||
};
|
||||
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`;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const cloneGitRawRepository = async (entity: {
|
||||
appName: string;
|
||||
customGitUrl?: string | null;
|
||||
customGitBranch?: string | null;
|
||||
customGitSSHKeyId?: string | null;
|
||||
}) => {
|
||||
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity;
|
||||
|
||||
if (!customGitUrl || !customGitBranch) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error: Repository not found",
|
||||
});
|
||||
}
|
||||
|
||||
const { SSH_PATH, COMPOSE_PATH } = paths();
|
||||
const keyPath = path.join(SSH_PATH, `${customGitSSHKeyId}_rsa`);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
|
||||
try {
|
||||
await addHostToKnownHosts(customGitUrl);
|
||||
await recreateDirectory(outputPath);
|
||||
|
||||
if (customGitSSHKeyId) {
|
||||
await updateSSHKeyById({
|
||||
sshKeyId: customGitSSHKeyId,
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
customGitBranch,
|
||||
"--depth",
|
||||
"1",
|
||||
customGitUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(data) => {},
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
...(customGitSSHKeyId && {
|
||||
GIT_SSH_COMMAND: `ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
}),
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
|
||||
const {
|
||||
appName,
|
||||
customGitBranch,
|
||||
customGitUrl,
|
||||
customGitSSHKeyId,
|
||||
serverId,
|
||||
} = compose;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
if (!customGitUrl) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Git Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const { SSH_PATH, COMPOSE_PATH } = paths(true);
|
||||
const keyPath = path.join(SSH_PATH, `${customGitSSHKeyId}_rsa`);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
|
||||
if (customGitSSHKeyId) {
|
||||
await updateSSHKeyById({
|
||||
sshKeyId: customGitSSHKeyId,
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
try {
|
||||
const command = [];
|
||||
if (!isHttpOrHttps(customGitUrl)) {
|
||||
command.push(addHostToKnownHostsCommand(customGitUrl));
|
||||
}
|
||||
command.push(`rm -rf ${outputPath};`);
|
||||
command.push(`mkdir -p ${outputPath};`);
|
||||
if (customGitSSHKeyId) {
|
||||
command.push(
|
||||
`GIT_SSH_COMMAND="ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}"`,
|
||||
);
|
||||
}
|
||||
|
||||
command.push(
|
||||
`if ! git clone --branch ${customGitBranch} --depth 1 --progress ${customGitUrl} ${outputPath} ; then
|
||||
echo "[ERROR] Fail to clone the repository ";
|
||||
exit 1;
|
||||
fi
|
||||
`,
|
||||
);
|
||||
|
||||
await execAsyncRemote(serverId, command.join("\n"));
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -1,336 +0,0 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@/server/constants";
|
||||
import type { InferResultType } from "@/server/types/with";
|
||||
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 { Compose } from "@/server/api/services/compose";
|
||||
import { type Github, findGithubById } from "@/server/api/services/github";
|
||||
import type { apiFindGithubBranches } from "@/server/db/schema";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
|
||||
export const authGithub = (githubProvider: Github) => {
|
||||
if (!haveGithubRequirements(githubProvider)) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Github Account not configured correctly",
|
||||
});
|
||||
}
|
||||
|
||||
const octokit = new Octokit({
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId: githubProvider?.githubAppId || 0,
|
||||
privateKey: githubProvider?.githubPrivateKey || "",
|
||||
installationId: githubProvider?.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 = (githubProvider: Github) => {
|
||||
return !!(
|
||||
githubProvider?.githubAppId &&
|
||||
githubProvider?.githubPrivateKey &&
|
||||
githubProvider?.githubInstallationId
|
||||
);
|
||||
};
|
||||
|
||||
const getErrorCloneRequirements = (entity: {
|
||||
repository?: string | null;
|
||||
owner?: string | null;
|
||||
branch?: string | null;
|
||||
}) => {
|
||||
const reasons: string[] = [];
|
||||
const { repository, owner, branch } = entity;
|
||||
|
||||
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 type ApplicationWithGithub = InferResultType<
|
||||
"applications",
|
||||
{ github: true }
|
||||
>;
|
||||
|
||||
export type ComposeWithGithub = InferResultType<"compose", { github: true }>;
|
||||
export const cloneGithubRepository = async (
|
||||
entity: ApplicationWithGithub | ComposeWithGithub,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const { appName, repository, owner, branch, githubId } = entity;
|
||||
|
||||
if (!githubId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "GitHub Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const requirements = getErrorCloneRequirements(entity);
|
||||
|
||||
// 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 githubProvider = await findGithubById(githubId);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const octokit = authGithub(githubProvider);
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
export const getGithubCloneCommand = async (
|
||||
entity: ApplicationWithGithub | ComposeWithGithub,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { appName, repository, owner, branch, githubId, serverId } = entity;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!githubId) {
|
||||
const command = `
|
||||
echo "Error: ❌ Github Provider not found" >> ${logPath};
|
||||
exit 1;
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "GitHub Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const requirements = getErrorCloneRequirements(entity);
|
||||
|
||||
// Build log messages
|
||||
let logMessages = "";
|
||||
if (requirements.length > 0) {
|
||||
logMessages += `\nGitHub Repository configuration failed for application: ${appName}\n`;
|
||||
logMessages += "Reasons:\n";
|
||||
logMessages += requirements.join("\n");
|
||||
const escapedLogMessages = logMessages
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, "\\n");
|
||||
|
||||
const bashCommand = `
|
||||
echo "${escapedLogMessages}" >> ${logPath};
|
||||
exit 1; # Exit with error code
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, bashCommand);
|
||||
return;
|
||||
}
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
||||
const githubProvider = await findGithubById(githubId);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const octokit = authGithub(githubProvider);
|
||||
const token = await getGithubToken(octokit);
|
||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
if ! git clone --branch ${branch} --depth 1 --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fallo al clonar el repositorio ${repoclone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
|
||||
`;
|
||||
|
||||
return cloneCommand;
|
||||
};
|
||||
|
||||
export const cloneRawGithubRepository = async (entity: Compose) => {
|
||||
const { appName, repository, owner, branch, githubId } = entity;
|
||||
|
||||
if (!githubId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "GitHub Provider not found",
|
||||
});
|
||||
}
|
||||
const { COMPOSE_PATH } = paths();
|
||||
const githubProvider = await findGithubById(githubId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const octokit = authGithub(githubProvider);
|
||||
const token = await getGithubToken(octokit);
|
||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||
await recreateDirectory(outputPath);
|
||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||
try {
|
||||
await spawnAsync("git", [
|
||||
"clone",
|
||||
"--branch",
|
||||
branch!,
|
||||
"--depth",
|
||||
"1",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
]);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
|
||||
const { appName, repository, owner, branch, githubId, serverId } = compose;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
if (!githubId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "GitHub Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
const githubProvider = await findGithubById(githubId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const octokit = authGithub(githubProvider);
|
||||
const token = await getGithubToken(octokit);
|
||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||
await recreateDirectory(outputPath);
|
||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||
try {
|
||||
await execAsyncRemote(
|
||||
serverId,
|
||||
`git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}`,
|
||||
);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getGithubRepositories = async (githubId?: string) => {
|
||||
if (!githubId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const githubProvider = await findGithubById(githubId);
|
||||
|
||||
const octokit = new Octokit({
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId: githubProvider.githubAppId,
|
||||
privateKey: githubProvider.githubPrivateKey,
|
||||
installationId: githubProvider.githubInstallationId,
|
||||
},
|
||||
});
|
||||
|
||||
const repositories = (await octokit.paginate(
|
||||
octokit.rest.apps.listReposAccessibleToInstallation,
|
||||
)) as unknown as Awaited<
|
||||
ReturnType<typeof octokit.rest.apps.listReposAccessibleToInstallation>
|
||||
>["data"]["repositories"];
|
||||
|
||||
return repositories;
|
||||
};
|
||||
|
||||
export const getGithubBranches = async (
|
||||
input: typeof apiFindGithubBranches._type,
|
||||
) => {
|
||||
if (!input.githubId) {
|
||||
return [];
|
||||
}
|
||||
const githubProvider = await findGithubById(input.githubId);
|
||||
|
||||
const octokit = new Octokit({
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId: githubProvider.githubAppId,
|
||||
privateKey: githubProvider.githubPrivateKey,
|
||||
installationId: githubProvider.githubInstallationId,
|
||||
},
|
||||
});
|
||||
|
||||
const branches = (await octokit.paginate(octokit.rest.repos.listBranches, {
|
||||
owner: input.owner,
|
||||
repo: input.repo,
|
||||
})) as unknown as Awaited<
|
||||
ReturnType<typeof octokit.rest.repos.listBranches>
|
||||
>["data"];
|
||||
|
||||
return branches;
|
||||
};
|
||||
@@ -1,447 +0,0 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { Compose } from "@/server/api/services/compose";
|
||||
import {
|
||||
type Gitlab,
|
||||
findGitlabById,
|
||||
updateGitlab,
|
||||
} from "@/server/api/services/gitlab";
|
||||
import { paths } from "@/server/constants";
|
||||
import type { apiGitlabTestConnection } from "@/server/db/schema";
|
||||
import type { InferResultType } from "@/server/types/with";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export const refreshGitlabToken = async (gitlabProviderId: string) => {
|
||||
const gitlabProvider = await findGitlabById(gitlabProviderId);
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
|
||||
const safetyMargin = 60;
|
||||
if (
|
||||
gitlabProvider.expiresAt &&
|
||||
currentTime + safetyMargin < gitlabProvider.expiresAt
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("https://gitlab.com/oauth/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: gitlabProvider.refreshToken as string,
|
||||
client_id: gitlabProvider.applicationId as string,
|
||||
client_secret: gitlabProvider.secret as string,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to refresh token: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const expiresAt = Math.floor(Date.now() / 1000) + data.expires_in;
|
||||
|
||||
console.log("Refreshed token");
|
||||
|
||||
await updateGitlab(gitlabProviderId, {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
expiresAt,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
export const haveGitlabRequirements = (gitlabProvider: Gitlab) => {
|
||||
return !!(gitlabProvider?.accessToken && gitlabProvider?.refreshToken);
|
||||
};
|
||||
|
||||
const getErrorCloneRequirements = (entity: {
|
||||
gitlabRepository?: string | null;
|
||||
gitlabOwner?: string | null;
|
||||
gitlabBranch?: string | null;
|
||||
gitlabPathNamespace?: string | null;
|
||||
}) => {
|
||||
const reasons: string[] = [];
|
||||
const { gitlabBranch, gitlabOwner, gitlabRepository, gitlabPathNamespace } =
|
||||
entity;
|
||||
|
||||
if (!gitlabRepository) reasons.push("1. Repository not assigned.");
|
||||
if (!gitlabOwner) reasons.push("2. Owner not specified.");
|
||||
if (!gitlabBranch) reasons.push("3. Branch not defined.");
|
||||
if (!gitlabPathNamespace) reasons.push("4. Path namespace not defined.");
|
||||
|
||||
return reasons;
|
||||
};
|
||||
|
||||
export type ApplicationWithGitlab = InferResultType<
|
||||
"applications",
|
||||
{ gitlab: true }
|
||||
>;
|
||||
|
||||
export type ComposeWithGitlab = InferResultType<"compose", { gitlab: true }>;
|
||||
|
||||
export const cloneGitlabRepository = async (
|
||||
entity: ApplicationWithGitlab | ComposeWithGitlab,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const { appName, gitlabBranch, gitlabId, gitlab, gitlabPathNamespace } =
|
||||
entity;
|
||||
|
||||
if (!gitlabId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitlab Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
await refreshGitlabToken(gitlabId);
|
||||
|
||||
const requirements = getErrorCloneRequirements(entity);
|
||||
|
||||
// Check if requirements are met
|
||||
if (requirements.length > 0) {
|
||||
writeStream.write(
|
||||
`\nGitLab 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: GitLab repository information is incomplete.",
|
||||
});
|
||||
}
|
||||
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths();
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlab?.accessToken}@${repoclone}`;
|
||||
|
||||
try {
|
||||
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
gitlabBranch!,
|
||||
"--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();
|
||||
}
|
||||
};
|
||||
|
||||
export const getGitlabCloneCommand = async (
|
||||
entity: ApplicationWithGitlab | ComposeWithGitlab,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const {
|
||||
appName,
|
||||
gitlabRepository,
|
||||
gitlabOwner,
|
||||
gitlabPathNamespace,
|
||||
gitlabBranch,
|
||||
gitlabId,
|
||||
serverId,
|
||||
gitlab,
|
||||
} = entity;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!gitlabId) {
|
||||
const command = `
|
||||
echo "Error: ❌ Gitlab Provider not found" >> ${logPath};
|
||||
exit 1;
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitlab Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const requirements = getErrorCloneRequirements(entity);
|
||||
|
||||
// Build log messages
|
||||
let logMessages = "";
|
||||
if (requirements.length > 0) {
|
||||
logMessages += `\nGitLab Repository configuration failed for application: ${appName}\n`;
|
||||
logMessages += "Reasons:\n";
|
||||
logMessages += requirements.join("\n");
|
||||
const escapedLogMessages = logMessages
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, "\\n");
|
||||
|
||||
const bashCommand = `
|
||||
echo "${escapedLogMessages}" >> ${logPath};
|
||||
exit 1; # Exit with error code
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, bashCommand);
|
||||
return;
|
||||
}
|
||||
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
||||
await refreshGitlabToken(gitlabId);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlab?.accessToken}@${repoclone}`;
|
||||
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
if ! git clone --branch ${gitlabBranch} --depth 1 --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
|
||||
`;
|
||||
|
||||
return cloneCommand;
|
||||
};
|
||||
|
||||
export const getGitlabRepositories = async (gitlabId?: string) => {
|
||||
if (!gitlabId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await refreshGitlabToken(gitlabId);
|
||||
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
|
||||
const response = await fetch(
|
||||
`https://gitlab.com/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const repositories = await response.json();
|
||||
|
||||
const filteredRepos = repositories.filter((repo: any) => {
|
||||
const { full_path, kind } = repo.namespace;
|
||||
const groupName = gitlabProvider.groupName?.toLowerCase();
|
||||
|
||||
if (groupName) {
|
||||
return full_path.toLowerCase().includes(groupName) && kind === "group";
|
||||
}
|
||||
return kind === "user";
|
||||
});
|
||||
const mappedRepositories = filteredRepos.map((repo: any) => {
|
||||
return {
|
||||
id: repo.id,
|
||||
name: repo.name,
|
||||
url: repo.path_with_namespace,
|
||||
owner: {
|
||||
username: repo.namespace.path,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return mappedRepositories as {
|
||||
id: number;
|
||||
name: string;
|
||||
url: string;
|
||||
owner: {
|
||||
username: string;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
export const getGitlabBranches = async (input: {
|
||||
id?: number;
|
||||
gitlabId?: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
}) => {
|
||||
if (!input.gitlabId || !input.id || input.id === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const gitlabProvider = await findGitlabById(input.gitlabId);
|
||||
|
||||
const branchesResponse = await fetch(
|
||||
`https://gitlab.com/api/v4/projects/${input.id}/repository/branches`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!branchesResponse.ok) {
|
||||
throw new Error(`Failed to fetch branches: ${branchesResponse.statusText}`);
|
||||
}
|
||||
|
||||
const branches = await branchesResponse.json();
|
||||
|
||||
return branches as {
|
||||
id: string;
|
||||
name: string;
|
||||
commit: {
|
||||
id: string;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
export const cloneRawGitlabRepository = async (entity: Compose) => {
|
||||
const {
|
||||
appName,
|
||||
gitlabRepository,
|
||||
gitlabOwner,
|
||||
gitlabBranch,
|
||||
gitlabId,
|
||||
gitlabPathNamespace,
|
||||
} = entity;
|
||||
|
||||
if (!gitlabId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitlab Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
const { COMPOSE_PATH } = paths();
|
||||
await refreshGitlabToken(gitlabId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
|
||||
|
||||
try {
|
||||
await spawnAsync("git", [
|
||||
"clone",
|
||||
"--branch",
|
||||
gitlabBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
]);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
|
||||
const { appName, gitlabPathNamespace, branch, gitlabId, serverId } = compose;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
if (!gitlabId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitlab Provider not found",
|
||||
});
|
||||
}
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
await refreshGitlabToken(gitlabId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
|
||||
try {
|
||||
await execAsyncRemote(
|
||||
serverId,
|
||||
`git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}`,
|
||||
);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const testGitlabConnection = async (
|
||||
input: typeof apiGitlabTestConnection._type,
|
||||
) => {
|
||||
const { gitlabId, groupName } = input;
|
||||
|
||||
if (!gitlabId) {
|
||||
throw new Error("Gitlab provider not found");
|
||||
}
|
||||
|
||||
await refreshGitlabToken(gitlabId);
|
||||
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
|
||||
const response = await fetch(
|
||||
`https://gitlab.com/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const repositories = await response.json();
|
||||
|
||||
const filteredRepos = repositories.filter((repo: any) => {
|
||||
const { full_path, kind } = repo.namespace;
|
||||
|
||||
if (groupName) {
|
||||
return full_path.toLowerCase().includes(groupName) && kind === "group";
|
||||
}
|
||||
return kind === "user";
|
||||
});
|
||||
|
||||
return filteredRepos.length;
|
||||
};
|
||||
@@ -1,80 +0,0 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { Compose } from "@/server/api/services/compose";
|
||||
import { paths } from "@/server/constants";
|
||||
import { encodeBase64 } from "../docker/utils";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
|
||||
export const createComposeFile = async (compose: Compose, logPath: string) => {
|
||||
const { COMPOSE_PATH } = paths();
|
||||
const { appName, composeFile } = compose;
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
||||
|
||||
try {
|
||||
await recreateDirectory(outputPath);
|
||||
writeStream.write(
|
||||
`\nCreating File 'docker-compose.yml' to ${outputPath}: ✅\n`,
|
||||
);
|
||||
|
||||
await writeFile(join(outputPath, "docker-compose.yml"), composeFile);
|
||||
|
||||
writeStream.write(`\nFile 'docker-compose.yml' created: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`\nERROR Creating Compose File: ${error}: ❌\n`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const getCreateComposeFileCommand = (
|
||||
compose: Compose,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
const { appName, composeFile } = compose;
|
||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
||||
const filePath = join(outputPath, "docker-compose.yml");
|
||||
const encodedContent = encodeBase64(composeFile);
|
||||
const bashCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
echo "${encodedContent}" | base64 -d > "${filePath}";
|
||||
echo "File 'docker-compose.yml' created: ✅" >> ${logPath};
|
||||
`;
|
||||
return bashCommand;
|
||||
};
|
||||
|
||||
export const createComposeFileRaw = async (compose: Compose) => {
|
||||
const { COMPOSE_PATH } = paths();
|
||||
const { appName, composeFile } = compose;
|
||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
||||
const filePath = join(outputPath, "docker-compose.yml");
|
||||
try {
|
||||
await recreateDirectory(outputPath);
|
||||
await writeFile(filePath, composeFile);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createComposeFileRawRemote = async (compose: Compose) => {
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
const { appName, composeFile, serverId } = compose;
|
||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
||||
const filePath = join(outputPath, "docker-compose.yml");
|
||||
|
||||
try {
|
||||
const encodedContent = encodeBase64(composeFile);
|
||||
const command = `
|
||||
mkdir -p ${outputPath};
|
||||
echo "${encodedContent}" | base64 -d > "${filePath}";
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { findServerById } from "@/server/api/services/server";
|
||||
import { findServerById } from "@dokploy/builders";
|
||||
import { docker } from "@/server/constants";
|
||||
import Dockerode from "dockerode";
|
||||
import { readSSHKey } from "../filesystem/ssh";
|
||||
|
||||
Reference in New Issue
Block a user