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