mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-07-05 06:05:39 +00:00
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:
@@ -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")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -185,8 +185,8 @@ type StatusUpdate struct {
|
||||
}
|
||||
|
||||
type ForwardAdd struct {
|
||||
ID string
|
||||
Fwd *config.Forward
|
||||
ID string
|
||||
}
|
||||
|
||||
type ErrorSet struct {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user