feat: add application and databases external servers

This commit is contained in:
Mauricio Siu
2024-09-09 15:58:58 -06:00
parent 5afe1645a0
commit 95f75fdccb
16 changed files with 385 additions and 233 deletions

View File

@@ -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) => {
try {
const filter = {

View File

@@ -1,3 +1,39 @@
import { exec } from "node:child_process";
import util from "node:util";
import { connectSSH } from "../servers/connection";
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();
});
});
});
};

View File

@@ -1,41 +1,10 @@
import { findServerById } from "@/server/api/services/server";
import { readSSHKey } from "../filesystem/ssh";
import { Client } from "ssh2";
import { execAsyncRemote } from "../process/execAsync";
export const executeCommand = async (serverId: string, command: string) => {
const server = await findServerById(serverId);
if (!server.sshKeyId) return;
const keys = await readSSHKey(server.sshKeyId);
const client = new Client();
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,
});
});
try {
await execAsyncRemote(serverId, command);
} catch (err) {
console.error("Execution error:", err);
throw err;
}
};

View 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,
});
});
};

View File

@@ -1,12 +1,10 @@
import fs, { writeFileSync } from "node:fs";
import path from "node:path";
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 type { FileConfig, HttpLoadBalancerService } from "./file-types";
import { findServerById } from "@/server/api/services/server";
import { Client } from "ssh2";
import { readSSHKey } from "../filesystem/ssh";
import { execAsyncRemote } from "../process/execAsync";
export const createTraefikConfig = (appName: string) => {
const defaultPort = 3000;
@@ -58,6 +56,16 @@ export const removeTraefikConfig = async (appName: string) => {
} 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 => {
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
if (fs.existsSync(configPath)) {
@@ -74,56 +82,20 @@ 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 fileConfig: FileConfig = { http: { routers: {}, services: {} } };
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);
try {
const { stdout } = await execAsyncRemote(serverId, `cat ${configPath}`);
resolve({ http: { routers: {}, services: {} } });
if (!stdout) return fileConfig;
// 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,
});
});
const parsedConfig = (load(stdout) as FileConfig) || {
http: { routers: {}, services: {} },
};
return parsedConfig;
} catch (err) {
return fileConfig;
}
};
export const readConfig = (appName: string) => {
@@ -135,51 +107,15 @@ export const readConfig = (appName: string) => {
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`);
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,
});
});
try {
const { stdout } = await execAsyncRemote(serverId, `cat ${configPath}`);
if (!stdout) return null;
return stdout;
} catch (err) {
return null;
}
};
export const readMonitoringConfig = () => {
@@ -228,13 +164,26 @@ export const writeTraefikConfig = (
try {
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
const yamlStr = dump(traefikConfig);
console.log(yamlStr);
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 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,

View File

@@ -5,13 +5,11 @@ import {
loadOrCreateConfig,
loadOrCreateConfigRemote,
removeTraefikConfig,
removeTraefikConfigRemote,
writeTraefikConfig,
writeTraefikConfigRemote,
} from "./application";
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) => {
const { appName } = app;
@@ -49,23 +47,24 @@ export const manageDomain = async (app: ApplicationNested, domain: Domain) => {
config.http.services[serviceName] = createServiceConfig(appName, domain);
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);
await writeTraefikConfigRemote(config, appName, app.serverId);
} else {
writeTraefikConfig(config, appName);
}
};
export const removeDomain = async (appName: string, uniqueKey: number) => {
const config: FileConfig = loadOrCreateConfig(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}`;
@@ -86,9 +85,17 @@ export const removeDomain = async (appName: string, uniqueKey: number) => {
config?.http?.routers &&
Object.keys(config?.http?.routers).length === 0
) {
await removeTraefikConfig(appName);
if (serverId) {
await removeTraefikConfigRemote(appName, serverId);
} else {
await removeTraefikConfig(appName);
}
} else {
writeTraefikConfig(config, appName);
if (serverId) {
await writeTraefikConfigRemote(config, appName, serverId);
} else {
writeTraefikConfig(config, appName);
}
}
};

View File

@@ -4,6 +4,7 @@ import { DYNAMIC_TRAEFIK_PATH } from "@/server/constants";
import { dump, load } from "js-yaml";
import type { ApplicationNested } from "../builders";
import type { FileConfig } from "./file-types";
import { execAsyncRemote } from "../process/execAsync";
export const addMiddleware = (config: FileConfig, middlewareName: string) => {
if (config.http?.routers) {
@@ -72,6 +73,25 @@ export const loadMiddlewares = <T>() => {
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) => {
const configPath = join(DYNAMIC_TRAEFIK_PATH, "middlewares.yml");
const newYamlContent = dump(config);

View File

@@ -1,15 +1,32 @@
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 {
addMiddleware,
deleteMiddleware,
loadMiddlewares,
loadRemoteMiddlewares,
writeMiddleware,
} from "./middleware";
import type { ApplicationNested } from "../builders";
export const updateRedirectMiddleware = (appName: string, data: Redirect) => {
const config = loadMiddlewares<FileConfig>();
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]) {
@@ -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) => {
const config = loadMiddlewares<FileConfig>();
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]: {
@@ -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);
writeTraefikConfig(appConfig, appName);
writeMiddleware(config);
if (serverId) {
await writeTraefikConfigRemote(config, "middlewares", serverId);
await writeTraefikConfigRemote(appConfig, appName, serverId);
} else {
writeMiddleware(config);
writeTraefikConfig(appConfig, appName);
}
};
export const removeRedirectMiddleware = (appName: string, data: Redirect) => {
const config = loadMiddlewares<FileConfig>();
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];
}
const appConfig = loadOrCreateConfig(appName);
let appConfig: FileConfig;
if (serverId) {
appConfig = await loadOrCreateConfigRemote(serverId, appName);
} else {
appConfig = loadOrCreateConfig(appName);
}
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);
}
};

View File

@@ -1,6 +1,11 @@
import type { Security } from "@/server/api/services/security";
import * as bcrypt from "bcrypt";
import { loadOrCreateConfig, writeTraefikConfig } from "./application";
import {
loadOrCreateConfig,
loadOrCreateConfigRemote,
writeTraefikConfig,
writeTraefikConfigRemote,
} from "./application";
import type {
BasicAuthMiddleware,
FileConfig,
@@ -10,14 +15,23 @@ import {
addMiddleware,
deleteMiddleware,
loadMiddlewares,
loadRemoteMiddlewares,
writeMiddleware,
} from "./middleware";
import type { ApplicationNested } from "../builders";
export const createSecurityMiddleware = async (
appName: string,
application: ApplicationNested,
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 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);
writeTraefikConfig(appConfig, appName);
writeMiddleware(config);
if (serverId) {
await writeTraefikConfigRemote(config, "middlewares", serverId);
await writeTraefikConfigRemote(appConfig, appName, serverId);
} else {
writeTraefikConfig(appConfig, appName);
writeMiddleware(config);
}
};
export const removeSecurityMiddleware = (appName: string, data: Security) => {
const config = loadMiddlewares<FileConfig>();
const appConfig = loadOrCreateConfig(appName);
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) {
@@ -67,12 +106,20 @@ export const removeSecurityMiddleware = (appName: string, data: Security) => {
delete config.http.middlewares[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 = (