github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/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/mattermost/mattermost-server/v5/mlog"
    16  	"github.com/mattermost/mattermost-server/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://mmuser:mostest@localhost:5432/mattermost_test
   133  // returns
   134  //	driverName = mysql
   135  //	dataSourceName = mmuser: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  }