package config import ( "fmt" "github.com/go-sql-driver/mysql" "github.com/icinga/icingadb/pkg/driver" "github.com/icinga/icingadb/pkg/icingadb" "github.com/icinga/icingadb/pkg/logging" "github.com/icinga/icingadb/pkg/utils" "github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx/reflectx" "github.com/pkg/errors" "net" "net/url" "strconv" "strings" "sync" "time" ) var registerDriverOnce sync.Once // Database defines database client configuration. type Database struct { Type string `yaml:"type" default:"mysql"` Host string `yaml:"host"` Port int `yaml:"port"` Database string `yaml:"database"` User string `yaml:"user"` Password string `yaml:"password"` TlsOptions TLS `yaml:",inline"` Options icingadb.Options `yaml:"options"` } // Open prepares the DSN string and driver configuration, // calls sqlx.Open, but returns *icingadb.DB. func (d *Database) Open(logger *logging.Logger) (*icingadb.DB, error) { registerDriverOnce.Do(func() { driver.Register(logger) }) var dsn string switch d.Type { case "mysql": config := mysql.NewConfig() config.User = d.User config.Passwd = d.Password if d.isUnixAddr() { config.Net = "unix" config.Addr = d.Host } else { config.Net = "tcp" port := d.Port if port == 0 { port = 3306 } config.Addr = net.JoinHostPort(d.Host, fmt.Sprint(port)) } config.DBName = d.Database config.Timeout = time.Minute config.Params = map[string]string{"sql_mode": "ANSI_QUOTES"} tlsConfig, err := d.TlsOptions.MakeConfig(d.Host) if err != nil { return nil, err } if tlsConfig != nil { config.TLSConfig = "icingadb" if err := mysql.RegisterTLSConfig(config.TLSConfig, tlsConfig); err != nil { return nil, errors.Wrap(err, "can't register TLS config") } } dsn = config.FormatDSN() case "pgsql": uri := &url.URL{ Scheme: "postgres", User: url.UserPassword(d.User, d.Password), Path: "/" + url.PathEscape(d.Database), } query := url.Values{ "connect_timeout": {"60"}, "binary_parameters": {"yes"}, // Host and port can alternatively be specified in the query string. lib/pq can't parse the connection URI // if a Unix domain socket path is specified in the host part of the URI, therefore always use the query // string. See also https://github.com/lib/pq/issues/796 "host": {d.Host}, } if d.Port != 0 { query["port"] = []string{strconv.FormatInt(int64(d.Port), 10)} } if _, err := d.TlsOptions.MakeConfig(d.Host); err != nil { return nil, err } if d.TlsOptions.Enable { if d.TlsOptions.Insecure { query["sslmode"] = []string{"require"} } else { query["sslmode"] = []string{"verify-full"} } if d.TlsOptions.Cert != "" { query["sslcert"] = []string{d.TlsOptions.Cert} } if d.TlsOptions.Key != "" { query["sslkey"] = []string{d.TlsOptions.Key} } if d.TlsOptions.Ca != "" { query["sslrootcert"] = []string{d.TlsOptions.Ca} } } else { query["sslmode"] = []string{"disable"} } uri.RawQuery = query.Encode() dsn = uri.String() default: return nil, unknownDbType(d.Type) } db, err := sqlx.Open("icingadb-"+d.Type, dsn) if err != nil { return nil, errors.Wrap(err, "can't open database") } db.SetMaxIdleConns(d.Options.MaxConnections / 3) db.SetMaxOpenConns(d.Options.MaxConnections) db.Mapper = reflectx.NewMapperFunc("db", func(s string) string { return utils.Key(s, '_') }) return icingadb.NewDb(db, logger, &d.Options), nil } // Validate checks constraints in the supplied database configuration and returns an error if they are violated. func (d *Database) Validate() error { switch d.Type { case "mysql", "pgsql": default: return unknownDbType(d.Type) } if d.Host == "" { return errors.New("database host missing") } if d.User == "" { return errors.New("database user missing") } if d.Database == "" { return errors.New("database name missing") } return d.Options.Validate() } func (d *Database) isUnixAddr() bool { return strings.HasPrefix(d.Host, "/") } func unknownDbType(t string) error { return errors.Errorf(`unknown database type %q, must be one of: "mysql", "pgsql"`, t) }