mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
feat: finish the docker plugin (B15)
This commit is contained in:
1 parent
0a3f05d99d
commit
993a91c899
167 files changed
+26299
-5136
No files matched your search
@@ -798,7 +798,6 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
|
||||
tunnel_connections TEXT,
|
||||
jump_hosts TEXT,
|
||||
enable_file_manager INTEGER NOT NULL DEFAULT 1,
|
||||
enable_docker INTEGER NOT NULL DEFAULT 0,
|
||||
enable_web_ui INTEGER NOT NULL DEFAULT 0,
|
||||
show_terminal_in_sidebar INTEGER NOT NULL DEFAULT 1,
|
||||
show_file_manager_in_sidebar INTEGER NOT NULL DEFAULT 0,
|
||||
@@ -808,7 +807,6 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
|
||||
default_path TEXT,
|
||||
status_check_enabled INTEGER NOT NULL DEFAULT 1,
|
||||
status_check_interval INTEGER,
|
||||
docker_config TEXT,
|
||||
web_ui_config TEXT,
|
||||
terminal_config TEXT,
|
||||
quick_actions TEXT,
|
||||
@@ -929,8 +927,8 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
|
||||
const sshHosts =
|
||||
await createCurrentHostRepository().listDecryptedByUserId(userId);
|
||||
const insertHost = exportDb.prepare(`
|
||||
INSERT INTO ssh_data (id, user_id, connection_type, name, ip, port, username, folder, tags, pin, auth_type, force_keyboard_interactive, password, key, key_password, key_type, sudo_password, autostart_password, autostart_key, autostart_key_password, credential_id, override_credential_username, enable_terminal, enable_tunnel, tunnel_connections, jump_hosts, enable_file_manager, enable_docker, enable_web_ui, show_terminal_in_sidebar, show_file_manager_in_sidebar, show_tunnel_in_sidebar, show_docker_in_sidebar, show_server_stats_in_sidebar, default_path, status_check_enabled, status_check_interval, docker_config, web_ui_config, terminal_config, quick_actions, notes, use_socks5, socks5_host, socks5_port, socks5_username, socks5_password, socks5_proxy_chain, domain, mac_address, port_knock_sequence, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO ssh_data (id, user_id, connection_type, name, ip, port, username, folder, tags, pin, auth_type, force_keyboard_interactive, password, key, key_password, key_type, sudo_password, autostart_password, autostart_key, autostart_key_password, credential_id, override_credential_username, enable_terminal, enable_tunnel, tunnel_connections, jump_hosts, enable_file_manager, enable_web_ui, show_terminal_in_sidebar, show_file_manager_in_sidebar, show_tunnel_in_sidebar, show_docker_in_sidebar, show_server_stats_in_sidebar, default_path, status_check_enabled, status_check_interval, web_ui_config, terminal_config, quick_actions, notes, use_socks5, socks5_host, socks5_port, socks5_username, socks5_password, socks5_proxy_chain, domain, mac_address, port_knock_sequence, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
for (const decrypted of sshHosts) {
|
||||
@@ -962,7 +960,6 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
|
||||
decrypted.tunnelConnections || null,
|
||||
decrypted.jumpHosts || null,
|
||||
decrypted.enableFileManager ? 1 : 0,
|
||||
decrypted.enableDocker ? 1 : 0,
|
||||
decrypted.enableWebUi ? 1 : 0,
|
||||
decrypted.showTerminalInSidebar ? 1 : 0,
|
||||
decrypted.showFileManagerInSidebar ? 1 : 0,
|
||||
@@ -972,7 +969,6 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
|
||||
decrypted.defaultPath || null,
|
||||
decrypted.statusCheckEnabled === false ? 0 : 1,
|
||||
decrypted.statusCheckInterval ?? null,
|
||||
decrypted.dockerConfig || null,
|
||||
decrypted.webUiConfig || null,
|
||||
decrypted.terminalConfig || null,
|
||||
decrypted.quickActions || null,
|
||||
@@ -1369,7 +1365,6 @@ app.post(
|
||||
tunnelConnections: host.tunnel_connections,
|
||||
jumpHosts: host.jump_hosts,
|
||||
enableFileManager: Boolean(host.enable_file_manager),
|
||||
enableDocker: Boolean(host.enable_docker),
|
||||
showTerminalInSidebar: Boolean(host.show_terminal_in_sidebar),
|
||||
showFileManagerInSidebar: Boolean(
|
||||
host.show_file_manager_in_sidebar,
|
||||
|
||||
@@ -327,7 +327,6 @@ async function initializeCompleteDatabase(): Promise<void> {
|
||||
enable_tunnel INTEGER NOT NULL DEFAULT 1,
|
||||
tunnel_connections TEXT,
|
||||
enable_file_manager INTEGER NOT NULL DEFAULT 1,
|
||||
enable_docker INTEGER NOT NULL DEFAULT 0,
|
||||
enable_web_ui INTEGER NOT NULL DEFAULT 0,
|
||||
default_path TEXT,
|
||||
autostart_password TEXT,
|
||||
@@ -336,7 +335,6 @@ async function initializeCompleteDatabase(): Promise<void> {
|
||||
force_keyboard_interactive TEXT,
|
||||
status_check_enabled INTEGER NOT NULL DEFAULT 1,
|
||||
status_check_interval INTEGER,
|
||||
docker_config TEXT,
|
||||
web_ui_config TEXT,
|
||||
terminal_config TEXT,
|
||||
notes TEXT,
|
||||
@@ -1075,12 +1073,6 @@ const migrateSchema = () => {
|
||||
addColumnIfNotExists("ssh_data", "status_check_interval", "INTEGER");
|
||||
addColumnIfNotExists("ssh_data", "terminal_config", "TEXT");
|
||||
addColumnIfNotExists("ssh_data", "quick_actions", "TEXT");
|
||||
addColumnIfNotExists(
|
||||
"ssh_data",
|
||||
"enable_docker",
|
||||
"INTEGER NOT NULL DEFAULT 0",
|
||||
);
|
||||
addColumnIfNotExists("ssh_data", "docker_config", "TEXT");
|
||||
addColumnIfNotExists(
|
||||
"ssh_data",
|
||||
"enable_web_ui",
|
||||
|
||||
@@ -264,9 +264,6 @@ export const hosts = mysqlTable(
|
||||
.notNull()
|
||||
.default(true),
|
||||
scpLegacy: boolean("scp_legacy").notNull().default(false),
|
||||
enableDocker: boolean("enable_docker")
|
||||
.notNull()
|
||||
.default(false),
|
||||
enableWebUi: boolean("enable_web_ui")
|
||||
.notNull()
|
||||
.default(false),
|
||||
@@ -299,7 +296,6 @@ export const hosts = mysqlTable(
|
||||
.notNull()
|
||||
.default(true),
|
||||
statusCheckInterval: int("status_check_interval"),
|
||||
dockerConfig: text("docker_config"),
|
||||
webUiConfig: text("web_ui_config"),
|
||||
terminalConfig: text("terminal_config"),
|
||||
quickActions: text("quick_actions"),
|
||||
|
||||
@@ -265,9 +265,6 @@ export const hosts = pgTable(
|
||||
.notNull()
|
||||
.default(true),
|
||||
scpLegacy: boolean("scp_legacy").notNull().default(false),
|
||||
enableDocker: boolean("enable_docker")
|
||||
.notNull()
|
||||
.default(false),
|
||||
enableWebUi: boolean("enable_web_ui")
|
||||
.notNull()
|
||||
.default(false),
|
||||
@@ -300,7 +297,6 @@ export const hosts = pgTable(
|
||||
.notNull()
|
||||
.default(true),
|
||||
statusCheckInterval: integer("status_check_interval"),
|
||||
dockerConfig: text("docker_config"),
|
||||
webUiConfig: text("web_ui_config"),
|
||||
terminalConfig: text("terminal_config"),
|
||||
quickActions: text("quick_actions"),
|
||||
|
||||
@@ -257,9 +257,6 @@ export const hosts = sqliteTable(
|
||||
.notNull()
|
||||
.default(true),
|
||||
scpLegacy: integer("scp_legacy", { mode: "boolean" }).notNull().default(false),
|
||||
enableDocker: integer("enable_docker", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
enableWebUi: integer("enable_web_ui", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
@@ -292,7 +289,6 @@ export const hosts = sqliteTable(
|
||||
.notNull()
|
||||
.default(true),
|
||||
statusCheckInterval: integer("status_check_interval"),
|
||||
dockerConfig: text("docker_config"),
|
||||
webUiConfig: text("web_ui_config"),
|
||||
terminalConfig: text("terminal_config"),
|
||||
quickActions: text("quick_actions"),
|
||||
|
||||
@@ -284,8 +284,6 @@ export function registerHostBulkRoutes(
|
||||
simpleUpdates.enableTunnel = updates.enableTunnel;
|
||||
if (typeof updates.enableFileManager === "boolean")
|
||||
simpleUpdates.enableFileManager = updates.enableFileManager;
|
||||
if (typeof updates.enableDocker === "boolean")
|
||||
simpleUpdates.enableDocker = updates.enableDocker;
|
||||
if (typeof updates.enableTmuxMonitor === "boolean")
|
||||
simpleUpdates.enableTmuxMonitor = updates.enableTmuxMonitor;
|
||||
if (typeof updates.enableTerminalToolbar === "boolean")
|
||||
@@ -751,7 +749,6 @@ export function registerHostBulkRoutes(
|
||||
enableTerminal: hostData.enableTerminal !== false,
|
||||
enableTunnel: hostData.enableTunnel !== false,
|
||||
enableFileManager: hostData.enableFileManager !== false,
|
||||
enableDocker: hostData.enableDocker || false,
|
||||
enableTmuxMonitor: hostData.enableTmuxMonitor || false,
|
||||
enableTerminalToolbar: hostData.enableTerminalToolbar !== false,
|
||||
enableAiAssistant: hostData.enableAiAssistant || false,
|
||||
@@ -771,9 +768,6 @@ export function registerHostBulkRoutes(
|
||||
? JSON.stringify(hostData.quickActions)
|
||||
: null,
|
||||
...importedStatusCheck(hostData as Record<string, unknown>),
|
||||
dockerConfig: hostData.dockerConfig
|
||||
? JSON.stringify(hostData.dockerConfig)
|
||||
: null,
|
||||
terminalConfig: hostData.terminalConfig
|
||||
? JSON.stringify(hostData.terminalConfig)
|
||||
: null,
|
||||
@@ -1016,7 +1010,6 @@ export function registerHostBulkRoutes(
|
||||
enableTerminal: true,
|
||||
enableTunnel: true,
|
||||
enableFileManager: true,
|
||||
enableDocker: false,
|
||||
enableTmuxMonitor: false,
|
||||
enableTerminalToolbar: true,
|
||||
enableAiAssistant: false,
|
||||
@@ -1034,7 +1027,6 @@ export function registerHostBulkRoutes(
|
||||
quickActions: null,
|
||||
statusCheckEnabled: true,
|
||||
statusCheckInterval: null,
|
||||
dockerConfig: null,
|
||||
terminalConfig: null,
|
||||
forceKeyboardInteractive: "false",
|
||||
notes: null,
|
||||
|
||||
@@ -174,7 +174,6 @@ export type NormalizedImportedHost = Record<string, unknown> & {
|
||||
enableTerminal?: unknown;
|
||||
enableTunnel?: unknown;
|
||||
enableFileManager?: unknown;
|
||||
enableDocker?: unknown;
|
||||
enableWebUi?: unknown;
|
||||
enableProxmox?: unknown;
|
||||
enableTmuxMonitor?: unknown;
|
||||
@@ -193,7 +192,6 @@ export type NormalizedImportedHost = Record<string, unknown> & {
|
||||
quickActions?: unknown;
|
||||
statusCheckEnabled?: unknown;
|
||||
statusCheckInterval?: unknown;
|
||||
dockerConfig?: unknown;
|
||||
webUiConfig?: unknown;
|
||||
proxmoxConfig?: unknown;
|
||||
enableProxmoxStats?: unknown;
|
||||
@@ -340,7 +338,6 @@ const CONNECT_LEVEL_FIELDS = new Set([
|
||||
"enableTerminal",
|
||||
"enableTunnel",
|
||||
"enableFileManager",
|
||||
"enableDocker",
|
||||
"enableTmuxMonitor",
|
||||
"enableTerminalToolbar",
|
||||
"enableAiAssistant",
|
||||
@@ -437,7 +434,6 @@ export function transformHostResponse(
|
||||
enableTerminal: !!host.enableTerminal,
|
||||
enableTunnel: !!host.enableTunnel,
|
||||
enableFileManager: host.enableFileManager !== false,
|
||||
enableDocker: !!host.enableDocker,
|
||||
enableTmuxMonitor: !!host.enableTmuxMonitor,
|
||||
enableTerminalToolbar: host.enableTerminalToolbar !== false,
|
||||
enableAiAssistant: !!host.enableAiAssistant,
|
||||
@@ -468,9 +464,6 @@ export function transformHostResponse(
|
||||
terminalConfig: host.terminalConfig
|
||||
? JSON.parse(host.terminalConfig as string)
|
||||
: undefined,
|
||||
dockerConfig: host.dockerConfig
|
||||
? JSON.parse(host.dockerConfig as string)
|
||||
: undefined,
|
||||
forceKeyboardInteractive: host.forceKeyboardInteractive === "true",
|
||||
useWarpgate: !!host.useWarpgate,
|
||||
socks5ProxyChain: host.socks5ProxyChain
|
||||
|
||||
@@ -199,7 +199,6 @@ router.post(
|
||||
enableTunnel,
|
||||
enableFileManager,
|
||||
scpLegacy,
|
||||
enableDocker,
|
||||
enableProxmox,
|
||||
enableTmuxMonitor,
|
||||
enableTerminalToolbar,
|
||||
@@ -215,7 +214,6 @@ router.post(
|
||||
quickActions,
|
||||
statusCheckEnabled,
|
||||
statusCheckInterval,
|
||||
dockerConfig,
|
||||
proxmoxConfig,
|
||||
enableProxmoxStats,
|
||||
proxmoxStatsConfig,
|
||||
@@ -332,7 +330,6 @@ router.post(
|
||||
: null,
|
||||
enableFileManager: enableFileManager ? 1 : 0,
|
||||
scpLegacy: scpLegacy ? 1 : 0,
|
||||
enableDocker: enableDocker ? 1 : 0,
|
||||
enableTmuxMonitor: enableTmuxMonitor ? 1 : 0,
|
||||
enableTerminalToolbar: enableTerminalToolbar === false ? 0 : 1,
|
||||
enableAiAssistant: enableAiAssistant ? 1 : 0,
|
||||
@@ -344,11 +341,6 @@ router.post(
|
||||
defaultPath: defaultPath || null,
|
||||
statusCheckEnabled: statusCheckEnabled === false ? 0 : 1,
|
||||
statusCheckInterval: normalizeStatusInterval(statusCheckInterval),
|
||||
dockerConfig: dockerConfig
|
||||
? typeof dockerConfig === "string"
|
||||
? dockerConfig
|
||||
: JSON.stringify(dockerConfig)
|
||||
: null,
|
||||
terminalConfig: terminalConfig
|
||||
? typeof terminalConfig === "string"
|
||||
? terminalConfig
|
||||
@@ -764,7 +756,6 @@ router.post(
|
||||
enableTerminal: true,
|
||||
enableTunnel: false,
|
||||
enableFileManager: true,
|
||||
enableDocker: false,
|
||||
enableWebUi: false,
|
||||
enableTmuxMonitor: false,
|
||||
enableTerminalToolbar: true,
|
||||
@@ -895,7 +886,6 @@ router.put(
|
||||
enableTunnel,
|
||||
enableFileManager,
|
||||
scpLegacy,
|
||||
enableDocker,
|
||||
enableProxmox,
|
||||
enableTmuxMonitor,
|
||||
enableTerminalToolbar,
|
||||
@@ -911,7 +901,6 @@ router.put(
|
||||
quickActions,
|
||||
statusCheckEnabled,
|
||||
statusCheckInterval,
|
||||
dockerConfig,
|
||||
proxmoxConfig,
|
||||
enableProxmoxStats,
|
||||
proxmoxStatsConfig,
|
||||
@@ -1029,7 +1018,6 @@ router.put(
|
||||
: null,
|
||||
enableFileManager: enableFileManager ? 1 : 0,
|
||||
scpLegacy: scpLegacy ? 1 : 0,
|
||||
enableDocker: enableDocker ? 1 : 0,
|
||||
enableTmuxMonitor: enableTmuxMonitor ? 1 : 0,
|
||||
enableTerminalToolbar: enableTerminalToolbar === false ? 0 : 1,
|
||||
enableAiAssistant: enableAiAssistant ? 1 : 0,
|
||||
@@ -1041,11 +1029,6 @@ router.put(
|
||||
defaultPath: defaultPath || null,
|
||||
statusCheckEnabled: statusCheckEnabled === false ? 0 : 1,
|
||||
statusCheckInterval: normalizeStatusInterval(statusCheckInterval),
|
||||
dockerConfig: dockerConfig
|
||||
? typeof dockerConfig === "string"
|
||||
? dockerConfig
|
||||
: JSON.stringify(dockerConfig)
|
||||
: null,
|
||||
terminalConfig: terminalConfig
|
||||
? typeof terminalConfig === "string"
|
||||
? terminalConfig
|
||||
@@ -2099,7 +2082,6 @@ router.get(
|
||||
enableTunnel: !!resolvedHost.enableTunnel,
|
||||
enableFileManager: resolvedHost.enableFileManager !== false,
|
||||
scpLegacy: !!resolvedHost.scpLegacy,
|
||||
enableDocker: !!resolvedHost.enableDocker,
|
||||
enableWebUi: !!webEndpointSettings?.enableWebUi,
|
||||
enableProxmox: !!proxmoxSettings?.enableProxmox,
|
||||
enableProxmoxStats: !!proxmoxSettings?.enableProxmoxStats,
|
||||
@@ -2123,9 +2105,6 @@ router.get(
|
||||
? JSON.parse(resolvedHost.quickActions as string)
|
||||
: null,
|
||||
webUiConfig: webEndpointSettings?.webUiConfig ?? { endpoints: [] },
|
||||
dockerConfig: resolvedHost.dockerConfig
|
||||
? JSON.parse(resolvedHost.dockerConfig as string)
|
||||
: null,
|
||||
proxmoxConfig: proxmoxSettings?.proxmoxConfig ?? null,
|
||||
proxmoxStatsConfig: proxmoxSettings?.proxmoxStatsConfig ?? null,
|
||||
terminalConfig: resolvedHost.terminalConfig
|
||||
@@ -2260,7 +2239,6 @@ router.get(
|
||||
enableCommandHistory: resolvedHost.enableCommandHistory !== false,
|
||||
enableTunnel: !!resolvedHost.enableTunnel,
|
||||
enableFileManager: resolvedHost.enableFileManager !== false,
|
||||
enableDocker: !!resolvedHost.enableDocker,
|
||||
enableWebUi: !!webEndpointSettings?.enableWebUi,
|
||||
enableProxmox: !!proxmoxSettings?.enableProxmox,
|
||||
enableTmuxMonitor: !!resolvedHost.enableTmuxMonitor,
|
||||
@@ -2288,9 +2266,6 @@ router.get(
|
||||
webUiConfig: webEndpointSettings?.webUiConfig ?? {
|
||||
endpoints: [],
|
||||
},
|
||||
dockerConfig: resolvedHost.dockerConfig
|
||||
? JSON.parse(resolvedHost.dockerConfig as string)
|
||||
: null,
|
||||
proxmoxConfig: proxmoxSettings?.proxmoxConfig ?? null,
|
||||
terminalConfig: resolvedHost.terminalConfig
|
||||
? JSON.parse(resolvedHost.terminalConfig as string)
|
||||
|
||||
@@ -85,7 +85,6 @@ function toRecord(host: Record<string, unknown>): PluginHostRecord {
|
||||
enableTerminal: (host.enableTerminal as boolean | null) ?? null,
|
||||
enableFileManager: (host.enableFileManager as boolean | null) ?? null,
|
||||
enableTunnel: (host.enableTunnel as boolean | null) ?? null,
|
||||
enableDocker: (host.enableDocker as boolean | null) ?? null,
|
||||
createdAt: (host.createdAt as string | null) ?? null,
|
||||
updatedAt: (host.updatedAt as string | null) ?? null,
|
||||
...host,
|
||||
|
||||
@@ -393,6 +393,10 @@ async function provisionLocalDesktopUserIfNeeded(): Promise<void> {
|
||||
const { runHostMetricsSettingsMigration } =
|
||||
await import("./utils/crypto-migration/host-metrics-settings-migration.js");
|
||||
await runHostMetricsSettingsMigration();
|
||||
|
||||
const { runDockerSettingsMigration } =
|
||||
await import("./utils/crypto-migration/docker-settings-migration.js");
|
||||
await runDockerSettingsMigration();
|
||||
} catch (error) {
|
||||
systemLogger.warn("Plugin runtime failed to initialize", {
|
||||
operation: "plugin_init",
|
||||
|
||||
@@ -129,7 +129,6 @@ describe("HostRepository Proxmox sync inserts", () => {
|
||||
enableTerminal: true,
|
||||
enableFileManager: true,
|
||||
enableTunnel: true,
|
||||
enableDocker: false,
|
||||
enableSsh: true,
|
||||
enableRdp: false,
|
||||
rdpUser: null,
|
||||
@@ -148,7 +147,6 @@ describe("HostRepository Proxmox sync inserts", () => {
|
||||
tunnelConnections: "[]",
|
||||
jumpHosts: null,
|
||||
quickActions: null,
|
||||
dockerConfig: null,
|
||||
terminalConfig: null,
|
||||
forceKeyboardInteractive: "false",
|
||||
useSocks5: 0,
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* The Docker host settings migration.
|
||||
*
|
||||
* An upgrade must be lossless: every host with Docker on, and every host on
|
||||
* Podman, must come through into the docker plugin's host settings, whatever
|
||||
* shape the engine returned the flag in. Running it twice must not duplicate
|
||||
* or clobber what it moved.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
pluginRows: [] as Array<{
|
||||
pluginId: string;
|
||||
scope: string;
|
||||
scopeId: string | null;
|
||||
key: string;
|
||||
value: string | null;
|
||||
}>,
|
||||
hostRows: [] as Array<Record<string, unknown>>,
|
||||
pluginInstalled: true,
|
||||
columnsGone: false,
|
||||
}));
|
||||
|
||||
vi.mock("../../database/repositories/factory.js", () => ({
|
||||
createCurrentPluginRepository: () => ({
|
||||
findById: async (id: string) =>
|
||||
state.pluginInstalled && id === "docker" ? { id } : null,
|
||||
}),
|
||||
createCurrentPluginSettingsRepository: () => ({
|
||||
getAll: async (_pluginId: string, scope: string, scopeId: string | null) =>
|
||||
state.pluginRows.filter(
|
||||
(row) => row.scope === scope && row.scopeId === scopeId,
|
||||
),
|
||||
set: async (
|
||||
pluginId: string,
|
||||
scope: string,
|
||||
scopeId: string | null,
|
||||
key: string,
|
||||
value: string | null,
|
||||
) => {
|
||||
const existing = state.pluginRows.find(
|
||||
(row) =>
|
||||
row.scope === scope && row.scopeId === scopeId && row.key === key,
|
||||
);
|
||||
if (existing) {
|
||||
existing.value = value;
|
||||
return;
|
||||
}
|
||||
state.pluginRows.push({ pluginId, scope, scopeId, key, value });
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/crypto-migration/raw-rows.js", () => ({
|
||||
selectRows: async () => {
|
||||
if (state.columnsGone) throw new Error("no such column: enable_docker");
|
||||
return state.hostRows;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/logger.js", () => ({
|
||||
databaseLogger: { info: vi.fn(), warn: vi.fn() },
|
||||
}));
|
||||
|
||||
import {
|
||||
dockerSettingsFromRow,
|
||||
runDockerSettingsMigration,
|
||||
} from "../../utils/crypto-migration/docker-settings-migration.js";
|
||||
|
||||
const hostValues = (hostId: number) =>
|
||||
Object.fromEntries(
|
||||
state.pluginRows
|
||||
.filter((row) => row.scope === "host" && row.scopeId === String(hostId))
|
||||
.map((row) => [row.key, JSON.parse(row.value ?? "null")]),
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
state.pluginRows = [];
|
||||
state.hostRows = [];
|
||||
state.pluginInstalled = true;
|
||||
state.columnsGone = false;
|
||||
});
|
||||
|
||||
describe("dockerSettingsFromRow", () => {
|
||||
it("reads SQLite integers, real booleans and strings", () => {
|
||||
expect(
|
||||
dockerSettingsFromRow({ id: 1, enable_docker: 1, docker_config: null }),
|
||||
).toEqual({ enableDocker: true });
|
||||
expect(
|
||||
dockerSettingsFromRow({
|
||||
id: 1,
|
||||
enable_docker: true,
|
||||
docker_config: null,
|
||||
}),
|
||||
).toEqual({ enableDocker: true });
|
||||
expect(
|
||||
dockerSettingsFromRow({ id: 1, enable_docker: 0, docker_config: null }),
|
||||
).toEqual({});
|
||||
expect(
|
||||
dockerSettingsFromRow({
|
||||
id: 1,
|
||||
enable_docker: false,
|
||||
docker_config: null,
|
||||
}),
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
it("keeps a Podman runtime and ignores the default or broken config", () => {
|
||||
expect(
|
||||
dockerSettingsFromRow({
|
||||
id: 1,
|
||||
enable_docker: 1,
|
||||
docker_config: '{"runtime":"podman"}',
|
||||
}),
|
||||
).toEqual({ enableDocker: true, containerRuntime: "podman" });
|
||||
expect(
|
||||
dockerSettingsFromRow({
|
||||
id: 1,
|
||||
enable_docker: 1,
|
||||
docker_config: '{"runtime":"docker"}',
|
||||
}),
|
||||
).toEqual({ enableDocker: true });
|
||||
expect(
|
||||
dockerSettingsFromRow({
|
||||
id: 1,
|
||||
enable_docker: 1,
|
||||
docker_config: "{oops",
|
||||
}),
|
||||
).toEqual({ enableDocker: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("runDockerSettingsMigration", () => {
|
||||
it("moves every host's docker options and skips hosts with nothing set", async () => {
|
||||
state.hostRows = [
|
||||
{ id: 1, enable_docker: 1, docker_config: null },
|
||||
{ id: 2, enable_docker: 1, docker_config: '{"runtime":"podman"}' },
|
||||
{ id: 3, enable_docker: 0, docker_config: null },
|
||||
];
|
||||
|
||||
const result = await runDockerSettingsMigration();
|
||||
|
||||
expect(result).toEqual({ hostsMoved: 2, hostsSkipped: 0 });
|
||||
expect(hostValues(1)).toEqual({ enableDocker: true });
|
||||
expect(hostValues(2)).toEqual({
|
||||
enableDocker: true,
|
||||
containerRuntime: "podman",
|
||||
});
|
||||
expect(hostValues(3)).toEqual({});
|
||||
});
|
||||
|
||||
it("changes nothing when run twice", async () => {
|
||||
state.hostRows = [
|
||||
{ id: 1, enable_docker: 1, docker_config: '{"runtime":"podman"}' },
|
||||
];
|
||||
await runDockerSettingsMigration();
|
||||
const before = JSON.stringify(state.pluginRows);
|
||||
|
||||
const second = await runDockerSettingsMigration();
|
||||
|
||||
expect(second).toEqual({ hostsMoved: 0, hostsSkipped: 1 });
|
||||
expect(JSON.stringify(state.pluginRows)).toBe(before);
|
||||
});
|
||||
|
||||
it("keeps a value already saved in the plugin", async () => {
|
||||
state.pluginRows.push({
|
||||
pluginId: "docker",
|
||||
scope: "host",
|
||||
scopeId: "1",
|
||||
key: "enableDocker",
|
||||
value: "false",
|
||||
});
|
||||
state.hostRows = [{ id: 1, enable_docker: 1, docker_config: null }];
|
||||
|
||||
await runDockerSettingsMigration();
|
||||
|
||||
expect(hostValues(1)).toEqual({ enableDocker: false });
|
||||
});
|
||||
|
||||
it("does nothing before the plugin row exists or once the columns are gone", async () => {
|
||||
state.hostRows = [{ id: 1, enable_docker: 1, docker_config: null }];
|
||||
state.pluginInstalled = false;
|
||||
expect(await runDockerSettingsMigration()).toEqual({
|
||||
hostsMoved: 0,
|
||||
hostsSkipped: 0,
|
||||
});
|
||||
|
||||
state.pluginInstalled = true;
|
||||
state.columnsGone = true;
|
||||
expect(await runDockerSettingsMigration()).toEqual({
|
||||
hostsMoved: 0,
|
||||
hostsSkipped: 0,
|
||||
});
|
||||
expect(state.pluginRows).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Moves each host's Docker options out of ssh_data and into the docker
|
||||
* plugin's host settings: enable_docker becomes enableDocker and the runtime
|
||||
* in docker_config becomes containerRuntime.
|
||||
*
|
||||
* The columns stay in the database, unused: core's drizzle migrations run
|
||||
* before this does, so dropping them there would lose the data first.
|
||||
*
|
||||
* Idempotent: a host that already has any docker row is skipped, so this is
|
||||
* safe on every boot.
|
||||
*/
|
||||
|
||||
import { sql } from "drizzle-orm";
|
||||
import { databaseLogger } from "../logger.js";
|
||||
import {
|
||||
createCurrentPluginRepository,
|
||||
createCurrentPluginSettingsRepository,
|
||||
} from "../../database/repositories/factory.js";
|
||||
import { selectRows } from "./raw-rows.js";
|
||||
|
||||
const PLUGIN_ID = "docker";
|
||||
|
||||
interface LegacyHostRow {
|
||||
id: number;
|
||||
enable_docker: number | boolean | string | null;
|
||||
docker_config: string | null;
|
||||
}
|
||||
|
||||
/** The plugin's host settings for one legacy row, defaults left out. */
|
||||
export function dockerSettingsFromRow(
|
||||
row: LegacyHostRow,
|
||||
): Record<string, unknown> {
|
||||
const values: Record<string, unknown> = {};
|
||||
const flag = row.enable_docker;
|
||||
if (flag === true || flag === 1 || flag === "1" || flag === "true") {
|
||||
values.enableDocker = true;
|
||||
}
|
||||
if (row.docker_config) {
|
||||
try {
|
||||
const config = JSON.parse(row.docker_config) as { runtime?: unknown };
|
||||
if (config?.runtime === "podman") values.containerRuntime = "podman";
|
||||
} catch {
|
||||
// unreadable config means the default runtime
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
export interface DockerSettingsMigrationResult {
|
||||
hostsMoved: number;
|
||||
hostsSkipped: number;
|
||||
}
|
||||
|
||||
export async function runDockerSettingsMigration(): Promise<DockerSettingsMigrationResult> {
|
||||
const result: DockerSettingsMigrationResult = {
|
||||
hostsMoved: 0,
|
||||
hostsSkipped: 0,
|
||||
};
|
||||
|
||||
// plugin_settings has a foreign key to plugins: nothing to migrate into
|
||||
// until the plugin row exists.
|
||||
try {
|
||||
const plugin = await createCurrentPluginRepository().findById(PLUGIN_ID);
|
||||
if (!plugin) return result;
|
||||
} catch {
|
||||
return result;
|
||||
}
|
||||
|
||||
const pluginSettings = createCurrentPluginSettingsRepository();
|
||||
|
||||
let rows: LegacyHostRow[];
|
||||
try {
|
||||
// Raw SQL: schema.ts no longer declares these columns.
|
||||
rows = await selectRows<LegacyHostRow>(
|
||||
sql`SELECT id, enable_docker, docker_config FROM ssh_data`,
|
||||
);
|
||||
} catch {
|
||||
// A fresh install never had the columns.
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const row of rows) {
|
||||
const values = dockerSettingsFromRow(row);
|
||||
if (Object.keys(values).length === 0) continue;
|
||||
const scopeId = String(row.id);
|
||||
try {
|
||||
const existing = await pluginSettings.getAll(PLUGIN_ID, "host", scopeId);
|
||||
if (existing.length > 0) {
|
||||
result.hostsSkipped++;
|
||||
continue;
|
||||
}
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
await pluginSettings.set(
|
||||
PLUGIN_ID,
|
||||
"host",
|
||||
scopeId,
|
||||
key,
|
||||
JSON.stringify(value),
|
||||
);
|
||||
}
|
||||
result.hostsMoved++;
|
||||
} catch (error) {
|
||||
databaseLogger.warn("Docker host settings migration failed for a host", {
|
||||
operation: "docker_settings_migration",
|
||||
hostId: row.id,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (result.hostsMoved > 0) {
|
||||
databaseLogger.info(
|
||||
`Moved Docker settings for ${result.hostsMoved} host(s) into plugin settings`,
|
||||
{ operation: "docker_settings_migration" },
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in new issue
Block a user