refactor: rename builders to server

This commit is contained in:
Mauricio Siu
2024-10-05 22:15:47 -06:00
parent 43555cdabe
commit f3ce69b656
361 changed files with 551 additions and 562 deletions

View File

@@ -0,0 +1,250 @@
import fs, { writeFileSync } from "node:fs";
import path from "node:path";
import { paths } from "@/server/constants";
import type { Domain } from "@/server/services/domain";
import { dump, load } from "js-yaml";
import { encodeBase64 } from "../docker/utils";
import { execAsyncRemote } from "../process/execAsync";
import type { FileConfig, HttpLoadBalancerService } from "./file-types";
export const createTraefikConfig = (appName: string) => {
const defaultPort = 3000;
const serviceURLDefault = `http://${appName}:${defaultPort}`;
const domainDefault = `Host(\`${appName}.docker.localhost\`)`;
const config: FileConfig = {
http: {
routers: {
...(process.env.NODE_ENV === "production"
? {}
: {
[`${appName}-router-1`]: {
rule: domainDefault,
service: `${appName}-service-1`,
entryPoints: ["web"],
},
}),
},
services: {
...(process.env.NODE_ENV === "production"
? {}
: {
[`${appName}-service-1`]: {
loadBalancer: {
servers: [{ url: serviceURLDefault }],
passHostHeader: true,
},
},
}),
},
},
};
const yamlStr = dump(config);
const { DYNAMIC_TRAEFIK_PATH } = paths();
fs.mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true });
writeFileSync(
path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`),
yamlStr,
"utf8",
);
};
export const removeTraefikConfig = async (
appName: string,
serverId?: string | null,
) => {
try {
const { DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
if (serverId) {
await execAsyncRemote(serverId, `rm ${configPath}`);
} else {
if (fs.existsSync(configPath)) {
await fs.promises.unlink(configPath);
}
}
if (fs.existsSync(configPath)) {
await fs.promises.unlink(configPath);
}
} catch (error) {}
};
export const removeTraefikConfigRemote = async (
appName: string,
serverId: string,
) => {
try {
const { DYNAMIC_TRAEFIK_PATH } = paths(true);
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
await execAsyncRemote(serverId, `rm ${configPath}`);
} catch (error) {}
};
export const loadOrCreateConfig = (appName: string): FileConfig => {
const { DYNAMIC_TRAEFIK_PATH } = paths();
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
if (fs.existsSync(configPath)) {
const yamlStr = fs.readFileSync(configPath, "utf8");
const parsedConfig = (load(yamlStr) as FileConfig) || {
http: { routers: {}, services: {} },
};
return parsedConfig;
}
return { http: { routers: {}, services: {} } };
};
export const loadOrCreateConfigRemote = async (
serverId: string,
appName: string,
) => {
const { DYNAMIC_TRAEFIK_PATH } = paths(true);
const fileConfig: FileConfig = { http: { routers: {}, services: {} } };
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
try {
const { stdout } = await execAsyncRemote(serverId, `cat ${configPath}`);
if (!stdout) return fileConfig;
const parsedConfig = (load(stdout) as FileConfig) || {
http: { routers: {}, services: {} },
};
return parsedConfig;
} catch (err) {
return fileConfig;
}
};
export const readConfig = (appName: string) => {
const { DYNAMIC_TRAEFIK_PATH } = paths();
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
if (fs.existsSync(configPath)) {
const yamlStr = fs.readFileSync(configPath, "utf8");
return yamlStr;
}
return null;
};
export const readRemoteConfig = async (serverId: string, appName: string) => {
const { DYNAMIC_TRAEFIK_PATH } = paths(true);
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
try {
const { stdout } = await execAsyncRemote(serverId, `cat ${configPath}`);
if (!stdout) return null;
return stdout;
} catch (err) {
return null;
}
};
export const readMonitoringConfig = () => {
const { DYNAMIC_TRAEFIK_PATH } = paths();
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, "access.log");
if (fs.existsSync(configPath)) {
const yamlStr = fs.readFileSync(configPath, "utf8");
return yamlStr;
}
return null;
};
export const readConfigInPath = async (pathFile: string, serverId?: string) => {
const configPath = path.join(pathFile);
if (serverId) {
const { stdout } = await execAsyncRemote(serverId, `cat ${configPath}`);
if (!stdout) return null;
return stdout;
}
if (fs.existsSync(configPath)) {
const yamlStr = fs.readFileSync(configPath, "utf8");
return yamlStr;
}
return null;
};
export const writeConfig = (appName: string, traefikConfig: string) => {
try {
const { DYNAMIC_TRAEFIK_PATH } = paths();
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
fs.writeFileSync(configPath, traefikConfig, "utf8");
} catch (e) {
console.error("Error saving the YAML config file:", e);
}
};
export const writeConfigRemote = async (
serverId: string,
appName: string,
traefikConfig: string,
) => {
try {
const { DYNAMIC_TRAEFIK_PATH } = paths(true);
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
await execAsyncRemote(serverId, `echo '${traefikConfig}' > ${configPath}`);
} catch (e) {
console.error("Error saving the YAML config file:", e);
}
};
export const writeTraefikConfigInPath = async (
pathFile: string,
traefikConfig: string,
serverId?: string,
) => {
try {
const configPath = path.join(pathFile);
if (serverId) {
const encoded = encodeBase64(traefikConfig);
await execAsyncRemote(
serverId,
`echo "${encoded}" | base64 -d > "${configPath}"`,
);
} else {
fs.writeFileSync(configPath, traefikConfig, "utf8");
}
fs.writeFileSync(configPath, traefikConfig, "utf8");
} catch (e) {
console.error("Error saving the YAML config file:", e);
}
};
export const writeTraefikConfig = (
traefikConfig: FileConfig,
appName: string,
) => {
try {
const { DYNAMIC_TRAEFIK_PATH } = paths();
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
const yamlStr = dump(traefikConfig);
fs.writeFileSync(configPath, yamlStr, "utf8");
} catch (e) {
console.error("Error saving the YAML config file:", e);
}
};
export const writeTraefikConfigRemote = async (
traefikConfig: FileConfig,
appName: string,
serverId: string,
) => {
try {
const { DYNAMIC_TRAEFIK_PATH } = paths(true);
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 = (
appName: string,
domain: Domain,
): {
loadBalancer: HttpLoadBalancerService;
} => ({
loadBalancer: {
servers: [{ url: `http://${appName}:${domain.port || 80}` }],
passHostHeader: true,
},
});

View File

@@ -0,0 +1,145 @@
import type { Domain } from "@/server/services/domain";
import type { ApplicationNested } from "../builders";
import {
createServiceConfig,
loadOrCreateConfig,
loadOrCreateConfigRemote,
removeTraefikConfig,
removeTraefikConfigRemote,
writeTraefikConfig,
writeTraefikConfigRemote,
} from "./application";
import type { FileConfig, HttpRouter } from "./file-types";
export const manageDomain = async (app: ApplicationNested, domain: Domain) => {
const { appName } = app;
let config: FileConfig;
if (app.serverId) {
config = await loadOrCreateConfigRemote(app.serverId, appName);
} else {
config = loadOrCreateConfig(appName);
}
const serviceName = `${appName}-service-${domain.uniqueConfigKey}`;
const routerName = `${appName}-router-${domain.uniqueConfigKey}`;
const routerNameSecure = `${appName}-router-websecure-${domain.uniqueConfigKey}`;
config.http = config.http || { routers: {}, services: {} };
config.http.routers = config.http.routers || {};
config.http.services = config.http.services || {};
config.http.routers[routerName] = await createRouterConfig(
app,
domain,
"web",
);
if (domain.https) {
config.http.routers[routerNameSecure] = await createRouterConfig(
app,
domain,
"websecure",
);
} else {
delete config.http.routers[routerNameSecure];
}
config.http.services[serviceName] = createServiceConfig(appName, domain);
if (app.serverId) {
await writeTraefikConfigRemote(config, appName, app.serverId);
} else {
writeTraefikConfig(config, appName);
}
};
export const removeDomain = async (
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 routerSecureKey = `${appName}-router-websecure-${uniqueKey}`;
const serviceKey = `${appName}-service-${uniqueKey}`;
if (config.http?.routers?.[routerKey]) {
delete config.http.routers[routerKey];
}
if (config.http?.routers?.[routerSecureKey]) {
delete config.http.routers[routerSecureKey];
}
if (config.http?.services?.[serviceKey]) {
delete config.http.services[serviceKey];
}
// verify if is the last router if so we delete the router
if (
config?.http?.routers &&
Object.keys(config?.http?.routers).length === 0
) {
if (serverId) {
await removeTraefikConfigRemote(appName, serverId);
} else {
await removeTraefikConfig(appName);
}
} else {
if (serverId) {
await writeTraefikConfigRemote(config, appName, serverId);
} else {
writeTraefikConfig(config, appName);
}
}
};
export const createRouterConfig = async (
app: ApplicationNested,
domain: Domain,
entryPoint: "web" | "websecure",
) => {
const { appName, redirects, security } = app;
const { certificateType } = domain;
const { host, path, https, uniqueConfigKey } = domain;
const routerConfig: HttpRouter = {
rule: `Host(\`${host}\`)${path !== null && path !== "/" ? ` && PathPrefix(\`${path}\`)` : ""}`,
service: `${appName}-service-${uniqueConfigKey}`,
middlewares: [],
entryPoints: [entryPoint],
};
if (entryPoint === "web" && https) {
routerConfig.middlewares = ["redirect-to-https"];
}
if ((entryPoint === "websecure" && https) || !https) {
// redirects
for (const redirect of redirects) {
const middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`;
routerConfig.middlewares?.push(middlewareName);
}
// security
if (security.length > 0) {
const middlewareName = `auth-${appName}`;
routerConfig.middlewares?.push(middlewareName);
}
}
if (entryPoint === "websecure") {
if (certificateType === "letsencrypt") {
routerConfig.tls = { certResolver: "letsencrypt" };
} else if (certificateType === "none") {
routerConfig.tls = undefined;
}
}
return routerConfig;
};

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,132 @@
import type { Redirect } from "@/server/services/redirect";
import type { ApplicationNested } from "../builders";
import {
loadOrCreateConfig,
loadOrCreateConfigRemote,
writeTraefikConfig,
writeTraefikConfigRemote,
} from "./application";
import type { FileConfig } from "./file-types";
import {
addMiddleware,
deleteMiddleware,
loadMiddlewares,
loadRemoteMiddlewares,
writeMiddleware,
} from "./middleware";
export const updateRedirectMiddleware = async (
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}`;
if (config?.http?.middlewares?.[middlewareName]) {
config.http.middlewares[middlewareName] = {
redirectRegex: {
regex: data.regex,
replacement: data.replacement,
permanent: data.permanent,
},
};
}
if (serverId) {
await writeTraefikConfigRemote(config, "middlewares", serverId);
} else {
writeMiddleware(config);
}
};
export const createRedirectMiddleware = async (
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 newMiddleware = {
[middlewareName]: {
redirectRegex: {
regex: data.regex,
replacement: data.replacement,
permanent: data.permanent,
},
},
};
if (config?.http) {
config.http.middlewares = {
...config.http.middlewares,
...newMiddleware,
};
}
let appConfig: FileConfig;
if (serverId) {
appConfig = await loadOrCreateConfigRemote(serverId, appName);
} else {
appConfig = loadOrCreateConfig(appName);
}
addMiddleware(appConfig, middlewareName);
if (serverId) {
await writeTraefikConfigRemote(config, "middlewares", serverId);
await writeTraefikConfigRemote(appConfig, appName, serverId);
} else {
writeMiddleware(config);
writeTraefikConfig(appConfig, appName);
}
};
export const removeRedirectMiddleware = async (
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}`;
if (config?.http?.middlewares?.[middlewareName]) {
delete config.http.middlewares[middlewareName];
}
let appConfig: FileConfig;
if (serverId) {
appConfig = await loadOrCreateConfigRemote(serverId, appName);
} else {
appConfig = loadOrCreateConfig(appName);
}
deleteMiddleware(appConfig, middlewareName);
if (serverId) {
await writeTraefikConfigRemote(config, "middlewares", serverId);
await writeTraefikConfigRemote(appConfig, appName, serverId);
} else {
writeTraefikConfig(appConfig, appName);
writeMiddleware(config);
}
};

View File

@@ -0,0 +1,75 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { paths } from "@/server/constants";
import type { Registry } from "@/server/services/registry";
import { dump, load } from "js-yaml";
import { removeDirectoryIfExistsContent } from "../filesystem/directory";
import type { FileConfig, HttpRouter } from "./file-types";
export const manageRegistry = async (registry: Registry) => {
const { REGISTRY_PATH } = paths();
if (!existsSync(REGISTRY_PATH)) {
mkdirSync(REGISTRY_PATH, { recursive: true });
}
const appName = "dokploy-registry";
const config: FileConfig = loadOrCreateConfig();
const serviceName = `${appName}-service`;
const routerName = `${appName}-router`;
config.http = config.http || { routers: {}, services: {} };
config.http.routers = config.http.routers || {};
config.http.services = config.http.services || {};
config.http.routers[routerName] = await createRegistryRouterConfig(registry);
config.http.services[serviceName] = {
loadBalancer: {
servers: [{ url: `http://${appName}:5000` }],
passHostHeader: true,
},
};
const yamlConfig = dump(config);
const configFile = join(REGISTRY_PATH, "registry.yml");
writeFileSync(configFile, yamlConfig);
};
export const removeSelfHostedRegistry = async () => {
const { REGISTRY_PATH } = paths();
await removeDirectoryIfExistsContent(REGISTRY_PATH);
};
const createRegistryRouterConfig = async (registry: Registry) => {
const { registryUrl } = registry;
const routerConfig: HttpRouter = {
rule: `Host(\`${registryUrl}\`)`,
service: "dokploy-registry-service",
middlewares: ["redirect-to-https"],
entryPoints: [
"web",
...(process.env.NODE_ENV === "production" ? ["websecure"] : []),
],
...(process.env.NODE_ENV === "production"
? {
tls: { certResolver: "letsencrypt" },
}
: {}),
};
return routerConfig;
};
const loadOrCreateConfig = (): FileConfig => {
const { REGISTRY_PATH } = paths();
const configPath = join(REGISTRY_PATH, "registry.yml");
if (existsSync(configPath)) {
const yamlStr = readFileSync(configPath, "utf8");
const parsedConfig = (load(yamlStr) as FileConfig) || {
http: { routers: {}, services: {} },
};
return parsedConfig;
}
return { http: { routers: {}, services: {} } };
};

View File

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

View File

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

View File

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