151 lines
5.3 KiB
Go
151 lines
5.3 KiB
Go
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_"
|
|
TreatVaultManagedLabel = "io.barkstack.treatvault"
|
|
TreatVaultSecretNameLabel = "io.barkstack.treatvault.name"
|
|
TreatVaultVaultIDLabel = "io.barkstack.treatvault.vault"
|
|
TreatVaultRevisionLabel = "io.barkstack.treatvault.revision"
|
|
TreatVaultConsumerLabel = "io.barkstack.treatvault.secrets"
|
|
TreatVaultNamesLabel = "io.barkstack.treatvault.names"
|
|
)
|
|
|
|
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"))
|
|
}
|
|
if c.TreatVault != nil {
|
|
if strings.TrimSpace(c.TreatVault.File) == "" {
|
|
errs = append(errs, errors.New("treatvault file is required"))
|
|
}
|
|
if !ValidSecretReference(c.TreatVault.IdentitySecret) {
|
|
errs = append(errs, fmt.Errorf("treatvault identity_secret %q must start with a lowercase letter, contain only lowercase letters, digits, or underscores, and be at most 54 characters", c.TreatVault.IdentitySecret))
|
|
}
|
|
}
|
|
|
|
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))
|
|
}
|
|
if c.TreatVault != nil && database.Postgres.PasswordSecret == c.TreatVault.IdentitySecret {
|
|
errs = append(errs, fmt.Errorf("database %q: password_secret must not reference the TreatVault identity secret", name))
|
|
}
|
|
} 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
|
|
}
|