feat: add domains in external server

This commit is contained in:
Mauricio Siu
2024-09-09 09:35:02 -06:00
parent cf06162be7
commit 5afe1645a0
11 changed files with 207 additions and 19 deletions

View File

@@ -174,7 +174,9 @@ export const AddDomain = ({
isLoading={isLoadingGenerate} isLoading={isLoadingGenerate}
onClick={() => { onClick={() => {
generateDomain({ generateDomain({
appName: application?.appName || "", applicationId:
application?.applicationId || "",
// appName: application?.appName || "",
}) })
.then((domain) => { .then((domain) => {
field.onChange(domain); field.onChange(domain);

View File

@@ -39,6 +39,7 @@ import {
} from "@/server/utils/filesystem/directory"; } from "@/server/utils/filesystem/directory";
import { import {
readConfig, readConfig,
readConfigInServer,
removeTraefikConfig, removeTraefikConfig,
writeConfig, writeConfig,
} from "@/server/utils/traefik/application"; } from "@/server/utils/traefik/application";
@@ -335,8 +336,15 @@ export const applicationRouter = createTRPCRouter({
.input(apiFindOneApplication) .input(apiFindOneApplication)
.query(async ({ input }) => { .query(async ({ input }) => {
const application = await findApplicationById(input.applicationId); const application = await findApplicationById(input.applicationId);
let traefikConfig = null;
const traefikConfig = readConfig(application.appName); if (application.serverId) {
traefikConfig = await readConfigInServer(
application.serverId,
application.appName,
);
} else {
traefikConfig = readConfig(application.appName);
}
return traefikConfig; return traefikConfig;
}), }),

View File

@@ -47,9 +47,9 @@ export const domainRouter = createTRPCRouter({
return await findDomainsByComposeId(input.composeId); return await findDomainsByComposeId(input.composeId);
}), }),
generateDomain: protectedProcedure generateDomain: protectedProcedure
.input(apiCreateTraefikMeDomain) .input(apiFindOneApplication)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
return generateTraefikMeDomain(input.appName); return generateTraefikMeDomain(input.applicationId);
}), }),
update: protectedProcedure update: protectedProcedure

View File

@@ -1,9 +1,5 @@
import { db } from "@/server/db"; import { db } from "@/server/db";
import { import { type apiCreateDomain, domains } from "@/server/db/schema";
type apiCreateDomain,
type apiFindDomainByApplication,
domains,
} from "@/server/db/schema";
import { manageDomain } from "@/server/utils/traefik/domain"; import { manageDomain } from "@/server/utils/traefik/domain";
import { generateRandomDomain } from "@/templates/utils"; import { generateRandomDomain } from "@/templates/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
@@ -38,10 +34,10 @@ export const createDomain = async (input: typeof apiCreateDomain._type) => {
}; };
export const generateTraefikMeDomain = async (appName: string) => { export const generateTraefikMeDomain = async (appName: string) => {
const admin = await findAdmin(); const application = await findApplicationById(appName);
return generateRandomDomain({ return generateRandomDomain({
serverIp: admin.serverIp || "", serverIp: application.server?.ipAddress || "",
projectName: appName, projectName: application.appName,
}); });
}; };

View File

@@ -7,7 +7,7 @@ export const BASE_PATH =
: path.join(process.cwd(), ".docker"); : path.join(process.cwd(), ".docker");
export const IS_CLOUD = process.env.IS_CLOUD === "true"; export const IS_CLOUD = process.env.IS_CLOUD === "true";
export const MAIN_TRAEFIK_PATH = `${BASE_PATH}/traefik`; export const MAIN_TRAEFIK_PATH = `${BASE_PATH}/traefik`;
export const DYNAMIC_TRAEFIK_PATH = `${BASE_PATH}/traefik/dynamic`; export const DYNAMIC_TRAEFIK_PATH = `/etc/dokploy/traefik/dynamic`;
export const LOGS_PATH = `/etc/dokploy/logs`; export const LOGS_PATH = `/etc/dokploy/logs`;
export const APPLICATIONS_PATH = `/etc/dokploy/applications`; export const APPLICATIONS_PATH = `/etc/dokploy/applications`;
export const COMPOSE_PATH = `/etc/dokploy/compose`; export const COMPOSE_PATH = `/etc/dokploy/compose`;

View File

@@ -215,6 +215,55 @@ export const getDefaultTraefikConfig = () => {
return yamlStr; return yamlStr;
}; };
export const getDefaultServerTraefikConfig = () => {
const configObject: MainTraefikConfig = {
providers: {
swarm: {
exposedByDefault: false,
watch: false,
},
docker: {
exposedByDefault: false,
},
file: {
directory: "/etc/dokploy/traefik/dynamic",
watch: true,
},
},
entryPoints: {
web: {
address: `:${TRAEFIK_PORT}`,
},
websecure: {
address: `:${TRAEFIK_SSL_PORT}`,
http: {
tls: {
certResolver: "letsencrypt",
},
},
},
},
api: {
insecure: true,
},
certificatesResolvers: {
letsencrypt: {
acme: {
email: "test@localhost.com",
storage: "/etc/dokploy/traefik/dynamic/acme.json",
httpChallenge: {
entryPoint: "web",
},
},
},
},
};
const yamlStr = dump(configObject);
return yamlStr;
};
export const createDefaultTraefikConfig = () => { export const createDefaultTraefikConfig = () => {
const mainConfig = path.join(MAIN_TRAEFIK_PATH, "traefik.yml"); const mainConfig = path.join(MAIN_TRAEFIK_PATH, "traefik.yml");
const acmeJsonPath = path.join(DYNAMIC_TRAEFIK_PATH, "acme.json"); const acmeJsonPath = path.join(DYNAMIC_TRAEFIK_PATH, "acme.json");

View File

@@ -11,7 +11,6 @@ export const executeCommand = async (serverId: string, command: string) => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
client client
.on("ready", () => { .on("ready", () => {
console.log("Client :: ready", command);
client.exec(command, (err, stream) => { client.exec(command, (err, stream) => {
if (err) { if (err) {
console.error("Execution error:", err); console.error("Execution error:", err);

View File

@@ -22,6 +22,7 @@ import { Client } from "ssh2";
import { readSSHKey } from "../filesystem/ssh"; import { readSSHKey } from "../filesystem/ssh";
import { import {
getDefaultMiddlewares, getDefaultMiddlewares,
getDefaultServerTraefikConfig,
getDefaultTraefikConfig, getDefaultTraefikConfig,
} from "@/server/setup/traefik-setup"; } from "@/server/setup/traefik-setup";
@@ -220,7 +221,7 @@ const validatePorts = () => `
`; `;
const createTraefikConfig = () => { const createTraefikConfig = () => {
const config = getDefaultTraefikConfig(); const config = getDefaultServerTraefikConfig();
const command = ` const command = `
if [ -f "/etc/dokploy/traefik/dynamic/acme.json" ]; then if [ -f "/etc/dokploy/traefik/dynamic/acme.json" ]; then

View File

@@ -4,6 +4,9 @@ import type { Domain } from "@/server/api/services/domain";
import { DYNAMIC_TRAEFIK_PATH, MAIN_TRAEFIK_PATH } from "@/server/constants"; import { DYNAMIC_TRAEFIK_PATH, MAIN_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 { Client } from "ssh2";
import { readSSHKey } from "../filesystem/ssh";
export const createTraefikConfig = (appName: string) => { export const createTraefikConfig = (appName: string) => {
const defaultPort = 3000; const defaultPort = 3000;
@@ -67,6 +70,62 @@ export const loadOrCreateConfig = (appName: string): FileConfig => {
return { http: { routers: {}, services: {} } }; return { http: { routers: {}, services: {} } };
}; };
export const loadOrCreateConfigRemote = async (
serverId: string,
appName: string,
) => {
const server = await findServerById(serverId);
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`);
return new Promise<FileConfig>((resolve, reject) => {
client
.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: {} } });
// reject(new Error(`Command exited with code ${code}`));
}
})
.on("data", (data: string) => {
console.log(data.toString());
fileConfig = data.toString() as unknown as 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) => {
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)) {
@@ -76,6 +135,53 @@ export const readConfig = (appName: string) => {
return null; return null;
}; };
export const readConfigInServer = async (serverId: string, appName: string) => {
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
let content = "";
// if (fs.existsSync(configPath)) {
// const yamlStr = fs.readFileSync(configPath, "utf8");
// return yamlStr;
// }
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 = () => {
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, "access.log"); const configPath = path.join(DYNAMIC_TRAEFIK_PATH, "access.log");
if (fs.existsSync(configPath)) { if (fs.existsSync(configPath)) {
@@ -122,6 +228,7 @@ 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);

View File

@@ -3,14 +3,25 @@ import type { ApplicationNested } from "../builders";
import { import {
createServiceConfig, createServiceConfig,
loadOrCreateConfig, loadOrCreateConfig,
loadOrCreateConfigRemote,
removeTraefikConfig, removeTraefikConfig,
writeTraefikConfig, writeTraefikConfig,
} 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;
const config: FileConfig = loadOrCreateConfig(appName); let config: FileConfig;
if (app.serverId) {
config = await loadOrCreateConfigRemote(app.serverId, appName);
} else {
config = loadOrCreateConfig(appName);
}
const serviceName = `${appName}-service-${domain.uniqueConfigKey}`; const serviceName = `${appName}-service-${domain.uniqueConfigKey}`;
const routerName = `${appName}-router-${domain.uniqueConfigKey}`; const routerName = `${appName}-router-${domain.uniqueConfigKey}`;
const routerNameSecure = `${appName}-router-websecure-${domain.uniqueConfigKey}`; const routerNameSecure = `${appName}-router-websecure-${domain.uniqueConfigKey}`;
@@ -36,7 +47,21 @@ export const manageDomain = async (app: ApplicationNested, domain: Domain) => {
} }
config.http.services[serviceName] = createServiceConfig(appName, domain); config.http.services[serviceName] = createServiceConfig(appName, domain);
writeTraefikConfig(config, appName);
if (app.serverId) {
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
const yamlStr = dump(config);
console.log(yamlStr);
const command = `
echo '${yamlStr}' > ${configPath}
`;
await executeCommand(app.serverId, command);
} else {
writeTraefikConfig(config, appName);
}
}; };
export const removeDomain = async (appName: string, uniqueKey: number) => { export const removeDomain = async (appName: string, uniqueKey: number) => {

View File

@@ -5,6 +5,7 @@ import type { Domain } from "@/server/api/services/domain";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { templates } from "../templates"; import { templates } from "../templates";
import type { TemplatesKeys } from "../types/templates-data.type"; import type { TemplatesKeys } from "../types/templates-data.type";
import { IS_CLOUD } from "@/server/constants";
export interface Schema { export interface Schema {
serverIp: string; serverIp: string;
@@ -28,7 +29,7 @@ export const generateRandomDomain = ({
}: Schema): string => { }: Schema): string => {
const hash = randomBytes(3).toString("hex"); const hash = randomBytes(3).toString("hex");
const slugIp = serverIp.replaceAll(".", "-"); const slugIp = serverIp.replaceAll(".", "-");
return `${projectName}-${hash}${process.env.NODE_ENV === "production" ? `-${slugIp}` : ""}.traefik.me`; return `${projectName}-${hash}${process.env.NODE_ENV === "production" || IS_CLOUD ? `-${slugIp}` : ""}.traefik.me`;
}; };
export const generateHash = (projectName: string, quantity = 3): string => { export const generateHash = (projectName: string, quantity = 3): string => {