From da40727dc09b37e4ffcbad4b69726147524ad0ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 17:25:39 +0000 Subject: [PATCH 1/3] Initial plan From 400121c7f72bd6e3f1c811e8ccd3891c602106d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 17:28:19 +0000 Subject: [PATCH 2/3] fix: API DELETE container now removes from Proxmox before DB Agent-Logs-Url: https://github.com/mieweb/opensource-server/sessions/0f37bcbb-69a5-482f-a2f3-831a012a8b4c Co-authored-by: runleveldev <44057501+runleveldev@users.noreply.github.com> --- create-a-container/routers/containers.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/create-a-container/routers/containers.js b/create-a-container/routers/containers.js index bcfa9db1..801b0be0 100644 --- a/create-a-container/routers/containers.js +++ b/create-a-container/routers/containers.js @@ -639,9 +639,28 @@ router.delete('/:id', requireAuth, async (req, res) => { if (isApiRequest(req)) { try { - const container = await Container.findByPk(containerId); + const container = await Container.findOne({ + where: { id: containerId }, + include: [{ model: Node, as: 'node' }] + }); if (!container) return res.status(404).json({ error: 'Not found' }); - await container.destroy(); // Triggers hooks/cascades + + const node = container.node; + + if (container.containerId && node && node.apiUrl && node.tokenId) { + const api = await node.api(); + try { + const config = await api.lxcConfig(node.name, container.containerId); + if (config.hostname && config.hostname !== container.hostname) { + return res.status(409).json({ error: `Hostname mismatch (DB: ${container.hostname} vs Proxmox: ${config.hostname}). Delete aborted.` }); + } + await api.deleteContainer(node.name, container.containerId, true, true); + } catch (proxmoxError) { + console.log(`Proxmox deletion skipped or failed: ${proxmoxError.message}`); + } + } + + await container.destroy(); return res.status(204).send(); } catch (err) { console.error('API DELETE Error:', err); From d080fc04a7602ff8121038e29ea252e1b0d3d24b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 17:39:34 +0000 Subject: [PATCH 3/3] refactor: extract deleteContainerFromProxmox helper, unify DELETE handler branches Agent-Logs-Url: https://github.com/mieweb/opensource-server/sessions/8dc9aa4d-8e25-4825-9a0b-feccd0a524a6 Co-authored-by: runleveldev <44057501+runleveldev@users.noreply.github.com> --- create-a-container/routers/containers.js | 138 +++++++++++------------ 1 file changed, 65 insertions(+), 73 deletions(-) diff --git a/create-a-container/routers/containers.js b/create-a-container/routers/containers.js index 801b0be0..be1d4fb2 100644 --- a/create-a-container/routers/containers.js +++ b/create-a-container/routers/containers.js @@ -632,93 +632,85 @@ router.put('/:id', requireAuth, async (req, res) => { } }); +/** + * Delete a container from Proxmox if it has an associated node with API credentials. + * Returns a hostname-mismatch error string if the safety check fails, otherwise null. + * Proxmox API errors are logged and swallowed so DB cleanup can still proceed. + * @param {object} container - Container instance with eager-loaded `node` + * @returns {Promise} Error message on hostname mismatch, null on success/skip + */ +async function deleteContainerFromProxmox(container) { + const node = container.node; + if (!container.containerId || !node || !node.apiUrl || !node.tokenId) return null; + + const api = await node.api(); + try { + const config = await api.lxcConfig(node.name, container.containerId); + if (config.hostname && config.hostname !== container.hostname) { + return `Hostname mismatch (DB: ${container.hostname} vs Proxmox: ${config.hostname}). Delete aborted.`; + } + await api.deleteContainer(node.name, container.containerId, true, true); + } catch (proxmoxError) { + console.log(`Proxmox deletion skipped or failed: ${proxmoxError.message}`); + } + return null; +} + // DELETE /sites/:siteId/containers/:id router.delete('/:id', requireAuth, async (req, res) => { const siteId = parseInt(req.params.siteId, 10); const containerId = parseInt(req.params.id, 10); + const api = isApiRequest(req); - if (isApiRequest(req)) { - try { - const container = await Container.findOne({ - where: { id: containerId }, - include: [{ model: Node, as: 'node' }] - }); - if (!container) return res.status(404).json({ error: 'Not found' }); + try { + const container = await Container.findOne({ + where: api ? { id: containerId } : { id: containerId, username: req.session.user }, + include: [ + { model: Node, as: 'node' }, + { model: Service, as: 'services', include: [{ model: HTTPService, as: 'httpService', include: [{ model: ExternalDomain, as: 'externalDomain' }] }] } + ] + }); - const node = container.node; + if (!container || (!api && (!container.node || container.node.siteId !== siteId))) { + if (api) return res.status(404).json({ error: 'Not found' }); + await req.flash('error', 'Container not found or access denied'); + return res.redirect(`/sites/${siteId}/containers`); + } - if (container.containerId && node && node.apiUrl && node.tokenId) { - const api = await node.api(); - try { - const config = await api.lxcConfig(node.name, container.containerId); - if (config.hostname && config.hostname !== container.hostname) { - return res.status(409).json({ error: `Hostname mismatch (DB: ${container.hostname} vs Proxmox: ${config.hostname}). Delete aborted.` }); - } - await api.deleteContainer(node.name, container.containerId, true, true); - } catch (proxmoxError) { - console.log(`Proxmox deletion skipped or failed: ${proxmoxError.message}`); - } + // Clean up DNS records for HTTP services with external domains + let dnsWarnings = []; + if (!api) { + const site = await Site.findByPk(siteId); + if (!site) return res.redirect('/sites'); + const httpServices = (container.services || []) + .filter(s => s.httpService?.externalDomain) + .map(s => ({ externalHostname: s.httpService.externalHostname, ExternalDomain: s.httpService.externalDomain })); + if (httpServices.length > 0) { + dnsWarnings = await manageDnsRecords(httpServices, site, 'delete'); } - - await container.destroy(); - return res.status(204).send(); - } catch (err) { - console.error('API DELETE Error:', err); - return res.status(500).json({ error: 'Internal server error' }); - } - } - - const site = await Site.findByPk(siteId); - if (!site) return res.redirect('/sites'); - - const container = await Container.findOne({ - where: { id: containerId, username: req.session.user }, - include: [ - { model: Node, as: 'node' }, - { model: Service, as: 'services', include: [{ model: HTTPService, as: 'httpService', include: [{ model: ExternalDomain, as: 'externalDomain' }] }] } - ] - }); - - if (!container || !container.node || container.node.siteId !== siteId) { - await req.flash('error', 'Container not found or access denied'); - return res.redirect(`/sites/${siteId}/containers`); - } - - const node = container.node; - let dnsWarnings = []; - try { - // Clean up DNS records for cross-site HTTP services - const httpServices = (container.services || []) - .filter(s => s.httpService?.externalDomain) - .map(s => ({ externalHostname: s.httpService.externalHostname, ExternalDomain: s.httpService.externalDomain })); - if (httpServices.length > 0) { - dnsWarnings = await manageDnsRecords(httpServices, site, 'delete'); } - if (container.containerId && node.apiUrl && node.tokenId) { - const api = await node.api(); - try { - const config = await api.lxcConfig(node.name, container.containerId); - if (config.hostname && config.hostname !== container.hostname) { - await req.flash('error', `Hostname mismatch (DB: ${container.hostname} vs Proxmox: ${config.hostname}). Delete aborted.`); - return res.redirect(`/sites/${siteId}/containers`); - } - await api.deleteContainer(node.name, container.containerId, true, true); - } catch (proxmoxError) { - console.log(`Proxmox deletion skipped or failed: ${proxmoxError.message}`); - } + const hostnameError = await deleteContainerFromProxmox(container); + if (hostnameError) { + if (api) return res.status(409).json({ error: hostnameError }); + await req.flash('error', hostnameError); + return res.redirect(`/sites/${siteId}/containers`); } + await container.destroy(); - } catch (error) { - console.error(error); - await req.flash('error', `Failed to delete: ${error.message}`); + + if (api) return res.status(204).send(); + + let msg = 'Container deleted successfully'; + for (const w of dnsWarnings) msg += ` ⚠️ ${w}`; + await req.flash('success', msg); + return res.redirect(`/sites/${siteId}/containers`); + } catch (err) { + console.error('Error deleting container:', err); + if (api) return res.status(500).json({ error: 'Internal server error' }); + await req.flash('error', `Failed to delete: ${err.message}`); return res.redirect(`/sites/${siteId}/containers`); } - - let msg = 'Container deleted successfully'; - for (const w of dnsWarnings) msg += ` ⚠️ ${w}`; - await req.flash('success', msg); - return res.redirect(`/sites/${siteId}/containers`); }); module.exports = router;