github.com/SaurabhDubey-Groww/go-cloud@v0.0.0-20221124105541-b26c29285fd8/mysql/gcpmysql/gcpmysql.go (about)

     1  // Copyright 2018 The Go Cloud Development Kit Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package gcpmysql provides connections to managed MySQL Cloud SQL instances.
    16  // See https://cloud.google.com/sql/docs/mysql/ for more information.
    17  //
    18  // # URLs
    19  //
    20  // For mysql.Open, gcpmysql registers for the scheme "gcpmysql".
    21  // The default URL opener will create a connection using the default
    22  // credentials from the environment, as described in
    23  // https://cloud.google.com/docs/authentication/production.
    24  // To customize the URL opener, or for more details on the URL format,
    25  // see URLOpener.
    26  //
    27  // See https://gocloud.dev/concepts/urls/ for background information.
    28  package gcpmysql // import "gocloud.dev/mysql/gcpmysql"
    29  
    30  import (
    31  	"context"
    32  	"database/sql"
    33  	"database/sql/driver"
    34  	"fmt"
    35  	"net/url"
    36  	"strings"
    37  	"sync"
    38  
    39  	"contrib.go.opencensus.io/integrations/ocsql"
    40  	"github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy"
    41  	"github.com/go-sql-driver/mysql"
    42  	"gocloud.dev/gcp"
    43  	"gocloud.dev/gcp/cloudsql"
    44  	cdkmysql "gocloud.dev/mysql"
    45  )
    46  
    47  // Scheme is the URL scheme gcpmysql registers its URLOpener under on
    48  // mysql.DefaultMux.
    49  const Scheme = "gcpmysql"
    50  
    51  func init() {
    52  	cdkmysql.DefaultURLMux().RegisterMySQL(Scheme, new(lazyCredsOpener))
    53  }
    54  
    55  // lazyCredsOpener obtains Application Default Credentials on the first call
    56  // to OpenMySQLURL.
    57  type lazyCredsOpener struct {
    58  	init   sync.Once
    59  	opener *URLOpener
    60  	err    error
    61  }
    62  
    63  func (o *lazyCredsOpener) OpenMySQLURL(ctx context.Context, u *url.URL) (*sql.DB, error) {
    64  	o.init.Do(func() {
    65  		creds, err := gcp.DefaultCredentials(ctx)
    66  		if err != nil {
    67  			o.err = err
    68  			return
    69  		}
    70  		client, err := gcp.NewHTTPClient(gcp.DefaultTransport(), creds.TokenSource)
    71  		if err != nil {
    72  			o.err = err
    73  			return
    74  		}
    75  		certSource := cloudsql.NewCertSource(client)
    76  		o.opener = &URLOpener{CertSource: certSource}
    77  	})
    78  	if o.err != nil {
    79  		return nil, fmt.Errorf("gcpmysql open %v: %v", u, o.err)
    80  	}
    81  	return o.opener.OpenMySQLURL(ctx, u)
    82  }
    83  
    84  // URLOpener opens Cloud MySQL URLs like
    85  // "gcpmysql://user:password@project/region/instance/dbname".
    86  type URLOpener struct {
    87  	// CertSource specifies how the opener will obtain authentication information.
    88  	// CertSource must not be nil.
    89  	CertSource proxy.CertSource
    90  
    91  	// TraceOpts contains options for OpenCensus.
    92  	TraceOpts []ocsql.TraceOption
    93  }
    94  
    95  // OpenMySQLURL opens a new GCP database connection wrapped with OpenCensus instrumentation.
    96  func (uo *URLOpener) OpenMySQLURL(ctx context.Context, u *url.URL) (*sql.DB, error) {
    97  	if uo.CertSource == nil {
    98  		return nil, fmt.Errorf("gcpmysql: URLOpener CertSource is nil")
    99  	}
   100  	// TODO(light): Avoid global registry once https://github.com/go-sql-driver/mysql/issues/771 is fixed.
   101  	dialerCounter.mu.Lock()
   102  	dialerNum := dialerCounter.n
   103  	dialerCounter.mu.Unlock()
   104  	dialerName := fmt.Sprintf("gocloud.dev/mysql/gcpmysql/%d", dialerNum)
   105  
   106  	cfg, err := configFromURL(u, dialerName)
   107  	if err != nil {
   108  		return nil, fmt.Errorf("gcpmysql: open config %v", err)
   109  	}
   110  
   111  	client := &proxy.Client{
   112  		Port:  3307,
   113  		Certs: uo.CertSource,
   114  	}
   115  	mysql.RegisterDial(dialerName, client.Dial)
   116  
   117  	db := sql.OpenDB(connector{cfg.FormatDSN(), uo.TraceOpts})
   118  	return db, nil
   119  }
   120  
   121  func configFromURL(u *url.URL, dialerName string) (*mysql.Config, error) {
   122  	instance, dbName, err := instanceFromURL(u)
   123  	if err != nil {
   124  		return nil, err
   125  	}
   126  
   127  	var cfg *mysql.Config
   128  	switch {
   129  	case len(u.RawQuery) > 0:
   130  		optDsn := fmt.Sprintf("/%s?%s", dbName, u.RawQuery)
   131  		if cfg, err = mysql.ParseDSN(optDsn); err != nil {
   132  			return nil, err
   133  		}
   134  	default:
   135  		cfg = mysql.NewConfig()
   136  	}
   137  
   138  	password, _ := u.User.Password()
   139  
   140  	cfg.AllowNativePasswords = true
   141  	cfg.Net = dialerName
   142  	cfg.Addr = instance
   143  	cfg.User = u.User.Username()
   144  	cfg.Passwd = password
   145  	cfg.DBName = dbName
   146  
   147  	return cfg, nil
   148  }
   149  
   150  func instanceFromURL(u *url.URL) (instance, db string, _ error) {
   151  	path := u.Host + u.Path // everything after scheme but before query or fragment
   152  	parts := strings.SplitN(path, "/", 4)
   153  	if len(parts) < 4 {
   154  		return "", "", fmt.Errorf("%s is not in the form project/region/instance/dbname", path)
   155  	}
   156  	for _, part := range parts {
   157  		if part == "" {
   158  			return "", "", fmt.Errorf("%s is not in the form project/region/instance/dbname", path)
   159  		}
   160  	}
   161  	return parts[0] + ":" + parts[1] + ":" + parts[2], parts[3], nil
   162  }
   163  
   164  var dialerCounter struct {
   165  	mu sync.Mutex
   166  	n  int
   167  }
   168  
   169  type connector struct {
   170  	dsn       string
   171  	traceOpts []ocsql.TraceOption
   172  }
   173  
   174  func (c connector) Connect(ctx context.Context) (driver.Conn, error) {
   175  	return c.Driver().Open(c.dsn)
   176  }
   177  
   178  func (c connector) Driver() driver.Driver {
   179  	return ocsql.Wrap(mysql.MySQLDriver{}, c.traceOpts...)
   180  }