diff --git a/src/lib/server/docker.ts b/src/lib/server/docker.ts index 6232f06..b953bff 100644 --- a/src/lib/server/docker.ts +++ b/src/lib/server/docker.ts @@ -14,6 +14,7 @@ import * as tls from 'node:tls'; import { createHash } from 'node:crypto'; import type { Environment } from './db'; import { getStackEnvVarsAsRecord } from './db'; +import { getAdditionalVolumeBinds } from './mount-dedupe'; import { isSystemContainer } from './scheduler/tasks/update-utils'; import { deepDiff } from '../utils/diff.js'; @@ -1866,20 +1867,7 @@ export async function recreateContainerFromInspect( } } - // Preserve anonymous volumes from Mounts not in HostConfig.Binds - const existingBinds = new Set((hostConfig.Binds || []).map((b: string) => { - const parts = b.split(':'); - return parts.length >= 2 ? parts[1] : parts[0]; - })); - const mounts = inspectData.Mounts || []; - const additionalBinds: string[] = []; - for (const mount of mounts) { - if (mount.Type === 'volume' && mount.Name && mount.Destination) { - if (!existingBinds.has(mount.Destination)) { - additionalBinds.push(`${mount.Name}:${mount.Destination}`); - } - } - } + const additionalBinds = getAdditionalVolumeBinds(hostConfig, inspectData.Mounts || []); if (additionalBinds.length > 0) { createConfig.HostConfig = { ...hostConfig, diff --git a/src/lib/server/mount-dedupe.test.ts b/src/lib/server/mount-dedupe.test.ts new file mode 100644 index 0000000..f5dceb6 --- /dev/null +++ b/src/lib/server/mount-dedupe.test.ts @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { getAdditionalVolumeBinds } from './mount-dedupe'; + +describe('getAdditionalVolumeBinds', () => { + it('skips volume mounts when the target already exists in HostConfig.Mounts', () => { + const additionalBinds = getAdditionalVolumeBinds( + { + Binds: ['/volume1/backups:/backup'], + Mounts: [{ Target: '/data' }] + }, + [ + { Type: 'volume', Name: 'docsight_docsis_data', Destination: '/data' }, + { Type: 'bind', Name: 'ignored', Destination: '/backup' } + ] + ); + + assert.deepEqual(additionalBinds, []); + }); + + it('adds volume mounts that are missing from HostConfig', () => { + const additionalBinds = getAdditionalVolumeBinds( + { + Binds: ['/volume1/backups:/backup'], + Mounts: [] + }, + [{ Type: 'volume', Name: 'docsight_docsis_data', Destination: '/data' }] + ); + + assert.deepEqual(additionalBinds, ['docsight_docsis_data:/data']); + }); +}); diff --git a/src/lib/server/mount-dedupe.ts b/src/lib/server/mount-dedupe.ts new file mode 100644 index 0000000..5d2b372 --- /dev/null +++ b/src/lib/server/mount-dedupe.ts @@ -0,0 +1,36 @@ +type HostConfigLike = { + Binds?: string[] | null; + Mounts?: Array<{ Target?: string | null }> | null; +}; + +type InspectMountLike = { + Type?: string | null; + Name?: string | null; + Destination?: string | null; +}; + +/** Build extra bind strings for volume mounts missing from HostConfig. */ +export function getAdditionalVolumeBinds( + hostConfig: HostConfigLike, + mounts: InspectMountLike[] +): string[] { + const existingMountTargets = new Set((hostConfig.Binds || []).map((bind: string) => { + const parts = bind.split(':'); + return parts.length >= 2 ? parts[1] : parts[0]; + })); + + for (const mount of hostConfig.Mounts || []) { + if (mount?.Target) existingMountTargets.add(mount.Target); + } + + const additionalBinds: string[] = []; + for (const mount of mounts || []) { + if (mount.Type === 'volume' && mount.Name && mount.Destination) { + if (!existingMountTargets.has(mount.Destination)) { + additionalBinds.push(`${mount.Name}:${mount.Destination}`); + } + } + } + + return additionalBinds; +} diff --git a/src/routes/api/self-update/+server.ts b/src/routes/api/self-update/+server.ts index 6cf2a24..65570e5 100644 --- a/src/routes/api/self-update/+server.ts +++ b/src/routes/api/self-update/+server.ts @@ -1,5 +1,6 @@ import { json } from '@sveltejs/kit'; import { authorize } from '$lib/server/authorize'; +import { getAdditionalVolumeBinds } from '$lib/server/mount-dedupe'; import { getOwnContainerId, getHostDockerSocket, getOwnDockerHost, getOwnNetworkMode } from '$lib/server/host-path'; import { buildRegistryAuthHeader, unixSocketRequest, unixSocketStreamRequest } from '$lib/server/docker'; import type { RequestHandler } from './$types'; @@ -160,20 +161,7 @@ function buildCreateConfig(inspectData: any, newImage: string): any { // Otherwise the old container's hostname is inherited, breaking self-identification delete createConfig.Hostname; - // Preserve anonymous volumes from Mounts not in HostConfig.Binds - const existingBinds = new Set((hostConfig.Binds || []).map((b: string) => { - const parts = b.split(':'); - return parts.length >= 2 ? parts[1] : parts[0]; - })); - const mounts = inspectData.Mounts || []; - const additionalBinds: string[] = []; - for (const mount of mounts) { - if (mount.Type === 'volume' && mount.Name && mount.Destination) { - if (!existingBinds.has(mount.Destination)) { - additionalBinds.push(`${mount.Name}:${mount.Destination}`); - } - } - } + const additionalBinds = getAdditionalVolumeBinds(hostConfig, inspectData.Mounts || []); if (additionalBinds.length > 0) { createConfig.HostConfig = { ...createConfig.HostConfig,