Skip to content

Commit fa714b4

Browse files
authored
Set custom per instance socket address (GoogleCloudPlatform#289)
1 parent dc57785 commit fa714b4

4 files changed

Lines changed: 92 additions & 49 deletions

File tree

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ cloud_sql_proxy takes a few arguments to configure what instances to connect to
1919
optional `-fuse_tmp` flag can specify where to place temporary files. The
2020
directory indicated by `-dir` is mounted.
2121
* `-instances="project1:region:instance1,project3:region:instance1"`: A comma-separated list
22-
of instances to open inside `-dir`. Also supports exposing a tcp port instead of using Unix Domain Sockets; see examples below.
22+
of instances to open inside `-dir`. Also supports exposing a tcp port and renaming the default Unix Domain Sockets; see examples below.
2323
Same list can be provided via INSTANCES environment variable, in case when both are provided - proxy will use command line flag.
2424
* `-instances_metadata=metadata_key`: Usable on [GCE](https://cloud.google.com/compute/docs/quickstart) only. The given [GCE metadata](https://cloud.google.com/compute/docs/metadata) key will be
2525
polled for a list of instances to open in `-dir`. The metadata key is relative from `computeMetadata/v1/`. The format for the value is the same as the 'instances' flag. A hanging-poll strategy is used, meaning that changes to
@@ -82,6 +82,14 @@ instead of passing this flag.
8282
./cloud_sql_proxy -dir=/cloudsql -instances=my-project:us-central1:sql-inst=tcp:3306 &
8383
mysql -u root -h 127.0.0.1
8484

85+
# For programs which require a certain Unix Domain Socket name:
86+
./cloud_sql_proxy -dir=/cloudsql -instances=my-project:us-central1:sql-inst=unix:custom_socket_name&
87+
mysql -u root -h 127.0.0.1
88+
89+
# For programs which require a the Unix Domain Socket at a specific location, set an absolute path (overrides -dir):
90+
./cloud_sql_proxy -dir=/cloudsql -instances=my-project:us-central1:sql-inst=unix:/my/custom/sql-socket&
91+
mysql -u root -h 127.0.0.1
92+
8593
## To use from Kubernetes:
8694

8795
### Deploying Cloud SQL Proxy as a sidecar container

cmd/cloud_sql_proxy/cloud_sql_proxy.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,18 @@ Connection:
149149
150150
When connecting over TCP, the -instances parameter is required.
151151
152+
To set a custom socket name, you can specify it as part of the instance
153+
string. The following example opens a unix socket in the directory
154+
specified by -dir, which will be proxied to connect to the instance
155+
'my-instance' in project 'my-project':
156+
157+
-instances=my-project:my-region:my-instance=unix:custom-socket-name
158+
159+
To override the -dir parameter, specify an absolute path as shown in the
160+
following example:
161+
162+
-instances=my-project:my-region:my-instance=unix:/my/custom/sql-socket
163+
152164
Supplying INSTANCES environment variable achieves the same effect. One can
153165
use that to keep k8s manifest files constant across multiple environments
154166

cmd/cloud_sql_proxy/proxy.go

Lines changed: 63 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -221,62 +221,64 @@ var validNets = func() map[string]bool {
221221

222222
func parseInstanceConfig(dir, instance string, cl *http.Client) (instanceConfig, error) {
223223
var ret instanceConfig
224-
eq := strings.Index(instance, "=")
225-
if eq != -1 {
226-
spl := strings.SplitN(instance[eq+1:], ":", 3)
227-
ret.Instance = instance[:eq]
228-
229-
switch len(spl) {
230-
default:
231-
return ret, fmt.Errorf("invalid %q: expected 'project:instance=tcp:port'", instance)
232-
case 2:
233-
// No "host" part of the address. Be safe and assume that they want a
234-
// loopback address.
235-
ret.Network = spl[0]
236-
addr, ok := loopbackForNet[spl[0]]
237-
if !ok {
238-
return ret, fmt.Errorf("invalid %q: unrecognized network %v", instance, spl[0])
224+
args := strings.Split(instance, "=")
225+
if len(args) > 2 {
226+
return instanceConfig{}, fmt.Errorf("invalid instance argument: must be either form - `<instance_connection_string>` or `<instance_connection_string>=<options>`; invalid arg was %q", instance)
227+
}
228+
// Parse the instance connection name - everything before the "=".
229+
ret.Instance = args[0]
230+
proj, _, name := util.SplitName(ret.Instance)
231+
if proj == "" || name == "" {
232+
return instanceConfig{}, fmt.Errorf("invalid instance connection string: must be in the form `project:region:instance-name`; invalid name was %q", args[0])
233+
}
234+
if len(args) == 1 {
235+
// Default to listening via unix socket in specified directory
236+
ret.Network = "unix"
237+
ret.Address = filepath.Join(dir, instance)
238+
} else {
239+
// Parse the instance options if present.
240+
opts := strings.SplitN(args[1], ":", 2)
241+
if len(opts) != 2 {
242+
return instanceConfig{}, fmt.Errorf("invalid instance options: must be in the form `unix:/path/to/socket`, `tcp:port`, `tcp:host:port`; invalid option was %q", strings.Join(opts, ":"))
243+
}
244+
ret.Network = opts[0]
245+
var err error
246+
if ret.Network == "unix" {
247+
if strings.HasPrefix(opts[1], "/") {
248+
ret.Address = opts[1] // Root path.
249+
} else {
250+
ret.Address = filepath.Join(dir, opts[1])
239251
}
240-
ret.Address = fmt.Sprintf("%s:%s", addr, spl[1])
241-
case 3:
242-
// User provided a host and port; use that.
243-
ret.Network = spl[0]
244-
ret.Address = fmt.Sprintf("%s:%s", spl[1], spl[2])
252+
} else {
253+
ret.Address, err = parseTCPOpts(opts[0], opts[1])
245254
}
246-
} else {
247-
sql, err := sqladmin.New(cl)
248255
if err != nil {
249256
return instanceConfig{}, err
250257
}
251-
sql.BasePath = *host
252-
ret.Instance = instance
253-
// Default to unix socket.
254-
ret.Network = "unix"
258+
}
255259

256-
proj, _, name := util.SplitName(instance)
257-
if proj == "" || name == "" {
258-
return instanceConfig{}, fmt.Errorf("invalid instance name: must be in the form `project:region:instance-name`; invalid name was %q", instance)
259-
}
260-
// We allow people to omit the region due to historical reasons. It'll
261-
// fail later in the code if this isn't allowed, so just assume it's
262-
// allowed until we actually need the region in this API call.
263-
in, err := sql.Instances.Get(proj, name).Do()
264-
if err != nil {
260+
// Use the SQL Admin API to verify compatibility with the instance.
261+
sql, err := sqladmin.New(cl)
262+
if err != nil {
263+
return instanceConfig{}, err
264+
}
265+
sql.BasePath = *host
266+
inst, err := sql.Instances.Get(proj, name).Do()
267+
if err != nil {
268+
return instanceConfig{}, err
269+
}
270+
if inst.BackendType == "FIRST_GEN" {
271+
logging.Errorf("WARNING: proxy client does not support first generation Cloud SQL instances.")
272+
return instanceConfig{}, fmt.Errorf("%q is a first generation instance", instance)
273+
}
274+
// Postgres instances use a special suffix on the unix socket.
275+
// See https://www.postgresql.org/docs/11/runtime-config-connection.html
276+
if ret.Network == "unix" && strings.HasPrefix(strings.ToLower(inst.DatabaseVersion), "postgres") {
277+
// Verify the directory exists.
278+
if err := os.MkdirAll(ret.Address, 0755); err != nil {
265279
return instanceConfig{}, err
266280
}
267-
if in.BackendType == "FIRST_GEN" {
268-
logging.Errorf("WARNING: proxy client does not support first generation Cloud SQL instances.")
269-
return instanceConfig{}, fmt.Errorf("%q is a first generation instance", instance)
270-
}
271-
if strings.HasPrefix(strings.ToLower(in.DatabaseVersion), "postgres") {
272-
path := filepath.Join(dir, instance)
273-
if err := os.MkdirAll(path, 0755); err != nil {
274-
return instanceConfig{}, err
275-
}
276-
ret.Address = filepath.Join(path, ".s.PGSQL.5432")
277-
} else {
278-
ret.Address = filepath.Join(dir, instance)
279-
}
281+
ret.Address = filepath.Join(ret.Address, ".s.PGSQL.5432")
280282
}
281283

282284
if !validNets[ret.Network] {
@@ -285,6 +287,19 @@ func parseInstanceConfig(dir, instance string, cl *http.Client) (instanceConfig,
285287
return ret, nil
286288
}
287289

290+
// parseTCPOpts parses the instance options when specifying tcp port options.
291+
func parseTCPOpts(ntwk, addrOpt string) (string, error) {
292+
if strings.Contains(addrOpt, ":") {
293+
return addrOpt, nil // User provided a host and port; use that.
294+
}
295+
// No "host" part of the address. Be safe and assume that they want a loopback address.
296+
addr, ok := loopbackForNet[ntwk]
297+
if !ok {
298+
return "", fmt.Errorf("invalid %q:%q: unrecognized network %v", ntwk, addrOpt, ntwk)
299+
}
300+
return net.JoinHostPort(addr, addrOpt), nil
301+
}
302+
288303
// parseInstanceConfigs calls parseInstanceConfig for each instance in the
289304
// provided slice, collecting errors along the way. There may be valid
290305
// instanceConfigs returned even if there's an error.

cmd/cloud_sql_proxy/proxy_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@ func TestParseInstanceConfig(t *testing.T) {
111111
"/x", "my-proj:my-reg:my-instance",
112112
instanceConfig{"my-proj:my-reg:my-instance", "unix", "/x/my-proj:my-reg:my-instance"},
113113
false, false,
114+
}, {
115+
"/x", "my-proj:my-reg:my-instance=unix:socket_name",
116+
instanceConfig{"my-proj:my-reg:my-instance", "unix", "/x/socket_name"},
117+
false, false,
118+
}, {
119+
"/x", "my-proj:my-reg:my-instance=unix:/my/custom/sql-socket",
120+
instanceConfig{"my-proj:my-reg:my-instance", "unix", "/my/custom/sql-socket"},
121+
false, false,
114122
}, {
115123
"/x", "my-proj:my-reg:my-instance=tcp:1234",
116124
instanceConfig{"my-proj:my-reg:my-instance", "tcp", "[::1]:1234"},

0 commit comments

Comments
 (0)