daemon: use exact-name lookup for VM-create uniqueness

reserveVM's duplicate-name guard routed through Daemon.FindVM, which
falls back to prefix-matching on both ids and names when no exact
match is found. That turns the uniqueness check into a correctness
bug: a brand-new VM name can be rejected because it happens to
prefix an existing VM's id, or an existing VM's name. So `vm create
--name beta` fails when `beta-sandbox` already exists.

Swap in a dedicated store.GetVMByName that does a literal `WHERE
name = ?` lookup, and use it from reserveVM. FindVM keeps its
prefix-matching behaviour for user-facing lookup paths (`vm ssh
<partial>`, `vm stop <partial>`) where "did you mean" semantics
are the feature.

Tests:
 - TestReserveVMAllowsNameThatPrefixesExistingVM — seeds a VM whose
   id + name both start with "longname", then reserves two new VMs
   named "longname" and "longname-sandbox". Both must succeed.
   Under the old FindVM-based check, both would fail.
 - TestReserveVMRejectsExactDuplicateName — actual collisions are
   still rejected after the swap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-20 14:00:33 -03:00
parent 108f7a0600
commit eba9a553bf
No known key found for this signature in database
GPG key ID: 33112E6833C34679
3 changed files with 105 additions and 2 deletions

View file

@ -203,6 +203,20 @@ func (s *Store) GetVMByID(ctx context.Context, id string) (model.VMRecord, error
return scanVMRow(row)
}
// GetVMByName is the exact-name lookup used for creation-time
// uniqueness checks. Unlike GetVM (which matches id OR name) and
// Daemon.FindVM (which also falls back to prefix-matching), this
// returns sql.ErrNoRows for anything except a literal name hit, so
// a new VM can't be rejected just because its name prefixes an
// existing VM's id or an existing VM's name.
func (s *Store) GetVMByName(ctx context.Context, name string) (model.VMRecord, error) {
row := s.db.QueryRowContext(ctx, `
SELECT id, name, image_id, guest_ip, state, created_at, updated_at, last_touched_at,
spec_json, runtime_json, stats_json
FROM vms WHERE name = ?`, name)
return scanVMRow(row)
}
func (s *Store) ListVMs(ctx context.Context) ([]model.VMRecord, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT id, name, image_id, guest_ip, state, created_at, updated_at, last_touched_at,