Skip to content

Commit

Permalink
feat: expose Dialer and add DialerOptions (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
kurtisvg authored Apr 2, 2021
1 parent 7e89552 commit 1235a9f
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 74 deletions.
82 changes: 9 additions & 73 deletions dial.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,95 +17,31 @@ package cloudsqlconn

import (
"context"
"crypto/rand"
"crypto/rsa"
"fmt"
"net"
"sync"

"cloud.google.com/cloudsqlconn/internal/cloudsql"
sqladmin "google.golang.org/api/sqladmin/v1beta4"
)

var (
once sync.Once
dm *dialManager
dErr error
once sync.Once
defaultDialer *Dialer
dErr error
)

// Dial returns a net.Conn connected to the specified Cloud SQL instance. The instance argument must be the
// instance's connection name, which is in the format "project-name:region:instance-name".
func Dial(ctx context.Context, instance string) (net.Conn, error) {
d, err := defaultDialer()
d, err := getDefaultDialer()
if err != nil {
return nil, err
}
return d.dial(ctx, instance)
return d.Dial(ctx, instance)
}

// defaultDialer provides the singleton dialer as a default for dial functions.
func defaultDialer() (*dialManager, error) {
// getDefaultDialer provides the singleton dialer as a default for dial functions.
func getDefaultDialer() (*Dialer, error) {
// TODO: Provide functionality for customizing/setting the default dialer
once.Do(func() {
dm, dErr = newDialManager()
defaultDialer, dErr = NewDialer(context.Background())
})
return dm, dErr
}

type dialManager struct {
lock sync.RWMutex
instances map[string]*cloudsql.Instance

sqladmin *sqladmin.Service
key *rsa.PrivateKey
}

func newDialManager() (*dialManager, error) {
// TODO: Add ability to customize keys / clients
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, fmt.Errorf("failed to generate rsa keys: %v", err)
}
client, err := sqladmin.NewService(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to create sqladmin client: %v", err)
}
d := &dialManager{
instances: make(map[string]*cloudsql.Instance),
sqladmin: client,
key: key,
}
return d, nil
}

func (d *dialManager) instance(connName string) (*cloudsql.Instance, error) {
// Check instance cache
d.lock.RLock()
i, ok := d.instances[connName]
d.lock.RUnlock()
if !ok {
d.lock.Lock()
// Recheck to ensure instance wasn't created between locks
i, ok = d.instances[connName]
if !ok {
// Create a new instance
var err error
i, err = cloudsql.NewInstance(connName, d.sqladmin, d.key)
if err != nil {
d.lock.Unlock()
return nil, err
}
d.instances[connName] = i
}
d.lock.Unlock()
}
return i, nil
}

func (d *dialManager) dial(ctx context.Context, instance string) (net.Conn, error) {
i, err := d.instance(instance)
if err != nil {
return nil, err
}
return i.Connect(ctx)
return defaultDialer, dErr
}
99 changes: 99 additions & 0 deletions dialer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright 2020 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

// https://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.

package cloudsqlconn

import (
"context"
"crypto/rand"
"crypto/rsa"
"fmt"
"net"
"sync"

"cloud.google.com/cloudsqlconn/internal/cloudsql"
apiopt "google.golang.org/api/option"
sqladmin "google.golang.org/api/sqladmin/v1beta4"
)

type dialerConfig struct {
sqladminOpts []apiopt.ClientOption
}

// A Dialer is used to create connections to Cloud SQL instances.
type Dialer struct {
lock sync.RWMutex
instances map[string]*cloudsql.Instance
key *rsa.PrivateKey

sqladmin *sqladmin.Service
}

// NewDialer creates a new Dialer.
func NewDialer(ctx context.Context, opts ...DialerOption) (*Dialer, error) {
// TODO: Add shared / async key generation
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, fmt.Errorf("failed to generate rsa keys: %v", err)
}

cfg := &dialerConfig{}
for _, opt := range opts {
opt(cfg)
}

client, err := sqladmin.NewService(context.Background(), cfg.sqladminOpts...)
if err != nil {
return nil, fmt.Errorf("failed to create sqladmin client: %v", err)
}
d := &Dialer{
instances: make(map[string]*cloudsql.Instance),
sqladmin: client,
key: key,
}
return d, nil
}

// Dial creates an authorized connection to a Cloud SQL instance specified by it's instance connection name.
func (d *Dialer) Dial(ctx context.Context, instance string) (net.Conn, error) {
i, err := d.instance(instance)
if err != nil {
return nil, err
}
return i.Connect(ctx)
}

func (d *Dialer) instance(connName string) (*cloudsql.Instance, error) {
// Check instance cache
d.lock.RLock()
i, ok := d.instances[connName]
d.lock.RUnlock()
if !ok {
d.lock.Lock()
// Recheck to ensure instance wasn't created between locks
i, ok = d.instances[connName]
if !ok {
// Create a new instance
var err error
i, err = cloudsql.NewInstance(connName, d.sqladmin, d.key)
if err != nil {
d.lock.Unlock()
return nil, err
}
d.instances[connName] = i
}
d.lock.Unlock()
}
return i, nil
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.14
require (
github.com/jackc/pgx/v4 v4.10.1
golang.org/x/net v0.0.0-20200904194848-62affa334b73
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 // indirect
google.golang.org/api v0.31.0
google.golang.org/genproto v0.0.0-20200911024640-645f7a48b24f // indirect
Expand Down
53 changes: 53 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2020 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

// https://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.

package cloudsqlconn

import (
"golang.org/x/oauth2"
apiopt "google.golang.org/api/option"
)

// A DialerOption is an option for configuring a Dialer.
type DialerOption func(d *dialerConfig)

// DialerOptions turns a list of DialerOption instances into a DialerOption.
func DialerOptions(opts ...DialerOption) DialerOption {
return func(d *dialerConfig) {
for _, opt := range opts {
opt(d)
}
}
}

// WithCredentialsFile returns a DialerOption that specifies a service account or refresh token JSON credentials file to be used as the basis for authentication.
func WithCredentialsFile(filename string) DialerOption {
return func(d *dialerConfig) {
d.sqladminOpts = append(d.sqladminOpts, apiopt.WithCredentialsFile(filename))
}
}

// WithCredentialsJSON returns a DialerOption that specifies a service account or refresh token JSON credentials to be used as the basis for authentication.
func WithCredentialsJSON(p []byte) DialerOption {
return func(d *dialerConfig) {
d.sqladminOpts = append(d.sqladminOpts, apiopt.WithCredentialsJSON(p))
}
}

// WithTokenSource returns a DialerOption that specifies an OAuth2 token source to be used as the basis for authentication.
func WithTokenSource(s oauth2.TokenSource) DialerOption {
return func(d *dialerConfig) {
d.sqladminOpts = append(d.sqladminOpts, apiopt.WithTokenSource(s))
}
}

0 comments on commit 1235a9f

Please sign in to comment.