From 1235a9f62beb678f18695afc6d22d0b8e6b7b506 Mon Sep 17 00:00:00 2001 From: Kurtis Van Gent <31518063+kurtisvg@users.noreply.github.com> Date: Fri, 2 Apr 2021 15:28:19 -0700 Subject: [PATCH] feat: expose Dialer and add DialerOptions (#7) --- dial.go | 82 +++++--------------------------------------- dialer.go | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- options.go | 53 +++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+), 74 deletions(-) create mode 100644 dialer.go create mode 100644 options.go diff --git a/dial.go b/dial.go index 1632b092..7b85f556 100644 --- a/dial.go +++ b/dial.go @@ -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 } diff --git a/dialer.go b/dialer.go new file mode 100644 index 00000000..1a2ad8a1 --- /dev/null +++ b/dialer.go @@ -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 +} diff --git a/go.mod b/go.mod index 891acc81..bf4080ae 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/options.go b/options.go new file mode 100644 index 00000000..0a453d48 --- /dev/null +++ b/options.go @@ -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)) + } +}