From 832334655cbd64d32b7ae4c6236538ae9de2942f Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Tue, 9 Dec 2025 08:11:21 -0600 Subject: [PATCH] Fix health check port binding conflict (issue #892) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Docker health check was attempting to bind to ports already in use by the running application, causing "address already in use" errors. This fix adds a health check mode that skips network binding operations while still validating output configuration (listen addresses, paths, etc.). Changes: - Add health check mode flag in pkg/poller/outputs.go - Update prometheus and webserver DebugOutput() to skip port binding in health check mode - Maintain full configuration validation without network conflicts Fixes #892 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- pkg/poller/commands.go | 7 ++++++- pkg/poller/outputs.go | 13 +++++++++++++ pkg/promunifi/collector.go | 6 ++++++ pkg/webserver/server.go | 10 ++++++++-- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/pkg/poller/commands.go b/pkg/poller/commands.go index d5ff9712..354fb9aa 100644 --- a/pkg/poller/commands.go +++ b/pkg/poller/commands.go @@ -127,6 +127,10 @@ func (u *UnifiPoller) DebugIO() error { // It validates configuration and checks if inputs/outputs are properly configured. // Returns nil (exit 0) if healthy, error (exit 1) if unhealthy. func (u *UnifiPoller) HealthCheck() error { + // Enable health check mode to skip network binding in output validation. + SetHealthCheckMode(true) + defer SetHealthCheckMode(false) + // Silence output for health checks (Docker doesn't need verbose logs). u.Quiet = true @@ -170,7 +174,8 @@ func (u *UnifiPoller) HealthCheck() error { return fmt.Errorf("health check failed: no enabled output plugins") } - // Perform basic validation checks on enabled outputs. + // Perform configuration validation on enabled outputs. + // Network binding checks will be skipped automatically due to health check mode. for _, output := range outputs { if !output.Enabled() { continue diff --git a/pkg/poller/outputs.go b/pkg/poller/outputs.go index 55afc981..0aedc7fd 100644 --- a/pkg/poller/outputs.go +++ b/pkg/poller/outputs.go @@ -11,6 +11,8 @@ var ( outputSync sync.RWMutex // nolint: gochecknoglobals errNoOutputPlugins = fmt.Errorf("no output plugins imported") errAllOutputStopped = fmt.Errorf("all output plugins have stopped, or none enabled") + // healthCheckMode indicates when we're running in health check mode to skip network operations + healthCheckMode bool // nolint: gochecknoglobals ) // Collect is passed into output packages so they may collect metrics to output. @@ -50,6 +52,17 @@ func NewOutput(o *Output) { outputs = append(outputs, o) } +// SetHealthCheckMode enables or disables health check mode. +// When enabled, output plugins should skip network operations in DebugOutput(). +func SetHealthCheckMode(enabled bool) { + healthCheckMode = enabled +} + +// IsHealthCheckMode returns true if we're running in health check mode. +func IsHealthCheckMode() bool { + return healthCheckMode +} + // Poller returns the poller config. func (u *UnifiPoller) Poller() Poller { return *u.Config.Poller diff --git a/pkg/promunifi/collector.go b/pkg/promunifi/collector.go index 4bf17a82..6fe3b5a4 100644 --- a/pkg/promunifi/collector.go +++ b/pkg/promunifi/collector.go @@ -139,6 +139,12 @@ func (u *promUnifi) DebugOutput() (bool, error) { return false, fmt.Errorf("invalid listen address: %s (must be of the form \"IP:Port\"", u.HTTPListen) } + // Skip network binding check during health checks to avoid "address already in use" + // errors when the main application is already running and bound to the port. + if poller.IsHealthCheckMode() { + return true, nil + } + ln, err := net.Listen("tcp", u.HTTPListen) if err != nil { return false, err diff --git a/pkg/webserver/server.go b/pkg/webserver/server.go index 2d74dfee..3a60289a 100644 --- a/pkg/webserver/server.go +++ b/pkg/webserver/server.go @@ -103,11 +103,11 @@ func (s *Server) DebugOutput() (bool, error) { if s == nil { return true, nil } - + if !s.Enabled() { return true, nil } - + if s.HTMLPath == "" { return true, nil } @@ -117,6 +117,12 @@ func (s *Server) DebugOutput() (bool, error) { return false, fmt.Errorf("problem with HTML path: %w", err) } + // Skip network binding check during health checks to avoid "address already in use" + // errors when the main application is already running and bound to the port. + if poller.IsHealthCheckMode() { + return true, nil + } + // check the port ln, err := net.Listen("tcp", fmt.Sprintf(":%d", s.Port)) if err != nil {