Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip #1

Merged
merged 27 commits into from
Nov 11, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
wip
  • Loading branch information
louislam committed Nov 5, 2023
commit 314630724b918111104ffbdfe7f0575d6948ed1a
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,33 @@ A fancy, easy-to-use and reactive docker stack (`docker-compose.yml`) manager.
## ⭐ Features

- Focus on `docker-compose.yml` stack management
- Interactive editor for `docker-compose.yml` files
- Interactive editor for `docker-compose.yml`
- Interactive web terminal for containers and any docker commands
- Reactive - Everything is just responsive. Progress and terminal output are in real-time
- Easy-to-use & fancy UI - If you love Uptime Kuma's UI, you will love this too
- Easy-to-use & fancy UI - If you love Uptime Kuma's UI/UX, you will love this too
- Build on top of [Compose V2](https://docs.docker.com/compose/migrate/), as known as `compose.yaml` and `docker compose`
- Convert `docker run ...` command into `docker-compose.yml` file

## Installation

## Motivations

- Try ES Module and TypeScript in 2023
- I have been using Portainer for some time, but I am sometimes not satisfied with it. For example, sometimes when I deploy a stack, it keeps spinning the loading icon for a few minutes, and I don't know what's going on.
- 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.
- 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)


If you love this project, please consider giving this project a ⭐.

## More Ideas?

- Container file manager
- Stats
- File manager
- App store for yaml templates
- Container stats
- Get app icons
- Switch Docker context
- Support Dockerfile and build
- Zero-config private docker registry
- Support Docker swarm



17 changes: 15 additions & 2 deletions backend/dockge-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,14 +424,27 @@ export class DockgeServer {
}

sendStackList(useCache = false) {
let stackList = Stack.getStackList(this, useCache);
let roomList = this.io.sockets.adapter.rooms.keys();
let map : Map<string, object> | undefined;

for (let room of roomList) {
// Check if the room is a number (user id)
if (Number(room)) {

// Get the list only if there is a room
if (!map) {
map = new Map();
let stackList = Stack.getStackList(this, useCache);

for (let [ stackName, stack ] of stackList) {
map.set(stackName, stack.toSimpleJSON());
}
}

log.debug("server", "Send stack list to room " + room);
this.io.to(room).emit("stackList", {
ok: true,
stackList: Object.fromEntries(stackList),
stackList: Object.fromEntries(map),
});
}
}
Expand Down
7 changes: 7 additions & 0 deletions backend/routers/main-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ export class MainRouter extends Router {
res.send(server.indexHTML);
});

// Robots.txt
router.get("/robots.txt", async (_request, response) => {
let txt = "User-agent: *\nDisallow: /";
response.setHeader("Content-Type", "text/plain");
response.send(txt);
});

return router;
}

Expand Down
106 changes: 106 additions & 0 deletions backend/socket-handlers/docker-socket-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class DockerSocketHandler extends SocketHandler {
server.sendStackList();
callback({
ok: true,
msg: "Deployed",
});
} catch (e) {
callbackError(e, callback);
Expand Down Expand Up @@ -69,6 +70,9 @@ export class DockerSocketHandler extends SocketHandler {
}

const stack = Stack.getStack(server, stackName);

stack.startCombinedTerminal(socket);

callback({
ok: true,
stack: stack.toJSON(),
Expand All @@ -77,6 +81,107 @@ export class DockerSocketHandler extends SocketHandler {
callbackError(e, callback);
}
});

// requestStackList
socket.on("requestStackList", async (callback) => {
try {
checkLogin(socket);
server.sendStackList();
callback({
ok: true,
msg: "Updated"
});
} catch (e) {
callbackError(e, callback);
}
});

// startStack
socket.on("startStack", async (stackName : unknown, callback) => {
try {
checkLogin(socket);

if (typeof(stackName) !== "string") {
throw new ValidationError("Stack name must be a string");
}

const stack = Stack.getStack(server, stackName);
await stack.start(socket);
callback({
ok: true,
msg: "Started"
});
server.sendStackList();

stack.startCombinedTerminal(socket);

} catch (e) {
callbackError(e, callback);
}
});

// stopStack
socket.on("stopStack", async (stackName : unknown, callback) => {
try {
checkLogin(socket);

if (typeof(stackName) !== "string") {
throw new ValidationError("Stack name must be a string");
}

const stack = Stack.getStack(server, stackName);
await stack.stop(socket);
callback({
ok: true,
msg: "Stopped"
});
server.sendStackList();
} catch (e) {
callbackError(e, callback);
}
});

// restartStack
socket.on("restartStack", async (stackName : unknown, callback) => {
try {
checkLogin(socket);

if (typeof(stackName) !== "string") {
throw new ValidationError("Stack name must be a string");
}

const stack = Stack.getStack(server, stackName);
await stack.restart(socket);
callback({
ok: true,
msg: "Restarted"
});
server.sendStackList();
} catch (e) {
callbackError(e, callback);
}
});

// updateStack
socket.on("updateStack", async (stackName : unknown, callback) => {
try {
checkLogin(socket);

if (typeof(stackName) !== "string") {
throw new ValidationError("Stack name must be a string");
}

const stack = Stack.getStack(server, stackName);
await stack.update(socket);
callback({
ok: true,
msg: "Updated"
});
server.sendStackList();
} catch (e) {
callbackError(e, callback);
}
});
}

saveStack(socket : DockgeSocket, server : DockgeServer, name : unknown, composeYAML : unknown, isAdd : unknown) : Stack {
Expand All @@ -95,5 +200,6 @@ export class DockerSocketHandler extends SocketHandler {
stack.save(isAdd);
return stack;
}

}

93 changes: 51 additions & 42 deletions backend/socket-handlers/terminal-socket-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,74 +8,92 @@ import fs from "fs";
import {
allowedCommandList,
allowedRawKeys,
getComposeTerminalName,
getComposeTerminalName, getContainerExecTerminalName,
isDev,
PROGRESS_TERMINAL_ROWS
} from "../util-common";
import { MainTerminal, Terminal } from "../terminal";
import { InteractiveTerminal, MainTerminal, Terminal } from "../terminal";

export class TerminalSocketHandler extends SocketHandler {
create(socket : DockgeSocket, server : DockgeServer) {

socket.on("terminalInputRaw", async (key : unknown) => {
socket.on("terminalInput", async (terminalName : unknown, cmd : unknown, errorCallback) => {
try {
checkLogin(socket);

if (typeof(key) !== "string") {
throw new Error("Key must be a string.");
if (typeof(terminalName) !== "string") {
throw new Error("Terminal name must be a string.");
}

if (allowedRawKeys.includes(key)) {
server.terminal.write(key);
if (typeof(cmd) !== "string") {
throw new Error("Command must be a string.");
}
} catch (e) {

let terminal = Terminal.getTerminal(terminalName);
if (terminal instanceof InteractiveTerminal) {
terminal.write(cmd);
} else {
throw new Error("Terminal not found or it is not a Interactive Terminal.");
}
} catch (e) {
errorCallback({
ok: false,
msg: e.message,
});
}
});

socket.on("terminalInput", async (terminalName : unknown, cmd : unknown, errorCallback : unknown) => {
// Main Terminal
socket.on("mainTerminal", async (terminalName : unknown, callback) => {
try {
checkLogin(socket);

if (typeof(cmd) !== "string") {
throw new Error("Command must be a string.");
// TODO: Reset the name here, force one main terminal for now
terminalName = "console";

if (typeof(terminalName) !== "string") {
throw new ValidationError("Terminal name must be a string.");
}

// Check if the command is allowed
const cmdParts = cmd.split(" ");
const executable = cmdParts[0].trim();
log.debug("console", "Executable: " + executable);
log.debug("console", "Executable length: " + executable.length);
log.debug("deployStack", "Terminal name: " + terminalName);

let terminal = Terminal.getTerminal(terminalName);

if (!allowedCommandList.includes(executable)) {
throw new Error("Command not allowed.");
if (!terminal) {
terminal = new MainTerminal(server, terminalName);
terminal.rows = 50;
log.debug("deployStack", "Terminal created");
}

server.terminal.write(cmd);
terminal.join(socket);
terminal.start();

callback({
ok: true,
});
} catch (e) {
if (typeof(errorCallback) === "function") {
errorCallback({
ok: false,
msg: e.message,
});
}
callbackError(e, callback);
}
});

// Create Terminal
socket.on("mainTerminal", async (terminalName : unknown, callback) => {
// Interactive Terminal for containers
socket.on("interactiveTerminal", async (stackName : unknown, serviceName : unknown, callback) => {
try {
checkLogin(socket);
if (typeof(terminalName) !== "string") {
throw new ValidationError("Terminal name must be a string.");

if (typeof(stackName) !== "string") {
throw new ValidationError("Stack name must be a string.");
}

log.debug("deployStack", "Terminal name: " + terminalName);
if (typeof(serviceName) !== "string") {
throw new ValidationError("Service name must be a string.");
}

const terminalName = getContainerExecTerminalName(stackName, serviceName, 0);
let terminal = Terminal.getTerminal(terminalName);

if (!terminal) {
terminal = new MainTerminal(server, terminalName);
terminal = new InteractiveTerminal(server, terminalName);
terminal.rows = 50;
log.debug("deployStack", "Terminal created");
}
Expand All @@ -91,7 +109,7 @@ export class TerminalSocketHandler extends SocketHandler {
}
});

// Join Terminal
// Join Output Terminal
socket.on("terminalJoin", async (terminalName : unknown, callback) => {
if (typeof(callback) !== "function") {
log.debug("console", "Callback is not a function.");
Expand Down Expand Up @@ -124,18 +142,9 @@ export class TerminalSocketHandler extends SocketHandler {

});

// Resize Terminal
// TODO: Resize Terminal
socket.on("terminalResize", async (rows : unknown) => {
try {
checkLogin(socket);
if (typeof(rows) !== "number") {
throw new Error("Rows must be a number.");
}
log.debug("console", "Resize terminal to " + rows + " rows.");
server.terminal.resize(rows);
} catch (e) {

}
});
}
}
Loading