90 lines
3.0 KiB
Go
90 lines
3.0 KiB
Go
// Package router resolves incoming SNI names to PostgreSQL backends.
|
|
package router
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
config "cloud.campbellwireless.net/git/barkstack/barkfile-parser"
|
|
)
|
|
|
|
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
|
|
}
|