github.com/teloshs/mattermost-server@v5.11.1+incompatible/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 "net/url" 11 "strings" 12 13 "github.com/jmoiron/sqlx" 14 "github.com/pkg/errors" 15 16 "github.com/mattermost/mattermost-server/mlog" 17 "github.com/mattermost/mattermost-server/model" 18 19 // Load the MySQL driver 20 _ "github.com/go-sql-driver/mysql" 21 // Load the Postgres driver 22 _ "github.com/lib/pq" 23 ) 24 25 // DatabaseStore is a config store backed by a database. 26 type DatabaseStore struct { 27 commonStore 28 29 originalDsn string 30 driverName string 31 dataSourceName string 32 db *sqlx.DB 33 } 34 35 // NewDatabaseStore creates a new instance of a config store backed by the given database. 36 func NewDatabaseStore(dsn string) (ds *DatabaseStore, err error) { 37 driverName, dataSourceName, err := parseDSN(dsn) 38 if err != nil { 39 return nil, errors.Wrap(err, "invalid DSN") 40 } 41 42 db, err := sqlx.Open(driverName, dataSourceName) 43 if err != nil { 44 return nil, errors.Wrapf(err, "failed to connect to %s database", driverName) 45 } 46 47 ds = &DatabaseStore{ 48 driverName: driverName, 49 originalDsn: dsn, 50 dataSourceName: dataSourceName, 51 db: db, 52 } 53 if err = initializeConfigurationsTable(ds.db); err != nil { 54 return nil, errors.Wrap(err, "failed to initialize") 55 } 56 57 if err = ds.Load(); err != nil { 58 return nil, errors.Wrap(err, "failed to load") 59 } 60 61 return ds, nil 62 } 63 64 // initializeConfigurationsTable ensures the requisite tables in place to form the backing store. 65 func initializeConfigurationsTable(db *sqlx.DB) error { 66 _, err := db.Exec(` 67 CREATE TABLE IF NOT EXISTS Configurations ( 68 Id VARCHAR(26) PRIMARY KEY, 69 Value TEXT NOT NULL, 70 CreateAt BIGINT NOT NULL, 71 Active BOOLEAN NULL UNIQUE 72 ) 73 `) 74 if err != nil { 75 return errors.Wrap(err, "failed to create Configurations table") 76 } 77 78 _, err = db.Exec(` 79 CREATE TABLE IF NOT EXISTS ConfigurationFiles ( 80 Name VARCHAR(64) PRIMARY KEY, 81 Data TEXT NOT NULL, 82 CreateAt BIGINT NOT NULL, 83 UpdateAt BIGINT NOT NULL 84 ) 85 `) 86 if err != nil { 87 return errors.Wrap(err, "failed to create ConfigurationFiles table") 88 } 89 90 return nil 91 } 92 93 // parseDSN splits up a connection string into a driver name and data source name. 94 // 95 // For example: 96 // mysql://mmuser:mostest@dockerhost:5432/mattermost_test 97 // returns 98 // driverName = mysql 99 // dataSourceName = mmuser:mostest@dockerhost:5432/mattermost_test 100 // 101 // By contrast, a Postgres DSN is returned unmodified. 102 func parseDSN(dsn string) (string, string, error) { 103 // Treat the DSN as the URL that it is. 104 u, err := url.Parse(dsn) 105 if err != nil { 106 return "", "", errors.Wrap(err, "failed to parse DSN as URL") 107 } 108 109 scheme := u.Scheme 110 switch scheme { 111 case "mysql": 112 // Strip off the mysql:// for the dsn with which to connect. 113 u.Scheme = "" 114 dsn = strings.TrimPrefix(u.String(), "//") 115 116 case "postgres": 117 // No changes required 118 119 default: 120 return "", "", errors.Wrapf(err, "unsupported scheme %s", scheme) 121 } 122 123 return scheme, dsn, nil 124 } 125 126 // Set replaces the current configuration in its entirety and updates the backing store. 127 func (ds *DatabaseStore) Set(newCfg *model.Config) (*model.Config, error) { 128 return ds.commonStore.set(newCfg, ds.commonStore.validate, ds.persist) 129 } 130 131 // persist writes the configuration to the configured database. 132 func (ds *DatabaseStore) persist(cfg *model.Config) error { 133 b, err := marshalConfig(cfg) 134 if err != nil { 135 return errors.Wrap(err, "failed to serialize") 136 } 137 138 id := model.NewId() 139 value := string(b) 140 createAt := model.GetMillis() 141 142 tx, err := ds.db.Beginx() 143 if err != nil { 144 return errors.Wrap(err, "failed to begin transaction") 145 } 146 defer func() { 147 // Rollback after Commit just returns sql.ErrTxDone. 148 if err := tx.Rollback(); err != nil && err != sql.ErrTxDone { 149 mlog.Error("Failed to rollback configuration transaction", mlog.Err(err)) 150 } 151 }() 152 153 params := map[string]interface{}{ 154 "id": id, 155 "value": value, 156 "create_at": createAt, 157 "key": "ConfigurationId", 158 } 159 160 // Skip the persist altogether if we're effectively writing the same configuration. 161 var oldValue []byte 162 row := ds.db.QueryRow("SELECT Value FROM Configurations WHERE Active") 163 if err := row.Scan(&oldValue); err != nil && err != sql.ErrNoRows { 164 return errors.Wrap(err, "failed to query active configuration") 165 } 166 if bytes.Equal(oldValue, b) { 167 return nil 168 } 169 170 if _, err := tx.Exec("UPDATE Configurations SET Active = NULL WHERE Active"); err != nil { 171 return errors.Wrap(err, "failed to deactivate current configuration") 172 } 173 174 if _, err := tx.NamedExec("INSERT INTO Configurations (Id, Value, CreateAt, Active) VALUES (:id, :value, :create_at, TRUE)", params); err != nil { 175 return errors.Wrap(err, "failed to record new configuration") 176 } 177 178 if err := tx.Commit(); err != nil { 179 return errors.Wrap(err, "failed to commit transaction") 180 } 181 182 return nil 183 } 184 185 // Load updates the current configuration from the backing store. 186 func (ds *DatabaseStore) Load() (err error) { 187 var needsSave bool 188 var configurationData []byte 189 190 row := ds.db.QueryRow("SELECT Value FROM Configurations WHERE Active") 191 if err = row.Scan(&configurationData); err != nil && err != sql.ErrNoRows { 192 return errors.Wrap(err, "failed to query active configuration") 193 } 194 195 // Initialize from the default config if no active configuration could be found. 196 if len(configurationData) == 0 { 197 needsSave = true 198 199 defaultCfg := &model.Config{} 200 defaultCfg.SetDefaults() 201 202 // Assume the database storing the config is also to be used for the application. 203 // This can be overridden using environment variables on first start if necessary, 204 // or changed from the system console afterwards. 205 *defaultCfg.SqlSettings.DriverName = ds.driverName 206 *defaultCfg.SqlSettings.DataSource = ds.dataSourceName 207 208 configurationData, err = marshalConfig(defaultCfg) 209 if err != nil { 210 return errors.Wrap(err, "failed to serialize default config") 211 } 212 } 213 214 return ds.commonStore.load(ioutil.NopCloser(bytes.NewReader(configurationData)), needsSave, ds.commonStore.validate, ds.persist) 215 } 216 217 // GetFile fetches the contents of a previously persisted configuration file. 218 func (ds *DatabaseStore) GetFile(name string) ([]byte, error) { 219 query, args, err := sqlx.Named("SELECT Data FROM ConfigurationFiles WHERE Name = :name", map[string]interface{}{ 220 "name": name, 221 }) 222 if err != nil { 223 return nil, err 224 } 225 226 var data []byte 227 row := ds.db.QueryRowx(query, args...) 228 if err = row.Scan(&data); err != nil { 229 return nil, errors.Wrapf(err, "failed to scan data from row for %s", name) 230 } 231 232 return data, nil 233 } 234 235 // SetFile sets or replaces the contents of a configuration file. 236 func (ds *DatabaseStore) SetFile(name string, data []byte) error { 237 params := map[string]interface{}{ 238 "name": name, 239 "data": data, 240 "create_at": model.GetMillis(), 241 "update_at": model.GetMillis(), 242 } 243 244 result, err := ds.db.NamedExec("UPDATE ConfigurationFiles SET Data = :data, UpdateAt = :update_at WHERE Name = :name", params) 245 if err != nil { 246 return errors.Wrapf(err, "failed to update row for %s", name) 247 } 248 249 count, err := result.RowsAffected() 250 if err != nil { 251 return errors.Wrapf(err, "failed to count rows affected for %s", name) 252 } else if count > 0 { 253 return nil 254 } 255 256 _, err = ds.db.NamedExec("INSERT INTO ConfigurationFiles (Name, Data, CreateAt, UpdateAt) VALUES (:name, :data, :create_at, :update_at)", params) 257 if err != nil { 258 return errors.Wrapf(err, "failed to insert row for %s", name) 259 } 260 261 return nil 262 } 263 264 // HasFile returns true if the given file was previously persisted. 265 func (ds *DatabaseStore) HasFile(name string) (bool, error) { 266 query, args, err := sqlx.Named("SELECT COUNT(*) FROM ConfigurationFiles WHERE Name = :name", map[string]interface{}{ 267 "name": name, 268 }) 269 if err != nil { 270 return false, err 271 } 272 273 var count int 274 row := ds.db.QueryRowx(query, args...) 275 if err = row.Scan(&count); err != nil { 276 return false, errors.Wrapf(err, "failed to scan count of rows for %s", name) 277 } 278 279 return count != 0, nil 280 } 281 282 // RemoveFile remoevs a previously persisted configuration file. 283 func (ds *DatabaseStore) RemoveFile(name string) error { 284 _, err := ds.db.NamedExec("DELETE FROM ConfigurationFiles WHERE Name = :name", map[string]interface{}{ 285 "name": name, 286 }) 287 if err != nil { 288 return errors.Wrapf(err, "failed to remove row for %s", name) 289 } 290 291 return nil 292 } 293 294 // String returns the path to the database backing the config, masking the password. 295 func (ds *DatabaseStore) String() string { 296 u, _ := url.Parse(ds.originalDsn) 297 298 // Strip out the password to avoid leaking in logs. 299 u.User = url.User(u.User.Username()) 300 301 return u.String() 302 } 303 304 // Close cleans up resources associated with the store. 305 func (ds *DatabaseStore) Close() error { 306 ds.configLock.Lock() 307 defer ds.configLock.Unlock() 308 309 return ds.db.Close() 310 }