console: the seeds operator doors answer without a deployment bucket (final review carry item)
handleAPISystemSeedsFloor and handleAPISystemLegacyRoutes read and wrote s.cfg.Releases without a nil check, so a console with no deployment bucket panicked in the store call (net/http recovered it per request). Each now answers its own failure sentence with a 503 when Releases is nil.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+39
−0
internal/console/apilegacyroutes.go+6 −0internal/console/operatornobucket_test.go+27 −0internal/console/seedfloor.go+6 −0
diff --git a/internal/console/apilegacyroutes.go b/internal/console/apilegacyroutes.go| index ae6cf8175..68c8d6452 100644 |
| --- a/internal/console/apilegacyroutes.go |
| +++ b/internal/console/apilegacyroutes.go |
| @@ -43,6 +43,12 @@ func (s *Server) handleAPISystemLegacyRoutes(w http.ResponseWriter, r *http.Requ |
| jsonError(w, ref.Status, ref.Msg) |
| return |
| } |
| + // A console with no deployment bucket has no assignments to read: the |
| + // handler's own sentence, not a panic (final review carry item). |
| + if s.cfg.Releases == nil { |
| + jsonError(w, http.StatusServiceUnavailable, "could not read the legacy route assignments. Try again.") |
| + return |
| + } |
| var req struct { |
| Name string `json:"name"` |
| To string `json:"to"` |
diff --git a/internal/console/operatornobucket_test.go b/internal/console/operatornobucket_test.go| new file mode 100644 |
| index 000000000..08049c540 |
| --- /dev/null |
| +++ b/internal/console/operatornobucket_test.go |
| @@ -0,0 +1,27 @@ |
| +package console |
| + |
| +import ( |
| + "net/http" |
| + "testing" |
| +) |
| + |
| +// Final review carry item: a console with no deployment bucket answers its |
| +// operator doors with the door's own failure sentence and a 503, instead of |
| +// panicking in the store read. |
| +func TestOperatorDoorsWithNoDeploymentBucket(t *testing.T) { |
| + fx := legacyFixture(t, "code-no-bucket") |
| + fx.srv.cfg.Releases = nil |
| + for _, c := range []struct { |
| + path string |
| + body map[string]any |
| + want string |
| + }{ |
| + {"/api/cli/system/seeds-floor", map[string]any{"min_release": "v0.27.0"}, "could not publish the seeds floor. Try again."}, |
| + {"/api/cli/system/legacy-routes", map[string]any{"name": "shop", "to": "cdef/shop"}, "could not read the legacy route assignments. Try again."}, |
| + } { |
| + st, out := postJSON(t, fx.ts.URL+c.path, c.body, fx.owner) |
| + if st != http.StatusServiceUnavailable || str(t, out, "error") != c.want { |
| + t.Errorf("%s = %d %v, want 503 %q", c.path, st, out, c.want) |
| + } |
| + } |
| +} |
diff --git a/internal/console/seedfloor.go b/internal/console/seedfloor.go| index a04e78a23..e41a694b3 100644 |
| --- a/internal/console/seedfloor.go |
| +++ b/internal/console/seedfloor.go |
| @@ -169,6 +169,12 @@ func (s *Server) handleAPISystemSeedsFloor(w http.ResponseWriter, r *http.Reques |
| if !ok { |
| return |
| } |
| + // A console with no deployment bucket has nowhere to publish: the |
| + // handler's own failure sentence, not a panic (final review carry item). |
| + if s.cfg.Releases == nil { |
| + jsonError(w, http.StatusServiceUnavailable, "could not publish the seeds floor. Try again.") |
| + return |
| + } |
| var req struct { |
| MinRelease string `json:"min_release"` |
| } |