Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions gearbox/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,12 @@ func main() {
authAdapter.SetEncryptor(encryptor)
eventsAdapter := services.NewEventsAdapter(eventHub)
serverAdapter := services.NewServerAdapter(db, encryptor, servers, logger)
// Share the handler's probe-table cache with the adapter so gear
// plugins (HAProxy, Logs, Services, …) can filter their box list by
// what the agent actually advertises — see issue #112. Without this,
// the HAProxy dashboard fires /htmx/{box}/stats|metrics polls at every
// enabled box and gets 503s back from agents that don't run HAProxy.
serverAdapter.SetCapabilitiesCache(h.CapabilitiesCache())

gearDeps := gear.Dependencies{
DB: db.GetDB(), // Get the underlying *sql.DB
Expand Down
10 changes: 10 additions & 0 deletions gearbox/internal/framework/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,16 @@ func NewHandler(
// explicitly via Handler.invalidateBoxCapabilities for faster refresh.
const capabilityCacheTTL = 5 * time.Minute

// CapabilitiesCache exposes the dashboard's shared probe-table cache so
// other framework components (notably the ServerAdapter used by gear
// plugins) can answer "is gear X available on box Y?" without each
// holding its own cache. Returns the same *agent.CapabilitiesCache that
// the handler uses for filterGearsByAgentCapabilities, so callers see
// consistent capability data across the dashboard.
func (h *Handler) CapabilitiesCache() *agent.CapabilitiesCache {
return h.capabilities
}

// getBoxCapabilities returns the cached capabilities for boxID, or fetches
// fresh ones if missing/stale. Returns (nil, false) when the box isn't
// configured or doesn't use the agent API. Errors are logged at debug —
Expand Down
65 changes: 65 additions & 0 deletions gearbox/internal/framework/services/server_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package services
import (
"log/slog"

"github.com/sarg3nt/gearbox/internal/framework/agent"
"github.com/sarg3nt/gearbox/internal/framework/services/crypto"
"github.com/sarg3nt/gearbox/internal/framework/database"
"github.com/sarg3nt/gearbox/internal/framework/gear"
Expand All @@ -16,6 +17,14 @@ type ServerAdapter struct {
encryptor *crypto.Encryptor
fallback []models.BoxConfig
logger *slog.Logger

// capabilities is an optional probe-table cache used by
// GetEnabledServersWithGearAvailable to filter boxes by which agent
// gears probed Available. Nil-safe: if unset, capability-aware
// filtering falls through to the full enabled-server list (fail-open),
// which preserves current behavior for any caller that hasn't been
// migrated yet.
capabilities *agent.CapabilitiesCache
}

// NewServerAdapter creates a new ServerAdapter.
Expand Down Expand Up @@ -93,6 +102,62 @@ func (a *ServerAdapter) fallbackServers() []gear.ServerConfig {
return servers
}

// SetCapabilitiesCache wires the dashboard's probe-table cache into the
// adapter so capability-aware methods (GetEnabledServersWithGearAvailable)
// can decide which boxes should appear on a gear-specific page. Optional:
// callers that don't need capability filtering can skip this and the
// adapter degrades to returning the full enabled-server set.
func (a *ServerAdapter) SetCapabilitiesCache(c *agent.CapabilitiesCache) {
a.capabilities = c
}

// GetEnabledServersWithGearAvailable returns the subset of enabled servers
// whose probe table reports the named **agent** gear as Available. Used by
// the HAProxy / Logs / Services / OS-Updates dashboard pages to skip
// rendering tiles and tabs for boxes the gear physically can't run on
// (e.g. an agent in a distroless container has no haproxy binary, so
// rendering its HAProxy stats tile produces a 503 storm — issue #112).
//
// The gearName argument is the **agent-side** gear name, not the
// dashboard gear name (see dashboardGearToAgentGear in handler/gears.go
// for the mapping). Pass "haproxy", "logs", "services", "metrics",
// "updates", "certificates", or "traffic" as appropriate.
Comment thread
sarg3nt marked this conversation as resolved.
Outdated
//
// Fail-open contract: a box whose capabilities aren't reachable (agent
// down, no API key, cache miss + fetch failure) is **included** in the
// result. This matches filterGearsByAgentCapabilities so a transient
// agent outage doesn't make gears disappear from the UI. If the
// capability cache hasn't been wired in (SetCapabilitiesCache wasn't
// called), every enabled server is returned — same fail-open posture.
//
// The result is in the same order as GetEnabledServersAsModels.
func (a *ServerAdapter) GetEnabledServersWithGearAvailable(gearName string) []models.BoxConfig {
all := a.GetEnabledServersAsModels()
if a.capabilities == nil || gearName == "" {
return all
}
out := make([]models.BoxConfig, 0, len(all))
for _, srv := range all {
caps, err := a.capabilities.Get(srv.ID, srv.AgentURL, srv.APIKey)
if err != nil || caps == nil {
// Fail-open: include the box if we can't see its probe table.
out = append(out, srv)
continue
}
entry, present := caps.Entry(gearName)
if !present {
// Agent didn't report this gear at all (older agent that
// pre-dates it). Fail-open — keep the box.
out = append(out, srv)
continue
}
if entry.IsAvailable() {
out = append(out, srv)
}
}
return out
Comment thread
sarg3nt marked this conversation as resolved.
}

// GetEnabledServersAsModels returns servers as models.BoxConfig.
// This is a helper for plugins that need to use templates expecting models.BoxConfig.
func (a *ServerAdapter) GetEnabledServersAsModels() []models.BoxConfig {
Expand Down
20 changes: 15 additions & 5 deletions gearbox/internal/gears/haproxy/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@ func NewHandlers(deps gear.Dependencies) *Handlers {

// OverviewPage serves the HAProxy overview page.
// If no servers are configured, redirects to the servers settings page.
//
// Only servers whose agent probe table reports the haproxy gear as
// available are rendered — boxes without HAProxy (e.g. a container-mode
// agent on a TrueNAS host) would otherwise show empty 503-storming
// stat tiles. See issue #112.
func (h *Handlers) OverviewPage(w http.ResponseWriter, r *http.Request) {
servers := h.getServers()
servers := h.getHAProxyServers()

// If no servers configured, redirect to servers settings page
if len(servers) == 0 {
Expand All @@ -36,8 +41,10 @@ func (h *Handlers) OverviewPage(w http.ResponseWriter, r *http.Request) {
}

// StatusGridPage serves the status grid page.
//
// Filtered to HAProxy-capable boxes for the same reason as OverviewPage.
func (h *Handlers) StatusGridPage(w http.ResponseWriter, r *http.Request) {
servers := h.getServers()
servers := h.getHAProxyServers()

if len(servers) == 0 {
http.Redirect(w, r, "/settings/boxes", http.StatusSeeOther)
Expand All @@ -48,14 +55,17 @@ func (h *Handlers) StatusGridPage(w http.ResponseWriter, r *http.Request) {
pages.StatusGrid(user, servers).Render(r.Context(), w) //nolint:errcheck
}

// getServers returns the list of enabled servers.
func (h *Handlers) getServers() []models.BoxConfig {
// getHAProxyServers returns the list of enabled servers whose agent
// reports the haproxy gear available. Fail-open: boxes whose capabilities
// can't be fetched are still included so a transient agent outage doesn't
// hide the page entirely.
func (h *Handlers) getHAProxyServers() []models.BoxConfig {
serverAdapter, ok := h.deps.Servers.(*services.ServerAdapter)
if !ok {
h.deps.Logger.Error("failed to get server adapter - unexpected type")
return nil
}
return serverAdapter.GetEnabledServersAsModels()
return serverAdapter.GetEnabledServersWithGearAvailable("haproxy")
}

// getUser returns the user from the auth context, or a fallback user.
Expand Down
Loading