mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: add application and databases external servers
This commit is contained in:
@@ -7,7 +7,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { AlertTriangle, Package } from "lucide-react";
|
import { Package } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { AddVolumes } from "./add-volumes";
|
import { AddVolumes } from "./add-volumes";
|
||||||
import { DeleteVolume } from "./delete-volume";
|
import { DeleteVolume } from "./delete-volume";
|
||||||
|
|||||||
@@ -23,11 +23,7 @@ import {
|
|||||||
type DeploymentJob,
|
type DeploymentJob,
|
||||||
cleanQueuesByApplication,
|
cleanQueuesByApplication,
|
||||||
} from "@/server/queues/deployments-queue";
|
} from "@/server/queues/deployments-queue";
|
||||||
import {
|
import { enqueueDeploymentJob, myQueue } from "@/server/queues/queueSetup";
|
||||||
enqueueDeploymentJob,
|
|
||||||
myQueue,
|
|
||||||
redisConfig,
|
|
||||||
} from "@/server/queues/queueSetup";
|
|
||||||
import {
|
import {
|
||||||
removeService,
|
removeService,
|
||||||
startService,
|
startService,
|
||||||
@@ -39,7 +35,7 @@ import {
|
|||||||
} from "@/server/utils/filesystem/directory";
|
} from "@/server/utils/filesystem/directory";
|
||||||
import {
|
import {
|
||||||
readConfig,
|
readConfig,
|
||||||
readConfigInServer,
|
readRemoteConfig,
|
||||||
removeTraefikConfig,
|
removeTraefikConfig,
|
||||||
writeConfig,
|
writeConfig,
|
||||||
} from "@/server/utils/traefik/application";
|
} from "@/server/utils/traefik/application";
|
||||||
@@ -338,7 +334,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
const application = await findApplicationById(input.applicationId);
|
const application = await findApplicationById(input.applicationId);
|
||||||
let traefikConfig = null;
|
let traefikConfig = null;
|
||||||
if (application.serverId) {
|
if (application.serverId) {
|
||||||
traefikConfig = await readConfigInServer(
|
traefikConfig = await readRemoteConfig(
|
||||||
application.serverId,
|
application.serverId,
|
||||||
application.appName,
|
application.appName,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -71,8 +71,10 @@ export const domainRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const domain = await findDomainById(input.domainId);
|
const domain = await findDomainById(input.domainId);
|
||||||
const result = await removeDomainById(input.domainId);
|
const result = await removeDomainById(input.domainId);
|
||||||
if (domain.application) {
|
|
||||||
await removeDomain(domain.application.appName, domain.uniqueConfigKey);
|
if (domain.applicationId) {
|
||||||
|
const application = await findApplicationById(domain.applicationId);
|
||||||
|
await removeDomain(application, domain.uniqueConfigKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { readSSHKey } from "@/server/utils/filesystem/ssh";
|
import { readSSHKey } from "@/server/utils/filesystem/ssh";
|
||||||
import { execAsync } from "@/server/utils/process/execAsync";
|
import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
|
||||||
import { tail } from "lodash";
|
import { tail } from "lodash";
|
||||||
import { stderr, stdout } from "node:process";
|
import { stderr, stdout } from "node:process";
|
||||||
import { Client } from "ssh2";
|
import { Client } from "ssh2";
|
||||||
@@ -86,40 +86,14 @@ export const getContainersByAppNameMatch = async (
|
|||||||
? `${cmd} --filter='label=com.docker.compose.project=${appName}'`
|
? `${cmd} --filter='label=com.docker.compose.project=${appName}'`
|
||||||
: `${cmd} | grep ${appName}`;
|
: `${cmd} | grep ${appName}`;
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
const server = await findServerById(serverId);
|
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
||||||
|
|
||||||
if (!server.sshKeyId) return;
|
if (stderr) {
|
||||||
const keys = await readSSHKey(server.sshKeyId);
|
return [];
|
||||||
const client = new Client();
|
}
|
||||||
result = await new Promise<string[]>((resolve, reject) => {
|
|
||||||
let output = "";
|
if (!stdout) return [];
|
||||||
client
|
result = stdout.trim().split("\n");
|
||||||
.on("ready", () => {
|
|
||||||
client.exec(command, (err, stream) => {
|
|
||||||
if (err) {
|
|
||||||
console.error("Execution error:", err);
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stream
|
|
||||||
.on("close", () => {
|
|
||||||
client.end();
|
|
||||||
resolve(output.trim().split("\n"));
|
|
||||||
})
|
|
||||||
.on("data", (data: string) => {
|
|
||||||
output += data.toString();
|
|
||||||
})
|
|
||||||
.stderr.on("data", (data) => {});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.connect({
|
|
||||||
host: server.ipAddress,
|
|
||||||
port: server.port,
|
|
||||||
username: server.username,
|
|
||||||
privateKey: keys.privateKey,
|
|
||||||
timeout: 99999,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
const { stdout, stderr } = await execAsync(command);
|
const { stdout, stderr } = await execAsync(command);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { rmdir, stat, unlink } from "node:fs/promises";
|
import path from "node:path";
|
||||||
import path, { join } from "node:path";
|
|
||||||
import { APPLICATIONS_PATH, COMPOSE_PATH } from "@/server/constants";
|
import { APPLICATIONS_PATH, COMPOSE_PATH } from "@/server/constants";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import {
|
import {
|
||||||
@@ -7,10 +6,11 @@ import {
|
|||||||
type apiCreateMount,
|
type apiCreateMount,
|
||||||
mounts,
|
mounts,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { createFile } from "@/server/utils/docker/utils";
|
import { createFile, getCreateFileCommand } from "@/server/utils/docker/utils";
|
||||||
import { removeFileOrDirectory } from "@/server/utils/filesystem/directory";
|
import { removeFileOrDirectory } from "@/server/utils/filesystem/directory";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { type SQL, eq, sql } from "drizzle-orm";
|
import { type SQL, eq, sql } from "drizzle-orm";
|
||||||
|
import { execAsyncRemote } from "@/server/utils/process/execAsync";
|
||||||
|
|
||||||
export type Mount = typeof mounts.$inferSelect;
|
export type Mount = typeof mounts.$inferSelect;
|
||||||
|
|
||||||
@@ -71,7 +71,19 @@ export const createFileMount = async (mountId: string) => {
|
|||||||
try {
|
try {
|
||||||
const mount = await findMountById(mountId);
|
const mount = await findMountById(mountId);
|
||||||
const baseFilePath = await getBaseFilesPath(mountId);
|
const baseFilePath = await getBaseFilesPath(mountId);
|
||||||
await createFile(baseFilePath, mount.filePath || "", mount.content || "");
|
|
||||||
|
const serverId = await getServerId(mount);
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
const command = getCreateFileCommand(
|
||||||
|
baseFilePath,
|
||||||
|
mount.filePath || "",
|
||||||
|
mount.content || "",
|
||||||
|
);
|
||||||
|
await execAsyncRemote(serverId, command);
|
||||||
|
} else {
|
||||||
|
await createFile(baseFilePath, mount.filePath || "", mount.content || "");
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`Error to create the file mount: ${error}`);
|
console.log(`Error to create the file mount: ${error}`);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -186,9 +198,16 @@ export const deleteFileMount = async (mountId: string) => {
|
|||||||
const mount = await findMountById(mountId);
|
const mount = await findMountById(mountId);
|
||||||
if (!mount.filePath) return;
|
if (!mount.filePath) return;
|
||||||
const basePath = await getBaseFilesPath(mountId);
|
const basePath = await getBaseFilesPath(mountId);
|
||||||
|
|
||||||
const fullPath = path.join(basePath, mount.filePath);
|
const fullPath = path.join(basePath, mount.filePath);
|
||||||
try {
|
try {
|
||||||
await removeFileOrDirectory(fullPath);
|
const serverId = await getServerId(mount);
|
||||||
|
if (serverId) {
|
||||||
|
const command = `rm -rf ${fullPath}`;
|
||||||
|
await execAsyncRemote(serverId, command);
|
||||||
|
} else {
|
||||||
|
await removeFileOrDirectory(fullPath);
|
||||||
|
}
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -219,3 +238,30 @@ export const getBaseFilesPath = async (mountId: string) => {
|
|||||||
|
|
||||||
return directoryPath;
|
return directoryPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MountNested = Awaited<ReturnType<typeof findMountById>>;
|
||||||
|
export const getServerId = async (mount: MountNested) => {
|
||||||
|
if (mount.serviceType === "application" && mount?.application?.serverId) {
|
||||||
|
return mount.application.serverId;
|
||||||
|
}
|
||||||
|
if (mount.serviceType === "postgres" && mount?.postgres?.serverId) {
|
||||||
|
return mount.postgres.serverId;
|
||||||
|
}
|
||||||
|
if (mount.serviceType === "mariadb" && mount?.mariadb?.serverId) {
|
||||||
|
return mount.mariadb.serverId;
|
||||||
|
}
|
||||||
|
if (mount.serviceType === "mongo" && mount?.mongo?.serverId) {
|
||||||
|
return mount.mongo.serverId;
|
||||||
|
}
|
||||||
|
if (mount.serviceType === "mysql" && mount?.mysql?.serverId) {
|
||||||
|
return mount.mysql.serverId;
|
||||||
|
}
|
||||||
|
if (mount.serviceType === "redis" && mount?.redis?.serverId) {
|
||||||
|
return mount.redis.serverId;
|
||||||
|
}
|
||||||
|
if (mount.serviceType === "compose" && mount?.compose?.serverId) {
|
||||||
|
return mount.compose.serverId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const createRedirect = async (
|
|||||||
|
|
||||||
const application = await findApplicationById(redirect.applicationId);
|
const application = await findApplicationById(redirect.applicationId);
|
||||||
|
|
||||||
createRedirectMiddleware(application.appName, redirect);
|
createRedirectMiddleware(application, redirect);
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -77,7 +77,7 @@ export const removeRedirectById = async (redirectId: string) => {
|
|||||||
|
|
||||||
const application = await findApplicationById(response.applicationId);
|
const application = await findApplicationById(response.applicationId);
|
||||||
|
|
||||||
removeRedirectMiddleware(application.appName, response);
|
await removeRedirectMiddleware(application, response);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -111,7 +111,7 @@ export const updateRedirectById = async (
|
|||||||
}
|
}
|
||||||
const application = await findApplicationById(redirect.applicationId);
|
const application = await findApplicationById(redirect.applicationId);
|
||||||
|
|
||||||
updateRedirectMiddleware(application.appName, redirect);
|
await updateRedirectMiddleware(application, redirect);
|
||||||
|
|
||||||
return redirect;
|
return redirect;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const createSecurity = async (
|
|||||||
message: "Error to create the security",
|
message: "Error to create the security",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await createSecurityMiddleware(application.appName, securityResponse);
|
await createSecurityMiddleware(application, securityResponse);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -74,7 +74,7 @@ export const deleteSecurityById = async (securityId: string) => {
|
|||||||
|
|
||||||
const application = await findApplicationById(result.applicationId);
|
const application = await findApplicationById(result.applicationId);
|
||||||
|
|
||||||
removeSecurityMiddleware(application.appName, result);
|
await removeSecurityMiddleware(application, result);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|||||||
@@ -393,6 +393,24 @@ export const createFile = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCreateFileCommand = (
|
||||||
|
outputPath: string,
|
||||||
|
filePath: string,
|
||||||
|
content: string,
|
||||||
|
) => {
|
||||||
|
const fullPath = path.join(outputPath, filePath);
|
||||||
|
if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) {
|
||||||
|
return `mkdir -p ${fullPath};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const directory = path.dirname(fullPath);
|
||||||
|
|
||||||
|
return `
|
||||||
|
mkdir -p ${directory};
|
||||||
|
echo "${content}" > ${fullPath};
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
export const getServiceContainer = async (appName: string) => {
|
export const getServiceContainer = async (appName: string) => {
|
||||||
try {
|
try {
|
||||||
const filter = {
|
const filter = {
|
||||||
|
|||||||
@@ -1,3 +1,39 @@
|
|||||||
import { exec } from "node:child_process";
|
import { exec } from "node:child_process";
|
||||||
import util from "node:util";
|
import util from "node:util";
|
||||||
|
import { connectSSH } from "../servers/connection";
|
||||||
export const execAsync = util.promisify(exec);
|
export const execAsync = util.promisify(exec);
|
||||||
|
|
||||||
|
export const execAsyncRemote = async (
|
||||||
|
serverId: string,
|
||||||
|
command: string,
|
||||||
|
): Promise<{ stdout: string; stderr: string }> => {
|
||||||
|
const client = await connectSSH(serverId);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
client.exec(command, (err, stream) => {
|
||||||
|
if (err) {
|
||||||
|
client.end();
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
|
||||||
|
stream
|
||||||
|
.on("data", (data: string) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
})
|
||||||
|
.on("close", (code, signal) => {
|
||||||
|
client.end();
|
||||||
|
if (code === 0) {
|
||||||
|
resolve({ stdout, stderr });
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Command exited with code ${code}`));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.stderr.on("data", (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,41 +1,10 @@
|
|||||||
import { findServerById } from "@/server/api/services/server";
|
import { execAsyncRemote } from "../process/execAsync";
|
||||||
import { readSSHKey } from "../filesystem/ssh";
|
|
||||||
import { Client } from "ssh2";
|
|
||||||
|
|
||||||
export const executeCommand = async (serverId: string, command: string) => {
|
export const executeCommand = async (serverId: string, command: string) => {
|
||||||
const server = await findServerById(serverId);
|
try {
|
||||||
|
await execAsyncRemote(serverId, command);
|
||||||
if (!server.sshKeyId) return;
|
} catch (err) {
|
||||||
const keys = await readSSHKey(server.sshKeyId);
|
console.error("Execution error:", err);
|
||||||
const client = new Client();
|
throw err;
|
||||||
return new Promise<void>((resolve, reject) => {
|
}
|
||||||
client
|
|
||||||
.on("ready", () => {
|
|
||||||
client.exec(command, (err, stream) => {
|
|
||||||
if (err) {
|
|
||||||
console.error("Execution error:", err);
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stream
|
|
||||||
.on("close", (code, signal) => {
|
|
||||||
client.end();
|
|
||||||
if (code === 0) {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
reject(new Error(`Command exited with code ${code}`));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on("data", (data: string) => {})
|
|
||||||
.stderr.on("data", (data) => {});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.connect({
|
|
||||||
host: server.ipAddress,
|
|
||||||
port: server.port,
|
|
||||||
username: server.username,
|
|
||||||
privateKey: keys.privateKey,
|
|
||||||
timeout: 99999,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
24
apps/dokploy/server/utils/servers/connection.ts
Normal file
24
apps/dokploy/server/utils/servers/connection.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { findServerById } from "@/server/api/services/server";
|
||||||
|
import { Client } from "ssh2";
|
||||||
|
import { readSSHKey } from "../filesystem/ssh";
|
||||||
|
|
||||||
|
export const connectSSH = async (serverId: string) => {
|
||||||
|
const server = await findServerById(serverId);
|
||||||
|
if (!server.sshKeyId) throw new Error("No SSH key available for this server");
|
||||||
|
|
||||||
|
const keys = await readSSHKey(server.sshKeyId);
|
||||||
|
const client = new Client();
|
||||||
|
|
||||||
|
return new Promise<Client>((resolve, reject) => {
|
||||||
|
client
|
||||||
|
.on("ready", () => resolve(client))
|
||||||
|
.on("error", reject)
|
||||||
|
.connect({
|
||||||
|
host: server.ipAddress,
|
||||||
|
port: server.port,
|
||||||
|
username: server.username,
|
||||||
|
privateKey: keys.privateKey,
|
||||||
|
timeout: 99999,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
import fs, { writeFileSync } from "node:fs";
|
import fs, { writeFileSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { Domain } from "@/server/api/services/domain";
|
import type { Domain } from "@/server/api/services/domain";
|
||||||
import { DYNAMIC_TRAEFIK_PATH, MAIN_TRAEFIK_PATH } from "@/server/constants";
|
import { DYNAMIC_TRAEFIK_PATH } from "@/server/constants";
|
||||||
import { dump, load } from "js-yaml";
|
import { dump, load } from "js-yaml";
|
||||||
import type { FileConfig, HttpLoadBalancerService } from "./file-types";
|
import type { FileConfig, HttpLoadBalancerService } from "./file-types";
|
||||||
import { findServerById } from "@/server/api/services/server";
|
import { execAsyncRemote } from "../process/execAsync";
|
||||||
import { Client } from "ssh2";
|
|
||||||
import { readSSHKey } from "../filesystem/ssh";
|
|
||||||
|
|
||||||
export const createTraefikConfig = (appName: string) => {
|
export const createTraefikConfig = (appName: string) => {
|
||||||
const defaultPort = 3000;
|
const defaultPort = 3000;
|
||||||
@@ -58,6 +56,16 @@ export const removeTraefikConfig = async (appName: string) => {
|
|||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const removeTraefikConfigRemote = async (
|
||||||
|
appName: string,
|
||||||
|
serverId: string,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
||||||
|
await execAsyncRemote(serverId, `rm ${configPath}`);
|
||||||
|
} catch (error) {}
|
||||||
|
};
|
||||||
|
|
||||||
export const loadOrCreateConfig = (appName: string): FileConfig => {
|
export const loadOrCreateConfig = (appName: string): FileConfig => {
|
||||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
||||||
if (fs.existsSync(configPath)) {
|
if (fs.existsSync(configPath)) {
|
||||||
@@ -74,56 +82,20 @@ export const loadOrCreateConfigRemote = async (
|
|||||||
serverId: string,
|
serverId: string,
|
||||||
appName: string,
|
appName: string,
|
||||||
) => {
|
) => {
|
||||||
const server = await findServerById(serverId);
|
const fileConfig: FileConfig = { http: { routers: {}, services: {} } };
|
||||||
if (!server.sshKeyId) return { http: { routers: {}, services: {} } };
|
|
||||||
|
|
||||||
const keys = await readSSHKey(server.sshKeyId);
|
|
||||||
const client = new Client();
|
|
||||||
let fileConfig: FileConfig;
|
|
||||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
||||||
return new Promise<FileConfig>((resolve, reject) => {
|
try {
|
||||||
client
|
const { stdout } = await execAsyncRemote(serverId, `cat ${configPath}`);
|
||||||
.on("ready", () => {
|
|
||||||
client.exec(`cat ${configPath}`, (err, stream) => {
|
|
||||||
if (err) {
|
|
||||||
console.error("Execution error:", err);
|
|
||||||
return { http: { routers: {}, services: {} } };
|
|
||||||
}
|
|
||||||
stream
|
|
||||||
.on("close", (code, signal) => {
|
|
||||||
client.end();
|
|
||||||
if (code === 0) {
|
|
||||||
if (!fileConfig) {
|
|
||||||
fileConfig = { http: { routers: {}, services: {} } };
|
|
||||||
}
|
|
||||||
resolve(
|
|
||||||
(load(fileConfig) as FileConfig) || {
|
|
||||||
http: { routers: {}, services: {} },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log(fileConfig);
|
|
||||||
|
|
||||||
resolve({ http: { routers: {}, services: {} } });
|
if (!stdout) return fileConfig;
|
||||||
|
|
||||||
// reject(new Error(`Command exited with code ${code}`));
|
const parsedConfig = (load(stdout) as FileConfig) || {
|
||||||
}
|
http: { routers: {}, services: {} },
|
||||||
})
|
};
|
||||||
.on("data", (data: string) => {
|
return parsedConfig;
|
||||||
console.log(data.toString());
|
} catch (err) {
|
||||||
fileConfig = data.toString() as unknown as FileConfig;
|
return fileConfig;
|
||||||
})
|
}
|
||||||
.stderr.on("data", (data) => {});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.connect({
|
|
||||||
host: server.ipAddress,
|
|
||||||
port: server.port,
|
|
||||||
username: server.username,
|
|
||||||
privateKey: keys.privateKey,
|
|
||||||
timeout: 99999,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const readConfig = (appName: string) => {
|
export const readConfig = (appName: string) => {
|
||||||
@@ -135,51 +107,15 @@ export const readConfig = (appName: string) => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const readConfigInServer = async (serverId: string, appName: string) => {
|
export const readRemoteConfig = async (serverId: string, appName: string) => {
|
||||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
||||||
let content = "";
|
try {
|
||||||
// if (fs.existsSync(configPath)) {
|
const { stdout } = await execAsyncRemote(serverId, `cat ${configPath}`);
|
||||||
// const yamlStr = fs.readFileSync(configPath, "utf8");
|
if (!stdout) return null;
|
||||||
// return yamlStr;
|
return stdout;
|
||||||
// }
|
} catch (err) {
|
||||||
|
return null;
|
||||||
const client = new Client();
|
}
|
||||||
const server = await findServerById(serverId);
|
|
||||||
if (!server.sshKeyId) return;
|
|
||||||
const keys = await readSSHKey(server.sshKeyId);
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
|
||||||
client
|
|
||||||
.on("ready", () => {
|
|
||||||
const bashCommand = `
|
|
||||||
cat ${configPath}
|
|
||||||
`;
|
|
||||||
|
|
||||||
client.exec(bashCommand, (err, stream) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stream
|
|
||||||
.on("close", () => {
|
|
||||||
client.end();
|
|
||||||
resolve(content);
|
|
||||||
})
|
|
||||||
.on("data", (data: string) => {
|
|
||||||
content = data.toString();
|
|
||||||
})
|
|
||||||
.stderr.on("data", (data) => {
|
|
||||||
reject(new Error(`stderr: ${data.toString()}`));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.connect({
|
|
||||||
host: server.ipAddress,
|
|
||||||
port: server.port,
|
|
||||||
username: server.username,
|
|
||||||
privateKey: keys.privateKey,
|
|
||||||
timeout: 99999,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const readMonitoringConfig = () => {
|
export const readMonitoringConfig = () => {
|
||||||
@@ -228,13 +164,26 @@ export const writeTraefikConfig = (
|
|||||||
try {
|
try {
|
||||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
||||||
const yamlStr = dump(traefikConfig);
|
const yamlStr = dump(traefikConfig);
|
||||||
console.log(yamlStr);
|
|
||||||
fs.writeFileSync(configPath, yamlStr, "utf8");
|
fs.writeFileSync(configPath, yamlStr, "utf8");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error saving the YAML config file:", e);
|
console.error("Error saving the YAML config file:", e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const writeTraefikConfigRemote = async (
|
||||||
|
traefikConfig: FileConfig,
|
||||||
|
appName: string,
|
||||||
|
serverId: string,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
||||||
|
const yamlStr = dump(traefikConfig);
|
||||||
|
await execAsyncRemote(serverId, `echo '${yamlStr}' > ${configPath}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error saving the YAML config file:", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const createServiceConfig = (
|
export const createServiceConfig = (
|
||||||
appName: string,
|
appName: string,
|
||||||
domain: Domain,
|
domain: Domain,
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ import {
|
|||||||
loadOrCreateConfig,
|
loadOrCreateConfig,
|
||||||
loadOrCreateConfigRemote,
|
loadOrCreateConfigRemote,
|
||||||
removeTraefikConfig,
|
removeTraefikConfig,
|
||||||
|
removeTraefikConfigRemote,
|
||||||
writeTraefikConfig,
|
writeTraefikConfig,
|
||||||
|
writeTraefikConfigRemote,
|
||||||
} from "./application";
|
} from "./application";
|
||||||
import type { FileConfig, HttpRouter } from "./file-types";
|
import type { FileConfig, HttpRouter } from "./file-types";
|
||||||
import { DYNAMIC_TRAEFIK_PATH } from "@/server/constants";
|
|
||||||
import path from "node:path";
|
|
||||||
import { dump } from "js-yaml";
|
|
||||||
import { executeCommand } from "../servers/command";
|
|
||||||
|
|
||||||
export const manageDomain = async (app: ApplicationNested, domain: Domain) => {
|
export const manageDomain = async (app: ApplicationNested, domain: Domain) => {
|
||||||
const { appName } = app;
|
const { appName } = app;
|
||||||
@@ -49,23 +47,24 @@ export const manageDomain = async (app: ApplicationNested, domain: Domain) => {
|
|||||||
config.http.services[serviceName] = createServiceConfig(appName, domain);
|
config.http.services[serviceName] = createServiceConfig(appName, domain);
|
||||||
|
|
||||||
if (app.serverId) {
|
if (app.serverId) {
|
||||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
await writeTraefikConfigRemote(config, appName, app.serverId);
|
||||||
const yamlStr = dump(config);
|
|
||||||
|
|
||||||
console.log(yamlStr);
|
|
||||||
|
|
||||||
const command = `
|
|
||||||
echo '${yamlStr}' > ${configPath}
|
|
||||||
`;
|
|
||||||
|
|
||||||
await executeCommand(app.serverId, command);
|
|
||||||
} else {
|
} else {
|
||||||
writeTraefikConfig(config, appName);
|
writeTraefikConfig(config, appName);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeDomain = async (appName: string, uniqueKey: number) => {
|
export const removeDomain = async (
|
||||||
const config: FileConfig = loadOrCreateConfig(appName);
|
application: ApplicationNested,
|
||||||
|
uniqueKey: number,
|
||||||
|
) => {
|
||||||
|
const { appName, serverId } = application;
|
||||||
|
let config: FileConfig;
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
config = await loadOrCreateConfigRemote(serverId, appName);
|
||||||
|
} else {
|
||||||
|
config = loadOrCreateConfig(appName);
|
||||||
|
}
|
||||||
|
|
||||||
const routerKey = `${appName}-router-${uniqueKey}`;
|
const routerKey = `${appName}-router-${uniqueKey}`;
|
||||||
const routerSecureKey = `${appName}-router-websecure-${uniqueKey}`;
|
const routerSecureKey = `${appName}-router-websecure-${uniqueKey}`;
|
||||||
@@ -86,9 +85,17 @@ export const removeDomain = async (appName: string, uniqueKey: number) => {
|
|||||||
config?.http?.routers &&
|
config?.http?.routers &&
|
||||||
Object.keys(config?.http?.routers).length === 0
|
Object.keys(config?.http?.routers).length === 0
|
||||||
) {
|
) {
|
||||||
await removeTraefikConfig(appName);
|
if (serverId) {
|
||||||
|
await removeTraefikConfigRemote(appName, serverId);
|
||||||
|
} else {
|
||||||
|
await removeTraefikConfig(appName);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
writeTraefikConfig(config, appName);
|
if (serverId) {
|
||||||
|
await writeTraefikConfigRemote(config, appName, serverId);
|
||||||
|
} else {
|
||||||
|
writeTraefikConfig(config, appName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { DYNAMIC_TRAEFIK_PATH } from "@/server/constants";
|
|||||||
import { dump, load } from "js-yaml";
|
import { dump, load } from "js-yaml";
|
||||||
import type { ApplicationNested } from "../builders";
|
import type { ApplicationNested } from "../builders";
|
||||||
import type { FileConfig } from "./file-types";
|
import type { FileConfig } from "./file-types";
|
||||||
|
import { execAsyncRemote } from "../process/execAsync";
|
||||||
|
|
||||||
export const addMiddleware = (config: FileConfig, middlewareName: string) => {
|
export const addMiddleware = (config: FileConfig, middlewareName: string) => {
|
||||||
if (config.http?.routers) {
|
if (config.http?.routers) {
|
||||||
@@ -72,6 +73,25 @@ export const loadMiddlewares = <T>() => {
|
|||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const loadRemoteMiddlewares = async (serverId: string) => {
|
||||||
|
const configPath = join(DYNAMIC_TRAEFIK_PATH, "middlewares.yml");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout, stderr } = await execAsyncRemote(
|
||||||
|
serverId,
|
||||||
|
`cat ${configPath}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
console.error(`Error: ${stderr}`);
|
||||||
|
throw new Error(`File not found: ${configPath}`);
|
||||||
|
}
|
||||||
|
const config = load(stdout) as FileConfig;
|
||||||
|
return config;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`File not found: ${configPath}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
export const writeMiddleware = <T>(config: T) => {
|
export const writeMiddleware = <T>(config: T) => {
|
||||||
const configPath = join(DYNAMIC_TRAEFIK_PATH, "middlewares.yml");
|
const configPath = join(DYNAMIC_TRAEFIK_PATH, "middlewares.yml");
|
||||||
const newYamlContent = dump(config);
|
const newYamlContent = dump(config);
|
||||||
|
|||||||
@@ -1,15 +1,32 @@
|
|||||||
import type { Redirect } from "@/server/api/services/redirect";
|
import type { Redirect } from "@/server/api/services/redirect";
|
||||||
import { loadOrCreateConfig, writeTraefikConfig } from "./application";
|
import {
|
||||||
|
loadOrCreateConfig,
|
||||||
|
loadOrCreateConfigRemote,
|
||||||
|
writeTraefikConfig,
|
||||||
|
writeTraefikConfigRemote,
|
||||||
|
} from "./application";
|
||||||
import type { FileConfig } from "./file-types";
|
import type { FileConfig } from "./file-types";
|
||||||
import {
|
import {
|
||||||
addMiddleware,
|
addMiddleware,
|
||||||
deleteMiddleware,
|
deleteMiddleware,
|
||||||
loadMiddlewares,
|
loadMiddlewares,
|
||||||
|
loadRemoteMiddlewares,
|
||||||
writeMiddleware,
|
writeMiddleware,
|
||||||
} from "./middleware";
|
} from "./middleware";
|
||||||
|
import type { ApplicationNested } from "../builders";
|
||||||
|
|
||||||
export const updateRedirectMiddleware = (appName: string, data: Redirect) => {
|
export const updateRedirectMiddleware = async (
|
||||||
const config = loadMiddlewares<FileConfig>();
|
application: ApplicationNested,
|
||||||
|
data: Redirect,
|
||||||
|
) => {
|
||||||
|
const { appName, serverId } = application;
|
||||||
|
let config: FileConfig;
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
config = await loadRemoteMiddlewares(serverId);
|
||||||
|
} else {
|
||||||
|
config = loadMiddlewares<FileConfig>();
|
||||||
|
}
|
||||||
const middlewareName = `redirect-${appName}-${data.uniqueConfigKey}`;
|
const middlewareName = `redirect-${appName}-${data.uniqueConfigKey}`;
|
||||||
|
|
||||||
if (config?.http?.middlewares?.[middlewareName]) {
|
if (config?.http?.middlewares?.[middlewareName]) {
|
||||||
@@ -22,10 +39,26 @@ export const updateRedirectMiddleware = (appName: string, data: Redirect) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
writeMiddleware(config);
|
if (serverId) {
|
||||||
|
await writeTraefikConfigRemote(config, "middlewares", serverId);
|
||||||
|
} else {
|
||||||
|
writeMiddleware(config);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
export const createRedirectMiddleware = (appName: string, data: Redirect) => {
|
export const createRedirectMiddleware = async (
|
||||||
const config = loadMiddlewares<FileConfig>();
|
application: ApplicationNested,
|
||||||
|
data: Redirect,
|
||||||
|
) => {
|
||||||
|
const { appName, serverId } = application;
|
||||||
|
|
||||||
|
let config: FileConfig;
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
config = await loadRemoteMiddlewares(serverId);
|
||||||
|
} else {
|
||||||
|
config = loadMiddlewares<FileConfig>();
|
||||||
|
}
|
||||||
|
|
||||||
const middlewareName = `redirect-${appName}-${data.uniqueConfigKey}`;
|
const middlewareName = `redirect-${appName}-${data.uniqueConfigKey}`;
|
||||||
const newMiddleware = {
|
const newMiddleware = {
|
||||||
[middlewareName]: {
|
[middlewareName]: {
|
||||||
@@ -44,25 +77,56 @@ export const createRedirectMiddleware = (appName: string, data: Redirect) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const appConfig = loadOrCreateConfig(appName);
|
let appConfig: FileConfig;
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
appConfig = await loadOrCreateConfigRemote(serverId, appName);
|
||||||
|
} else {
|
||||||
|
appConfig = loadOrCreateConfig(appName);
|
||||||
|
}
|
||||||
|
|
||||||
addMiddleware(appConfig, middlewareName);
|
addMiddleware(appConfig, middlewareName);
|
||||||
|
|
||||||
writeTraefikConfig(appConfig, appName);
|
if (serverId) {
|
||||||
writeMiddleware(config);
|
await writeTraefikConfigRemote(config, "middlewares", serverId);
|
||||||
|
await writeTraefikConfigRemote(appConfig, appName, serverId);
|
||||||
|
} else {
|
||||||
|
writeMiddleware(config);
|
||||||
|
writeTraefikConfig(appConfig, appName);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeRedirectMiddleware = (appName: string, data: Redirect) => {
|
export const removeRedirectMiddleware = async (
|
||||||
const config = loadMiddlewares<FileConfig>();
|
application: ApplicationNested,
|
||||||
|
data: Redirect,
|
||||||
|
) => {
|
||||||
|
const { appName, serverId } = application;
|
||||||
|
let config: FileConfig;
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
config = await loadRemoteMiddlewares(serverId);
|
||||||
|
} else {
|
||||||
|
config = loadMiddlewares<FileConfig>();
|
||||||
|
}
|
||||||
const middlewareName = `redirect-${appName}-${data.uniqueConfigKey}`;
|
const middlewareName = `redirect-${appName}-${data.uniqueConfigKey}`;
|
||||||
|
|
||||||
if (config?.http?.middlewares?.[middlewareName]) {
|
if (config?.http?.middlewares?.[middlewareName]) {
|
||||||
delete config.http.middlewares[middlewareName];
|
delete config.http.middlewares[middlewareName];
|
||||||
}
|
}
|
||||||
|
let appConfig: FileConfig;
|
||||||
const appConfig = loadOrCreateConfig(appName);
|
if (serverId) {
|
||||||
|
appConfig = await loadOrCreateConfigRemote(serverId, appName);
|
||||||
|
} else {
|
||||||
|
appConfig = loadOrCreateConfig(appName);
|
||||||
|
}
|
||||||
|
|
||||||
deleteMiddleware(appConfig, middlewareName);
|
deleteMiddleware(appConfig, middlewareName);
|
||||||
writeTraefikConfig(appConfig, appName);
|
|
||||||
writeMiddleware(config);
|
if (serverId) {
|
||||||
|
await writeTraefikConfigRemote(config, "middlewares", serverId);
|
||||||
|
await writeTraefikConfigRemote(appConfig, appName, serverId);
|
||||||
|
} else {
|
||||||
|
writeTraefikConfig(appConfig, appName);
|
||||||
|
writeMiddleware(config);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import type { Security } from "@/server/api/services/security";
|
import type { Security } from "@/server/api/services/security";
|
||||||
import * as bcrypt from "bcrypt";
|
import * as bcrypt from "bcrypt";
|
||||||
import { loadOrCreateConfig, writeTraefikConfig } from "./application";
|
import {
|
||||||
|
loadOrCreateConfig,
|
||||||
|
loadOrCreateConfigRemote,
|
||||||
|
writeTraefikConfig,
|
||||||
|
writeTraefikConfigRemote,
|
||||||
|
} from "./application";
|
||||||
import type {
|
import type {
|
||||||
BasicAuthMiddleware,
|
BasicAuthMiddleware,
|
||||||
FileConfig,
|
FileConfig,
|
||||||
@@ -10,14 +15,23 @@ import {
|
|||||||
addMiddleware,
|
addMiddleware,
|
||||||
deleteMiddleware,
|
deleteMiddleware,
|
||||||
loadMiddlewares,
|
loadMiddlewares,
|
||||||
|
loadRemoteMiddlewares,
|
||||||
writeMiddleware,
|
writeMiddleware,
|
||||||
} from "./middleware";
|
} from "./middleware";
|
||||||
|
import type { ApplicationNested } from "../builders";
|
||||||
|
|
||||||
export const createSecurityMiddleware = async (
|
export const createSecurityMiddleware = async (
|
||||||
appName: string,
|
application: ApplicationNested,
|
||||||
data: Security,
|
data: Security,
|
||||||
) => {
|
) => {
|
||||||
const config = loadMiddlewares<FileConfig>();
|
const { appName, serverId } = application;
|
||||||
|
let config: FileConfig;
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
config = await loadRemoteMiddlewares(serverId);
|
||||||
|
} else {
|
||||||
|
config = loadMiddlewares<FileConfig>();
|
||||||
|
}
|
||||||
const middlewareName = `auth-${appName}`;
|
const middlewareName = `auth-${appName}`;
|
||||||
|
|
||||||
const user = `${data.username}:${await bcrypt.hash(data.password, 10)}`;
|
const user = `${data.username}:${await bcrypt.hash(data.password, 10)}`;
|
||||||
@@ -38,17 +52,42 @@ export const createSecurityMiddleware = async (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let appConfig: FileConfig;
|
||||||
|
|
||||||
const appConfig = loadOrCreateConfig(appName);
|
if (serverId) {
|
||||||
|
appConfig = await loadOrCreateConfigRemote(serverId, appName);
|
||||||
|
} else {
|
||||||
|
appConfig = loadOrCreateConfig(appName);
|
||||||
|
}
|
||||||
addMiddleware(appConfig, middlewareName);
|
addMiddleware(appConfig, middlewareName);
|
||||||
writeTraefikConfig(appConfig, appName);
|
if (serverId) {
|
||||||
writeMiddleware(config);
|
await writeTraefikConfigRemote(config, "middlewares", serverId);
|
||||||
|
await writeTraefikConfigRemote(appConfig, appName, serverId);
|
||||||
|
} else {
|
||||||
|
writeTraefikConfig(appConfig, appName);
|
||||||
|
writeMiddleware(config);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeSecurityMiddleware = (appName: string, data: Security) => {
|
export const removeSecurityMiddleware = async (
|
||||||
const config = loadMiddlewares<FileConfig>();
|
application: ApplicationNested,
|
||||||
const appConfig = loadOrCreateConfig(appName);
|
data: Security,
|
||||||
|
) => {
|
||||||
|
const { appName, serverId } = application;
|
||||||
|
let config: FileConfig;
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
config = await loadRemoteMiddlewares(serverId);
|
||||||
|
} else {
|
||||||
|
config = loadMiddlewares<FileConfig>();
|
||||||
|
}
|
||||||
|
let appConfig: FileConfig;
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
appConfig = await loadOrCreateConfigRemote(serverId, appName);
|
||||||
|
} else {
|
||||||
|
appConfig = loadOrCreateConfig(appName);
|
||||||
|
}
|
||||||
const middlewareName = `auth-${appName}`;
|
const middlewareName = `auth-${appName}`;
|
||||||
|
|
||||||
if (config.http?.middlewares) {
|
if (config.http?.middlewares) {
|
||||||
@@ -67,12 +106,20 @@ export const removeSecurityMiddleware = (appName: string, data: Security) => {
|
|||||||
delete config.http.middlewares[middlewareName];
|
delete config.http.middlewares[middlewareName];
|
||||||
}
|
}
|
||||||
deleteMiddleware(appConfig, middlewareName);
|
deleteMiddleware(appConfig, middlewareName);
|
||||||
writeTraefikConfig(appConfig, appName);
|
if (serverId) {
|
||||||
|
await writeTraefikConfigRemote(appConfig, appName, serverId);
|
||||||
|
} else {
|
||||||
|
writeTraefikConfig(appConfig, appName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeMiddleware(config);
|
if (serverId) {
|
||||||
|
await writeTraefikConfigRemote(config, "middlewares", serverId);
|
||||||
|
} else {
|
||||||
|
writeMiddleware(config);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isBasicAuthMiddleware = (
|
const isBasicAuthMiddleware = (
|
||||||
|
|||||||
Reference in New Issue
Block a user