Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for unix sockets (linux, mac, and windows) #1182

Merged
merged 34 commits into from May 25, 2022
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
691dfde
feat: add support for unix sockets
enocom Apr 22, 2022
b958911
Cleanup description of flag
enocom Apr 22, 2022
62ffcd9
typo in error message
enocom Apr 22, 2022
7ce059e
PR comments
enocom Apr 25, 2022
fe58ea2
Clean instance name for Windows
enocom Apr 25, 2022
bafc096
License header and typo fix
enocom Apr 25, 2022
b89cd7e
Get the build constraints right
enocom Apr 25, 2022
414031d
Try again
enocom Apr 25, 2022
a5c4331
Use build constraints
enocom Apr 25, 2022
4564f61
Improve comment wording
enocom Apr 26, 2022
6a8fa11
Simple unix testw
enocom Apr 27, 2022
bb139db
try windows server 2022
enocom Apr 27, 2022
8913329
Dial the right address
enocom Apr 27, 2022
48814ea
Some cleanup
enocom Apr 27, 2022
d37c785
Import strings
enocom Apr 27, 2022
1f57b9f
woops
enocom Apr 27, 2022
eebee9f
Another mistake
enocom Apr 27, 2022
6fdffd8
Revert "try windows server 2022"
enocom May 5, 2022
4f0097d
What's the GOPATH
enocom May 5, 2022
37764bc
Make integration tests Windows friendly
enocom May 5, 2022
3fa813d
Add logging to understand what's wrong
enocom May 5, 2022
98efdcf
Use pgx in postgres tests
enocom May 5, 2022
0da8a27
Don't run Postgres Unix socket test on Windows
enocom May 5, 2022
85d7c5b
Cleanup
enocom May 5, 2022
e9666c3
Correct compilation error
enocom May 5, 2022
ecf9250
Test patch to pgconn
enocom May 5, 2022
11459ef
Fix go sum
enocom May 5, 2022
dafbe6d
Another adjustmnet
enocom May 5, 2022
ea02874
Revert pgconn test
enocom May 5, 2022
d4824f4
Add note about open issue
enocom May 5, 2022
70b0bab
Test pgconn patch
enocom May 6, 2022
874f3a4
Pgx supports Windows Unix sockets now
enocom May 9, 2022
a85354c
Simplify boolean expression
enocom May 25, 2022
a85a98f
Allow user and group access to socket
enocom May 25, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 36 additions & 6 deletions cmd/root.go
Expand Up @@ -120,6 +120,8 @@ any client SSL certificates.`,
"Address on which to bind Cloud SQL instance listeners.")
cmd.PersistentFlags().IntVarP(&c.conf.Port, "port", "p", 0,
"Initial port to use for listeners. Subsequent listeners increment from this value.")
cmd.PersistentFlags().StringVarP(&c.conf.UnixSocket, "unix-socket", "u", "",
`Enables Unix sockets for all listeners using the provided directory.`)
enocom marked this conversation as resolved.
Show resolved Hide resolved

c.Command = cmd
return c
Expand All @@ -130,20 +132,29 @@ func parseConfig(cmd *cobra.Command, conf *proxy.Config, args []string) error {
if len(args) == 0 {
return newBadCommandError("missing instance_connection_name (e.g., project:region:instance)")
}
// First, validate global config.

userHasSet := func(f string) bool {
enocom marked this conversation as resolved.
Show resolved Hide resolved
return cmd.PersistentFlags().Lookup(f).Changed
}
if userHasSet("address") && userHasSet("unix-socket") {
return newBadCommandError("cannot specify --unix-socket and --address together")
}
if userHasSet("port") && userHasSet("unix-socket") {
return newBadCommandError("cannot specify --unix-socket and --port together")
}
if ip := net.ParseIP(conf.Addr); ip == nil {
return newBadCommandError(fmt.Sprintf("not a valid IP address: %q", conf.Addr))
}

// If more than one auth method is set, error.
if conf.Token != "" && conf.CredentialsFile != "" {
return newBadCommandError("Cannot specify --token and --credentials-file flags at the same time")
return newBadCommandError("cannot specify --token and --credentials-file flags at the same time")
}
if conf.Token != "" && conf.GcloudAuth {
return newBadCommandError("Cannot specify --token and --gcloud-auth flags at the same time")
return newBadCommandError("cannot specify --token and --gcloud-auth flags at the same time")
}
if conf.CredentialsFile != "" && conf.GcloudAuth {
return newBadCommandError("Cannot specify --credentials-file and --gcloud-auth flags at the same time")
return newBadCommandError("cannot specify --credentials-file and --gcloud-auth flags at the same time")
}
opts := []cloudsqlconn.Option{
cloudsqlconn.WithUserAgent(userAgent),
Expand Down Expand Up @@ -185,7 +196,18 @@ func parseConfig(cmd *cobra.Command, conf *proxy.Config, args []string) error {
return newBadCommandError(fmt.Sprintf("could not parse query: %q", res[1]))
}

if a, ok := q["address"]; ok {
a, aok := q["address"]
enocom marked this conversation as resolved.
Show resolved Hide resolved
p, pok := q["port"]
u, uok := q["unix-socket"]

if aok && uok {
return newBadCommandError("cannot specify both address and unix-socket query params")
}
if pok && uok {
return newBadCommandError("cannot specify both port and unix-socket query params")
}

if aok {
if len(a) != 1 {
return newBadCommandError(fmt.Sprintf("address query param should be only one value: %q", a))
}
Expand All @@ -198,7 +220,7 @@ func parseConfig(cmd *cobra.Command, conf *proxy.Config, args []string) error {
ic.Addr = a[0]
}

if p, ok := q["port"]; ok {
if pok {
if len(p) != 1 {
return newBadCommandError(fmt.Sprintf("port query param should be only one value: %q", a))
}
Expand All @@ -211,6 +233,14 @@ func parseConfig(cmd *cobra.Command, conf *proxy.Config, args []string) error {
}
ic.Port = pp
}

if uok {
if len(u) != 1 {
return newBadCommandError(fmt.Sprintf("unix query param should be only one value: %q", a))
}
ic.UnixSocket = u[0]

}
}
ics = append(ics, ic)
}
Expand Down
43 changes: 43 additions & 0 deletions cmd/root_test.go
Expand Up @@ -151,6 +151,29 @@ func TestNewCommandArguments(t *testing.T) {
GcloudAuth: true,
}),
},
{
desc: "using the unix socket flag",
args: []string{"--unix-socket", "/path/to/dir/", "proj:region:inst"},
want: withDefaults(&proxy.Config{
UnixSocket: "/path/to/dir/",
}),
},
{
desc: "using the (short) unix socket flag",
args: []string{"-u", "/path/to/dir/", "proj:region:inst"},
want: withDefaults(&proxy.Config{
UnixSocket: "/path/to/dir/",
}),
},
{
desc: "using the unix socket query param",
args: []string{"proj:region:inst?unix-socket=/path/to/dir/"},
want: withDefaults(&proxy.Config{
Instances: []proxy.InstanceConnConfig{{
UnixSocket: "/path/to/dir/",
}},
}),
},
}

for _, tc := range tcs {
Expand Down Expand Up @@ -237,6 +260,26 @@ func TestNewCommandWithErrors(t *testing.T) {
"--gcloud-auth",
"--credential-file", "/path/to/file", "proj:region:inst"},
},
{
desc: "when the unix socket query param contains multiple values",
args: []string{"proj:region:inst?unix-socket=/one&unix-socket=/two"},
},
{
desc: "using the unix socket flag with addr",
args: []string{"-u", "/path/to/dir/", "-a", "127.0.0.1", "proj:region:inst"},
},
{
desc: "using the unix socket flag with port",
args: []string{"-u", "/path/to/dir/", "-p", "5432", "proj:region:inst"},
},
{
desc: "using the unix socket and addr query params",
args: []string{"proj:region:inst?unix-socket=/path&address=127.0.0.1"},
},
{
desc: "using the unix socket and port query params",
args: []string{"proj:region:inst?unix-socket=/path&port=5000"},
},
}

for _, tc := range tcs {
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Expand Up @@ -11,10 +11,11 @@ require (
github.com/go-sql-driver/mysql v1.6.0
github.com/google/go-cmp v0.5.7
github.com/hanwen/go-fuse/v2 v2.1.0
github.com/jackc/pgx/v4 v4.16.0
github.com/jackc/pgx/v4 v4.16.1
github.com/lib/pq v1.10.5
github.com/spf13/cobra v1.2.1
go.uber.org/zap v1.21.0
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 // indirect
golang.org/x/net v0.0.0-20220412020605-290c469a71a5
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
golang.org/x/sys v0.0.0-20220429121018-84afa8d3f7b3
Expand Down
12 changes: 7 additions & 5 deletions go.sum
Expand Up @@ -254,8 +254,8 @@ github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfG
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.11.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.12.0 h1:/RvQ24k3TnNdfBSW0ou9EOi5jx2cX7zfE8n2nLKuiP0=
github.com/jackc/pgconn v1.12.0/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono=
github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8=
github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
Expand Down Expand Up @@ -289,8 +289,8 @@ github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw=
github.com/jackc/pgx/v4 v4.16.0 h1:4k1tROTJctHotannFYzu77dY3bgtMRymQP7tXQjqpPk=
github.com/jackc/pgx/v4 v4.16.0/go.mod h1:N0A9sFdWzkw/Jy1lwoiB64F2+ugFZi987zRxcPez/wI=
github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y=
github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
Expand Down Expand Up @@ -435,8 +435,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8=
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
Expand Down Expand Up @@ -512,6 +513,7 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
Expand Down
94 changes: 78 additions & 16 deletions internal/proxy/proxy.go
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"
"io"
"net"
"os"
"strings"
"sync"
"time"
Expand All @@ -37,6 +38,10 @@ type InstanceConnConfig struct {
Addr string
// Port is the port on which to bind a listener for the instance.
Port int
// UnixSocket is the directory where a Unix socket will be created,
// connected to the Cloud SQL instance. If set, takes precedence over Addr
// and Port.
UnixSocket string
}

// Config contains all the configuration provided by the caller.
Expand All @@ -58,6 +63,10 @@ type Config struct {
// increments from this value.
Port int

// UnixSocket is the directory where Unix sockets will be created,
// connected to any Instances. If set, takes precedence over Addr and Port.
UnixSocket string

// Instances are configuration for individual instances. Instance
// configuration takes precedence over global configuration.
Instances []InstanceConnConfig
Expand Down Expand Up @@ -134,31 +143,84 @@ func NewClient(ctx context.Context, d cloudsql.Dialer, cmd *cobra.Command, conf
}
pc := newPortConfig(conf.Port)
for _, inst := range conf.Instances {
m := &socketMount{inst: inst.Name}
a := conf.Addr
if inst.Addr != "" {
a = inst.Addr
}
version, err := d.EngineVersion(ctx, inst.Name)
if err != nil {
return nil, err
}
var np int
switch {
case inst.Port != 0:
np = inst.Port
case conf.Port != 0:
np = pc.nextPort()
default:
np = pc.nextDBPort(version)

var (
// network is one of "tcp" or "unix"
network string
// 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
// (e.g., I didn't set a Unix socket globally or for this instance)
// OR
// a global Unix socket directory IS set, AND
// an instance-level TCP address or port IS set
// (e.g., I want Unix sockets globally, but I'm overriding it for this instance)
// use a TCP listener.
// Otherwise, use a Unix socket.
if (conf.UnixSocket == "" && inst.UnixSocket == "") ||
conf.UnixSocket != "" && (inst.Addr != "" || inst.Port != 0) {
enocom marked this conversation as resolved.
Show resolved Hide resolved
network = "tcp"

a := conf.Addr
if inst.Addr != "" {
a = inst.Addr
}

var np int
switch {
case inst.Port != 0:
np = inst.Port
case conf.Port != 0:
np = pc.nextPort()
default:
np = pc.nextDBPort(version)
}

address = net.JoinHostPort(a, fmt.Sprint(np))
} else {
network = "unix"

dir := conf.UnixSocket
if dir == "" {
dir = inst.UnixSocket
}
// Attempt to make the directory if it does not already exist.
if _, err := os.Stat(dir); err != nil {
if err = os.Mkdir(dir, 0750); err != nil {
enocom marked this conversation as resolved.
Show resolved Hide resolved
return nil, err
}
}
address = UnixAddress(dir, inst.Name)
// When setting up a listener for Postgres, create address as a
// directory, and use the Postgres-specific socket name
// .s.PGSQL.5432.
if strings.HasPrefix(version, "POSTGRES") {
// Make the directory only if it hasn't already been created.
if _, err := os.Stat(address); err != nil {
if err = os.Mkdir(address, 0750); err != nil {
return nil, err
}
}
address = UnixAddress(address, ".s.PGSQL.5432")
}
}
addr, err := m.listen(ctx, "tcp", net.JoinHostPort(a, fmt.Sprint(np)))

m := &socketMount{inst: inst.Name}
addr, err := m.listen(ctx, network, address)
if err != nil {
for _, m := range mnts {
m.close()
}
return nil, fmt.Errorf("[%v] Unable to mount socket: %v", inst.Name, err)
}

cmd.Printf("[%s] Listening on %s\n", inst.Name, addr.String())
mnts = append(mnts, m)
}
Expand Down Expand Up @@ -241,9 +303,9 @@ type socketMount struct {
}

// listen causes a socketMount to create a Listener at the specified network address.
func (s *socketMount) listen(ctx context.Context, network string, host string) (net.Addr, error) {
func (s *socketMount) listen(ctx context.Context, network string, address string) (net.Addr, error) {
lc := net.ListenConfig{KeepAlive: 30 * time.Second}
l, err := lc.Listen(ctx, network, host)
l, err := lc.Listen(ctx, network, address)
if err != nil {
return nil, err
}
Expand Down
27 changes: 27 additions & 0 deletions internal/proxy/proxy_other.go
@@ -0,0 +1,27 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build !windows
// +build !windows

package proxy

import "path/filepath"

// UnixAddress is defined as a function to distinguish between Linux-based
// implementations where the dir and inst and simply joins, and Windows-based
// implementations where the inst must be further altered.
func UnixAddress(dir, inst string) string {
return filepath.Join(dir, inst)
}
27 changes: 27 additions & 0 deletions internal/proxy/proxy_other_test.go
@@ -0,0 +1,27 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build !windows
// +build !windows

package proxy_test

var (
pg = "proj:region:pg"
pg2 = "proj:region:pg2"
mysql = "proj:region:mysql"
mysql2 = "proj:region:mysql2"
sqlserver = "proj:region:sqlserver"
sqlserver2 = "proj:region:sqlserver2"
)