// Package router resolves incoming SNI names to PostgreSQL backends. package router import ( "context" "errors" "fmt" "strings" "github.com/barkstack/pawsql/internal/config" ) var ( ErrUnknownHostname = errors.New("unknown database hostname") ErrUnknownDatabase = errors.New("unknown configured database") ) // Backend is a resolved PostgreSQL destination. type Backend struct { DatabaseName string Address string } // BackendResolver permits future lifecycle-aware backend discovery. type BackendResolver interface { Resolve(context.Context, string) (Backend, error) ResolveDatabase(context.Context, string) (Backend, error) } // ConnectionLeaseManager releases a backend lease when a proxied client session ends. // Resolvers that do not manage lifecycle state may ignore this optional interface. type ConnectionLeaseManager interface { ReleaseConnection(string) } // TrafficMeter receives byte counts flowing through a proxied backend session. // clientToBackend identifies the PostgreSQL client-to-server direction. type TrafficMeter interface { RecordTraffic(database string, clientToBackend bool, bytes int64) } // StaticResolver resolves routes loaded from a Barkfile. type StaticResolver struct { hostnameRoutes map[string]Backend databaseRoutes map[string]Backend } // NewStaticResolver builds a resolver from typed configuration. func NewStaticResolver(databases []config.DatabaseConfig) (*StaticResolver, error) { hostnameRoutes := make(map[string]Backend, len(databases)) databaseRoutes := make(map[string]Backend, len(databases)) for _, database := range databases { if database.Name == "" { return nil, errors.New("database name is required") } if _, exists := databaseRoutes[database.Name]; exists { return nil, fmt.Errorf("duplicate database name %q", database.Name) } backend := Backend{DatabaseName: database.Name, Address: database.Upstream} databaseRoutes[database.Name] = backend hostname := config.NormalizeHostname(database.Hostname) if hostname == "" { continue } if _, exists := hostnameRoutes[hostname]; exists { return nil, fmt.Errorf("duplicate hostname %q", hostname) } hostnameRoutes[hostname] = backend } return &StaticResolver{hostnameRoutes: hostnameRoutes, databaseRoutes: databaseRoutes}, nil } // Resolve returns the exact case-insensitive hostname match, never a default route. func (r *StaticResolver) Resolve(_ context.Context, hostname string) (Backend, error) { backend, ok := r.hostnameRoutes[strings.ToLower(strings.TrimSuffix(strings.TrimSpace(hostname), "."))] if !ok { return Backend{}, fmt.Errorf("%w: %s", ErrUnknownHostname, hostname) } return backend, nil } // ResolveDatabase returns the exact configured PostgreSQL database match. func (r *StaticResolver) ResolveDatabase(_ context.Context, database string) (Backend, error) { backend, ok := r.databaseRoutes[database] if !ok { return Backend{}, fmt.Errorf("%w: %s", ErrUnknownDatabase, database) } return backend, nil }