This commit is contained in:
Louis Lam 2023-11-06 15:15:55 +08:00
parent 314630724b
commit 2ed739b1b9
12 changed files with 114 additions and 104 deletions

View file

@ -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

View file

@ -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);

View file

@ -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,

View file

@ -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<number> {
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<number> {
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<number> {
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<number> {
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();
}
}

View file

@ -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);

View file

@ -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;

View file

@ -14,7 +14,7 @@
<div class="function">
<router-link v-if="!isEditMode" class="btn btn-normal" :to="terminalRouteLink">
<font-awesome-icon icon="terminal" />
Terminal
Bash
</router-link>
</div>
</div>
@ -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];
},

View file

@ -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 {
<style scoped lang="scss">
.main-terminal {
height: 100%;
overflow-x: scroll;
}
</style>

View file

@ -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);

View file

@ -104,7 +104,7 @@
<!-- Combined Terminal Output -->
<div v-show="!isEditMode">
<h4 class="mb-3">Logs</h4>
<h4 class="mb-3">Terminal</h4>
<Terminal
ref="combinedTerminal"
class="mb-3 terminal"
@ -135,13 +135,15 @@
{{ yamlError }}
</div>
<h4 class="mb-3">{{ $tc("network", 2) }}</h4>
<div class="shadow-box big-padding mb-3">
<NetworkInput />
</div>
<div v-if="isEditMode">
<h4 class="mb-3">{{ $tc("network", 2) }}</h4>
<div class="shadow-box big-padding mb-3">
<NetworkInput />
</div>
<h4 class="mb-3">{{ $tc("volume", 2) }}</h4>
<div class="shadow-box big-padding mb-3">
<h4 class="mb-3">{{ $tc("volume", 2) }}</h4>
<div class="shadow-box big-padding mb-3">
</div>
</div>
<!-- <div class="shadow-box big-padding mb-3">
@ -531,43 +533,6 @@ export default {
});
},
combineNetworks() {
let networks = this.jsonConfig.networks;
if (!networks) {
networks = {};
}
for (let serviceName in this.jsonConfig.services) {
let service = this.jsonConfig.services[serviceName];
let serviceNetworks = service.networks;
if (!networks) {
continue;
}
// If it is an array, it should be array of string
if (Array.isArray(serviceNetworks)) {
for (let n of serviceNetworks) {
console.log(n);
if (!n) {
continue;
}
if (!networks[n]) {
networks[n] = {};
}
}
} else if (typeof serviceNetworks === "object") {
}
}
console.debug(networks);
return networks;
}
}
};
</script>

View file

@ -1,37 +1,39 @@
<template>
<transition name="slide-fade" appear>
<div>
<h1 class="mb-3">Console</h1>
<h1 class="mb-3">Bash</h1>
<p>
Stack: {{ stackName }}<br />
Container: {{ serviceName }}
</p>
<div>
<p>
Allowed commands:
<template v-for="(command, index) in allowedCommandList" :key="command">
<code>{{ command }}</code>
<!-- No comma at the end -->
<span v-if="index !== allowedCommandList.length - 1">, </span>
</template>
</p>
</div>
<Terminal class="terminal" :rows="20" mode="mainTerminal" name="console"></Terminal>
<Terminal class="terminal" :rows="20" mode="interactive" :name="terminalName" :stack-name="stackName" :service-name="serviceName"></Terminal>
</div>
</transition>
</template>
<script>
import { allowedCommandList } from "../../../backend/util-common";
import { getContainerExecTerminalName } from "../../../backend/util-common";
export default {
components: {
},
data() {
return {
allowedCommandList,
};
},
computed: {
stackName() {
return this.$route.params.stackName;
},
serviceName() {
return this.$route.params.serviceName;
},
terminalName() {
return getContainerExecTerminalName(this.stackName, this.serviceName, 0);
}
},
mounted() {
},

View file

@ -39,13 +39,11 @@ const routes = [
name: "compose",
component: Compose,
props: true,
children: [
{
path: "/compose/:stackName/terminal/:serviceName/:type",
component: ContainerTerminal,
name: "containerTerminal",
},
]
},
{
path: "/terminal/:stackName/:serviceName/:type",
component: ContainerTerminal,
name: "containerTerminal",
},
]
},