From b8c62731a61c8d007b350d296ec83ffc10346eeb Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Tue, 22 Mar 2016 21:55:58 -0700 Subject: [PATCH] adding bootstrapping and config update for notary server Signed-off-by: David Lawrence (github: endophage) --- cmd/notary-server/bootstrap.go | 17 +++ cmd/notary-server/config.go | 44 +++++--- cmd/notary-server/main.go | 18 ++-- cmd/notary-server/main_test.go | 11 +- cmd/notary-signer/main.go | 18 ++-- cmd/notary-signer/main_test.go | 13 +-- const.go | 5 + server/storage/rethinkdb.go | 46 +++++--- server/storage/rethinkdb_models.go | 21 ++++ signer/keydbstore/rethink_keydbstore.go | 7 +- storage/interface.go | 8 ++ storage/rethinkdb/bootstrap.go | 137 ++++++++++++++++++++++++ utils/configuration.go | 88 ++++++++++----- utils/configuration_test.go | 34 ++---- 14 files changed, 357 insertions(+), 110 deletions(-) create mode 100644 cmd/notary-server/bootstrap.go create mode 100644 server/storage/rethinkdb_models.go create mode 100644 storage/interface.go create mode 100644 storage/rethinkdb/bootstrap.go diff --git a/cmd/notary-server/bootstrap.go b/cmd/notary-server/bootstrap.go new file mode 100644 index 0000000000..95aeee451f --- /dev/null +++ b/cmd/notary-server/bootstrap.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + + "github.com/docker/notary/storage" + "golang.org/x/net/context" +) + +func bootstrap(ctx context.Context) error { + s := ctx.Value("metaStore") + store, ok := s.(storage.Bootstrapper) + if !ok { + return fmt.Errorf("Store does not support bootstrapping.") + } + return store.Bootstrap() +} diff --git a/cmd/notary-server/config.go b/cmd/notary-server/config.go index 4a50a6e19c..8a6046e68f 100644 --- a/cmd/notary-server/config.go +++ b/cmd/notary-server/config.go @@ -7,6 +7,7 @@ import ( "time" "github.com/Sirupsen/logrus" + "github.com/dancannon/gorethink" _ "github.com/docker/distribution/registry/auth/htpasswd" _ "github.com/docker/distribution/registry/auth/token" "github.com/docker/go-connections/tlsconfig" @@ -62,34 +63,45 @@ func grpcTLS(configuration *viper.Viper) (*tls.Config, error) { } // parses the configuration and returns a backing store for the TUF files -func getStore(configuration *viper.Viper, allowedBackends []string, hRegister healthRegister) ( +func getStore(configuration *viper.Viper, hRegister healthRegister) ( storage.MetaStore, error) { var ( store storage.MetaStore err error ) - storeConfig, err := utils.ParseStorage(configuration, allowedBackends) - if err != nil { - return nil, err - } - logrus.Infof("Using %s backend", storeConfig.Backend) + backend := configuration.GetString("storage.backend") + logrus.Infof("Using %s backend", backend) - switch storeConfig.Backend { + switch backend { case notary.MemoryBackend: return storage.NewMemStorage(), nil - case notary.MySQLBackend: - store, err = storage.NewSQLStorage(storeConfig.Backend, storeConfig.Source) + case notary.MySQLBackend, notary.SQLiteBackend: + storeConfig, err := utils.ParseSQLStorage(configuration) + if err != nil { + return nil, err + } + var s *storage.SQLStorage + s, err = storage.NewSQLStorage(storeConfig.Backend, storeConfig.Source) + store = s + hRegister("DB operational", s.CheckHealth, time.Second*60) case notary.RethinkDBBackend: - backend := rethinkdb.Connection(storeConfig.CA, storeConfig.Source, storeConfig.Password) - store, err = storage.NewRethinkDBStorage(backend) + var sess *gorethink.Session + storeConfig, err := utils.ParseRethinkDBStorage(configuration) + if err != nil { + return nil, err + } + sess, err = rethinkdb.Connection(storeConfig.CA, storeConfig.Source, storeConfig.AuthKey) + if err == nil { + s := storage.NewRethinkDBStorage(storeConfig.DBName, sess) + store = s + hRegister("DB operational", s.CheckHealth, time.Second*60) + } default: - err = fmt.Errorf("%s not a supported storage backend", storeConfig.Backend) + err = fmt.Errorf("%s not a supported storage backend", backend) } if err != nil { - return nil, fmt.Errorf("Error starting DB driver: %s", err.Error()) + return nil, fmt.Errorf("Error starting %s driver: %s", backend, err.Error()) } - hRegister( - "DB operational", store.CheckHealth, time.Second*60) return store, nil } @@ -212,7 +224,7 @@ func parseServerConfig(configFilePath string, hRegister healthRegister) (context } ctx = context.WithValue(ctx, "keyAlgorithm", keyAlgo) - store, err := getStore(config, []string{utils.MySQLBackend, utils.MemoryBackend}, hRegister) + store, err := getStore(config, hRegister) if err != nil { return nil, server.Config{}, err } diff --git a/cmd/notary-server/main.go b/cmd/notary-server/main.go index ae3da854d5..f85f640e90 100644 --- a/cmd/notary-server/main.go +++ b/cmd/notary-server/main.go @@ -21,10 +21,11 @@ const ( ) var ( - debug bool - logFormat string - configFile string - envPrefix = "NOTARY_SERVER" + debug bool + logFormat string + configFile string + envPrefix = "NOTARY_SERVER" + doBootstrap bool ) func init() { @@ -32,6 +33,7 @@ func init() { flag.StringVar(&configFile, "config", "", "Path to configuration file") flag.BoolVar(&debug, "debug", false, "Enable the debugging server on localhost:8080") flag.StringVar(&logFormat, "logf", "json", "Set the format of the logs. Only 'json' and 'logfmt' are supported at the moment.") + flag.BoolVar(&doBootstrap, "bootstrap", false, "Do any necessary setup of configured backend storage services") // this needs to be in init so that _ALL_ logs are in the correct format if logFormat == jsonLogFormat { @@ -55,8 +57,12 @@ func main() { logrus.Fatal(err.Error()) } - logrus.Info("Starting Server") - err = server.Run(ctx, serverConfig) + if doBootstrap { + err = bootstrap(ctx) + } else { + logrus.Info("Starting Server") + err = server.Run(ctx, serverConfig) + } logrus.Error(err.Error()) return diff --git a/cmd/notary-server/main_test.go b/cmd/notary-server/main_test.go index 6ff1f96345..e4441ceae1 100644 --- a/cmd/notary-server/main_test.go +++ b/cmd/notary-server/main_test.go @@ -307,7 +307,7 @@ func TestGetStoreInvalid(t *testing.T) { registerCalled++ } - _, err := getStore(configure(config), []string{"mysql"}, fakeRegister) + _, err := getStore(configure(config), fakeRegister) require.Error(t, err) // no health function ever registered @@ -321,14 +321,14 @@ func TestGetStoreDBStore(t *testing.T) { defer os.Remove(tmpFile.Name()) config := fmt.Sprintf(`{"storage": {"backend": "%s", "db_url": "%s"}}`, - utils.SqliteBackend, tmpFile.Name()) + notary.SQLiteBackend, tmpFile.Name()) var registerCalled = 0 var fakeRegister = func(_ string, _ func() error, _ time.Duration) { registerCalled++ } - store, err := getStore(configure(config), []string{utils.SqliteBackend}, fakeRegister) + store, err := getStore(configure(config), fakeRegister) require.NoError(t, err) _, ok := store.(*storage.SQLStorage) require.True(t, ok) @@ -343,9 +343,8 @@ func TestGetMemoryStore(t *testing.T) { registerCalled++ } - config := fmt.Sprintf(`{"storage": {"backend": "%s"}}`, utils.MemoryBackend) - store, err := getStore(configure(config), - []string{utils.MySQLBackend, utils.MemoryBackend}, fakeRegister) + config := fmt.Sprintf(`{"storage": {"backend": "%s"}}`, notary.MemoryBackend) + store, err := getStore(configure(config), fakeRegister) require.NoError(t, err) _, ok := store.(*storage.MemStorage) require.True(t, ok) diff --git a/cmd/notary-signer/main.go b/cmd/notary-signer/main.go index 1ed0294a99..9a3a01b7b7 100644 --- a/cmd/notary-signer/main.go +++ b/cmd/notary-signer/main.go @@ -17,6 +17,7 @@ import ( "google.golang.org/grpc/credentials" "github.com/docker/distribution/health" + "github.com/docker/notary" "github.com/docker/notary/cryptoservice" "github.com/docker/notary/passphrase" "github.com/docker/notary/signer" @@ -74,17 +75,18 @@ func passphraseRetriever(keyName, alias string, createNew bool, attempts int) (p // mapping func setUpCryptoservices(configuration *viper.Viper, allowedBackends []string) ( signer.CryptoServiceIndex, error) { - - storeConfig, err := utils.ParseStorage(configuration, allowedBackends) - if err != nil { - return nil, err - } + backend := configuration.GetString("storage.backend") var keyStore trustmanager.KeyStore - if storeConfig.Backend == utils.MemoryBackend { + switch backend { + case notary.MemoryBackend: keyStore = trustmanager.NewKeyMemoryStore( passphrase.ConstantRetriever("memory-db-ignore")) - } else { + case notary.MySQLBackend, notary.SQLiteBackend: + storeConfig, err := utils.ParseSQLStorage(configuration) + if err != nil { + return nil, err + } defaultAlias := configuration.GetString("storage.default_alias") if defaultAlias == "" { // backwards compatibility - support this environment variable @@ -207,7 +209,7 @@ func main() { // setup the cryptoservices cryptoServices, err := setUpCryptoservices(mainViper, - []string{utils.MySQLBackend, utils.MemoryBackend}) + []string{notary.MySQLBackend, notary.MemoryBackend}) if err != nil { logrus.Fatal(err.Error()) } diff --git a/cmd/notary-signer/main_test.go b/cmd/notary-signer/main_test.go index 454e17357d..e890bae030 100644 --- a/cmd/notary-signer/main_test.go +++ b/cmd/notary-signer/main_test.go @@ -8,6 +8,7 @@ import ( "os" "testing" + "github.com/docker/notary" "github.com/docker/notary/signer" "github.com/docker/notary/signer/keydbstore" "github.com/docker/notary/tuf/data" @@ -105,8 +106,8 @@ func TestSetupCryptoServicesDBStoreNoDefaultAlias(t *testing.T) { _, err = setUpCryptoservices( configure(fmt.Sprintf( `{"storage": {"backend": "%s", "db_url": "%s"}}`, - utils.SqliteBackend, tmpFile.Name())), - []string{utils.SqliteBackend}) + notary.SqliteBackend, tmpFile.Name())), + []string{notary.SqliteBackend}) require.Error(t, err) require.Contains(t, err.Error(), "must provide a default alias for the key DB") } @@ -136,8 +137,8 @@ func TestSetupCryptoServicesDBStoreSuccess(t *testing.T) { configure(fmt.Sprintf( `{"storage": {"backend": "%s", "db_url": "%s"}, "default_alias": "timestamp"}`, - utils.SqliteBackend, tmpFile.Name())), - []string{utils.SqliteBackend}) + notary.SqliteBackend, tmpFile.Name())), + []string{notary.SqliteBackend}) require.NoError(t, err) require.Len(t, cryptoServices, 2) @@ -164,9 +165,9 @@ func TestSetupCryptoServicesDBStoreSuccess(t *testing.T) { // a valid CryptoService is returned. func TestSetupCryptoServicesMemoryStore(t *testing.T) { config := configure(fmt.Sprintf(`{"storage": {"backend": "%s"}}`, - utils.MemoryBackend)) + notary.MemoryBackend)) cryptoServices, err := setUpCryptoservices(config, - []string{utils.SqliteBackend, utils.MemoryBackend}) + []string{notary.SqliteBackend, notary.MemoryBackend}) require.NoError(t, err) require.Len(t, cryptoServices, 2) diff --git a/const.go b/const.go index 22296f7103..f3d834ad16 100644 --- a/const.go +++ b/const.go @@ -49,6 +49,11 @@ const ( // (one year, in seconds, since one year is forever in terms of internet // content) CacheMaxAgeLimit = 1 * Year + + MySQLBackend = "mysql" + MemoryBackend = "memory" + SQLiteBackend = "sqlite3" + RethinkDBBackend = "rethinkdb" ) // NotaryDefaultExpiries is the construct used to configure the default expiry times of diff --git a/server/storage/rethinkdb.go b/server/storage/rethinkdb.go index fceb72567c..205ec81ea1 100644 --- a/server/storage/rethinkdb.go +++ b/server/storage/rethinkdb.go @@ -1,12 +1,16 @@ package storage import ( + "crypto/sha256" + "encoding/hex" + "fmt" "time" "github.com/dancannon/gorethink" "github.com/docker/notary/storage/rethinkdb" ) +// RDBTUFFile is a tuf file record type RDBTUFFile struct { rethinkdb.Timing Gun string `gorethink:"gun"` @@ -16,14 +20,12 @@ type RDBTUFFile struct { Data []byte `gorethink:"data"` } -func (_ RDBTufFile) TableName() string { +// TableName returns the table name for the record type +func (r RDBTUFFile) TableName() string { return "tuf_files" } -func (_ RDBTufFile) DatabaseName() string { - return "notaryserver" -} - +// RDBKey is the public key record type RDBKey struct { rethinkdb.Timing Gun string `gorethink:"gun"` @@ -32,25 +34,22 @@ type RDBKey struct { Public []byte `gorethink:"public"` } -func (_ RDBKey) TableName() string { +// TableName returns the table name for the record type +func (r RDBKey) TableName() string { return "tuf_keys" } -func (_ RDBKey) DatabaseName() string { - return "notaryserver" -} - // RethinkDB implements a MetaStore against the Rethink Database type RethinkDB struct { dbName string - rdb *gorethink.Session + sess *gorethink.Session } // NewRethinkDBStorage initializes a RethinkDB object -func NewRethinkDBStorage(dbName string, sess *gorethink.Session) MetaStore { +func NewRethinkDBStorage(dbName string, sess *gorethink.Session) RethinkDB { return RethinkDB{ dbName: dbName, - rdb: sess, + sess: sess, } } @@ -137,7 +136,7 @@ func (rdb RethinkDB) UpdateMany(gun string, updates []MetaUpdate) error { // the latest version of the given GUN and role. If there is no data for // the given GUN and role, an error is returned. func (rdb RethinkDB) GetCurrent(gun, role string) (created *time.Time, data []byte, err error) { - var file RDBTUFFile + file := RDBTUFFile{} res, err := gorethink.DB(rdb.dbName).Table(file.TableName()).Get( RDBTUFFile{ Gun: gun, @@ -147,14 +146,14 @@ func (rdb RethinkDB) GetCurrent(gun, role string) (created *time.Time, data []by return nil, nil, err } defer res.Close() - err = res.One(&key) + err = res.One(&file) return &file.CreatedAt, file.Data, err } // GetChecksum returns the given TUF role file and creation date for the // GUN with the provided checksum. If the given (gun, role, checksum) are // not found, it returns storage.ErrNotFound -func (rdb RethinkDB) GetChecksum(gun, tufRole, checksum string) (created *time.Time, data []byte, err error) { +func (rdb RethinkDB) GetChecksum(gun, role, checksum string) (created *time.Time, data []byte, err error) { var file RDBTUFFile res, err := gorethink.DB(rdb.dbName).Table(file.TableName()).Get( RDBTUFFile{ @@ -166,7 +165,7 @@ func (rdb RethinkDB) GetChecksum(gun, tufRole, checksum string) (created *time.T return nil, nil, err } defer res.Close() - err = res.One(&key) + err = res.One(&file) return &file.CreatedAt, file.Data, err } @@ -180,3 +179,16 @@ func (rdb RethinkDB) Delete(gun string) error { } return nil } + +// Bootstrap sets up the database and tables +func (rdb RethinkDB) Bootstrap() error { + return rethinkdb.SetupDB(rdb.sess, rdb.dbName, []rethinkdb.Table{ + tufFiles, + keys, + }) +} + +// CheckHealth is currently a noop +func (rdb RethinkDB) CheckHealth() error { + return nil +} diff --git a/server/storage/rethinkdb_models.go b/server/storage/rethinkdb_models.go new file mode 100644 index 0000000000..6b06333335 --- /dev/null +++ b/server/storage/rethinkdb_models.go @@ -0,0 +1,21 @@ +package storage + +import ( + "github.com/docker/notary/storage/rethinkdb" +) + +var ( + tufFiles = rethinkdb.Table{ + Name: RDBTUFFile{}.TableName(), + PrimaryKey: []string{"gun", "role", "version"}, + SecondaryIndexes: map[string][]string{ + "sha256": nil, + }, + } + + keys = rethinkdb.Table{ + Name: RDBKey{}.TableName(), + PrimaryKey: "id", + SecondaryIndexes: nil, + } +) diff --git a/signer/keydbstore/rethink_keydbstore.go b/signer/keydbstore/rethink_keydbstore.go index 6561bb2a95..8c530346c6 100644 --- a/signer/keydbstore/rethink_keydbstore.go +++ b/signer/keydbstore/rethink_keydbstore.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "sync" + "time" "github.com/dancannon/gorethink" "github.com/docker/notary/passphrase" @@ -39,7 +40,7 @@ func (g RethinkPrivateKey) TableName() string { return "private_keys" } -// TableName sets a specific table name for our RethinkPrivateKey +// DatabaseName sets a specific table name for our RethinkPrivateKey func (g RethinkPrivateKey) DatabaseName() string { return "notarysigner" } @@ -76,7 +77,7 @@ func (s *KeyRethinkDBStore) AddKey(keyInfo trustmanager.KeyInfo, privKey data.Pr now := time.Now() rethinkPrivKey := RethinkPrivateKey{ - rethinkdb.Timing{ + Timing: rethinkdb.Timing{ CreatedAt: now, UpdatedAt: now, }, @@ -112,7 +113,7 @@ func (s *KeyRethinkDBStore) GetKey(name string) (data.PrivateKey, string, error) } // Retrieve the RethinkDB private key from the database - var dbPrivateKey RethinkPrivateKey + dbPrivateKey := RethinkPrivateKey{} res, err := gorethink.DB(dbPrivateKey.DatabaseName()).Table(dbPrivateKey.TableName()).Get(RethinkPrivateKey{KeyID: name}).Run(s.session) if err != nil { return nil, "", trustmanager.ErrKeyNotFound{} diff --git a/storage/interface.go b/storage/interface.go new file mode 100644 index 0000000000..2951e248fe --- /dev/null +++ b/storage/interface.go @@ -0,0 +1,8 @@ +package storage + +// Bootstrapper is a thing that can set itself up +type Bootstrapper interface { + // Bootstrap instructs a configured Bootstrapper to perform + // its setup operations. + Bootstrap() error +} diff --git a/storage/rethinkdb/bootstrap.go b/storage/rethinkdb/bootstrap.go new file mode 100644 index 0000000000..a0258cf23e --- /dev/null +++ b/storage/rethinkdb/bootstrap.go @@ -0,0 +1,137 @@ +package rethinkdb + +import ( + "fmt" + "strings" + + "github.com/dancannon/gorethink" +) + +func makeDB(session *gorethink.Session, name string) error { + _, err := gorethink.DBCreate(name).RunWrite(session) + if err != nil { + if strings.Contains(err.Error(), "already exists") { + return nil + } + + return err + } + + resp, err := gorethink.DB(name).Wait().Run(session) + if resp != nil { + resp.Close() + } + + return err +} + +// Table holds the configuration for setting up a RethinkDB table +type Table struct { + Name string + PrimaryKey interface{} + // Keys are the index names. If len(value) is 0, it is a simple index + // on the field matching the key. Otherwise, it is a compound index + // on the list of fields in the corrensponding slice value. + SecondaryIndexes map[string][]string +} + +func (t Table) term(dbName string) gorethink.Term { + return gorethink.DB(dbName).Table(t.Name) +} + +func (t Table) wait(session *gorethink.Session, dbName string) error { + resp, err := t.term(dbName).Wait().Run(session) + + if resp != nil { + resp.Close() + } + + return err +} + +func (t Table) create(session *gorethink.Session, dbName string, numReplicas uint) error { + createOpts := gorethink.TableCreateOpts{ + PrimaryKey: t.PrimaryKey, + Durability: "hard", + } + + if _, err := gorethink.DB(dbName).TableCreate(t.Name, createOpts).RunWrite(session); err != nil { + if !strings.Contains(err.Error(), "already exists") { + return fmt.Errorf("unable to run table creation: %s", err) + } + } + + reconfigureOpts := gorethink.ReconfigureOpts{ + Shards: 1, + Replicas: numReplicas, + } + + if _, err := t.term(dbName).Reconfigure(reconfigureOpts).RunWrite(session); err != nil { + return fmt.Errorf("unable to reconfigure table replication: %s", err) + } + + if err := t.wait(session, dbName); err != nil { + return fmt.Errorf("unable to wait for table to be ready after reconfiguring replication: %s", err) + } + + for indexName, fieldNames := range t.SecondaryIndexes { + if len(fieldNames) == 0 { + // The field name is the index name. + fieldNames = []string{indexName} + } + + if _, err := t.term(dbName).IndexCreateFunc(indexName, func(row gorethink.Term) interface{} { + fields := make([]interface{}, len(fieldNames)) + + for i, fieldName := range fieldNames { + term := row + for _, subfield := range strings.Split(fieldName, ".") { + term = term.Field(subfield) + } + + fields[i] = term + } + + if len(fields) == 1 { + return fields[0] + } + + return fields + }).RunWrite(session); err != nil { + if !strings.Contains(err.Error(), "already exists") { + return fmt.Errorf("unable to create secondary index %q: %s", indexName, err) + } + } + } + + if err := t.wait(session, dbName); err != nil { + return fmt.Errorf("unable to wait for table to be ready after creating secondary indexes: %s", err) + } + + return nil +} + +// SetupDB hadles creating the database and creating all tables and indexes. +func SetupDB(session *gorethink.Session, dbName string, tables []Table) error { + if err := makeDB(session, dbName); err != nil { + return fmt.Errorf("unable to create database: %s", err) + } + + cursor, err := gorethink.DB("rethinkdb").Table("server_config").Count().Run(session) + if err != nil { + return fmt.Errorf("unable to query db server config: %s", err) + } + + var replicaCount uint + if err := cursor.One(&replicaCount); err != nil { + return fmt.Errorf("unable to scan db server config count: %s", err) + } + + for _, table := range tables { + if err = table.create(session, dbName, replicaCount); err != nil { + return fmt.Errorf("unable to create table %q: %s", table.Name, err) + } + } + + return nil +} diff --git a/utils/configuration.go b/utils/configuration.go index bc17c01557..9dd0a6c34a 100644 --- a/utils/configuration.go +++ b/utils/configuration.go @@ -13,13 +13,8 @@ import ( "github.com/bugsnag/bugsnag-go" "github.com/docker/go-connections/tlsconfig" "github.com/spf13/viper" -) -// Specifies the list of recognized backends -const ( - MemoryBackend = "memory" - MySQLBackend = "mysql" - SqliteBackend = "sqlite3" + "github.com/docker/notary" ) // Storage is a configuration about what storage backend a server should use @@ -28,6 +23,14 @@ type Storage struct { Source string } +// RethinkDBStorage is configuration about a RethinkDB backend service +type RethinkDBStorage struct { + Storage + CA string + AuthKey string + DBName string +} + // GetPathRelativeToConfig gets a configuration key which is a path, and if // it is not empty or an absolute path, returns the absolute path relative // to the configuration file @@ -81,37 +84,72 @@ func ParseLogLevel(configuration *viper.Viper, defaultLevel logrus.Level) ( return logrus.ParseLevel(logStr) } -// ParseStorage tries to parse out Storage from a Viper. If backend and +// ParseSQLStorage tries to parse out Storage from a Viper. If backend and // URL are not provided, returns a nil pointer. Storage is required (if // a backend is not provided, an error will be returned.) -func ParseStorage(configuration *viper.Viper, allowedBackends []string) (*Storage, error) { +func ParseSQLStorage(configuration *viper.Viper) (*Storage, error) { store := Storage{ Backend: configuration.GetString("storage.backend"), Source: configuration.GetString("storage.db_url"), } - supported := false - store.Backend = strings.ToLower(store.Backend) - for _, backend := range allowedBackends { - if backend == store.Backend { - supported = true - break - } + switch { + case store.Backend != notary.MySQLBackend && store.Backend != notary.SQLiteBackend: + return nil, fmt.Errorf( + "%s is not a supported SQL backend driver", + store.Backend, + ) + case store.Source == "": + return nil, fmt.Errorf( + "must provide a non-empty database source for %s", + store.Backend, + ) + } + return &store, nil +} + +// ParseRethinkDBStorage tries to parse out Storage from a Viper. If backend and +// URL are not provided, returns a nil pointer. Storage is required (if +// a backend is not provided, an error will be returned.) +func ParseRethinkDBStorage(configuration *viper.Viper) (*RethinkDBStorage, error) { + store := RethinkDBStorage{ + Storage: Storage{ + Backend: configuration.GetString("storage.backend"), + Source: configuration.GetString("storage.db_url"), + }, + CA: configuration.GetString("storage.tls_ca_file"), + AuthKey: configuration.GetString("storage.auth_key"), + DBName: configuration.GetString("storage.database"), } - if !supported { + switch { + case store.Backend != notary.RethinkDBBackend: return nil, fmt.Errorf( - "must specify one of these supported backends: %s", - strings.Join(allowedBackends, ", ")) + "%s is not a supported RethinkDB backend driver", + store.Backend, + ) + case store.Source == "": + return nil, fmt.Errorf( + "must provide a non-empty host:port for %s", + store.Backend, + ) + case store.CA == "": + return nil, fmt.Errorf( + "cowardly refusal to connect to %s without a CA cert", + store.Backend, + ) + case store.AuthKey == "": + return nil, fmt.Errorf( + "cowardly refusal to connect to %s without an AuthKey", + store.Backend, + ) + case store.DBName == "": + return nil, fmt.Errorf( + "%s requires a specific database to connect to", + store.Backend, + ) } - if store.Backend == MemoryBackend { - return &Storage{Backend: MemoryBackend}, nil - } - if store.Source == "" { - return nil, fmt.Errorf( - "must provide a non-empty database source for %s", store.Backend) - } return &store, nil } diff --git a/utils/configuration_test.go b/utils/configuration_test.go index a952bd4b3c..d7bca8402f 100644 --- a/utils/configuration_test.go +++ b/utils/configuration_test.go @@ -11,6 +11,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/bugsnag/bugsnag-go" + "github.com/docker/notary" "github.com/docker/notary/trustmanager" "github.com/spf13/viper" "github.com/stretchr/testify/require" @@ -164,11 +165,10 @@ func TestParseInvalidStorageBackend(t *testing.T) { `{}`, } for _, configJSON := range invalids { - _, err := ParseStorage(configure(configJSON), - []string{MySQLBackend, SqliteBackend}) + _, err := ParseSQLStorage(configure(configJSON)) require.Error(t, err, fmt.Sprintf("'%s' should be an error", configJSON)) require.Contains(t, err.Error(), - "must specify one of these supported backends: mysql, sqlite3") + "is not a supported SQL backend driver") } } @@ -178,11 +178,10 @@ func TestParseInvalidStorageNoDBSource(t *testing.T) { `{"storage": {"backend": "%s"}}`, `{"storage": {"backend": "%s", "db_url": ""}}`, } - for _, backend := range []string{MySQLBackend, SqliteBackend} { + for _, backend := range []string{notary.MySQLBackend, notary.SQLiteBackend} { for _, configJSONFmt := range invalids { configJSON := fmt.Sprintf(configJSONFmt, backend) - _, err := ParseStorage(configure(configJSON), - []string{MySQLBackend, SqliteBackend}) + _, err := ParseSQLStorage(configure(configJSON)) require.Error(t, err, fmt.Sprintf("'%s' should be an error", configJSON)) require.Contains(t, err.Error(), fmt.Sprintf("must provide a non-empty database source for %s", backend)) @@ -190,22 +189,11 @@ func TestParseInvalidStorageNoDBSource(t *testing.T) { } } -// If a memory storage backend is specified, no DB URL is necessary for a -// successful storage parse. -func TestParseStorageMemoryStore(t *testing.T) { - config := configure(`{"storage": {"backend": "MEMORY"}}`) - expected := Storage{Backend: MemoryBackend} - - store, err := ParseStorage(config, []string{MySQLBackend, MemoryBackend}) - require.NoError(t, err) - require.Equal(t, expected, *store) -} - // A supported backend with DB source will be successfully parsed. -func TestParseStorageDBStore(t *testing.T) { +func TestParseSQLStorageDBStore(t *testing.T) { config := configure(`{ "storage": { - "backend": "MySQL", + "backend": "mysql", "db_url": "username:passord@tcp(hostname:1234)/dbname" } }`) @@ -215,19 +203,19 @@ func TestParseStorageDBStore(t *testing.T) { Source: "username:passord@tcp(hostname:1234)/dbname", } - store, err := ParseStorage(config, []string{"mysql"}) + store, err := ParseSQLStorage(config) require.NoError(t, err) require.Equal(t, expected, *store) } -func TestParseStorageWithEnvironmentVariables(t *testing.T) { +func TestParseSQLStorageWithEnvironmentVariables(t *testing.T) { config := configure(`{ "storage": { "db_url": "username:passord@tcp(hostname:1234)/dbname" } }`) - vars := map[string]string{"STORAGE_BACKEND": "MySQL"} + vars := map[string]string{"STORAGE_BACKEND": "mysql"} setupEnvironmentVariables(t, vars) defer cleanupEnvironmentVariables(t, vars) @@ -236,7 +224,7 @@ func TestParseStorageWithEnvironmentVariables(t *testing.T) { Source: "username:passord@tcp(hostname:1234)/dbname", } - store, err := ParseStorage(config, []string{"mysql"}) + store, err := ParseSQLStorage(config) require.NoError(t, err) require.Equal(t, expected, *store) }