mirror of https://github.com/docker/docs.git
adding bootstrapping and config update for notary server
Signed-off-by: David Lawrence <david.lawrence@docker.com> (github: endophage)
This commit is contained in:
parent
045721250f
commit
b8c62731a6
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
5
const.go
5
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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
)
|
|
@ -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{}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue