123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372 |
- 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";
- import {
- COMBINED_TERMINAL_COLS,
- COMBINED_TERMINAL_ROWS,
- CREATED_FILE,
- CREATED_STACK,
- EXITED, getCombinedTerminalName,
- getComposeTerminalName, getContainerExecTerminalName,
- PROGRESS_TERMINAL_ROWS,
- RUNNING, TERMINAL_ROWS,
- UNKNOWN
- } from "./util-common";
- import { InteractiveTerminal, Terminal } from "./terminal";
- import childProcess from "child_process";
- export class Stack {
- name: string;
- protected _status: number = UNKNOWN;
- protected _composeYAML?: string;
- protected _configFilePath?: string;
- protected _composeFileName: string = "compose.yaml";
- protected server: DockgeServer;
- protected combinedTerminal? : Terminal;
- protected static managedStackList: Map<string, Stack> = new Map();
- constructor(server : DockgeServer, name : string, composeYAML? : string) {
- this.name = name;
- this.server = server;
- this._composeYAML = composeYAML;
- // Check if compose file name is different from compose.yaml
- if (fs.existsSync(path.join(this.path, "compose.yml"))){
- this._composeFileName = "compose.yml";
- } else if (fs.existsSync(path.join(this.path, "docker-compose.yml"))){
- this._composeFileName = "docker-compose.yml";
- } else if (fs.existsSync(path.join(this.path, "docker-compose.yaml"))){
- this._composeFileName = "docker-compose.yaml";
- }
- }
- toJSON() : object {
- let obj = this.toSimpleJSON();
- return {
- ...obj,
- composeYAML: this.composeYAML,
- };
- }
- toSimpleJSON() : object {
- return {
- name: this.name,
- status: this._status,
- tags: [],
- isManagedByDockge: this.isManagedByDockge,
- composeFileName: this._composeFileName,
- };
- }
- /**
- * 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());
- }
- get isManagedByDockge() : boolean {
- return fs.existsSync(this.path) && fs.statSync(this.path).isDirectory();
- }
- get status() : number {
- return this._status;
- }
- validate() {
- // Check name, allows [a-z][0-9] _ - only
- if (!this.name.match(/^[a-z0-9_-]+$/)) {
- throw new ValidationError("Stack name can only contain [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, this._composeFileName), "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, this._composeFileName), this.composeYAML);
- }
- async deploy(socket? : DockgeSocket) : Promise<number> {
- 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 deploy, please check the terminal output for more information.");
- }
- return exitCode;
- }
- async delete(socket?: DockgeSocket) : Promise<number> {
- const terminalName = getComposeTerminalName(this.name);
- let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down", "--remove-orphans", "all" ], this.path);
- if (exitCode !== 0) {
- throw new Error("Failed to delete, please check the terminal output for more information.");
- }
- // Remove the stack folder
- fs.rmSync(this.path, {
- recursive: true,
- force: true
- });
- return exitCode;
- }
- static getStackList(server : DockgeServer, useCacheForManaged = false) : Map<string, Stack> {
- let stacksDir = server.stacksDir;
- 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 {
- // Check if it is a directory
- let stat = fs.statSync(path.join(stacksDir, filename));
- if (!stat.isDirectory()) {
- continue;
- }
- 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}`);
- }
- }
- // 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());
- for (let composeStack of composeList) {
- // Skip the dockge stack
- // TODO: Could be self managed?
- if (composeStack.Name === "dockge") {
- continue;
- }
- let stack = stackList.get(composeStack.Name);
- // 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);
- }
- stack._status = this.statusConvert(composeStack.Status);
- stack._configFilePath = composeStack.ConfigFiles;
- }
- return stackList;
- }
- /**
- * 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;
- }
- /**
- * Convert the status string from `docker compose ls` to the status number
- * @param status
- */
- 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;
- }
- }
- static getStack(server: DockgeServer, stackName: string) : Stack {
- let dir = path.join(server.stacksDir, stackName);
- 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");
- }
- }
- let stack = new Stack(server, stackName);
- stack._status = UNKNOWN;
- stack._configFilePath = path.resolve(dir);
- return stack;
- }
- 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 joinCombinedTerminal(socket: DockgeSocket) {
- const terminalName = getCombinedTerminalName(this.name);
- const terminal = Terminal.getOrCreateTerminal(this.server, terminalName, "docker", [ "compose", "logs", "-f", "--tail", "100" ], this.path);
- terminal.rows = COMBINED_TERMINAL_ROWS;
- terminal.cols = COMBINED_TERMINAL_COLS;
- terminal.join(socket);
- terminal.start();
- }
- async joinContainerTerminal(socket: DockgeSocket, serviceName: string, shell : string = "sh", index: number = 0) {
- const terminalName = getContainerExecTerminalName(this.name, serviceName, index);
- let terminal = Terminal.getTerminal(terminalName);
- if (!terminal) {
- terminal = new InteractiveTerminal(this.server, terminalName, "docker", [ "compose", "exec", serviceName, shell ], this.path);
- terminal.rows = TERMINAL_ROWS;
- log.debug("joinContainerTerminal", "Terminal created");
- }
- terminal.join(socket);
- terminal.start();
- }
- async getServiceStatusList() {
- let statusList = new Map<string, number>();
- let res = childProcess.execSync("docker compose ps --format json", {
- cwd: this.path,
- });
- let lines = res.toString().split("\n");
- for (let line of lines) {
- try {
- let obj = JSON.parse(line);
- statusList.set(obj.Service, obj.State);
- } catch (e) {
- }
- }
- return statusList;
- }
- }
|