feat: add PawSQL docs examples and image CI
All checks were successful
Build and Push Image / docker-build-and-push (push) Successful in 2m28s
All checks were successful
Build and Push Image / docker-build-and-push (push) Successful in 2m28s
This commit is contained in:
89
internal/router/routes.go
Normal file
89
internal/router/routes.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// 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
|
||||
}
|
||||
64
internal/router/routes_test.go
Normal file
64
internal/router/routes_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/barkstack/pawsql/internal/config"
|
||||
)
|
||||
|
||||
func TestStaticResolverRoutesCaseInsensitiveHostnames(t *testing.T) {
|
||||
resolver, err := NewStaticResolver([]config.DatabaseConfig{
|
||||
{Name: "foo", Hostname: "foo.pawsql.barkstack.dev", Upstream: "postgres-foo:5432"},
|
||||
{Name: "bar", Hostname: "bar.pawsql.barkstack.dev", Upstream: "postgres-bar:5432"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewStaticResolver() error = %v", err)
|
||||
}
|
||||
for _, test := range []struct{ hostname, name, address string }{
|
||||
{"FOO.pawsql.barkstack.dev", "foo", "postgres-foo:5432"},
|
||||
{"bar.pawsql.barkstack.dev", "bar", "postgres-bar:5432"},
|
||||
} {
|
||||
backend, err := resolver.Resolve(context.Background(), test.hostname)
|
||||
if err != nil {
|
||||
t.Errorf("Resolve(%q) error = %v", test.hostname, err)
|
||||
continue
|
||||
}
|
||||
if backend.DatabaseName != test.name || backend.Address != test.address {
|
||||
t.Errorf("Resolve(%q) = %#v", test.hostname, backend)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticResolverRejectsUnknownHostname(t *testing.T) {
|
||||
resolver, err := NewStaticResolver([]config.DatabaseConfig{{Name: "foo", Hostname: "foo.pawsql.barkstack.dev", Upstream: "postgres-foo:5432"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = resolver.Resolve(context.Background(), "unknown.pawsql.barkstack.dev")
|
||||
if !errors.Is(err, ErrUnknownHostname) {
|
||||
t.Errorf("Resolve() error = %v, want ErrUnknownHostname", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticResolverRoutesConfiguredDatabase(t *testing.T) {
|
||||
resolver, err := NewStaticResolver([]config.DatabaseConfig{
|
||||
{Name: "analytics", Upstream: "postgres-shared:5432"},
|
||||
{Name: "app", Hostname: "app.pawsql.barkstack.dev", Upstream: "postgres-shared:5432"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backend, err := resolver.ResolveDatabase(context.Background(), "analytics")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if backend.DatabaseName != "analytics" || backend.Address != "postgres-shared:5432" {
|
||||
t.Errorf("ResolveDatabase() = %#v", backend)
|
||||
}
|
||||
_, err = resolver.ResolveDatabase(context.Background(), "unknown")
|
||||
if !errors.Is(err, ErrUnknownDatabase) {
|
||||
t.Errorf("ResolveDatabase() error = %v, want ErrUnknownDatabase", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user