package barkfile import ( "errors" "fmt" "net" "regexp" "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)), ".") } const DockerSecretPrefix = "barkstack_" var secretReferencePattern = regexp.MustCompile(`^[a-z][a-z0-9_]*$`) // DockerSecretName returns the namespaced Docker object name for a validated // short Barkfile secret reference. func DockerSecretName(reference string) string { return DockerSecretPrefix + reference } // 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)) } certificateSet := strings.TrimSpace(c.TLS.CertFile) != "" keySet := strings.TrimSpace(c.TLS.KeyFile) != "" if certificateSet != keySet { errs = append(errs, errors.New("tls requires both cert and key")) } 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 !ValidSecretReference(postgres.PasswordSecret) { return fmt.Errorf("password_secret %q must start with a lowercase letter, contain only lowercase letters, digits, or underscores, and be at most 54 characters", postgres.PasswordSecret) } 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 } // ValidSecretReference reports whether reference is safe for use as a short // Barkfile secret name and as part of a Docker secret name. func ValidSecretReference(reference string) bool { return len(reference) <= 54 && secretReferencePattern.MatchString(reference) } 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 }