github.com/vnforks/kid/v5@v5.22.1-0.20200408055009-b89d99c65676/config/database.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package config 5 6 import ( 7 "bytes" 8 "database/sql" 9 "io/ioutil" 10 "strings" 11 12 "github.com/jmoiron/sqlx" 13 "github.com/pkg/errors" 14 15 "github.com/vnforks/kid/v5/mlog" 16 "github.com/vnforks/kid/v5/model" 17 18 // Load the MySQL driver 19 _ "github.com/go-sql-driver/mysql" 20 // Load the Postgres driver 21 _ "github.com/lib/pq" 22 ) 23 24 // MaxWriteLength defines the maximum length accepted for write to the Configurations or 25 // ConfigurationFiles table. 26 // 27 // It is imposed by MySQL's default max_allowed_packet value of 4Mb. 28 const MaxWriteLength = 4 * 1024 * 1024 29 30 // DatabaseStore is a config store backed by a database. 31 type DatabaseStore struct { 32 commonStore 33 34 originalDsn string 35 driverName string 36 dataSourceName string 37 db *sqlx.DB 38 } 39 40 // NewDatabaseStore creates a new instance of a config store backed by the given database. 41 func NewDatabaseStore(dsn string) (ds *DatabaseStore, err error) { 42 driverName, dataSourceName, err := parseDSN(dsn) 43 if err != nil { 44 return nil, errors.Wrap(err, "invalid DSN") 45 } 46 47 db, err := sqlx.Open(driverName, dataSourceName) 48 if err != nil { 49 return nil, errors.Wrapf(err, "failed to connect to %s database", driverName) 50 } 51 52 ds = &DatabaseStore{ 53 driverName: driverName, 54 originalDsn: dsn, 55 dataSourceName: dataSourceName, 56 db: db, 57 } 58 if err = initializeConfigurationsTable(ds.db); err != nil { 59 return nil, errors.Wrap(err, "failed to initialize") 60 } 61 62 if err = ds.Load(); err != nil { 63 return nil, errors.Wrap(err, "failed to load") 64 } 65 66 return ds, nil 67 } 68 69 // initializeConfigurationsTable ensures the requisite tables in place to form the backing store. 70 // 71 // Uses MEDIUMTEXT on MySQL, and TEXT on sane databases. 72 func initializeConfigurationsTable(db *sqlx.DB) error { 73 mysqlCharset := "" 74 if db.DriverName() == "mysql" { 75 mysqlCharset = "DEFAULT CHARACTER SET utf8mb4" 76 } 77 78 _, err := db.Exec(` 79 CREATE TABLE IF NOT EXISTS Configurations ( 80 Id VARCHAR(26) PRIMARY KEY, 81 Value TEXT NOT NULL, 82 CreateAt BIGINT NOT NULL, 83 Active BOOLEAN NULL UNIQUE 84 ) 85 ` + mysqlCharset) 86 87 if err != nil { 88 return errors.Wrap(err, "failed to create Configurations table") 89 } 90 91 _, err = db.Exec(` 92 CREATE TABLE IF NOT EXISTS ConfigurationFiles ( 93 Name VARCHAR(64) PRIMARY KEY, 94 Data TEXT NOT NULL, 95 CreateAt BIGINT NOT NULL, 96 UpdateAt BIGINT NOT NULL 97 ) 98 ` + mysqlCharset) 99 if err != nil { 100 return errors.Wrap(err, "failed to create ConfigurationFiles table") 101 } 102 103 // Change from TEXT (65535 limit) to MEDIUM TEXT (16777215) on MySQL. This is a 104 // backwards-compatible migration for any existing schema. 105 // Also fix using the wrong encoding initially 106 if db.DriverName() == "mysql" { 107 _, err = db.Exec(`ALTER TABLE Configurations MODIFY Value MEDIUMTEXT`) 108 if err != nil { 109 return errors.Wrap(err, "failed to alter Configurations table") 110 } 111 _, err = db.Exec(`ALTER TABLE Configurations CONVERT TO CHARACTER SET utf8mb4`) 112 if err != nil { 113 return errors.Wrap(err, "failed to alter Configurations table character set") 114 } 115 116 _, err = db.Exec(`ALTER TABLE ConfigurationFiles MODIFY Data MEDIUMTEXT`) 117 if err != nil { 118 return errors.Wrap(err, "failed to alter ConfigurationFiles table") 119 } 120 _, err = db.Exec(`ALTER TABLE ConfigurationFiles CONVERT TO CHARACTER SET utf8mb4`) 121 if err != nil { 122 return errors.Wrap(err, "failed to alter ConfigurationFiles table character set") 123 } 124 } 125 126 return nil 127 } 128 129 // parseDSN splits up a connection string into a driver name and data source name. 130 // 131 // For example: 132 // mysql://kuser:mostest@localhost:5432/mattermost_test 133 // returns 134 // driverName = mysql 135 // dataSourceName = kuser:mostest@localhost:5432/mattermost_test 136 // 137 // By contrast, a Postgres DSN is returned unmodified. 138 func parseDSN(dsn string) (string, string, error) { 139 // Treat the DSN as the URL that it is. 140 s := strings.SplitN(dsn, "://", 2) 141 if len(s) != 2 { 142 return "", "", errors.New("failed to parse DSN as URL") 143 } 144 145 scheme := s[0] 146 switch scheme { 147 case "mysql": 148 // Strip off the mysql:// for the dsn with which to connect. 149 dsn = s[1] 150 151 case "postgres": 152 // No changes required 153 154 default: 155 return "", "", errors.Errorf("unsupported scheme %s", scheme) 156 } 157 158 return scheme, dsn, nil 159 } 160 161 // Set replaces the current configuration in its entirety and updates the backing store. 162 func (ds *DatabaseStore) Set(newCfg *model.Config) (*model.Config, error) { 163 return ds.commonStore.set(newCfg, true, ds.commonStore.validate, ds.persist) 164 } 165 166 // maxLength identifies the maximum length of a configuration or configuration file 167 func (ds *DatabaseStore) checkLength(length int) error { 168 if ds.db.DriverName() == "mysql" && length > MaxWriteLength { 169 return errors.Errorf("value is too long: %d > %d bytes", length, MaxWriteLength) 170 } 171 172 return nil 173 } 174 175 // persist writes the configuration to the configured database. 176 func (ds *DatabaseStore) persist(cfg *model.Config) error { 177 b, err := marshalConfig(cfg) 178 if err != nil { 179 return errors.Wrap(err, "failed to serialize") 180 } 181 182 id := model.NewId() 183 value := string(b) 184 createAt := model.GetMillis() 185 186 err = ds.checkLength(len(value)) 187 if err != nil { 188 return errors.Wrap(err, "marshalled configuration failed length check") 189 } 190 191 tx, err := ds.db.Beginx() 192 if err != nil { 193 return errors.Wrap(err, "failed to begin transaction") 194 } 195 defer func() { 196 // Rollback after Commit just returns sql.ErrTxDone. 197 if err := tx.Rollback(); err != nil && err != sql.ErrTxDone { 198 mlog.Error("Failed to rollback configuration transaction", mlog.Err(err)) 199 } 200 }() 201 202 params := map[string]interface{}{ 203 "id": id, 204 "value": value, 205 "create_at": createAt, 206 "key": "ConfigurationId", 207 } 208 209 // Skip the persist altogether if we're effectively writing the same configuration. 210 var oldValue []byte 211 row := ds.db.QueryRow("SELECT Value FROM Configurations WHERE Active") 212 if err := row.Scan(&oldValue); err != nil && err != sql.ErrNoRows { 213 return errors.Wrap(err, "failed to query active configuration") 214 } 215 if bytes.Equal(oldValue, b) { 216 return nil 217 } 218 219 if _, err := tx.Exec("UPDATE Configurations SET Active = NULL WHERE Active"); err != nil { 220 return errors.Wrap(err, "failed to deactivate current configuration") 221 } 222 223 if _, err := tx.NamedExec("INSERT INTO Configurations (Id, Value, CreateAt, Active) VALUES (:id, :value, :create_at, TRUE)", params); err != nil { 224 return errors.Wrap(err, "failed to record new configuration") 225 } 226 227 if err := tx.Commit(); err != nil { 228 return errors.Wrap(err, "failed to commit transaction") 229 } 230 231 return nil 232 } 233 234 // Load updates the current configuration from the backing store. 235 func (ds *DatabaseStore) Load() (err error) { 236 var needsSave bool 237 var configurationData []byte 238 239 row := ds.db.QueryRow("SELECT Value FROM Configurations WHERE Active") 240 if err = row.Scan(&configurationData); err != nil && err != sql.ErrNoRows { 241 return errors.Wrap(err, "failed to query active configuration") 242 } 243 244 // Initialize from the default config if no active configuration could be found. 245 if len(configurationData) == 0 { 246 needsSave = true 247 248 defaultCfg := &model.Config{} 249 defaultCfg.SetDefaults() 250 251 // Assume the database storing the config is also to be used for the application. 252 // This can be overridden using environment variables on first start if necessary, 253 // or changed from the system console afterwards. 254 *defaultCfg.SqlSettings.DriverName = ds.driverName 255 *defaultCfg.SqlSettings.DataSource = ds.dataSourceName 256 257 configurationData, err = marshalConfig(defaultCfg) 258 if err != nil { 259 return errors.Wrap(err, "failed to serialize default config") 260 } 261 } 262 263 return ds.commonStore.load(ioutil.NopCloser(bytes.NewReader(configurationData)), needsSave, ds.commonStore.validate, ds.persist) 264 } 265 266 // GetFile fetches the contents of a previously persisted configuration file. 267 func (ds *DatabaseStore) GetFile(name string) ([]byte, error) { 268 query, args, err := sqlx.Named("SELECT Data FROM ConfigurationFiles WHERE Name = :name", map[string]interface{}{ 269 "name": name, 270 }) 271 if err != nil { 272 return nil, err 273 } 274 275 var data []byte 276 row := ds.db.QueryRowx(ds.db.Rebind(query), args...) 277 if err = row.Scan(&data); err != nil { 278 return nil, errors.Wrapf(err, "failed to scan data from row for %s", name) 279 } 280 281 return data, nil 282 } 283 284 // SetFile sets or replaces the contents of a configuration file. 285 func (ds *DatabaseStore) SetFile(name string, data []byte) error { 286 err := ds.checkLength(len(data)) 287 if err != nil { 288 return errors.Wrap(err, "file data failed length check") 289 } 290 291 params := map[string]interface{}{ 292 "name": name, 293 "data": data, 294 "create_at": model.GetMillis(), 295 "update_at": model.GetMillis(), 296 } 297 298 result, err := ds.db.NamedExec("UPDATE ConfigurationFiles SET Data = :data, UpdateAt = :update_at WHERE Name = :name", params) 299 if err != nil { 300 return errors.Wrapf(err, "failed to update row for %s", name) 301 } 302 303 count, err := result.RowsAffected() 304 if err != nil { 305 return errors.Wrapf(err, "failed to count rows affected for %s", name) 306 } else if count > 0 { 307 return nil 308 } 309 310 _, err = ds.db.NamedExec("INSERT INTO ConfigurationFiles (Name, Data, CreateAt, UpdateAt) VALUES (:name, :data, :create_at, :update_at)", params) 311 if err != nil { 312 return errors.Wrapf(err, "failed to insert row for %s", name) 313 } 314 315 return nil 316 } 317 318 // HasFile returns true if the given file was previously persisted. 319 func (ds *DatabaseStore) HasFile(name string) (bool, error) { 320 query, args, err := sqlx.Named("SELECT COUNT(*) FROM ConfigurationFiles WHERE Name = :name", map[string]interface{}{ 321 "name": name, 322 }) 323 if err != nil { 324 return false, err 325 } 326 327 var count int64 328 row := ds.db.QueryRowx(ds.db.Rebind(query), args...) 329 if err = row.Scan(&count); err != nil { 330 return false, errors.Wrapf(err, "failed to scan count of rows for %s", name) 331 } 332 333 return count != 0, nil 334 } 335 336 // RemoveFile remoevs a previously persisted configuration file. 337 func (ds *DatabaseStore) RemoveFile(name string) error { 338 _, err := ds.db.NamedExec("DELETE FROM ConfigurationFiles WHERE Name = :name", map[string]interface{}{ 339 "name": name, 340 }) 341 if err != nil { 342 return errors.Wrapf(err, "failed to remove row for %s", name) 343 } 344 345 return nil 346 } 347 348 // String returns the path to the database backing the config, masking the password. 349 func (ds *DatabaseStore) String() string { 350 return stripPassword(ds.originalDsn, ds.driverName) 351 } 352 353 // Close cleans up resources associated with the store. 354 func (ds *DatabaseStore) Close() error { 355 ds.configLock.Lock() 356 defer ds.configLock.Unlock() 357 358 return ds.db.Close() 359 }