mirror of
https://github.com/YuzuZensai/Termix.git
synced 2026-09-14 11:08:59 +00:00
d1a8dcf3c6
* chore(deps-dev): bump the dev-minor-updates group across 1 directory with 12 updates (#910) Bumps the dev-minor-updates group with 12 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.2.6` | `2.3.1` | | [@radix-ui/react-slider](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slider) | `1.3.6` | `1.4.1` | | [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.2.5` | `1.3.0` | | [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.2.6` | `1.3.1` | | [cytoscape](https://github.com/cytoscape/cytoscape.js) | `3.33.4` | `3.34.0` | | [electron](https://github.com/electron/electron) | `42.2.0` | `42.4.1` | | [electron-builder](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-builder) | `26.8.1` | `26.15.3` | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.16.0` | `1.20.0` | | [radix-ui](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/radix-ui) | `1.4.3` | `1.6.0` | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.76.1` | `7.79.0` | | [sharp](https://github.com/lovell/sharp) | `0.34.5` | `0.35.1` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.60.0` | `8.61.1` | Updates `@radix-ui/react-select` from 2.2.6 to 2.3.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select) Updates `@radix-ui/react-slider` from 1.3.6 to 1.4.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slider/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slider) Updates `@radix-ui/react-slot` from 1.2.5 to 1.3.0 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot) Updates `@radix-ui/react-switch` from 1.2.6 to 1.3.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch) Updates `cytoscape` from 3.33.4 to 3.34.0 - [Release notes](https://github.com/cytoscape/cytoscape.js/releases) - [Commits](https://github.com/cytoscape/cytoscape.js/compare/v3.33.4...v3.34.0) Updates `electron` from 42.2.0 to 42.4.1 - [Release notes](https://github.com/electron/electron/releases) - [Commits](https://github.com/electron/electron/compare/v42.2.0...v42.4.1) Updates `electron-builder` from 26.8.1 to 26.15.3 - [Release notes](https://github.com/electron-userland/electron-builder/releases) - [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-builder/CHANGELOG.md) - [Commits](https://github.com/electron-userland/electron-builder/commits/electron-builder@26.15.3/packages/electron-builder) Updates `lucide-react` from 1.16.0 to 1.20.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/1.20.0/packages/lucide-react) Updates `radix-ui` from 1.4.3 to 1.6.0 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/radix-ui/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/radix-ui) Updates `react-hook-form` from 7.76.1 to 7.79.0 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.76.1...v7.79.0) Updates `sharp` from 0.34.5 to 0.35.1 - [Release notes](https://github.com/lovell/sharp/releases) - [Commits](https://github.com/lovell/sharp/compare/v0.34.5...v0.35.1) Updates `typescript-eslint` from 8.60.0 to 8.61.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.1/packages/typescript-eslint) --- updated-dependencies: - dependency-name: "@radix-ui/react-select" dependency-version: 2.3.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: "@radix-ui/react-slider" dependency-version: 1.4.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: "@radix-ui/react-slot" dependency-version: 1.3.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: "@radix-ui/react-switch" dependency-version: 1.3.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: cytoscape dependency-version: 3.34.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: electron dependency-version: 42.4.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: electron-builder dependency-version: 26.15.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: lucide-react dependency-version: 1.18.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: radix-ui dependency-version: 1.6.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: react-hook-form dependency-version: 7.79.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: sharp dependency-version: 0.35.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: typescript-eslint dependency-version: 8.61.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump node in /docker in the docker-major-updates group (#914) Bumps the docker-major-updates group in /docker with 1 update: node. Updates `node` from 24-slim to 26-slim --- updated-dependencies: - dependency-name: node dependency-version: 26-slim dependency-type: direct:production dependency-group: docker-major-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * ci(deps): bump the github-actions group with 2 updates (#915) Bumps the github-actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [actions/setup-node](https://github.com/actions/setup-node). Updates `actions/checkout` from 5 to 6 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) Updates `actions/setup-node` from 4 to 6 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/setup-node dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump the prod-minor-updates group across 1 directory with 5 updates (#916) Bumps the prod-minor-updates group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [axios](https://github.com/axios/axios) | `1.17.0` | `1.18.0` | | [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) | `12.10.0` | `12.11.1` | | [body-parser](https://github.com/expressjs/body-parser) | `2.2.2` | `2.3.0` | | [multer](https://github.com/expressjs/multer) | `2.1.1` | `2.2.0` | | [undici](https://github.com/nodejs/undici) | `8.4.0` | `8.5.0` | Updates `axios` from 1.17.0 to 1.18.0 - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.17.0...v1.18.0) Updates `better-sqlite3` from 12.10.0 to 12.11.1 - [Release notes](https://github.com/WiseLibs/better-sqlite3/releases) - [Commits](https://github.com/WiseLibs/better-sqlite3/compare/v12.10.0...v12.11.1) Updates `body-parser` from 2.2.2 to 2.3.0 - [Release notes](https://github.com/expressjs/body-parser/releases) - [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/body-parser/compare/v2.2.2...v2.3.0) Updates `multer` from 2.1.1 to 2.2.0 - [Release notes](https://github.com/expressjs/multer/releases) - [Changelog](https://github.com/expressjs/multer/blob/main/CHANGELOG.md) - [Commits](https://github.com/expressjs/multer/compare/v2.1.1...v2.2.0) Updates `undici` from 8.4.0 to 8.5.0 - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v8.4.0...v8.5.0) --- updated-dependencies: - dependency-name: axios dependency-version: 1.18.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: better-sqlite3 dependency-version: 12.11.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: body-parser dependency-version: 2.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: multer dependency-version: 2.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: undici dependency-version: 8.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat: add unlink button for dual auth * fix: general bug fixes * fix: general qol/small feature additions * fix: 100mb max upload size * fix: terminal syntax not handling carriage returns or shell prompt lines properly * feat: continued general improvements * fix: terminal outputting success right after folder path * chore: update release notes * feat: continued fixes and improvements * chore: lint, format, and bump version to 2.4.1 * chore: sync Crowdin translations for 2.4.1 * fix: rebase dev branch onto main before Crowdin download * fix: use merge instead of rebase to sync main before Crowdin --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1103 lines
29 KiB
TypeScript
1103 lines
29 KiB
TypeScript
import type express from "express";
|
|
import { logger } from "../utils/logger.js";
|
|
|
|
const sshLogger = logger;
|
|
|
|
type DockerSession = {
|
|
isConnected: boolean;
|
|
lastActive: number;
|
|
activeOperations: number;
|
|
hostId?: number;
|
|
isWindows?: boolean;
|
|
};
|
|
|
|
type PendingDockerTotpSession = unknown;
|
|
|
|
type ExecuteDockerCommand = (
|
|
session: DockerSession,
|
|
command: string,
|
|
sessionId: string,
|
|
userId: string,
|
|
hostId?: number,
|
|
) => Promise<string>;
|
|
|
|
type DockerContainerRoutesDeps = {
|
|
sshSessions: Record<string, DockerSession>;
|
|
pendingTOTPSessions: Record<string, PendingDockerTotpSession>;
|
|
getRequestUserId: (req: express.Request) => string | undefined;
|
|
executeDockerCommand: ExecuteDockerCommand;
|
|
dockerTimestampPattern: RegExp;
|
|
};
|
|
|
|
export function registerDockerContainerRoutes(
|
|
app: express.Express,
|
|
{
|
|
sshSessions,
|
|
pendingTOTPSessions,
|
|
getRequestUserId,
|
|
executeDockerCommand,
|
|
dockerTimestampPattern: DOCKER_TIMESTAMP_RE,
|
|
}: DockerContainerRoutesDeps,
|
|
): void {
|
|
/**
|
|
* @openapi
|
|
* /docker/containers/{sessionId}:
|
|
* get:
|
|
* summary: List all containers
|
|
* description: Lists all Docker containers on the host.
|
|
* tags:
|
|
* - Docker
|
|
* parameters:
|
|
* - in: path
|
|
* name: sessionId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* - in: query
|
|
* name: all
|
|
* schema:
|
|
* type: boolean
|
|
* responses:
|
|
* 200:
|
|
* description: A list of containers.
|
|
* 400:
|
|
* description: SSH session not found or not connected.
|
|
* 500:
|
|
* description: Failed to list containers.
|
|
*/
|
|
app.get("/docker/containers/:sessionId", async (req, res) => {
|
|
const { sessionId } = req.params;
|
|
const all = req.query.all !== "false";
|
|
const userId = getRequestUserId(req);
|
|
|
|
if (!userId) {
|
|
return res.status(401).json({ error: "Authentication required" });
|
|
}
|
|
|
|
if (pendingTOTPSessions[sessionId]) {
|
|
return res.status(400).json({
|
|
error: "Connection pending authentication",
|
|
code: "AUTH_PENDING",
|
|
});
|
|
}
|
|
|
|
const session = sshSessions[sessionId];
|
|
|
|
if (!session || !session.isConnected) {
|
|
return res.status(400).json({
|
|
error: "SSH session not found or not connected",
|
|
});
|
|
}
|
|
|
|
session.lastActive = Date.now();
|
|
session.activeOperations++;
|
|
|
|
try {
|
|
const allFlag = all ? "-a " : "";
|
|
const formatStr = session.isWindows
|
|
? `"{\\"id\\":\\"{{.ID}}\\",\\"name\\":\\"{{.Names}}\\",\\"image\\":\\"{{.Image}}\\",\\"status\\":\\"{{.Status}}\\",\\"state\\":\\"{{.State}}\\",\\"ports\\":\\"{{.Ports}}\\",\\"created\\":\\"{{.CreatedAt}}\\"}"`
|
|
: `'{"id":"{{.ID}}","name":"{{.Names}}","image":"{{.Image}}","status":"{{.Status}}","state":"{{.State}}","ports":"{{.Ports}}","created":"{{.CreatedAt}}"}' `;
|
|
const command = `docker ps ${allFlag}--format ${formatStr}`;
|
|
|
|
const output = await executeDockerCommand(
|
|
session,
|
|
command,
|
|
sessionId,
|
|
userId,
|
|
session.hostId,
|
|
);
|
|
|
|
const containers = output
|
|
.split("\n")
|
|
.filter((line) => line.trim())
|
|
.map((line) => {
|
|
try {
|
|
return JSON.parse(line);
|
|
} catch {
|
|
sshLogger.warn("Failed to parse container line", {
|
|
operation: "parse_container",
|
|
line,
|
|
});
|
|
return null;
|
|
}
|
|
})
|
|
.filter((c) => c !== null);
|
|
|
|
session.activeOperations--;
|
|
|
|
res.json(containers);
|
|
} catch (error) {
|
|
session.activeOperations--;
|
|
sshLogger.error("Failed to list Docker containers", error, {
|
|
operation: "list_containers",
|
|
sessionId,
|
|
userId,
|
|
});
|
|
|
|
res.status(500).json({
|
|
error:
|
|
error instanceof Error ? error.message : "Failed to list containers",
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @openapi
|
|
* /docker/containers/{sessionId}/{containerId}:
|
|
* get:
|
|
* summary: Get container details
|
|
* description: Retrieves detailed information about a specific container.
|
|
* tags:
|
|
* - Docker
|
|
* parameters:
|
|
* - in: path
|
|
* name: sessionId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* - in: path
|
|
* name: containerId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* responses:
|
|
* 200:
|
|
* description: Container details.
|
|
* 400:
|
|
* description: SSH session not found or not connected.
|
|
* 404:
|
|
* description: Container not found.
|
|
* 500:
|
|
* description: Failed to get container details.
|
|
*/
|
|
app.get("/docker/containers/:sessionId/:containerId", async (req, res) => {
|
|
const { sessionId, containerId } = req.params;
|
|
const userId = getRequestUserId(req);
|
|
|
|
if (!userId) {
|
|
return res.status(401).json({ error: "Authentication required" });
|
|
}
|
|
|
|
const session = sshSessions[sessionId];
|
|
|
|
if (!session || !session.isConnected) {
|
|
return res.status(400).json({
|
|
error: "SSH session not found or not connected",
|
|
});
|
|
}
|
|
|
|
session.lastActive = Date.now();
|
|
session.activeOperations++;
|
|
|
|
try {
|
|
const command = `docker inspect ${containerId}`;
|
|
const output = await executeDockerCommand(
|
|
session,
|
|
command,
|
|
sessionId,
|
|
userId,
|
|
session.hostId,
|
|
);
|
|
const details = JSON.parse(output);
|
|
|
|
session.activeOperations--;
|
|
|
|
if (details && details.length > 0) {
|
|
res.json(details[0]);
|
|
} else {
|
|
res.status(404).json({
|
|
error: "Container not found",
|
|
code: "CONTAINER_NOT_FOUND",
|
|
});
|
|
}
|
|
} catch (error) {
|
|
session.activeOperations--;
|
|
|
|
const errorMsg = error instanceof Error ? error.message : "";
|
|
if (errorMsg.includes("No such container")) {
|
|
return res.status(404).json({
|
|
error: "Container not found",
|
|
code: "CONTAINER_NOT_FOUND",
|
|
});
|
|
}
|
|
|
|
sshLogger.error("Failed to get container details", error, {
|
|
operation: "get_container_details",
|
|
sessionId,
|
|
containerId,
|
|
userId,
|
|
});
|
|
|
|
res.status(500).json({
|
|
error: errorMsg || "Failed to get container details",
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @openapi
|
|
* /docker/containers/{sessionId}/{containerId}/start:
|
|
* post:
|
|
* summary: Start container
|
|
* description: Starts a specific container.
|
|
* tags:
|
|
* - Docker
|
|
* parameters:
|
|
* - in: path
|
|
* name: sessionId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* - in: path
|
|
* name: containerId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* responses:
|
|
* 200:
|
|
* description: Container started successfully.
|
|
* 400:
|
|
* description: SSH session not found or not connected.
|
|
* 404:
|
|
* description: Container not found.
|
|
* 500:
|
|
* description: Failed to start container.
|
|
*/
|
|
app.post(
|
|
"/docker/containers/:sessionId/:containerId/start",
|
|
async (req, res) => {
|
|
const { sessionId, containerId } = req.params;
|
|
const userId = getRequestUserId(req);
|
|
|
|
if (!userId) {
|
|
return res.status(401).json({ error: "Authentication required" });
|
|
}
|
|
|
|
const session = sshSessions[sessionId];
|
|
|
|
if (!session || !session.isConnected) {
|
|
return res.status(400).json({
|
|
error: "SSH session not found or not connected",
|
|
});
|
|
}
|
|
|
|
session.lastActive = Date.now();
|
|
session.activeOperations++;
|
|
|
|
try {
|
|
sshLogger.info("Docker container operation", {
|
|
operation: "docker_container_op",
|
|
sessionId,
|
|
userId,
|
|
hostId: session.hostId,
|
|
containerId,
|
|
action: "start",
|
|
});
|
|
await executeDockerCommand(
|
|
session,
|
|
`docker start ${containerId}`,
|
|
sessionId,
|
|
userId,
|
|
session.hostId,
|
|
);
|
|
|
|
session.activeOperations--;
|
|
|
|
res.json({
|
|
success: true,
|
|
message: "Container started successfully",
|
|
});
|
|
} catch (error) {
|
|
session.activeOperations--;
|
|
|
|
const errorMsg = error instanceof Error ? error.message : "";
|
|
if (errorMsg.includes("No such container")) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: "Container not found",
|
|
code: "CONTAINER_NOT_FOUND",
|
|
});
|
|
}
|
|
|
|
sshLogger.error("Failed to start container", error, {
|
|
operation: "start_container",
|
|
sessionId,
|
|
containerId,
|
|
userId,
|
|
});
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: errorMsg || "Failed to start container",
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @openapi
|
|
* /docker/containers/{sessionId}/{containerId}/stop:
|
|
* post:
|
|
* summary: Stop container
|
|
* description: Stops a specific container.
|
|
* tags:
|
|
* - Docker
|
|
* parameters:
|
|
* - in: path
|
|
* name: sessionId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* - in: path
|
|
* name: containerId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* responses:
|
|
* 200:
|
|
* description: Container stopped successfully.
|
|
* 400:
|
|
* description: SSH session not found or not connected.
|
|
* 404:
|
|
* description: Container not found.
|
|
* 500:
|
|
* description: Failed to stop container.
|
|
*/
|
|
app.post(
|
|
"/docker/containers/:sessionId/:containerId/stop",
|
|
async (req, res) => {
|
|
const { sessionId, containerId } = req.params;
|
|
const userId = getRequestUserId(req);
|
|
|
|
if (!userId) {
|
|
return res.status(401).json({ error: "Authentication required" });
|
|
}
|
|
|
|
const session = sshSessions[sessionId];
|
|
|
|
if (!session || !session.isConnected) {
|
|
return res.status(400).json({
|
|
error: "SSH session not found or not connected",
|
|
});
|
|
}
|
|
|
|
session.lastActive = Date.now();
|
|
session.activeOperations++;
|
|
|
|
try {
|
|
sshLogger.info("Docker container operation", {
|
|
operation: "docker_container_op",
|
|
sessionId,
|
|
userId,
|
|
hostId: session.hostId,
|
|
containerId,
|
|
action: "stop",
|
|
});
|
|
await executeDockerCommand(
|
|
session,
|
|
`docker stop ${containerId}`,
|
|
sessionId,
|
|
userId,
|
|
session.hostId,
|
|
);
|
|
|
|
session.activeOperations--;
|
|
|
|
res.json({
|
|
success: true,
|
|
message: "Container stopped successfully",
|
|
});
|
|
} catch (error) {
|
|
session.activeOperations--;
|
|
|
|
const errorMsg = error instanceof Error ? error.message : "";
|
|
if (errorMsg.includes("No such container")) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: "Container not found",
|
|
code: "CONTAINER_NOT_FOUND",
|
|
});
|
|
}
|
|
|
|
sshLogger.error("Failed to stop container", error, {
|
|
operation: "stop_container",
|
|
sessionId,
|
|
containerId,
|
|
userId,
|
|
});
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: errorMsg || "Failed to stop container",
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @openapi
|
|
* /docker/containers/{sessionId}/{containerId}/restart:
|
|
* post:
|
|
* summary: Restart container
|
|
* description: Restarts a specific container.
|
|
* tags:
|
|
* - Docker
|
|
* parameters:
|
|
* - in: path
|
|
* name: sessionId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* - in: path
|
|
* name: containerId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* responses:
|
|
* 200:
|
|
* description: Container restarted successfully.
|
|
* 400:
|
|
* description: SSH session not found or not connected.
|
|
* 404:
|
|
* description: Container not found.
|
|
* 500:
|
|
* description: Failed to restart container.
|
|
*/
|
|
app.post(
|
|
"/docker/containers/:sessionId/:containerId/restart",
|
|
async (req, res) => {
|
|
const { sessionId, containerId } = req.params;
|
|
const userId = getRequestUserId(req);
|
|
|
|
if (!userId) {
|
|
return res.status(401).json({ error: "Authentication required" });
|
|
}
|
|
|
|
const session = sshSessions[sessionId];
|
|
|
|
if (!session || !session.isConnected) {
|
|
return res.status(400).json({
|
|
error: "SSH session not found or not connected",
|
|
});
|
|
}
|
|
|
|
session.lastActive = Date.now();
|
|
session.activeOperations++;
|
|
|
|
try {
|
|
sshLogger.info("Docker container operation", {
|
|
operation: "docker_container_op",
|
|
sessionId,
|
|
userId,
|
|
hostId: session.hostId,
|
|
containerId,
|
|
action: "restart",
|
|
});
|
|
await executeDockerCommand(
|
|
session,
|
|
`docker restart ${containerId}`,
|
|
sessionId,
|
|
userId,
|
|
session.hostId,
|
|
);
|
|
|
|
session.activeOperations--;
|
|
|
|
res.json({
|
|
success: true,
|
|
message: "Container restarted successfully",
|
|
});
|
|
} catch (error) {
|
|
session.activeOperations--;
|
|
|
|
const errorMsg = error instanceof Error ? error.message : "";
|
|
if (errorMsg.includes("No such container")) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: "Container not found",
|
|
code: "CONTAINER_NOT_FOUND",
|
|
});
|
|
}
|
|
|
|
sshLogger.error("Failed to restart container", error, {
|
|
operation: "restart_container",
|
|
sessionId,
|
|
containerId,
|
|
userId,
|
|
});
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: errorMsg || "Failed to restart container",
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @openapi
|
|
* /docker/containers/{sessionId}/{containerId}/pause:
|
|
* post:
|
|
* summary: Pause container
|
|
* description: Pauses a specific container.
|
|
* tags:
|
|
* - Docker
|
|
* parameters:
|
|
* - in: path
|
|
* name: sessionId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* - in: path
|
|
* name: containerId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* responses:
|
|
* 200:
|
|
* description: Container paused successfully.
|
|
* 400:
|
|
* description: SSH session not found or not connected.
|
|
* 404:
|
|
* description: Container not found.
|
|
* 500:
|
|
* description: Failed to pause container.
|
|
*/
|
|
app.post(
|
|
"/docker/containers/:sessionId/:containerId/pause",
|
|
async (req, res) => {
|
|
const { sessionId, containerId } = req.params;
|
|
const userId = getRequestUserId(req);
|
|
|
|
if (!userId) {
|
|
return res.status(401).json({ error: "Authentication required" });
|
|
}
|
|
|
|
const session = sshSessions[sessionId];
|
|
|
|
if (!session || !session.isConnected) {
|
|
return res.status(400).json({
|
|
error: "SSH session not found or not connected",
|
|
});
|
|
}
|
|
|
|
session.lastActive = Date.now();
|
|
session.activeOperations++;
|
|
|
|
try {
|
|
sshLogger.info("Docker container operation", {
|
|
operation: "docker_container_op",
|
|
sessionId,
|
|
userId,
|
|
hostId: session.hostId,
|
|
containerId,
|
|
action: "pause",
|
|
});
|
|
await executeDockerCommand(
|
|
session,
|
|
`docker pause ${containerId}`,
|
|
sessionId,
|
|
userId,
|
|
session.hostId,
|
|
);
|
|
|
|
session.activeOperations--;
|
|
|
|
res.json({
|
|
success: true,
|
|
message: "Container paused successfully",
|
|
});
|
|
} catch (error) {
|
|
session.activeOperations--;
|
|
|
|
const errorMsg = error instanceof Error ? error.message : "";
|
|
if (errorMsg.includes("No such container")) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: "Container not found",
|
|
code: "CONTAINER_NOT_FOUND",
|
|
});
|
|
}
|
|
|
|
sshLogger.error("Failed to pause container", error, {
|
|
operation: "pause_container",
|
|
sessionId,
|
|
containerId,
|
|
userId,
|
|
});
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: errorMsg || "Failed to pause container",
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @openapi
|
|
* /docker/containers/{sessionId}/{containerId}/unpause:
|
|
* post:
|
|
* summary: Unpause container
|
|
* description: Unpauses a specific container.
|
|
* tags:
|
|
* - Docker
|
|
* parameters:
|
|
* - in: path
|
|
* name: sessionId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* - in: path
|
|
* name: containerId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* responses:
|
|
* 200:
|
|
* description: Container unpaused successfully.
|
|
* 400:
|
|
* description: SSH session not found or not connected.
|
|
* 404:
|
|
* description: Container not found.
|
|
* 500:
|
|
* description: Failed to unpause container.
|
|
*/
|
|
app.post(
|
|
"/docker/containers/:sessionId/:containerId/unpause",
|
|
async (req, res) => {
|
|
const { sessionId, containerId } = req.params;
|
|
const userId = getRequestUserId(req);
|
|
|
|
if (!userId) {
|
|
return res.status(401).json({ error: "Authentication required" });
|
|
}
|
|
|
|
const session = sshSessions[sessionId];
|
|
|
|
if (!session || !session.isConnected) {
|
|
return res.status(400).json({
|
|
error: "SSH session not found or not connected",
|
|
});
|
|
}
|
|
|
|
session.lastActive = Date.now();
|
|
session.activeOperations++;
|
|
|
|
try {
|
|
sshLogger.info("Docker container operation", {
|
|
operation: "docker_container_op",
|
|
sessionId,
|
|
userId,
|
|
hostId: session.hostId,
|
|
containerId,
|
|
action: "unpause",
|
|
});
|
|
await executeDockerCommand(
|
|
session,
|
|
`docker unpause ${containerId}`,
|
|
sessionId,
|
|
userId,
|
|
session.hostId,
|
|
);
|
|
|
|
session.activeOperations--;
|
|
|
|
res.json({
|
|
success: true,
|
|
message: "Container unpaused successfully",
|
|
});
|
|
} catch (error) {
|
|
session.activeOperations--;
|
|
|
|
const errorMsg = error instanceof Error ? error.message : "";
|
|
if (errorMsg.includes("No such container")) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: "Container not found",
|
|
code: "CONTAINER_NOT_FOUND",
|
|
});
|
|
}
|
|
|
|
sshLogger.error("Failed to unpause container", error, {
|
|
operation: "unpause_container",
|
|
sessionId,
|
|
containerId,
|
|
userId,
|
|
});
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: errorMsg || "Failed to unpause container",
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @openapi
|
|
* /docker/containers/{sessionId}/{containerId}/remove:
|
|
* delete:
|
|
* summary: Remove container
|
|
* description: Removes a specific container.
|
|
* tags:
|
|
* - Docker
|
|
* parameters:
|
|
* - in: path
|
|
* name: sessionId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* - in: path
|
|
* name: containerId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* - in: query
|
|
* name: force
|
|
* schema:
|
|
* type: boolean
|
|
* responses:
|
|
* 200:
|
|
* description: Container removed successfully.
|
|
* 400:
|
|
* description: SSH session not found or not connected, or cannot remove a running container.
|
|
* 404:
|
|
* description: Container not found.
|
|
* 500:
|
|
* description: Failed to remove container.
|
|
*/
|
|
app.delete(
|
|
"/docker/containers/:sessionId/:containerId/remove",
|
|
async (req, res) => {
|
|
const { sessionId, containerId } = req.params;
|
|
const force = req.query.force === "true";
|
|
const userId = getRequestUserId(req);
|
|
|
|
if (!userId) {
|
|
return res.status(401).json({ error: "Authentication required" });
|
|
}
|
|
|
|
const session = sshSessions[sessionId];
|
|
|
|
if (!session || !session.isConnected) {
|
|
return res.status(400).json({
|
|
error: "SSH session not found or not connected",
|
|
});
|
|
}
|
|
|
|
session.lastActive = Date.now();
|
|
session.activeOperations++;
|
|
|
|
try {
|
|
sshLogger.info("Docker container operation", {
|
|
operation: "docker_container_op",
|
|
sessionId,
|
|
userId,
|
|
hostId: session.hostId,
|
|
containerId,
|
|
action: "remove",
|
|
});
|
|
const forceFlag = force ? "-f " : "";
|
|
await executeDockerCommand(
|
|
session,
|
|
`docker rm ${forceFlag}${containerId}`,
|
|
sessionId,
|
|
userId,
|
|
session.hostId,
|
|
);
|
|
|
|
session.activeOperations--;
|
|
|
|
res.json({
|
|
success: true,
|
|
message: "Container removed successfully",
|
|
});
|
|
} catch (error) {
|
|
session.activeOperations--;
|
|
|
|
const errorMsg = error instanceof Error ? error.message : "";
|
|
if (errorMsg.includes("No such container")) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: "Container not found",
|
|
code: "CONTAINER_NOT_FOUND",
|
|
});
|
|
}
|
|
|
|
if (errorMsg.includes("cannot remove a running container")) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error:
|
|
"Cannot remove a running container. Stop it first or use force.",
|
|
code: "CONTAINER_RUNNING",
|
|
});
|
|
}
|
|
|
|
sshLogger.error("Failed to remove container", error, {
|
|
operation: "remove_container",
|
|
sessionId,
|
|
containerId,
|
|
userId,
|
|
});
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: errorMsg || "Failed to remove container",
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @openapi
|
|
* /docker/containers/{sessionId}/{containerId}/logs:
|
|
* get:
|
|
* summary: Get container logs
|
|
* description: Retrieves logs for a specific container.
|
|
* tags:
|
|
* - Docker
|
|
* parameters:
|
|
* - in: path
|
|
* name: sessionId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* - in: path
|
|
* name: containerId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* - in: query
|
|
* name: tail
|
|
* schema:
|
|
* type: integer
|
|
* - in: query
|
|
* name: timestamps
|
|
* schema:
|
|
* type: boolean
|
|
* - in: query
|
|
* name: since
|
|
* schema:
|
|
* type: string
|
|
* - in: query
|
|
* name: until
|
|
* schema:
|
|
* type: string
|
|
* responses:
|
|
* 200:
|
|
* description: Container logs.
|
|
* 400:
|
|
* description: SSH session not found or not connected.
|
|
* 404:
|
|
* description: Container not found.
|
|
* 500:
|
|
* description: Failed to get container logs.
|
|
*/
|
|
app.get(
|
|
"/docker/containers/:sessionId/:containerId/logs",
|
|
async (req, res) => {
|
|
const { sessionId, containerId } = req.params;
|
|
const tail = req.query.tail ? parseInt(req.query.tail as string) : 100;
|
|
const timestamps = req.query.timestamps === "true";
|
|
const since = req.query.since as string;
|
|
const until = req.query.until as string;
|
|
const userId = getRequestUserId(req);
|
|
|
|
if (!userId) {
|
|
return res.status(401).json({ error: "Authentication required" });
|
|
}
|
|
|
|
const session = sshSessions[sessionId];
|
|
|
|
if (!session || !session.isConnected) {
|
|
return res.status(400).json({
|
|
error: "SSH session not found or not connected",
|
|
});
|
|
}
|
|
|
|
session.lastActive = Date.now();
|
|
session.activeOperations++;
|
|
|
|
try {
|
|
let command = `docker logs ${containerId}`;
|
|
|
|
if (tail && tail > 0) {
|
|
command += ` --tail ${Math.floor(tail)}`;
|
|
}
|
|
|
|
if (timestamps) {
|
|
command += " --timestamps";
|
|
}
|
|
|
|
if (since && DOCKER_TIMESTAMP_RE.test(since)) {
|
|
command += ` --since ${since}`;
|
|
}
|
|
|
|
if (until && DOCKER_TIMESTAMP_RE.test(until)) {
|
|
command += ` --until ${until}`;
|
|
}
|
|
|
|
command += " 2>&1";
|
|
|
|
const logs = await executeDockerCommand(
|
|
session,
|
|
command,
|
|
sessionId,
|
|
userId,
|
|
session.hostId,
|
|
);
|
|
|
|
session.activeOperations--;
|
|
|
|
res.json({
|
|
success: true,
|
|
logs,
|
|
});
|
|
} catch (error) {
|
|
session.activeOperations--;
|
|
|
|
const errorMsg = error instanceof Error ? error.message : "";
|
|
if (errorMsg.includes("No such container")) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: "Container not found",
|
|
code: "CONTAINER_NOT_FOUND",
|
|
});
|
|
}
|
|
|
|
sshLogger.error("Failed to get container logs", error, {
|
|
operation: "get_logs",
|
|
sessionId,
|
|
containerId,
|
|
userId,
|
|
});
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: errorMsg || "Failed to get container logs",
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @openapi
|
|
* /docker/containers/{sessionId}/{containerId}/stats:
|
|
* get:
|
|
* summary: Get container stats
|
|
* description: Retrieves stats for a specific container.
|
|
* tags:
|
|
* - Docker
|
|
* parameters:
|
|
* - in: path
|
|
* name: sessionId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* - in: path
|
|
* name: containerId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* responses:
|
|
* 200:
|
|
* description: Container stats.
|
|
* 400:
|
|
* description: SSH session not found or not connected.
|
|
* 404:
|
|
* description: Container not found.
|
|
* 500:
|
|
* description: Failed to get container stats.
|
|
*/
|
|
app.get(
|
|
"/docker/containers/:sessionId/:containerId/stats",
|
|
async (req, res) => {
|
|
const { sessionId, containerId } = req.params;
|
|
const userId = getRequestUserId(req);
|
|
|
|
if (!userId) {
|
|
return res.status(401).json({ error: "Authentication required" });
|
|
}
|
|
|
|
const session = sshSessions[sessionId];
|
|
|
|
if (!session || !session.isConnected) {
|
|
return res.status(400).json({
|
|
error: "SSH session not found or not connected",
|
|
});
|
|
}
|
|
|
|
session.lastActive = Date.now();
|
|
session.activeOperations++;
|
|
|
|
try {
|
|
const statsFormatStr = session.isWindows
|
|
? `"{\\"cpu\\":\\"{{.CPUPerc}}\\",\\"memory\\":\\"{{.MemUsage}}\\",\\"memoryPercent\\":\\"{{.MemPerc}}\\",\\"netIO\\":\\"{{.NetIO}}\\",\\"blockIO\\":\\"{{.BlockIO}}\\",\\"pids\\":\\"{{.PIDs}}\\"}"`
|
|
: `'{"cpu":"{{.CPUPerc}}","memory":"{{.MemUsage}}","memoryPercent":"{{.MemPerc}}","netIO":"{{.NetIO}}","blockIO":"{{.BlockIO}}","pids":"{{.PIDs}}"}' `;
|
|
const command = `docker stats ${containerId} --no-stream --format ${statsFormatStr}`;
|
|
|
|
const output = await executeDockerCommand(
|
|
session,
|
|
command,
|
|
sessionId,
|
|
userId,
|
|
session.hostId,
|
|
);
|
|
const rawStats = JSON.parse(output.trim());
|
|
|
|
const memoryParts = rawStats.memory.split(" / ");
|
|
const memoryUsed = memoryParts[0]?.trim() || "0B";
|
|
const memoryLimit = memoryParts[1]?.trim() || "0B";
|
|
|
|
const netIOParts = rawStats.netIO.split(" / ");
|
|
const netInput = netIOParts[0]?.trim() || "0B";
|
|
const netOutput = netIOParts[1]?.trim() || "0B";
|
|
|
|
const blockIOParts = rawStats.blockIO.split(" / ");
|
|
const blockRead = blockIOParts[0]?.trim() || "0B";
|
|
const blockWrite = blockIOParts[1]?.trim() || "0B";
|
|
|
|
const stats = {
|
|
cpu: rawStats.cpu,
|
|
memoryUsed,
|
|
memoryLimit,
|
|
memoryPercent: rawStats.memoryPercent,
|
|
netInput,
|
|
netOutput,
|
|
blockRead,
|
|
blockWrite,
|
|
pids: rawStats.pids,
|
|
};
|
|
|
|
session.activeOperations--;
|
|
|
|
res.json(stats);
|
|
} catch (error) {
|
|
session.activeOperations--;
|
|
|
|
const errorMsg = error instanceof Error ? error.message : "";
|
|
if (errorMsg.includes("No such container")) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: "Container not found",
|
|
code: "CONTAINER_NOT_FOUND",
|
|
});
|
|
}
|
|
|
|
sshLogger.error("Failed to get container stats", error, {
|
|
operation: "get_stats",
|
|
sessionId,
|
|
containerId,
|
|
userId,
|
|
});
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: errorMsg || "Failed to get container stats",
|
|
});
|
|
}
|
|
},
|
|
);
|
|
}
|