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, ResourceRequest } = db;
const { Container, Node, Site, Service, HTTPService, ExternalDomain, Setting, ResourceRequest, Job } = db;

// Load utilities
const { parseArgs } = require(path.join(__dirname, '..', 'utils', 'cli'));
Expand Down Expand Up @@ -230,6 +230,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);
}
Comment on lines +237 to +240

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this would probably completely rewrite this PR but could we instead have job-runner watch for cancelled jobs and send SIGTERM to them? Then this job would have a SIGTERM handler to rollback it's changes before exitting.

}

if (isDocker) {
// Docker image: pull from OCI registry, then create container
Expand Down Expand Up @@ -341,6 +350,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);
}

// Snapshot the template's env/entrypoint onto the container record now, as
// if the user had supplied them (user-supplied values still win). Templates
// are mutable Docker refs we can't re-query on a later reconfigure, so we
Expand Down
18 changes: 18 additions & 0 deletions create-a-container/routers/api/v1/containers.js
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,18 @@ router.post(
} = req.body || {};

if (!hostname || !hostname.trim()) throw new ApiError(400, 'invalid_request', 'hostname is required');
const existingContainer = await Container.findOne({
where: { siteId: site.id, hostname },
transaction: t,
lock: t.LOCK.UPDATE,
});
if (existingContainer) {
throw new ApiError(
409,
'duplicate_hostname',
`Container with hostname "${hostname}" already exists in this site`,
);
}
Comment on lines +416 to +427

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Container should have a unique condition in the database on siteId + hostname already. The create statement on line 466 below should throw in this case. This preflight check adds an extra DB roundtrip for something that should be caught there.


// Contract: `collaborators`, when present, is an array of usernames.
// Validated here so the insert below can trust its shape.
Expand Down Expand Up @@ -748,6 +760,12 @@ router.delete(
);
const node = container.node;
let dnsWarnings = [];
if (container.creationJobId) {
const creationJob = await Job.findByPk(container.creationJobId);
if (creationJob && (creationJob.status === 'pending' || creationJob.status === 'running')) {
await creationJob.update({ status: 'cancelled' });
}
}
const httpServices = (container.services || [])
.filter((s) => s.httpService?.externalDomain)
.map((s) => ({
Expand Down