console: the gate copy tables are functions, not package variables (Task 4 fix round 3)
mainGateCopy, mainMoveCopy and phase1MoveCopy each return a fresh struct literal, so no caller can mutate a shared table. The sentences are unchanged; TestMoveDoorGateSentences pins them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+41
−35
internal/console/apicli.go+13 −11internal/console/apiinstancemove.go+27 −23internal/console/apiinstances.go+1 −1
diff --git a/internal/console/apicli.go b/internal/console/apicli.go| index dfc352eb8..863d199e8 100644 |
| --- a/internal/console/apicli.go |
| +++ b/internal/console/apicli.go |
| @@ -57,7 +57,7 @@ func (s *Server) apiTrashedApp(w http.ResponseWriter, r *http.Request) (string, |
| // half resolves first, then `has` — the last gate and the only variable — |
| // says whether THIS door's subject exists. |
| func (s *Server) apiAccountAppBy(w http.ResponseWriter, r *http.Request, has func(int64, string) (bool, error)) (string, *Account, string, bool) { |
| - return s.apiAccountAppSaying(w, r, has, mainGateCopy) |
| + return s.apiAccountAppSaying(w, r, has, mainGateCopy()) |
| } |
| // apiAccountAppSaying is apiAccountAppBy with the sentences of c. |
| @@ -180,27 +180,29 @@ func (s *Server) serviceBinding(p Principal) (*Account, bool) { |
| // settle an ambiguity can never itself be ambiguous. It never widens access |
| // — a non-member's probe meets the same 404 either way. |
| func (s *Server) apiPrincipalAccount(w http.ResponseWriter, r *http.Request) (Principal, *Account, bool) { |
| - return s.apiPrincipalAccountSaying(w, r, mainGateCopy) |
| + return s.apiPrincipalAccountSaying(w, r, mainGateCopy()) |
| } |
| // gateCopy is what the shared CLI gates say when they refuse in a sentence |
| -// of their own. Every door speaks mainGateCopy, main's bytes; a door with |
| +// of their own. Every door speaks mainGateCopy(), main's bytes; a door with |
| // its own reviewed copy passes its own table (the Phase 1 move door, |
| -// phase1MoveCopy) and the gates' logic is unchanged. |
| +// phase1MoveCopy()) and the gates' logic is unchanged. |
| type gateCopy struct { |
| ambiguous func(seg, alts string) string // the account segment matches several of the caller's accounts |
| tooMany func(noun string) string // the instance verbs' rate limit |
| noBucket string // the console runs without a deployment bucket |
| } |
| -// mainGateCopy is main's sentences, byte for byte (the dashes are escaped |
| +// mainGateCopy returns main's sentences, byte for byte (the dashes are escaped |
| // only in the source). |
| -var mainGateCopy = gateCopy{ |
| - ambiguous: func(seg, alts string) string { |
| - return fmt.Sprintf("account %q is ambiguous \u2014 pass --account-id with one of: %s", seg, alts) |
| - }, |
| - tooMany: func(noun string) string { return "too many " + noun + " requests \u2014 wait a minute and try again" }, |
| - noBucket: "no deployment bucket on this host \u2014 the console is running without one", |
| +func mainGateCopy() gateCopy { |
| + return gateCopy{ |
| + ambiguous: func(seg, alts string) string { |
| + return fmt.Sprintf("account %q is ambiguous \u2014 pass --account-id with one of: %s", seg, alts) |
| + }, |
| + tooMany: func(noun string) string { return "too many " + noun + " requests \u2014 wait a minute and try again" }, |
| + noBucket: "no deployment bucket on this host \u2014 the console is running without one", |
| + } |
| } |
| // apiPrincipalAccountSaying is apiPrincipalAccount with the sentences of c. |
diff --git a/internal/console/apiinstancemove.go b/internal/console/apiinstancemove.go| index 80a2fe56c..cd52673fe 100644 |
| --- a/internal/console/apiinstancemove.go |
| +++ b/internal/console/apiinstancemove.go |
| @@ -37,9 +37,9 @@ import ( |
| // (spec §Out of scope), and fleet placements keep their deliberate |
| // state-does-not-follow semantics. |
| func (s *Server) moveGate(w http.ResponseWriter, r *http.Request) (acct *Account, app, host string, rec *provision.Record, ok bool) { |
| - c := mainMoveCopy |
| + c := mainMoveCopy() |
| if s.cfg.MovesEnabled { |
| - c = phase1MoveCopy |
| + c = phase1MoveCopy() |
| } |
| _, acct, app, gok := s.instanceGateSaying(w, r, "instance", c.gate) |
| if !gok { |
| @@ -86,7 +86,7 @@ func (s *Server) moveGate(w http.ResponseWriter, r *http.Request) (acct *Account |
| // moveCopy is what moveGate says: its class rails and the shared gates' |
| // sentences. With the switch off the move doors speak main's bytes |
| -// (mainMoveCopy); with it on, the Phase 1 sentences (phase1MoveCopy, copy |
| +// (mainMoveCopy()); with it on, the Phase 1 sentences (phase1MoveCopy(), copy |
| // record MV-N47 to MV-N54), which no other door ever speaks. |
| type moveCopy struct { |
| gate gateCopy |
| @@ -94,30 +94,34 @@ type moveCopy struct { |
| addr, fleet, pool, hibernate string |
| } |
| -// mainMoveCopy is main's sentences, byte for byte (the dashes are escaped |
| +// mainMoveCopy returns main's sentences, byte for byte (the dashes are escaped |
| // only in the source). |
| -var mainMoveCopy = moveCopy{ |
| - gate: mainGateCopy, |
| - kind: func(k string) string { return "only exec-backed instances move \u2014 this route is kind " + k }, |
| - addr: "an addr-backed route's process is not platform-managed \u2014 nothing to move", |
| - fleet: "fleet-placed instances keep state-does-not-follow semantics \u2014 move is for platform regions", |
| - pool: "pooled instances do not move in v1 \u2014 their data lives on the pool's own box", |
| - hibernate: "a non-hibernating instance runs outside the activator \u2014 nothing manages its data, nothing can move it", |
| +func mainMoveCopy() moveCopy { |
| + return moveCopy{ |
| + gate: mainGateCopy(), |
| + kind: func(k string) string { return "only exec-backed instances move \u2014 this route is kind " + k }, |
| + addr: "an addr-backed route's process is not platform-managed \u2014 nothing to move", |
| + fleet: "fleet-placed instances keep state-does-not-follow semantics \u2014 move is for platform regions", |
| + pool: "pooled instances do not move in v1 \u2014 their data lives on the pool's own box", |
| + hibernate: "a non-hibernating instance runs outside the activator \u2014 nothing manages its data, nothing can move it", |
| + } |
| } |
| -var phase1MoveCopy = moveCopy{ |
| - gate: gateCopy{ |
| - ambiguous: func(seg, alts string) string { |
| - return fmt.Sprintf("account %q is ambiguous. pass --account-id with one of: %s", seg, alts) |
| +func phase1MoveCopy() moveCopy { |
| + return moveCopy{ |
| + gate: gateCopy{ |
| + ambiguous: func(seg, alts string) string { |
| + return fmt.Sprintf("account %q is ambiguous. pass --account-id with one of: %s", seg, alts) |
| + }, |
| + tooMany: func(noun string) string { return "too many " + noun + " requests. wait a minute and try again" }, |
| + noBucket: "this console has no deployment bucket, so it cannot answer this", |
| }, |
| - tooMany: func(noun string) string { return "too many " + noun + " requests. wait a minute and try again" }, |
| - noBucket: "this console has no deployment bucket, so it cannot answer this", |
| - }, |
| - kind: func(k string) string { return "only exec-backed instances move. this route is kind " + k }, |
| - addr: "an addr-backed route's process is not platform-managed. there is nothing to move", |
| - fleet: "fleet-placed instances keep state-does-not-follow semantics. moves are for platform regions", |
| - pool: "pooled instances do not move yet. their data lives on the pool's own box", |
| - hibernate: "a non-hibernating instance runs outside the activator. nothing manages its data, so nothing can move it", |
| + kind: func(k string) string { return "only exec-backed instances move. this route is kind " + k }, |
| + addr: "an addr-backed route's process is not platform-managed. there is nothing to move", |
| + fleet: "fleet-placed instances keep state-does-not-follow semantics. moves are for platform regions", |
| + pool: "pooled instances do not move yet. their data lives on the pool's own box", |
| + hibernate: "a non-hibernating instance runs outside the activator. nothing manages its data, so nothing can move it", |
| + } |
| } |
| // moveView is the flat answer every move door speaks — titogo's consumer |
diff --git a/internal/console/apiinstances.go b/internal/console/apiinstances.go| index 3476b1efb..991423de3 100644 |
| --- a/internal/console/apiinstances.go |
| +++ b/internal/console/apiinstances.go |
| @@ -79,7 +79,7 @@ func (s *Server) instanceGate(w http.ResponseWriter, r *http.Request) (string, * |
| // requests" when they hit it. Only the sentence varies; nothing about the |
| // order, the key or the limiter does. |
| func (s *Server) instanceGateFor(w http.ResponseWriter, r *http.Request, noun string) (string, *Account, string, bool) { |
| - return s.instanceGateSaying(w, r, noun, mainGateCopy) |
| + return s.instanceGateSaying(w, r, noun, mainGateCopy()) |
| } |
| // instanceGateSaying is instanceGateFor with the sentences of c. |