fix(proxmox): make host registration idempotent on name - #137
Conversation
The deploy bundle POSTs /v1/proxmox/hosts on every scenario run, and nothing stopped a second row under an existing name: create_host never checked for a clash and proxmox_hosts.name had no constraint, so the documented "idempotent via 409" was never reachable. Each re-run added a host, and the UI picker filled up with duplicates of the same hypervisor. Delete-and-recreate was not an option for the fix: deployments.target_host_id is a FK to proxmox_hosts.id, so a new row per re-run strands every earlier deployment on a host nobody updates. POST now upserts on name and keeps the id, returning 200 instead of 201. Credentials are refreshed too, so a rotated PVE token reaches the backend on the next deploy instead of leaving it authenticating with a stale one -- there is no update route to fix it by hand. Migration 0002 collapses duplicates already in the field before adding the constraint: for each name it keeps the earliest added_at row, repoints any deployments at it, deletes the rest, and logs every collapse.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 83d279487c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| rows = conn.execute( | ||
| sa.text("SELECT id, name FROM proxmox_hosts ORDER BY name, added_at, id") | ||
| ).fetchall() |
There was a problem hiding this comment.
Preserve the newest host configuration during deduplication
When duplicate registrations contain a rotated token or updated URL/node, sorting oldest-first and retaining that entire row permanently deletes the newest configuration while repointing every deployment to the stale credentials. This can make existing deployments and health checks fail immediately after upgrade, before another deploy bundle happens to re-register the host. Keep the oldest ID if identity preservation is required, but copy the newest duplicate's mutable host fields onto it before deleting the later rows.
Useful? React with 👍 / 👎.
| existing = ( | ||
| await session.execute( | ||
| select(ProxmoxHost).where(ProxmoxHost.name == payload.name) | ||
| ) | ||
| ).scalar_one_or_none() |
There was a problem hiding this comment.
Make the name-based upsert atomic
When two scenario runs register the same previously unseen name concurrently, both requests can complete this query before either inserts; each then takes the create path, and the second commit violates uq_proxmox_host_name, producing an unhandled database error instead of the promised idempotent 200 response. Use an atomic database upsert or recover from the unique-conflict by rolling back, loading the winning row, and applying the update.
Useful? React with 👍 / 👎.
…om a lost race Two problems found in review of the previous commit. The migration kept the oldest duplicate wholesale. Duplicates accumulated one per scenario re-run, so the newest row holds the credentials in force -- and keeping the first row resurrected a token that may have been rotated away, with every deployment repointed at it. The keeper now takes the newest duplicate's api_url / node / token / bridge / overrides while keeping its own id and added_at. The earlier test asserted on ids only, which is why it did not catch this. The upsert's duplicate check and its commit are separated by an await, so two concurrent registrations of the same new name could both take the insert path -- WEB_CONCURRENCY=1 bounds this to one process but not to one task. The loser now rolls back, re-reads the winner and applies its update, matching how create_deployment already handles the same shape.
The bug
The deploy bundle POSTs
/v1/proxmox/hostson every scenario run, and nothing stopped a second row under an existing name —create_hostnever checked for a clash andproxmox_hosts.namehad no constraint. The documented "idempotent via 409" was unreachable: 409 could never be returned. Each re-run added a host, and the UI picker filled up with duplicates of the same hypervisor.Why upsert rather than 409
Delete-and-recreate is not a viable re-seed path:
deployments.target_host_idis a FK toproxmox_hosts.id, so a new row per re-run strands every earlier deployment on a host nobody updates.So POST now upserts on
nameand keeps the id, returning 200 instead of 201.added_atis deliberately untouched — it records first registration, and the migration keys on it.Credentials are part of what gets refreshed, which closes a second trap: there is no PUT/PATCH on hosts and the UI is read-only on them, so a rotated PVE token was previously unfixable short of a manual DB edit. It now reaches the backend on the next deploy.
Migration 0002
Databases in the field already carry duplicates, and the constraint cannot be added on top of them. For each name the migration keeps the earliest
added_atrow, repoints any deployments at it, deletes the rest, and logs every collapse:Verified
DID NOT RAISE IntegrityErrorand un-repointed FKs.alembic upgrade, not by inspection: a 0001-era DB seeded with threepve01duplicates plus deployments pointing at the later two, then asserted on the survivor and the repointing. Plus a no-op case on clean data.openapi.jsonregenerated for the drift gate.Pairs with
range42/range42-playbooks#142, which drops 409 from the bundle's accepted status codes — keeping it would only mask a real conflict.