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)