ccp: surface slug-collision as 409, not raw Prisma error

agents.routes.ts approve handler: wrap prisma.instance.create in
try/catch. When a PrismaClientKnownRequestError P2002 with target
including 'slug' is thrown, convert to a 409 AppError with a clear
message directing the admin at DELETE /api/instances/:id or the new
scripts/ccp-deregister.sh on the target host.

Before this, re-registering a previously-registered host (after the
operator tore down the underlying stack without cleaning CCP's DB
row) returned 500 with a raw Prisma error string — the operator had
to read a stack trace to understand the cause. Now the error is
self-describing and points at the fix.

Matches the pattern other CCP error paths use.

Bunker Admin
This commit is contained in:
bunker-admin 2026-04-16 13:08:23 -06:00
parent ce8c5aaf1f
commit 6504598752

View File

@ -150,23 +150,41 @@ router.post('/registrations/:id/approve', authenticate, requireRole('SUPER_ADMIN
throw new AppError(400, `Registration is ${registration.status}, not PENDING`);
}
// Create the Instance record
const instance = await prisma.instance.create({
data: {
slug: registration.slug,
name: registration.name,
domain: registration.domain,
status: InstanceStatus.STOPPED,
statusMessage: 'Remote instance registered — agent connecting',
basePath: registration.basePath,
composeProject: registration.composeProject,
portConfig: (registration.metadata as Record<string, unknown>)?.portConfig || { api: 4000, admin: 3000, postgres: 5432, nginx: 80 },
isRegistered: true,
isRemote: true,
agentUrl: registration.agentUrl,
adminEmail: (registration.metadata as Record<string, unknown>)?.adminEmail as string || 'admin@example.com',
},
});
// Create the Instance record.
// The slug is unique. If a stale Instance with the same slug exists (e.g. the
// operator tore down the underlying stack without running ccp-deregister.sh)
// surface a clean 409 instead of leaking the raw Prisma error.
let instance;
try {
instance = await prisma.instance.create({
data: {
slug: registration.slug,
name: registration.name,
domain: registration.domain,
status: InstanceStatus.STOPPED,
statusMessage: 'Remote instance registered — agent connecting',
basePath: registration.basePath,
composeProject: registration.composeProject,
portConfig: (registration.metadata as Record<string, unknown>)?.portConfig || { api: 4000, admin: 3000, postgres: 5432, nginx: 80 },
isRegistered: true,
isRemote: true,
agentUrl: registration.agentUrl,
adminEmail: (registration.metadata as Record<string, unknown>)?.adminEmail as string || 'admin@example.com',
},
});
} catch (err) {
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2002') {
const target = (err.meta?.target as string[] | undefined) ?? [];
if (target.includes('slug')) {
throw new AppError(
409,
`Slug '${registration.slug}' is already in use by another Instance. Delete the stale instance first (DELETE /api/instances/:id) or run scripts/ccp-deregister.sh from the target host.`,
'SLUG_CONFLICT'
);
}
}
throw err;
}
// Issue mTLS certificates
const certMaterials = await issueAgentCert(instance.id, registration.slug, registration.agentUrl);