bugfixes nov2025 pt3 (#6)

* Minor improvements.
* DRY the codebase.
* Add version checker / updater.
This commit is contained in:
2025-11-25 01:28:23 +00:00
committed by GitHub
parent 49acba5679
commit 3a7cc6f502
14 changed files with 634 additions and 158 deletions
+55 -38
View File
@@ -1,6 +1,7 @@
package config
import (
"bytes"
"fmt"
"os"
"time"
@@ -9,7 +10,20 @@ import (
)
const (
maxConfigSize = 10 * 1024 * 1024 // 10MB
// maxConfigSize is the maximum allowed configuration file size (10MB)
maxConfigSize = 10 * 1024 * 1024
// Default health check settings
DefaultHealthCheckInterval = 3 * time.Second // How often to check connection health
DefaultHealthCheckTimeout = 2 * time.Second // Timeout for health check probes
DefaultHealthCheckMethod = "data-transfer" // More reliable than tcp-dial
DefaultMaxConnectionAge = 25 * time.Minute // Reconnect before k8s 30min timeout
DefaultMaxIdleTime = 10 * time.Minute // Reconnect if no activity
// Default reliability settings
DefaultTCPKeepalive = 30 * time.Second // OS-level TCP keepalive interval
DefaultDialTimeout = 30 * time.Second // Connection establishment timeout
DefaultWatchdogPeriod = 30 * time.Second // Goroutine health check interval
)
// Config represents the root configuration structure from .kportal.yaml
@@ -36,24 +50,31 @@ type ReliabilitySpec struct {
WatchdogPeriod string `yaml:"watchdogPeriod,omitempty"` // e.g., "30s" - goroutine watchdog interval
}
// parseDurationOrDefault parses a duration string and returns the default if empty or invalid.
func parseDurationOrDefault(value string, defaultDur time.Duration) time.Duration {
if value == "" {
return defaultDur
}
if d, err := time.ParseDuration(value); err == nil {
return d
}
return defaultDur
}
// GetHealthCheckIntervalOrDefault returns the health check interval or default value
func (c *Config) GetHealthCheckIntervalOrDefault() time.Duration {
if c.HealthCheck != nil && c.HealthCheck.Interval != "" {
if d, err := time.ParseDuration(c.HealthCheck.Interval); err == nil {
return d
}
if c.HealthCheck == nil {
return DefaultHealthCheckInterval
}
return 3 * time.Second // Default: check every 3 seconds
return parseDurationOrDefault(c.HealthCheck.Interval, DefaultHealthCheckInterval)
}
// GetHealthCheckTimeoutOrDefault returns the health check timeout or default value
func (c *Config) GetHealthCheckTimeoutOrDefault() time.Duration {
if c.HealthCheck != nil && c.HealthCheck.Timeout != "" {
if d, err := time.ParseDuration(c.HealthCheck.Timeout); err == nil {
return d
}
if c.HealthCheck == nil {
return DefaultHealthCheckTimeout
}
return 2 * time.Second // Default: 2 second timeout
return parseDurationOrDefault(c.HealthCheck.Timeout, DefaultHealthCheckTimeout)
}
// GetHealthCheckMethod returns the health check method or default
@@ -61,37 +82,31 @@ func (c *Config) GetHealthCheckMethod() string {
if c.HealthCheck != nil && c.HealthCheck.Method != "" {
return c.HealthCheck.Method
}
return "data-transfer" // Default: more reliable data transfer test
return DefaultHealthCheckMethod
}
// GetMaxConnectionAge returns the max connection age or default
func (c *Config) GetMaxConnectionAge() time.Duration {
if c.HealthCheck != nil && c.HealthCheck.MaxConnectionAge != "" {
if d, err := time.ParseDuration(c.HealthCheck.MaxConnectionAge); err == nil {
return d
}
if c.HealthCheck == nil {
return DefaultMaxConnectionAge
}
return 25 * time.Minute // Default: 25 minutes (before typical 30min k8s timeout)
return parseDurationOrDefault(c.HealthCheck.MaxConnectionAge, DefaultMaxConnectionAge)
}
// GetMaxIdleTime returns the max idle time or default
func (c *Config) GetMaxIdleTime() time.Duration {
if c.HealthCheck != nil && c.HealthCheck.MaxIdleTime != "" {
if d, err := time.ParseDuration(c.HealthCheck.MaxIdleTime); err == nil {
return d
}
if c.HealthCheck == nil {
return DefaultMaxIdleTime
}
return 10 * time.Minute // Default: 10 minutes idle before reconnect
return parseDurationOrDefault(c.HealthCheck.MaxIdleTime, DefaultMaxIdleTime)
}
// GetTCPKeepalive returns the TCP keepalive duration or default
func (c *Config) GetTCPKeepalive() time.Duration {
if c.Reliability != nil && c.Reliability.TCPKeepalive != "" {
if d, err := time.ParseDuration(c.Reliability.TCPKeepalive); err == nil {
return d
}
if c.Reliability == nil {
return DefaultTCPKeepalive
}
return 30 * time.Second // Default: 30 second keepalive
return parseDurationOrDefault(c.Reliability.TCPKeepalive, DefaultTCPKeepalive)
}
// GetRetryOnStale returns whether to retry on stale connections
@@ -104,22 +119,18 @@ func (c *Config) GetRetryOnStale() bool {
// GetWatchdogPeriod returns the goroutine watchdog check period or default
func (c *Config) GetWatchdogPeriod() time.Duration {
if c.Reliability != nil && c.Reliability.WatchdogPeriod != "" {
if d, err := time.ParseDuration(c.Reliability.WatchdogPeriod); err == nil {
return d
}
if c.Reliability == nil {
return DefaultWatchdogPeriod
}
return 30 * time.Second // Default: check every 30 seconds
return parseDurationOrDefault(c.Reliability.WatchdogPeriod, DefaultWatchdogPeriod)
}
// GetDialTimeout returns the connection dial timeout or default
func (c *Config) GetDialTimeout() time.Duration {
if c.Reliability != nil && c.Reliability.DialTimeout != "" {
if d, err := time.ParseDuration(c.Reliability.DialTimeout); err == nil {
return d
}
if c.Reliability == nil {
return DefaultDialTimeout
}
return 30 * time.Second // Default: 30 second dial timeout
return parseDurationOrDefault(c.Reliability.DialTimeout, DefaultDialTimeout)
}
// Context represents a Kubernetes context with its namespaces
@@ -209,9 +220,15 @@ func LoadConfig(path string) (*Config, error) {
}
// ParseConfig parses YAML configuration data into a Config struct.
// It uses strict parsing that rejects unknown keys to catch typos.
func ParseConfig(data []byte) (*Config, error) {
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
// Use decoder with KnownFields to reject unknown keys (catches typos)
decoder := yaml.NewDecoder(bytes.NewReader(data))
decoder.KnownFields(true)
if err := decoder.Decode(&cfg); err != nil {
return nil, fmt.Errorf("failed to parse YAML: %w", err)
}
+12 -7
View File
@@ -6,10 +6,15 @@ import (
)
const (
minPort = 1
maxPort = 65535
MinPort = 1
MaxPort = 65535
)
// IsValidPort returns true if the port number is within the valid range (1-65535).
func IsValidPort(port int) bool {
return port >= MinPort && port <= MaxPort
}
// ValidationError represents a configuration validation error with context.
type ValidationError struct {
Field string // The field that failed validation
@@ -84,7 +89,7 @@ func (v *Validator) validateStructure(cfg *Config) []ValidationError {
Field: fmt.Sprintf("contexts[%d].namespaces", i),
Message: fmt.Sprintf("Context '%s' must have at least one namespace", ctx.Name),
})
continue
// Don't continue - still validate other aspects of the context if any
}
for j, ns := range ctx.Namespaces {
@@ -130,17 +135,17 @@ func (v *Validator) validateForward(fwd *Forward) []ValidationError {
}
// Validate ports
if fwd.Port < minPort || fwd.Port > maxPort {
if fwd.Port < MinPort || fwd.Port > MaxPort {
errs = append(errs, ValidationError{
Field: "port",
Message: fmt.Sprintf("Invalid port %d for forward %s (must be between %d and %d)", fwd.Port, fwd.ID(), minPort, maxPort),
Message: fmt.Sprintf("Invalid port %d for forward %s (must be between %d and %d)", fwd.Port, fwd.ID(), MinPort, MaxPort),
})
}
if fwd.LocalPort < minPort || fwd.LocalPort > maxPort {
if fwd.LocalPort < MinPort || fwd.LocalPort > MaxPort {
errs = append(errs, ValidationError{
Field: "localPort",
Message: fmt.Sprintf("Invalid localPort %d for forward %s (must be between %d and %d)", fwd.LocalPort, fwd.ID(), minPort, maxPort),
Message: fmt.Sprintf("Invalid localPort %d for forward %s (must be between %d and %d)", fwd.LocalPort, fwd.ID(), MinPort, MaxPort),
})
}