mirror of
https://github.com/Finsys/dockhand.git
synced 2026-06-17 19:09:33 +03:00
1.0.18
This commit is contained in:
+87
-25
@@ -1404,6 +1404,17 @@ export async function recreateContainerFromInspect(
|
||||
const oldContainerId = inspectData.Id;
|
||||
const wasRunning = inspectData.State?.Running;
|
||||
|
||||
// Detect shared/special network modes where network manipulation must be skipped
|
||||
const networkMode = hostConfig.NetworkMode || '';
|
||||
const isSharedNetwork = networkMode.startsWith('container:') ||
|
||||
networkMode.startsWith('service:') ||
|
||||
networkMode === 'host' ||
|
||||
networkMode === 'none';
|
||||
|
||||
if (isSharedNetwork) {
|
||||
log?.(`Shared network mode detected: ${networkMode} — skipping network manipulation`);
|
||||
}
|
||||
|
||||
// 1. Stop the container
|
||||
if (wasRunning) {
|
||||
log?.('Stopping container...');
|
||||
@@ -1419,24 +1430,27 @@ export async function recreateContainerFromInspect(
|
||||
).then(r => { if (!r.ok) throw new Error('Failed to rename old container'); });
|
||||
|
||||
// 3. Disconnect all networks from old container (frees static IPs)
|
||||
// Skip for shared network modes (container:X, host, none) — Docker manages these
|
||||
// Capture the first network for use during container creation
|
||||
let initialNetworkName: string | null = null;
|
||||
let initialNetworkConfig: any = null;
|
||||
|
||||
for (const [netName, netConfig] of Object.entries(networks)) {
|
||||
const networkId = (netConfig as any).NetworkID;
|
||||
if (networkId) {
|
||||
try {
|
||||
await disconnectContainerFromNetwork(networkId, oldContainerId, true, envId);
|
||||
} catch {
|
||||
// Best effort - network may already be disconnected
|
||||
if (!isSharedNetwork) {
|
||||
for (const [netName, netConfig] of Object.entries(networks)) {
|
||||
const networkId = (netConfig as any).NetworkID;
|
||||
if (networkId) {
|
||||
try {
|
||||
await disconnectContainerFromNetwork(networkId, oldContainerId, true, envId);
|
||||
} catch {
|
||||
// Best effort - network may already be disconnected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use first network for creation
|
||||
if (!initialNetworkName) {
|
||||
initialNetworkName = netName;
|
||||
initialNetworkConfig = netConfig;
|
||||
// Use first network for creation
|
||||
if (!initialNetworkName) {
|
||||
initialNetworkName = netName;
|
||||
initialNetworkConfig = netConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1452,10 +1466,12 @@ export async function recreateContainerFromInspect(
|
||||
).catch(() => {});
|
||||
|
||||
// Reconnect networks using full EndpointSettings from inspect
|
||||
for (const [, netConfig] of Object.entries(networks)) {
|
||||
const nc = netConfig as any;
|
||||
if (nc.NetworkID) {
|
||||
await connectContainerToNetworkRaw(nc.NetworkID, oldContainerId, nc, envId).catch(() => {});
|
||||
if (!isSharedNetwork) {
|
||||
for (const [, netConfig] of Object.entries(networks)) {
|
||||
const nc = netConfig as any;
|
||||
if (nc.NetworkID) {
|
||||
await connectContainerToNetworkRaw(nc.NetworkID, oldContainerId, nc, envId).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1475,6 +1491,48 @@ export async function recreateContainerFromInspect(
|
||||
HostConfig: hostConfig
|
||||
};
|
||||
|
||||
// container:<name> mode shares the network namespace — Docker rejects
|
||||
// networking-related fields on the dependent container since they're
|
||||
// owned by the network provider container
|
||||
if (networkMode.startsWith('container:')) {
|
||||
delete createConfig.Hostname;
|
||||
delete createConfig.Domainname;
|
||||
delete createConfig.ExposedPorts;
|
||||
delete createConfig.MacAddress;
|
||||
// HostConfig fields that conflict with container network mode
|
||||
if (createConfig.HostConfig) {
|
||||
delete createConfig.HostConfig.PortBindings;
|
||||
delete createConfig.HostConfig.PublishAllPorts;
|
||||
delete createConfig.HostConfig.DNS;
|
||||
delete createConfig.HostConfig.DNSOptions;
|
||||
delete createConfig.HostConfig.DNSSearch;
|
||||
delete createConfig.HostConfig.ExtraHosts;
|
||||
delete createConfig.HostConfig.Links;
|
||||
}
|
||||
|
||||
// Resolve container ID references to names for resilience.
|
||||
// Docker stores NetworkMode with the full container SHA ID, but if that container
|
||||
// gets recreated (new ID), the reference goes stale. Using the container name
|
||||
// instead makes the reference survive recreation.
|
||||
const containerRef = networkMode.slice('container:'.length);
|
||||
const isHexId = /^[0-9a-f]{12,64}$/.test(containerRef);
|
||||
if (isHexId) {
|
||||
try {
|
||||
const refInspect = await inspectContainer(containerRef, envId);
|
||||
// Container exists — switch from ID to name for resilience
|
||||
const refName = (refInspect as any).Name?.replace(/^\//, '');
|
||||
if (refName) {
|
||||
createConfig.HostConfig.NetworkMode = `container:${refName}`;
|
||||
log?.(`Resolved network container ID to name: ${refName}`);
|
||||
}
|
||||
} catch {
|
||||
// Container ID is stale — the referenced container was likely recreated
|
||||
// with a new ID. We can't resolve without knowing the original name.
|
||||
log?.(`WARNING: Network reference container:${containerRef.slice(0, 12)}... is stale (container not found). The container may fail to start if the referenced container was recreated.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve anonymous volumes from Mounts not in HostConfig.Binds
|
||||
const existingBinds = new Set((hostConfig.Binds || []).map((b: string) => {
|
||||
const parts = b.split(':');
|
||||
@@ -1498,8 +1556,9 @@ export async function recreateContainerFromInspect(
|
||||
|
||||
// Docker can only connect to one network at creation. Pass the first network
|
||||
// from the old container's settings to avoid getting a random bridge IP.
|
||||
// Skip for shared network modes — EndpointsConfig conflicts with container:/host/none modes.
|
||||
// Clear MacAddress for Docker API < 1.44 compatibility.
|
||||
if (initialNetworkName && initialNetworkConfig) {
|
||||
if (!isSharedNetwork && initialNetworkName && initialNetworkConfig) {
|
||||
const endpointConfig = { ...initialNetworkConfig };
|
||||
delete endpointConfig.MacAddress;
|
||||
createConfig.NetworkingConfig = {
|
||||
@@ -1529,15 +1588,18 @@ export async function recreateContainerFromInspect(
|
||||
}
|
||||
|
||||
// 6. Connect additional networks using full EndpointSettings from inspect
|
||||
for (const [netName, netConfig] of Object.entries(networks)) {
|
||||
if (netName === initialNetworkName) continue; // Already connected at creation
|
||||
// Skip for shared network modes — Docker manages networking via the parent container
|
||||
if (!isSharedNetwork) {
|
||||
for (const [netName, netConfig] of Object.entries(networks)) {
|
||||
if (netName === initialNetworkName) continue; // Already connected at creation
|
||||
|
||||
const nc = netConfig as any;
|
||||
if (nc.NetworkID) {
|
||||
try {
|
||||
await connectContainerToNetworkRaw(nc.NetworkID, newContainerId, nc, envId);
|
||||
} catch (netError: any) {
|
||||
log?.(`Warning: Failed to connect to network "${netName}": ${netError.message}`);
|
||||
const nc = netConfig as any;
|
||||
if (nc.NetworkID) {
|
||||
try {
|
||||
await connectContainerToNetworkRaw(nc.NetworkID, newContainerId, nc, envId);
|
||||
} catch (netError: any) {
|
||||
log?.(`Warning: Failed to connect to network "${netName}": ${netError.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,7 +569,7 @@ export async function runContainerUpdate(
|
||||
// =============================================================================
|
||||
|
||||
log(`Recreating container with full config passthrough...`);
|
||||
const success = await recreateContainer(containerName, envId, log);
|
||||
const success = await recreateContainer(containerName, envId, log, imageNameFromConfig);
|
||||
|
||||
if (success) {
|
||||
await updateAutoUpdateLastUpdated(containerName, envId);
|
||||
@@ -626,7 +626,8 @@ export async function runContainerUpdate(
|
||||
export async function recreateContainer(
|
||||
containerName: string,
|
||||
envId?: number,
|
||||
log?: (msg: string) => void
|
||||
log?: (msg: string) => void,
|
||||
imageNameOverride?: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const containers = await listContainers(true, envId);
|
||||
@@ -638,7 +639,7 @@ export async function recreateContainer(
|
||||
}
|
||||
|
||||
const inspectData = await inspectContainer(container.id, envId) as any;
|
||||
const imageName = inspectData.Config?.Image;
|
||||
const imageName = imageNameOverride || inspectData.Config?.Image;
|
||||
|
||||
log?.(`Recreating container: ${containerName} (image: ${imageName})`);
|
||||
|
||||
|
||||
@@ -464,7 +464,7 @@ export const POST: RequestHandler = async (event) => {
|
||||
message: `Recreating ${containerName}...`
|
||||
});
|
||||
|
||||
updateSuccess = await recreateContainer(containerName, envIdNum, logProgress);
|
||||
updateSuccess = await recreateContainer(containerName, envIdNum, logProgress, imageName);
|
||||
if (updateSuccess) {
|
||||
const updatedContainers = await listContainers(true, envIdNum);
|
||||
const updatedContainer = updatedContainers.find(c => c.name === containerName);
|
||||
|
||||
@@ -1760,17 +1760,16 @@
|
||||
{#if container.systemContainer === 'dockhand'}
|
||||
{#if hasUpdate}
|
||||
<div class="space-y-2">
|
||||
<p class="font-medium text-sm flex items-center gap-1.5">
|
||||
<p class="font-medium text-sm flex items-center gap-1.5 whitespace-nowrap">
|
||||
<CircleArrowUp class="w-4 h-4 text-amber-500" />
|
||||
Update available
|
||||
</p>
|
||||
<p class="text-muted-foreground text-xs">Update Dockhand from the About page:</p>
|
||||
<a
|
||||
href="/settings?tab=about"
|
||||
class="text-primary hover:underline text-xs flex items-center gap-1"
|
||||
class="text-primary hover:underline text-xs flex items-center gap-1 whitespace-nowrap"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Settings > About > Update now
|
||||
Settings > About
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
Reference in New Issue
Block a user