Files
barkfile-parser/validate.go
Shaun Campbell c7654ea72a
Some checks failed
Test and Release Module / test (push) Failing after 8s
Test and Release Module / release (push) Has been skipped
feat: centralize Barkfile schema
2026-09-15 19:44:54 -04:00

116 lines
3.6 KiB
Go

package barkfile
import (
"errors"
"fmt"
"net"
"strconv"
"strings"
)
// NormalizeHostname returns the canonical lookup form for a DNS hostname.
func NormalizeHostname(hostname string) string {
return strings.TrimSuffix(strings.ToLower(strings.TrimSpace(hostname)), ".")
}
// Validate verifies configuration invariants that do not require opening files.
func (c Config) Validate() error {
var errs []error
if strings.TrimSpace(c.Listen) == "" {
errs = append(errs, errors.New("listen address is required"))
} else if err := validateListenAddress(c.Listen); err != nil {
errs = append(errs, fmt.Errorf("listen address %q: %w", c.Listen, err))
}
if strings.TrimSpace(c.TLS.CertFile) == "" {
errs = append(errs, errors.New("TLS certificate is required"))
}
if strings.TrimSpace(c.TLS.KeyFile) == "" {
errs = append(errs, errors.New("TLS private key is required"))
}
if len(c.Databases) == 0 {
errs = append(errs, errors.New("at least one database route is required"))
}
seenHostnames := make(map[string]string, len(c.Databases))
seenNames := make(map[string]struct{}, len(c.Databases))
for _, database := range c.Databases {
name := strings.TrimSpace(database.Name)
if name == "" {
errs = append(errs, errors.New("database name is required"))
} else if _, exists := seenNames[name]; exists {
errs = append(errs, fmt.Errorf("duplicate database name %q", name))
} else {
seenNames[name] = struct{}{}
}
hostname := NormalizeHostname(database.Hostname)
if hostname != "" {
if existing, ok := seenHostnames[hostname]; ok {
errs = append(errs, fmt.Errorf("duplicate hostname %q for databases %q and %q", hostname, existing, name))
} else {
seenHostnames[hostname] = name
}
}
if database.Postgres != nil {
if database.Upstream != "" {
errs = append(errs, fmt.Errorf("database %q: upstream and postgres cannot both be configured", name))
}
if err := validatePostgres(*database.Postgres); err != nil {
errs = append(errs, fmt.Errorf("database %q: postgres: %w", name, err))
}
} else if strings.TrimSpace(database.Upstream) == "" {
errs = append(errs, fmt.Errorf("database %q: upstream or postgres is required", name))
} else if err := validateAddress(database.Upstream); err != nil {
errs = append(errs, fmt.Errorf("database %q: upstream %q: %w", name, database.Upstream, err))
}
}
return errors.Join(errs...)
}
func validatePostgres(postgres PostgresConfig) error {
switch postgres.Image {
case "postgres:16", "postgres:17", "postgres:18":
default:
return fmt.Errorf("image %q must be postgres:16, postgres:17, or postgres:18", postgres.Image)
}
if strings.TrimSpace(postgres.Volume) == "" {
return errors.New("volume is required")
}
if strings.TrimSpace(postgres.PasswordEnv) == "" {
return errors.New("password_env is required")
}
if postgres.IdleTimeout < 0 {
return errors.New("idle_timeout cannot be negative")
}
if postgres.TrafficIdleTimeout < 0 {
return errors.New("traffic_idle_timeout cannot be negative")
}
return nil
}
func validateListenAddress(address string) error {
_, port, err := net.SplitHostPort(address)
if err != nil {
return err
}
portNumber, err := strconv.ParseUint(port, 10, 16)
if err != nil || portNumber == 0 {
return errors.New("port must be between 1 and 65535")
}
return nil
}
func validateAddress(address string) error {
host, port, err := net.SplitHostPort(address)
if err != nil {
return err
}
if strings.TrimSpace(host) == "" {
return errors.New("host is required")
}
portNumber, err := strconv.ParseUint(port, 10, 16)
if err != nil || portNumber == 0 {
return errors.New("port must be between 1 and 65535")
}
return nil
}