X Tutup
Skip to content

Commit f440380

Browse files
committed
sa: use mysql.Config to set flags on connect strings
This also exposes an interface to open a connection using a mysql.Config, so we can start changing config files to use those. Part of letsencrypt#1505
1 parent f568f63 commit f440380

File tree

2 files changed

+99
-43
lines changed

2 files changed

+99
-43
lines changed

sa/database.go

Lines changed: 70 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,50 @@ import (
1111
"net/url"
1212
"strings"
1313

14-
// Provide access to the MySQL driver
15-
_ "github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/go-sql-driver/mysql"
14+
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/go-sql-driver/mysql"
1615
gorp "github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
1716
"github.com/letsencrypt/boulder/core"
1817
blog "github.com/letsencrypt/boulder/log"
1918
)
2019

2120
// NewDbMap creates the root gorp mapping object. Create one of these for each
22-
// database schema you wish to map. Each DbMap contains a list of mapped tables.
23-
// It automatically maps the tables for the primary parts of Boulder around the
24-
// Storage Authority. This may require some further work when we use a disjoint
25-
// schema, like that for `certificate-authority-data.go`.
21+
// database schema you wish to map. Each DbMap contains a list of mapped
22+
// tables. It automatically maps the tables for the primary parts of Boulder
23+
// around the Storage Authority.
2624
func NewDbMap(dbConnect string) (*gorp.DbMap, error) {
27-
logger := blog.GetAuditLogger()
28-
2925
var err error
30-
dbConnect, err = recombineURLForDB(dbConnect)
26+
var config *mysql.Config
27+
if strings.HasPrefix(dbConnect, "mysql+tcp://") {
28+
dbConnect, err = recombineCustomMySQLURL(dbConnect)
29+
if err != nil {
30+
return nil, err
31+
}
32+
}
33+
34+
config, err = mysql.ParseDSN(dbConnect)
3135
if err != nil {
3236
return nil, err
3337
}
3438

39+
return NewDbMapFromConfig(config)
40+
}
41+
42+
// sqlOpen is used in the tests to check that the arguments are properly
43+
// transformed
44+
var sqlOpen = func(dbType, connectStr string) (*sql.DB, error) {
45+
return sql.Open(dbType, connectStr)
46+
}
47+
48+
// NewDbMapFromConfig functions similarly to NewDbMap, but it takes the
49+
// decomposed form of the connection string, a *mysql.Config.
50+
func NewDbMapFromConfig(config *mysql.Config) (*gorp.DbMap, error) {
51+
adjustMySQLConfig(config)
52+
53+
logger := blog.GetAuditLogger()
54+
3555
logger.Debug("Connecting to database")
3656

37-
db, err := sql.Open("mysql", dbConnect)
57+
db, err := sqlOpen("mysql", config.FormatDSN())
3858
if err != nil {
3959
return nil, err
4060
}
@@ -52,18 +72,45 @@ func NewDbMap(dbConnect string) (*gorp.DbMap, error) {
5272
return dbmap, err
5373
}
5474

55-
// recombineURLForDB transforms a database URL to a URL-like string
56-
// that the mysql driver can use. The mysql driver needs the Host data
57-
// to be wrapped in "tcp()" but url.Parse will escape the parentheses
58-
// and the mysql driver doesn't understand them. So, we can't have
59-
// "tcp()" in the configs, but can't leave it out before passing it to
60-
// the mysql driver. Similarly, the driver needs the password and
61-
// username unescaped. Compromise by doing the leg work if the config
62-
// says the database URL's scheme is a fake one called
63-
// "mysql+tcp://". See
64-
// https://github.com/go-sql-driver/mysql/issues/362 for why we have
65-
// to futz around and avoid URL.String.
66-
func recombineURLForDB(dbConnect string) (string, error) {
75+
// adjustMySQLConfig sets certain flags that we want on every connection.
76+
func adjustMySQLConfig(conf *mysql.Config) *mysql.Config {
77+
// Required to turn DATETIME fields into time.Time
78+
conf.ParseTime = true
79+
80+
// Required to make UPDATE return the number of rows matched,
81+
// instead of the number of rows changed by the UPDATE.
82+
conf.ClientFoundRows = true
83+
84+
// Ensures that MySQL/MariaDB warnings are treated as errors. This
85+
// avoids a number of nasty edge conditions we could wander into.
86+
// Common things this discovers includes places where data being sent
87+
// had a different type than what is in the schema, strings being
88+
// truncated, writing null to a NOT NULL column, and so on. See
89+
// <https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sql-mode-strict>.
90+
conf.Strict = true
91+
92+
return conf
93+
}
94+
95+
// recombineCustomMySQLURL transforms the legacy database URLs into the
96+
// URL-like strings expected by the mysql database driver.
97+
//
98+
// In the past, changes to the connection string were achieved by passing it
99+
// into url.Parse and editing the query string that way, so the string had to
100+
// be a valid URL. The mysql driver needs the Host data to be wrapped in
101+
// "tcp()" but url.Parse will escape the parentheses and the mysql driver
102+
// doesn't understand them. So, we couldn't have "tcp()" in the configs, but
103+
// couldn't leave it out before passing it to the mysql driver. Similarly, the
104+
// driver needs the password and username unescaped. The compromise was to do
105+
// the leg work if the connection string's scheme is a fake one called
106+
// "mysql+tcp://".
107+
//
108+
// Upon the addition of
109+
// https://godoc.org/github.com/go-sql-driver/mysql#Config, this was no longer
110+
// necessary, as the changes could be made on the decomposed struct version of
111+
// the connection url. This method converts the old format into the format
112+
// expected by the library.
113+
func recombineCustomMySQLURL(dbConnect string) (string, error) {
67114
dbConnect = strings.TrimSpace(dbConnect)
68115
dbURL, err := url.Parse(dbConnect)
69116
if err != nil {
@@ -75,26 +122,6 @@ func recombineURLForDB(dbConnect string) (string, error) {
75122
return "", fmt.Errorf(format, dbURL.Scheme)
76123
}
77124

78-
dsnVals, err := url.ParseQuery(dbURL.RawQuery)
79-
if err != nil {
80-
return "", err
81-
}
82-
83-
dsnVals.Set("parseTime", "true")
84-
85-
// Required to make UPDATE return the number of rows matched,
86-
// instead of the number of rows changed by the UPDATE.
87-
dsnVals.Set("clientFoundRows", "true")
88-
89-
// Ensures that MySQL/MariaDB warnings are treated as errors. This
90-
// avoids a number of nasty edge conditions we could wander
91-
// into. Common things this discovers includes places where data
92-
// being sent had a different type than what is in the schema,
93-
// strings being truncated, writing null to a NOT NULL column, and
94-
// so on. See
95-
// <https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sql-mode-strict>.
96-
dsnVals.Set("strict", "true")
97-
98125
user := dbURL.User.Username()
99126
passwd, hasPass := dbURL.User.Password()
100127
dbConn := ""
@@ -105,7 +132,7 @@ func recombineURLForDB(dbConnect string) (string, error) {
105132
dbConn += ":" + passwd
106133
}
107134
dbConn += "@tcp(" + dbURL.Host + ")"
108-
return dbConn + dbURL.EscapedPath() + "?" + dsnVals.Encode(), nil
135+
return dbConn + dbURL.EscapedPath() + "?" + dbURL.RawQuery, nil
109136
}
110137

111138
// SetSQLDebug enables/disables GORP SQL-level Debugging

sa/database_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
package sa
77

88
import (
9+
"database/sql"
10+
"errors"
911
"testing"
1012

1113
"github.com/letsencrypt/boulder/test"
@@ -15,3 +17,30 @@ func TestInvalidDSN(t *testing.T) {
1517
_, err := NewDbMap("invalid")
1618
test.AssertError(t, err, "DB connect string missing the slash separating the database name")
1719
}
20+
21+
var errExpected = errors.New("expected")
22+
23+
func TestNewDbMap(t *testing.T) {
24+
const mysqlConnectUrl = "mysql+tcp://policy:password@localhost:3306/boulder_policy_integration?readTimeout=800ms&writeTimeout=800ms"
25+
const expectedTransformed = "policy:password@tcp(localhost:3306)/boulder_policy_integration?clientFoundRows=true&parseTime=true&readTimeout=800ms&strict=true&writeTimeout=800ms"
26+
27+
oldSqlOpen := sqlOpen
28+
defer func() {
29+
sqlOpen = oldSqlOpen
30+
}()
31+
sqlOpen = func(dbType, connectString string) (*sql.DB, error) {
32+
if connectString != expectedTransformed {
33+
t.Errorf("incorrect connection string mangling, got %v", connectString)
34+
}
35+
return nil, errExpected
36+
}
37+
38+
dbMap, err := NewDbMap(mysqlConnectUrl)
39+
if err != errExpected {
40+
t.Errorf("got incorrect error: %v", err)
41+
}
42+
if dbMap != nil {
43+
t.Errorf("expected nil, got %v", dbMap)
44+
}
45+
46+
}

0 commit comments

Comments
 (0)
X Tutup