mirror of
https://github.com/louislam/dockge.git
synced 2024-11-27 13:14:03 +00:00
added support for multiple stacks in repos
This commit is contained in:
parent
b946b11f5f
commit
26bd199c69
7 changed files with 503 additions and 171 deletions
51
.vscode/launch.json
vendored
Normal file
51
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch Dev with Debugger",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"runtimeArgs": ["run", "dev"],
|
||||||
|
"port": 9229,
|
||||||
|
"env": {
|
||||||
|
"NODE_ENV": "development"
|
||||||
|
},
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"sourceMaps": true,
|
||||||
|
"smartStep": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Launch Backend",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeExecutable": "ts-node",
|
||||||
|
"args": ["${workspaceFolder}/backend/dockge-server.ts"],
|
||||||
|
"preLaunchTask": "tsc: build - tsconfig.json",
|
||||||
|
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||||
|
"env": {
|
||||||
|
"NODE_ENV": "development"
|
||||||
|
},
|
||||||
|
"sourceMaps": true,
|
||||||
|
"smartStep": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Launch Frontend",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"url": "http://localhost:3000",
|
||||||
|
"webRoot": "${workspaceFolder}/frontend",
|
||||||
|
"preLaunchTask": "npm: start"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Attach to Backend",
|
||||||
|
"type": "node",
|
||||||
|
"request": "attach",
|
||||||
|
"port": 9229,
|
||||||
|
"restart": true,
|
||||||
|
"protocol": "inspector",
|
||||||
|
"sourceMaps": true,
|
||||||
|
"smartStep": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"editor.formatOnSave": false,
|
||||||
|
"editor.formatOnType": false,
|
||||||
|
"editor.formatOnPaste": false
|
||||||
|
}
|
|
@ -12,21 +12,20 @@ import dayjs, { Dayjs } from "dayjs";
|
||||||
* One AgentManager per Socket connection
|
* One AgentManager per Socket connection
|
||||||
*/
|
*/
|
||||||
export class AgentManager {
|
export class AgentManager {
|
||||||
|
protected socket: DockgeSocket;
|
||||||
protected socket : DockgeSocket;
|
protected agentSocketList: Record<string, SocketClient> = {};
|
||||||
protected agentSocketList : Record<string, SocketClient> = {};
|
protected agentLoggedInList: Record<string, boolean> = {};
|
||||||
protected agentLoggedInList : Record<string, boolean> = {};
|
protected _firstConnectTime: Dayjs = dayjs();
|
||||||
protected _firstConnectTime : Dayjs = dayjs();
|
|
||||||
|
|
||||||
constructor(socket: DockgeSocket) {
|
constructor(socket: DockgeSocket) {
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
get firstConnectTime() : Dayjs {
|
get firstConnectTime(): Dayjs {
|
||||||
return this._firstConnectTime;
|
return this._firstConnectTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
test(url : string, username : string, password : string) : Promise<void> {
|
test(url: string, username: string, password: string): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let obj = new URL(url);
|
let obj = new URL(url);
|
||||||
let endpoint = obj.host;
|
let endpoint = obj.host;
|
||||||
|
@ -43,26 +42,32 @@ export class AgentManager {
|
||||||
reconnection: false,
|
reconnection: false,
|
||||||
extraHeaders: {
|
extraHeaders: {
|
||||||
endpoint,
|
endpoint,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on("connect", () => {
|
client.on("connect", () => {
|
||||||
client.emit("login", {
|
client.emit(
|
||||||
username: username,
|
"login",
|
||||||
password: password,
|
{
|
||||||
}, (res : LooseObject) => {
|
username: username,
|
||||||
if (res.ok) {
|
password: password,
|
||||||
resolve();
|
},
|
||||||
} else {
|
(res: LooseObject) => {
|
||||||
reject(new Error(res.msg));
|
if (res.ok) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(res.msg));
|
||||||
|
}
|
||||||
|
client.disconnect();
|
||||||
}
|
}
|
||||||
client.disconnect();
|
);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on("connect_error", (err) => {
|
client.on("connect_error", (err) => {
|
||||||
if (err.message === "xhr poll error") {
|
if (err.message === "xhr poll error") {
|
||||||
reject(new Error("Unable to connect to the Dockge instance"));
|
reject(
|
||||||
|
new Error("Unable to connect to the Dockge instance")
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
|
@ -77,7 +82,7 @@ export class AgentManager {
|
||||||
* @param username
|
* @param username
|
||||||
* @param password
|
* @param password
|
||||||
*/
|
*/
|
||||||
async add(url : string, username : string, password : string) : Promise<Agent> {
|
async add(url: string, username: string, password: string): Promise<Agent> {
|
||||||
let bean = R.dispense("agent") as Agent;
|
let bean = R.dispense("agent") as Agent;
|
||||||
bean.url = url;
|
bean.url = url;
|
||||||
bean.username = username;
|
bean.username = username;
|
||||||
|
@ -90,10 +95,8 @@ export class AgentManager {
|
||||||
*
|
*
|
||||||
* @param url
|
* @param url
|
||||||
*/
|
*/
|
||||||
async remove(url : string) {
|
async remove(url: string) {
|
||||||
let bean = await R.findOne("agent", " url = ? ", [
|
let bean = await R.findOne("agent", " url = ? ", [url]);
|
||||||
url,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (bean) {
|
if (bean) {
|
||||||
await R.trash(bean);
|
await R.trash(bean);
|
||||||
|
@ -106,7 +109,7 @@ export class AgentManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(url : string, username : string, password : string) {
|
connect(url: string, username: string, password: string) {
|
||||||
let obj = new URL(url);
|
let obj = new URL(url);
|
||||||
let endpoint = obj.host;
|
let endpoint = obj.host;
|
||||||
|
|
||||||
|
@ -116,49 +119,74 @@ export class AgentManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!endpoint) {
|
if (!endpoint) {
|
||||||
log.error("agent-manager", "Invalid endpoint: " + endpoint + " URL: " + url);
|
log.error(
|
||||||
|
"agent-manager",
|
||||||
|
"Invalid endpoint: " + endpoint + " URL: " + url
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.agentSocketList[endpoint]) {
|
if (this.agentSocketList[endpoint]) {
|
||||||
log.debug("agent-manager", "Already connected to the socket server: " + endpoint);
|
log.debug(
|
||||||
|
"agent-manager",
|
||||||
|
"Already connected to the socket server: " + endpoint
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("agent-manager", "Connecting to the socket server: " + endpoint);
|
log.info(
|
||||||
|
"agent-manager",
|
||||||
|
"Connecting to the socket server: " + endpoint
|
||||||
|
);
|
||||||
let client = io(url, {
|
let client = io(url, {
|
||||||
extraHeaders: {
|
extraHeaders: {
|
||||||
endpoint,
|
endpoint,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on("connect", () => {
|
client.on("connect", () => {
|
||||||
log.info("agent-manager", "Connected to the socket server: " + endpoint);
|
log.info(
|
||||||
|
"agent-manager",
|
||||||
|
"Connected to the socket server: " + endpoint
|
||||||
|
);
|
||||||
|
|
||||||
client.emit("login", {
|
client.emit(
|
||||||
username: username,
|
"login",
|
||||||
password: password,
|
{
|
||||||
}, (res : LooseObject) => {
|
username: username,
|
||||||
if (res.ok) {
|
password: password,
|
||||||
log.info("agent-manager", "Logged in to the socket server: " + endpoint);
|
},
|
||||||
this.agentLoggedInList[endpoint] = true;
|
(res: LooseObject) => {
|
||||||
this.socket.emit("agentStatus", {
|
if (res.ok) {
|
||||||
endpoint: endpoint,
|
log.info(
|
||||||
status: "online",
|
"agent-manager",
|
||||||
});
|
"Logged in to the socket server: " + endpoint
|
||||||
} else {
|
);
|
||||||
log.error("agent-manager", "Failed to login to the socket server: " + endpoint);
|
this.agentLoggedInList[endpoint] = true;
|
||||||
this.agentLoggedInList[endpoint] = false;
|
this.socket.emit("agentStatus", {
|
||||||
this.socket.emit("agentStatus", {
|
endpoint: endpoint,
|
||||||
endpoint: endpoint,
|
status: "online",
|
||||||
status: "offline",
|
});
|
||||||
});
|
} else {
|
||||||
|
log.error(
|
||||||
|
"agent-manager",
|
||||||
|
"Failed to login to the socket server: " + endpoint
|
||||||
|
);
|
||||||
|
this.agentLoggedInList[endpoint] = false;
|
||||||
|
this.socket.emit("agentStatus", {
|
||||||
|
endpoint: endpoint,
|
||||||
|
status: "offline",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on("connect_error", (err) => {
|
client.on("connect_error", (err) => {
|
||||||
log.error("agent-manager", "Error from the socket server: " + endpoint);
|
log.error(
|
||||||
|
"agent-manager",
|
||||||
|
"Error from the socket server: " + endpoint
|
||||||
|
);
|
||||||
this.socket.emit("agentStatus", {
|
this.socket.emit("agentStatus", {
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
status: "offline",
|
status: "offline",
|
||||||
|
@ -166,14 +194,17 @@ export class AgentManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on("disconnect", () => {
|
client.on("disconnect", () => {
|
||||||
log.info("agent-manager", "Disconnected from the socket server: " + endpoint);
|
log.info(
|
||||||
|
"agent-manager",
|
||||||
|
"Disconnected from the socket server: " + endpoint
|
||||||
|
);
|
||||||
this.socket.emit("agentStatus", {
|
this.socket.emit("agentStatus", {
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
status: "offline",
|
status: "offline",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on("agent", (...args : unknown[]) => {
|
client.on("agent", (...args: unknown[]) => {
|
||||||
this.socket.emit("agent", ...args);
|
this.socket.emit("agent", ...args);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -194,7 +225,7 @@ export class AgentManager {
|
||||||
this.agentSocketList[endpoint] = client;
|
this.agentSocketList[endpoint] = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect(endpoint : string) {
|
disconnect(endpoint: string) {
|
||||||
let client = this.agentSocketList[endpoint];
|
let client = this.agentSocketList[endpoint];
|
||||||
client?.disconnect();
|
client?.disconnect();
|
||||||
}
|
}
|
||||||
|
@ -203,14 +234,20 @@ export class AgentManager {
|
||||||
this._firstConnectTime = dayjs();
|
this._firstConnectTime = dayjs();
|
||||||
|
|
||||||
if (this.socket.endpoint) {
|
if (this.socket.endpoint) {
|
||||||
log.info("agent-manager", "This connection is connected as an agent, skip connectAll()");
|
log.info(
|
||||||
|
"agent-manager",
|
||||||
|
"This connection is connected as an agent, skip connectAll()"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let list : Record<string, Agent> = await Agent.getAgentList();
|
let list: Record<string, Agent> = await Agent.getAgentList();
|
||||||
|
|
||||||
if (Object.keys(list).length !== 0) {
|
if (Object.keys(list).length !== 0) {
|
||||||
log.info("agent-manager", "Connecting to all instance socket server(s)...");
|
log.info(
|
||||||
|
"agent-manager",
|
||||||
|
"Connecting to all instance socket server(s)..."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let endpoint in list) {
|
for (let endpoint in list) {
|
||||||
|
@ -225,13 +262,22 @@ export class AgentManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async emitToEndpoint(endpoint: string, eventName: string, ...args : unknown[]) {
|
async emitToEndpoint(
|
||||||
|
endpoint: string,
|
||||||
|
eventName: string,
|
||||||
|
...args: unknown[]
|
||||||
|
) {
|
||||||
log.debug("agent-manager", "Emitting event to endpoint: " + endpoint);
|
log.debug("agent-manager", "Emitting event to endpoint: " + endpoint);
|
||||||
let client = this.agentSocketList[endpoint];
|
let client = this.agentSocketList[endpoint];
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
log.error("agent-manager", "Socket client not found for endpoint: " + endpoint);
|
log.error(
|
||||||
throw new Error("Socket client not found for endpoint: " + endpoint);
|
"agent-manager",
|
||||||
|
"Socket client not found for endpoint: " + endpoint
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
"Socket client not found for endpoint: " + endpoint
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!client.connected || !this.agentLoggedInList[endpoint]) {
|
if (!client.connected || !this.agentLoggedInList[endpoint]) {
|
||||||
|
@ -242,25 +288,36 @@ export class AgentManager {
|
||||||
let ok = false;
|
let ok = false;
|
||||||
while (diff < 10) {
|
while (diff < 10) {
|
||||||
if (client.connected && this.agentLoggedInList[endpoint]) {
|
if (client.connected && this.agentLoggedInList[endpoint]) {
|
||||||
log.debug("agent-manager", `${endpoint}: Connected & Logged in`);
|
log.debug(
|
||||||
|
"agent-manager",
|
||||||
|
`${endpoint}: Connected & Logged in`
|
||||||
|
);
|
||||||
ok = true;
|
ok = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
log.debug("agent-manager", endpoint + ": not ready yet, retrying in 1 second...");
|
log.debug(
|
||||||
|
"agent-manager",
|
||||||
|
endpoint + ": not ready yet, retrying in 1 second..."
|
||||||
|
);
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
diff = dayjs().diff(this.firstConnectTime, "second");
|
diff = dayjs().diff(this.firstConnectTime, "second");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
log.error("agent-manager", `${endpoint}: Socket client not connected`);
|
log.error(
|
||||||
throw new Error("Socket client not connected for endpoint: " + endpoint);
|
"agent-manager",
|
||||||
|
`${endpoint}: Socket client not connected`
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
"Socket client not connected for endpoint: " + endpoint
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client.emit("agent", endpoint, eventName, ...args);
|
client.emit("agent", endpoint, eventName, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
emitToAllEndpoints(eventName: string, ...args : unknown[]) {
|
emitToAllEndpoints(eventName: string, ...args: unknown[]) {
|
||||||
log.debug("agent-manager", "Emitting event to all endpoints");
|
log.debug("agent-manager", "Emitting event to all endpoints");
|
||||||
for (let endpoint in this.agentSocketList) {
|
for (let endpoint in this.agentSocketList) {
|
||||||
this.emitToEndpoint(endpoint, eventName, ...args).catch((e) => {
|
this.emitToEndpoint(endpoint, eventName, ...args).catch((e) => {
|
||||||
|
@ -271,7 +328,7 @@ export class AgentManager {
|
||||||
|
|
||||||
async sendAgentList() {
|
async sendAgentList() {
|
||||||
let list = await Agent.getAgentList();
|
let list = await Agent.getAgentList();
|
||||||
let result : Record<string, LooseObject> = {};
|
let result: Record<string, LooseObject> = {};
|
||||||
|
|
||||||
// Myself
|
// Myself
|
||||||
result[""] = {
|
result[""] = {
|
||||||
|
|
|
@ -48,7 +48,7 @@ export class DockgeServer {
|
||||||
io : socketIO.Server;
|
io : socketIO.Server;
|
||||||
config : Config;
|
config : Config;
|
||||||
indexHTML : string = "";
|
indexHTML : string = "";
|
||||||
gitUpdateInterval? : NodeJS.Timeout;
|
gitUpdateInterval : NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of express routers
|
* List of express routers
|
||||||
|
@ -665,7 +665,7 @@ export class DockgeServer {
|
||||||
};
|
};
|
||||||
|
|
||||||
await check();
|
await check();
|
||||||
this.gitUpdateInterval = setInterval(check, GIT_UPDATE_CHECKER_INTERVAL_MS);
|
this.gitUpdateInterval = setInterval(check, GIT_UPDATE_CHECKER_INTERVAL_MS) as NodeJS.Timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDockerNetworkList() : Promise<string[]> {
|
async getDockerNetworkList() : Promise<string[]> {
|
||||||
|
@ -702,6 +702,10 @@ export class DockgeServer {
|
||||||
log.info("server", "Shutdown requested");
|
log.info("server", "Shutdown requested");
|
||||||
log.info("server", "Called signal: " + signal);
|
log.info("server", "Called signal: " + signal);
|
||||||
|
|
||||||
|
if (this.gitUpdateInterval) {
|
||||||
|
clearInterval(this.gitUpdateInterval);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Close all terminals?
|
// TODO: Close all terminals?
|
||||||
|
|
||||||
await Database.close();
|
await Database.close();
|
||||||
|
|
415
backend/stack.ts
415
backend/stack.ts
|
@ -10,11 +10,14 @@ import {
|
||||||
COMBINED_TERMINAL_ROWS,
|
COMBINED_TERMINAL_ROWS,
|
||||||
CREATED_FILE,
|
CREATED_FILE,
|
||||||
CREATED_STACK,
|
CREATED_STACK,
|
||||||
EXITED, getCombinedTerminalName,
|
EXITED,
|
||||||
getComposeTerminalName, getContainerExecTerminalName,
|
getCombinedTerminalName,
|
||||||
|
getComposeTerminalName,
|
||||||
|
getContainerExecTerminalName,
|
||||||
PROGRESS_TERMINAL_ROWS,
|
PROGRESS_TERMINAL_ROWS,
|
||||||
RUNNING, TERMINAL_ROWS,
|
RUNNING,
|
||||||
UNKNOWN
|
TERMINAL_ROWS,
|
||||||
|
UNKNOWN,
|
||||||
} from "../common/util-common";
|
} from "../common/util-common";
|
||||||
import { InteractiveTerminal, Terminal } from "./terminal";
|
import { InteractiveTerminal, Terminal } from "./terminal";
|
||||||
import childProcessAsync from "promisify-child-process";
|
import childProcessAsync from "promisify-child-process";
|
||||||
|
@ -23,7 +26,6 @@ import { execSync } from "child_process";
|
||||||
import ini from "ini";
|
import ini from "ini";
|
||||||
|
|
||||||
export class Stack {
|
export class Stack {
|
||||||
|
|
||||||
name: string;
|
name: string;
|
||||||
protected _status: number = UNKNOWN;
|
protected _status: number = UNKNOWN;
|
||||||
protected _composeYAML?: string;
|
protected _composeYAML?: string;
|
||||||
|
@ -32,11 +34,17 @@ export class Stack {
|
||||||
protected _composeFileName: string = "compose.yaml";
|
protected _composeFileName: string = "compose.yaml";
|
||||||
protected server: DockgeServer;
|
protected server: DockgeServer;
|
||||||
|
|
||||||
protected combinedTerminal? : Terminal;
|
protected combinedTerminal?: Terminal;
|
||||||
|
|
||||||
protected static managedStackList: Map<string, Stack> = new Map();
|
protected static managedStackList: Map<string, Stack> = new Map();
|
||||||
|
|
||||||
constructor(server : DockgeServer, name : string, composeYAML? : string, composeENV? : string, skipFSOperations = false) {
|
constructor(
|
||||||
|
server: DockgeServer,
|
||||||
|
name: string,
|
||||||
|
composeYAML?: string,
|
||||||
|
composeENV?: string,
|
||||||
|
skipFSOperations = false
|
||||||
|
) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this._composeYAML = composeYAML;
|
this._composeYAML = composeYAML;
|
||||||
|
@ -53,8 +61,7 @@ export class Stack {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async toJSON(endpoint : string) : Promise<object> {
|
async toJSON(endpoint: string): Promise<object> {
|
||||||
|
|
||||||
// Since we have multiple agents now, embed primary hostname in the stack object too.
|
// Since we have multiple agents now, embed primary hostname in the stack object too.
|
||||||
let primaryHostname = await Settings.get("primaryHostname");
|
let primaryHostname = await Settings.get("primaryHostname");
|
||||||
if (!primaryHostname) {
|
if (!primaryHostname) {
|
||||||
|
@ -63,7 +70,7 @@ export class Stack {
|
||||||
} else {
|
} else {
|
||||||
// Use the endpoint as the primary hostname
|
// Use the endpoint as the primary hostname
|
||||||
try {
|
try {
|
||||||
primaryHostname = (new URL("https://" + endpoint).hostname);
|
primaryHostname = new URL("https://" + endpoint).hostname;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Just in case if the endpoint is in a incorrect format
|
// Just in case if the endpoint is in a incorrect format
|
||||||
primaryHostname = "localhost";
|
primaryHostname = "localhost";
|
||||||
|
@ -80,7 +87,7 @@ export class Stack {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
toSimpleJSON(endpoint : string) : object {
|
toSimpleJSON(endpoint: string): object {
|
||||||
return {
|
return {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
status: this._status,
|
status: this._status,
|
||||||
|
@ -98,29 +105,42 @@ export class Stack {
|
||||||
/**
|
/**
|
||||||
* Get the status of the stack from `docker compose ps --format json`
|
* Get the status of the stack from `docker compose ps --format json`
|
||||||
*/
|
*/
|
||||||
async ps() : Promise<object> {
|
async ps(): Promise<object> {
|
||||||
let res = await childProcessAsync.spawn("docker", [ "compose", "ps", "--format", "json" ], {
|
let res = await childProcessAsync.spawn(
|
||||||
cwd: this.path,
|
"docker",
|
||||||
encoding: "utf-8",
|
["compose", "ps", "--format", "json"],
|
||||||
});
|
{
|
||||||
|
cwd: this.path,
|
||||||
|
encoding: "utf-8",
|
||||||
|
}
|
||||||
|
);
|
||||||
if (!res.stdout) {
|
if (!res.stdout) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return JSON.parse(res.stdout.toString());
|
return JSON.parse(res.stdout.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
get isManagedByDockge() : boolean {
|
get isManagedByDockge(): boolean {
|
||||||
return fs.existsSync(this.path) && fs.statSync(this.path).isDirectory();
|
return fs.existsSync(this.path) && fs.statSync(this.path).isDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
get isGitRepo() : boolean {
|
get isGitRepo(): boolean {
|
||||||
return fs.existsSync(path.join(this.path, ".git")) && fs.statSync(path.join(this.path, ".git")).isDirectory();
|
try {
|
||||||
|
execSync("git rev-parse --is-inside-work-tree", { cwd: this.path });
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get gitUrl() : string {
|
get gitUrl() : string {
|
||||||
if (this.isGitRepo) {
|
if (this.isGitRepo) {
|
||||||
const gitConfig = ini.parse(fs.readFileSync(path.join(this.path, ".git", "config"), "utf-8"));
|
try {
|
||||||
return gitConfig["remote \"origin\""]?.url;
|
let stdout = execSync("git config --get remote.origin.url", { cwd: this.path });
|
||||||
|
return stdout.toString().trim();
|
||||||
|
} catch (error) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -140,20 +160,22 @@ export class Stack {
|
||||||
get webhook() : string {
|
get webhook() : string {
|
||||||
//TODO: refine this.
|
//TODO: refine this.
|
||||||
if (this.server.config.hostname) {
|
if (this.server.config.hostname) {
|
||||||
return `http://${this.server.config.hostname}:${this.server.config.port}/webhook/update/${this.name}`;
|
return `http://${this.server.config.hostname}:${this.server.config.port}/webhook/update/${encodeURIComponent(this.name)}`;
|
||||||
} else {
|
} else {
|
||||||
return `http://localhost:${this.server.config.port}/webhook/update/${this.name}`;
|
return `http://localhost:${this.server.config.port}/webhook/update/${encodeURIComponent(this.name)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get status() : number {
|
get status(): number {
|
||||||
return this._status;
|
return this._status;
|
||||||
}
|
}
|
||||||
|
|
||||||
validate() {
|
validate() {
|
||||||
// Check name, allows [a-z][0-9] _ - only
|
// Check name, allows [a-z][0-9] _ - only
|
||||||
if (!this.name.match(/^[a-z0-9_-]+$/)) {
|
if (!this.name.match(/^[a-z0-9_-]+$/)) {
|
||||||
throw new ValidationError("Stack name can only contain [a-z][0-9] _ - only");
|
throw new ValidationError(
|
||||||
|
"Stack name can only contain [a-z][0-9] _ - only"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check YAML format
|
// Check YAML format
|
||||||
|
@ -164,15 +186,22 @@ export class Stack {
|
||||||
// Check if the .env is able to pass docker-compose
|
// Check if the .env is able to pass docker-compose
|
||||||
// Prevent "setenv: The parameter is incorrect"
|
// Prevent "setenv: The parameter is incorrect"
|
||||||
// It only happens when there is one line and it doesn't contain "="
|
// It only happens when there is one line and it doesn't contain "="
|
||||||
if (lines.length === 1 && !lines[0].includes("=") && lines[0].length > 0) {
|
if (
|
||||||
|
lines.length === 1 &&
|
||||||
|
!lines[0].includes("=") &&
|
||||||
|
lines[0].length > 0
|
||||||
|
) {
|
||||||
throw new ValidationError("Invalid .env format");
|
throw new ValidationError("Invalid .env format");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get composeYAML() : string {
|
get composeYAML(): string {
|
||||||
if (this._composeYAML === undefined) {
|
if (this._composeYAML === undefined) {
|
||||||
try {
|
try {
|
||||||
this._composeYAML = fs.readFileSync(path.join(this.path, this._composeFileName), "utf-8");
|
this._composeYAML = fs.readFileSync(
|
||||||
|
path.join(this.path, this._composeFileName),
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._composeYAML = "";
|
this._composeYAML = "";
|
||||||
}
|
}
|
||||||
|
@ -180,10 +209,13 @@ export class Stack {
|
||||||
return this._composeYAML;
|
return this._composeYAML;
|
||||||
}
|
}
|
||||||
|
|
||||||
get composeENV() : string {
|
get composeENV(): string {
|
||||||
if (this._composeENV === undefined) {
|
if (this._composeENV === undefined) {
|
||||||
try {
|
try {
|
||||||
this._composeENV = fs.readFileSync(path.join(this.path, ".env"), "utf-8");
|
this._composeENV = fs.readFileSync(
|
||||||
|
path.join(this.path, ".env"),
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._composeENV = "";
|
this._composeENV = "";
|
||||||
}
|
}
|
||||||
|
@ -191,11 +223,11 @@ export class Stack {
|
||||||
return this._composeENV;
|
return this._composeENV;
|
||||||
}
|
}
|
||||||
|
|
||||||
get path() : string {
|
get path(): string {
|
||||||
return path.join(this.server.stacksDir, this.name);
|
return path.join(this.server.stacksDir, this.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
get fullPath() : string {
|
get fullPath(): string {
|
||||||
let dir = this.path;
|
let dir = this.path;
|
||||||
|
|
||||||
// Compose up via node-pty
|
// Compose up via node-pty
|
||||||
|
@ -214,7 +246,7 @@ export class Stack {
|
||||||
* Save the stack to the disk
|
* Save the stack to the disk
|
||||||
* @param isAdd
|
* @param isAdd
|
||||||
*/
|
*/
|
||||||
async save(isAdd : boolean) {
|
async save(isAdd: boolean) {
|
||||||
this.validate();
|
this.validate();
|
||||||
|
|
||||||
let dir = this.path;
|
let dir = this.path;
|
||||||
|
@ -228,43 +260,64 @@ export class Stack {
|
||||||
// Create the stack folder
|
// Create the stack folder
|
||||||
await fsAsync.mkdir(dir);
|
await fsAsync.mkdir(dir);
|
||||||
} else {
|
} else {
|
||||||
if (!await fileExists(dir)) {
|
if (!(await fileExists(dir))) {
|
||||||
throw new ValidationError("Stack not found");
|
throw new ValidationError("Stack not found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write or overwrite the compose.yaml
|
// Write or overwrite the compose.yaml
|
||||||
await fsAsync.writeFile(path.join(dir, this._composeFileName), this.composeYAML);
|
await fsAsync.writeFile(
|
||||||
|
path.join(dir, this._composeFileName),
|
||||||
|
this.composeYAML
|
||||||
|
);
|
||||||
|
|
||||||
const envPath = path.join(dir, ".env");
|
const envPath = path.join(dir, ".env");
|
||||||
|
|
||||||
// Write or overwrite the .env
|
// Write or overwrite the .env
|
||||||
// If .env is not existing and the composeENV is empty, we don't need to write it
|
// If .env is not existing and the composeENV is empty, we don't need to write it
|
||||||
if (await fileExists(envPath) || this.composeENV.trim() !== "") {
|
if ((await fileExists(envPath)) || this.composeENV.trim() !== "") {
|
||||||
await fsAsync.writeFile(envPath, this.composeENV);
|
await fsAsync.writeFile(envPath, this.composeENV);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deploy(socket : DockgeSocket) : Promise<number> {
|
async deploy(socket: DockgeSocket): Promise<number> {
|
||||||
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
|
const terminalName = getComposeTerminalName(socket.endpoint, 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) {
|
if (exitCode !== 0) {
|
||||||
throw new Error("Failed to deploy, please check the terminal output for more information.");
|
throw new Error(
|
||||||
|
"Failed to deploy, please check the terminal output for more information."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return exitCode;
|
return exitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(socket: DockgeSocket) : Promise<number> {
|
async delete(socket: DockgeSocket): Promise<number> {
|
||||||
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
|
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
|
||||||
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down", "--remove-orphans" ], this.path);
|
let exitCode = await Terminal.exec(
|
||||||
|
this.server,
|
||||||
|
socket,
|
||||||
|
terminalName,
|
||||||
|
"docker",
|
||||||
|
["compose", "down", "--remove-orphans"],
|
||||||
|
this.path
|
||||||
|
);
|
||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
throw new Error("Failed to delete, please check the terminal output for more information.");
|
throw new Error(
|
||||||
|
"Failed to delete, please check the terminal output for more information."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the stack folder
|
// Remove the stack folder
|
||||||
await fsAsync.rm(this.path, {
|
await fsAsync.rm(this.path, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
force: true
|
force: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return exitCode;
|
return exitCode;
|
||||||
|
@ -285,15 +338,13 @@ export class Stack {
|
||||||
* Checks if a compose file exists in the specified directory.
|
* Checks if a compose file exists in the specified directory.
|
||||||
* @async
|
* @async
|
||||||
* @static
|
* @static
|
||||||
* @param {string} stacksDir - The directory of the stack.
|
* @param {string} dir - The directory to search.
|
||||||
* @param {string} filename - The name of the directory to check for the compose file.
|
|
||||||
* @returns {Promise<boolean>} A promise that resolves to a boolean indicating whether any compose file exists.
|
* @returns {Promise<boolean>} A promise that resolves to a boolean indicating whether any compose file exists.
|
||||||
*/
|
*/
|
||||||
static async composeFileExists(stacksDir : string, filename : string) : Promise<boolean> {
|
static async composeFileExists(dir: string): Promise<boolean> {
|
||||||
let filenamePath = path.join(stacksDir, filename);
|
|
||||||
// Check if any compose file exists
|
// Check if any compose file exists
|
||||||
for (const filename of acceptedComposeFileNames) {
|
for (const filename of acceptedComposeFileNames) {
|
||||||
let composeFile = path.join(filenamePath, filename);
|
let composeFile = path.join(dir, filename);
|
||||||
if (await fileExists(composeFile)) {
|
if (await fileExists(composeFile)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -301,9 +352,15 @@ export class Stack {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getStackList(server : DockgeServer, useCacheForManaged = false) : Promise<Map<string, Stack>> {
|
static async getStackList(
|
||||||
|
server: DockgeServer,
|
||||||
|
useCacheForManaged = false,
|
||||||
|
depth = 0,
|
||||||
|
dir = ""
|
||||||
|
): Promise<Map<string, Stack>> {
|
||||||
let stacksDir = server.stacksDir;
|
let stacksDir = server.stacksDir;
|
||||||
let stackList : Map<string, Stack>;
|
let currentDir = dir ? path.join(stacksDir, dir) : stacksDir;
|
||||||
|
let stackList: Map<string, Stack>;
|
||||||
|
|
||||||
// Use cached stack list?
|
// Use cached stack list?
|
||||||
if (useCacheForManaged && this.managedStackList.size > 0) {
|
if (useCacheForManaged && this.managedStackList.size > 0) {
|
||||||
|
@ -312,25 +369,50 @@ export class Stack {
|
||||||
stackList = new Map<string, Stack>();
|
stackList = new Map<string, Stack>();
|
||||||
|
|
||||||
// Scan the stacks directory, and get the stack list
|
// Scan the stacks directory, and get the stack list
|
||||||
let filenameList = await fsAsync.readdir(stacksDir);
|
let filenameList = await fsAsync.readdir(currentDir);
|
||||||
|
|
||||||
for (let filename of filenameList) {
|
for (let filename of filenameList) {
|
||||||
try {
|
try {
|
||||||
// Check if it is a directory
|
// Check if it is a directory
|
||||||
let stat = await fsAsync.stat(path.join(stacksDir, filename));
|
let stat = await fsAsync.stat(
|
||||||
|
path.join(currentDir, filename)
|
||||||
|
);
|
||||||
if (!stat.isDirectory()) {
|
if (!stat.isDirectory()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// If no compose file exists, skip it
|
// If no compose file exists, skip it
|
||||||
if (!await Stack.composeFileExists(stacksDir, filename)) {
|
if (
|
||||||
continue;
|
!(await Stack.composeFileExists(
|
||||||
|
path.join(currentDir, filename)
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
if (depth >= 3) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
let subStackList = await this.getStackList(
|
||||||
|
server,
|
||||||
|
useCacheForManaged,
|
||||||
|
depth + 1,
|
||||||
|
path.join(dir, filename)
|
||||||
|
);
|
||||||
|
for (let [subFilename, subStack] of subStackList) {
|
||||||
|
stackList.set(subFilename, subStack);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let stack = await this.getStack(server, filename);
|
let stack = await this.getStack(
|
||||||
|
server,
|
||||||
|
path.join(dir, filename)
|
||||||
|
);
|
||||||
stack._status = CREATED_FILE;
|
stack._status = CREATED_FILE;
|
||||||
stackList.set(filename, stack);
|
stackList.set(path.join(dir, filename), stack);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
log.warn("getStackList", `Failed to get stack ${filename}, error: ${e.message}`);
|
log.warn(
|
||||||
|
"getStackList",
|
||||||
|
`Failed to get stack ${filename}, error: ${e.message}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -340,9 +422,13 @@ export class Stack {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get status from docker compose ls
|
// Get status from docker compose ls
|
||||||
let res = await childProcessAsync.spawn("docker", [ "compose", "ls", "--all", "--format", "json" ], {
|
let res = await childProcessAsync.spawn(
|
||||||
encoding: "utf-8",
|
"docker",
|
||||||
});
|
["compose", "ls", "--all", "--format", "json"],
|
||||||
|
{
|
||||||
|
encoding: "utf-8",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!res.stdout) {
|
if (!res.stdout) {
|
||||||
return stackList;
|
return stackList;
|
||||||
|
@ -351,7 +437,21 @@ export class Stack {
|
||||||
let composeList = JSON.parse(res.stdout.toString());
|
let composeList = JSON.parse(res.stdout.toString());
|
||||||
|
|
||||||
for (let composeStack of composeList) {
|
for (let composeStack of composeList) {
|
||||||
let stack = stackList.get(composeStack.Name);
|
let stackName = composeStack.Name;
|
||||||
|
|
||||||
|
// Adjust the stack name based on the config file path
|
||||||
|
// Get the first config file path
|
||||||
|
let configFile = composeStack.ConfigFiles.split(",")[0];
|
||||||
|
// Just use the stack name if the config file path is not in the stacks directory
|
||||||
|
if (configFile.trim().startsWith(server.stacksDir) && acceptedComposeFileNames.some(file => configFile.trim().endsWith(file))) {
|
||||||
|
const relativePath = path.relative(server.stacksDir, configFile.trim());
|
||||||
|
const match = relativePath.match(new RegExp("^(.+)/(.+)\\.ya?ml$"));
|
||||||
|
if (match) {
|
||||||
|
stackName = match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let stack = stackList.get(stackName);
|
||||||
|
|
||||||
// This stack probably is not managed by Dockge, but we still want to show it
|
// This stack probably is not managed by Dockge, but we still want to show it
|
||||||
if (!stack) {
|
if (!stack) {
|
||||||
|
@ -359,8 +459,8 @@ export class Stack {
|
||||||
if (composeStack.Name === "dockge") {
|
if (composeStack.Name === "dockge") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
stack = new Stack(server, composeStack.Name);
|
stack = new Stack(server, stackName);
|
||||||
stackList.set(composeStack.Name, stack);
|
stackList.set(stackName, stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
stack._status = this.statusConvert(composeStack.Status);
|
stack._status = this.statusConvert(composeStack.Status);
|
||||||
|
@ -374,12 +474,16 @@ export class Stack {
|
||||||
* Get the status list, it will be used to update the status of the stacks
|
* Get the status list, it will be used to update the status of the stacks
|
||||||
* Not all status will be returned, only the stack that is deployed or created to `docker compose` will be returned
|
* Not all status will be returned, only the stack that is deployed or created to `docker compose` will be returned
|
||||||
*/
|
*/
|
||||||
static async getStatusList() : Promise<Map<string, number>> {
|
static async getStatusList(): Promise<Map<string, number>> {
|
||||||
let statusList = new Map<string, number>();
|
let statusList = new Map<string, number>();
|
||||||
|
|
||||||
let res = await childProcessAsync.spawn("docker", [ "compose", "ls", "--all", "--format", "json" ], {
|
let res = await childProcessAsync.spawn(
|
||||||
encoding: "utf-8",
|
"docker",
|
||||||
});
|
["compose", "ls", "--all", "--format", "json"],
|
||||||
|
{
|
||||||
|
encoding: "utf-8",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!res.stdout) {
|
if (!res.stdout) {
|
||||||
return statusList;
|
return statusList;
|
||||||
|
@ -388,7 +492,10 @@ export class Stack {
|
||||||
let composeList = JSON.parse(res.stdout.toString());
|
let composeList = JSON.parse(res.stdout.toString());
|
||||||
|
|
||||||
for (let composeStack of composeList) {
|
for (let composeStack of composeList) {
|
||||||
statusList.set(composeStack.Name, this.statusConvert(composeStack.Status));
|
statusList.set(
|
||||||
|
composeStack.Name,
|
||||||
|
this.statusConvert(composeStack.Status)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return statusList;
|
return statusList;
|
||||||
|
@ -399,7 +506,7 @@ export class Stack {
|
||||||
* Input Example: "exited(1), running(1)"
|
* Input Example: "exited(1), running(1)"
|
||||||
* @param status
|
* @param status
|
||||||
*/
|
*/
|
||||||
static statusConvert(status : string) : number {
|
static statusConvert(status: string): number {
|
||||||
if (status.startsWith("created")) {
|
if (status.startsWith("created")) {
|
||||||
return CREATED_STACK;
|
return CREATED_STACK;
|
||||||
} else if (status.includes("exited")) {
|
} else if (status.includes("exited")) {
|
||||||
|
@ -413,11 +520,18 @@ export class Stack {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getStack(server: DockgeServer, stackName: string, skipFSOperations = false) : Promise<Stack> {
|
static async getStack(
|
||||||
|
server: DockgeServer,
|
||||||
|
stackName: string,
|
||||||
|
skipFSOperations = false
|
||||||
|
): Promise<Stack> {
|
||||||
let dir = path.join(server.stacksDir, stackName);
|
let dir = path.join(server.stacksDir, stackName);
|
||||||
|
|
||||||
if (!skipFSOperations) {
|
if (!skipFSOperations) {
|
||||||
if (!await fileExists(dir) || !(await fsAsync.stat(dir)).isDirectory()) {
|
if (
|
||||||
|
!(await fileExists(dir)) ||
|
||||||
|
!(await fsAsync.stat(dir)).isDirectory()
|
||||||
|
) {
|
||||||
// Maybe it is a stack managed by docker compose directly
|
// Maybe it is a stack managed by docker compose directly
|
||||||
let stackList = await this.getStackList(server, true);
|
let stackList = await this.getStackList(server, true);
|
||||||
let stack = stackList.get(stackName);
|
let stack = stackList.get(stackName);
|
||||||
|
@ -433,7 +547,7 @@ export class Stack {
|
||||||
//log.debug("getStack", "Skip FS operations");
|
//log.debug("getStack", "Skip FS operations");
|
||||||
}
|
}
|
||||||
|
|
||||||
let stack : Stack;
|
let stack: Stack;
|
||||||
|
|
||||||
if (!skipFSOperations) {
|
if (!skipFSOperations) {
|
||||||
stack = new Stack(server, stackName);
|
stack = new Stack(server, stackName);
|
||||||
|
@ -448,36 +562,72 @@ export class Stack {
|
||||||
|
|
||||||
async start(socket: DockgeSocket) {
|
async start(socket: DockgeSocket) {
|
||||||
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
|
const terminalName = getComposeTerminalName(socket.endpoint, 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) {
|
if (exitCode !== 0) {
|
||||||
throw new Error("Failed to start, please check the terminal output for more information.");
|
throw new Error(
|
||||||
|
"Failed to start, please check the terminal output for more information."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return exitCode;
|
return exitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop(socket: DockgeSocket) : Promise<number> {
|
async stop(socket: DockgeSocket): Promise<number> {
|
||||||
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
|
const terminalName = getComposeTerminalName(socket.endpoint, 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) {
|
if (exitCode !== 0) {
|
||||||
throw new Error("Failed to stop, please check the terminal output for more information.");
|
throw new Error(
|
||||||
|
"Failed to stop, please check the terminal output for more information."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return exitCode;
|
return exitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
async restart(socket: DockgeSocket) : Promise<number> {
|
async restart(socket: DockgeSocket): Promise<number> {
|
||||||
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
|
const terminalName = getComposeTerminalName(socket.endpoint, 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) {
|
if (exitCode !== 0) {
|
||||||
throw new Error("Failed to restart, please check the terminal output for more information.");
|
throw new Error(
|
||||||
|
"Failed to restart, please check the terminal output for more information."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return exitCode;
|
return exitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
async down(socket: DockgeSocket) : Promise<number> {
|
async down(socket: DockgeSocket): Promise<number> {
|
||||||
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
|
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
|
||||||
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down" ], this.path);
|
let exitCode = await Terminal.exec(
|
||||||
|
this.server,
|
||||||
|
socket,
|
||||||
|
terminalName,
|
||||||
|
"docker",
|
||||||
|
["compose", "down"],
|
||||||
|
this.path
|
||||||
|
);
|
||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
throw new Error("Failed to down, please check the terminal output for more information.");
|
throw new Error(
|
||||||
|
"Failed to down, please check the terminal output for more information."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return exitCode;
|
return exitCode;
|
||||||
}
|
}
|
||||||
|
@ -485,9 +635,35 @@ export class Stack {
|
||||||
async update(socket: DockgeSocket) {
|
async update(socket: DockgeSocket) {
|
||||||
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
|
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
|
||||||
|
|
||||||
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "pull" ], this.path);
|
if (this.isGitRepo) {
|
||||||
|
// TODO: error handling e.g. local changes
|
||||||
|
let exitCode = await Terminal.exec(
|
||||||
|
this.server,
|
||||||
|
socket,
|
||||||
|
terminalName,
|
||||||
|
"git",
|
||||||
|
["pull"],
|
||||||
|
this.path
|
||||||
|
);
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
"Failed to update, please check the terminal output for more information."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let exitCode = await Terminal.exec(
|
||||||
|
this.server,
|
||||||
|
socket,
|
||||||
|
terminalName,
|
||||||
|
"docker",
|
||||||
|
["compose", "pull"],
|
||||||
|
this.path
|
||||||
|
);
|
||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
throw new Error("Failed to pull, please check the terminal output for more information.");
|
throw new Error(
|
||||||
|
"Failed to pull, please check the terminal output for more information."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the stack is not running, we don't need to restart it
|
// If the stack is not running, we don't need to restart it
|
||||||
|
@ -497,9 +673,18 @@ export class Stack {
|
||||||
return exitCode;
|
return exitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
if (exitCode !== 0) {
|
||||||
throw new Error("Failed to restart, please check the terminal output for more information.");
|
throw new Error(
|
||||||
|
"Failed to restart, please check the terminal output for more information."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return exitCode;
|
return exitCode;
|
||||||
}
|
}
|
||||||
|
@ -553,8 +738,17 @@ export class Stack {
|
||||||
}
|
}
|
||||||
|
|
||||||
async joinCombinedTerminal(socket: DockgeSocket) {
|
async joinCombinedTerminal(socket: DockgeSocket) {
|
||||||
const terminalName = getCombinedTerminalName(socket.endpoint, this.name);
|
const terminalName = getCombinedTerminalName(
|
||||||
const terminal = Terminal.getOrCreateTerminal(this.server, terminalName, "docker", [ "compose", "logs", "-f", "--tail", "100" ], this.path);
|
socket.endpoint,
|
||||||
|
this.name
|
||||||
|
);
|
||||||
|
const terminal = Terminal.getOrCreateTerminal(
|
||||||
|
this.server,
|
||||||
|
terminalName,
|
||||||
|
"docker",
|
||||||
|
["compose", "logs", "-f", "--tail", "100"],
|
||||||
|
this.path
|
||||||
|
);
|
||||||
terminal.enableKeepAlive = true;
|
terminal.enableKeepAlive = true;
|
||||||
terminal.rows = COMBINED_TERMINAL_ROWS;
|
terminal.rows = COMBINED_TERMINAL_ROWS;
|
||||||
terminal.cols = COMBINED_TERMINAL_COLS;
|
terminal.cols = COMBINED_TERMINAL_COLS;
|
||||||
|
@ -563,19 +757,38 @@ export class Stack {
|
||||||
}
|
}
|
||||||
|
|
||||||
async leaveCombinedTerminal(socket: DockgeSocket) {
|
async leaveCombinedTerminal(socket: DockgeSocket) {
|
||||||
const terminalName = getCombinedTerminalName(socket.endpoint, this.name);
|
const terminalName = getCombinedTerminalName(
|
||||||
|
socket.endpoint,
|
||||||
|
this.name
|
||||||
|
);
|
||||||
const terminal = Terminal.getTerminal(terminalName);
|
const terminal = Terminal.getTerminal(terminalName);
|
||||||
if (terminal) {
|
if (terminal) {
|
||||||
terminal.leave(socket);
|
terminal.leave(socket);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async joinContainerTerminal(socket: DockgeSocket, serviceName: string, shell : string = "sh", index: number = 0) {
|
async joinContainerTerminal(
|
||||||
const terminalName = getContainerExecTerminalName(socket.endpoint, this.name, serviceName, index);
|
socket: DockgeSocket,
|
||||||
|
serviceName: string,
|
||||||
|
shell: string = "sh",
|
||||||
|
index: number = 0
|
||||||
|
) {
|
||||||
|
const terminalName = getContainerExecTerminalName(
|
||||||
|
socket.endpoint,
|
||||||
|
this.name,
|
||||||
|
serviceName,
|
||||||
|
index
|
||||||
|
);
|
||||||
let terminal = Terminal.getTerminal(terminalName);
|
let terminal = Terminal.getTerminal(terminalName);
|
||||||
|
|
||||||
if (!terminal) {
|
if (!terminal) {
|
||||||
terminal = new InteractiveTerminal(this.server, terminalName, "docker", [ "compose", "exec", serviceName, shell ], this.path);
|
terminal = new InteractiveTerminal(
|
||||||
|
this.server,
|
||||||
|
terminalName,
|
||||||
|
"docker",
|
||||||
|
["compose", "exec", serviceName, shell],
|
||||||
|
this.path
|
||||||
|
);
|
||||||
terminal.rows = TERMINAL_ROWS;
|
terminal.rows = TERMINAL_ROWS;
|
||||||
log.debug("joinContainerTerminal", "Terminal created");
|
log.debug("joinContainerTerminal", "Terminal created");
|
||||||
}
|
}
|
||||||
|
@ -588,10 +801,14 @@ export class Stack {
|
||||||
let statusList = new Map<string, number>();
|
let statusList = new Map<string, number>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let res = await childProcessAsync.spawn("docker", [ "compose", "ps", "--format", "json" ], {
|
let res = await childProcessAsync.spawn(
|
||||||
cwd: this.path,
|
"docker",
|
||||||
encoding: "utf-8",
|
["compose", "ps", "--format", "json"],
|
||||||
});
|
{
|
||||||
|
cwd: this.path,
|
||||||
|
encoding: "utf-8",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!res.stdout) {
|
if (!res.stdout) {
|
||||||
return statusList;
|
return statusList;
|
||||||
|
@ -607,8 +824,7 @@ export class Stack {
|
||||||
} else {
|
} else {
|
||||||
statusList.set(obj.Service, obj.Health);
|
statusList.set(obj.Service, obj.Health);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return statusList;
|
return statusList;
|
||||||
|
@ -616,6 +832,5 @@ export class Stack {
|
||||||
log.error("getServiceStatusList", e);
|
log.error("getServiceStatusList", e);
|
||||||
return statusList;
|
return statusList;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,9 +61,9 @@ export default {
|
||||||
},
|
},
|
||||||
url() {
|
url() {
|
||||||
if (this.stack.endpoint) {
|
if (this.stack.endpoint) {
|
||||||
return `/compose/${this.stack.name}/${this.stack.endpoint}`;
|
return `/compose/${encodeURIComponent(this.stack.name)}/${this.stack.endpoint}`;
|
||||||
} else {
|
} else {
|
||||||
return `/compose/${this.stack.name}`;
|
return `/compose/${encodeURIComponent(this.stack.name)}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
depthMargin() {
|
depthMargin() {
|
||||||
|
|
|
@ -473,9 +473,9 @@ export default {
|
||||||
|
|
||||||
url() {
|
url() {
|
||||||
if (this.stack.endpoint) {
|
if (this.stack.endpoint) {
|
||||||
return `/compose/${this.stack.name}/${this.stack.endpoint}`;
|
return `/compose/${encodeURIComponent(this.stack.name)}/${this.stack.endpoint}`;
|
||||||
} else {
|
} else {
|
||||||
return `/compose/${this.stack.name}`;
|
return `/compose/${encodeURIComponent(this.stack.name)}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue