From 8a9525452c8733c3282df31c3dabfe008c5fc847 Mon Sep 17 00:00:00 2001 From: cmyers-mieweb Date: Mon, 18 May 2026 10:01:49 -0700 Subject: [PATCH] Prevent orphaned containers and duplicate hosts Add safeguards to container creation and deletion to avoid orphaned Proxmox containers and duplicate hostnames. Import Job model and check for cancelled creation jobs during provisioning; if cancelled, abort creation. After provisioning but before applying config, verify the DB record still exists and if it was deleted, destroy the orphaned Proxmox container and abort. In the containers router, enforce a unique hostname-per-site check (with transaction lock) and return 409 for API requests or flash an error for web UI to prevent duplicates. Also cancel any pending/running creation job when a container is deleted. Changes affect create-a-container/bin/create-container.js and create-a-container/routers/containers.js. --- create-a-container/bin/create-container.js | 26 +++++++++++++++++- create-a-container/routers/containers.js | 31 ++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/create-a-container/bin/create-container.js b/create-a-container/bin/create-container.js index 7b89acba..be9a9fcf 100755 --- a/create-a-container/bin/create-container.js +++ b/create-a-container/bin/create-container.js @@ -28,7 +28,7 @@ const path = require('path'); // Load models from parent directory const db = require(path.join(__dirname, '..', 'models')); -const { Container, Node, Site, Service, HTTPService, ExternalDomain, Setting } = db; +const { Container, Node, Site, Service, HTTPService, ExternalDomain, Setting, Job } = db; // Load utilities const { parseArgs } = require(path.join(__dirname, '..', 'utils', 'cli')); @@ -199,6 +199,15 @@ async function main() { console.log('Allocating VMID from Proxmox...'); const vmid = await client.nextId(); console.log(`Allocated VMID: ${vmid}`); + + // Check if our job was cancelled (e.g., container was deleted while we were starting) + if (container.creationJobId) { + const job = await Job.findByPk(container.creationJobId); + if (job && job.status === 'cancelled') { + console.error('Job was cancelled — aborting container creation.'); + process.exit(1); + } + } if (isDocker) { // Docker image: pull from OCI registry, then create container @@ -310,6 +319,21 @@ async function main() { console.log('Container configured'); } + // Verify the DB record still exists before proceeding. + // If another workflow deleted it while we were creating on Proxmox, + // destroy the orphan and abort to prevent duplicate containers. + const freshContainer = await Container.findByPk(container.id); + if (!freshContainer) { + console.error(`Container record ${container.id} was deleted during creation. Destroying orphaned Proxmox container ${vmid}...`); + try { + await client.deleteContainer(node.name, vmid, true, true); + console.log(`Orphaned Proxmox container ${vmid} destroyed successfully.`); + } catch (cleanupErr) { + console.error(`Failed to destroy orphaned container ${vmid}: ${cleanupErr.message}`); + } + process.exit(1); + } + // Apply environment variables and entrypoint // First read defaults from the image, then merge with user-specified values const defaultConfig = await client.lxcConfig(node.name, vmid); diff --git a/create-a-container/routers/containers.js b/create-a-container/routers/containers.js index 5ce773b1..eb81e596 100644 --- a/create-a-container/routers/containers.js +++ b/create-a-container/routers/containers.js @@ -378,6 +378,28 @@ router.post('/', async (req, res) => { if (!node) { throw new Error('No nodes with API access available in this site'); } + + // Check for existing container with same hostname in this site (prevent duplicates) + const existingContainer = await Container.findOne({ + where: { siteId, hostname }, + transaction: t, + lock: t.LOCK.UPDATE + }); + if (existingContainer) { + await t.rollback(); + if (isApi) { + return res.status(409).json({ + error: `Container with hostname "${hostname}" already exists in this site`, + container: { + id: existingContainer.id, + hostname: existingContainer.hostname, + status: existingContainer.status + } + }); + } + await req.flash('error', `Container "${hostname}" already exists.`); + return res.redirect(`/sites/${siteId}/containers`); + } // Create container record const container = await Container.create({ @@ -698,6 +720,15 @@ router.delete('/:id', requireAuth, async (req, res) => { const node = container.node; let dnsWarnings = []; try { + // Cancel any pending/running creation job to prevent orphaned Proxmox containers + if (container.creationJobId) { + const creationJob = await Job.findByPk(container.creationJobId); + if (creationJob && (creationJob.status === 'pending' || creationJob.status === 'running')) { + await creationJob.update({ status: 'cancelled' }); + console.log(`Cancelled creation job ${creationJob.id} for container ${container.hostname}`); + } + } + // Clean up DNS records for cross-site HTTP services const httpServices = (container.services || []) .filter(s => s.httpService?.externalDomain)