mirror of
https://github.com/louislam/dockge.git
synced 2024-11-30 22:24:03 +00:00
add git-managed stacks (pull only)
This commit is contained in:
parent
8e362f3fa9
commit
26f8602e84
14 changed files with 889 additions and 402 deletions
|
@ -3,6 +3,8 @@ import { DockgeServer } from "../dockge-server";
|
||||||
import { callbackError, callbackResult, checkLogin, DockgeSocket, ValidationError } from "../util-server";
|
import { callbackError, callbackResult, checkLogin, DockgeSocket, ValidationError } from "../util-server";
|
||||||
import { Stack } from "../stack";
|
import { Stack } from "../stack";
|
||||||
import { AgentSocket } from "../../common/agent-socket";
|
import { AgentSocket } from "../../common/agent-socket";
|
||||||
|
import { Terminal } from "../terminal";
|
||||||
|
import { getComposeTerminalName } from "../../common/util-common";
|
||||||
|
|
||||||
export class DockerSocketHandler extends AgentSocketHandler {
|
export class DockerSocketHandler extends AgentSocketHandler {
|
||||||
create(socket : DockgeSocket, server : DockgeServer, agentSocket : AgentSocket) {
|
create(socket : DockgeSocket, server : DockgeServer, agentSocket : AgentSocket) {
|
||||||
|
@ -25,6 +27,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) => {
|
agentSocket.on("saveStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
@ -196,6 +239,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
|
// down stack
|
||||||
agentSocket.on("downStack", async (stackName : unknown, callback) => {
|
agentSocket.on("downStack", async (stackName : unknown, callback) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import { MainRouter } from "./routers/main-router";
|
import { MainRouter } from "./routers/main-router";
|
||||||
|
import { WebhookRouter } from "./routers/webhook-router";
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import { PackageJson } from "type-fest";
|
import { PackageJson } from "type-fest";
|
||||||
import { Database } from "./database";
|
import { Database } from "./database";
|
||||||
|
@ -21,7 +22,7 @@ import { R } from "redbean-node";
|
||||||
import { genSecret, isDev, LooseObject } from "../common/util-common";
|
import { genSecret, isDev, LooseObject } from "../common/util-common";
|
||||||
import { generatePasswordHash } from "./password-hash";
|
import { generatePasswordHash } from "./password-hash";
|
||||||
import { Bean } from "redbean-node/dist/bean";
|
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 { DockerSocketHandler } from "./agent-socket-handlers/docker-socket-handler";
|
||||||
import expressStaticGzip from "express-static-gzip";
|
import expressStaticGzip from "express-static-gzip";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
@ -38,6 +39,8 @@ import { AgentSocket } from "../common/agent-socket";
|
||||||
import { ManageAgentSocketHandler } from "./socket-handlers/manage-agent-socket-handler";
|
import { ManageAgentSocketHandler } from "./socket-handlers/manage-agent-socket-handler";
|
||||||
import { Terminal } from "./terminal";
|
import { Terminal } from "./terminal";
|
||||||
|
|
||||||
|
const GIT_UPDATE_CHECKER_INTERVAL_MS = 1000 * 60 * 10;
|
||||||
|
|
||||||
export class DockgeServer {
|
export class DockgeServer {
|
||||||
app : Express;
|
app : Express;
|
||||||
httpServer : http.Server;
|
httpServer : http.Server;
|
||||||
|
@ -45,12 +48,14 @@ export class DockgeServer {
|
||||||
io : socketIO.Server;
|
io : socketIO.Server;
|
||||||
config : Config;
|
config : Config;
|
||||||
indexHTML : string = "";
|
indexHTML : string = "";
|
||||||
|
gitUpdateInterval? : NodeJS.Timeout;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of express routers
|
* List of express routers
|
||||||
*/
|
*/
|
||||||
routerList : Router[] = [
|
routerList : Router[] = [
|
||||||
new MainRouter(),
|
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
|
// Create Socket.io
|
||||||
this.io = new socketIO.Server(this.httpServer, {
|
this.io = new socketIO.Server(this.httpServer, {
|
||||||
cors,
|
cors,
|
||||||
|
@ -398,6 +414,7 @@ export class DockgeServer {
|
||||||
});
|
});
|
||||||
|
|
||||||
checkVersion.startInterval();
|
checkVersion.startInterval();
|
||||||
|
this.startGitUpdater();
|
||||||
});
|
});
|
||||||
|
|
||||||
gracefulShutdown(this.httpServer, {
|
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[]> {
|
async getDockerNetworkList() : Promise<string[]> {
|
||||||
let res = await childProcessAsync.spawn("docker", [ "network", "ls", "--format", "{{.Name}}" ], {
|
let res = await childProcessAsync.spawn("docker", [ "network", "ls", "--format", "{{.Name}}" ], {
|
||||||
encoding: "utf-8",
|
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 { InteractiveTerminal, Terminal } from "./terminal";
|
||||||
import childProcessAsync from "promisify-child-process";
|
import childProcessAsync from "promisify-child-process";
|
||||||
import { Settings } from "./settings";
|
import { Settings } from "./settings";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import ini from "ini";
|
||||||
|
|
||||||
export class Stack {
|
export class Stack {
|
||||||
|
|
||||||
|
@ -84,6 +86,10 @@ export class Stack {
|
||||||
status: this._status,
|
status: this._status,
|
||||||
tags: [],
|
tags: [],
|
||||||
isManagedByDockge: this.isManagedByDockge,
|
isManagedByDockge: this.isManagedByDockge,
|
||||||
|
isGitRepo: this.isGitRepo,
|
||||||
|
gitUrl: this.gitUrl,
|
||||||
|
branch: this.branch,
|
||||||
|
webhook: this.webhook,
|
||||||
composeFileName: this._composeFileName,
|
composeFileName: this._composeFileName,
|
||||||
endpoint,
|
endpoint,
|
||||||
};
|
};
|
||||||
|
@ -107,6 +113,39 @@ export class Stack {
|
||||||
return fs.existsSync(this.path) && fs.statSync(this.path).isDirectory();
|
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 {
|
get status() : number {
|
||||||
return this._status;
|
return this._status;
|
||||||
}
|
}
|
||||||
|
@ -445,6 +484,7 @@ 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);
|
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.");
|
||||||
|
@ -464,6 +504,54 @@ export class Stack {
|
||||||
return exitCode;
|
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) {
|
async joinCombinedTerminal(socket: DockgeSocket) {
|
||||||
const terminalName = getCombinedTerminalName(socket.endpoint, this.name);
|
const terminalName = getCombinedTerminalName(socket.endpoint, this.name);
|
||||||
const terminal = Terminal.getOrCreateTerminal(this.server, terminalName, "docker", [ "compose", "logs", "-f", "--tail", "100" ], this.path);
|
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']
|
Confirm: typeof import('./src/components/Confirm.vue')['default']
|
||||||
Container: typeof import('./src/components/Container.vue')['default']
|
Container: typeof import('./src/components/Container.vue')['default']
|
||||||
General: typeof import('./src/components/settings/General.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']
|
HiddenInput: typeof import('./src/components/HiddenInput.vue')['default']
|
||||||
Login: typeof import('./src/components/Login.vue')['default']
|
Login: typeof import('./src/components/Login.vue')['default']
|
||||||
NetworkInput: typeof import('./src/components/NetworkInput.vue')['default']
|
NetworkInput: typeof import('./src/components/NetworkInput.vue')['default']
|
||||||
|
|
|
@ -5,6 +5,9 @@
|
||||||
<span>{{ stackName }}</span>
|
<span>{{ stackName }}</span>
|
||||||
<div v-if="$root.agentCount > 1" class="endpoint">{{ endpointDisplay }}</div>
|
<div v-if="$root.agentCount > 1" class="endpoint">{{ endpointDisplay }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="icon-container">
|
||||||
|
<font-awesome-icon :icon="stack.isGitRepo ? 'code-branch' : 'file'" />
|
||||||
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -178,4 +181,8 @@ export default {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-container {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</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,
|
faTerminal, faWarehouse, faHome, faRocket,
|
||||||
faRotate,
|
faRotate,
|
||||||
faCloudArrowDown, faArrowsRotate,
|
faCloudArrowDown, faArrowsRotate,
|
||||||
|
faCodeBranch,
|
||||||
|
faFileCode,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
|
@ -109,6 +111,8 @@ library.add(
|
||||||
faRotate,
|
faRotate,
|
||||||
faCloudArrowDown,
|
faCloudArrowDown,
|
||||||
faArrowsRotate,
|
faArrowsRotate,
|
||||||
|
faCodeBranch,
|
||||||
|
faFileCode,
|
||||||
);
|
);
|
||||||
|
|
||||||
export { FontAwesomeIcon };
|
export { FontAwesomeIcon };
|
||||||
|
|
|
@ -128,5 +128,15 @@
|
||||||
"New Container Name...": "New Container Name...",
|
"New Container Name...": "New Container Name...",
|
||||||
"Network name...": "Network name...",
|
"Network name...": "Network name...",
|
||||||
"Select a network...": "Select a network...",
|
"Select a network...": "Select a network...",
|
||||||
"NoNetworksAvailable": "No networks available. You need to add internal networks or enable external networks in the right side first."
|
"NoNetworksAvailable": "No networks available. You need to add internal networks or enable external networks in the right side first.",
|
||||||
|
"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") }}
|
{{ $t("deployStack") }}
|
||||||
</button>
|
</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" />
|
<font-awesome-icon icon="save" class="me-1" />
|
||||||
{{ $t("saveStackDraft") }}
|
{{ $t("saveStackDraft") }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -41,6 +41,11 @@
|
||||||
{{ $t("updateStack") }}
|
{{ $t("updateStack") }}
|
||||||
</button>
|
</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">
|
<button v-if="!isEditMode && active" class="btn btn-normal" :disabled="processing" @click="stopStack">
|
||||||
<font-awesome-icon icon="stop" class="me-1" />
|
<font-awesome-icon icon="stop" class="me-1" />
|
||||||
{{ $t("stopStack") }}
|
{{ $t("stopStack") }}
|
||||||
|
@ -103,39 +108,117 @@
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Containers -->
|
<!-- 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">
|
<div v-if="isEditMode" class="input-group mb-3">
|
||||||
<input
|
<input
|
||||||
v-model="newContainerName"
|
v-model="newContainerName"
|
||||||
:placeholder="$t(`New Container Name...`)"
|
:placeholder="$t(`New Container Name...`)"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@keyup.enter="addContainer"
|
@keyup.enter="addContainer"
|
||||||
/>
|
/>
|
||||||
<button class="btn btn-primary" @click="addContainer">
|
<button class="btn btn-primary" @click="addContainer">
|
||||||
{{ $t("addContainer") }}
|
{{ $t("addContainer") }}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="isEditMode && !stack.isGitRepo" class="input-group mb-3">
|
||||||
|
<input
|
||||||
|
v-model="newContainerName"
|
||||||
|
:placeholder="$t(`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>
|
||||||
|
|
||||||
<div ref="containerList">
|
<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>
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- General -->
|
<!-- General -->
|
||||||
<div v-if="isEditMode">
|
<div v-if="isEditMode && !stack.isGitRepo">
|
||||||
<h4 class="mb-3">{{ $t("extra") }}</h4>
|
<h4 class="mb-3">{{ $t("extra") }}</h4>
|
||||||
<div class="shadow-box big-padding mb-3">
|
<div class="shadow-box big-padding mb-3">
|
||||||
<!-- URLs -->
|
<!-- URLs -->
|
||||||
|
@ -162,7 +245,7 @@
|
||||||
></Terminal>
|
></Terminal>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<h4 class="mb-3">{{ stack.composeFileName }}</h4>
|
||||||
|
|
||||||
<!-- YAML editor -->
|
<!-- YAML editor -->
|
||||||
|
@ -545,45 +628,59 @@ export default {
|
||||||
|
|
||||||
deployStack() {
|
deployStack() {
|
||||||
this.processing = true;
|
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.bindTerminal();
|
||||||
|
|
||||||
this.$root.emitAgent(this.stack.endpoint, "deployStack", this.stack.name, this.stack.composeYAML, this.stack.composeENV, this.isAdd, (res) => {
|
if (this.stack.isGitRepo) {
|
||||||
this.processing = false;
|
this.$root.emitAgent(this.stack.endpoint, "gitDeployStack", this.stack.name, this.stack.gitUrl, this.stack.branch, this.isAdd, (res) => {
|
||||||
this.$root.toastRes(res);
|
this.processing = false;
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.isEditMode = false;
|
this.isEditMode = false;
|
||||||
this.$router.push(this.url);
|
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() {
|
saveStack() {
|
||||||
|
@ -645,6 +742,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() {
|
deleteDialog() {
|
||||||
this.$root.emitAgent(this.endpoint, "deleteStack", this.stack.name, (res) => {
|
this.$root.emitAgent(this.endpoint, "deleteStack", this.stack.name, (res) => {
|
||||||
this.$root.toastRes(res);
|
this.$root.toastRes(res);
|
||||||
|
@ -786,6 +892,19 @@ export default {
|
||||||
this.stack.name = this.stack?.name?.toLowerCase();
|
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>
|
</script>
|
||||||
|
|
|
@ -80,6 +80,9 @@ export default {
|
||||||
appearance: {
|
appearance: {
|
||||||
title: this.$t("Appearance"),
|
title: this.$t("Appearance"),
|
||||||
},
|
},
|
||||||
|
gitOps: {
|
||||||
|
title: this.$t("GitOps"),
|
||||||
|
},
|
||||||
security: {
|
security: {
|
||||||
title: this.$t("Security"),
|
title: this.$t("Security"),
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,6 +15,7 @@ import Appearance from "./components/settings/Appearance.vue";
|
||||||
import General from "./components/settings/General.vue";
|
import General from "./components/settings/General.vue";
|
||||||
const Security = () => import("./components/settings/Security.vue");
|
const Security = () => import("./components/settings/Security.vue");
|
||||||
import About from "./components/settings/About.vue";
|
import About from "./components/settings/About.vue";
|
||||||
|
import GitOps from "./components/settings/GitOps.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
|
@ -74,6 +75,10 @@ const routes = [
|
||||||
path: "appearance",
|
path: "appearance",
|
||||||
component: Appearance,
|
component: Appearance,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "gitops",
|
||||||
|
component: GitOps,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "security",
|
path: "security",
|
||||||
component: Security,
|
component: Security,
|
||||||
|
@ -81,7 +86,7 @@ const routes = [
|
||||||
{
|
{
|
||||||
path: "about",
|
path: "about",
|
||||||
component: About,
|
component: About,
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
"express": "~4.21.1",
|
"express": "~4.21.1",
|
||||||
"express-static-gzip": "~2.1.8",
|
"express-static-gzip": "~2.1.8",
|
||||||
"http-graceful-shutdown": "~3.1.13",
|
"http-graceful-shutdown": "~3.1.13",
|
||||||
|
"ini": "^4.1.2",
|
||||||
"jsonwebtoken": "~9.0.2",
|
"jsonwebtoken": "~9.0.2",
|
||||||
"jwt-decode": "~3.1.2",
|
"jwt-decode": "~3.1.2",
|
||||||
"knex": "~2.5.1",
|
"knex": "~2.5.1",
|
||||||
|
@ -70,6 +71,7 @@
|
||||||
"@types/express": "~4.17.21",
|
"@types/express": "~4.17.21",
|
||||||
"@types/jsonwebtoken": "~9.0.7",
|
"@types/jsonwebtoken": "~9.0.7",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
|
"@types/ini": "^4.1.0",
|
||||||
"@typescript-eslint/eslint-plugin": "~6.8.0",
|
"@typescript-eslint/eslint-plugin": "~6.8.0",
|
||||||
"@typescript-eslint/parser": "~6.8.0",
|
"@typescript-eslint/parser": "~6.8.0",
|
||||||
"@vitejs/plugin-vue": "~4.5.2",
|
"@vitejs/plugin-vue": "~4.5.2",
|
||||||
|
|
705
pnpm-lock.yaml
705
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue