diff --git a/README.md b/README.md index 151d1fd..099a21f 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ A fancy, easy-to-use and reactive docker stack (`docker-compose.yml`) manager. ## Motivations -- I have been using Portainer for some time, but I am sometimes not satisfied with it. For example, sometimes when I deploy a stack, the loading icon keeps spinning for a few minutes without progress. And sometimes error messages are not clear. +- I have been using Portainer for some time, but for the stack management, I am sometimes not satisfied with it. For example, sometimes when I try to deploy a stack, the loading icon keeps spinning for a few minutes without progress. And sometimes error messages are not clear. - Try to develop with ES Module + TypeScript (Originally, I planned to use Deno or Bun.js, but they do not support for arm64, so I stepped back to Node.js) @@ -34,8 +34,6 @@ If you love this project, please consider giving this project a ⭐. - Get app icons - Switch Docker context - Support Dockerfile and build -- Zero-config private docker registry - Support Docker swarm - diff --git a/backend/socket-handlers/docker-socket-handler.ts b/backend/socket-handlers/docker-socket-handler.ts index 92e4b93..f31b6b8 100644 --- a/backend/socket-handlers/docker-socket-handler.ts +++ b/backend/socket-handlers/docker-socket-handler.ts @@ -71,7 +71,7 @@ export class DockerSocketHandler extends SocketHandler { const stack = Stack.getStack(server, stackName); - stack.startCombinedTerminal(socket); + stack.joinCombinedTerminal(socket); callback({ ok: true, @@ -113,7 +113,7 @@ export class DockerSocketHandler extends SocketHandler { }); server.sendStackList(); - stack.startCombinedTerminal(socket); + stack.joinCombinedTerminal(socket); } catch (e) { callbackError(e, callback); diff --git a/backend/socket-handlers/terminal-socket-handler.ts b/backend/socket-handlers/terminal-socket-handler.ts index 6162e7a..2b4fd9e 100644 --- a/backend/socket-handlers/terminal-socket-handler.ts +++ b/backend/socket-handlers/terminal-socket-handler.ts @@ -13,6 +13,7 @@ import { PROGRESS_TERMINAL_ROWS } from "../util-common"; import { InteractiveTerminal, MainTerminal, Terminal } from "../terminal"; +import { Stack } from "../stack"; export class TerminalSocketHandler extends SocketHandler { create(socket : DockgeSocket, server : DockgeServer) { @@ -31,6 +32,7 @@ export class TerminalSocketHandler extends SocketHandler { let terminal = Terminal.getTerminal(terminalName); if (terminal instanceof InteractiveTerminal) { + log.debug("terminalInput", "Terminal found, writing to terminal."); terminal.write(cmd); } else { throw new Error("Terminal not found or it is not a Interactive Terminal."); @@ -89,17 +91,12 @@ export class TerminalSocketHandler extends SocketHandler { throw new ValidationError("Service name must be a string."); } - const terminalName = getContainerExecTerminalName(stackName, serviceName, 0); - let terminal = Terminal.getTerminal(terminalName); + log.debug("interactiveTerminal", "Stack name: " + stackName); + log.debug("interactiveTerminal", "Service name: " + serviceName); - if (!terminal) { - terminal = new InteractiveTerminal(server, terminalName); - terminal.rows = 50; - log.debug("deployStack", "Terminal created"); - } - - terminal.join(socket); - terminal.start(); + // Get stack + const stack = Stack.getStack(server, stackName); + stack.joinContainerTerminal(socket, serviceName); callback({ ok: true, diff --git a/backend/stack.ts b/backend/stack.ts index 7db7c4f..293739c 100644 --- a/backend/stack.ts +++ b/backend/stack.ts @@ -10,12 +10,12 @@ import { CREATED_FILE, CREATED_STACK, EXITED, getCombinedTerminalName, - getComposeTerminalName, + getComposeTerminalName, getContainerExecTerminalName, PROGRESS_TERMINAL_ROWS, - RUNNING, + RUNNING, TERMINAL_ROWS, UNKNOWN } from "./util-common"; -import { Terminal } from "./terminal"; +import { InteractiveTerminal, Terminal } from "./terminal"; import childProcess from "child_process"; export class Stack { @@ -144,7 +144,7 @@ export class Stack { async deploy(socket? : DockgeSocket) : Promise { const terminalName = getComposeTerminalName(this.name); - let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "up", "-d", "--remove-orphans" ], this.path); + 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."); } @@ -153,7 +153,7 @@ export class Stack { async delete(socket?: DockgeSocket) : Promise { const terminalName = getComposeTerminalName(this.name); - let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "down", "--remove-orphans", "--rmi", "all" ], this.path); + 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."); } @@ -270,7 +270,7 @@ export class 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); + 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."); } @@ -279,7 +279,7 @@ export class Stack { async stop(socket: DockgeSocket) : Promise { const terminalName = getComposeTerminalName(this.name); - let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "stop" ], this.path); + 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."); } @@ -288,7 +288,7 @@ export class Stack { async restart(socket: DockgeSocket) : Promise { const terminalName = getComposeTerminalName(this.name); - let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "restart" ], this.path); + 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."); } @@ -297,23 +297,37 @@ export class Stack { async update(socket: DockgeSocket) { const terminalName = getComposeTerminalName(this.name); - let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker-compose", [ "pull" ], this.path); + 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); + 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) { + async joinCombinedTerminal(socket: DockgeSocket) { const terminalName = getCombinedTerminalName(this.name); - const terminal = Terminal.getOrCreateTerminal(this.server, terminalName, "docker-compose", [ "logs", "-f" ], this.path); + 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(); } + + async joinContainerTerminal(socket: DockgeSocket, serviceName: string, 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, "bash" ], this.path); + terminal.rows = TERMINAL_ROWS; + log.debug("deployStack", "Terminal created"); + } + + terminal.join(socket); + terminal.start(); + } } diff --git a/backend/terminal.ts b/backend/terminal.ts index 352372e..63324f1 100644 --- a/backend/terminal.ts +++ b/backend/terminal.ts @@ -51,7 +51,11 @@ export class Terminal { set rows(rows : number) { this._rows = rows; - this.ptyProcess?.resize(this.cols, this.rows); + try { + this.ptyProcess?.resize(this.cols, this.rows); + } catch (e) { + log.debug("Terminal", "Failed to resize terminal: " + e.message); + } } get cols() { @@ -60,7 +64,11 @@ export class Terminal { set cols(cols : number) { this._cols = cols; - this.ptyProcess?.resize(this.cols, this.rows); + try { + this.ptyProcess?.resize(this.cols, this.rows); + } catch (e) { + log.debug("Terminal", "Failed to resize terminal: " + e.message); + } } public start() { @@ -133,11 +141,16 @@ export class Terminal { this._ptyProcess?.kill(); } + /** + * Get a running and non-exited terminal + * @param name + */ public static getTerminal(name : string) : Terminal | undefined { return Terminal.terminalMap.get(name); } public static getOrCreateTerminal(server : DockgeServer, name : string, file : string, args : string | string[], cwd : string) : Terminal { + // Since exited terminal will be removed from the map, it is safe to get the terminal from the map let terminal = Terminal.getTerminal(name); if (!terminal) { terminal = new Terminal(server, name, file, args, cwd); diff --git a/backend/util-common.ts b/backend/util-common.ts index 637cf8c..ec1eef5 100644 --- a/backend/util-common.ts +++ b/backend/util-common.ts @@ -84,7 +84,7 @@ export const TERMINAL_COLS = 105; export const TERMINAL_ROWS = 10; export const PROGRESS_TERMINAL_ROWS = 8; -export const COMBINED_TERMINAL_COLS = 50; +export const COMBINED_TERMINAL_COLS = 56; export const COMBINED_TERMINAL_ROWS = 15; export const ERROR_TYPE_VALIDATION = 1; diff --git a/frontend/src/components/Container.vue b/frontend/src/components/Container.vue index affa592..716fe91 100644 --- a/frontend/src/components/Container.vue +++ b/frontend/src/components/Container.vue @@ -14,7 +14,7 @@
- Terminal + Bash
@@ -160,12 +160,17 @@ export default defineComponent({ return { name: "containerTerminal", params: { + stackName: this.stackName, serviceName: this.name, - type: "logs", + type: "bash", }, }; }, + stackName() { + return this.$parent.$parent.stack.name; + }, + service() { return this.jsonObject.services[this.name]; }, diff --git a/frontend/src/components/Terminal.vue b/frontend/src/components/Terminal.vue index 269b82d..5b6bde0 100644 --- a/frontend/src/components/Terminal.vue +++ b/frontend/src/components/Terminal.vue @@ -20,7 +20,17 @@ export default { props: { name: { type: String, - required: true, + require: true, + }, + + // Require if mode is interactive + stackName: { + type: String, + }, + + // Require if mode is interactive + serviceName: { + type: String, }, rows: { @@ -99,7 +109,8 @@ export default { } }); } else if (this.mode === "interactive") { - this.$root.getSocket().emit("interactiveTerminal", this.name, (res) => { + console.debug("Create Interactive terminal:", this.name); + this.$root.getSocket().emit("interactiveTerminal", this.stackName, this.serviceName, (res) => { if (!res.ok) { this.$root.toastRes(res); } @@ -184,7 +195,13 @@ export default { }, interactiveTerminalConfig() { - + this.terminal.onKey(e => { + this.$root.getSocket().emit("terminalInput", this.name, e.key, (res) => { + if (!res.ok) { + this.$root.toastRes(res); + } + }); + }); } } }; @@ -193,6 +210,7 @@ export default { diff --git a/frontend/src/mixins/socket.ts b/frontend/src/mixins/socket.ts index 7e76565..83666f0 100644 --- a/frontend/src/mixins/socket.ts +++ b/frontend/src/mixins/socket.ts @@ -141,7 +141,7 @@ export default defineComponent({ socket.on("terminalWrite", (terminalName, data) => { const terminal = terminalMap.get(terminalName); if (!terminal) { - console.error("Terminal not found: " + terminalName); + //console.error("Terminal not found: " + terminalName); return; } terminal.write(data); diff --git a/frontend/src/pages/Compose.vue b/frontend/src/pages/Compose.vue index 6075a90..e1122f9 100644 --- a/frontend/src/pages/Compose.vue +++ b/frontend/src/pages/Compose.vue @@ -104,7 +104,7 @@
-

Logs

+

Terminal

-

{{ $tc("network", 2) }}

-
- -
+
+

{{ $tc("network", 2) }}

+
+ +
-

{{ $tc("volume", 2) }}

-
+

{{ $tc("volume", 2) }}

+
+
- , - -

-
- - +