Compare commits

..

1 Commits

Author SHA1 Message Date
79552b90a3 Sort non managed stack to the end 2023-12-03 21:19:53 +08:00
24 changed files with 556 additions and 1573 deletions

View File

@ -1,4 +1,5 @@
labels: [help]
title: "❓ Ask for help"
labels: [help]
body:
- type: checkboxes
id: no-duplicate-issues

View File

@ -1,3 +1,4 @@
title: 🚀 Feature Request
labels: [feature-request]
body:
- type: checkboxes
@ -51,4 +52,4 @@ body:
attributes:
label: "📝 Additional Context"
description: "Add any other context or screenshots about the feature request here."
placeholder: "..."
placeholder: "..."

View File

@ -1,14 +1,14 @@
name: "⚠️ Ask for help (Please go to the \"Discussions\" tab to submit a Help Request)"
description: "⚠️ Please go to the \"Discussions\" tab to submit a Help Request"
name: " Ask for help"
description: "Please go to the Discussions tab to submit a Help Request"
body:
- type: markdown
attributes:
value: |
⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ Please go to https://github.com/louislam/dockge/discussions/new?category=ask-for-help
Please go to https://github.com/louislam/dockge/discussions/new?category=ask-for-help
- type: checkboxes
id: no-duplicate-issues
attributes:
label: "Issues are for bug reports only, please go to the \"Discussions\" tab to submit a Feature Request"
label: "Issues are for bug reports only"
options:
- label: "I understand"
required: true

View File

@ -1,14 +1,14 @@
name: 🚀 Feature Request (Please go to the "Discussions" tab to submit a Feature Request)
description: "⚠️ Please go to the \"Discussions\" tab to submit a Feature Request"
name: 🚀 Feature Request
description: "Please go to the Discussions tab to submit a Feature Request"
body:
- type: markdown
attributes:
value: |
⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ Please go to https://github.com/louislam/dockge/discussions/new?category=ask-for-help
Please go to https://github.com/louislam/dockge/discussions/new?category=feature-request
- type: checkboxes
id: no-duplicate-issues
attributes:
label: "Issues are for bug reports only, please go to the \"Discussions\" tab to submit a Feature Request"
label: "Issues are for bug reports only"
options:
- label: "I understand"
required: true

View File

@ -131,7 +131,7 @@ Be sure to read the [guide](https://github.com/louislam/dockge/blob/master/CONTR
#### "Dockge"?
"Dockge" is a coinage word which is created by myself. I originally hoped it sounds like `Dodge`, but apparently many people called it `Dockage`, it is also acceptable.
"Dockge" is a coinage word which is created by myself. I hope it sounds like `Dodge`.
The naming idea came from Twitch emotes like `sadge`, `bedge` or `wokege`. They all end in `-ge`.
@ -148,13 +148,13 @@ Yes, you can. However, you need to move your compose file into the stacks direct
3. In Dockge, click the " Scan Stacks Folder" button in the top-right corner's dropdown menu
4. Now you should see your stack in the list
#### Is Dockge a Portainer replacement?
#### Is Dockge a Portainer replcement?
Yes or no. Portainer provides a lot of Docker features. While Dockge is currently only focusing on docker-compose with a better user interface and better user experience.
If you want to manage your container with docker-compose only, the answer may be yes.
If you still need to manage something like docker networks, single containers, the answer may be no.
If you still need to manage something like docker networks, signle containers, the answer may be no.
#### Can I install both Dockge and Portainer?

View File

@ -3,55 +3,69 @@ import compareVersions from "compare-versions";
import packageJSON from "../package.json";
import { Settings } from "./settings";
export const obj = {
version: packageJSON.version,
latestVersion: null,
};
export default obj;
// How much time in ms to wait between update checks
const UPDATE_CHECKER_INTERVAL_MS = 1000 * 60 * 60 * 48;
const CHECK_URL = "https://dockge.kuma.pet/version";
class CheckVersion {
version = packageJSON.version;
latestVersion? : string;
interval? : NodeJS.Timeout;
let interval : NodeJS.Timeout;
async startInterval() {
const check = async () => {
if (await Settings.get("checkUpdate") === false) {
return;
export function startInterval() {
const check = async () => {
if (await Settings.get("checkUpdate") === false) {
return;
}
log.debug("update-checker", "Retrieving latest versions");
try {
const res = await fetch(CHECK_URL);
const data = await res.json();
// For debug
if (process.env.TEST_CHECK_VERSION === "1") {
data.slow = "1000.0.0";
}
log.debug("update-checker", "Retrieving latest versions");
const checkBeta = await Settings.get("checkBeta");
try {
const res = await fetch(CHECK_URL);
const data = await res.json();
// For debug
if (process.env.TEST_CHECK_VERSION === "1") {
data.slow = "1000.0.0";
if (checkBeta && data.beta) {
if (compareVersions.compare(data.beta, data.slow, ">")) {
obj.latestVersion = data.beta;
return;
}
const checkBeta = await Settings.get("checkBeta");
if (checkBeta && data.beta) {
if (compareVersions.compare(data.beta, data.slow, ">")) {
this.latestVersion = data.beta;
return;
}
}
if (data.slow) {
this.latestVersion = data.slow;
}
} catch (_) {
log.info("update-checker", "Failed to check for new versions");
}
};
if (data.slow) {
obj.latestVersion = data.slow;
}
await check();
this.interval = setInterval(check, UPDATE_CHECKER_INTERVAL_MS);
}
} catch (_) {
log.info("update-checker", "Failed to check for new versions");
}
};
check();
interval = setInterval(check, UPDATE_CHECKER_INTERVAL_MS);
}
const checkVersion = new CheckVersion();
export default checkVersion;
/**
* Enable the check update feature
* @param value Should the check update feature be enabled?
* @returns
*/
export async function enableCheckUpdate(value : boolean) {
await Settings.set("checkUpdate", value);
clearInterval(interval);
if (value) {
startInterval();
}
}

3
backend/docker.ts Normal file
View File

@ -0,0 +1,3 @@
export class Docker {
}

View File

@ -29,11 +29,9 @@ import { Stack } from "./stack";
import { Cron } from "croner";
import gracefulShutdown from "http-graceful-shutdown";
import User from "./models/user";
import childProcessAsync from "promisify-child-process";
import childProcess from "child_process";
import { Terminal } from "./terminal";
import "dotenv/config";
export class DockgeServer {
app : Express;
httpServer : http.Server;
@ -308,7 +306,6 @@ export class DockgeServer {
this.sendStackList(true);
});
checkVersion.startInterval();
});
gracefulShutdown(this.httpServer, {
@ -486,7 +483,7 @@ export class DockgeServer {
return jwtSecretBean;
}
async sendStackList(useCache = false) {
sendStackList(useCache = false) {
let roomList = this.io.sockets.adapter.rooms.keys();
let map : Map<string, object> | undefined;
@ -497,7 +494,7 @@ export class DockgeServer {
// Get the list only if there is a room
if (!map) {
map = new Map();
let stackList = await Stack.getStackList(this, useCache);
let stackList = Stack.getStackList(this, useCache);
for (let [ stackName, stack ] of stackList) {
map.set(stackName, stack.toSimpleJSON());
@ -513,8 +510,8 @@ export class DockgeServer {
}
}
async sendStackStatusList() {
let statusList = await Stack.getStatusList();
sendStackStatusList() {
let statusList = Stack.getStatusList();
let roomList = this.io.sockets.adapter.rooms.keys();
@ -532,15 +529,8 @@ export class DockgeServer {
}
}
async getDockerNetworkList() : Promise<string[]> {
let res = await childProcessAsync.spawn("docker", [ "network", "ls", "--format", "{{.Name}}" ], {
encoding: "utf-8",
});
if (!res.stdout) {
return [];
}
getDockerNetworkList() : string[] {
let res = childProcess.spawnSync("docker", [ "network", "ls", "--format", "{{.Name}}" ]);
let list = res.stdout.toString().split("\n");
// Remove empty string item

View File

@ -12,7 +12,7 @@ export class DockerSocketHandler extends SocketHandler {
socket.on("deployStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => {
try {
checkLogin(socket);
const stack = await this.saveStack(socket, server, name, composeYAML, composeENV, isAdd);
const stack = this.saveStack(socket, server, name, composeYAML, composeENV, isAdd);
await stack.deploy(socket);
server.sendStackList();
callback({
@ -45,7 +45,7 @@ export class DockerSocketHandler extends SocketHandler {
if (typeof(name) !== "string") {
throw new ValidationError("Name must be a string");
}
const stack = await Stack.getStack(server, name);
const stack = Stack.getStack(server, name);
try {
await stack.delete(socket);
@ -65,7 +65,7 @@ export class DockerSocketHandler extends SocketHandler {
}
});
socket.on("getStack", async (stackName : unknown, callback) => {
socket.on("getStack", (stackName : unknown, callback) => {
try {
checkLogin(socket);
@ -73,7 +73,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string");
}
const stack = await Stack.getStack(server, stackName);
const stack = Stack.getStack(server, stackName);
if (stack.isManagedByDockge) {
stack.joinCombinedTerminal(socket);
@ -111,7 +111,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string");
}
const stack = await Stack.getStack(server, stackName);
const stack = Stack.getStack(server, stackName);
await stack.start(socket);
callback({
ok: true,
@ -135,7 +135,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string");
}
const stack = await Stack.getStack(server, stackName);
const stack = Stack.getStack(server, stackName);
await stack.stop(socket);
callback({
ok: true,
@ -156,7 +156,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string");
}
const stack = await Stack.getStack(server, stackName);
const stack = Stack.getStack(server, stackName);
await stack.restart(socket);
callback({
ok: true,
@ -177,7 +177,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string");
}
const stack = await Stack.getStack(server, stackName);
const stack = Stack.getStack(server, stackName);
await stack.update(socket);
callback({
ok: true,
@ -198,7 +198,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string");
}
const stack = await Stack.getStack(server, stackName);
const stack = Stack.getStack(server, stackName);
await stack.down(socket);
callback({
ok: true,
@ -219,7 +219,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string");
}
const stack = await Stack.getStack(server, stackName, true);
const stack = Stack.getStack(server, stackName, true);
const serviceStatusList = Object.fromEntries(await stack.getServiceStatusList());
callback({
ok: true,
@ -234,7 +234,7 @@ export class DockerSocketHandler extends SocketHandler {
socket.on("getDockerNetworkList", async (callback) => {
try {
checkLogin(socket);
const dockerNetworkList = await server.getDockerNetworkList();
const dockerNetworkList = server.getDockerNetworkList();
callback({
ok: true,
dockerNetworkList,
@ -264,7 +264,7 @@ export class DockerSocketHandler extends SocketHandler {
});
}
async saveStack(socket : DockgeSocket, server : DockgeServer, name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown) : Promise<Stack> {
saveStack(socket : DockgeSocket, server : DockgeServer, name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown) : Stack {
// Check types
if (typeof(name) !== "string") {
throw new ValidationError("Name must be a string");
@ -280,7 +280,7 @@ export class DockerSocketHandler extends SocketHandler {
}
const stack = new Stack(server, name, composeYAML, composeENV, false);
await stack.save(isAdd);
stack.save(isAdd);
return stack;
}

View File

@ -101,7 +101,7 @@ export class TerminalSocketHandler extends SocketHandler {
log.debug("interactiveTerminal", "Service name: " + serviceName);
// Get stack
const stack = await Stack.getStack(server, stackName);
const stack = Stack.getStack(server, stackName);
stack.joinContainerTerminal(socket, serviceName, shell);
callback({
@ -151,7 +151,7 @@ export class TerminalSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string.");
}
const stack = await Stack.getStack(server, stackName);
const stack = Stack.getStack(server, stackName);
await stack.leaveCombinedTerminal(socket);
callback({

View File

@ -1,8 +1,8 @@
import { DockgeServer } from "./dockge-server";
import fs, { promises as fsAsync } from "fs";
import fs from "fs";
import { log } from "./log";
import yaml from "yaml";
import { DockgeSocket, fileExists, ValidationError } from "./util-server";
import { DockgeSocket, ValidationError } from "./util-server";
import path from "path";
import {
COMBINED_TERMINAL_COLS,
@ -16,7 +16,7 @@ import {
UNKNOWN
} from "./util-common";
import { InteractiveTerminal, Terminal } from "./terminal";
import childProcessAsync from "promisify-child-process";
import childProcess from "child_process";
export class Stack {
@ -72,15 +72,11 @@ export class Stack {
/**
* Get the status of the stack from `docker compose ps --format json`
*/
async ps() : Promise<object> {
let res = await childProcessAsync.spawn("docker", [ "compose", "ps", "--format", "json" ], {
cwd: this.path,
encoding: "utf-8",
ps() : object {
let res = childProcess.execSync("docker compose ps --format json", {
cwd: this.path
});
if (!res.stdout) {
return {};
}
return JSON.parse(res.stdout.toString());
return JSON.parse(res.toString());
}
get isManagedByDockge() : boolean {
@ -99,15 +95,6 @@ export class Stack {
// Check YAML format
yaml.parse(this.composeYAML);
let lines = this.composeENV.split("\n");
// Check if the .env is able to pass docker-compose
// Prevent "setenv: The parameter is incorrect"
// It only happens when there is one line and it doesn't contain "="
if (lines.length === 1 && !lines[0].includes("=") && lines[0].length > 0) {
throw new ValidationError("Invalid .env format");
}
}
get composeYAML() : string {
@ -155,35 +142,29 @@ export class Stack {
* Save the stack to the disk
* @param isAdd
*/
async save(isAdd : boolean) {
save(isAdd : boolean) {
this.validate();
let dir = this.path;
// Check if the name is used if isAdd
if (isAdd) {
if (await fileExists(dir)) {
if (fs.existsSync(dir)) {
throw new ValidationError("Stack name already exists");
}
// Create the stack folder
await fsAsync.mkdir(dir);
fs.mkdirSync(dir);
} else {
if (!await fileExists(dir)) {
if (!fs.existsSync(dir)) {
throw new ValidationError("Stack not found");
}
}
// Write or overwrite the compose.yaml
await fsAsync.writeFile(path.join(dir, this._composeFileName), this.composeYAML);
const envPath = path.join(dir, ".env");
fs.writeFileSync(path.join(dir, this._composeFileName), this.composeYAML);
// Write or overwrite the .env
// If .env is not existing and the composeENV is empty, we don't need to write it
if (await fileExists(envPath) || this.composeENV.trim() !== "") {
await fsAsync.writeFile(envPath, this.composeENV);
}
fs.writeFileSync(path.join(dir, ".env"), this.composeENV);
}
async deploy(socket? : DockgeSocket) : Promise<number> {
@ -203,7 +184,7 @@ export class Stack {
}
// Remove the stack folder
await fsAsync.rm(this.path, {
fs.rmSync(this.path, {
recursive: true,
force: true
});
@ -211,8 +192,8 @@ export class Stack {
return exitCode;
}
async updateStatus() {
let statusList = await Stack.getStatusList();
updateStatus() {
let statusList = Stack.getStatusList();
let status = statusList.get(this.name);
if (status) {
@ -222,7 +203,7 @@ export class Stack {
}
}
static async getStackList(server : DockgeServer, useCacheForManaged = false) : Promise<Map<string, Stack>> {
static getStackList(server : DockgeServer, useCacheForManaged = false) : Map<string, Stack> {
let stacksDir = server.stacksDir;
let stackList : Map<string, Stack>;
@ -233,16 +214,16 @@ export class Stack {
stackList = new Map<string, Stack>();
// Scan the stacks directory, and get the stack list
let filenameList = await fsAsync.readdir(stacksDir);
let filenameList = fs.readdirSync(stacksDir);
for (let filename of filenameList) {
try {
// Check if it is a directory
let stat = await fsAsync.stat(path.join(stacksDir, filename));
let stat = fs.statSync(path.join(stacksDir, filename));
if (!stat.isDirectory()) {
continue;
}
let stack = await this.getStack(server, filename);
let stack = this.getStack(server, filename);
stack._status = CREATED_FILE;
stackList.set(filename, stack);
} catch (e) {
@ -257,15 +238,8 @@ export class Stack {
}
// Get status from docker compose ls
let res = await childProcessAsync.spawn("docker", [ "compose", "ls", "--all", "--format", "json" ], {
encoding: "utf-8",
});
if (!res.stdout) {
return stackList;
}
let composeList = JSON.parse(res.stdout.toString());
let res = childProcess.execSync("docker compose ls --all --format json");
let composeList = JSON.parse(res.toString());
for (let composeStack of composeList) {
let stack = stackList.get(composeStack.Name);
@ -291,18 +265,11 @@ export class Stack {
* 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
*/
static async getStatusList() : Promise<Map<string, number>> {
static getStatusList() : Map<string, number> {
let statusList = new Map<string, number>();
let res = await childProcessAsync.spawn("docker", [ "compose", "ls", "--all", "--format", "json" ], {
encoding: "utf-8",
});
if (!res.stdout) {
return statusList;
}
let composeList = JSON.parse(res.stdout.toString());
let res = childProcess.execSync("docker compose ls --all --format json");
let composeList = JSON.parse(res.toString());
for (let composeStack of composeList) {
statusList.set(composeStack.Name, this.statusConvert(composeStack.Status));
@ -330,13 +297,13 @@ export class Stack {
}
}
static async getStack(server: DockgeServer, stackName: string, skipFSOperations = false) : Promise<Stack> {
static getStack(server: DockgeServer, stackName: string, skipFSOperations = false) : Stack {
let dir = path.join(server.stacksDir, stackName);
if (!skipFSOperations) {
if (!await fileExists(dir) || !(await fsAsync.stat(dir)).isDirectory()) {
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
// Maybe it is a stack managed by docker compose directly
let stackList = await this.getStackList(server, true);
let stackList = this.getStackList(server, true);
let stack = stackList.get(stackName);
if (stack) {
@ -407,7 +374,7 @@ export class Stack {
}
// If the stack is not running, we don't need to restart it
await this.updateStatus();
this.updateStatus();
log.debug("update", "Status: " + this.status);
if (this.status !== RUNNING) {
return exitCode;
@ -455,35 +422,24 @@ export class Stack {
async getServiceStatusList() {
let statusList = new Map<string, number>();
try {
let res = await childProcessAsync.spawn("docker", [ "compose", "ps", "--format", "json" ], {
cwd: this.path,
encoding: "utf-8",
});
let res = childProcess.spawnSync("docker", [ "compose", "ps", "--format", "json" ], {
cwd: this.path,
});
if (!res.stdout) {
return statusList;
}
let lines = res.stdout.toString().split("\n");
let lines = res.stdout?.toString().split("\n");
for (let line of lines) {
try {
let obj = JSON.parse(line);
if (obj.Health === "") {
statusList.set(obj.Service, obj.State);
} else {
statusList.set(obj.Service, obj.Health);
}
} catch (e) {
for (let line of lines) {
try {
let obj = JSON.parse(line);
if (obj.Health === "") {
statusList.set(obj.Service, obj.State);
} else {
statusList.set(obj.Service, obj.Health);
}
} catch (e) {
}
return statusList;
} catch (e) {
log.error("getServiceStatusList", e);
return statusList;
}
return statusList;
}
}

View File

@ -1,17 +1,13 @@
/*
* Common utilities for backend and frontend
*/
import yaml, { Document, Pair, Scalar } from "yaml";
import { DotenvParseOutput } from "dotenv";
import { Document } from "yaml";
// Init dayjs
import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import relativeTime from "dayjs/plugin/relativeTime";
// @ts-ignore
import { replaceVariablesSync } from "@inventage/envsubst";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime);
@ -344,48 +340,3 @@ export function parseDockerPort(input : string, defaultHostname : string = "loca
display: display,
};
}
export function envsubst(string : string, variables : LooseObject) : string {
return replaceVariablesSync(string, variables)[0];
}
/**
* Traverse all values in the yaml and for each value, if there are template variables, replace it environment variables
* Emulates the behavior of how docker-compose handles environment variables in yaml files
* @param content Yaml string
* @param env Environment variables
* @returns string Yaml string with environment variables replaced
*/
export function envsubstYAML(content : string, env : DotenvParseOutput) : string {
const doc = yaml.parseDocument(content);
if (doc.contents) {
// @ts-ignore
for (const item of doc.contents.items) {
traverseYAML(item, env);
}
}
return doc.toString();
}
/**
* Used for envsubstYAML(...)
* @param pair
* @param env
*/
function traverseYAML(pair : Pair, env : DotenvParseOutput) : void {
// @ts-ignore
if (pair.value && pair.value.items) {
// @ts-ignore
for (const item of pair.value.items) {
if (item instanceof Pair) {
traverseYAML(item, env);
} else if (item instanceof Scalar) {
item.value = envsubst(item.value, env);
}
}
// @ts-ignore
} else if (pair.value && typeof(pair.value.value) === "string") {
// @ts-ignore
pair.value.value = envsubst(pair.value.value, env);
}
}

View File

@ -5,7 +5,6 @@ import { log } from "./log";
import { ERROR_TYPE_VALIDATION } from "./util-common";
import { R } from "redbean-node";
import { verifyPassword } from "./password-hash";
import fs from "fs";
export interface JWTDecoded {
username : string;
@ -83,9 +82,3 @@ export async function doubleCheckPassword(socket : DockgeSocket, currentPassword
return user;
}
export function fileExists(file : string) {
return fs.promises.access(file, fs.constants.F_OK)
.then(() => true)
.catch(() => false);
}

View File

@ -1,92 +0,0 @@
import fsAsync from "fs/promises";
import unzipper from "unzipper";
import stream from "node:stream";
import { fileExists } from "../backend/util-server";
const version = process.env.VERSION;
if (!version) {
console.error("VERSION env not set");
process.exit(1);
}
const output = `./private/build/dockgen-${version}-win-x64.zip`;
if (await fileExists(output)) {
console.error(`${output} already exists`);
process.exit(1);
}
console.log(`Building ${output}`);
const nodeVersion = "18.17.1";
const buildPath = "./private/build/windows";
const nodePath = `${buildPath}/node`;
const nodeTempPath = `${buildPath}/node-v${nodeVersion}-win-x64`;
const corePath = `${buildPath}/core`;
// Clear
await fsAsync.rm(`${buildPath}/dockge-${version}`, {
recursive: true,
force: true
});
await fsAsync.rm(corePath, {
recursive: true,
force: true
});
// mkdir
await fsAsync.mkdir(buildPath, {
recursive: true
});
// Download Node.js if not exists
// Download,pipe to unzipper and extract to nodePath
if (!await fileExists(nodePath)) {
console.log(`Downloading Node.js ${nodeVersion}`);
try {
await download(`https://nodejs.org/dist/v${nodeVersion}/node-v${nodeVersion}-win-x64.zip`);
// Rename folder
await fsAsync.rename(nodeTempPath, nodePath);
} catch (e) {
if (e instanceof Error) {
console.error(e.message);
}
process.exit(1);
}
} else {
console.log(`Node.js ${nodeVersion} already exists, skipping download`);
}
// Download Dockge from GitHub
console.log(`Downloading Dockge ${version} from GitHub`);
try {
await download(`https://github.com/louislam/dockge/archive/refs/tags/${version}.zip`);
// Rename folder
await fsAsync.rename(`${buildPath}/dockge-${version}`, corePath);
} catch (e) {
if (e instanceof Error) {
console.error(e.message);
}
process.exit(1);
}
function download(url : string) {
return new Promise((resolve, reject) => {
fetch(url).then((res) => {
if (res.body) {
// @ts-ignore
stream.Readable.fromWeb(res.body)
.pipe(unzipper.Extract({
path: buildPath,
}))
.on("close", resolve);
} else {
reject(new Error(`Unable to download ${url}`));
}
});
});
}

View File

@ -9,7 +9,7 @@
<div v-if="!isEditMode">
<span class="badge me-1" :class="bgStyle">{{ status }}</span>
<a v-for="port in envsubstService.ports" :key="port" :href="parsePort(port).url" target="_blank">
<a v-for="port in service.ports" :key="port" :href="parsePort(port).url" target="_blank">
<span class="badge me-1 bg-secondary">{{ parsePort(port).display }}</span>
</a>
</div>
@ -213,29 +213,16 @@ export default defineComponent({
jsonObject() {
return this.$parent.$parent.jsonConfig;
},
envsubstJSONConfig() {
return this.$parent.$parent.envsubstJSONConfig;
},
envsubstService() {
if (!this.envsubstJSONConfig.services[this.name]) {
return {};
}
return this.envsubstJSONConfig.services[this.name];
},
imageName() {
if (this.envsubstService.image) {
return this.envsubstService.image.split(":")[0];
if (this.service.image) {
return this.service.image.split(":")[0];
} else {
return "";
}
},
imageTag() {
if (this.envsubstService.image) {
let tag = this.envsubstService.image.split(":")[1];
if (this.service.image) {
let tag = this.service.image.split(":")[1];
if (tag) {
return tag;

View File

@ -39,7 +39,7 @@ for (let lang in languageList) {
};
}
const rtlLangs = [ "fa", "ar-SY", "ur", "ar" ];
const rtlLangs = [ "fa", "ar-SY", "ur" ];
export const currentLocale = () => localStorage.locale
|| languageList[navigator.language] && navigator.language

View File

@ -98,6 +98,5 @@
"reconnecting...": "Reconnecting…",
"connecting...": "Connecting to the socket server…",
"url": "URL | URLs",
"extra": "Extra",
"newUpdate": "New Update"
"extra": "Extra"
}

View File

@ -12,7 +12,7 @@
"registry": "Registro",
"compose": "Compose",
"addFirstStackMsg": "Componi il tuo primo stack!",
"stackName": "Nome dello stack",
"stackName" : "Nome dello stack",
"deployStack": "Deploy",
"deleteStack": "Cancella",
"stopStack": "Stop",
@ -23,7 +23,7 @@
"editStack": "Modifica",
"discardStack": "Annulla",
"saveStackDraft": "Salva",
"notAvailableShort": "N/D",
"notAvailableShort" : "N/D",
"deleteStackMsg": "Sei sicuro di voler eliminare questo stack?",
"stackNotManagedByDockgeMsg": "Questo stack non è gestito da Dockge.",
"primaryHostname": "Hostname primario",
@ -91,11 +91,5 @@
"Allowed commands:": "Comandi permessi:",
"Internal Networks": "Reti interne",
"External Networks": "Reti esterne",
"No External Networks": "Nessuna rete esterna",
"reverseProxyMsg1": "Utilizzando un proxy inverso?",
"reverseProxyMsg2": "Controlla come configurarlo per WebSocket",
"Cannot connect to the socket server.": "Impossibile connettersi al server socket.",
"connecting...": "Connessione al server socket…",
"extra": "Extra",
"reconnecting...": "Riconnessione…"
"No External Networks": "Nessuna rete esterna"
}

View File

@ -17,7 +17,7 @@
"editStack": "編集",
"discardStack": "破棄",
"saveStackDraft": "保存",
"stackNotManagedByDockgeMsg": "このスタックはDockgeによって管理されていません。",
"stackNotManagedByDockgeMsg": "このスタックはDockageによって管理されていません。",
"general": "一般",
"scanFolder": "スタックフォルダをスキャン",
"dockerImage": "イメージ",

View File

@ -90,13 +90,5 @@
"Allowed commands:": "허용된 명령어:",
"Internal Networks": "내부 네트워크",
"External Networks": "외부 네트워크",
"No External Networks": "외부 네트워크 없음",
"reverseProxyMsg2": "여기서 WebSocket을 위한 설정을 확인해 보세요",
"downStack": "정지 & Down",
"reverseProxyMsg1": "리버스 프록시를 사용하고 계신가요?",
"Cannot connect to the socket server.": "소켓 서버에 연결하지 못했습니다.",
"connecting...": "소켓 서버에 연결하는 중…",
"extra": "기타",
"url": "URL | URL",
"reconnecting...": "재연결 중…"
"No External Networks": "외부 네트워크 없음"
}

View File

@ -16,8 +16,8 @@
<span class="fs-4 title">Dockge</span>
</router-link>
<a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/dockge/releases" class="btn btn-warning me-3">
<font-awesome-icon icon="arrow-alt-circle-up" /> {{ $t("newUpdate") }}
<a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/dockge/releases" class="btn btn-info me-3">
<font-awesome-icon icon="arrow-alt-circle-up" /> {{ $t("New Update") }}
</a>
<ul class="nav nav-pills">

View File

@ -231,7 +231,7 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import {
COMBINED_TERMINAL_COLS,
COMBINED_TERMINAL_ROWS,
copyYAMLComments, envsubstYAML,
copyYAMLComments,
getCombinedTerminalName,
getComposeTerminalName,
PROGRESS_TERMINAL_ROWS,
@ -239,7 +239,6 @@ import {
} from "../../../backend/util-common";
import { BModal } from "bootstrap-vue-next";
import NetworkInput from "../components/NetworkInput.vue";
import dotenv from "dotenv";
const template = `version: "3.8"
services:
@ -278,7 +277,6 @@ export default {
return {
editorFocus: false,
jsonConfig: {},
envsubstJSONConfig: {},
yamlError: "",
processing: true,
showProgressTerminal: false,
@ -299,12 +297,12 @@ export default {
computed: {
urls() {
if (!this.envsubstJSONConfig["x-dockge"] || !this.envsubstJSONConfig["x-dockge"].urls || !Array.isArray(this.envsubstJSONConfig["x-dockge"].urls)) {
if (!this.jsonConfig["x-dockge"] || !this.jsonConfig["x-dockge"].urls || !Array.isArray(this.jsonConfig["x-dockge"].urls)) {
return [];
}
let urls = [];
for (const url of this.envsubstJSONConfig["x-dockge"].urls) {
for (const url of this.jsonConfig["x-dockge"].urls) {
let display;
try {
let obj = new URL(url);
@ -374,17 +372,6 @@ export default {
},
deep: true,
},
"stack.composeENV": {
handler() {
if (this.editorFocus) {
console.debug("env code changed");
this.yamlCodeChange();
}
},
deep: true,
},
jsonConfig: {
handler() {
if (!this.editorFocus) {
@ -635,7 +622,7 @@ export default {
greedy: true
},
"keyword": {
pattern: /^\w*(?=[:=])/m,
pattern: /^[^ :=]*(?=[:=])/m,
greedy: true
},
"value": {
@ -658,41 +645,28 @@ export default {
return highlight(code, languages.docker_env);
},
yamlToJSON(yaml) {
let doc = parseDocument(yaml);
if (doc.errors.length > 0) {
throw doc.errors[0];
}
const config = doc.toJS() ?? {};
// Check data types
// "services" must be an object
if (!config.services) {
config.services = {};
}
if (Array.isArray(config.services) || typeof config.services !== "object") {
throw new Error("Services must be an object");
}
return {
config,
doc,
};
},
yamlCodeChange() {
try {
let { config, doc } = this.yamlToJSON(this.stack.composeYAML);
let doc = parseDocument(this.stack.composeYAML);
if (doc.errors.length > 0) {
throw doc.errors[0];
}
const config = doc.toJS() ?? {};
// Check data types
// "services" must be an object
if (!config.services) {
config.services = {};
}
if (Array.isArray(config.services) || typeof config.services !== "object") {
throw new Error("Services must be an object");
}
this.yamlDoc = doc;
this.jsonConfig = config;
let env = dotenv.parse(this.stack.composeENV);
let envYAML = envsubstYAML(this.stack.composeYAML, env);
this.envsubstJSONConfig = this.yamlToJSON(envYAML).config;
clearTimeout(yamlErrorTimeout);
this.yamlError = "";
} catch (e) {

View File

@ -1,6 +1,6 @@
{
"name": "dockge",
"version": "1.3.2",
"version": "1.2.0",
"type": "module",
"engines": {
"node": ">= 18.0.0 && <= 18.17.1"
@ -10,15 +10,13 @@
"lint": "eslint \"**/*.{ts,vue}\"",
"check-ts": "tsc --noEmit",
"start": "tsx ./backend/index.ts",
"dev": "concurrently -k -r \"wait-on tcp:5000 && pnpm run dev:backend \" \"pnpm run dev:frontend\"",
"dev:backend": "cross-env NODE_ENV=development tsx watch --inspect ./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 && pnpm run build:docker",
"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",
"build:docker-base": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:base -f ./docker/Base.Dockerfile . --push",
"build:docker": "node ./extra/env2arg.js docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:latest -t louislam/dockge:1 -t louislam/dockge:$VERSION --target release -f ./docker/Dockerfile . --push",
"build:docker-nightly": "pnpm run build:frontend && docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:nightly --target nightly -f ./docker/Dockerfile . --push",
"build:windows": "tsx ./extra/build-windows.ts",
"build:healthcheck": "docker buildx build -f docker/BuildHealthCheck.Dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:build-healthcheck . --push",
"start-docker": "docker run --rm -p 5001:5001 --name dockge louislam/dockge:latest",
"mark-as-nightly": "tsx ./extra/mark-as-nightly.ts",
@ -26,8 +24,7 @@
"reset-password": "tsx ./extra/reset-password.ts"
},
"dependencies": {
"@homebridge/node-pty-prebuilt-multiarch": "~0.11.12",
"@inventage/envsubst": "^0.16.0",
"@homebridge/node-pty-prebuilt-multiarch": "~0.11.11",
"@louislam/sqlite3": "~15.1.6",
"bcryptjs": "~2.4.3",
"check-password-strength": "~2.0.7",
@ -36,7 +33,6 @@
"composerize": "~1.4.1",
"croner": "~7.0.5",
"dayjs": "~1.11.10",
"dotenv": "~16.3.1",
"express": "~4.18.2",
"express-static-gzip": "~2.1.7",
"http-graceful-shutdown": "~3.1.13",
@ -44,37 +40,33 @@
"jwt-decode": "~3.1.2",
"knex": "~2.5.1",
"limiter-es6-compat": "~2.1.2",
"mysql2": "~3.6.5",
"node-windows": "1.0.0-beta.8",
"promisify-child-process": "~4.1.2",
"mysql2": "~3.6.3",
"redbean-node": "~0.3.3",
"socket.io": "~4.7.2",
"socket.io-client": "~4.7.2",
"timezones-list": "~3.0.2",
"ts-command-line-args": "~2.5.1",
"tsx": "~4.6.2",
"tsx": "~3.14.0",
"type-fest": "~4.3.3",
"yaml": "~2.3.4"
},
"devDependencies": {
"@actions/github": "^6.0.0",
"@fontsource/jetbrains-mono": "^5.0.18",
"@fontsource/jetbrains-mono": "^5.0.17",
"@fortawesome/fontawesome-svg-core": "6.4.2",
"@fortawesome/free-regular-svg-icons": "6.4.2",
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@fortawesome/vue-fontawesome": "3.0.3",
"@types/bcryptjs": "^2.4.6",
"@types/bootstrap": "~5.2.10",
"@types/bootstrap": "~5.2.9",
"@types/command-exists": "~1.2.3",
"@types/express": "~4.17.21",
"@types/jsonwebtoken": "~9.0.5",
"@types/unzipper": "^0.10.9",
"@typescript-eslint/eslint-plugin": "~6.8.0",
"@typescript-eslint/parser": "~6.8.0",
"@vitejs/plugin-vue": "~4.5.2",
"@vitejs/plugin-vue": "~4.5.0",
"bootstrap": "5.3.2",
"bootstrap-vue-next": "~0.14.10",
"concurrently": "^8.2.2",
"cross-env": "~7.0.3",
"eslint": "~8.50.0",
"eslint-plugin-jsdoc": "~46.8.2",
@ -83,17 +75,15 @@
"sass": "~1.68.0",
"typescript": "~5.2.2",
"unplugin-vue-components": "~0.25.2",
"unzipper": "^0.10.14",
"vite": "~5.0.7",
"vite": "~5.0.0",
"vite-plugin-compression": "~0.5.1",
"vue": "~3.3.11",
"vue": "~3.3.8",
"vue-eslint-parser": "~9.3.2",
"vue-i18n": "~9.5.0",
"vue-prism-editor": "2.0.0-alpha.2",
"vue-qrcode": "~2.2.0",
"vue-router": "~4.2.5",
"vue-toastification": "2.0.0-rc.5",
"wait-on": "^7.2.0",
"xterm": "5.4.0-beta.37",
"xterm-addon-web-links": "~0.9.0"
}

1524
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff