fixup! Update go.mod and go.sum (#48)

This commit is contained in:
2026-02-20 15:39:27 +00:00
parent 0aaf2dc78c
commit 8e5eaab0af
43 changed files with 5271 additions and 129 deletions
+335 -18
View File
@@ -2,12 +2,39 @@ package config
import (
"fmt"
"regexp"
"strings"
"time"
)
const (
MinPort = 1
MaxPort = 65535
// DNS1123LabelMaxLength is the maximum length of a DNS label (RFC 1123)
DNS1123LabelMaxLength = 63
// DNS1123SubdomainMaxLength is the maximum length of a DNS subdomain name
DNS1123SubdomainMaxLength = 253
)
var (
// dns1123LabelRegexp matches valid DNS labels (RFC 1123)
// Must consist of lowercase alphanumeric characters or '-', start with alphanumeric, end with alphanumeric
dns1123LabelRegexp = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)
// dns1123SubdomainRegexp matches valid DNS subdomain names
// A series of DNS labels separated by dots (no consecutive dots allowed)
dns1123SubdomainRegexp = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
// contextNameRegexp matches valid context names
// Allows alphanumeric characters, hyphens, and underscores (to support various kubeconfig naming conventions)
contextNameRegexp = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$`)
// validResourceTypes contains the allowed Kubernetes resource types
validResourceTypes = []string{"pod", "service"}
// validHealthCheckMethods contains the allowed health check methods
validHealthCheckMethods = []string{"tcp-dial", "data-transfer"}
)
// IsValidPort returns true if the port number is within the valid range (1-65535).
@@ -51,6 +78,7 @@ func (v *Validator) ValidateConfigWithOptions(cfg *Config, allowEmpty bool) []Va
// If empty configs are allowed and this config is empty, skip structure validation
if allowEmpty && cfg.IsEmpty() {
// Still validate health check and reliability if present (they don't require forwards)
errs = append(errs, v.validateSpecDurations(cfg)...)
return errs
}
@@ -74,6 +102,9 @@ func (v *Validator) ValidateConfigWithOptions(cfg *Config, allowEmpty bool) []Va
errs = append(errs, v.validateMDNS(cfg)...)
}
// Validate duration fields in specs
errs = append(errs, v.validateSpecDurations(cfg)...)
return errs
}
@@ -95,6 +126,11 @@ func (v *Validator) validateStructure(cfg *Config) []ValidationError {
Field: fmt.Sprintf("contexts[%d].name", i),
Message: "Context name cannot be empty",
})
} else {
// Validate context name format (alphanumeric, hyphens, underscores)
if err := validateContextName(ctx.Name, fmt.Sprintf("contexts[%d].name", i)); err != nil {
errs = append(errs, *err)
}
}
if len(ctx.Namespaces) == 0 {
@@ -111,6 +147,11 @@ func (v *Validator) validateStructure(cfg *Config) []ValidationError {
Field: fmt.Sprintf("contexts[%d].namespaces[%d].name", i, j),
Message: fmt.Sprintf("Namespace name cannot be empty in context '%s'", ctx.Name),
})
} else {
// Validate namespace name follows DNS subdomain conventions (Kubernetes requirement)
if err := validateNamespaceName(ns.Name, fmt.Sprintf("contexts[%d].namespaces[%d].name", i, j)); err != nil {
errs = append(errs, *err)
}
}
if len(ns.Forwards) == 0 {
@@ -139,29 +180,38 @@ func (v *Validator) validateForward(fwd *Forward) []ValidationError {
errs = append(errs, v.validateResource(fwd)...)
}
// Validate protocol
if fwd.Protocol != "" && fwd.Protocol != "tcp" && fwd.Protocol != "udp" {
// Validate protocol - only "tcp" is currently supported
if fwd.Protocol != "" && fwd.Protocol != "tcp" {
errs = append(errs, ValidationError{
Field: "protocol",
Message: fmt.Sprintf("Invalid protocol '%s' for forward %s (must be 'tcp' or 'udp')", fwd.Protocol, fwd.ID()),
Message: fmt.Sprintf("Invalid protocol '%s' for forward %s (only 'tcp' is supported)", fwd.Protocol, fwd.ID()),
})
}
// Validate ports
if fwd.Port < MinPort || fwd.Port > MaxPort {
if !IsValidPort(fwd.Port) {
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),
})
}
if fwd.LocalPort < MinPort || fwd.LocalPort > MaxPort {
if !IsValidPort(fwd.LocalPort) {
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),
})
}
// Note: Alias validation is handled in validateMDNS since aliases are primarily
// used for mDNS hostname registration. We only validate alias format when mDNS
// is enabled to avoid unnecessary restrictions on non-mDNS usage.
// Validate HTTP log configuration if enabled
if fwd.HTTPLog != nil && fwd.HTTPLog.Enabled {
errs = append(errs, v.validateHTTPLog(fwd)...)
}
return errs
}
@@ -169,18 +219,44 @@ func (v *Validator) validateForward(fwd *Forward) []ValidationError {
func (v *Validator) validateResource(fwd *Forward) []ValidationError {
var errs []ValidationError
// Validate resource format (must be "type/name" or just "type" for pod with selector)
parts := strings.SplitN(fwd.Resource, "/", 2)
resourceType := parts[0]
// Valid resource types: pod, service
if resourceType != "pod" && resourceType != "service" {
// Validate resource type
if !isValidResourceType(resourceType) {
errs = append(errs, ValidationError{
Field: "resource",
Message: fmt.Sprintf("Invalid resource type '%s' for forward %s (must be 'pod' or 'service')", resourceType, fwd.ID()),
Message: fmt.Sprintf("Invalid resource type '%s' for forward %s (must be one of: %s)", resourceType, fwd.ID(), strings.Join(validResourceTypes, ", ")),
})
return errs
}
// Validate resource name if provided
if len(parts) == 2 {
resourceName := parts[1]
if resourceName == "" {
// Use resource-type-specific error message for better clarity
entityType := "Resource"
switch resourceType {
case "pod":
entityType = "Pod"
case "service":
entityType = "Service"
}
errs = append(errs, ValidationError{
Field: "resource",
Message: fmt.Sprintf("%s name cannot be empty for forward %s", entityType, fwd.ID()),
})
} else {
// Validate resource name follows DNS subdomain conventions
if err := validateDNS1123Subdomain(resourceName, "resource", "Resource name"); err != nil {
err.Message = fmt.Sprintf("%s for forward %s", err.Message, fwd.ID())
errs = append(errs, *err)
}
}
}
// For pod resources
if resourceType == "pod" {
if len(parts) == 2 {
@@ -191,14 +267,6 @@ func (v *Validator) validateResource(fwd *Forward) []ValidationError {
Message: fmt.Sprintf("Forward %s uses explicit pod name (%s) and should not have a selector", fwd.ID(), fwd.Resource),
})
}
// Validate pod name is not empty
if parts[1] == "" {
errs = append(errs, ValidationError{
Field: "resource",
Message: fmt.Sprintf("Pod name cannot be empty for forward %s", fwd.ID()),
})
}
} else if fwd.Selector == "" {
// pod (no name) - must have selector
errs = append(errs, ValidationError{
@@ -213,7 +281,7 @@ func (v *Validator) validateResource(fwd *Forward) []ValidationError {
if len(parts) < 2 || parts[1] == "" {
errs = append(errs, ValidationError{
Field: "resource",
Message: fmt.Sprintf("Service name cannot be empty for forward %s", fwd.ID()),
Message: fmt.Sprintf("Service name cannot be empty for forward %s (format: service/name)", fwd.ID()),
})
}
@@ -259,6 +327,109 @@ func (v *Validator) validateDuplicatePorts(cfg *Config) []ValidationError {
return errs
}
// validateSpecDurations validates duration strings in HealthCheck and Reliability specs.
func (v *Validator) validateSpecDurations(cfg *Config) []ValidationError {
var errs []ValidationError
// Validate HealthCheck durations
if cfg.HealthCheck != nil {
if cfg.HealthCheck.Interval != "" {
if _, err := time.ParseDuration(cfg.HealthCheck.Interval); err != nil {
errs = append(errs, ValidationError{
Field: "healthCheck.interval",
Message: fmt.Sprintf("Invalid health check interval '%s': %v", cfg.HealthCheck.Interval, err),
})
}
}
if cfg.HealthCheck.Timeout != "" {
if _, err := time.ParseDuration(cfg.HealthCheck.Timeout); err != nil {
errs = append(errs, ValidationError{
Field: "healthCheck.timeout",
Message: fmt.Sprintf("Invalid health check timeout '%s': %v", cfg.HealthCheck.Timeout, err),
})
}
}
if cfg.HealthCheck.MaxConnectionAge != "" {
if _, err := time.ParseDuration(cfg.HealthCheck.MaxConnectionAge); err != nil {
errs = append(errs, ValidationError{
Field: "healthCheck.maxConnectionAge",
Message: fmt.Sprintf("Invalid max connection age '%s': %v", cfg.HealthCheck.MaxConnectionAge, err),
})
}
}
if cfg.HealthCheck.MaxIdleTime != "" {
if _, err := time.ParseDuration(cfg.HealthCheck.MaxIdleTime); err != nil {
errs = append(errs, ValidationError{
Field: "healthCheck.maxIdleTime",
Message: fmt.Sprintf("Invalid max idle time '%s': %v", cfg.HealthCheck.MaxIdleTime, err),
})
}
}
// Validate health check method
if cfg.HealthCheck.Method != "" && !isValidHealthCheckMethod(cfg.HealthCheck.Method) {
errs = append(errs, ValidationError{
Field: "healthCheck.method",
Message: fmt.Sprintf("Invalid health check method '%s' (must be one of: %s)", cfg.HealthCheck.Method, strings.Join(validHealthCheckMethods, ", ")),
})
}
}
// Validate Reliability durations
if cfg.Reliability != nil {
if cfg.Reliability.TCPKeepalive != "" {
if _, err := time.ParseDuration(cfg.Reliability.TCPKeepalive); err != nil {
errs = append(errs, ValidationError{
Field: "reliability.tcpKeepalive",
Message: fmt.Sprintf("Invalid TCP keepalive duration '%s': %v", cfg.Reliability.TCPKeepalive, err),
})
}
}
if cfg.Reliability.DialTimeout != "" {
if _, err := time.ParseDuration(cfg.Reliability.DialTimeout); err != nil {
errs = append(errs, ValidationError{
Field: "reliability.dialTimeout",
Message: fmt.Sprintf("Invalid dial timeout '%s': %v", cfg.Reliability.DialTimeout, err),
})
}
}
if cfg.Reliability.WatchdogPeriod != "" {
if _, err := time.ParseDuration(cfg.Reliability.WatchdogPeriod); err != nil {
errs = append(errs, ValidationError{
Field: "reliability.watchdogPeriod",
Message: fmt.Sprintf("Invalid watchdog period '%s': %v", cfg.Reliability.WatchdogPeriod, err),
})
}
}
}
return errs
}
// validateHTTPLog validates HTTP log configuration.
func (v *Validator) validateHTTPLog(fwd *Forward) []ValidationError {
var errs []ValidationError
if fwd.HTTPLog == nil {
return errs
}
// Validate maxBodySize is non-negative
if fwd.HTTPLog.MaxBodySize < 0 {
errs = append(errs, ValidationError{
Field: "httpLog.maxBodySize",
Message: fmt.Sprintf("Invalid maxBodySize %d for forward %s (must be non-negative)", fwd.HTTPLog.MaxBodySize, fwd.ID()),
})
}
return errs
}
// FormatValidationErrors formats validation errors into a human-readable string.
func FormatValidationErrors(errs []ValidationError) string {
if len(errs) == 0 {
@@ -334,7 +505,7 @@ func (v *Validator) validateMDNS(cfg *Config) []ValidationError {
// Hostnames must start with alphanumeric, contain only alphanumeric and hyphens,
// and be 1-63 characters long.
func isValidHostname(name string) bool {
if len(name) == 0 || len(name) > 63 {
if len(name) == 0 || len(name) > DNS1123LabelMaxLength {
return false
}
@@ -363,3 +534,149 @@ func isValidHostname(name string) bool {
func isAlphanumeric(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
}
// isValidResourceType returns true if the resource type is valid.
func isValidResourceType(resourceType string) bool {
for _, rt := range validResourceTypes {
if rt == resourceType {
return true
}
}
return false
}
// isValidHealthCheckMethod returns true if the health check method is valid.
func isValidHealthCheckMethod(method string) bool {
for _, m := range validHealthCheckMethods {
if m == method {
return true
}
}
return false
}
// validateContextName validates that a context name follows the allowed format.
// Context names must consist of alphanumeric characters, hyphens, or underscores,
// and must start and end with an alphanumeric character.
// This more permissive validation supports various kubeconfig naming conventions
// (e.g., "gke_project_zone_cluster", "minikube", "docker-desktop").
func validateContextName(name, field string) *ValidationError {
if len(name) > DNS1123SubdomainMaxLength {
return &ValidationError{
Field: field,
Message: fmt.Sprintf("Context name '%s' exceeds maximum length of %d characters", name, DNS1123SubdomainMaxLength),
}
}
if !contextNameRegexp.MatchString(name) {
return &ValidationError{
Field: field,
Message: fmt.Sprintf("Context name '%s' is not valid (must consist of alphanumeric characters, hyphens, or underscores, and start/end with alphanumeric)", name),
}
}
return nil
}
// validateNamespaceName validates that a namespace name is a valid DNS subdomain (RFC 1123).
// Kubernetes namespaces must follow DNS subdomain format which allows dots for subdomain separation.
// This is more permissive than DNS labels and supports names like "kube-system", "my-app.ns".
func validateNamespaceName(name, field string) *ValidationError {
if len(name) > DNS1123SubdomainMaxLength {
return &ValidationError{
Field: field,
Message: fmt.Sprintf("Namespace name '%s' exceeds maximum length of %d characters", name, DNS1123SubdomainMaxLength),
}
}
if !dns1123SubdomainRegexp.MatchString(name) {
return &ValidationError{
Field: field,
Message: fmt.Sprintf("Namespace name '%s' is not a valid DNS subdomain (must consist of lowercase alphanumeric characters, '-', or '.', start with alphanumeric, end with alphanumeric)", name),
}
}
return nil
}
// validateDNS1123Label validates that a name is a valid DNS label (RFC 1123).
// Used for context names and namespace names.
func validateDNS1123Label(name, field, entityType string) *ValidationError {
if len(name) > DNS1123LabelMaxLength {
return &ValidationError{
Field: field,
Message: fmt.Sprintf("%s name '%s' exceeds maximum length of %d characters", entityType, name, DNS1123LabelMaxLength),
}
}
if !dns1123LabelRegexp.MatchString(name) {
return &ValidationError{
Field: field,
Message: fmt.Sprintf("%s name '%s' is not a valid DNS label (must consist of lowercase alphanumeric characters or '-', start with alphanumeric, end with alphanumeric)", entityType, name),
}
}
return nil
}
// validateDNS1123Subdomain validates that a name is a valid DNS subdomain name (RFC 1123).
// Used for resource names which can contain dots.
func validateDNS1123Subdomain(name, field, entityType string) *ValidationError {
if len(name) > DNS1123SubdomainMaxLength {
return &ValidationError{
Field: field,
Message: fmt.Sprintf("%s '%s' exceeds maximum length of %d characters", entityType, name, DNS1123SubdomainMaxLength),
}
}
if !dns1123SubdomainRegexp.MatchString(name) {
return &ValidationError{
Field: field,
Message: fmt.Sprintf("%s '%s' is not a valid DNS subdomain name (must consist of lowercase alphanumeric characters, '-', or '.', start with alphanumeric, end with alphanumeric)", entityType, name),
}
}
return nil
}
// ValidatePort validates a port number and returns an error if invalid.
// This is a public function that can be used externally.
func ValidatePort(port int, name string) error {
if !IsValidPort(port) {
return fmt.Errorf("%s must be between %d and %d, got %d", name, MinPort, MaxPort, port)
}
return nil
}
// ValidateResourceFormat validates that a resource string is in the correct format.
// This is a public function that can be used externally.
func ValidateResourceFormat(resource string) error {
parts := strings.SplitN(resource, "/", 2)
if len(parts) != 2 {
return fmt.Errorf("resource must be in format 'type/name', got: %s", resource)
}
resourceType := parts[0]
if !isValidResourceType(resourceType) {
return fmt.Errorf("invalid resource type '%s' (must be one of: %s)", resourceType, strings.Join(validResourceTypes, ", "))
}
if parts[1] == "" {
return fmt.Errorf("resource name cannot be empty in '%s'", resource)
}
return nil
}
// ValidateDuration validates that a string is a valid duration.
// This is a public function that can be used externally.
func ValidateDuration(duration, name string) error {
if duration == "" {
return nil // Empty durations are allowed (will use defaults)
}
_, err := time.ParseDuration(duration)
if err != nil {
return fmt.Errorf("invalid %s '%s': %v", name, duration, err)
}
return nil
}