feat: read managed database passwords from secrets
This commit is contained in:
@@ -9,10 +9,11 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
config "cloud.campbellwireless.net/git/barkstack/barkfile-parser"
|
||||
config "cloud.campbellwireless.net/git/barkstack/barkfile-parser/v2"
|
||||
)
|
||||
|
||||
const managedDatabaseLabel = "io.barkstack.pawsql.database"
|
||||
@@ -20,8 +21,9 @@ const managedDatabaseLabel = "io.barkstack.pawsql.database"
|
||||
// Provisioner ensures configured PostgreSQL containers exist and exposes their
|
||||
// loopback-published PostgreSQL address to PawSQL's static router.
|
||||
type Provisioner struct {
|
||||
DockerPath string
|
||||
Logger *slog.Logger
|
||||
DockerPath string
|
||||
SecretDirectory string
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewProvisioner creates a Docker CLI-backed provisioner.
|
||||
@@ -29,7 +31,7 @@ func NewProvisioner(logger *slog.Logger) *Provisioner {
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
}
|
||||
return &Provisioner{DockerPath: "docker", Logger: logger}
|
||||
return &Provisioner{DockerPath: "docker", SecretDirectory: "/run/secrets", Logger: logger}
|
||||
}
|
||||
|
||||
// Ensure provisions each PostgreSQL-backed database and returns configuration
|
||||
@@ -65,13 +67,13 @@ func (p *Provisioner) EnsureDatabase(ctx context.Context, database string, postg
|
||||
return "", err
|
||||
}
|
||||
if !exists {
|
||||
password, ok := os.LookupEnv(postgres.PasswordEnv)
|
||||
if !ok || password == "" {
|
||||
return "", fmt.Errorf("environment variable %q is required to create the container", postgres.PasswordEnv)
|
||||
password, err := p.readSecret(postgres.PasswordSecret)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
p.Logger.Info("creating PostgreSQL container", "database", database, "container", name, "image", postgres.Image, "volume", postgres.Volume)
|
||||
mount := postgresDataMount(postgres.Image)
|
||||
_, err := p.run(ctx, []string{"POSTGRES_PASSWORD=" + password},
|
||||
_, err = p.run(ctx, []string{"POSTGRES_PASSWORD=" + password},
|
||||
"container", "create",
|
||||
"--name", name,
|
||||
"--label", managedDatabaseLabel+"="+database,
|
||||
@@ -110,6 +112,26 @@ func (p *Provisioner) EnsureDatabase(ctx context.Context, database string, postg
|
||||
return address, nil
|
||||
}
|
||||
|
||||
func (p *Provisioner) readSecret(reference string) (string, error) {
|
||||
if !config.ValidSecretReference(reference) {
|
||||
return "", fmt.Errorf("invalid password secret reference %q", reference)
|
||||
}
|
||||
directory := p.SecretDirectory
|
||||
if directory == "" {
|
||||
directory = "/run/secrets"
|
||||
}
|
||||
name := config.DockerSecretName(reference)
|
||||
contents, err := os.ReadFile(filepath.Join(directory, name))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read Docker secret %q: %w", name, err)
|
||||
}
|
||||
password := strings.TrimRight(string(contents), "\r\n")
|
||||
if password == "" {
|
||||
return "", fmt.Errorf("Docker secret %q is empty", name)
|
||||
}
|
||||
return password, nil
|
||||
}
|
||||
|
||||
// StopDatabase stops a managed PostgreSQL container without removing its data volume.
|
||||
func (p *Provisioner) StopDatabase(ctx context.Context, database string) error {
|
||||
name := containerName(database)
|
||||
|
||||
@@ -5,20 +5,26 @@ package postgres
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
config "cloud.campbellwireless.net/git/barkstack/barkfile-parser"
|
||||
config "cloud.campbellwireless.net/git/barkstack/barkfile-parser/v2"
|
||||
)
|
||||
|
||||
func TestProvisionerCreatesPersistentPostgres18(t *testing.T) {
|
||||
const (
|
||||
database = "pawsql_integration_test"
|
||||
volume = "pawsql-integration-test-data"
|
||||
envName = "PAWSQL_INTEGRATION_POSTGRES_PASSWORD"
|
||||
database = "pawsql_integration_test"
|
||||
volume = "pawsql-integration-test-data"
|
||||
secretRef = "integration_postgres_password"
|
||||
)
|
||||
t.Setenv(envName, "integration-test-password")
|
||||
secretDirectory := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(secretDirectory, config.DockerSecretName(secretRef)), []byte("integration-test-password"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
provisioner := NewProvisioner(nil)
|
||||
provisioner.SecretDirectory = secretDirectory
|
||||
defer func() {
|
||||
_, _ = provisioner.run(context.Background(), nil, "container", "rm", "--force", containerName(database))
|
||||
_, _ = provisioner.run(context.Background(), nil, "volume", "rm", "--force", volume)
|
||||
@@ -27,9 +33,9 @@ func TestProvisionerCreatesPersistentPostgres18(t *testing.T) {
|
||||
cfg := config.Config{Databases: []config.DatabaseConfig{{
|
||||
Name: database,
|
||||
Postgres: &config.PostgresConfig{
|
||||
Image: "postgres:18",
|
||||
Volume: volume,
|
||||
PasswordEnv: envName,
|
||||
Image: "postgres:18",
|
||||
Volume: volume,
|
||||
PasswordSecret: secretRef,
|
||||
},
|
||||
}}}
|
||||
resolved, err := provisioner.Ensure(context.Background(), cfg)
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
package postgres
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
config "cloud.campbellwireless.net/git/barkstack/barkfile-parser/v2"
|
||||
)
|
||||
|
||||
func TestPostgresDataMount(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -17,3 +24,41 @@ func TestPostgresDataMount(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadSecretUsesPrefixedDockerName(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
path := filepath.Join(directory, config.DockerSecretName("application_password"))
|
||||
if err := os.WriteFile(path, []byte("correct horse battery staple\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
provisioner := &Provisioner{SecretDirectory: directory}
|
||||
password, err := provisioner.readSecret("application_password")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if password != "correct horse battery staple" {
|
||||
t.Fatalf("password = %q", password)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadSecretRejectsUnsafeReference(t *testing.T) {
|
||||
provisioner := &Provisioner{SecretDirectory: t.TempDir()}
|
||||
_, err := provisioner.readSecret("../../etc/passwd")
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid password secret reference") {
|
||||
t.Fatalf("readSecret() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadSecretRejectsMissingAndEmptySecret(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
provisioner := &Provisioner{SecretDirectory: directory}
|
||||
if _, err := provisioner.readSecret("missing"); err == nil || !strings.Contains(err.Error(), "barkstack_missing") {
|
||||
t.Fatalf("missing secret error = %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(directory, config.DockerSecretName("empty")), []byte("\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := provisioner.readSecret("empty"); err == nil || !strings.Contains(err.Error(), "is empty") {
|
||||
t.Fatalf("empty secret error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
config "cloud.campbellwireless.net/git/barkstack/barkfile-parser"
|
||||
config "cloud.campbellwireless.net/git/barkstack/barkfile-parser/v2"
|
||||
"github.com/barkstack/pawsql/internal/router"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
config "cloud.campbellwireless.net/git/barkstack/barkfile-parser"
|
||||
config "cloud.campbellwireless.net/git/barkstack/barkfile-parser/v2"
|
||||
"github.com/barkstack/pawsql/internal/router"
|
||||
)
|
||||
|
||||
func TestResolverDefersManagedDatabaseStartup(t *testing.T) {
|
||||
routes, err := router.NewStaticResolver([]config.DatabaseConfig{
|
||||
{Name: "external", Upstream: "192.0.2.1:5432"},
|
||||
{Name: "managed", Postgres: &config.PostgresConfig{Image: "postgres:18", Volume: "managed-data", PasswordEnv: "MANAGED_PASSWORD"}},
|
||||
{Name: "managed", Postgres: &config.PostgresConfig{Image: "postgres:18", Volume: "managed-data", PasswordSecret: "managed_password"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -21,7 +21,7 @@ func TestResolverDefersManagedDatabaseStartup(t *testing.T) {
|
||||
ensurer := &fakeEnsurer{addresses: map[string]string{"managed": "127.0.0.1:55432"}}
|
||||
resolver := NewResolver(routes, []config.DatabaseConfig{
|
||||
{Name: "external", Upstream: "192.0.2.1:5432"},
|
||||
{Name: "managed", Postgres: &config.PostgresConfig{Image: "postgres:18", Volume: "managed-data", PasswordEnv: "MANAGED_PASSWORD"}},
|
||||
{Name: "managed", Postgres: &config.PostgresConfig{Image: "postgres:18", Volume: "managed-data", PasswordSecret: "managed_password"}},
|
||||
}, ensurer)
|
||||
|
||||
backend, err := resolver.ResolveDatabase(context.Background(), "external")
|
||||
@@ -42,12 +42,12 @@ func TestResolverDefersManagedDatabaseStartup(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolverSerializesManagedStartup(t *testing.T) {
|
||||
routes, err := router.NewStaticResolver([]config.DatabaseConfig{{Name: "managed", Postgres: &config.PostgresConfig{Image: "postgres:18", Volume: "managed-data", PasswordEnv: "MANAGED_PASSWORD"}}})
|
||||
routes, err := router.NewStaticResolver([]config.DatabaseConfig{{Name: "managed", Postgres: &config.PostgresConfig{Image: "postgres:18", Volume: "managed-data", PasswordSecret: "managed_password"}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ensurer := &fakeEnsurer{addresses: map[string]string{"managed": "127.0.0.1:55432"}, gate: make(chan struct{}), started: make(chan struct{})}
|
||||
resolver := NewResolver(routes, []config.DatabaseConfig{{Name: "managed", Postgres: &config.PostgresConfig{Image: "postgres:18", Volume: "managed-data", PasswordEnv: "MANAGED_PASSWORD"}}}, ensurer)
|
||||
resolver := NewResolver(routes, []config.DatabaseConfig{{Name: "managed", Postgres: &config.PostgresConfig{Image: "postgres:18", Volume: "managed-data", PasswordSecret: "managed_password"}}}, ensurer)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for range 2 {
|
||||
@@ -69,12 +69,12 @@ func TestResolverSerializesManagedStartup(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolverStopsDatabaseAfterIdleTimeout(t *testing.T) {
|
||||
routes, err := router.NewStaticResolver([]config.DatabaseConfig{{Name: "managed", Postgres: &config.PostgresConfig{Image: "postgres:18", Volume: "managed-data", PasswordEnv: "MANAGED_PASSWORD", IdleTimeout: 20 * time.Millisecond}}})
|
||||
routes, err := router.NewStaticResolver([]config.DatabaseConfig{{Name: "managed", Postgres: &config.PostgresConfig{Image: "postgres:18", Volume: "managed-data", PasswordSecret: "managed_password", IdleTimeout: 20 * time.Millisecond}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
controller := &fakeEnsurer{addresses: map[string]string{"managed": "127.0.0.1:55432"}, stopped: make(chan string, 1)}
|
||||
resolver := NewResolver(routes, []config.DatabaseConfig{{Name: "managed", Postgres: &config.PostgresConfig{Image: "postgres:18", Volume: "managed-data", PasswordEnv: "MANAGED_PASSWORD", IdleTimeout: 20 * time.Millisecond}}}, controller)
|
||||
resolver := NewResolver(routes, []config.DatabaseConfig{{Name: "managed", Postgres: &config.PostgresConfig{Image: "postgres:18", Volume: "managed-data", PasswordSecret: "managed_password", IdleTimeout: 20 * time.Millisecond}}}, controller)
|
||||
|
||||
if _, err := resolver.ResolveDatabase(context.Background(), "managed"); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -91,12 +91,12 @@ func TestResolverStopsDatabaseAfterIdleTimeout(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolverCancelsIdleStopForNewConnection(t *testing.T) {
|
||||
routes, err := router.NewStaticResolver([]config.DatabaseConfig{{Name: "managed", Postgres: &config.PostgresConfig{Image: "postgres:18", Volume: "managed-data", PasswordEnv: "MANAGED_PASSWORD", IdleTimeout: 40 * time.Millisecond}}})
|
||||
routes, err := router.NewStaticResolver([]config.DatabaseConfig{{Name: "managed", Postgres: &config.PostgresConfig{Image: "postgres:18", Volume: "managed-data", PasswordSecret: "managed_password", IdleTimeout: 40 * time.Millisecond}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
controller := &fakeEnsurer{addresses: map[string]string{"managed": "127.0.0.1:55432"}, stopped: make(chan string, 1)}
|
||||
resolver := NewResolver(routes, []config.DatabaseConfig{{Name: "managed", Postgres: &config.PostgresConfig{Image: "postgres:18", Volume: "managed-data", PasswordEnv: "MANAGED_PASSWORD", IdleTimeout: 40 * time.Millisecond}}}, controller)
|
||||
resolver := NewResolver(routes, []config.DatabaseConfig{{Name: "managed", Postgres: &config.PostgresConfig{Image: "postgres:18", Volume: "managed-data", PasswordSecret: "managed_password", IdleTimeout: 40 * time.Millisecond}}}, controller)
|
||||
|
||||
if _, err := resolver.ResolveDatabase(context.Background(), "managed"); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -122,7 +122,7 @@ func TestResolverMetersTrafficAndStopsTrafficIdleSession(t *testing.T) {
|
||||
postgres := &config.PostgresConfig{
|
||||
Image: "postgres:18",
|
||||
Volume: "managed-data",
|
||||
PasswordEnv: "MANAGED_PASSWORD",
|
||||
PasswordSecret: "managed_password",
|
||||
TrafficIdleTimeout: 100 * time.Millisecond,
|
||||
}
|
||||
routes, err := router.NewStaticResolver([]config.DatabaseConfig{{Name: "managed", Postgres: postgres}})
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
config "cloud.campbellwireless.net/git/barkstack/barkfile-parser"
|
||||
config "cloud.campbellwireless.net/git/barkstack/barkfile-parser/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
config "cloud.campbellwireless.net/git/barkstack/barkfile-parser"
|
||||
config "cloud.campbellwireless.net/git/barkstack/barkfile-parser/v2"
|
||||
)
|
||||
|
||||
func TestStaticResolverRoutesCaseInsensitiveHostnames(t *testing.T) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
config "cloud.campbellwireless.net/git/barkstack/barkfile-parser"
|
||||
config "cloud.campbellwireless.net/git/barkstack/barkfile-parser/v2"
|
||||
"github.com/barkstack/pawsql/internal/pgwire"
|
||||
"github.com/barkstack/pawsql/internal/router"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user