diff --git a/cmd/root.go b/cmd/root.go index 0fe94b6b..0f4806c5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -175,6 +175,16 @@ Instance Level Configuration 'projects/PROJECT/locations/REGION/clusters/CLUSTER/instances/INSTANCE1' \ 'projects/PROJECT/locations/REGION/clusters/CLUSTER/instances/INSTANCE2?address=0.0.0.0&port=7000' + When necessary, you may specify the full path to a Unix socket. Set the + unix-socket-path query parameter to the absolute path of the Unix socket for + the database instance. The parent directory of the unix-socket-path must + exist when the proxy starts or else socket creation will fail. For Postgres + instances, the proxy will ensure that the last path element is + '.s.PGSQL.5432' appending it if necessary. For example, + + ./cloud-sql-proxy \ + 'my-project:us-central1:my-db-server?unix-socket-path=/path/to/socket' + Health checks When enabling the --health-check flag, the proxy will start an HTTP server @@ -541,6 +551,7 @@ func parseConfig(cmd *Command, conf *proxy.Config, args []string) error { a, aok := q["address"] p, pok := q["port"] u, uok := q["unix-socket"] + up, upok := q["unix-socket-path"] if aok && uok { return newBadCommandError("cannot specify both address and unix-socket query params") @@ -548,6 +559,15 @@ func parseConfig(cmd *Command, conf *proxy.Config, args []string) error { if pok && uok { return newBadCommandError("cannot specify both port and unix-socket query params") } + if aok && upok { + return newBadCommandError("cannot specify both address and unix-socket-path query params") + } + if pok && upok { + return newBadCommandError("cannot specify both port and unix-socket-path query params") + } + if uok && upok { + return newBadCommandError("cannot specify both unix-socket-path and unix-socket query params") + } if aok { if len(a) != 1 { @@ -562,6 +582,13 @@ func parseConfig(cmd *Command, conf *proxy.Config, args []string) error { ic.Addr = a[0] } + if upok { + if len(up) != 1 { + return newBadCommandError(fmt.Sprintf("unix-socket-path query param should be only one value: %q", a)) + } + ic.UnixSocketPath = up[0] + } + if pok { if len(p) != 1 { return newBadCommandError(fmt.Sprintf("port query param should be only one value: %q", a)) diff --git a/cmd/root_test.go b/cmd/root_test.go index 9c6badfb..37799f05 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -242,6 +242,15 @@ func TestNewCommandArguments(t *testing.T) { }}, }), }, + { + desc: "using the unix socket path query param", + args: []string{"projects/proj/locations/region/clusters/clust/instances/inst?unix-socket-path=/path/to/file"}, + want: withDefaults(&proxy.Config{ + Instances: []proxy.InstanceConnConfig{{ + UnixSocketPath: "/path/to/file", + }}, + }), + }, { desc: "using the max connections flag", args: []string{"--max-connections", "1", "projects/proj/locations/region/clusters/clust/instances/inst"}, @@ -773,6 +782,18 @@ func TestNewCommandWithErrors(t *testing.T) { desc: "using the unix socket flag with port", args: []string{"-u", "/path/to/dir/", "-p", "5432", "projects/proj/locations/region/clusters/clust/instances/inst"}, }, + { + desc: "using the unix socket and unix-socket-path", + args: []string{"projects/proj/locations/region/clusters/clust/instances/inst?unix-socket=/path&unix-socket-path=/another/path"}, + }, + { + desc: "using the unix socket path and addr query params", + args: []string{"projects/proj/locations/region/clusters/clust/instances/inst?unix-socket-path=/path&address=127.0.0.1"}, + }, + { + desc: "using the unix socket path and port query params", + args: []string{"projects/proj/locations/region/clusters/clust/instances/inst?unix-socket-path=/path&port=5000"}, + }, { desc: "using the unix socket and addr query params", args: []string{"projects/proj/locations/region/clusters/clust/instances/inst?unix-socket=/path&address=127.0.0.1"}, diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index 0c24893c..f299d174 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -20,6 +20,7 @@ import ( "io" "net" "os" + "path" "path/filepath" "regexp" "strings" @@ -48,6 +49,13 @@ type InstanceConnConfig struct { // connected to the AlloyDB instance. If set, takes precedence over Addr // and Port. UnixSocket string + // UnixSocketPath is the path where a Unix socket will be created, + // connected to the Cloud SQL instance. The full path to the socket will be + // UnixSocketPath. Because this is a Postgres database, the proxy will ensure + // the last path element is `.s.PGSQL.5432`, appending this path element if + // necessary. If set, UnixSocketPath takes precedence over UnixSocket, Addr + // and Port. + UnixSocketPath string } // Config contains all the configuration provided by the caller. @@ -648,6 +656,7 @@ func newSocketMount(ctx context.Context, conf *Config, pc *portConfig, inst Inst // address is either a TCP host port, or a Unix socket address string ) + // IF // a global Unix socket directory is NOT set AND // an instance-level Unix socket is NOT set @@ -658,7 +667,7 @@ func newSocketMount(ctx context.Context, conf *Config, pc *portConfig, inst Inst // instance) // use a TCP listener. // Otherwise, use a Unix socket. - if (conf.UnixSocket == "" && inst.UnixSocket == "") || + if (conf.UnixSocket == "" && inst.UnixSocket == "" && inst.UnixSocketPath == "") || (inst.Addr != "" || inst.Port != 0) { network = "tcp" @@ -678,23 +687,10 @@ func newSocketMount(ctx context.Context, conf *Config, pc *portConfig, inst Inst address = net.JoinHostPort(a, fmt.Sprint(np)) } else { network = "unix" - - dir := conf.UnixSocket - if dir == "" { - dir = inst.UnixSocket - } - ud, err := UnixSocketDir(dir, inst.Name) + address, err = newUnixSocketMount(inst, conf.UnixSocket, true) if err != nil { return nil, err } - // Create the parent directory that will hold the socket. - if _, err := os.Stat(ud); err != nil { - if err = os.Mkdir(ud, 0777); err != nil { - return nil, err - } - } - // use the Postgres-specific socket name - address = filepath.Join(ud, ".s.PGSQL.5432") } lc := net.ListenConfig{KeepAlive: 30 * time.Second} @@ -717,6 +713,54 @@ func newSocketMount(ctx context.Context, conf *Config, pc *portConfig, inst Inst return m, nil } +// newUnixSocketMount parses the configuration and returns the path to the unix +// socket, or an error if that path is not valid. +func newUnixSocketMount(inst InstanceConnConfig, unixSocketDir string, postgres bool) (string, error) { + var ( + // the path to the unix socket + address string + // the parent directory of the unix socket + dir string + err error + ) + if inst.UnixSocketPath != "" { + // When UnixSocketPath is set + address = inst.UnixSocketPath + // If UnixSocketPath ends .s.PGSQL.5432, remove it for consistency + if postgres && path.Base(address) == ".s.PGSQL.5432" { + address = path.Dir(address) + } + dir = path.Dir(address) + } else { + // When UnixSocket is set + dir = unixSocketDir + if dir == "" { + dir = inst.UnixSocket + } + address, err = UnixSocketDir(dir, inst.Name) + if err != nil { + return "", err + } + } + // if base directory does not exist, fail + if _, err := os.Stat(dir); err != nil { + return "", err + } + // When setting up a listener for Postgres, create address as a + // directory, and use the Postgres-specific socket name + // .s.PGSQL.5432. + if postgres { + // Make the directory only if it hasn't already been created. + if _, err := os.Stat(address); err != nil { + if err = os.Mkdir(address, 0777); err != nil { + return "", err + } + } + address = UnixAddress(address, ".s.PGSQL.5432") + } + return address, nil +} + func (s *socketMount) Addr() net.Addr { return s.listener.Addr() } diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go index 7b84335c..2b613061 100644 --- a/internal/proxy/proxy_test.go +++ b/internal/proxy/proxy_test.go @@ -22,6 +22,7 @@ import ( "net/http" "net/http/httptest" "os" + "path" "path/filepath" "sync" "testing" @@ -103,6 +104,8 @@ func TestClientInitialization(t *testing.T) { inst1 := "projects/proj/locations/region/clusters/clust/instances/inst1" inst2 := "projects/proj/locations/region/clusters/clust/instances/inst2" wantUnix := "proj.region.clust.inst1" + testUnixSocketPath := path.Join(testDir, "db") + testUnixSocketPathPg := path.Join(testDir, "db", ".s.PGSQL.5432") tcs := []testCase{ { @@ -207,6 +210,40 @@ func TestClientInitialization(t *testing.T) { "127.0.0.1:5000", }, }, + { + desc: "with a Unix socket path overriding Unix socket", + in: &proxy.Config{ + UnixSocket: testDir, + Instances: []proxy.InstanceConnConfig{ + {Name: inst1, UnixSocketPath: testUnixSocketPath}, + }, + }, + wantUnixAddrs: []string{ + filepath.Join(testUnixSocketPathPg), + }, + }, + { + desc: "with a Unix socket path per pg instance", + in: &proxy.Config{ + Instances: []proxy.InstanceConnConfig{ + {Name: inst1, UnixSocketPath: testUnixSocketPath}, + }, + }, + wantUnixAddrs: []string{ + filepath.Join(testUnixSocketPathPg), + }, + }, + { + desc: "with a Unix socket path per pg instance and explicit pg path suffix", + in: &proxy.Config{ + Instances: []proxy.InstanceConnConfig{ + {Name: inst1, UnixSocketPath: testUnixSocketPathPg}, + }, + }, + wantUnixAddrs: []string{ + filepath.Join(testUnixSocketPathPg), + }, + }, } _, isFlex := os.LookupEnv("FLEX") if !isFlex {