2424// To customize the URL opener, or for more details on the URL format,
2525// see URLOpener.
2626//
27+ // # IP Type
28+ //
29+ // By default, connections use auto-IP selection (public IP if available,
30+ // otherwise private IP), matching the behavior of the legacy cloudsql-proxy.
31+ // To use a specific IP type, set URLOpener.IPType or pass the "ip_type" query
32+ // parameter in the URL (requires URLOpener.Dialer to be set):
33+ //
34+ // gcpmysql://user:pass@project/region/instance/dbname?ip_type=psc
35+ //
36+ // Valid values for ip_type: auto, public, private, psc.
37+ //
2738// See https://gocloud.dev/concepts/urls/ for background information.
2839package gcpmysql // import "gocloud.dev/mysql/gcpmysql"
2940
@@ -37,6 +48,7 @@ import (
3748 "strings"
3849 "sync"
3950
51+ "cloud.google.com/go/cloudsqlconn"
4052 "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy"
4153 "github.com/XSAM/otelsql"
4254 "github.com/go-sql-driver/mysql"
@@ -68,13 +80,13 @@ func (o *lazyCredsOpener) OpenMySQLURL(ctx context.Context, u *url.URL) (*sql.DB
6880 o .err = err
6981 return
7082 }
71- client , err := gcp .NewHTTPClient (gcp .DefaultTransport (), creds .TokenSource )
83+ // Ignore cleanup: the dialer lives for the process lifetime in this global opener.
84+ d , _ , err := cloudsql .NewDialer (ctx , creds .TokenSource )
7285 if err != nil {
7386 o .err = err
7487 return
7588 }
76- certSource := cloudsql .NewCertSource (client )
77- o .opener = & URLOpener {CertSource : certSource }
89+ o .opener = & URLOpener {Dialer : d }
7890 })
7991 if o .err != nil {
8092 return nil , fmt .Errorf ("gcpmysql open %v: %v" , u , o .err )
@@ -85,35 +97,72 @@ func (o *lazyCredsOpener) OpenMySQLURL(ctx context.Context, u *url.URL) (*sql.DB
8597// URLOpener opens Cloud MySQL URLs like
8698// "gcpmysql://user:password@project/region/instance/dbname".
8799type URLOpener struct {
100+ // Dialer creates Cloud SQL connections using the Cloud SQL Go Connector.
101+ // Supports PSC and explicit IP type selection via IPType or the "ip_type"
102+ // URL query parameter.
103+ // If both Dialer and CertSource are set, Dialer takes precedence.
104+ Dialer * cloudsqlconn.Dialer
105+
88106 // CertSource specifies how the opener will obtain authentication information.
89- // CertSource must not be nil.
107+ //
108+ // Deprecated: Use Dialer instead. CertSource does not support PSC or
109+ // explicit IP type selection. Ignored if Dialer is also set.
90110 CertSource proxy.CertSource
91111
92112 // TraceOpts contains options for OpenTelemetry.
93113 TraceOpts []otelsql.Option
114+
115+ // IPType controls which IP address type is used to connect to the Cloud SQL
116+ // instance. Only applies when Dialer is set; ignored for CertSource connections.
117+ // Defaults to IPTypeAuto (public IP if available, otherwise private).
118+ // Can be overridden per-URL via the "ip_type" query parameter.
119+ IPType cloudsql.IPType
94120}
95121
96122// OpenMySQLURL opens a new GCP database connection wrapped with OpenTelemetry instrumentation.
97123func (uo * URLOpener ) OpenMySQLURL (ctx context.Context , u * url.URL ) (* sql.DB , error ) {
98- if uo .CertSource == nil {
99- return nil , fmt .Errorf ("gcpmysql: URLOpener CertSource is nil" )
124+ if uo .Dialer == nil && uo . CertSource == nil {
125+ return nil , fmt .Errorf ("gcpmysql: URLOpener Dialer and CertSource are both nil" )
100126 }
101- var (
102- client = & proxy.Client {Certs : uo .CertSource , Port : 3307 }
103- cfg , err = configFromURL (u )
104- )
127+
128+ cfg , err := configFromURL (u )
105129 if err != nil {
106130 return nil , fmt .Errorf ("gcpmysql: open config %v" , err )
107131 }
108- cfg .DialFunc = func (ctx context.Context , _ , addr string ) (net.Conn , error ) {
109- // MySQL driver's addr is in the form "[host]:3306" after normalized.
110- // https://github.com/go-sql-driver/mysql/blob/76c00e35a8d48f8f70f0e7dffe584692bd3fa612/dsn.go#L193-L195
111- instance , _ , err := net .SplitHostPort (addr )
112- if err != nil {
113- return nil , err
132+
133+ if uo .Dialer != nil {
134+ // New path: Cloud SQL Go Connector with IP type and PSC support.
135+ ipType := uo .IPType
136+ if s := u .Query ().Get ("ip_type" ); s != "" {
137+ parsed , err := parseIPType (s )
138+ if err != nil {
139+ return nil , fmt .Errorf ("gcpmysql: open: %v" , err )
140+ }
141+ ipType = parsed
142+ }
143+ dialOpts := ipType .DialOptions ()
144+ d := uo .Dialer
145+ cfg .DialFunc = func (ctx context.Context , _ , addr string ) (net.Conn , error ) {
146+ // MySQL driver's addr is in the form "[host]:3306" after normalized.
147+ // https://github.com/go-sql-driver/mysql/blob/76c00e35a8d48f8f70f0e7dffe584692bd3fa612/dsn.go#L193-L195
148+ instance , _ , err := net .SplitHostPort (addr )
149+ if err != nil {
150+ return nil , err
151+ }
152+ return d .Dial (ctx , instance , dialOpts ... )
153+ }
154+ } else {
155+ // Legacy path: cloudsql-proxy v1 (no PSC or IP type selection).
156+ client := & proxy.Client {Certs : uo .CertSource , Port : 3307 }
157+ cfg .DialFunc = func (ctx context.Context , _ , addr string ) (net.Conn , error ) {
158+ instance , _ , err := net .SplitHostPort (addr )
159+ if err != nil {
160+ return nil , err
161+ }
162+ return client .DialContext (ctx , instance )
114163 }
115- return client .DialContext (ctx , instance )
116164 }
165+
117166 c , err := mysql .NewConnector (cfg )
118167 if err != nil {
119168 return nil , fmt .Errorf ("gcpmysql: open connector %v" , err )
@@ -127,10 +176,14 @@ func configFromURL(u *url.URL) (*mysql.Config, error) {
127176 return nil , err
128177 }
129178
179+ // Strip ip_type from query before passing to MySQL DSN parser.
180+ query := u .Query ()
181+ query .Del ("ip_type" )
182+
130183 var cfg * mysql.Config
131184 switch {
132- case len (u . RawQuery ) > 0 :
133- optDsn := fmt .Sprintf ("/%s?%s" , dbName , u . RawQuery )
185+ case len (query ) > 0 :
186+ optDsn := fmt .Sprintf ("/%s?%s" , dbName , query . Encode () )
134187 if cfg , err = mysql .ParseDSN (optDsn ); err != nil {
135188 return nil , err
136189 }
@@ -161,3 +214,18 @@ func instanceFromURL(u *url.URL) (instance, db string, _ error) {
161214 }
162215 return parts [0 ] + ":" + parts [1 ] + ":" + parts [2 ], parts [3 ], nil
163216}
217+
218+ func parseIPType (s string ) (cloudsql.IPType , error ) {
219+ switch strings .ToLower (s ) {
220+ case "auto" , "" :
221+ return cloudsql .IPTypeAuto , nil
222+ case "public" :
223+ return cloudsql .IPTypePublic , nil
224+ case "private" :
225+ return cloudsql .IPTypePrivate , nil
226+ case "psc" :
227+ return cloudsql .IPTypePSC , nil
228+ default :
229+ return 0 , fmt .Errorf ("unknown ip_type %q (valid: auto, public, private, psc)" , s )
230+ }
231+ }
0 commit comments