Add shared postgres client package#111
Conversation
jhrozek
left a comment
There was a problem hiding this comment.
Batched review of the postgres/ package. Six findings — two HIGH (DSN injection / pool tuning naming), two MEDIUM (auth-hook composition / SSL default), two LOW (dead nil-check / stability marker). None blocking, but recommend addressing the HIGH and MEDIUM before tagging a release. Test coverage at 82.5% and lint clean.
The previous NewPool path formatted a libpq URL with fmt.Sprintf and passed it to pgxpool.ParseConfig. url.UserPassword only escapes the userinfo segment, so a Host containing "@" shifted authority parsing (url.Parse uses the last "@" as the userinfo delimiter, so "good.rds@evil.example.com" resolved to "evil.example.com") and a Database containing "?" introduced a second query section that could silently override sslmode. Switch NewPool to a hybrid approach: bootstrap pgxpool.Config from a minimal "postgres://localhost?sslmode=X" DSN so pgx still translates the SSL mode into a *tls.Config, then assign Host/Port/User/Password/Database structurally onto the resulting ConnConfig. Caller-controlled fields never enter url.Parse this way. Add defense in depth in Config.Validate by rejecting Host containing @/?/# or whitespace and Database containing ?/#/ or whitespace. Tighten the Port check to require 1..65535 so the uint16 conversion in buildPoolConfig has a real invariant behind it. The connection-string methods stay public for callers that hand a DSN to a migration tool; they are now safe by virtue of Validate rejecting the dangerous shapes at the boundary. Addresses code review feedback on #111. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In database/sql, MaxIdleConns is a ceiling on connections sitting idle. In pgxpool, MinConns is a floor — the pool actively keeps that many connections open even when the application is quiet. Mapping MaxIdleConns to pgxpool.MinConns meant a developer setting MaxIdleConns: 50 was opening 50 permanent connections, the opposite of what the field name implied. Rename the field to MinConns and add a doc comment that points out the database/sql contrast for readers carrying that mental model. Since this package is still Alpha (per Stability Guarantees in CLAUDE.md), the break is cheap now and disappears later. Addresses code review feedback on #111. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the early return on AWSRDSIAM == nil, the subsequent AWSRDSIAM != nil check is always true. staticcheck flags this as SA4031. Addresses code review feedback on #111. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Declare stability via a top-of-file `// Stability: Alpha` marker on doc.go, mirroring the convention documented in CLAUDE.md. Add a row to the Available Packages table in README.md and the Current Packages table in CLAUDE.md so the package is discoverable through the same indexes consumers already use. Addresses code review feedback on #111. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed 4 commits addressing the HIGH and LOW findings. The two MEDIUMs are still open — counter-proposals below. Addressed
Open — pushing back on both MEDIUMs[MEDIUM] [MEDIUM]
Proposal: keep 🤖 Generated with Claude Code |
Introduce a postgres/ package that wraps github.com/jackc/pgx/v5/pgxpool with a Config type, a NewPool factory, and a dynamic-authentication dispatcher. The package is the consolidation point for PostgreSQL connection plumbing currently duplicated across consumers (toolhive-registry-server today, with additional consumers planned). Schema management - migrations, sqlc bindings, application-specific type codecs - is intentionally left to each caller. The public surface mirrors the redis/ package: a single validated Config including pool tuning knobs and a DynamicAuth block, plus a NewPool constructor that auto-installs a BeforeConnect hook when DynamicAuth is configured. WithAfterConnect lets callers register custom pgx type codecs (for example, application-defined enum array codecs) without leaking those schema concerns into this package. NewAuthToken is exported for short-lived connections - migrations, in particular - where a pool hook is not available. AWS RDS IAM is the first dynamic-auth backend. Region "detect" triggers IMDS-based discovery. The auth dispatcher is structured so additional backends (Vault, GCP IAM) can be added without changing the call sites in NewPool or NewAuthToken. Config implements slog.LogValuer to redact password fields and reports only presence-indicators (has_password, has_migration_password, dynamic_auth) for credentials. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous NewPool path formatted a libpq URL with fmt.Sprintf and passed it to pgxpool.ParseConfig. url.UserPassword only escapes the userinfo segment, so a Host containing "@" shifted authority parsing (url.Parse uses the last "@" as the userinfo delimiter, so "good.rds@evil.example.com" resolved to "evil.example.com") and a Database containing "?" introduced a second query section that could silently override sslmode. Switch NewPool to a hybrid approach: bootstrap pgxpool.Config from a minimal "postgres://localhost?sslmode=X" DSN so pgx still translates the SSL mode into a *tls.Config, then assign Host/Port/User/Password/Database structurally onto the resulting ConnConfig. Caller-controlled fields never enter url.Parse this way. Add defense in depth in Config.Validate by rejecting Host containing @/?/# or whitespace and Database containing ?/#/ or whitespace. Tighten the Port check to require 1..65535 so the uint16 conversion in buildPoolConfig has a real invariant behind it. The connection-string methods stay public for callers that hand a DSN to a migration tool; they are now safe by virtue of Validate rejecting the dangerous shapes at the boundary. Addresses code review feedback on #111. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In database/sql, MaxIdleConns is a ceiling on connections sitting idle. In pgxpool, MinConns is a floor — the pool actively keeps that many connections open even when the application is quiet. Mapping MaxIdleConns to pgxpool.MinConns meant a developer setting MaxIdleConns: 50 was opening 50 permanent connections, the opposite of what the field name implied. Rename the field to MinConns and add a doc comment that points out the database/sql contrast for readers carrying that mental model. Since this package is still Alpha (per Stability Guarantees in CLAUDE.md), the break is cheap now and disappears later. Addresses code review feedback on #111. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the early return on AWSRDSIAM == nil, the subsequent AWSRDSIAM != nil check is always true. staticcheck flags this as SA4031. Addresses code review feedback on #111. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Declare stability via a top-of-file `// Stability: Alpha` marker on doc.go, mirroring the convention documented in CLAUDE.md. Add a row to the Available Packages table in README.md and the Current Packages table in CLAUDE.md so the package is discoverable through the same indexes consumers already use. Addresses code review feedback on #111. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13ca610 to
677615b
Compare
Previously NewPool silently let WithBeforeConnect win when cfg.DynamicAuth was also set — a documented footgun whose failure mode is production tokens expiring ~15 minutes after deploy. Compose-by-default was considered but rejected: both hooks write to conn.Password, so composition order is itself a surprise. Reject the combination at construction time instead. Callers that genuinely want to layer logic on top of dynamic auth can call NewDynamicAuthFunc, compose the two hooks themselves, and pass the composition via WithBeforeConnect with cfg.DynamicAuth left nil. The ambiguity goes away and the failure mode is loud at startup rather than silent in production. Addresses code review feedback on #111. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The existing comment overstated the protection of "require" by calling it "the safe production default" — sslmode=require encrypts the connection but does not verify the server certificate against a CA, so it does not defend against an active attacker who can intercept TCP. Flipping the default to "verify-full" was considered and rejected: it would silently break self-signed and dev environments on first run, and registry-server today relies on the "require" default. Drop the "safe production default" wording on DefaultSSLMode and add a TLS and SSL Mode section to the package overview that explains the tradeoff and points production deployments at "verify-full" with a CA bundle. Also update the Hooks section to describe the WithBeforeConnect/DynamicAuth mutual-exclusion that was introduced in the previous commit. Addresses code review feedback on #111. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed both MEDIUM fixes — but didn't take the reviewer's suggested shape on either; took the alternatives from my earlier reply instead.
Happy to revisit either if the reasoning doesn't land — open to a compose-by-default 🤖 Generated with Claude Code |
Summary
Introduces a
postgres/package mirroringredis/: a validatedConfig, aNewPoolfactory that wrapspgxpool, and a dynamic-authentication dispatcher with AWS RDS IAM as the first backend.This is the consolidation point for PostgreSQL connection plumbing currently duplicated in toolhive-registry-server (
internal/app/storage/database_factory.go+internal/app/storage/auth/). Additional consumers planned. Schema concerns — migrations, sqlc bindings, application-specific type codecs — stay in each caller.Public surface
postgres.Config— pool tuning, app + migration user,DynamicAuthblock. Implementsslog.LogValuerto redact credentials.postgres.NewPool(ctx, *Config, ...Option)— auto-installs aBeforeConnectdynamic-auth hook whenDynamicAuthis set.WithAfterConnect(for custom type codecs),WithBeforeConnect(escape hatch),WithLogger.postgres.NewAuthToken/postgres.NewDynamicAuthFunc— token / hook constructors for callers that need them outsideNewPool(e.g. golang-migrate's one-shot connection).postgres.StartPoolStatsLogger— opt-in periodic pool-stats emitter.postgres.DynamicAuthAWSRDSIAM—Region: "detect"discovers region via IMDS; static regions are used as-is.Design notes
*backend*.gofile and a new dispatcher branch — no API churn at the call sites.PasswordandDynamicAuthat the library boundary. Some callers want a local-dev static fallback. Mutual exclusion belongs in the consumer's config-validation layer (per registry-server's.claude/rules/secrets.md§4).pgxpool.NewWithConfigis lazy.NewPoolreturns without verifying connectivity; dial errors surface on first acquire, matching pgxpool's design.Test plan
task lint— 0 issuestask test— all packages pass under-racetask license-check— clean🤖 Generated with Claude Code