diff --git a/postgresql/config.go b/postgresql/config.go index c2f1410c..91437109 100644 --- a/postgresql/config.go +++ b/postgresql/config.go @@ -183,6 +183,7 @@ type Config struct { SSLClientCert *ClientCertificateConfig SSLRootCertPath string GCPIAMImpersonateServiceAccount string + ProxyURL string } // Client struct holding connection string @@ -290,7 +291,10 @@ func (c *Client) Connect() (*DBConnection, error) { var db *sql.DB var err error if c.config.Scheme == "postgres" { - db, err = sql.Open(proxyDriverName, dsn) + db = sql.OpenDB(proxyConnector{ + dsn: dsn, + proxyURL: c.config.ProxyURL, + }) } else if c.config.Scheme == "gcppostgres" && c.config.GCPIAMImpersonateServiceAccount != "" { db, err = openImpersonatedGCPDBConnection(context.Background(), dsn, c.config.GCPIAMImpersonateServiceAccount) } else { diff --git a/postgresql/provider.go b/postgresql/provider.go index 8bc7546d..3e8fedca 100644 --- a/postgresql/provider.go +++ b/postgresql/provider.go @@ -3,9 +3,10 @@ package postgresql import ( "context" "fmt" + "os" + "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/sts" - "os" "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" @@ -199,6 +200,12 @@ func Provider() *schema.Provider { Description: "Specify the expected version of PostgreSQL.", ValidateFunc: validateExpectedVersion, }, + "proxy_url": { + Type: schema.TypeString, + Optional: true, + Description: "SOCKS5 proxy URL.", + ValidateFunc: validation.IsURLWithScheme([]string{"socks5", "socks5h"}), + }, }, ResourcesMap: map[string]*schema.Resource{ @@ -382,6 +389,7 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { ExpectedVersion: version, SSLRootCertPath: d.Get("sslrootcert").(string), GCPIAMImpersonateServiceAccount: d.Get("gcp_iam_impersonate_service_account").(string), + ProxyURL: d.Get("proxy_url").(string), } if value, ok := d.GetOk("clientcert"); ok { diff --git a/postgresql/proxy_driver.go b/postgresql/proxy_driver.go index f08c2b1a..13af6c32 100644 --- a/postgresql/proxy_driver.go +++ b/postgresql/proxy_driver.go @@ -4,7 +4,10 @@ import ( "context" "database/sql" "database/sql/driver" + "fmt" "net" + "net/url" + "os" "time" "github.com/lib/pq" @@ -13,21 +16,87 @@ import ( const proxyDriverName = "postgresql-proxy" -type proxyDriver struct{} +type proxyDriver struct { + proxyURL string +} func (d proxyDriver) Open(name string) (driver.Conn, error) { return pq.DialOpen(d, name) } func (d proxyDriver) Dial(network, address string) (net.Conn, error) { - dialer := proxy.FromEnvironment() + dialer, err := d.dialer() + if err != nil { + return nil, err + } return dialer.Dial(network, address) } func (d proxyDriver) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) { ctx, cancel := context.WithTimeout(context.TODO(), timeout) defer cancel() - return proxy.Dial(ctx, network, address) + + dialer, err := d.dialer() + if err != nil { + return nil, err + } + + if xd, ok := dialer.(proxy.ContextDialer); ok { + return xd.DialContext(ctx, network, address) + } else { + return nil, fmt.Errorf("unexpected protocol error") + } +} + +func (d proxyDriver) dialer() (proxy.Dialer, error) { + proxyURL := d.proxyURL + if proxyURL == "" { + proxyURL = os.Getenv("PGPROXY") + } + if proxyURL == "" { + return proxy.FromEnvironment(), nil + } + + u, err := url.Parse(proxyURL) + if err != nil { + return nil, err + } + + dialer, err := proxy.FromURL(u, proxy.Direct) + if err != nil { + return nil, err + } + + noProxy := "" + if v := os.Getenv("NO_PROXY"); v != "" { + noProxy = v + } + if v := os.Getenv("no_proxy"); noProxy == "" && v != "" { + noProxy = v + } + if noProxy != "" { + perHost := proxy.NewPerHost(dialer, proxy.Direct) + perHost.AddFromString(noProxy) + + dialer = perHost + } + + return dialer, nil +} + +type proxyConnector struct { + dsn string + proxyURL string +} + +var _ driver.Connector = (*proxyConnector)(nil) + +func (c proxyConnector) Connect(ctx context.Context) (driver.Conn, error) { + return c.Driver().Open(c.dsn) +} + +func (c proxyConnector) Driver() driver.Driver { + return proxyDriver{c.proxyURL} } func init() { diff --git a/postgresql/resource_postgresql_grant_role_test.go b/postgresql/resource_postgresql_grant_role_test.go index ed25777a..fa431791 100644 --- a/postgresql/resource_postgresql_grant_role_test.go +++ b/postgresql/resource_postgresql_grant_role_test.go @@ -141,7 +141,7 @@ func TestAccPostgresqlGrantRole(t *testing.T) { func checkGrantRole(t *testing.T, dsn, role string, grantRole string, withAdmin bool) resource.TestCheckFunc { return func(s *terraform.State) error { - db, err := sql.Open("postgres", dsn) + db, err := sql.Open(proxyDriverName, dsn) if err != nil { t.Fatalf("could to create connection pool: %v", err) } diff --git a/postgresql/resource_postgresql_grant_test.go b/postgresql/resource_postgresql_grant_test.go index 4b62180b..550b09f7 100644 --- a/postgresql/resource_postgresql_grant_test.go +++ b/postgresql/resource_postgresql_grant_test.go @@ -1442,7 +1442,7 @@ func TestAccPostgresqlGrantOwnerPG15(t *testing.T) { // Change the owner to the new pg_database_owner role func() { config := getTestConfig(t) - db, err := sql.Open("postgres", config.connStr(dbName)) + db, err := sql.Open(proxyDriverName, config.connStr(dbName)) if err != nil { t.Fatalf("could not connect to database %s: %v", dbName, err) } diff --git a/postgresql/resource_postgresql_role_test.go b/postgresql/resource_postgresql_role_test.go index fc958840..419e8209 100644 --- a/postgresql/resource_postgresql_role_test.go +++ b/postgresql/resource_postgresql_role_test.go @@ -309,7 +309,7 @@ func testAccCheckRoleCanLogin(t *testing.T, role, password string) resource.Test config := getTestConfig(t) config.Username = role config.Password = password - db, err := sql.Open("postgres", config.connStr("postgres")) + db, err := sql.Open(proxyDriverName, config.connStr("postgres")) if err != nil { return fmt.Errorf("could not open SQL connection: %v", err) } diff --git a/postgresql/utils_test.go b/postgresql/utils_test.go index 5324a935..b7b3d919 100644 --- a/postgresql/utils_test.go +++ b/postgresql/utils_test.go @@ -77,7 +77,7 @@ func skipIfNotSuperuser(t *testing.T) { // dbExecute is a test helper to create a pool, execute one query then close the pool func dbExecute(t *testing.T, dsn, query string, args ...interface{}) { - db, err := sql.Open("postgres", dsn) + db, err := sql.Open(proxyDriverName, dsn) if err != nil { t.Fatalf("could to create connection pool: %v", err) } @@ -147,7 +147,7 @@ func createTestTables(t *testing.T, dbSuffix string, tables []string, owner stri dbName, _ := getTestDBNames(dbSuffix) adminUser := config.getDatabaseUsername() - db, err := sql.Open("postgres", config.connStr(dbName)) + db, err := sql.Open(proxyDriverName, config.connStr(dbName)) if err != nil { t.Fatalf("could not open connection pool for db %s: %v", dbName, err) } @@ -182,7 +182,7 @@ func createTestTables(t *testing.T, dbSuffix string, tables []string, owner stri // In this case we need to drop table after each test. return func() { - db, err := sql.Open("postgres", config.connStr(dbName)) + db, err := sql.Open(proxyDriverName, config.connStr(dbName)) if err != nil { t.Fatalf("could not open connection pool for db %s: %v", dbName, err) } @@ -213,7 +213,7 @@ func createTestSchemas(t *testing.T, dbSuffix string, schemas []string, owner st dbName, _ := getTestDBNames(dbSuffix) adminUser := config.getDatabaseUsername() - db, err := sql.Open("postgres", config.connStr(dbName)) + db, err := sql.Open(proxyDriverName, config.connStr(dbName)) if err != nil { t.Fatalf("could not open connection pool for db %s: %v", dbName, err) } @@ -248,7 +248,7 @@ func createTestSchemas(t *testing.T, dbSuffix string, schemas []string, owner st // In this case we need to drop schema after each test. return func() { - db, err := sql.Open("postgres", config.connStr(dbName)) + db, err := sql.Open(proxyDriverName, config.connStr(dbName)) if err != nil { t.Fatalf("could not open connection pool for db %s: %v", dbName, err) } @@ -278,7 +278,7 @@ func createTestSequences(t *testing.T, dbSuffix string, sequences []string, owne dbName, _ := getTestDBNames(dbSuffix) adminUser := config.getDatabaseUsername() - db, err := sql.Open("postgres", config.connStr(dbName)) + db, err := sql.Open(proxyDriverName, config.connStr(dbName)) if err != nil { t.Fatalf("could not open connection pool for db %s: %v", dbName, err) } @@ -312,7 +312,7 @@ func createTestSequences(t *testing.T, dbSuffix string, sequences []string, owne } return func() { - db, err := sql.Open("postgres", config.connStr(dbName)) + db, err := sql.Open(proxyDriverName, config.connStr(dbName)) if err != nil { t.Fatalf("could not open connection pool for db %s: %v", dbName, err) } @@ -362,7 +362,7 @@ func connectAsTestRole(t *testing.T, role, dbName string) *sql.DB { config.Username = role config.Password = testRolePassword - db, err := sql.Open("postgres", config.connStr(dbName)) + db, err := sql.Open(proxyDriverName, config.connStr(dbName)) if err != nil { t.Fatalf("could not open connection pool for db %s: %v", dbName, err) } diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 6eb18a01..5aeb6a21 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -18,9 +18,14 @@ services: environment: POSTGRES_PASSWORD: ${PGPASSWORD} ports: - - 25432:5432 + - "25432:5432" healthcheck: test: [ "CMD-SHELL", "pg_isready" ] interval: 10s timeout: 5s retries: 5 + + proxy: + image: ghcr.io/httptoolkit/docker-socks-tunnel + ports: + - "11080:1080" diff --git a/tests/switch_proxy.sh b/tests/switch_proxy.sh new file mode 100644 index 00000000..7f125b51 --- /dev/null +++ b/tests/switch_proxy.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +export TF_ACC=true +export PGHOST=postgres +export PGPORT=5432 +export PGUSER=postgres +export PGPASSWORD=postgres +export PGSSLMODE=disable +export PGSUPERUSER=true +export PGPROXY=socks5://127.0.0.1:11080 diff --git a/tests/switch_rds.sh b/tests/switch_rds.sh index 25ea4901..22a65999 100755 --- a/tests/switch_rds.sh +++ b/tests/switch_rds.sh @@ -30,3 +30,4 @@ export PGUSER=rds export PGPASSWORD=rds export PGSSLMODE=disable export PGSUPERUSER=false +export PGPROXY="" diff --git a/tests/switch_superuser.sh b/tests/switch_superuser.sh index 7a63aa96..a273a7aa 100755 --- a/tests/switch_superuser.sh +++ b/tests/switch_superuser.sh @@ -7,3 +7,4 @@ export PGUSER=postgres export PGPASSWORD=postgres export PGSSLMODE=disable export PGSUPERUSER=true +export PGPROXY="" diff --git a/tests/testacc_full.sh b/tests/testacc_full.sh index 39b2941f..6a88e119 100755 --- a/tests/testacc_full.sh +++ b/tests/testacc_full.sh @@ -13,7 +13,7 @@ setup() { run() { go test -count=1 ./postgresql -v -timeout 120m - + # keep the return value for the scripts to fail and clean properly return $? } @@ -31,4 +31,5 @@ run_suite() { } run_suite "superuser" +run_suite "proxy" run_suite "rds" diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index bc0a1b03..eb5c4618 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -186,6 +186,7 @@ The following arguments are supported: * `aws_rds_iam_provider_role_arn` - (Optional) AWS IAM role to assume while using AWS RDS IAM Auth. * `azure_identity_auth` - (Optional) If set to `true`, call the Azure OAuth token endpoint for temporary token * `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) +* `proxy_url` - (Optional) SOCKS5 proxy URL. Must be a valid URL with schema `socks5` or `socks5h`. ## GoCloud @@ -332,7 +333,9 @@ provider "postgresql" { ### SOCKS5 Proxy Support -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`. +The provider supports connecting via a SOCKS5 proxy, but only when the `postgres` scheme is used. It can be configured +by setting the `proxy_url` provider attribute, or `PGPROXY`, `ALL_PROXY` or `all_proxy` environment variable to a value +like `socks5://127.0.0.1:1080`. The `NO_PROXY` or `no_proxy` environment can also be set to opt out of proxying for specific hostnames or ports.