Skip to content

Commit ac30564

Browse files
take-fivedavidspek
authored andcommitted
Support setting up proxy via provider attribute proxy_url
Previously, the only way to configure proxying requests to Postgres through a SOCKS5 proxy server was to set `ALL_PROXY` environment variable. However, this could have unintended side effects and impact other providers. With `proxy_url` provider attribute, users can configure proxy for each provider instance individually and make use of e.g. Terraform variables. It is also possible to use `PGPROXY` environment variable to configure SOCKS5 proxy for all instances of PostgreSQL provider in the current root module. This environment variable is also used in acceptance tests for this provider. Signed-off-by: David van der Spek <david.vanderspek@flyrlabs.com>
1 parent 25fbd1c commit ac30564

13 files changed

+121
-19
lines changed

postgresql/config.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ type Config struct {
183183
SSLClientCert *ClientCertificateConfig
184184
SSLRootCertPath string
185185
GCPIAMImpersonateServiceAccount string
186+
ProxyURL string
186187
}
187188

188189
// Client struct holding connection string
@@ -290,7 +291,10 @@ func (c *Client) Connect() (*DBConnection, error) {
290291
var db *sql.DB
291292
var err error
292293
if c.config.Scheme == "postgres" {
293-
db, err = sql.Open(proxyDriverName, dsn)
294+
db = sql.OpenDB(proxyConnector{
295+
dsn: dsn,
296+
proxyURL: c.config.ProxyURL,
297+
})
294298
} else if c.config.Scheme == "gcppostgres" && c.config.GCPIAMImpersonateServiceAccount != "" {
295299
db, err = openImpersonatedGCPDBConnection(context.Background(), dsn, c.config.GCPIAMImpersonateServiceAccount)
296300
} else {

postgresql/provider.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ package postgresql
33
import (
44
"context"
55
"fmt"
6+
"os"
7+
68
"github.com/aws/aws-sdk-go-v2/credentials"
79
"github.com/aws/aws-sdk-go-v2/service/sts"
8-
"os"
910

1011
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
1112
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
@@ -199,6 +200,12 @@ func Provider() *schema.Provider {
199200
Description: "Specify the expected version of PostgreSQL.",
200201
ValidateFunc: validateExpectedVersion,
201202
},
203+
"proxy_url": {
204+
Type: schema.TypeString,
205+
Optional: true,
206+
Description: "SOCKS5 proxy URL.",
207+
ValidateFunc: validation.IsURLWithScheme([]string{"socks5", "socks5h"}),
208+
},
202209
},
203210

204211
ResourcesMap: map[string]*schema.Resource{
@@ -382,6 +389,7 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
382389
ExpectedVersion: version,
383390
SSLRootCertPath: d.Get("sslrootcert").(string),
384391
GCPIAMImpersonateServiceAccount: d.Get("gcp_iam_impersonate_service_account").(string),
392+
ProxyURL: d.Get("proxy_url").(string),
385393
}
386394

387395
if value, ok := d.GetOk("clientcert"); ok {

postgresql/proxy_driver.go

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import (
44
"context"
55
"database/sql"
66
"database/sql/driver"
7+
"fmt"
78
"net"
9+
"net/url"
10+
"os"
811
"time"
912

1013
"github.com/lib/pq"
@@ -13,21 +16,87 @@ import (
1316

1417
const proxyDriverName = "postgresql-proxy"
1518

16-
type proxyDriver struct{}
19+
type proxyDriver struct {
20+
proxyURL string
21+
}
1722

1823
func (d proxyDriver) Open(name string) (driver.Conn, error) {
1924
return pq.DialOpen(d, name)
2025
}
2126

2227
func (d proxyDriver) Dial(network, address string) (net.Conn, error) {
23-
dialer := proxy.FromEnvironment()
28+
dialer, err := d.dialer()
29+
if err != nil {
30+
return nil, err
31+
}
2432
return dialer.Dial(network, address)
2533
}
2634

2735
func (d proxyDriver) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
2836
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
2937
defer cancel()
30-
return proxy.Dial(ctx, network, address)
38+
39+
dialer, err := d.dialer()
40+
if err != nil {
41+
return nil, err
42+
}
43+
44+
if xd, ok := dialer.(proxy.ContextDialer); ok {
45+
return xd.DialContext(ctx, network, address)
46+
} else {
47+
return nil, fmt.Errorf("unexpected protocol error")
48+
}
49+
}
50+
51+
func (d proxyDriver) dialer() (proxy.Dialer, error) {
52+
proxyURL := d.proxyURL
53+
if proxyURL == "" {
54+
proxyURL = os.Getenv("PGPROXY")
55+
}
56+
if proxyURL == "" {
57+
return proxy.FromEnvironment(), nil
58+
}
59+
60+
u, err := url.Parse(proxyURL)
61+
if err != nil {
62+
return nil, err
63+
}
64+
65+
dialer, err := proxy.FromURL(u, proxy.Direct)
66+
if err != nil {
67+
return nil, err
68+
}
69+
70+
noProxy := ""
71+
if v := os.Getenv("NO_PROXY"); v != "" {
72+
noProxy = v
73+
}
74+
if v := os.Getenv("no_proxy"); noProxy == "" && v != "" {
75+
noProxy = v
76+
}
77+
if noProxy != "" {
78+
perHost := proxy.NewPerHost(dialer, proxy.Direct)
79+
perHost.AddFromString(noProxy)
80+
81+
dialer = perHost
82+
}
83+
84+
return dialer, nil
85+
}
86+
87+
type proxyConnector struct {
88+
dsn string
89+
proxyURL string
90+
}
91+
92+
var _ driver.Connector = (*proxyConnector)(nil)
93+
94+
func (c proxyConnector) Connect(ctx context.Context) (driver.Conn, error) {
95+
return c.Driver().Open(c.dsn)
96+
}
97+
98+
func (c proxyConnector) Driver() driver.Driver {
99+
return proxyDriver{c.proxyURL}
31100
}
32101

33102
func init() {

postgresql/resource_postgresql_grant_role_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ func TestAccPostgresqlGrantRole(t *testing.T) {
141141

142142
func checkGrantRole(t *testing.T, dsn, role string, grantRole string, withAdmin bool) resource.TestCheckFunc {
143143
return func(s *terraform.State) error {
144-
db, err := sql.Open("postgres", dsn)
144+
db, err := sql.Open(proxyDriverName, dsn)
145145
if err != nil {
146146
t.Fatalf("could to create connection pool: %v", err)
147147
}

postgresql/resource_postgresql_grant_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1442,7 +1442,7 @@ func TestAccPostgresqlGrantOwnerPG15(t *testing.T) {
14421442
// Change the owner to the new pg_database_owner role
14431443
func() {
14441444
config := getTestConfig(t)
1445-
db, err := sql.Open("postgres", config.connStr(dbName))
1445+
db, err := sql.Open(proxyDriverName, config.connStr(dbName))
14461446
if err != nil {
14471447
t.Fatalf("could not connect to database %s: %v", dbName, err)
14481448
}

postgresql/resource_postgresql_role_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ func testAccCheckRoleCanLogin(t *testing.T, role, password string) resource.Test
309309
config := getTestConfig(t)
310310
config.Username = role
311311
config.Password = password
312-
db, err := sql.Open("postgres", config.connStr("postgres"))
312+
db, err := sql.Open(proxyDriverName, config.connStr("postgres"))
313313
if err != nil {
314314
return fmt.Errorf("could not open SQL connection: %v", err)
315315
}

postgresql/utils_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func skipIfNotSuperuser(t *testing.T) {
7777

7878
// dbExecute is a test helper to create a pool, execute one query then close the pool
7979
func dbExecute(t *testing.T, dsn, query string, args ...interface{}) {
80-
db, err := sql.Open("postgres", dsn)
80+
db, err := sql.Open(proxyDriverName, dsn)
8181
if err != nil {
8282
t.Fatalf("could to create connection pool: %v", err)
8383
}
@@ -147,7 +147,7 @@ func createTestTables(t *testing.T, dbSuffix string, tables []string, owner stri
147147
dbName, _ := getTestDBNames(dbSuffix)
148148
adminUser := config.getDatabaseUsername()
149149

150-
db, err := sql.Open("postgres", config.connStr(dbName))
150+
db, err := sql.Open(proxyDriverName, config.connStr(dbName))
151151
if err != nil {
152152
t.Fatalf("could not open connection pool for db %s: %v", dbName, err)
153153
}
@@ -182,7 +182,7 @@ func createTestTables(t *testing.T, dbSuffix string, tables []string, owner stri
182182

183183
// In this case we need to drop table after each test.
184184
return func() {
185-
db, err := sql.Open("postgres", config.connStr(dbName))
185+
db, err := sql.Open(proxyDriverName, config.connStr(dbName))
186186
if err != nil {
187187
t.Fatalf("could not open connection pool for db %s: %v", dbName, err)
188188
}
@@ -213,7 +213,7 @@ func createTestSchemas(t *testing.T, dbSuffix string, schemas []string, owner st
213213
dbName, _ := getTestDBNames(dbSuffix)
214214
adminUser := config.getDatabaseUsername()
215215

216-
db, err := sql.Open("postgres", config.connStr(dbName))
216+
db, err := sql.Open(proxyDriverName, config.connStr(dbName))
217217
if err != nil {
218218
t.Fatalf("could not open connection pool for db %s: %v", dbName, err)
219219
}
@@ -248,7 +248,7 @@ func createTestSchemas(t *testing.T, dbSuffix string, schemas []string, owner st
248248

249249
// In this case we need to drop schema after each test.
250250
return func() {
251-
db, err := sql.Open("postgres", config.connStr(dbName))
251+
db, err := sql.Open(proxyDriverName, config.connStr(dbName))
252252
if err != nil {
253253
t.Fatalf("could not open connection pool for db %s: %v", dbName, err)
254254
}
@@ -278,7 +278,7 @@ func createTestSequences(t *testing.T, dbSuffix string, sequences []string, owne
278278
dbName, _ := getTestDBNames(dbSuffix)
279279
adminUser := config.getDatabaseUsername()
280280

281-
db, err := sql.Open("postgres", config.connStr(dbName))
281+
db, err := sql.Open(proxyDriverName, config.connStr(dbName))
282282
if err != nil {
283283
t.Fatalf("could not open connection pool for db %s: %v", dbName, err)
284284
}
@@ -312,7 +312,7 @@ func createTestSequences(t *testing.T, dbSuffix string, sequences []string, owne
312312
}
313313

314314
return func() {
315-
db, err := sql.Open("postgres", config.connStr(dbName))
315+
db, err := sql.Open(proxyDriverName, config.connStr(dbName))
316316
if err != nil {
317317
t.Fatalf("could not open connection pool for db %s: %v", dbName, err)
318318
}
@@ -362,7 +362,7 @@ func connectAsTestRole(t *testing.T, role, dbName string) *sql.DB {
362362
config.Username = role
363363
config.Password = testRolePassword
364364

365-
db, err := sql.Open("postgres", config.connStr(dbName))
365+
db, err := sql.Open(proxyDriverName, config.connStr(dbName))
366366
if err != nil {
367367
t.Fatalf("could not open connection pool for db %s: %v", dbName, err)
368368
}

tests/docker-compose.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@ services:
1818
environment:
1919
POSTGRES_PASSWORD: ${PGPASSWORD}
2020
ports:
21-
- 25432:5432
21+
- "25432:5432"
2222
healthcheck:
2323
test: [ "CMD-SHELL", "pg_isready" ]
2424
interval: 10s
2525
timeout: 5s
2626
retries: 5
27+
28+
proxy:
29+
image: ghcr.io/httptoolkit/docker-socks-tunnel
30+
ports:
31+
- "11080:1080"

tests/switch_proxy.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/bin/bash
2+
3+
export TF_ACC=true
4+
export PGHOST=postgres
5+
export PGPORT=5432
6+
export PGUSER=postgres
7+
export PGPASSWORD=postgres
8+
export PGSSLMODE=disable
9+
export PGSUPERUSER=true
10+
export PGPROXY=socks5://127.0.0.1:11080

tests/switch_rds.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ export PGUSER=rds
3030
export PGPASSWORD=rds
3131
export PGSSLMODE=disable
3232
export PGSUPERUSER=false
33+
export PGPROXY=""

tests/switch_superuser.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export PGUSER=postgres
77
export PGPASSWORD=postgres
88
export PGSSLMODE=disable
99
export PGSUPERUSER=true
10+
export PGPROXY=""

tests/testacc_full.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ setup() {
1313

1414
run() {
1515
go test -count=1 ./postgresql -v -timeout 120m
16-
16+
1717
# keep the return value for the scripts to fail and clean properly
1818
return $?
1919
}
@@ -31,4 +31,5 @@ run_suite() {
3131
}
3232

3333
run_suite "superuser"
34+
run_suite "proxy"
3435
run_suite "rds"

website/docs/index.html.markdown

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ The following arguments are supported:
186186
* `aws_rds_iam_provider_role_arn` - (Optional) AWS IAM role to assume while using AWS RDS IAM Auth.
187187
* `azure_identity_auth` - (Optional) If set to `true`, call the Azure OAuth token endpoint for temporary token
188188
* `azure_tenant_id` - (Optional) (Required if `azure_identity_auth` is `true`) Azure tenant ID [read more](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config.html)
189+
* `proxy_url` - (Optional) SOCKS5 proxy URL. Must be a valid URL with schema `socks5` or `socks5h`.
189190

190191
## GoCloud
191192

@@ -332,7 +333,9 @@ provider "postgresql" {
332333

333334
### SOCKS5 Proxy Support
334335

335-
The provider supports connecting via a SOCKS5 proxy, but when the `postgres` scheme is used. It can be configured by setting the `ALL_PROXY` or `all_proxy` environment variable to a value like `socks5://127.0.0.1:1080`.
336+
The provider supports connecting via a SOCKS5 proxy, but only when the `postgres` scheme is used. It can be configured
337+
by setting the `proxy_url` provider attribute, or `PGPROXY`, `ALL_PROXY` or `all_proxy` environment variable to a value
338+
like `socks5://127.0.0.1:1080`.
336339

337340
The `NO_PROXY` or `no_proxy` environment can also be set to opt out of proxying for specific hostnames or ports.
338341

0 commit comments

Comments
 (0)