diff --git a/postgres_pgx/README.md b/postgres_pgx/README.md new file mode 100644 index 0000000..461b663 --- /dev/null +++ b/postgres_pgx/README.md @@ -0,0 +1,120 @@ +# Postgres PGX Plugin + +A Go plugin for PostgreSQL database management using PGX driver with support for connection pooling, migrations, and test containers. + +## Features + +- Connection pooling with PGX +- Database migrations support +- Test containers integration +- Health checks (readiness and liveness probes) +- Configurable connection parameters +- Automatic database creation for tests + +## Installation +```bash +go get github.com/lastbackend/toolkit-plugins/postgres_pgx +``` + +## Usage + +### Basic Usage +```go +import "github.com/lastbackend/toolkit-plugins/postgres_pgx" + +func main() { + // Initialize runtime (from your toolkit) + rt := runtime.New() + + // Create plugin + pg := postgres_pgx.NewPlugin(rt, &postgres_pgx.Options{ + Name: "my-postgres", + }) + + // Get database connection pool + db := pg.DB() + + // Use the database + // ... +} +``` + +### Configuration + +Configuration can be provided via environment variables: + +```env +PSQL_DSN=postgresql://user:pass@localhost:5432/dbname +# or individual parameters +PSQL_HOST=localhost +PSQL_PORT=5432 +PSQL_DATABASE=dbname +PSQL_USERNAME=user +PSQL_PASSWORD=pass +PSQL_SSLMODE=disable +PSQL_MAX_POOL_SIZE=2 +PSQL_CONN_ATTEMPTS=10 +PSQL_CONN_TIMEOUT=15s +PSQL_MIGRATIONS_DIR=/path/to/migrations +``` + +### Running Migrations +```go +if err := pg.RunMigration(); err != nil { + log.Fatal(err) +} +``` + +### Testing with Containers +```go +ctx := context.Background() + +pg, err := postgres_pgx.NewTestPlugin(ctx, postgres_pgx.TestConfig{ + Config: postgres_pgx.Config{ + Database: "testdb", + }, + RunContainer: true, + // Optional container configuration + ContainerImage: "postgres:15.2", + ContainerName: "my-test-postgres", + MacConnections: 100, +}) +if err != nil { + log.Fatal(err) +} +// Use pg for testing +``` + +## Configuration Options + +### Plugin Options + +| Option | Description | +|--------|-------------| +| Name | Plugin name prefix for configuration | + +### Database Config + +| Parameter | Environment Variable | Default | Description | +|-----------|---------------------|---------|-------------| +| DSN | PSQL_DSN | "" | Complete connection string | +| Host | PSQL_HOST | "127.0.0.1" | Database host | +| Port | PSQL_PORT | 5432 | Database port | +| Database | PSQL_DATABASE | "postgres" | Database name | +| Username | PSQL_USERNAME | "postgres" | Database user | +| Password | PSQL_PASSWORD | "" | Database password | +| SSLMode | PSQL_SSLMODE | "disable" | SSL mode | +| MaxPoolSize | PSQL_MAX_POOL_SIZE | 2 | Connection pool size | +| ConnAttempts | PSQL_CONN_ATTEMPTS | 10 | Connection retry attempts | +| ConnTimeout | PSQL_CONN_TIMEOUT | "15s" | Connection timeout | +| MigrationsDir | PSQL_MIGRATIONS_DIR | "" | Migrations directory path | + +### Test Config + +| Parameter | Description | +|-----------|-------------| +| RunContainer | Whether to run a test container | +| ContainerImage | Custom PostgreSQL image | +| ContainerName | Custom container name | +| MacConnections | Max connections limit | + diff --git a/postgres_pgx/go.mod b/postgres_pgx/go.mod index 7e12955..f4d8e3f 100644 --- a/postgres_pgx/go.mod +++ b/postgres_pgx/go.mod @@ -8,6 +8,8 @@ require ( github.com/lastbackend/toolkit v0.0.0-20240115154229-332ae80d9328 github.com/lib/pq v1.10.9 github.com/pkg/errors v0.9.1 + github.com/testcontainers/testcontainers-go v0.29.1 + github.com/testcontainers/testcontainers-go/modules/postgres v0.29.1 ) require ( @@ -59,8 +61,6 @@ require ( github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/testcontainers/testcontainers-go v0.29.1 // indirect - github.com/testcontainers/testcontainers-go/modules/postgres v0.29.1 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect diff --git a/postgres_pgx/go.sum b/postgres_pgx/go.sum index 068dc9a..b9abd51 100644 --- a/postgres_pgx/go.sum +++ b/postgres_pgx/go.sum @@ -1,5 +1,7 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -24,6 +26,8 @@ github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7 github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -31,14 +35,8 @@ github.com/dhui/dktest v0.4.0 h1:z05UmuXZHO/bgj/ds2bGMBu8FI4WA+Ag/m3ghL+om7M= github.com/dhui/dktest v0.4.0/go.mod h1:v/Dbz1LgCBOi2Uki2nUqLBGa83hWBGFMu5MrgMDCc78= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= -github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= -github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v25.0.3+incompatible h1:D5fy/lYmY7bvZa0XTZ5/UJPljor41F+vdyJG5luQLfQ= github.com/docker/docker v25.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -66,8 +64,8 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -76,6 +74,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -174,8 +174,6 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -194,6 +192,7 @@ github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11 github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= @@ -215,10 +214,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/testcontainers/testcontainers-go v0.29.1 h1:z8kxdFlovA2y97RWx98v/TQ+tR+SXZm6p35M+xB92zk= github.com/testcontainers/testcontainers-go v0.29.1/go.mod h1:SnKnKQav8UcgtKqjp/AD8bE1MqZm+3TDb/B8crE3XnI= github.com/testcontainers/testcontainers-go/modules/postgres v0.29.1 h1:hTn3MzhR9w4btwfzr/NborGCaeNZG0MPBpufeDj10KA= @@ -237,10 +235,18 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q= go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -283,8 +289,6 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= -golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -342,6 +346,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -354,8 +360,6 @@ golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= -golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -366,6 +370,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 h1:/IWabOtPziuXTEtI1KYCpM6Ss7vaAkeMxk+uXV/xvZs= google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= +google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 h1:OPXtXn7fNMaXwO3JvOmF1QyTc00jsSFFz1vXXBOdCDo= +google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg= google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU= google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= @@ -382,4 +388,6 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= +gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/postgres_pgx/plugin.go b/postgres_pgx/plugin.go index fde3504..5babbd7 100755 --- a/postgres_pgx/plugin.go +++ b/postgres_pgx/plugin.go @@ -5,6 +5,7 @@ import ( "database/sql" "fmt" "log" + "strings" "time" "github.com/golang-migrate/migrate/v4" @@ -29,38 +30,70 @@ const ( defaultContainerName = "postgres-test-container" ) +// Plugin represents a PostgreSQL database plugin interface type Plugin interface { + // DB returns the database connection pool DB() *pgxpool.Pool + // RunMigration executes database migrations from the configured directory RunMigration() error } +// Options contains plugin initialization options type Options struct { + // Name is the prefix used for configuration variables Name string } +// Config defines the database configuration parameters type Config struct { DSN string `env:"DSN" envDefault:"" comment:"DSN = postgresql://[user[:password]@][netloc][:port][/dbname][?param1=value1&...] complete connection string"` Host string `env:"HOST" envDefault:"127.0.0.1" comment:"The host to connect to"` Port int32 `env:"PORT" envDefault:"5432" comment:"The port to connect to"` - Database string `env:"DATABASE" comment:"Database name"` - Username string `env:"USERNAME" comment:"The username to connect with"` - Password string `env:"PASSWORD" comment:"The password to connect with"` - SSLMode string `env:"SSLMODE" comment:"SSL mode (disable, allow, prefer, require, verify-ca, verify-full)"` + Database string `env:"DATABASE" envDefault:"postgres" comment:"Database name"` + Username string `env:"USERNAME" envDefault:"postgres" comment:"The username to connect with"` + Password string `env:"PASSWORD" envDefault:"" comment:"The password to connect with"` + SSLMode string `env:"SSLMODE" envDefault:"disable" comment:"SSL mode (disable, allow, prefer, require, verify-ca, verify-full)"` MaxPoolSize int `env:"MAX_POOL_SIZE" envDefault:"2" comment:"Max pool size"` ConnAttempts int `env:"CONN_ATTEMPTS" envDefault:"10" comment:"Connection attempts"` ConnTimeout time.Duration `env:"CONN_TIMEOUT" envDefault:"15s" comment:"Connection timeout"` MigrationsDir string `env:"MIGRATIONS_DIR" comment:"Migrations directory"` } +// TestConfig extends Config with additional testing-specific options type TestConfig struct { Config - RunContainer bool + // RunContainer indicates whether to start a test container + RunContainer bool + // ContainerImage specifies custom PostgreSQL image for test container ContainerImage string - ContainerName string + // ContainerName sets custom name for test container + ContainerName string + // MacConnections limits maximum number of connections MacConnections int } +// PostgresContainer defines interface for database container management +type PostgresContainer interface { + GetDSN(ctx context.Context) (string, error) + Close(ctx context.Context) error +} + +// connConfig holds database connection parameters +type connConfig struct { + host string + port string + user string + password string + dbName string + sslMode string +} + +func (c *connConfig) toDSN() string { + return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s", + c.user, c.password, c.host, c.port, c.dbName, c.sslMode) +} + type plugin struct { runtime runtime.Runtime opts Config @@ -68,6 +101,7 @@ type plugin struct { prefix string } +// NewPlugin creates a new instance of the PostgreSQL plugin func NewPlugin(runtime runtime.Runtime, opts *Options) Plugin { p := &plugin{ runtime: runtime, @@ -85,6 +119,7 @@ func NewPlugin(runtime runtime.Runtime, opts *Options) Plugin { return p } +// NewTestPlugin creates a plugin instance configured for testing func NewTestPlugin(ctx context.Context, cfg TestConfig) (Plugin, error) { if cfg.DSN == "" && !cfg.RunContainer { if cfg.Host == "" { @@ -98,6 +133,14 @@ func NewTestPlugin(ctx context.Context, cfg TestConfig) (Plugin, error) { cfg.MaxPoolSize = 1 } + // Set default connection attempts and timeout if not specified + if cfg.ConnAttempts == 0 { + cfg.ConnAttempts = 10 + } + if cfg.ConnTimeout == 0 { + cfg.ConnTimeout = 15 * time.Second + } + if cfg.RunContainer { dbURL, err := runPostgresContainer(ctx, cfg) if err != nil { @@ -114,62 +157,104 @@ func NewTestPlugin(ctx context.Context, cfg TestConfig) (Plugin, error) { return p, nil } +// runPostgresContainer starts a PostgreSQL container for testing func runPostgresContainer(ctx context.Context, cfg TestConfig) (string, error) { image := getImage(cfg.ContainerImage) containerName := getContainerName(cfg.ContainerName) - cmd := fmt.Sprintf("-N %d", getMaxConnections(cfg.MacConnections)) + + containerReq := testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Name: containerName, + Image: image, + ExposedPorts: []string{"5432/tcp"}, + Cmd: []string{fmt.Sprintf("-N %d", getMaxConnections(cfg.MacConnections))}, + WaitingFor: wait.ForLog("database system is ready to accept connections"). + WithOccurrence(2). + WithStartupTimeout(5 * time.Second), + }, + Reuse: true, + } container, err := postgres.RunContainer(ctx, - testcontainers.WithImage(image), + testcontainers.CustomizeRequest(containerReq), postgres.WithDatabase("postgres"), postgres.WithUsername("user"), postgres.WithPassword("pass"), - testcontainers.WithWaitStrategy(wait.ForLog("database system is ready to accept connections").WithOccurrence(2).WithStartupTimeout(5*time.Second)), - testcontainers.CustomizeRequest(testcontainers.GenericContainerRequest{ - ContainerRequest: testcontainers.ContainerRequest{ - Name: containerName, - Cmd: []string{cmd}, - }, - Reuse: true, - }), ) if err != nil { - return "", err + return "", fmt.Errorf("failed to start postgres container: %w", err) } - host, err := container.Host(ctx) + cleanup := func() { + if err := container.Terminate(context.Background()); err != nil { + log.Printf("failed to terminate container: %v", err) + } + } + + // Handle cleanup on context cancellation + go func() { + <-ctx.Done() + cleanup() + }() + + connCfg, err := getConnectionConfig(ctx, container, cfg.Database) if err != nil { - return "", err + cleanup() + return "", fmt.Errorf("failed to get connection config: %w", err) } - port, err := container.MappedPort(ctx, "5432") + + if err := ensureDatabaseExists(ctx, connCfg.toDSN(), cfg.Database); err != nil { + cleanup() + return "", fmt.Errorf("failed to ensure database exists: %w", err) + } + + return connCfg.toDSN(), nil +} + +// getConnectionConfig retrieves connection parameters from container +func getConnectionConfig(ctx context.Context, container *postgres.PostgresContainer, dbName string) (*connConfig, error) { + host, err := container.Host(ctx) if err != nil { - return "", err + return nil, fmt.Errorf("failed to get container host: %w", err) } - dbURL := fmt.Sprintf("postgres://user:pass@%s:%s/postgres?sslmode=disable", host, port.Port()) - if err := ensureDatabaseExists(ctx, dbURL, cfg.Database); err != nil { - return "", err + port, err := container.MappedPort(ctx, "5432") + if err != nil { + return nil, fmt.Errorf("failed to get container port: %w", err) } - return fmt.Sprintf("postgres://user:pass@%s:%s/%s?sslmode=disable", host, port.Port(), cfg.Database), nil + return &connConfig{ + host: host, + port: port.Port(), + user: "user", + password: "pass", + dbName: dbName, + sslMode: "disable", + }, nil } +// ensureDatabaseExists checks if database exists and creates it if necessary func ensureDatabaseExists(ctx context.Context, dbURL, dbName string) error { - pool, err := pgxpool.New(ctx, dbURL) + // Connect to default postgres database first + host, port := getHostPort(dbURL) + postgresURL := fmt.Sprintf("postgres://user:pass@%s:%s/postgres?sslmode=disable", host, port) + + pool, err := pgxpool.New(ctx, postgresURL) if err != nil { - return err + return fmt.Errorf("failed to connect to postgres database: %w", err) } defer pool.Close() exists, err := databaseExists(ctx, pool, dbName) if err != nil { - return err + return fmt.Errorf("failed to check if database exists: %w", err) } if !exists { + // Create database if it doesn't exist _, err = pool.Exec(ctx, fmt.Sprintf("CREATE DATABASE %s", dbName)) if err != nil { - return err + return fmt.Errorf("failed to create database: %w", err) } fmt.Printf("Database %s created successfully\n", dbName) } else { @@ -179,6 +264,22 @@ func ensureDatabaseExists(ctx context.Context, dbURL, dbName string) error { return nil } +// Helper function to extract host and port from DSN +func getHostPort(dsn string) (host, port string) { + // Parse DSN to extract host and port + parts := strings.Split(dsn, "@") + if len(parts) != 2 { + return "", "" + } + hostPart := strings.Split(parts[1], "/")[0] + hostPortParts := strings.Split(hostPart, ":") + if len(hostPortParts) != 2 { + return "", "" + } + return hostPortParts[0], hostPortParts[1] +} + +// databaseExists checks if a database with given name exists func databaseExists(ctx context.Context, pool *pgxpool.Pool, dbName string) (bool, error) { var exists bool err := pool.QueryRow(ctx, "SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname=$1)", dbName).Scan(&exists) @@ -189,6 +290,7 @@ func (p *plugin) DB() *pgxpool.Pool { return p.pool } +// PreStart prepares the plugin before application start func (p *plugin) PreStart(ctx context.Context) error { if p.opts.DSN == "" { p.opts.DSN = fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s", @@ -205,6 +307,7 @@ func (p *plugin) PreStart(ctx context.Context) error { return nil } +// OnStop performs cleanup when application stops func (p *plugin) OnStop(ctx context.Context) error { if p.pool != nil { p.pool.Close() @@ -212,6 +315,7 @@ func (p *plugin) OnStop(ctx context.Context) error { return nil } +// PostgresPingChecker creates a health check function for the database func PostgresPingChecker(pool *pgxpool.Pool, timeout time.Duration) probes.HandleFunc { return func() error { ctx, cancel := context.WithTimeout(context.Background(), timeout) @@ -256,25 +360,43 @@ func (p *plugin) RunMigration() error { return nil } +// initPlugin initializes database connection pool with retries func (p *plugin) initPlugin(ctx context.Context) error { if p.pool != nil { return nil } - var err error - for i := 0; i < p.opts.ConnAttempts; i++ { - p.pool, err = pgxpool.New(ctx, p.opts.DSN) + config, err := pgxpool.ParseConfig(p.opts.DSN) + if err != nil { + return fmt.Errorf("failed to parse connection string: %w", err) + } + + // Set pool configuration + config.MaxConns = int32(p.opts.MaxPoolSize) + config.ConnConfig.ConnectTimeout = p.opts.ConnTimeout + + var lastErr error + for attempt := 1; attempt <= p.opts.ConnAttempts; attempt++ { + p.pool, err = pgxpool.NewWithConfig(ctx, config) if err == nil { return nil } - log.Printf("Connection attempt %d/%d failed: %v", i+1, p.opts.ConnAttempts, err) - time.Sleep(p.opts.ConnTimeout) + lastErr = err + log.Printf("Connection attempt %d/%d failed: %v", attempt, p.opts.ConnAttempts, err) + + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(p.opts.ConnTimeout): + continue + } } - return fmt.Errorf("failed to connect to the database after %d attempts: %v", p.opts.ConnAttempts, err) + return fmt.Errorf("failed to connect after %d attempts: %w", p.opts.ConnAttempts, lastErr) } +// getImage returns container image name with fallback to default func getImage(image string) string { if image == "" { return defaultImage @@ -282,6 +404,7 @@ func getImage(image string) string { return image } +// getContainerName returns container name with fallback to default func getContainerName(name string) string { if name == "" { return defaultContainerName @@ -289,6 +412,7 @@ func getContainerName(name string) string { return name } +// getMaxConnections returns max connections number with fallback func getMaxConnections(connections int) int { if connections <= 0 { return 100