github.com/crspeller/mattermost-server@v0.0.0-20190328001957-a200beb3d111/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/crspeller/mattermost-server/mlog"
    17  	"github.com/crspeller/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  }