Track E: recreate, system prune, Hub search, swarm secrets/configs
CI / test (push) Successful in 10m3s

Investigate Docker/Portainer gaps and ship high-value ops: recreate
container, system prune + builder prune UI, Docker Hub search in
pull modal, Swarm secrets/configs tables. Update roadmap Track E/F.
This commit is contained in:
2026-07-10 23:48:30 -04:00
parent 869ba6a932
commit b62bcd697e
10 changed files with 490 additions and 92 deletions
+123
View File
@@ -536,6 +536,14 @@ export function registerContainerHandlers(session) {
return duplicateContainer(args, session)
})
/**
* Recreate container with the same config (Portainer-style).
* Stops/removes the old container, creates a new one with the same name, starts if it was running.
*/
session.respond('recreateContainer', async (args) => {
return recreateContainer(args, session)
})
session.respond('execContainer', async (args) => {
const container = docker.getContainer(args.id)
const exec = await container.exec({
@@ -633,6 +641,121 @@ async function duplicateContainer(args, session) {
return { success: true, message: `Container '${name}' duplicated and started successfully.` }
}
/**
* Build dockerode create options from an inspect result.
* @param {object} inspect
* @param {string} name
*/
function createOptsFromInspect(inspect, name) {
const config = { ...(inspect.Config || {}) }
// Runtime-only / identity fields must not be passed to create
for (const key of [
'Hostname',
'Domainname',
'Image',
'AttachStdin',
'AttachStdout',
'AttachStderr',
'Tty',
'OpenStdin',
'StdinOnce',
]) {
// keep these intentionally from Config below
void key
}
const hostConfig = { ...(inspect.HostConfig || {}) }
// Drop empty/null host config noise that can break recreate
for (const [k, v] of Object.entries(hostConfig)) {
if (v === null || v === undefined || v === '' || (Array.isArray(v) && v.length === 0)) {
delete hostConfig[k]
}
}
const networks = inspect.NetworkSettings?.Networks || {}
const endpoints = {}
for (const [netName, conf] of Object.entries(networks)) {
if (!conf || netName === 'host' || netName === 'none') continue
endpoints[netName] = {
Aliases: conf.Aliases || undefined,
IPAMConfig: conf.IPAMConfig || undefined,
Links: conf.Links || undefined,
NetworkID: conf.NetworkID || undefined,
}
}
return {
name,
Image: config.Image,
Env: config.Env,
Cmd: config.Cmd,
Entrypoint: config.Entrypoint,
Labels: config.Labels,
WorkingDir: config.WorkingDir,
User: config.User,
Hostname: config.Hostname,
Domainname: config.Domainname,
Tty: config.Tty,
OpenStdin: config.OpenStdin,
StdinOnce: config.StdinOnce,
ExposedPorts: config.ExposedPorts,
Volumes: config.Volumes,
StopSignal: config.StopSignal,
StopTimeout: config.StopTimeout,
Healthcheck: config.Healthcheck,
HostConfig: hostConfig,
NetworkingConfig:
Object.keys(endpoints).length > 0 ? { EndpointsConfig: endpoints } : undefined,
}
}
/**
* @param {{ id: string, start?: boolean, removeVolumes?: boolean, timeout?: number }} args
*/
async function recreateContainer(args, session) {
const id = args.id
if (!id) throw Object.assign(new Error('id required'), { code: 'INVALID_ARGS' })
const container = docker.getContainer(id)
const inspect = await container.inspect()
const name = String(inspect.Name || '').replace(/^\//, '') || id.slice(0, 12)
const wasRunning = Boolean(inspect.State?.Running)
const shouldStart = args.start !== false && (args.start === true || wasRunning)
session._cleanupLogsForContainer?.(id)
try {
if (wasRunning) {
await container.stop({ t: Number(args.timeout) >= 0 ? Number(args.timeout) : 10 })
}
} catch {
// already stopped
}
await container.remove({
force: true,
v: Boolean(args.removeVolumes || args.v),
})
const createOpts = createOptsFromInspect(inspect, name)
const created = await docker.createContainer(createOpts)
if (shouldStart) {
await created.start()
}
await broadcastContainers()
return {
success: true,
message: shouldStart
? `Container '${name}' recreated and started`
: `Container '${name}' recreated`,
id: created.id,
name,
started: shouldStart,
}
}
export async function broadcastContainers() {
try {
const containers = await docker.listContainers({ all: true })