github.com/thiagoyeds/go-cloud@v0.26.0/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 }