| index 2f1392633..d63ff66d6 100644 |
| --- a/internal/console/server.go |
| +++ b/internal/console/server.go |
| @@ -749,16 +749,21 @@ type channelDeleter interface { |
| Delete(ctx context.Context, key string) error |
| } |
| |
| -// channelHosts lists the registry hosts whose routes follow app's channel. |
| -// Read-only, best-effort: a registry hiccup reads as "no hosts". |
| -func (s *Server) channelHosts(app, channel string) []string { |
| +// channelHosts lists the registry hosts whose routes follow app's channel, |
| +// and reports whether the registry could be read at all. The two call sites |
| +// deliberately treat that error differently: handleNewCanary's use is |
| +// cosmetic (which URL to show in a success flash) and downgrades an error to |
| +// "no hosts", but handleRetireCanary's use is load-bearing — a canary with |
| +// followers must never be deleted — so it must fail CLOSED on a registry |
| +// hiccup rather than silently reading the error as "safe to retire". |
| +func (s *Server) channelHosts(app, channel string) ([]string, error) { |
| reg, err := openRegistry(s.cfg.Registry) |
| if err != nil { |
| - return nil |
| + return nil, err |
| } |
| routes, err := reg.List() |
| if err != nil { |
| - return nil |
| + return nil, err |
| } |
| var out []string |
| for _, rt := range routes { |
| @@ -766,7 +771,7 @@ func (s *Server) channelHosts(app, channel string) []string { |
| out = append(out, rt.Host) |
| } |
| } |
| - return out |
| + return out, nil |
| } |
| |
| // handleNewCanary creates a canary: a brand-new canary/<name> channel |
| @@ -822,7 +827,10 @@ func (s *Server) handleNewCanary(w http.ResponseWriter, r *http.Request) { |
| return |
| } |
| s.audit(app, addr, "canary-create", "created canary "+name+" at "+version) |
| - if hosts := s.channelHosts(app, channel); len(hosts) > 0 { |
| + // Best-effort here: which URL to show in the success flash is cosmetic, |
| + // so a registry hiccup just falls back to the generic "attach one" copy |
| + // rather than failing the create that already succeeded. |
| + if hosts, _ := s.channelHosts(app, channel); len(hosts) > 0 { |
| back("ok", "Created canary "+name+" — live at https://"+hosts[0]+" serving "+version+" (canaries never bake).") |
| return |
| } |
| @@ -856,8 +864,16 @@ func (s *Server) handleRetireCanary(w http.ResponseWriter, r *http.Request) { |
| } |
| channel := "canary/" + name |
| // A canary with followers is load-bearing: retiring its pointer would |
| - // strand every host on it. |
| - if hosts := s.channelHosts(app, channel); len(hosts) > 0 { |
| + // strand every host on it. Fail CLOSED on a registry error — an "I |
| + // couldn't check" must never be read as "nobody follows this channel". |
| + hosts, err := s.channelHosts(app, channel) |
| + if err != nil { |
| + log.Printf("console: retire %s %s: check followers: %v", app, channel, err) |
| + s.renderMessage(w, r, http.StatusInternalServerError, "Error", |
| + "Could not verify no hosts follow this channel — try again.") |
| + return |
| + } |
| + if len(hosts) > 0 { |
| back("err", "Canary "+name+" still has a host ("+hosts[0]+") following it — remove or repoint that route first.") |
| return |
| } |