Skip to content

Commit e51ef9b

Browse files
feat: add support for public IP (#474)
1 parent 42c8ae3 commit e51ef9b

File tree

12 files changed

+187
-52
lines changed

12 files changed

+187
-52
lines changed

README.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ language. Using an AlloyDB connector provides the following benefits:
2929
## Installation
3030

3131
You can install this repo with `go get`:
32+
3233
```sh
3334
go get cloud.google.com/go/alloydbconn
3435
```
@@ -39,9 +40,11 @@ This package provides several functions for authorizing and encrypting
3940
connections. These functions can be used with your database driver to connect to
4041
your AlloyDB instance.
4142

42-
AlloyDB supports network connectivity through private, internal IP addresses only.
43-
This package must be run in an environment that is connected to the
44-
[VPC Network][vpc] that hosts your AlloyDB private IP address.
43+
AlloyDB supports network connectivity through public IP addresses and private,
44+
internal IP addresses. By default this package will attempt to connect over a
45+
private IP connection. When doing so, this package must be run in an
46+
environment that is connected to the [VPC Network][vpc] that hosts your
47+
AlloyDB private IP address.
4548

4649
Please see [Configuring AlloyDB Connectivity][alloydb-connectivity] for more details.
4750

@@ -52,12 +55,12 @@ Please see [Configuring AlloyDB Connectivity][alloydb-connectivity] for more det
5255

5356
This package requires the following to connect successfully:
5457

55-
- IAM principal (user, service account, etc.) with the [AlloyDB
58+
* IAM principal (user, service account, etc.) with the [AlloyDB
5659
Client and Service Usage Consumer][client-role] roles or equivalent
5760
permissions. [Credentials](#credentials) for the IAM principal are
5861
used to authorize connections to an AlloyDB instance.
5962

60-
- The [AlloyDB Admin API][admin-api] to be enabled within your Google Cloud
63+
* The [AlloyDB Admin API][admin-api] to be enabled within your Google Cloud
6164
Project. By default, the API will be called in the project associated with the
6265
IAM principal.
6366

@@ -136,14 +139,14 @@ For a full list of customizable behavior, see alloydbconn.Option.
136139

137140
### Using DialOptions
138141

139-
If you want to customize things about how the connection is created, use
140-
`DialOption`:
142+
If you want to customize things about how the connection is created, such as
143+
connecting to AlloyDB over a public IP, use a `DialOption`:
141144

142145
```go
143146
conn, err := d.Dial(
144147
ctx,
145148
"projects/<PROJECT>/locations/<REGION>/clusters/<CLUSTER>/instances/<INSTANCE>",
146-
alloydbconn.WithTCPKeepAlive(30*time.Second),
149+
alloydbconn.WithPublicIP(),
147150
)
148151
```
149152

@@ -154,7 +157,7 @@ be used by default:
154157
d, err := alloydbconn.NewDialer(
155158
ctx,
156159
alloydbconn.WithDefaultDialOptions(
157-
alloydbconn.WithTCPKeepAlive(30*time.Second),
160+
alloydbconn.WithPublicIP(),
158161
),
159162
)
160163
```

dialer.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ import (
3030
"sync/atomic"
3131
"time"
3232

33-
alloydbadmin "cloud.google.com/go/alloydb/apiv1beta"
34-
"cloud.google.com/go/alloydb/connectors/apiv1beta/connectorspb"
33+
alloydbadmin "cloud.google.com/go/alloydb/apiv1alpha"
34+
"cloud.google.com/go/alloydb/connectors/apiv1alpha/connectorspb"
3535
"cloud.google.com/go/alloydbconn/errtype"
3636
"cloud.google.com/go/alloydbconn/internal/alloydb"
3737
"cloud.google.com/go/alloydbconn/internal/trace"
@@ -75,7 +75,7 @@ func getDefaultKeys() (*rsa.PrivateKey, error) {
7575

7676
type connectionInfoCache interface {
7777
OpenConns() *uint64
78-
ConnectInfo(context.Context) (string, *tls.Config, error)
78+
ConnectInfo(context.Context, string) (string, *tls.Config, error)
7979
ForceRefresh()
8080
io.Closer
8181
}
@@ -156,6 +156,7 @@ func NewDialer(ctx context.Context, opts ...Option) (*Dialer, error) {
156156
}
157157

158158
dialCfg := dialCfg{
159+
ipType: alloydb.PrivateIP,
159160
tcpKeepAlive: defaultTCPKeepAlive,
160161
}
161162
for _, opt := range cfg.dialOpts {
@@ -211,7 +212,7 @@ func (d *Dialer) Dial(ctx context.Context, instance string, opts ...DialOption)
211212
endInfo(err)
212213
return nil, err
213214
}
214-
addr, tlsCfg, err := i.ConnectInfo(ctx)
215+
addr, tlsCfg, err := i.ConnectInfo(ctx, cfg.ipType)
215216
if err != nil {
216217
d.lock.Lock()
217218
defer d.lock.Unlock()
@@ -231,7 +232,7 @@ func (d *Dialer) Dial(ctx context.Context, instance string, opts ...DialOption)
231232
if invalidClientCert(tlsCfg) {
232233
i.ForceRefresh()
233234
// Block on refreshed connection info
234-
addr, tlsCfg, err = i.ConnectInfo(ctx)
235+
addr, tlsCfg, err = i.ConnectInfo(ctx, cfg.ipType)
235236
if err != nil {
236237
d.lock.Lock()
237238
defer d.lock.Unlock()

dialer_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import (
2828
"testing"
2929
"time"
3030

31-
alloydbadmin "cloud.google.com/go/alloydb/apiv1beta"
31+
alloydbadmin "cloud.google.com/go/alloydb/apiv1alpha"
3232
"cloud.google.com/go/alloydbconn/errtype"
3333
"cloud.google.com/go/alloydbconn/internal/alloydb"
3434
"cloud.google.com/go/alloydbconn/internal/mock"
@@ -341,7 +341,7 @@ type spyConnectionInfoCache struct {
341341
connectionInfoCache
342342
}
343343

344-
func (s *spyConnectionInfoCache) ConnectInfo(_ context.Context) (string, *tls.Config, error) {
344+
func (s *spyConnectionInfoCache) ConnectInfo(_ context.Context, _ string) (string, *tls.Config, error) {
345345
s.mu.Lock()
346346
defer s.mu.Unlock()
347347
res := s.connectInfoCalls[s.connectInfoIndex]

e2e_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,40 @@ func TestAutoIAMAuthN(t *testing.T) {
227227
}
228228
t.Log(tt)
229229
}
230+
231+
func TestPublicIP(t *testing.T) {
232+
if testing.Short() {
233+
t.Skip("skipping integration test")
234+
}
235+
ctx := context.Background()
236+
237+
d, err := alloydbconn.NewDialer(ctx)
238+
if err != nil {
239+
t.Fatalf("failed to init Dialer: %v", err)
240+
}
241+
242+
dsn := fmt.Sprintf(
243+
"user=%s password=%s dbname=%s sslmode=disable",
244+
alloydbUser, alloydbPass, alloydbDB,
245+
)
246+
config, err := pgx.ParseConfig(dsn)
247+
if err != nil {
248+
t.Fatalf("failed to parse pgx config: %v", err)
249+
}
250+
251+
config.DialFunc = func(ctx context.Context, network string, instance string) (net.Conn, error) {
252+
return d.Dial(ctx, alloydbInstanceName, alloydbconn.WithPublicIP())
253+
}
254+
255+
conn, connErr := pgx.ConnectConfig(ctx, config)
256+
if connErr != nil {
257+
t.Fatalf("failed to connect: %s", connErr)
258+
}
259+
defer conn.Close(ctx)
260+
261+
var tt time.Time
262+
if err := conn.QueryRow(context.Background(), "SELECT NOW()").Scan(&tt); err != nil {
263+
t.Fatal(err)
264+
}
265+
t.Log(tt)
266+
}

internal/alloydb/instance.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
"sync"
2424
"time"
2525

26-
alloydbadmin "cloud.google.com/go/alloydb/apiv1beta"
26+
alloydbadmin "cloud.google.com/go/alloydb/apiv1alpha"
2727
"cloud.google.com/go/alloydbconn/errtype"
2828
"golang.org/x/time/rate"
2929
)
@@ -197,13 +197,26 @@ func (i *Instance) Close() error {
197197
return nil
198198
}
199199

200-
// ConnectInfo returns an IP address of the AlloyDB instance.
201-
func (i *Instance) ConnectInfo(ctx context.Context) (string, *tls.Config, error) {
200+
// ConnectInfo returns an IP address specified by ipType (i.e., public or
201+
// private) of the AlloyDB instance.
202+
func (i *Instance) ConnectInfo(ctx context.Context, ipType string) (string, *tls.Config, error) {
202203
res, err := i.result(ctx)
203204
if err != nil {
204205
return "", nil, err
205206
}
206-
return res.result.instanceIPAddr, res.result.conf, nil
207+
var (
208+
addr string
209+
ok bool
210+
)
211+
addr, ok = res.result.ipAddrs[ipType]
212+
if !ok {
213+
err := errtype.NewConfigError(
214+
fmt.Sprintf("instance does not have IP of type %q", ipType),
215+
i.instanceURI.String(),
216+
)
217+
return "", nil, err
218+
}
219+
return addr, res.result.conf, nil
207220
}
208221

209222
// ForceRefresh triggers an immediate refresh operation to be scheduled and

internal/alloydb/instance_test.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import (
2222
"testing"
2323
"time"
2424

25-
alloydbadmin "cloud.google.com/go/alloydb/apiv1beta"
25+
alloydbadmin "cloud.google.com/go/alloydb/apiv1alpha"
2626
"cloud.google.com/go/alloydbconn/errtype"
2727
"cloud.google.com/go/alloydbconn/internal/mock"
2828
"golang.org/x/oauth2"
@@ -127,7 +127,7 @@ func TestConnectInfo(t *testing.T) {
127127
wantAddr := "0.0.0.0"
128128
inst := mock.NewFakeInstance(
129129
"my-project", "my-region", "my-cluster", "my-instance",
130-
mock.WithIPAddr(wantAddr),
130+
mock.WithPrivateIP(wantAddr),
131131
)
132132
mc, url, cleanup := mock.HTTPClient(
133133
mock.InstanceGetSuccess(inst, 1),
@@ -156,7 +156,7 @@ func TestConnectInfo(t *testing.T) {
156156
t.Fatalf("failed to create mock instance: %v", err)
157157
}
158158

159-
gotAddr, _, err := i.ConnectInfo(ctx)
159+
gotAddr, _, err := i.ConnectInfo(ctx, PrivateIP)
160160
if err != nil {
161161
t.Fatalf("failed to retrieve connect info: %v", err)
162162
}
@@ -190,11 +190,17 @@ func TestConnectInfoErrors(t *testing.T) {
190190
t.Fatalf("failed to initialize Instance: %v", err)
191191
}
192192

193-
_, _, err = i.ConnectInfo(ctx)
193+
_, _, err = i.ConnectInfo(ctx, PrivateIP)
194194
var wantErr *errtype.DialError
195195
if !errors.As(err, &wantErr) {
196196
t.Fatalf("when connect info fails, want = %T, got = %v", wantErr, err)
197197
}
198+
199+
// when client asks for wrong IP address type
200+
gotAddr, _, err := i.ConnectInfo(ctx, PublicIP)
201+
if err == nil {
202+
t.Fatalf("expected ConnectInfo to fail but returned IP address = %v", gotAddr)
203+
}
198204
}
199205

200206
func TestClose(t *testing.T) {
@@ -214,7 +220,7 @@ func TestClose(t *testing.T) {
214220
}
215221
i.Close()
216222

217-
_, _, err = i.ConnectInfo(ctx)
223+
_, _, err = i.ConnectInfo(ctx, PrivateIP)
218224
if !errors.Is(err, context.Canceled) {
219225
t.Fatalf("failed to retrieve connect info: %v", err)
220226
}

internal/alloydb/refresh.go

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,23 @@ import (
2626
"strings"
2727
"time"
2828

29-
alloydbadmin "cloud.google.com/go/alloydb/apiv1beta"
30-
"cloud.google.com/go/alloydb/apiv1beta/alloydbpb"
29+
alloydbadmin "cloud.google.com/go/alloydb/apiv1alpha"
30+
"cloud.google.com/go/alloydb/apiv1alpha/alloydbpb"
3131
"cloud.google.com/go/alloydbconn/errtype"
3232
"cloud.google.com/go/alloydbconn/internal/trace"
3333
"google.golang.org/protobuf/types/known/durationpb"
3434
)
3535

36+
const (
37+
// PublicIP is the value for public IP connections.
38+
PublicIP = "PUBLIC"
39+
// PrivateIP is the value for private IP connections.
40+
PrivateIP = "PRIVATE"
41+
)
42+
3643
type connectInfo struct {
37-
// ipAddr is the instance's IP addresses
38-
ipAddr string
44+
// ipAddrs is the instance's IP addresses
45+
ipAddrs map[string]string
3946
// uid is the instance UID
4047
uid string
4148
}
@@ -56,7 +63,23 @@ func fetchMetadata(ctx context.Context, cl *alloydbadmin.AlloyDBAdminClient, ins
5663
if err != nil {
5764
return connectInfo{}, errtype.NewRefreshError("failed to get instance metadata", inst.String(), err)
5865
}
59-
return connectInfo{ipAddr: resp.IpAddress, uid: resp.InstanceUid}, nil
66+
67+
// parse any ip addresses that might be used to connect
68+
ipAddrs := make(map[string]string)
69+
if resp.GetIpAddress() != "" {
70+
ipAddrs[PrivateIP] = resp.GetIpAddress()
71+
}
72+
if resp.GetPublicIpAddress() != "" {
73+
ipAddrs[PublicIP] = resp.GetPublicIpAddress()
74+
}
75+
76+
if len(ipAddrs) == 0 {
77+
return connectInfo{}, errtype.NewConfigError(
78+
"cannot connect to instance - it has no supported IP addresses",
79+
inst.String(),
80+
)
81+
}
82+
return connectInfo{ipAddrs: ipAddrs, uid: resp.InstanceUid}, nil
6083
}
6184

6285
var errInvalidPEM = errors.New("certificate is not a valid PEM")
@@ -184,9 +207,9 @@ type refresher struct {
184207
}
185208

186209
type refreshResult struct {
187-
instanceIPAddr string
188-
conf *tls.Config
189-
expiry time.Time
210+
ipAddrs map[string]string
211+
conf *tls.Config
212+
expiry time.Time
190213
}
191214

192215
type certs struct {
@@ -254,9 +277,9 @@ func (r refresher) performRefresh(ctx context.Context, cn InstanceURI, k *rsa.Pr
254277
c := &tls.Config{
255278
Certificates: []tls.Certificate{cc.certChain},
256279
RootCAs: caCerts,
257-
ServerName: info.ipAddr,
280+
ServerName: info.ipAddrs[PrivateIP],
258281
MinVersion: tls.VersionTLS13,
259282
}
260283

261-
return refreshResult{instanceIPAddr: info.ipAddr, conf: c, expiry: cc.expiry}, nil
284+
return refreshResult{ipAddrs: info.ipAddrs, conf: c, expiry: cc.expiry}, nil
262285
}

internal/alloydb/refresh_test.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,16 @@ import (
2020
"testing"
2121
"time"
2222

23-
alloydbadmin "cloud.google.com/go/alloydb/apiv1beta"
23+
alloydbadmin "cloud.google.com/go/alloydb/apiv1alpha"
2424
"cloud.google.com/go/alloydbconn/internal/mock"
2525
"google.golang.org/api/option"
2626
)
2727

2828
const testDialerID = "some-dialer-id"
2929

3030
func TestRefresh(t *testing.T) {
31-
wantIP := "10.0.0.1"
31+
wantPrivateIP := "10.0.0.1"
32+
wantPublicIP := "127.0.0.1"
3233
wantExpiry := time.Now().Add(time.Hour).UTC().Round(time.Second)
3334
wantInstURI := "/projects/my-project/locations/my-region/clusters/my-cluster/instances/my-instance"
3435
cn, err := ParseInstURI(wantInstURI)
@@ -37,7 +38,8 @@ func TestRefresh(t *testing.T) {
3738
}
3839
inst := mock.NewFakeInstance(
3940
"my-project", "my-region", "my-cluster", "my-instance",
40-
mock.WithIPAddr(wantIP),
41+
mock.WithPrivateIP(wantPrivateIP),
42+
mock.WithPublicIP(wantPublicIP),
4143
mock.WithCertExpiry(wantExpiry),
4244
)
4345
mc, url, cleanup := mock.HTTPClient(
@@ -64,8 +66,19 @@ func TestRefresh(t *testing.T) {
6466
t.Fatalf("performRefresh unexpectedly failed with error: %v", err)
6567
}
6668

67-
if got := res.instanceIPAddr; wantIP != got {
68-
t.Fatalf("metadata IP mismatch, want = %v, got = %v", wantIP, got)
69+
gotIP, ok := res.ipAddrs[PrivateIP]
70+
if !ok {
71+
t.Fatal("metadata IP addresses did not include private address")
72+
}
73+
if wantPrivateIP != gotIP {
74+
t.Fatalf("metadata IP mismatch, want = %v, got = %v", wantPrivateIP, gotIP)
75+
}
76+
gotIP, ok = res.ipAddrs[PublicIP]
77+
if !ok {
78+
t.Fatal("metadata IP addresses did not include public address")
79+
}
80+
if wantPublicIP != gotIP {
81+
t.Fatalf("metadata IP mismatch, want = %v, got = %v", wantPublicIP, gotIP)
6982
}
7083
if got := res.expiry; wantExpiry != got {
7184
t.Fatalf("expiry mismatch, want = %v, got = %v", wantExpiry, got)

0 commit comments

Comments
 (0)