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
Prev Previous commit
Next Next commit
wip
  • Loading branch information
louislam committed Nov 6, 2023
commit 2ed739b1b92cd9861e582f17df35845d5fecb4d6
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)


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



4 changes: 2 additions & 2 deletions backend/socket-handlers/docker-socket-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export class DockerSocketHandler extends SocketHandler {

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

stack.startCombinedTerminal(socket);
stack.joinCombinedTerminal(socket);

callback({
ok: true,
Expand Down Expand Up @@ -113,7 +113,7 @@ export class DockerSocketHandler extends SocketHandler {
});
server.sendStackList();

stack.startCombinedTerminal(socket);
stack.joinCombinedTerminal(socket);

} catch (e) {
callbackError(e, callback);
Expand Down
17 changes: 7 additions & 10 deletions backend/socket-handlers/terminal-socket-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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.");
Expand Down Expand Up @@ -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);

if (!terminal) {
terminal = new InteractiveTerminal(server, terminalName);
terminal.rows = 50;
log.debug("deployStack", "Terminal created");
}
log.debug("interactiveTerminal", "Stack name: " + stackName);
log.debug("interactiveTerminal", "Service name: " + serviceName);

terminal.join(socket);
terminal.start();
// Get stack
const stack = Stack.getStack(server, stackName);
stack.joinContainerTerminal(socket, serviceName);

callback({
ok: true,
Expand Down
38 changes: 26 additions & 12 deletions backend/stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.");
}
Expand All @@ -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.");
}
Expand Down Expand Up @@ -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.");
}
Expand All @@ -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.");
}
Expand All @@ -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.");
}
Expand All @@ -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();
}
}
17 changes: 15 additions & 2 deletions backend/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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() {
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion backend/util-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/components/Container.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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];
},
Expand Down
24 changes: 21 additions & 3 deletions frontend/src/components/Terminal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
});
});
}
}
};
Expand All @@ -193,6 +210,7 @@ export default {
<style scoped lang="scss">
.main-terminal {
height: 100%;
overflow-x: scroll;
}
</style>

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/mixins/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
53 changes: 9 additions & 44 deletions frontend/src/pages/Compose.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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">
Expand Down Expand Up @@ -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>
Expand Down
Loading