OLD | NEW |
1 package database | 1 package database |
2 | 2 |
3 import ( | 3 import ( |
| 4 "bufio" |
4 "database/sql" | 5 "database/sql" |
5 "fmt" | 6 "fmt" |
| 7 "os" |
6 "time" | 8 "time" |
7 | 9 |
| 10 "strings" |
| 11 |
8 _ "github.com/go-sql-driver/mysql" | 12 _ "github.com/go-sql-driver/mysql" |
9 "github.com/golang/glog" | 13 "github.com/golang/glog" |
10 » _ "github.com/mattn/go-sqlite3" | 14 » "skia.googlesource.com/buildbot.git/go/metadata" |
11 "skia.googlesource.com/buildbot.git/go/util" | 15 "skia.googlesource.com/buildbot.git/go/util" |
12 ) | 16 ) |
13 | 17 |
| 18 const ( |
| 19 // Template for database address given a hostname and port number. |
| 20 DB_ADDR_TMPL = "tcp(%s:%s)" |
| 21 |
| 22 // Template for DB connection strings. |
| 23 DB_CONN_TMPL = "%s:%s@%s/%s?parseTime=true" |
| 24 |
| 25 // Local test database. |
| 26 TEST_DB_HOST = "" |
| 27 TEST_USER_ROOT = "test_root" |
| 28 TEST_PASSWORD = "" |
| 29 |
| 30 // Name of the root user. |
| 31 USER_ROOT = "root" |
| 32 |
| 33 // Name of the readwrite user. |
| 34 USER_RW = "readwrite" |
| 35 |
| 36 // Key of the password for the readwrite user. |
| 37 RW_METADATA_KEY = "readwrite" |
| 38 ) |
| 39 |
| 40 // AddrFromHostPort takes a host and port and returns a database address. |
| 41 func AddrFromHostPort(host, port string) string { |
| 42 return fmt.Sprintf(DB_ADDR_TMPL, host, port) |
| 43 } |
| 44 |
14 // Config information to create a database connection. | 45 // Config information to create a database connection. |
15 type DatabaseConfig struct { | 46 type DatabaseConfig struct { |
16 MySQLString string | 47 MySQLString string |
17 SQLiteFilePath string | |
18 MigrationSteps []MigrationStep | 48 MigrationSteps []MigrationStep |
19 } | 49 } |
20 | 50 |
| 51 // NewDatabaseConfig constructs a DatabaseConfig from the given options. |
| 52 func NewDatabaseConfig(user, password, host, database string, m []MigrationStep)
*DatabaseConfig { |
| 53 return &DatabaseConfig{ |
| 54 MySQLString: fmt.Sprintf(DB_CONN_TMPL, user, password, host,
database), |
| 55 MigrationSteps: m, |
| 56 } |
| 57 } |
| 58 |
| 59 // LocalDatabaseConfig returns a DatabaseConfig appropriate for local use. |
| 60 func LocalDatabaseConfig(database string, m []MigrationStep) *DatabaseConfig { |
| 61 return NewDatabaseConfig(USER_RW, TEST_PASSWORD, TEST_DB_HOST, database,
m) |
| 62 } |
| 63 |
| 64 // LocalDatabaseConfig returns a DatabaseConfig appropriate use in a local |
| 65 // testing-only database. |
| 66 func LocalTestDatabaseConfig(database string, m []MigrationStep) *DatabaseConfig
{ |
| 67 return NewDatabaseConfig(USER_RW, TEST_PASSWORD, TEST_DB_HOST, database,
m) |
| 68 } |
| 69 |
| 70 // LocalDatabaseConfig returns a DatabaseConfig appropriate use in a local |
| 71 // testing-only database with root access. |
| 72 func LocalTestRootDatabaseConfig(database string, m []MigrationStep) *DatabaseCo
nfig { |
| 73 return NewDatabaseConfig(TEST_USER_ROOT, TEST_PASSWORD, TEST_DB_HOST, da
tabase, m) |
| 74 } |
| 75 |
| 76 // ProdDatabaseConfig returns a DatabaseConfig appropriate for running in |
| 77 // production. |
| 78 func ProdDatabaseConfig(host, database string, m []MigrationStep) *DatabaseConfi
g { |
| 79 // First, get the password from the metadata server. |
| 80 // See https://developers.google.com/compute/docs/metadata#custom. |
| 81 password, err := metadata.Get(RW_METADATA_KEY) |
| 82 if err != nil { |
| 83 glog.Fatalf("Failed to find metadata. Use 'local' flag when runn
ing locally.") |
| 84 } |
| 85 return NewDatabaseConfig(USER_RW, password, host, database, m) |
| 86 } |
| 87 |
| 88 // ResolveCustomMySQLString determines whether a password was entered as part |
| 89 // of the given string, prompts for the password if necessary, and then returns |
| 90 // a DatabaseConfig. |
| 91 func ResolveCustomMySQLString(mySQLConnStr string, m []MigrationStep) (*Database
Config, error) { |
| 92 useMySQLConnStr := mySQLConnStr |
| 93 if strings.Contains(useMySQLConnStr, "%s") { |
| 94 // Assume the missing part of the string is a password, prompt |
| 95 // and replace. |
| 96 reader := bufio.NewReader(os.Stdin) |
| 97 fmt.Print("Enter password for MySQL: ") |
| 98 password, err := reader.ReadString('\n') |
| 99 if err != nil { |
| 100 return nil, fmt.Errorf("Failed to get password: %v", err
) |
| 101 } |
| 102 useMySQLConnStr = fmt.Sprintf(useMySQLConnStr, strings.TrimRight
(password, "\n")) |
| 103 } |
| 104 return &DatabaseConfig{ |
| 105 MySQLString: useMySQLConnStr, |
| 106 MigrationSteps: m, |
| 107 }, nil |
| 108 } |
| 109 |
21 // Single step to migrated from one database version to the next and back. | 110 // Single step to migrated from one database version to the next and back. |
22 type MigrationStep struct { | 111 type MigrationStep struct { |
23 » MySQLUp []string | 112 » MySQLUp []string |
24 » MySQLDown []string | 113 » MySQLDown []string |
25 » SQLiteUp []string | |
26 » SQLiteDown []string | |
27 } | 114 } |
28 | 115 |
29 // Database handle to send queries to the underlying database. | 116 // Database handle to send queries to the underlying database. |
30 type VersionedDB struct { | 117 type VersionedDB struct { |
31 » // Database intance that is either backed by SQLite or MySQl. | 118 » // Database intance that is backed by MySQL. |
32 DB *sql.DB | 119 DB *sql.DB |
33 | 120 |
34 // Keeps track if we are connected to MySQL or SQLite | |
35 IsMySQL bool | |
36 | |
37 // List of migration steps for this database. | 121 // List of migration steps for this database. |
38 migrationSteps []MigrationStep | 122 migrationSteps []MigrationStep |
39 } | 123 } |
40 | 124 |
41 // Init must be called once before DB is used. | 125 // Init must be called once before DB is used. |
42 // | 126 // |
43 // Since it used glog, make sure it is also called after flag.Parse is called. | 127 // Since it used glog, make sure it is also called after flag.Parse is called. |
44 func NewVersionedDB(conf *DatabaseConfig) *VersionedDB { | 128 func NewVersionedDB(conf *DatabaseConfig) *VersionedDB { |
45 // If there is a connection string then connect to the MySQL server. | 129 // If there is a connection string then connect to the MySQL server. |
46 // This is for testing only. In production we get the relevant informati
on | 130 // This is for testing only. In production we get the relevant informati
on |
47 // from the metadata server. | 131 // from the metadata server. |
48 var err error | 132 var err error |
49 var isMySQL = true | |
50 var DB *sql.DB = nil | 133 var DB *sql.DB = nil |
51 | 134 |
52 » if conf.MySQLString != "" { | 135 » glog.Infoln("Opening SQL database.") |
53 » » glog.Infoln("Opening SQL database.") | 136 » if DB, err = sql.Open("mysql", conf.MySQLString); err == nil { |
54 » » if DB, err = sql.Open("mysql", conf.MySQLString); err == nil { | 137 » » glog.Infoln("Sending Ping.") |
55 » » » glog.Infoln("Sending Ping.") | 138 » » err = DB.Ping() |
56 » » » err = DB.Ping() | 139 » } |
57 » » } | |
58 | 140 |
59 » » if err != nil { | 141 » if err != nil { |
60 » » » glog.Fatalln("Failed to open connection to SQL server:",
err) | 142 » » glog.Fatalln("Failed to open connection to SQL server:", err) |
61 » » } | |
62 » } else { | |
63 » » // Open a local SQLite database instead. | |
64 » » glog.Infof("Opening local sqlite database at: %s", conf.SQLiteFi
lePath) | |
65 » » // Fallback to sqlite for local use. | |
66 » » DB, err = sql.Open("sqlite3", conf.SQLiteFilePath) | |
67 » » if err != nil { | |
68 » » » glog.Fatalln("Failed to open:", err) | |
69 » » } | |
70 » » isMySQL = false | |
71 } | 143 } |
72 | 144 |
73 result := &VersionedDB{ | 145 result := &VersionedDB{ |
74 DB: DB, | 146 DB: DB, |
75 IsMySQL: isMySQL, | |
76 migrationSteps: conf.MigrationSteps, | 147 migrationSteps: conf.MigrationSteps, |
77 } | 148 } |
78 | 149 |
79 // Make sure the migration table exists. | 150 // Make sure the migration table exists. |
80 if err := result.checkVersionTable(); err != nil { | 151 if err := result.checkVersionTable(); err != nil { |
81 // We are using panic() instead of Fataln() to be able to trap t
his | 152 // We are using panic() instead of Fataln() to be able to trap t
his |
82 // in tests and make sure it fails when no version table exists. | 153 // in tests and make sure it fails when no version table exists. |
83 glog.Errorln("Unable to create version table.") | 154 glog.Errorln("Unable to create version table.") |
84 panic("Attempt to create version table returned: " + err.Error()
) | 155 panic("Attempt to create version table returned: " + err.Error()
) |
85 } | 156 } |
86 glog.Infoln("Version table OK.") | 157 glog.Infoln("Version table OK.") |
87 | 158 |
88 // Migrate to the latest version if we are using SQLite, so we don't hav
e | |
89 // to run the *_migratdb command for a local database. | |
90 if !result.IsMySQL { | |
91 result.Migrate(result.MaxDBVersion()) | |
92 } | |
93 | |
94 // Ping the database occasionally to keep the connection fresh. | 159 // Ping the database occasionally to keep the connection fresh. |
95 go func() { | 160 go func() { |
96 c := time.Tick(1 * time.Minute) | 161 c := time.Tick(1 * time.Minute) |
97 for _ = range c { | 162 for _ = range c { |
98 if err := result.DB.Ping(); err != nil { | 163 if err := result.DB.Ping(); err != nil { |
99 glog.Warningln("Database failed to respond:", er
r) | 164 glog.Warningln("Database failed to respond:", er
r) |
100 } | 165 } |
101 glog.Infof("db: Successful ping") | 166 glog.Infof("db: Successful ping") |
102 } | 167 } |
103 }() | 168 }() |
(...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
178 return version, err | 243 return version, err |
179 } | 244 } |
180 | 245 |
181 // Returns the highest version currently available. | 246 // Returns the highest version currently available. |
182 func (vdb *VersionedDB) MaxDBVersion() int { | 247 func (vdb *VersionedDB) MaxDBVersion() int { |
183 return len(vdb.migrationSteps) | 248 return len(vdb.migrationSteps) |
184 } | 249 } |
185 | 250 |
186 // Returns an error if the version table does not exist. | 251 // Returns an error if the version table does not exist. |
187 func (vdb *VersionedDB) checkVersionTable() error { | 252 func (vdb *VersionedDB) checkVersionTable() error { |
188 » // Check if the table exists in MySQL or SQLite. | 253 » // Check if the table exists in MySQL. |
189 stmt := "SHOW TABLES LIKE 'sk_db_version'" | 254 stmt := "SHOW TABLES LIKE 'sk_db_version'" |
190 if !vdb.IsMySQL { | |
191 stmt = "SELECT name FROM sqlite_master WHERE type='table' AND na
me='sk_db_version';" | |
192 } | |
193 | 255 |
194 var temp string | 256 var temp string |
195 err := vdb.DB.QueryRow(stmt).Scan(&temp) | 257 err := vdb.DB.QueryRow(stmt).Scan(&temp) |
196 if err != nil { | 258 if err != nil { |
197 // See if we can create the version table. | 259 // See if we can create the version table. |
198 return vdb.ensureVersionTable() | 260 return vdb.ensureVersionTable() |
199 } | 261 } |
200 | 262 |
201 return nil | 263 return nil |
202 } | 264 } |
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
256 if inc < 0 { | 318 if inc < 0 { |
257 idx = currentVersion - 1 | 319 idx = currentVersion - 1 |
258 } | 320 } |
259 delta := util.AbsInt(targetVersion - currentVersion) | 321 delta := util.AbsInt(targetVersion - currentVersion) |
260 result := make([][]string, 0, delta) | 322 result := make([][]string, 0, delta) |
261 | 323 |
262 for i := 0; i < delta; i++ { | 324 for i := 0; i < delta; i++ { |
263 var temp []string | 325 var temp []string |
264 switch { | 326 switch { |
265 // using mysqlp | 327 // using mysqlp |
266 » » case (inc > 0) && vdb.IsMySQL: | 328 » » case (inc > 0): |
267 temp = vdb.migrationSteps[idx].MySQLUp | 329 temp = vdb.migrationSteps[idx].MySQLUp |
268 » » case (inc < 0) && vdb.IsMySQL: | 330 » » case (inc < 0): |
269 temp = vdb.migrationSteps[idx].MySQLDown | 331 temp = vdb.migrationSteps[idx].MySQLDown |
270 // using sqlite | |
271 case (inc > 0): | |
272 temp = vdb.migrationSteps[idx].SQLiteUp | |
273 case (inc < 0): | |
274 temp = vdb.migrationSteps[idx].SQLiteDown | |
275 } | 332 } |
276 result = append(result, temp) | 333 result = append(result, temp) |
277 idx += inc | 334 idx += inc |
278 } | 335 } |
279 return result | 336 return result |
280 } | 337 } |
OLD | NEW |