dockge/backend/stack.ts

320 lines
10 KiB
TypeScript
Raw Normal View History

2023-10-26 07:23:45 +02:00
import { DockgeServer } from "./dockge-server";
import fs from "fs";
import { log } from "./log";
import yaml from "yaml";
import { DockgeSocket, ValidationError } from "./util-server";
import path from "path";
2023-10-29 08:25:52 +01:00
import {
2023-11-05 18:18:02 +01:00
COMBINED_TERMINAL_COLS,
COMBINED_TERMINAL_ROWS,
2023-10-29 08:25:52 +01:00
CREATED_FILE,
CREATED_STACK,
2023-11-05 18:18:02 +01:00
EXITED, getCombinedTerminalName,
2023-10-29 08:25:52 +01:00
getComposeTerminalName,
PROGRESS_TERMINAL_ROWS,
RUNNING,
UNKNOWN
} from "./util-common";
2023-10-26 07:23:45 +02:00
import { Terminal } from "./terminal";
2023-10-29 08:25:52 +01:00
import childProcess from "child_process";
2023-10-26 07:23:45 +02:00
export class Stack {
name: string;
2023-10-29 08:25:52 +01:00
protected _status: number = UNKNOWN;
2023-10-26 07:23:45 +02:00
protected _composeYAML?: string;
2023-10-29 08:25:52 +01:00
protected _configFilePath?: string;
2023-10-26 07:23:45 +02:00
protected server: DockgeServer;
2023-11-05 18:18:02 +01:00
protected combinedTerminal? : Terminal;
2023-10-29 08:25:52 +01:00
protected static managedStackList: Map<string, Stack> = new Map();
2023-10-26 07:23:45 +02:00
constructor(server : DockgeServer, name : string, composeYAML? : string) {
this.name = name;
this.server = server;
this._composeYAML = composeYAML;
}
toJSON() : object {
let obj = this.toSimpleJSON();
return {
...obj,
composeYAML: this.composeYAML,
};
}
toSimpleJSON() : object {
return {
name: this.name,
2023-10-29 08:25:52 +01:00
status: this._status,
2023-10-26 07:23:45 +02:00
tags: [],
2023-11-05 18:18:02 +01:00
isManagedByDockge: this.isManagedByDockge,
2023-10-26 07:23:45 +02:00
};
}
2023-11-05 18:18:02 +01:00
/**
* Get the status of the stack from `docker compose ps --format json`
*/
ps() : object {
let res = childProcess.execSync("docker compose ps --format json", {
cwd: this.path
});
return JSON.parse(res.toString());
}
2023-10-29 08:25:52 +01:00
get isManagedByDockge() : boolean {
if (this._configFilePath) {
return this._configFilePath.startsWith(this.server.stackDirFullPath) && fs.existsSync(this.path) && fs.statSync(this.path).isDirectory();
} else {
return false;
}
}
get status() : number {
return this._status;
}
2023-10-26 07:23:45 +02:00
validate() {
// Check name, allows [a-z][A-Z][0-9] _ - only
if (!this.name.match(/^[a-zA-Z0-9_-]+$/)) {
throw new ValidationError("Stack name can only contain [a-z][A-Z][0-9] _ - only");
}
// Check YAML format
yaml.parse(this.composeYAML);
}
get composeYAML() : string {
if (this._composeYAML === undefined) {
try {
this._composeYAML = fs.readFileSync(path.join(this.path, "compose.yaml"), "utf-8");
} catch (e) {
this._composeYAML = "";
}
}
return this._composeYAML;
}
get path() : string {
return path.join(this.server.stacksDir, this.name);
}
get fullPath() : string {
let dir = this.path;
// Compose up via node-pty
let fullPathDir;
// if dir is relative, make it absolute
if (!path.isAbsolute(dir)) {
fullPathDir = path.join(process.cwd(), dir);
} else {
fullPathDir = dir;
}
return fullPathDir;
}
/**
* Save the stack to the disk
* @param isAdd
*/
save(isAdd : boolean) {
this.validate();
let dir = this.path;
// Check if the name is used if isAdd
if (isAdd) {
if (fs.existsSync(dir)) {
throw new ValidationError("Stack name already exists");
}
// Create the stack folder
fs.mkdirSync(dir);
} else {
if (!fs.existsSync(dir)) {
throw new ValidationError("Stack not found");
}
}
// Write or overwrite the compose.yaml
fs.writeFileSync(path.join(dir, "compose.yaml"), this.composeYAML);
}
2023-11-05 18:18:02 +01:00
async deploy(socket? : DockgeSocket) : Promise<number> {
2023-10-26 07:23:45 +02:00
const terminalName = getComposeTerminalName(this.name);
2023-11-05 18:18:02 +01:00
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "up", "-d", "--remove-orphans" ], this.path);
if (exitCode !== 0) {
throw new Error("Failed to deploy, please check the terminal output for more information.");
2023-10-26 07:23:45 +02:00
}
2023-11-05 18:18:02 +01:00
return exitCode;
2023-10-26 07:23:45 +02:00
}
2023-11-05 18:18:02 +01:00
async delete(socket?: DockgeSocket) : Promise<number> {
2023-10-29 08:25:52 +01:00
const terminalName = getComposeTerminalName(this.name);
2023-11-05 18:18:02 +01:00
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "down", "--remove-orphans", "--rmi", "all" ], this.path);
if (exitCode !== 0) {
throw new Error("Failed to delete, please check the terminal output for more information.");
2023-10-29 08:25:52 +01:00
}
2023-11-05 18:18:02 +01:00
// Remove the stack folder
fs.rmSync(this.path, {
recursive: true,
force: true
2023-10-29 08:25:52 +01:00
});
2023-11-05 18:18:02 +01:00
return exitCode;
2023-10-29 08:25:52 +01:00
}
static getStackList(server : DockgeServer, useCacheForManaged = false) : Map<string, Stack> {
2023-10-26 07:23:45 +02:00
let stacksDir = server.stacksDir;
2023-10-29 08:25:52 +01:00
let stackList : Map<string, Stack>;
if (useCacheForManaged && this.managedStackList.size > 0) {
stackList = this.managedStackList;
} else {
stackList = new Map<string, Stack>();
// Scan the stacks directory, and get the stack list
let filenameList = fs.readdirSync(stacksDir);
for (let filename of filenameList) {
try {
let stack = this.getStack(server, filename);
stack._status = CREATED_FILE;
stackList.set(filename, stack);
} catch (e) {
log.warn("getStackList", `Failed to get stack ${filename}, error: ${e.message}`);
}
}
2023-10-26 07:23:45 +02:00
2023-10-29 08:25:52 +01:00
// Cache by copying
this.managedStackList = new Map(stackList);
}
// Also get the list from `docker compose ls --all --format json`
let res = childProcess.execSync("docker compose ls --all --format json");
let composeList = JSON.parse(res.toString());
2023-10-26 07:23:45 +02:00
2023-10-29 08:25:52 +01:00
for (let composeStack of composeList) {
let stack = stackList.get(composeStack.Name);
2023-10-26 07:23:45 +02:00
2023-10-29 08:25:52 +01:00
// This stack probably is not managed by Dockge, but we still want to show it
if (!stack) {
stack = new Stack(server, composeStack.Name);
stackList.set(composeStack.Name, stack);
2023-10-26 07:23:45 +02:00
}
2023-10-29 08:25:52 +01:00
stack._status = this.statusConvert(composeStack.Status);
stack._configFilePath = composeStack.ConfigFiles;
2023-10-26 07:23:45 +02:00
}
2023-10-29 08:25:52 +01:00
2023-10-26 07:23:45 +02:00
return stackList;
}
2023-10-29 08:25:52 +01:00
/**
* Get the status list, it will be used to update the status of the stacks
* Not all status will be returned, only the stack that is deployed or created to `docker compose` will be returned
*/
static getStatusList() : Map<string, number> {
let statusList = new Map<string, number>();
let res = childProcess.execSync("docker compose ls --all --format json");
let composeList = JSON.parse(res.toString());
for (let composeStack of composeList) {
statusList.set(composeStack.Name, this.statusConvert(composeStack.Status));
}
return statusList;
}
2023-11-05 18:18:02 +01:00
/**
* Convert the status string from `docker compose ls` to the status number
* @param status
*/
2023-10-29 08:25:52 +01:00
static statusConvert(status : string) : number {
if (status.startsWith("created")) {
return CREATED_STACK;
} else if (status.startsWith("running")) {
return RUNNING;
} else if (status.startsWith("exited")) {
return EXITED;
} else {
return UNKNOWN;
}
}
2023-10-26 07:23:45 +02:00
static getStack(server: DockgeServer, stackName: string) : Stack {
let dir = path.join(server.stacksDir, stackName);
2023-10-29 08:25:52 +01:00
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
// Maybe it is a stack managed by docker compose directly
let stackList = this.getStackList(server);
let stack = stackList.get(stackName);
if (stack) {
return stack;
} else {
// Really not found
throw new ValidationError("Stack not found");
}
2023-10-26 07:23:45 +02:00
}
2023-10-29 08:25:52 +01:00
let stack = new Stack(server, stackName);
stack._status = UNKNOWN;
stack._configFilePath = path.resolve(dir);
return stack;
2023-10-26 07:23:45 +02:00
}
2023-11-05 18:18:02 +01:00
async start(socket: DockgeSocket) {
const terminalName = getComposeTerminalName(this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "up", "-d", "--remove-orphans" ], this.path);
if (exitCode !== 0) {
throw new Error("Failed to start, please check the terminal output for more information.");
}
return exitCode;
}
async stop(socket: DockgeSocket) : Promise<number> {
const terminalName = getComposeTerminalName(this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "stop" ], this.path);
if (exitCode !== 0) {
throw new Error("Failed to stop, please check the terminal output for more information.");
}
return exitCode;
}
async restart(socket: DockgeSocket) : Promise<number> {
const terminalName = getComposeTerminalName(this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "restart" ], this.path);
if (exitCode !== 0) {
throw new Error("Failed to restart, please check the terminal output for more information.");
}
return exitCode;
}
async update(socket: DockgeSocket) {
const terminalName = getComposeTerminalName(this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "pull" ], this.path);
if (exitCode !== 0) {
throw new Error("Failed to pull, please check the terminal output for more information.");
}
exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "up", "-d", "--remove-orphans" ], this.path);
if (exitCode !== 0) {
throw new Error("Failed to restart, please check the terminal output for more information.");
}
return exitCode;
}
async startCombinedTerminal(socket: DockgeSocket) {
const terminalName = getCombinedTerminalName(this.name);
const terminal = Terminal.getOrCreateTerminal(this.server, terminalName, "docker-compose", [ "logs", "-f" ], this.path);
terminal.rows = COMBINED_TERMINAL_ROWS;
terminal.cols = COMBINED_TERMINAL_COLS;
terminal.join(socket);
terminal.start();
}
2023-10-26 07:23:45 +02:00
}