Seats keep their former names, so a rename breaks nothing (ADR 0122, phase 2)
Phase 1 made the set data; a rename still broke every reference to the old name. This adds the stable identity: a seat's canonical name changes and its old name becomes an alias that resolves to it forever. SeatNamed and the holder and display matching resolve a name (former or current) to its seat, so a manifest's claim, a held record, the git-seat lookup and the build machine's embedded set all go on working unchanged after a rename. seat_alias table (migration 0035), inventory Aliases/RenameSeat, openInventory loads them, and a 'seat rename <from> <to>' command does the whole thing — one operation, no rebuild, no re-registration, no freeze. Behaviour-neutral until a seat is renamed. Validated against postgres.
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
-- A seat keeps its former names, so a rename breaks nothing (novox/hq ADR 0122).
|
||||
--
|
||||
-- Phase 1 made the seat set data, but a rename still broke every reference to the old name — a
|
||||
-- manifest's claim, a held record, the git-seat lookup — because they name the seat and the name
|
||||
-- had changed. This is the stable identity ADR 0122 asked for, realised the simple way: a seat's
|
||||
-- canonical name changes, and its old name becomes an alias that resolves to it forever. Nothing
|
||||
-- downstream has to change — a manifest goes on claiming the old name, the build machine goes on
|
||||
-- validating it — and a rename is one operation: set the new name, remember the old.
|
||||
create table seat_alias (
|
||||
alias text primary key, -- a former name of a seat
|
||||
seat text not null -- the seat's current canonical name it resolves to
|
||||
);
|
||||
@@ -2,6 +2,7 @@ package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
@@ -54,3 +55,54 @@ func (i *Inventory) SeedSeats(ctx context.Context, defaults []catalogue.Seat) (i
|
||||
}
|
||||
return added, nil
|
||||
}
|
||||
|
||||
// Aliases is every former seat name and the seat it now resolves to (novox/hq ADR 0122).
|
||||
func (i *Inventory) Aliases(ctx context.Context) (map[string]string, error) {
|
||||
rows, err := i.store.Pool().Query(ctx, `select alias, seat from seat_alias`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
aliases := map[string]string{}
|
||||
for rows.Next() {
|
||||
var alias, seat string
|
||||
if err := rows.Scan(&alias, &seat); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aliases[alias] = seat
|
||||
}
|
||||
return aliases, rows.Err()
|
||||
}
|
||||
|
||||
// RenameSeat gives a seat a new name and keeps the old one as an alias (novox/hq ADR 0122).
|
||||
//
|
||||
// **This is the whole of a rename.** The seat's canonical name becomes `to`; `from` is remembered as
|
||||
// an alias so every reference to it — a manifest's claim, a held record, the build machine's
|
||||
// embedded set — goes on resolving to the same seat, unchanged. Nothing is rebuilt and nothing
|
||||
// freezes. Any alias that pointed to `from` is repointed to `to`, so a chain of renames does not
|
||||
// leave an older name resolving to a name that no longer exists.
|
||||
func (i *Inventory) RenameSeat(ctx context.Context, from, to string) error {
|
||||
if from == to {
|
||||
return fmt.Errorf("a seat is renamed to a different name; %q is already its name", to)
|
||||
}
|
||||
tag, err := i.store.Pool().Exec(ctx, `update seat set name = $1 where name = $2`, to, from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return fmt.Errorf("no seat named %q to rename", from)
|
||||
}
|
||||
// The old name resolves to the new one; and any name that resolved to the old one now resolves
|
||||
// to the new one, so no alias is left pointing at a name that is gone.
|
||||
if _, err := i.store.Pool().Exec(ctx,
|
||||
`insert into seat_alias (alias, seat) values ($1, $2)
|
||||
on conflict (alias) do update set seat = excluded.seat`, from, to); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := i.store.Pool().Exec(ctx,
|
||||
`update seat_alias set seat = $1 where seat = $2`, to, from); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -76,3 +76,40 @@ func TestReSeedingAnUnchangedSetIsANoOp(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A rename is one operation: the seat gets the new name, the old name becomes an alias that still
|
||||
// resolves to it (novox/hq ADR 0122).
|
||||
func TestRenameSeatKeepsTheFormerNameAsAnAlias(t *testing.T) {
|
||||
inv := ForTest(t)
|
||||
if _, err := inv.SeedSeats(t.Context(), catalogue.DefaultSeats()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RenameSeat(t.Context(), "node-packet-filter", "node-firewall"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seats, err := inv.Seats(t.Context())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
names := map[string]bool{}
|
||||
for _, s := range seats {
|
||||
names[s.Name] = true
|
||||
}
|
||||
if !names["node-firewall"] || names["node-packet-filter"] {
|
||||
t.Fatalf("the seat was not renamed in place: %v", names)
|
||||
}
|
||||
aliases, err := inv.Aliases(t.Context())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if aliases["node-packet-filter"] != "node-firewall" {
|
||||
t.Fatalf("the former name is not an alias of the new one: %v", aliases)
|
||||
}
|
||||
// Renaming what has no seat is refused; renaming to the same name is refused.
|
||||
if err := inv.RenameSeat(t.Context(), "no-such-seat", "x"); err == nil {
|
||||
t.Fatal("renaming a seat that does not exist was accepted")
|
||||
}
|
||||
if err := inv.RenameSeat(t.Context(), "node-firewall", "node-firewall"); err == nil {
|
||||
t.Fatal("renaming a seat to its own name was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user