style: Extract UI constants and refactor main view rendering (#30)

- [x] Add golangci-lint configuration with gocritic ifElseChain disabled
- [x] Rename error variables to avoid shadowing (createErr, watcherErr, watchErr, etc.)
- [x] Replace `interface{}` with `any` type alias throughout codebase
- [x] Add package-level documentation comments to all internal packages
- [x] Reorder struct fields alphabetically for consistency
- [x] Extract UI constants (terminal dimensions, column widths, colors) to constants.go
- [x] Refactor BubbleTeaUI main view rendering into smaller helper functions
- [x] Simplify nested conditionals and improve code clarity
- [x] Add `isForwardDisabled()` helper method to BubbleTeaUI
- [x] Update file permissions from 0644 to 0600 in config tests
- [x] Add `#nosec` comments and error suppression where appropriate
- [x] Improve test table struct field ordering for readability
- [x] Fix resource parsing in AddForward using strings.SplitN
- [x] Add comprehensive tests for new UI helper functions and constants
This commit is contained in:
2026-01-13 09:37:45 +00:00
committed by GitHub
parent 3d71f64901
commit 96ae1d45e0
54 changed files with 1319 additions and 730 deletions
+22 -8
View File
@@ -1,3 +1,17 @@
// Package forward provides the core port-forwarding orchestration for kportal.
// It manages the lifecycle of port-forward workers, handles hot-reload of
// configuration changes, and coordinates with the health checker and watchdog.
//
// The Manager is the central orchestrator that:
// - Creates and manages ForwardWorker instances for each configured forward
// - Handles graceful startup, shutdown, and reconfiguration
// - Coordinates with the HealthChecker for connection monitoring
// - Integrates with mDNS for hostname publishing
//
// ForwardWorker handles individual port-forward connections with:
// - Automatic retry with exponential backoff (1s → 2s → 4s → 8s → 10s max)
// - Pod restart detection and re-resolution
// - Graceful shutdown support
package forward
import (
@@ -24,19 +38,19 @@ type StatusUpdater interface {
// Manager orchestrates all port-forward workers.
// It handles starting, stopping, and hot-reloading forwards.
type Manager struct {
workers map[string]*ForwardWorker // key: forward.ID()
workersMu sync.RWMutex
statusUI StatusUpdater
healthChecker *healthcheck.Checker
clientPool *k8s.ClientPool
resolver *k8s.ResourceResolver
portForwarder *k8s.PortForwarder
portChecker *PortChecker
healthChecker *healthcheck.Checker
workers map[string]*ForwardWorker
watchdog *Watchdog
mdnsPublisher *mdns.Publisher
eventBus *events.Bus // Event bus for decoupled communication
verbose bool
eventBus *events.Bus
currentConfig *config.Config
statusUI StatusUpdater
workersMu sync.RWMutex
verbose bool
}
// NewManager creates a new forward Manager.
@@ -414,11 +428,11 @@ func (m *Manager) startWorker(fwd config.Forward) error {
// Find and notify the worker to reconnect
m.workersMu.RLock()
worker, exists := m.workers[forwardID]
staleWorker, exists := m.workers[forwardID]
m.workersMu.RUnlock()
if exists {
worker.TriggerReconnect("stale connection")
staleWorker.TriggerReconnect("stale connection")
}
}
})
+1 -1
View File
@@ -185,8 +185,8 @@ type StatusUpdate struct {
}
type ForwardAdd struct {
ID string
Fwd *config.Forward
ID string
}
type ErrorSet struct {
+4 -4
View File
@@ -99,9 +99,9 @@ func getProcessNameByPIDWindows(pid string) string {
// PortConflict represents a local port that is already in use.
type PortConflict struct {
Port int // The conflicting port number
Resource string // The forward resource that needs this port
UsedBy string // Process information (PID, command) using the port
Resource string
UsedBy string
Port int
}
// PortChecker checks port availability on the local system.
@@ -146,7 +146,7 @@ func (pc *PortChecker) isPortAvailable(port int) bool {
if err != nil {
return false
}
_ = listener.Close()
_ = listener.Close() // Best-effort cleanup; port check succeeded, Close error is non-critical
return true
}
+9 -5
View File
@@ -40,8 +40,8 @@ func TestIsValidPID(t *testing.T) {
func TestFormatProcessInfo(t *testing.T) {
tests := []struct {
name string
info processInfo
expected string
info processInfo
}{
{
name: "invalid process",
@@ -72,8 +72,8 @@ func TestFormatProcessInfo(t *testing.T) {
func TestFormatProcessList(t *testing.T) {
tests := []struct {
name string
processes []processInfo
expected string
processes []processInfo
}{
{
name: "empty list",
@@ -206,7 +206,8 @@ func TestPortChecker_CheckAvailability_EmptyPorts(t *testing.T) {
func TestPortChecker_CheckAvailability_ExcludeMap(t *testing.T) {
pc := NewPortChecker()
// Create a listener to occupy a port
// Create a listener to occupy a port on all interfaces (matching production behavior)
// #nosec G102 -- test intentionally binds to all interfaces to match production port checking
listener, err := net.Listen("tcp", ":0")
assert.NoError(t, err, "should create listener")
defer listener.Close()
@@ -231,11 +232,13 @@ func TestPortChecker_CheckAvailability_ExcludeMap(t *testing.T) {
func TestPortChecker_CheckAvailability_MultipleSkipPorts(t *testing.T) {
pc := NewPortChecker()
// Create multiple listeners
// Create multiple listeners on all interfaces (matching production behavior)
// #nosec G102 -- test intentionally binds to all interfaces to match production port checking
listener1, err := net.Listen("tcp", ":0")
assert.NoError(t, err)
defer listener1.Close()
// #nosec G102 -- test intentionally binds to all interfaces to match production port checking
listener2, err := net.Listen("tcp", ":0")
assert.NoError(t, err)
defer listener2.Close()
@@ -353,7 +356,8 @@ func TestNewPortChecker(t *testing.T) {
func TestPortChecker_PortAvailability_Integration(t *testing.T) {
pc := NewPortChecker()
// Create a listener to occupy a port
// Create a listener to occupy a port on all interfaces (matching production behavior)
// #nosec G102 -- test intentionally binds to all interfaces to match production port checking
listener, err := net.Listen("tcp", ":0")
assert.NoError(t, err, "should create listener")
defer listener.Close()
+10 -10
View File
@@ -19,25 +19,25 @@ const (
// the watchdog polls workers periodically. This reduces goroutine count and
// simplifies worker implementation.
type Watchdog struct {
mu sync.RWMutex
workers map[string]*workerState // key: forward ID
checkInterval time.Duration
hangThreshold time.Duration // How long without heartbeat before considered hung
heartbeatInterval time.Duration // How often to poll workers for heartbeat
ctx context.Context
workers map[string]*workerState
cancel context.CancelFunc
eventBus *events.Bus
wg sync.WaitGroup
eventBus *events.Bus // Optional event bus for decoupled communication
checkInterval time.Duration
hangThreshold time.Duration
heartbeatInterval time.Duration
mu sync.RWMutex
}
// workerState tracks the health of a single worker
type workerState struct {
forwardID string
lastHeartbeat time.Time
worker HeartbeatResponder
onHungCallback func(forwardID string)
forwardID string
heartbeatCount uint64
isHung bool
onHungCallback func(forwardID string)
worker HeartbeatResponder // Reference to worker for heartbeat polling
}
// HeartbeatResponder is an interface for workers that can respond to heartbeat checks
@@ -204,8 +204,8 @@ func (w *Watchdog) pollHeartbeats() {
// hungWorkerInfo stores information about a hung worker for deferred callback execution
type hungWorkerInfo struct {
forwardID string
callback func(string)
forwardID string
}
// checkWorkers checks all registered workers for hung state
+22 -22
View File
@@ -23,23 +23,23 @@ const (
// ForwardWorker manages a single port-forward connection with automatic retry.
type ForwardWorker struct {
forward config.Forward
portForwarder *k8s.PortForwarder
ctx context.Context
cancel context.CancelFunc
stopChan chan struct{}
doneChan chan struct{}
reconnectChan chan string // Channel to trigger reconnection
successChan chan struct{} // Channel to signal successful connection (for backoff reset)
verbose bool
lastPod string // Track the last pod we connected to
startTime time.Time
statusUI StatusUpdater
healthChecker *healthcheck.Checker
ctx context.Context
reconnectChan chan string
httpProxy *httplog.Proxy
watchdog *Watchdog
startTime time.Time // Track when the worker started
forwardCancel context.CancelFunc // Cancel function for current forward attempt
forwardCancelMu sync.Mutex // Protects forwardCancel
httpProxy *httplog.Proxy // HTTP logging proxy (nil if not enabled)
cancel context.CancelFunc
doneChan chan struct{}
portForwarder *k8s.PortForwarder
successChan chan struct{}
healthChecker *healthcheck.Checker
forwardCancel context.CancelFunc
stopChan chan struct{}
lastPod string
forward config.Forward
forwardCancelMu sync.Mutex
verbose bool
}
// NewForwardWorker creates a new ForwardWorker for a single forward configuration.
@@ -142,7 +142,7 @@ func (w *ForwardWorker) run() {
// Start HTTP logging proxy if enabled
if err := w.startHTTPProxy(); err != nil {
logger.Error("Failed to start HTTP logging proxy", map[string]interface{}{
logger.Error("Failed to start HTTP logging proxy", map[string]any{
"forward_id": w.forward.ID(),
"error": err.Error(),
})
@@ -175,7 +175,7 @@ func (w *ForwardWorker) run() {
)
if err != nil {
logger.Error("Failed to resolve resource", map[string]interface{}{
logger.Error("Failed to resolve resource", map[string]any{
"forward_id": w.forward.ID(),
"context": w.forward.GetContext(),
"namespace": w.forward.GetNamespace(),
@@ -191,7 +191,7 @@ func (w *ForwardWorker) run() {
if w.healthChecker != nil {
w.healthChecker.MarkReconnecting(w.forward.ID())
}
logger.Info("Pod restart detected, switching to new pod", map[string]interface{}{
logger.Info("Pod restart detected, switching to new pod", map[string]any{
"forward_id": w.forward.ID(),
"old_pod": w.lastPod,
"new_pod": podName,
@@ -199,7 +199,7 @@ func (w *ForwardWorker) run() {
"namespace": w.forward.GetNamespace(),
})
} else if w.lastPod == "" {
logger.Info("Starting port forward", map[string]interface{}{
logger.Info("Starting port forward", map[string]any{
"forward_id": w.forward.ID(),
"target": w.forward.String(),
"local_port": w.forward.LocalPort,
@@ -228,7 +228,7 @@ func (w *ForwardWorker) run() {
}
// Log the error
logger.Warn("Port-forward connection failed, will retry", map[string]interface{}{
logger.Warn("Port-forward connection failed, will retry", map[string]any{
"forward_id": w.forward.ID(),
"context": w.forward.GetContext(),
"namespace": w.forward.GetNamespace(),
@@ -433,7 +433,7 @@ func (w *ForwardWorker) startHTTPProxy() error {
w.httpProxy = proxy
logger.Info("HTTP logging proxy started", map[string]interface{}{
logger.Info("HTTP logging proxy started", map[string]any{
"forward_id": w.forward.ID(),
"local_port": w.forward.LocalPort,
"target_port": targetPort,
@@ -446,7 +446,7 @@ func (w *ForwardWorker) startHTTPProxy() error {
func (w *ForwardWorker) stopHTTPProxy() {
if w.httpProxy != nil {
if err := w.httpProxy.Stop(); err != nil {
logger.Warn("Failed to stop HTTP proxy", map[string]interface{}{
logger.Warn("Failed to stop HTTP proxy", map[string]any{
"forward_id": w.forward.ID(),
"error": err.Error(),
})
+5 -5
View File
@@ -55,8 +55,8 @@ func TestLogWriter_Write(t *testing.T) {
func TestForwardWorker_GetForward(t *testing.T) {
tests := []struct {
name string
forward config.Forward
description string
forward config.Forward
}{
{
name: "get pod forward",
@@ -141,9 +141,9 @@ func TestForwardWorker_IsRunning(t *testing.T) {
func TestForwardID(t *testing.T) {
tests := []struct {
name string
description string
forward config.Forward
expectUnique bool
description string
}{
{
name: "unique IDs for different forwards",
@@ -183,9 +183,9 @@ func TestForwardID(t *testing.T) {
func TestForwardString(t *testing.T) {
tests := []struct {
name string
forward config.Forward
expectedContains []string
description string
expectedContains []string
forward config.Forward
}{
{
name: "pod forward string",
@@ -259,8 +259,8 @@ func TestSleepWithBackoffConcept(t *testing.T) {
func TestWorkerVerboseMode(t *testing.T) {
tests := []struct {
name string
verbose bool
description string
verbose bool
}{
{
name: "verbose mode enabled",