Compare commits

..

1 Commits

Author SHA1 Message Date
491059cebf Add "docker compose down" 2023-11-21 15:12:06 +08:00
8 changed files with 99 additions and 82 deletions

View File

@ -103,10 +103,6 @@ class Logger {
* @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized.
*/
log(module: string, msg: unknown, level: string) {
if (level === "DEBUG" && !isDev) {
return;
}
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
return;
}

View File

@ -187,6 +187,27 @@ export class DockerSocketHandler extends SocketHandler {
}
});
// down stack
socket.on("downStack", 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.down(socket);
callback({
ok: true,
msg: "Downed"
});
server.sendStackList();
} catch (e) {
callbackError(e, callback);
}
});
// Services status
socket.on("serviceStatusList", async (stackName : unknown, callback) => {
try {
@ -196,7 +217,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string");
}
const stack = Stack.getStack(server, stackName, true);
const stack = Stack.getStack(server, stackName);
const serviceStatusList = Object.fromEntries(await stack.getServiceStatusList());
callback({
ok: true,

View File

@ -31,19 +31,17 @@ export class Stack {
protected static managedStackList: Map<string, Stack> = new Map();
constructor(server : DockgeServer, name : string, composeYAML? : string, skipFSOperations = false) {
constructor(server : DockgeServer, name : string, composeYAML? : string) {
this.name = name;
this.server = server;
this._composeYAML = composeYAML;
if (!skipFSOperations) {
// Check if compose file name is different from compose.yaml
const supportedFileNames = [ "compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml" ];
for (const filename of supportedFileNames) {
if (fs.existsSync(path.join(this.path, filename))) {
this._composeFileName = filename;
break;
}
// Check if compose file name is different from compose.yaml
const supportedFileNames = [ "compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml" ];
for (const filename of supportedFileNames) {
if (fs.existsSync(path.join(this.path, filename))) {
this._composeFileName = filename;
break;
}
}
}
@ -283,34 +281,23 @@ export class Stack {
}
}
static getStack(server: DockgeServer, stackName: string, skipFSOperations = false) : Stack {
static getStack(server: DockgeServer, stackName: string) : Stack {
let dir = path.join(server.stacksDir, stackName);
if (!skipFSOperations) {
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
// Maybe it is a stack managed by docker compose directly
let stackList = this.getStackList(server, true);
let stack = stackList.get(stackName);
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
// Maybe it is a stack managed by docker compose directly
let stackList = this.getStackList(server);
let stack = stackList.get(stackName);
if (stack) {
return stack;
} else {
// Really not found
throw new ValidationError("Stack not found");
}
if (stack) {
return stack;
} else {
// Really not found
throw new ValidationError("Stack not found");
}
} else {
log.debug("getStack", "Skip FS operations");
}
let stack : Stack;
if (!skipFSOperations) {
stack = new Stack(server, stackName);
} else {
stack = new Stack(server, stackName, undefined, true);
}
let stack = new Stack(server, stackName);
stack._status = UNKNOWN;
stack._configFilePath = path.resolve(dir);
return stack;
@ -343,6 +330,15 @@ export class Stack {
return exitCode;
}
async down(socket: DockgeSocket) : Promise<number> {
const terminalName = getComposeTerminalName(this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down" ], this.path);
if (exitCode !== 0) {
throw new Error("Failed to down, please check the terminal output for more information.");
}
return exitCode;
}
async update(socket: DockgeSocket) {
const terminalName = getComposeTerminalName(this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "pull" ], this.path);
@ -390,11 +386,11 @@ export class Stack {
async getServiceStatusList() {
let statusList = new Map<string, number>();
let res = childProcess.spawnSync("docker", [ "compose", "ps", "--format", "json" ], {
let res = childProcess.execSync("docker compose ps --format json", {
cwd: this.path,
});
let lines = res.stdout.toString().split("\n");
let lines = res.toString().split("\n");
for (let line of lines) {
try {

View File

@ -80,53 +80,37 @@ export class Terminal {
return;
}
try {
this._ptyProcess = pty.spawn(this.file, this.args, {
name: this.name,
cwd: this.cwd,
cols: TERMINAL_COLS,
rows: this.rows,
});
this._ptyProcess = pty.spawn(this.file, this.args, {
name: this.name,
cwd: this.cwd,
cols: TERMINAL_COLS,
rows: this.rows,
});
// On Data
this._ptyProcess.onData((data) => {
this.buffer.pushItem(data);
if (this.server.io) {
this.server.io.to(this.name).emit("terminalWrite", this.name, data);
}
});
// On Exit
this._ptyProcess.onExit(this.exit);
} catch (error) {
if (error instanceof Error) {
log.error("Terminal", "Failed to start terminal: " + error.message);
const exitCode = Number(error.message.split(" ").pop());
this.exit({
exitCode,
});
// On Data
this._ptyProcess.onData((data) => {
this.buffer.pushItem(data);
if (this.server.io) {
this.server.io.to(this.name).emit("terminalWrite", this.name, data);
}
}
});
// On Exit
this._ptyProcess.onExit((res) => {
this.server.io.to(this.name).emit("terminalExit", this.name, res.exitCode);
// Remove room
this.server.io.in(this.name).socketsLeave(this.name);
Terminal.terminalMap.delete(this.name);
log.debug("Terminal", "Terminal " + this.name + " exited with code " + res.exitCode);
if (this.callback) {
this.callback(res.exitCode);
}
});
}
/**
* Exit event handler
* @param res
*/
protected exit = (res : {exitCode: number, signal?: number | undefined}) => {
this.server.io.to(this.name).emit("terminalExit", this.name, res.exitCode);
// Remove room
this.server.io.in(this.name).socketsLeave(this.name);
Terminal.terminalMap.delete(this.name);
log.debug("Terminal", "Terminal " + this.name + " exited with code " + res.exitCode);
if (this.callback) {
this.callback(res.exitCode);
}
};
public onExit(callback : (exitCode : number) => void) {
this.callback = callback;
}

View File

@ -11,6 +11,9 @@ declare module 'vue' {
Appearance: typeof import('./src/components/settings/Appearance.vue')['default']
ArrayInput: typeof import('./src/components/ArrayInput.vue')['default']
ArraySelect: typeof import('./src/components/ArraySelect.vue')['default']
BDropdown: typeof import('bootstrap-vue-next')['BDropdown']
BDropdownDivider: typeof import('bootstrap-vue-next')['BDropdownDivider']
BDropdownItem: typeof import('bootstrap-vue-next')['BDropdownItem']
BModal: typeof import('bootstrap-vue-next')['BModal']
Confirm: typeof import('./src/components/Confirm.vue')['default']
Container: typeof import('./src/components/Container.vue')['default']

View File

@ -19,6 +19,7 @@
"restartStack": "Restart",
"updateStack": "Update",
"startStack": "Start",
"downStack": "Stop & Down",
"editStack": "Edit",
"discardStack": "Discard",
"saveStackDraft": "Save",

View File

@ -40,6 +40,13 @@
<font-awesome-icon icon="stop" class="me-1" />
{{ $t("stopStack") }}
</button>
<BDropdown v-if="!isEditMode && active" right text="" variant="normal">
<BDropdownItem @click="downStack">
<font-awesome-icon icon="stop" class="me-1" />
{{ $t("downStack") }}
</BDropdownItem>
</BDropdown>
</div>
<button v-if="isEditMode && !isAdd" class="btn btn-normal" :disabled="processing" @click="discardStack">{{ $t("discardStack") }}</button>
@ -473,6 +480,15 @@ export default {
});
},
downStack() {
this.processing = true;
this.$root.getSocket().emit("downStack", this.stack.name, (res) => {
this.processing = false;
this.$root.toastRes(res);
});
},
restartStack() {
this.processing = true;

View File

@ -7,7 +7,7 @@
"lint": "eslint \"**/*.{ts,vue}\"",
"check-ts": "tsc --noEmit",
"start": "tsx ./backend/index.ts",
"dev:backend": "cross-env NODE_ENV=development tsx watch --inspect ./backend/index.ts",
"dev:backend": "cross-env NODE_ENV=development tsx watch ./backend/index.ts",
"dev:frontend": "cross-env NODE_ENV=development vite --host --config ./frontend/vite.config.ts",
"release-final": "tsx ./extra/test-docker.ts && tsx extra/update-version.ts && pnpm run build:frontend && npm run build:docker",
"build:frontend": "vite build --config ./frontend/vite.config.ts",