Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion create-a-container/bin/create-container.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
31 changes: 31 additions & 0 deletions create-a-container/routers/containers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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)
Expand Down
Loading