Compare commits

...

5 Commits

Author SHA1 Message Date
bac498f97f Update to 1.5.0 2025-03-31 01:37:19 +08:00
3e37f38fc7 Fix: request service status during add mode 2025-03-30 17:51:47 +08:00
6dff52cc73 Fix: generate compose without project name 2025-03-30 17:35:33 +08:00
7fcc4c510c Update README.md 2025-03-30 07:19:28 +08:00
0ceb6336dd Console Improvements (#767) 2025-03-30 07:14:33 +08:00
13 changed files with 71 additions and 58 deletions

View File

@ -106,7 +106,7 @@ docker compose pull && docker compose up -d
## Motivations
- 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 don't have support for arm64, so I stepped back to Node.js)
- Try to develop with ES Module + TypeScript
If you love this project, please consider giving it a ⭐.

View File

@ -38,6 +38,11 @@ export class TerminalSocketHandler extends AgentSocketHandler {
try {
checkLogin(socket);
// Throw an error if console is not enabled
if (!server.config.enableConsole) {
throw new ValidationError("Console is not enabled.");
}
// TODO: Reset the name here, force one main terminal for now
terminalName = "console";
@ -66,6 +71,18 @@ export class TerminalSocketHandler extends AgentSocketHandler {
}
});
// Check if MainTerminal is enabled
agentSocket.on("checkMainTerminal", async (callback) => {
try {
checkLogin(socket);
callbackResult({
ok: server.config.enableConsole,
}, callback);
} catch (e) {
callbackError(e, callback);
}
});
// Interactive Terminal for containers
agentSocket.on("interactiveTerminal", async (stackName : unknown, serviceName : unknown, shell : unknown, callback) => {
try {

View File

@ -136,6 +136,11 @@ export class DockgeServer {
stacksDir: {
type: String,
optional: true,
},
enableConsole: {
type: Boolean,
optional: true,
defaultValue: false,
}
});
@ -149,6 +154,7 @@ export class DockgeServer {
this.config.hostname = args.hostname || process.env.DOCKGE_HOSTNAME || undefined;
this.config.dataDir = args.dataDir || process.env.DOCKGE_DATA_DIR || "./data/";
this.config.stacksDir = args.stacksDir || process.env.DOCKGE_STACKS_DIR || defaultStacksDir;
this.config.enableConsole = args.enableConsole || process.env.DOCKGE_ENABLE_CONSOLE === "true" || false;
this.stacksDir = this.config.stacksDir;
log.debug("server", this.config);

View File

@ -311,7 +311,12 @@ export class MainSocketHandler extends SocketHandler {
throw new ValidationError("dockerRunCommand must be a string");
}
const composeTemplate = composerize(dockerRunCommand);
// Option: 'latest' | 'v2x' | 'v3x'
let composeTemplate = composerize(dockerRunCommand, "", "latest");
// Remove the first line "name: <your project name>"
composeTemplate = composeTemplate.split("\n").slice(1).join("\n");
callback({
ok: true,
composeTemplate,

View File

@ -4,7 +4,6 @@ import * as pty from "@homebridge/node-pty-prebuilt-multiarch";
import { LimitQueue } from "./utils/limit-queue";
import { DockgeSocket } from "./util-server";
import {
allowedCommandList, allowedRawKeys,
PROGRESS_TERMINAL_ROWS,
TERMINAL_COLS,
TERMINAL_ROWS
@ -16,7 +15,6 @@ import { log } from "./log";
* Terminal for running commands, no user interaction
*/
export class Terminal {
protected static terminalMap : Map<string, Terminal> = new Map();
protected _ptyProcess? : pty.IPty;
@ -272,6 +270,11 @@ export class MainTerminal extends InteractiveTerminal {
constructor(server : DockgeServer, name : string) {
let shell;
// Throw an error if console is not enabled
if (!server.config.enableConsole) {
throw new Error("Console is not enabled.");
}
if (os.platform() === "win32") {
if (commandExistsSync("pwsh.exe")) {
shell = "pwsh.exe";
@ -285,21 +288,6 @@ export class MainTerminal extends InteractiveTerminal {
}
public write(input : string) {
// For like Ctrl + C
if (allowedRawKeys.includes(input)) {
super.write(input);
return;
}
// Check if the command is allowed
const cmdParts = input.split(" ");
const executable = cmdParts[0].trim();
log.debug("console", "Executable: " + executable);
log.debug("console", "Executable length: " + executable.length);
if (!allowedCommandList.includes(executable)) {
throw new Error("Command not allowed.");
}
super.write(input);
}
}

View File

@ -30,6 +30,7 @@ export interface Arguments {
hostname? : string;
dataDir? : string;
stacksDir? : string;
enableConsole? : boolean;
}
// Some config values are required

View File

@ -107,17 +107,6 @@ export const COMBINED_TERMINAL_ROWS = 20;
export const ERROR_TYPE_VALIDATION = 1;
export const allowedCommandList : string[] = [
"docker",
"ls",
"cd",
"dir",
];
export const allowedRawKeys = [
"\u0003", // Ctrl + C
];
export const acceptedComposeFileNames = [
"compose.yaml",
"docker-compose.yaml",

View File

@ -201,7 +201,6 @@ export default {
} else {
this.cursorPosition++;
this.terminalInputBuffer += e.key;
console.log(this.terminalInputBuffer);
this.terminal.write(e.key);
}
});

View File

@ -279,7 +279,6 @@ export default defineComponent({
});
socket.on("agentList", (res) => {
console.log(res);
if (res.ok) {
this.agentList = res.agentList;
}

View File

@ -1,7 +1,7 @@
<template>
<transition name="slide-fade" appear>
<div>
<h1 v-if="isAdd" class="mb-3">{{$t("compose")}}</h1>
<h1 v-if="isAdd" class="mb-3">{{ $t("compose") }}</h1>
<h1 v-else class="mb-3">
<Uptime :stack="globalStack" :pill="true" /> {{ stack.name }}
<span v-if="$root.agentCount > 1" class="agent-name">
@ -150,7 +150,7 @@
<!-- Combined Terminal Output -->
<div v-show="!isEditMode">
<h4 class="mb-3">{{$t("terminal")}}</h4>
<h4 class="mb-3">{{ $t("terminal") }}</h4>
<Terminal
ref="combinedTerminal"
class="mb-3 terminal"
@ -491,6 +491,11 @@ export default {
},
requestServiceStatus() {
// Do not request if it is add mode
if (this.isAdd) {
return;
}
this.$root.emitAgent(this.endpoint, "serviceStatusList", this.stack.name, (res) => {
if (res.ok) {
this.serviceStatusList = res.serviceStatusList;

View File

@ -1,35 +1,36 @@
<template>
<transition name="slide-fade" appear>
<div>
<div v-if="!processing">
<h1 class="mb-3">Console</h1>
<div>
<p>
{{ $t("Allowed commands:") }}
<template v-for="(command, index) in allowedCommandList" :key="command">
<code>{{ command }}</code>
<Terminal v-if="enableConsole" class="terminal" :rows="20" mode="mainTerminal" name="console" :endpoint="endpoint"></Terminal>
<!-- No comma at the end -->
<span v-if="index !== allowedCommandList.length - 1">, </span>
</template>
<div v-else class="alert alert-warning shadow-box" role="alert">
<h4 class="alert-heading">Console is not enabled</h4>
<p>
Console is a powerful tool that allows you to execute any commands such as <code>docker</code>, <code>rm</code> within the Dockge's container in this Web UI.
</p>
<p>
It might be dangerous since this Dockge container is connecting to the host's Docker daemon. Also Dockge could be possibly taken down by commands like <code>rm -rf</code>.
</p>
<p>
If you understand the risk, you can enable it by setting <code>DOCKGE_ENABLE_CONSOLE=true</code> in the environment variables.
</p>
</div>
<Terminal class="terminal" :rows="20" mode="mainTerminal" name="console" :endpoint="endpoint"></Terminal>
</div>
</transition>
</template>
<script>
import { allowedCommandList } from "../../../common/util-common";
export default {
components: {
},
data() {
return {
allowedCommandList,
processing: true,
enableConsole: false,
};
},
computed: {
@ -38,7 +39,10 @@ export default {
},
},
mounted() {
this.$root.emitAgent(this.endpoint, "checkMainTerminal", (res) => {
this.enableConsole = res.ok;
this.processing = false;
});
},
methods: {

12
package-lock.json generated
View File

@ -53,7 +53,7 @@
"@types/semver": "^7.7.0",
"@typescript-eslint/eslint-plugin": "~6.8.0",
"@typescript-eslint/parser": "~6.8.0",
"@vitejs/plugin-vue": "~4.5.2",
"@vitejs/plugin-vue": "~5.2.3",
"@xterm/addon-fit": "beta",
"@xterm/xterm": "beta",
"bootstrap": "5.3.2",
@ -2153,16 +2153,16 @@
}
},
"node_modules/@vitejs/plugin-vue": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.5.2.tgz",
"integrity": "sha512-UGR3DlzLi/SaVBPX0cnSyE37vqxU3O6chn8l0HJNzQzDia6/Au2A4xKv+iIJW8w2daf80G7TYHhi1pAUjdZ0bQ==",
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz",
"integrity": "sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^14.18.0 || >=16.0.0"
"node": "^18.0.0 || >=20.0.0"
},
"peerDependencies": {
"vite": "^4.0.0 || ^5.0.0",
"vite": "^5.0.0 || ^6.0.0",
"vue": "^3.2.25"
}
},

View File

@ -1,6 +1,6 @@
{
"name": "dockge",
"version": "1.4.2",
"version": "1.5.0",
"type": "module",
"engines": {
"node": ">= 22.14.0"
@ -72,7 +72,7 @@
"@types/semver": "^7.7.0",
"@typescript-eslint/eslint-plugin": "~6.8.0",
"@typescript-eslint/parser": "~6.8.0",
"@vitejs/plugin-vue": "~4.5.2",
"@vitejs/plugin-vue": "~5.2.3",
"@xterm/addon-fit": "beta",
"@xterm/xterm": "beta",
"bootstrap": "5.3.2",