mirror of
https://github.com/louislam/dockge.git
synced 2024-11-24 03:44:03 +00:00
add git-managed stacks (pull only)
This commit is contained in:
parent
c7ea2f9ee9
commit
ce79965d0c
14 changed files with 521 additions and 68 deletions
|
@ -3,6 +3,8 @@ import { DockgeServer } from "../dockge-server";
|
|||
import { callbackError, callbackResult, checkLogin, DockgeSocket, ValidationError } from "../util-server";
|
||||
import { Stack } from "../stack";
|
||||
import { AgentSocket } from "../../common/agent-socket";
|
||||
import { Terminal } from "../terminal";
|
||||
import { getComposeTerminalName } from "../../common/util-common";
|
||||
|
||||
export class DockerSocketHandler extends AgentSocketHandler {
|
||||
create(socket : DockgeSocket, server : DockgeServer, agentSocket : AgentSocket) {
|
||||
|
@ -24,6 +26,47 @@ export class DockerSocketHandler extends AgentSocketHandler {
|
|||
}
|
||||
});
|
||||
|
||||
agentSocket.on("gitDeployStack", async (stackName : unknown, gitUrl : unknown, branch : unknown, isAdd : unknown, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
if (typeof(stackName) !== "string") {
|
||||
throw new ValidationError("Stack name must be a string");
|
||||
}
|
||||
if (typeof(gitUrl) !== "string") {
|
||||
throw new ValidationError("Git URL must be a string");
|
||||
}
|
||||
if (typeof(branch) !== "string") {
|
||||
throw new ValidationError("Git Ref must be a string");
|
||||
}
|
||||
|
||||
const terminalName = getComposeTerminalName(socket.endpoint, stackName);
|
||||
|
||||
// TODO: this could be done smarter.
|
||||
if (!isAdd) {
|
||||
const stack = await Stack.getStack(server, stackName);
|
||||
await stack.delete(socket);
|
||||
}
|
||||
|
||||
let exitCode = await Terminal.exec(server, socket, terminalName, "git", [ "clone", "-b", branch, gitUrl, stackName ], server.stacksDir);
|
||||
if (exitCode !== 0) {
|
||||
throw new Error("Failed to clone git repo");
|
||||
}
|
||||
|
||||
const stack = await Stack.getStack(server, stackName);
|
||||
await stack.deploy(socket);
|
||||
|
||||
server.sendStackList();
|
||||
callbackResult({
|
||||
ok: true,
|
||||
msg: "Deployed"
|
||||
}, callback);
|
||||
stack.joinCombinedTerminal(socket);
|
||||
} catch (e) {
|
||||
callbackError(e, callback);
|
||||
}
|
||||
});
|
||||
|
||||
agentSocket.on("saveStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
@ -188,6 +231,27 @@ export class DockerSocketHandler extends AgentSocketHandler {
|
|||
}
|
||||
});
|
||||
|
||||
// gitSync
|
||||
agentSocket.on("gitSync", async (stackName : unknown, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
if (typeof(stackName) !== "string") {
|
||||
throw new ValidationError("Stack name must be a string");
|
||||
}
|
||||
|
||||
const stack = await Stack.getStack(server, stackName);
|
||||
await stack.gitSync(socket);
|
||||
callbackResult({
|
||||
ok: true,
|
||||
msg: "Synced"
|
||||
}, callback);
|
||||
server.sendStackList();
|
||||
} catch (e) {
|
||||
callbackError(e, callback);
|
||||
}
|
||||
});
|
||||
|
||||
// down stack
|
||||
agentSocket.on("downStack", async (stackName : unknown, callback) => {
|
||||
try {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import "dotenv/config";
|
||||
import { MainRouter } from "./routers/main-router";
|
||||
import { WebhookRouter } from "./routers/webhook-router";
|
||||
import * as fs from "node:fs";
|
||||
import { PackageJson } from "type-fest";
|
||||
import { Database } from "./database";
|
||||
|
@ -21,7 +22,7 @@ import { R } from "redbean-node";
|
|||
import { genSecret, isDev, LooseObject } from "../common/util-common";
|
||||
import { generatePasswordHash } from "./password-hash";
|
||||
import { Bean } from "redbean-node/dist/bean";
|
||||
import { Arguments, Config, DockgeSocket } from "./util-server";
|
||||
import { Arguments, Config, DockgeSocket, ValidationError } from "./util-server";
|
||||
import { DockerSocketHandler } from "./agent-socket-handlers/docker-socket-handler";
|
||||
import expressStaticGzip from "express-static-gzip";
|
||||
import path from "path";
|
||||
|
@ -38,6 +39,8 @@ import { AgentSocket } from "../common/agent-socket";
|
|||
import { ManageAgentSocketHandler } from "./socket-handlers/manage-agent-socket-handler";
|
||||
import { Terminal } from "./terminal";
|
||||
|
||||
const GIT_UPDATE_CHECKER_INTERVAL_MS = 1000 * 60 * 10;
|
||||
|
||||
export class DockgeServer {
|
||||
app : Express;
|
||||
httpServer : http.Server;
|
||||
|
@ -45,12 +48,14 @@ export class DockgeServer {
|
|||
io : socketIO.Server;
|
||||
config : Config;
|
||||
indexHTML : string = "";
|
||||
gitUpdateInterval? : NodeJS.Timeout;
|
||||
|
||||
/**
|
||||
* List of express routers
|
||||
*/
|
||||
routerList : Router[] = [
|
||||
new MainRouter(),
|
||||
new WebhookRouter(),
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -204,6 +209,17 @@ export class DockgeServer {
|
|||
};
|
||||
}
|
||||
|
||||
// add a middleware to handle errors
|
||||
this.app.use((err : unknown, _req : express.Request, res : express.Response, _next : express.NextFunction) => {
|
||||
if (err instanceof Error) {
|
||||
res.status(500).json({ error: err.message });
|
||||
} else if (err instanceof ValidationError) {
|
||||
res.status(400).json({ error: err.message });
|
||||
} else {
|
||||
res.status(500).json({ error: "Unknown error: " + err });
|
||||
}
|
||||
});
|
||||
|
||||
// Create Socket.io
|
||||
this.io = new socketIO.Server(this.httpServer, {
|
||||
cors,
|
||||
|
@ -398,6 +414,7 @@ export class DockgeServer {
|
|||
});
|
||||
|
||||
checkVersion.startInterval();
|
||||
this.startGitUpdater();
|
||||
});
|
||||
|
||||
gracefulShutdown(this.httpServer, {
|
||||
|
@ -610,6 +627,47 @@ export class DockgeServer {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the git updater. This checks for outdated stacks and updates them.
|
||||
* @param useCache
|
||||
*/
|
||||
async startGitUpdater(useCache = false) {
|
||||
const check = async () => {
|
||||
if (await Settings.get("gitAutoUpdate") !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("git-updater", "checking for outdated stacks");
|
||||
|
||||
let socketList = this.io.sockets.sockets.values();
|
||||
|
||||
let stackList;
|
||||
for (let socket of socketList) {
|
||||
let dockgeSocket = socket as DockgeSocket;
|
||||
|
||||
// Get the list of stacks only once
|
||||
if (!stackList) {
|
||||
stackList = await Stack.getStackList(this, useCache);
|
||||
}
|
||||
|
||||
for (let [ stackName, stack ] of stackList) {
|
||||
|
||||
if (stack.isGitRepo) {
|
||||
stack.checkRemoteChanges().then(async (outdated) => {
|
||||
if (outdated) {
|
||||
log.info("git-updater", `Stack ${stackName} is outdated, Updating...`);
|
||||
await stack.update(dockgeSocket);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await check();
|
||||
this.gitUpdateInterval = setInterval(check, GIT_UPDATE_CHECKER_INTERVAL_MS);
|
||||
}
|
||||
|
||||
async getDockerNetworkList() : Promise<string[]> {
|
||||
let res = await childProcessAsync.spawn("docker", [ "network", "ls", "--format", "{{.Name}}" ], {
|
||||
encoding: "utf-8",
|
||||
|
|
34
backend/routers/webhook-router.ts
Normal file
34
backend/routers/webhook-router.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { DockgeServer } from "../dockge-server";
|
||||
import { log } from "../log";
|
||||
import { Router } from "../router";
|
||||
import express, { Express, Router as ExpressRouter } from "express";
|
||||
import { Stack } from "../stack";
|
||||
|
||||
export class WebhookRouter extends Router {
|
||||
create(app: Express, server: DockgeServer): ExpressRouter {
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/webhook/update/:stackname", async (req, res, _next) => {
|
||||
try {
|
||||
const stackname = req.params.stackname;
|
||||
|
||||
log.info("router", `Webhook received for stack: ${stackname}`);
|
||||
const stack = await Stack.getStack(server, stackname);
|
||||
if (!stack) {
|
||||
log.error("router", `Stack not found: ${stackname}`);
|
||||
res.status(404).json({ message: `Stack not found: ${stackname}` });
|
||||
return;
|
||||
}
|
||||
await stack.gitSync(undefined);
|
||||
|
||||
// Send a response
|
||||
res.json({ message: `Updated stack: ${stackname}` });
|
||||
|
||||
} catch (error) {
|
||||
_next(error);
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
|
@ -19,6 +19,8 @@ import {
|
|||
import { InteractiveTerminal, Terminal } from "./terminal";
|
||||
import childProcessAsync from "promisify-child-process";
|
||||
import { Settings } from "./settings";
|
||||
import { execSync } from "child_process";
|
||||
import ini from "ini";
|
||||
|
||||
export class Stack {
|
||||
|
||||
|
@ -84,6 +86,10 @@ export class Stack {
|
|||
status: this._status,
|
||||
tags: [],
|
||||
isManagedByDockge: this.isManagedByDockge,
|
||||
isGitRepo: this.isGitRepo,
|
||||
gitUrl: this.gitUrl,
|
||||
branch: this.branch,
|
||||
webhook: this.webhook,
|
||||
composeFileName: this._composeFileName,
|
||||
endpoint,
|
||||
};
|
||||
|
@ -107,6 +113,39 @@ export class Stack {
|
|||
return fs.existsSync(this.path) && fs.statSync(this.path).isDirectory();
|
||||
}
|
||||
|
||||
get isGitRepo() : boolean {
|
||||
return fs.existsSync(path.join(this.path, ".git")) && fs.statSync(path.join(this.path, ".git")).isDirectory();
|
||||
}
|
||||
|
||||
get gitUrl() : string {
|
||||
if (this.isGitRepo) {
|
||||
const gitConfig = ini.parse(fs.readFileSync(path.join(this.path, ".git", "config"), "utf-8"));
|
||||
return gitConfig["remote \"origin\""]?.url;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
get branch() : string {
|
||||
if (this.isGitRepo) {
|
||||
try {
|
||||
let stdout = execSync("git branch --show-current", { cwd: this.path });
|
||||
return stdout.toString().trim();
|
||||
} catch (error) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
get webhook() : string {
|
||||
//TODO: refine this.
|
||||
if (this.server.config.hostname) {
|
||||
return `http://${this.server.config.hostname}:${this.server.config.port}/webhook/update/${this.name}`;
|
||||
} else {
|
||||
return `http://localhost:${this.server.config.port}/webhook/update/${this.name}`;
|
||||
}
|
||||
}
|
||||
|
||||
get status() : number {
|
||||
return this._status;
|
||||
}
|
||||
|
@ -445,6 +484,7 @@ export class Stack {
|
|||
|
||||
async update(socket: DockgeSocket) {
|
||||
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
|
||||
|
||||
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.");
|
||||
|
@ -464,6 +504,54 @@ export class Stack {
|
|||
return exitCode;
|
||||
}
|
||||
|
||||
async gitSync(socket?: DockgeSocket) {
|
||||
const terminalName = socket ? getComposeTerminalName(socket.endpoint, this.name) : "";
|
||||
|
||||
if (!this.isGitRepo) {
|
||||
throw new Error("This stack is not a git repository");
|
||||
}
|
||||
|
||||
let exitCode = await Terminal.exec(this.server, socket, terminalName, "git", [ "pull", "--strategy-option", "theirs" ], this.path);
|
||||
if (exitCode !== 0) {
|
||||
throw new Error("Failed to sync, please check the terminal output for more information.");
|
||||
}
|
||||
|
||||
// If the stack is not running, we don't need to restart it
|
||||
await this.updateStatus();
|
||||
log.debug("update", "Status: " + this.status);
|
||||
if (this.status !== RUNNING) {
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
checkRemoteChanges() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isGitRepo) {
|
||||
reject("This stack is not a git repository");
|
||||
return;
|
||||
}
|
||||
//fetch remote changes and check if the current branch is behind
|
||||
try {
|
||||
const stdout = execSync("git fetch origin && git status -uno", { cwd: this.path }).toString();
|
||||
if (stdout.includes("Your branch is behind")) {
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("checkRemoteChanges", error);
|
||||
reject("Failed to check local status");
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async joinCombinedTerminal(socket: DockgeSocket) {
|
||||
const terminalName = getCombinedTerminalName(socket.endpoint, this.name);
|
||||
const terminal = Terminal.getOrCreateTerminal(this.server, terminalName, "docker", [ "compose", "logs", "-f", "--tail", "100" ], this.path);
|
||||
|
|
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
|
@ -17,6 +17,7 @@ declare module 'vue' {
|
|||
Confirm: typeof import('./src/components/Confirm.vue')['default']
|
||||
Container: typeof import('./src/components/Container.vue')['default']
|
||||
General: typeof import('./src/components/settings/General.vue')['default']
|
||||
GitOps: typeof import('./src/components/settings/GitOps.vue')['default']
|
||||
HiddenInput: typeof import('./src/components/HiddenInput.vue')['default']
|
||||
Login: typeof import('./src/components/Login.vue')['default']
|
||||
NetworkInput: typeof import('./src/components/NetworkInput.vue')['default']
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
<span>{{ stackName }}</span>
|
||||
<div v-if="$root.agentCount > 1" class="endpoint">{{ endpointDisplay }}</div>
|
||||
</div>
|
||||
<div class="icon-container">
|
||||
<font-awesome-icon :icon="stack.isGitRepo ? 'code-branch' : 'file'" />
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
|
@ -178,4 +181,8 @@ export default {
|
|||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
57
frontend/src/components/settings/GitOps.vue
Normal file
57
frontend/src/components/settings/GitOps.vue
Normal file
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div>
|
||||
<form class="my-4" autocomplete="off" @submit.prevent="saveGitOps">
|
||||
<!-- Enable Auto Updates -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="gitAutoUpdate">
|
||||
{{ $t("gitAutoUpdate") }}
|
||||
</label>
|
||||
|
||||
<div class="form-check form-switch my-3">
|
||||
<input id="git-auto-update" v-model="settings.gitAutoUpdate" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">
|
||||
{{ $t("enableAutoUpdate") }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-text"></div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
{{ $t("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
|
||||
computed: {
|
||||
settings() {
|
||||
return this.$parent.$parent.$parent.settings;
|
||||
},
|
||||
saveSettings() {
|
||||
return this.$parent.$parent.$parent.saveSettings;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/** Save the settings */
|
||||
saveGitOps() {
|
||||
this.saveSettings();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "../../styles/vars.scss";
|
||||
</style>
|
||||
|
|
@ -54,6 +54,8 @@ import {
|
|||
faTerminal, faWarehouse, faHome, faRocket,
|
||||
faRotate,
|
||||
faCloudArrowDown, faArrowsRotate,
|
||||
faCodeBranch,
|
||||
faFileCode,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(
|
||||
|
@ -109,6 +111,8 @@ library.add(
|
|||
faRotate,
|
||||
faCloudArrowDown,
|
||||
faArrowsRotate,
|
||||
faCodeBranch,
|
||||
faFileCode,
|
||||
);
|
||||
|
||||
export { FontAwesomeIcon };
|
||||
|
|
|
@ -112,5 +112,15 @@
|
|||
"agentRemovedSuccessfully": "Agent removed successfully.",
|
||||
"removeAgent": "Remove Agent",
|
||||
"removeAgentMsg": "Are you sure you want to remove this agent?",
|
||||
"LongSyntaxNotSupported": "Long syntax is not supported here. Please use the YAML editor."
|
||||
"LongSyntaxNotSupported": "Long syntax is not supported here. Please use the YAML editor.",
|
||||
"repositoryUrl": "Repository URL",
|
||||
"branch": "Branch",
|
||||
"gitAutoUpdate": "[GitOps] Auto Update",
|
||||
"enableAutoUpdate": "Check periodically for updates",
|
||||
"ManageWithGit": "Manage this stack with Git",
|
||||
"webhook": "Webhook URL to trigger update",
|
||||
"copy": "Copy",
|
||||
"GitOps": "GitOps",
|
||||
"GitConfig": "Git Configuration",
|
||||
"gitSync": "Sync Repo"
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
{{ $t("deployStack") }}
|
||||
</button>
|
||||
|
||||
<button v-if="isEditMode" class="btn btn-normal" :disabled="processing" @click="saveStack">
|
||||
<button v-if="isEditMode && !stack.isGitRepo" class="btn btn-normal" :disabled="processing" @click="saveStack">
|
||||
<font-awesome-icon icon="save" class="me-1" />
|
||||
{{ $t("saveStackDraft") }}
|
||||
</button>
|
||||
|
@ -41,6 +41,11 @@
|
|||
{{ $t("updateStack") }}
|
||||
</button>
|
||||
|
||||
<button v-if="!isEditMode && stack.isGitRepo" class="btn btn-normal" :disabled="processing" @click="gitSync">
|
||||
<font-awesome-icon icon="rotate" class="me-1" />
|
||||
{{ $t("gitSync") }}
|
||||
</button>
|
||||
|
||||
<button v-if="!isEditMode && active" class="btn btn-normal" :disabled="processing" @click="stopStack">
|
||||
<font-awesome-icon icon="stop" class="me-1" />
|
||||
{{ $t("stopStack") }}
|
||||
|
@ -103,39 +108,106 @@
|
|||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Manage with git -->
|
||||
<div class="form-check form-switch my-3">
|
||||
<input id="gitops" v-model="stack.isGitRepo" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">
|
||||
{{ $t("ManageWithGit") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GitOps -->
|
||||
<div v-if="stack.isGitRepo">
|
||||
<h4 class="mb-3">{{ $t("GitConfig") }}</h4>
|
||||
<div class="shadow-box big-padding mb-3">
|
||||
<!-- Repo URL -->
|
||||
<div v-if="isEditMode" class="mb-4">
|
||||
<label class="form-label">
|
||||
{{ $t("repositoryUrl") }}
|
||||
</label>
|
||||
<div class="input-group mb-3">
|
||||
<input
|
||||
v-model="stack.gitUrl"
|
||||
class="form-control"
|
||||
placeholder="https://"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-text"></div>
|
||||
</div>
|
||||
|
||||
<!-- branch -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">
|
||||
{{ $t("branch") }}
|
||||
</label>
|
||||
<div class="input-group mb-3">
|
||||
<span v-if="!isEditMode" class="form-control">{{ stack.branch }}</span>
|
||||
<input
|
||||
v-else
|
||||
v-model="stack.branch"
|
||||
class="form-control"
|
||||
placeholder="main"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-text"></div>
|
||||
</div>
|
||||
|
||||
<!-- Webhook -->
|
||||
<div v-if="!isAdd" class="mb-4">
|
||||
<label class="form-label">
|
||||
{{ $t("webhook") }}
|
||||
</label>
|
||||
<div class="input-group mb-3">
|
||||
<input
|
||||
v-model="stack.webhook"
|
||||
class="form-control"
|
||||
readonly
|
||||
@click="selectText"
|
||||
/>
|
||||
<button class="btn btn-outline-primary" type="button" @click="copyWebhookToClipboard">
|
||||
{{ $t("copy") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Containers -->
|
||||
<h4 class="mb-3">{{ $tc("container", 2) }}</h4>
|
||||
<div v-if="!stack.isGitRepo || (stack.isGitRepo && !isEditMode)">
|
||||
<h4 class="mb-3">{{ $tc("container", 2) }}</h4>
|
||||
|
||||
<div v-if="isEditMode" class="input-group mb-3">
|
||||
<input
|
||||
v-model="newContainerName"
|
||||
placeholder="New Container Name..."
|
||||
class="form-control"
|
||||
@keyup.enter="addContainer"
|
||||
/>
|
||||
<button class="btn btn-primary" @click="addContainer">
|
||||
{{ $t("addContainer") }}
|
||||
</button>
|
||||
<div v-if="isEditMode && !stack.isGitRepo" class="input-group mb-3">
|
||||
<input
|
||||
v-model="newContainerName"
|
||||
placeholder="New Container Name..."
|
||||
class="form-control"
|
||||
@keyup.enter="addContainer"
|
||||
/>
|
||||
<button class="btn btn-primary" @click="addContainer">
|
||||
{{ $t("addContainer") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div ref="containerList">
|
||||
<Container
|
||||
v-for="(service, name) in jsonConfig.services"
|
||||
:key="name"
|
||||
:name="name"
|
||||
:is-edit-mode="isEditMode"
|
||||
:first="name === Object.keys(jsonConfig.services)[0]"
|
||||
:status="serviceStatusList[name]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref="containerList">
|
||||
<Container
|
||||
v-for="(service, name) in jsonConfig.services"
|
||||
:key="name"
|
||||
:name="name"
|
||||
:is-edit-mode="isEditMode"
|
||||
:first="name === Object.keys(jsonConfig.services)[0]"
|
||||
:status="serviceStatusList[name]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button v-if="false && isEditMode && jsonConfig.services && Object.keys(jsonConfig.services).length > 0" class="btn btn-normal mb-3" @click="addContainer">{{ $t("addContainer") }}</button>
|
||||
|
||||
<button v-if="false && isEditMode && !stack.isGitRepo && jsonConfig.services && Object.keys(jsonConfig.services).length > 0" class="btn btn-normal mb-3" @click="addContainer">{{ $t("addContainer") }}</button>
|
||||
<!-- General -->
|
||||
<div v-if="isEditMode">
|
||||
<div v-if="isEditMode && !stack.isGitRepo">
|
||||
<h4 class="mb-3">{{ $t("extra") }}</h4>
|
||||
<div class="shadow-box big-padding mb-3">
|
||||
<!-- URLs -->
|
||||
|
@ -162,7 +234,7 @@
|
|||
></Terminal>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div v-if="!stack.isGitRepo || (stack.isGitRepo && !isEditMode)" class="col-lg-6">
|
||||
<h4 class="mb-3">{{ stack.composeFileName }}</h4>
|
||||
|
||||
<!-- YAML editor -->
|
||||
|
@ -545,45 +617,59 @@ export default {
|
|||
|
||||
deployStack() {
|
||||
this.processing = true;
|
||||
|
||||
if (!this.jsonConfig.services) {
|
||||
this.$root.toastError("No services found in compose.yaml");
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if services is object
|
||||
if (typeof this.jsonConfig.services !== "object") {
|
||||
this.$root.toastError("Services must be an object");
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let serviceNameList = Object.keys(this.jsonConfig.services);
|
||||
|
||||
// Set the stack name if empty, use the first container name
|
||||
if (!this.stack.name && serviceNameList.length > 0) {
|
||||
let serviceName = serviceNameList[0];
|
||||
let service = this.jsonConfig.services[serviceName];
|
||||
|
||||
if (service && service.container_name) {
|
||||
this.stack.name = service.container_name;
|
||||
} else {
|
||||
this.stack.name = serviceName;
|
||||
}
|
||||
}
|
||||
|
||||
this.bindTerminal();
|
||||
|
||||
this.$root.emitAgent(this.stack.endpoint, "deployStack", this.stack.name, this.stack.composeYAML, this.stack.composeENV, this.isAdd, (res) => {
|
||||
this.processing = false;
|
||||
this.$root.toastRes(res);
|
||||
if (this.stack.isGitRepo) {
|
||||
this.$root.emitAgent(this.stack.endpoint, "gitDeployStack", this.stack.name, this.stack.gitUrl, this.stack.branch, this.isAdd, (res) => {
|
||||
this.processing = false;
|
||||
this.$root.toastRes(res);
|
||||
|
||||
if (res.ok) {
|
||||
this.isEditMode = false;
|
||||
this.$router.push(this.url);
|
||||
if (res.ok) {
|
||||
this.isEditMode = false;
|
||||
this.$router.push(this.url);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
} else {
|
||||
if (!this.jsonConfig.services) {
|
||||
this.$root.toastError("No services found in compose.yaml");
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Check if services is object
|
||||
if (typeof this.jsonConfig.services !== "object") {
|
||||
this.$root.toastError("Services must be an object");
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let serviceNameList = Object.keys(this.jsonConfig.services);
|
||||
|
||||
// Set the stack name if empty, use the first container name
|
||||
if (!this.stack.name && serviceNameList.length > 0) {
|
||||
let serviceName = serviceNameList[0];
|
||||
let service = this.jsonConfig.services[serviceName];
|
||||
|
||||
if (service && service.container_name) {
|
||||
this.stack.name = service.container_name;
|
||||
} else {
|
||||
this.stack.name = serviceName;
|
||||
}
|
||||
}
|
||||
|
||||
this.$root.emitAgent(this.stack.endpoint, "deployStack", this.stack.name, this.stack.composeYAML, this.stack.composeENV, this.isAdd, (res) => {
|
||||
this.processing = false;
|
||||
this.$root.toastRes(res);
|
||||
|
||||
if (res.ok) {
|
||||
this.isEditMode = false;
|
||||
this.$router.push(this.url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
saveStack() {
|
||||
|
@ -645,6 +731,15 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
gitSync() {
|
||||
this.processing = true;
|
||||
|
||||
this.$root.emitAgent(this.endpoint, "gitSync", this.stack.name, (res) => {
|
||||
this.processing = false;
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
|
||||
deleteDialog() {
|
||||
this.$root.emitAgent(this.endpoint, "deleteStack", this.stack.name, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
|
@ -786,6 +881,19 @@ export default {
|
|||
this.stack.name = this.stack?.name?.toLowerCase();
|
||||
},
|
||||
|
||||
async copyWebhookToClipboard() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.stack.webhook);
|
||||
} catch (err) {
|
||||
this.$root.toastError("Failed to copy to clipboard");
|
||||
}
|
||||
this.$root.toastSuccess("Copied to clipboard");
|
||||
},
|
||||
|
||||
selectText(event) {
|
||||
event.target.select();
|
||||
},
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -80,6 +80,9 @@ export default {
|
|||
appearance: {
|
||||
title: this.$t("Appearance"),
|
||||
},
|
||||
gitOps: {
|
||||
title: this.$t("GitOps"),
|
||||
},
|
||||
security: {
|
||||
title: this.$t("Security"),
|
||||
},
|
||||
|
|
|
@ -15,6 +15,7 @@ import Appearance from "./components/settings/Appearance.vue";
|
|||
import General from "./components/settings/General.vue";
|
||||
const Security = () => import("./components/settings/Security.vue");
|
||||
import About from "./components/settings/About.vue";
|
||||
import GitOps from "./components/settings/GitOps.vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
|
@ -74,6 +75,10 @@ const routes = [
|
|||
path: "appearance",
|
||||
component: Appearance,
|
||||
},
|
||||
{
|
||||
path: "gitops",
|
||||
component: GitOps,
|
||||
},
|
||||
{
|
||||
path: "security",
|
||||
component: Security,
|
||||
|
@ -81,7 +86,7 @@ const routes = [
|
|||
{
|
||||
path: "about",
|
||||
component: About,
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
"express": "~4.18.2",
|
||||
"express-static-gzip": "~2.1.7",
|
||||
"http-graceful-shutdown": "~3.1.13",
|
||||
"ini": "^4.1.2",
|
||||
"jsonwebtoken": "~9.0.2",
|
||||
"jwt-decode": "~3.1.2",
|
||||
"knex": "~2.5.1",
|
||||
|
@ -68,6 +69,7 @@
|
|||
"@types/bootstrap": "~5.2.10",
|
||||
"@types/command-exists": "~1.2.3",
|
||||
"@types/express": "~4.17.21",
|
||||
"@types/ini": "^4.1.0",
|
||||
"@types/jsonwebtoken": "~9.0.5",
|
||||
"@types/semver": "^7.5.6",
|
||||
"@typescript-eslint/eslint-plugin": "~6.8.0",
|
||||
|
|
|
@ -47,6 +47,9 @@ dependencies:
|
|||
http-graceful-shutdown:
|
||||
specifier: ~3.1.13
|
||||
version: 3.1.13
|
||||
ini:
|
||||
specifier: ^4.1.2
|
||||
version: 4.1.2
|
||||
jsonwebtoken:
|
||||
specifier: ~9.0.2
|
||||
version: 9.0.2
|
||||
|
@ -124,6 +127,9 @@ devDependencies:
|
|||
'@types/express':
|
||||
specifier: ~4.17.21
|
||||
version: 4.17.21
|
||||
'@types/ini':
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
'@types/jsonwebtoken':
|
||||
specifier: ~9.0.5
|
||||
version: 9.0.5
|
||||
|
@ -903,9 +909,6 @@ packages:
|
|||
/@louislam/sqlite3@15.1.6:
|
||||
resolution: {integrity: sha512-cVf7hcMrfywYnycatLvorngTFpL3BSWvEy7/NrEfcTyQX8xxj9fdeD553oCTv5fIAk85fluo6mzPq89V3YzrVA==}
|
||||
requiresBuild: true
|
||||
peerDependenciesMeta:
|
||||
node-gyp:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@mapbox/node-pre-gyp': 1.0.11
|
||||
node-addon-api: 4.3.0
|
||||
|
@ -1275,6 +1278,10 @@ packages:
|
|||
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
|
||||
dev: true
|
||||
|
||||
/@types/ini@4.1.0:
|
||||
resolution: {integrity: sha512-mTehMtc+xtnWBBvqizcqYCktKDBH2WChvx1GU3Sfe4PysFDXiNe+1YwtpVX1MDtCa4NQrSPw2+3HmvXHY3gt1w==}
|
||||
dev: true
|
||||
|
||||
/@types/json-schema@7.0.15:
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
dev: true
|
||||
|
@ -3305,6 +3312,11 @@ packages:
|
|||
requiresBuild: true
|
||||
dev: false
|
||||
|
||||
/ini@4.1.2:
|
||||
resolution: {integrity: sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==}
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
dev: false
|
||||
|
||||
/internal-slot@1.0.6:
|
||||
resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
Loading…
Reference in a new issue