1.0.17
This commit is contained in:
1 parent
cb8be12f1a
commit
133c9f1e8f
12 files changed
+323
-554
No files matched your search
@@ -1338,6 +1338,195 @@ export async function createContainer(options: CreateContainerOptions, envId?: n
|
||||
return { id: result.Id, start: () => startContainer(result.Id, envId) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Recreate a container using full Config/HostConfig passthrough from inspect data.
|
||||
* Passes Config and HostConfig directly from inspect to create, only changing
|
||||
* the image. No field mapping or stripping.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Stop container
|
||||
* 2. Rename to name-old (frees the name for the new container)
|
||||
* 3. Disconnect all networks (frees static IPs)
|
||||
* 4. Create new container with original name, one network
|
||||
* 5. Connect additional networks
|
||||
* 6. Start new container
|
||||
* 7. Remove old container
|
||||
*
|
||||
* On failure: rollback (rename old back, reconnect networks, restart old)
|
||||
*/
|
||||
export async function recreateContainerFromInspect(
|
||||
inspectData: any,
|
||||
newImage: string,
|
||||
envId?: number | null,
|
||||
log?: (msg: string) => void
|
||||
): Promise<{ Id: string }> {
|
||||
const config = inspectData.Config || {};
|
||||
const hostConfig = inspectData.HostConfig || {};
|
||||
const networks: Record<string, any> = inspectData.NetworkSettings?.Networks || {};
|
||||
const name = inspectData.Name?.replace(/^\//, '') || '';
|
||||
const oldContainerId = inspectData.Id;
|
||||
const wasRunning = inspectData.State?.Running;
|
||||
|
||||
// 1. Stop the container
|
||||
if (wasRunning) {
|
||||
log?.('Stopping container...');
|
||||
await stopContainer(oldContainerId, envId);
|
||||
}
|
||||
|
||||
// 2. Rename old container to free the name
|
||||
log?.('Renaming old container...');
|
||||
await dockerFetch(
|
||||
`/containers/${oldContainerId}/rename?name=${encodeURIComponent(name + '-old')}`,
|
||||
{ method: 'POST' },
|
||||
envId
|
||||
).then(r => { if (!r.ok) throw new Error('Failed to rename old container'); });
|
||||
|
||||
// 3. Disconnect all networks from old container (frees static IPs)
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// Use first network for creation
|
||||
if (!initialNetworkName) {
|
||||
initialNetworkName = netName;
|
||||
initialNetworkConfig = netConfig;
|
||||
}
|
||||
}
|
||||
|
||||
// Rollback helper: restore old container on failure
|
||||
const rollback = async () => {
|
||||
try {
|
||||
log?.('Rolling back: restoring old container...');
|
||||
// Rename back
|
||||
await dockerFetch(
|
||||
`/containers/${oldContainerId}/rename?name=${encodeURIComponent(name)}`,
|
||||
{ method: 'POST' },
|
||||
envId
|
||||
).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(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// Restart
|
||||
if (wasRunning) {
|
||||
await startContainer(oldContainerId, envId).catch(() => {});
|
||||
}
|
||||
} catch {
|
||||
log?.('Rollback failed');
|
||||
}
|
||||
};
|
||||
|
||||
// 4. Build create config - pass Config and HostConfig directly from inspect
|
||||
const createConfig: any = {
|
||||
...config,
|
||||
Image: newImage,
|
||||
HostConfig: hostConfig
|
||||
};
|
||||
|
||||
// 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (additionalBinds.length > 0) {
|
||||
createConfig.HostConfig = {
|
||||
...hostConfig,
|
||||
Binds: [...(hostConfig.Binds || []), ...additionalBinds]
|
||||
};
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Clear MacAddress for Docker API < 1.44 compatibility.
|
||||
if (initialNetworkName && initialNetworkConfig) {
|
||||
const endpointConfig = { ...initialNetworkConfig };
|
||||
delete endpointConfig.MacAddress;
|
||||
createConfig.NetworkingConfig = {
|
||||
EndpointsConfig: {
|
||||
[initialNetworkName]: endpointConfig
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 5. Create new container
|
||||
log?.('Creating new container...');
|
||||
let newContainerId: string;
|
||||
try {
|
||||
const result = await dockerJsonRequest<{ Id: string }>(
|
||||
`/containers/create?name=${encodeURIComponent(name)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(createConfig)
|
||||
},
|
||||
envId
|
||||
);
|
||||
newContainerId = result.Id;
|
||||
} catch (createError: any) {
|
||||
log?.(`Create failed: ${createError.message}`);
|
||||
await rollback();
|
||||
throw createError;
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Start new container
|
||||
if (wasRunning) {
|
||||
log?.('Starting new container...');
|
||||
try {
|
||||
await startContainer(newContainerId, envId);
|
||||
} catch (startError: any) {
|
||||
log?.(`Start failed: ${startError.message}, rolling back...`);
|
||||
// Remove failed new container
|
||||
await removeContainer(newContainerId, true, envId).catch(() => {});
|
||||
await rollback();
|
||||
throw startError;
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Remove old container (best effort)
|
||||
log?.('Removing old container...');
|
||||
await removeContainer(oldContainerId, true, envId).catch(() => {});
|
||||
|
||||
log?.('Container recreated successfully');
|
||||
return { Id: newContainerId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all container options from Docker inspect data.
|
||||
* This preserves ALL container settings for recreation.
|
||||
@@ -2767,6 +2956,37 @@ export async function connectContainerToNetwork(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect a container to a network using a raw EndpointSettings object from inspect data.
|
||||
* Passes the full EndpointSettings as-is, preserving all fields (Links, DriverOpts,
|
||||
* IPAMConfig.LinkLocalIPs, MacAddress, etc.) without manual field extraction.
|
||||
*/
|
||||
export async function connectContainerToNetworkRaw(
|
||||
networkId: string,
|
||||
containerId: string,
|
||||
endpointSettings: any,
|
||||
envId?: number | null
|
||||
): Promise<void> {
|
||||
const body: any = {
|
||||
Container: containerId,
|
||||
EndpointConfig: endpointSettings
|
||||
};
|
||||
|
||||
const response = await dockerFetch(
|
||||
`/networks/${networkId}/connect`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
},
|
||||
envId
|
||||
);
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
throw new Error(data.message || 'Failed to connect container to network');
|
||||
}
|
||||
}
|
||||
|
||||
export async function disconnectContainerFromNetwork(
|
||||
networkId: string,
|
||||
containerId: string,
|
||||
|
||||
Reference in new issue
Block a user