etcd: refactors to simplify etcd configuration

This refactor removes a redundancy where we had etcd configuration under
kvdb and kvdb/etcd packages.
This commit is contained in:
Andras Banki-Horvath 2021-02-09 20:19:31 +01:00
parent 44e312ace9
commit 6757e14998
No known key found for this signature in database
GPG Key ID: 80E5375C094198D8
16 changed files with 136 additions and 157 deletions

@ -33,30 +33,3 @@ type BoltConfig struct {
DBTimeout time.Duration `long:"dbtimeout" description:"Specify the timeout value used when opening the database."`
}
// EtcdConfig holds etcd configuration.
type EtcdConfig struct {
Embedded bool `long:"embedded" description:"Use embedded etcd instance instead of the external one. Note: use for testing only."`
EmbeddedClientPort uint16 `long:"embedded_client_port" description:"Client port to use for the embedded instance. Note: use for testing only."`
EmbeddedPeerPort uint16 `long:"embedded_peer_port" description:"Peer port to use for the embedded instance. Note: use for testing only."`
Host string `long:"host" description:"Etcd database host."`
User string `long:"user" description:"Etcd database user."`
Pass string `long:"pass" description:"Password for the database user."`
Namespace string `long:"namespace" description:"The etcd namespace to use."`
DisableTLS bool `long:"disabletls" description:"Disable TLS for etcd connection. Caution: use for development only."`
CertFile string `long:"cert_file" description:"Path to the TLS certificate for etcd RPC."`
KeyFile string `long:"key_file" description:"Path to the TLS private key for etcd RPC."`
InsecureSkipVerify bool `long:"insecure_skip_verify" description:"Whether we intend to skip TLS verification"`
CollectStats bool `long:"collect_stats" description:"Whether to collect etcd commit stats."`
}

@ -7,7 +7,7 @@ package etcd
func bkey(buckets ...string) string {
var bucketKey []byte
rootID := makeBucketID([]byte(""))
rootID := makeBucketID([]byte(etcdDefaultRootBucketId))
parent := rootID[:]
for _, bucketName := range buckets {
@ -29,7 +29,7 @@ func bval(buckets ...string) string {
// vkey is a helper function used in tests to create a value key from the
// passed key and bucket list.
func vkey(key string, buckets ...string) string {
rootID := makeBucketID([]byte(""))
rootID := makeBucketID([]byte(etcdDefaultRootBucketId))
bucket := rootID[:]
for _, bucketName := range buckets {

@ -0,0 +1,28 @@
package etcd
// Config holds etcd configuration alongside with configuration related to our higher level interface.
type Config struct {
Embedded bool `long:"embedded" description:"Use embedded etcd instance instead of the external one. Note: use for testing only."`
EmbeddedClientPort uint16 `long:"embedded_client_port" description:"Client port to use for the embedded instance. Note: use for testing only."`
EmbeddedPeerPort uint16 `long:"embedded_peer_port" description:"Peer port to use for the embedded instance. Note: use for testing only."`
Host string `long:"host" description:"Etcd database host."`
User string `long:"user" description:"Etcd database user."`
Pass string `long:"pass" description:"Password for the database user."`
Namespace string `long:"namespace" description:"The etcd namespace to use."`
DisableTLS bool `long:"disabletls" description:"Disable TLS for etcd connection. Caution: use for development only."`
CertFile string `long:"cert_file" description:"Path to the TLS certificate for etcd RPC."`
KeyFile string `long:"key_file" description:"Path to the TLS private key for etcd RPC."`
InsecureSkipVerify bool `long:"insecure_skip_verify" description:"Whether we intend to skip TLS verification"`
CollectStats bool `long:"collect_stats" description:"Whether to collect etcd commit stats."`
}

@ -120,7 +120,8 @@ func (c *commitStatsCollector) callback(succ bool, stats CommitStats) {
// db holds a reference to the etcd client connection.
type db struct {
config BackendConfig
cfg Config
ctx context.Context
cli *clientv3.Client
commitStatsCollector *commitStatsCollector
txQueue *commitQueue
@ -129,61 +130,23 @@ type db struct {
// Enforce db implements the walletdb.DB interface.
var _ walletdb.DB = (*db)(nil)
// BackendConfig holds and etcd backend config and connection parameters.
type BackendConfig struct {
// Ctx is the context we use to cancel operations upon exit.
Ctx context.Context
// Host holds the peer url of the etcd instance.
Host string
// User is the username for the etcd peer.
User string
// Pass is the password for the etcd peer.
Pass string
// DisableTLS disables the use of TLS for etcd connections.
DisableTLS bool
// CertFile holds the path to the TLS certificate for etcd RPC.
CertFile string
// KeyFile holds the path to the TLS private key for etcd RPC.
KeyFile string
// InsecureSkipVerify should be set to true if we intend to
// skip TLS verification.
InsecureSkipVerify bool
// Namespace is the etcd namespace that we'll use for all keys.
Namespace string
// CollectCommitStats indicates wheter to commit commit stats.
CollectCommitStats bool
}
// newEtcdBackend returns a db object initialized with the passed backend
// config. If etcd connection cannot be estabished, then returns error.
func newEtcdBackend(config BackendConfig) (*db, error) {
if config.Ctx == nil {
config.Ctx = context.Background()
}
func newEtcdBackend(ctx context.Context, cfg Config) (*db, error) {
clientCfg := clientv3.Config{
Context: config.Ctx,
Endpoints: []string{config.Host},
Context: ctx,
Endpoints: []string{cfg.Host},
DialTimeout: etcdConnectionTimeout,
Username: config.User,
Password: config.Pass,
Username: cfg.User,
Password: cfg.Pass,
MaxCallSendMsgSize: 16384*1024 - 1,
}
if !config.DisableTLS {
if !cfg.DisableTLS {
tlsInfo := transport.TLSInfo{
CertFile: config.CertFile,
KeyFile: config.KeyFile,
InsecureSkipVerify: config.InsecureSkipVerify,
CertFile: cfg.CertFile,
KeyFile: cfg.KeyFile,
InsecureSkipVerify: cfg.InsecureSkipVerify,
}
tlsConfig, err := tlsInfo.ClientConfig()
@ -200,17 +163,18 @@ func newEtcdBackend(config BackendConfig) (*db, error) {
}
// Apply the namespace.
cli.KV = namespace.NewKV(cli.KV, config.Namespace)
cli.Watcher = namespace.NewWatcher(cli.Watcher, config.Namespace)
cli.Lease = namespace.NewLease(cli.Lease, config.Namespace)
cli.KV = namespace.NewKV(cli.KV, cfg.Namespace)
cli.Watcher = namespace.NewWatcher(cli.Watcher, cfg.Namespace)
cli.Lease = namespace.NewLease(cli.Lease, cfg.Namespace)
backend := &db{
cfg: cfg,
ctx: ctx,
cli: cli,
config: config,
txQueue: NewCommitQueue(config.Ctx),
txQueue: NewCommitQueue(ctx),
}
if config.CollectCommitStats {
if cfg.CollectStats {
backend.commitStatsCollector = newCommitStatsColletor()
}
@ -220,10 +184,10 @@ func newEtcdBackend(config BackendConfig) (*db, error) {
// getSTMOptions creats all STM options based on the backend config.
func (db *db) getSTMOptions() []STMOptionFunc {
opts := []STMOptionFunc{
WithAbortContext(db.config.Ctx),
WithAbortContext(db.ctx),
}
if db.config.CollectCommitStats {
if db.cfg.CollectStats {
opts = append(opts,
WithCommitStatsCallback(db.commitStatsCollector.callback),
)
@ -293,7 +257,7 @@ func (db *db) BeginReadTx() (walletdb.ReadTx, error) {
// start a read-only transaction to perform all operations.
// This function is part of the walletdb.Db interface implementation.
func (db *db) Copy(w io.Writer) error {
ctx, cancel := context.WithTimeout(db.config.Ctx, etcdLongTimeout)
ctx, cancel := context.WithTimeout(db.ctx, etcdLongTimeout)
defer cancel()
readCloser, err := db.cli.Snapshot(ctx)

@ -17,7 +17,7 @@ func TestCopy(t *testing.T) {
f := NewEtcdTestFixture(t)
defer f.Cleanup()
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(context.TODO(), f.BackendConfig())
require.NoError(t, err)
err = db.Update(func(tx walletdb.ReadWriteTx) error {
@ -53,10 +53,9 @@ func TestAbortContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
config := f.BackendConfig()
config.Ctx = ctx
// Pass abort context and abort right away.
db, err := newEtcdBackend(config)
db, err := newEtcdBackend(ctx, config)
require.NoError(t, err)
cancel()

@ -3,6 +3,7 @@
package etcd
import (
"context"
"fmt"
"github.com/btcsuite/btcwallet/walletdb"
@ -13,45 +14,55 @@ const (
)
// parseArgs parses the arguments from the walletdb Open/Create methods.
func parseArgs(funcName string, args ...interface{}) (*BackendConfig, error) {
if len(args) != 1 {
return nil, fmt.Errorf("invalid number of arguments to %s.%s -- "+
"expected: etcd.BackendConfig",
func parseArgs(funcName string, args ...interface{}) (context.Context,
*Config, error) {
if len(args) != 2 {
return nil, nil, fmt.Errorf("invalid number of arguments to "+
"%s.%s -- expected: context.Context, etcd.Config",
dbType, funcName,
)
}
config, ok := args[0].(BackendConfig)
ctx, ok := args[0].(context.Context)
if !ok {
return nil, fmt.Errorf("argument to %s.%s is invalid -- "+
"expected: etcd.BackendConfig",
return nil, nil, fmt.Errorf("argument 0 to %s.%s is invalid "+
"-- expected: context.Context",
dbType, funcName,
)
}
return &config, nil
config, ok := args[1].(*Config)
if !ok {
return nil, nil, fmt.Errorf("argument 1 to %s.%s is invalid -- "+
"expected: etcd.Config",
dbType, funcName,
)
}
return ctx, config, nil
}
// createDBDriver is the callback provided during driver registration that
// creates, initializes, and opens a database for use.
func createDBDriver(args ...interface{}) (walletdb.DB, error) {
config, err := parseArgs("Create", args...)
ctx, config, err := parseArgs("Create", args...)
if err != nil {
return nil, err
}
return newEtcdBackend(*config)
return newEtcdBackend(ctx, *config)
}
// openDBDriver is the callback provided during driver registration that opens
// an existing database for use.
func openDBDriver(args ...interface{}) (walletdb.DB, error) {
config, err := parseArgs("Open", args...)
ctx, config, err := parseArgs("Open", args...)
if err != nil {
return nil, err
}
return newEtcdBackend(*config)
return newEtcdBackend(ctx, *config)
}
func init() {

@ -3,7 +3,6 @@
package etcd
import (
"context"
"fmt"
"net"
"net/url"
@ -62,7 +61,7 @@ func getFreePort() int {
// listening on random open ports. Returns the backend config and a cleanup
// func that will stop the etcd instance.
func NewEmbeddedEtcdInstance(path string, clientPort, peerPort uint16) (
*BackendConfig, func(), error) {
*Config, func(), error) {
cfg := embed.NewConfig()
cfg.Dir = path
@ -98,10 +97,7 @@ func NewEmbeddedEtcdInstance(path string, clientPort, peerPort uint16) (
fmt.Errorf("etcd failed to start after: %v", readyTimeout)
}
ctx, cancel := context.WithCancel(context.Background())
connConfig := &BackendConfig{
Ctx: ctx,
connConfig := &Config{
Host: "http://" + peerURL,
User: "user",
Pass: "pass",
@ -110,7 +106,6 @@ func NewEmbeddedEtcdInstance(path string, clientPort, peerPort uint16) (
}
return connConfig, func() {
cancel()
etcd.Close()
}, nil
}

@ -22,14 +22,14 @@ const (
type EtcdTestFixture struct {
t *testing.T
cli *clientv3.Client
config *BackendConfig
config *Config
cleanup func()
}
// NewTestEtcdInstance creates an embedded etcd instance for testing, listening
// on random open ports. Returns the connection config and a cleanup func that
// will stop the etcd instance.
func NewTestEtcdInstance(t *testing.T, path string) (*BackendConfig, func()) {
func NewTestEtcdInstance(t *testing.T, path string) (*Config, func()) {
t.Helper()
config, cleanup, err := NewEmbeddedEtcdInstance(path, 0, 0)
@ -124,7 +124,7 @@ func (f *EtcdTestFixture) Dump() map[string]string {
// BackendConfig returns the backend config for connecting to theembedded
// etcd instance.
func (f *EtcdTestFixture) BackendConfig() BackendConfig {
func (f *EtcdTestFixture) BackendConfig() Config {
return *f.config
}

@ -3,6 +3,7 @@
package etcd
import (
"context"
"fmt"
"math"
"testing"
@ -17,7 +18,7 @@ func TestBucketCreation(t *testing.T) {
f := NewEtcdTestFixture(t)
defer f.Cleanup()
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(context.TODO(), f.BackendConfig())
require.NoError(t, err)
err = db.Update(func(tx walletdb.ReadWriteTx) error {
@ -98,7 +99,7 @@ func TestBucketDeletion(t *testing.T) {
f := NewEtcdTestFixture(t)
defer f.Cleanup()
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(context.TODO(), f.BackendConfig())
require.NoError(t, err)
err = db.Update(func(tx walletdb.ReadWriteTx) error {
@ -208,7 +209,7 @@ func TestBucketForEach(t *testing.T) {
f := NewEtcdTestFixture(t)
defer f.Cleanup()
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(context.TODO(), f.BackendConfig())
require.NoError(t, err)
err = db.Update(func(tx walletdb.ReadWriteTx) error {
@ -284,7 +285,7 @@ func TestBucketForEachWithError(t *testing.T) {
f := NewEtcdTestFixture(t)
defer f.Cleanup()
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(context.TODO(), f.BackendConfig())
require.NoError(t, err)
err = db.Update(func(tx walletdb.ReadWriteTx) error {
@ -374,7 +375,7 @@ func TestBucketSequence(t *testing.T) {
f := NewEtcdTestFixture(t)
defer f.Cleanup()
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(context.TODO(), f.BackendConfig())
require.NoError(t, err)
err = db.Update(func(tx walletdb.ReadWriteTx) error {
@ -413,7 +414,7 @@ func TestKeyClash(t *testing.T) {
f := NewEtcdTestFixture(t)
defer f.Cleanup()
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(context.TODO(), f.BackendConfig())
require.NoError(t, err)
// First:
@ -481,7 +482,7 @@ func TestBucketCreateDelete(t *testing.T) {
f := NewEtcdTestFixture(t)
defer f.Cleanup()
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(context.TODO(), f.BackendConfig())
require.NoError(t, err)
err = db.Update(func(tx walletdb.ReadWriteTx) error {

@ -3,6 +3,7 @@
package etcd
import (
"context"
"testing"
"github.com/btcsuite/btcwallet/walletdb"
@ -15,7 +16,7 @@ func TestReadCursorEmptyInterval(t *testing.T) {
f := NewEtcdTestFixture(t)
defer f.Cleanup()
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(context.TODO(), f.BackendConfig())
require.NoError(t, err)
err = db.Update(func(tx walletdb.ReadWriteTx) error {
@ -59,7 +60,7 @@ func TestReadCursorNonEmptyInterval(t *testing.T) {
f := NewEtcdTestFixture(t)
defer f.Cleanup()
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(context.TODO(), f.BackendConfig())
require.NoError(t, err)
testKeyValues := []KV{
@ -136,7 +137,7 @@ func TestReadWriteCursor(t *testing.T) {
f := NewEtcdTestFixture(t)
defer f.Cleanup()
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(context.TODO(), f.BackendConfig())
require.NoError(t, err)
testKeyValues := []KV{
@ -300,7 +301,7 @@ func TestReadWriteCursorWithBucketAndValue(t *testing.T) {
f := NewEtcdTestFixture(t)
defer f.Cleanup()
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(context.TODO(), f.BackendConfig())
require.NoError(t, err)
// Pre-store the first half of the interval.

@ -3,6 +3,7 @@
package etcd
import (
"context"
"testing"
"github.com/btcsuite/btcwallet/walletdb"
@ -15,7 +16,7 @@ func TestTxManualCommit(t *testing.T) {
f := NewEtcdTestFixture(t)
defer f.Cleanup()
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(context.TODO(), f.BackendConfig())
require.NoError(t, err)
tx, err := db.BeginReadWriteTx()
@ -55,7 +56,7 @@ func TestTxRollback(t *testing.T) {
f := NewEtcdTestFixture(t)
defer f.Cleanup()
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(context.TODO(), f.BackendConfig())
require.NoError(t, err)
tx, err := db.BeginReadWriteTx()
@ -79,7 +80,7 @@ func TestChangeDuringManualTx(t *testing.T) {
f := NewEtcdTestFixture(t)
defer f.Cleanup()
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(context.TODO(), f.BackendConfig())
require.NoError(t, err)
tx, err := db.BeginReadWriteTx()
@ -108,7 +109,7 @@ func TestChangeDuringUpdate(t *testing.T) {
f := NewEtcdTestFixture(t)
defer f.Cleanup()
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(context.TODO(), f.BackendConfig())
require.NoError(t, err)
count := 0

@ -3,6 +3,7 @@
package etcd
import (
"context"
"errors"
"testing"
@ -21,13 +22,16 @@ func TestPutToEmpty(t *testing.T) {
t.Parallel()
f := NewEtcdTestFixture(t)
txQueue := NewCommitQueue(f.config.Ctx)
ctx, cancel := context.WithCancel(context.Background())
txQueue := NewCommitQueue(ctx)
defer func() {
cancel()
f.Cleanup()
txQueue.Wait()
}()
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(ctx, f.BackendConfig())
require.NoError(t, err)
apply := func(stm STM) error {
@ -45,8 +49,11 @@ func TestGetPutDel(t *testing.T) {
t.Parallel()
f := NewEtcdTestFixture(t)
txQueue := NewCommitQueue(f.config.Ctx)
ctx, cancel := context.WithCancel(context.Background())
txQueue := NewCommitQueue(ctx)
defer func() {
cancel()
f.Cleanup()
txQueue.Wait()
}()
@ -63,7 +70,7 @@ func TestGetPutDel(t *testing.T) {
f.Put(kv.key, kv.val)
}
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(ctx, f.BackendConfig())
require.NoError(t, err)
apply := func(stm STM) error {
@ -128,8 +135,11 @@ func TestFirstLastNextPrev(t *testing.T) {
t.Parallel()
f := NewEtcdTestFixture(t)
txQueue := NewCommitQueue(f.config.Ctx)
ctx, cancel := context.WithCancel(context.Background())
txQueue := NewCommitQueue(ctx)
defer func() {
cancel()
f.Cleanup()
txQueue.Wait()
}()
@ -145,7 +155,7 @@ func TestFirstLastNextPrev(t *testing.T) {
f.Put(kv.key, kv.val)
}
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(ctx, f.BackendConfig())
require.NoError(t, err)
apply := func(stm STM) error {
@ -283,13 +293,16 @@ func TestCommitError(t *testing.T) {
t.Parallel()
f := NewEtcdTestFixture(t)
txQueue := NewCommitQueue(f.config.Ctx)
ctx, cancel := context.WithCancel(context.Background())
txQueue := NewCommitQueue(ctx)
defer func() {
cancel()
f.Cleanup()
txQueue.Wait()
}()
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(ctx, f.BackendConfig())
require.NoError(t, err)
// Preset DB state.
@ -328,13 +341,16 @@ func TestManualTxError(t *testing.T) {
t.Parallel()
f := NewEtcdTestFixture(t)
txQueue := NewCommitQueue(f.config.Ctx)
ctx, cancel := context.WithCancel(context.Background())
txQueue := NewCommitQueue(ctx)
defer func() {
cancel()
f.Cleanup()
txQueue.Wait()
}()
db, err := newEtcdBackend(f.BackendConfig())
db, err := newEtcdBackend(ctx, f.BackendConfig())
require.NoError(t, err)
// Preset DB state.

@ -3,6 +3,7 @@
package etcd
import (
"context"
"testing"
"github.com/btcsuite/btcwallet/walletdb/walletdbtest"
@ -13,5 +14,6 @@ import (
func TestWalletDBInterface(t *testing.T) {
f := NewEtcdTestFixture(t)
defer f.Cleanup()
walletdbtest.TestInterface(t, dbType, f.BackendConfig())
cfg := f.BackendConfig()
walletdbtest.TestInterface(t, dbType, context.TODO(), &cfg)
}

@ -14,25 +14,10 @@ const TestBackend = EtcdBackendName
// GetEtcdBackend returns an etcd backend configured according to the
// passed etcdConfig.
func GetEtcdBackend(ctx context.Context, etcdConfig *EtcdConfig) (
func GetEtcdBackend(ctx context.Context, etcdConfig *etcd.Config) (
Backend, error) {
// Config translation is needed here in order to keep the
// etcd package fully independent from the rest of the source tree.
backendConfig := etcd.BackendConfig{
Ctx: ctx,
Host: etcdConfig.Host,
User: etcdConfig.User,
Pass: etcdConfig.Pass,
DisableTLS: etcdConfig.DisableTLS,
CertFile: etcdConfig.CertFile,
KeyFile: etcdConfig.KeyFile,
InsecureSkipVerify: etcdConfig.InsecureSkipVerify,
Namespace: etcdConfig.Namespace,
CollectCommitStats: etcdConfig.CollectStats,
}
return Open(EtcdBackendName, backendConfig)
return Open(EtcdBackendName, etcdConfig)
}
// GetEtcdTestBackend creates an embedded etcd backend for testing
@ -49,7 +34,7 @@ func GetEtcdTestBackend(path string, clientPort, peerPort uint16) (
return nil, empty, err
}
backend, err := Open(EtcdBackendName, *config)
backend, err := Open(EtcdBackendName, context.Background(), config)
if err != nil {
cleanup()
return nil, empty, err

@ -5,6 +5,8 @@ package kvdb
import (
"context"
"fmt"
"github.com/lightningnetwork/lnd/channeldb/kvdb/etcd"
)
// TestBackend is conditionally set to bdb when the kvdb_etcd build tag is
@ -14,7 +16,7 @@ const TestBackend = BoltBackendName
var errEtcdNotAvailable = fmt.Errorf("etcd backend not available")
// GetEtcdBackend is a stub returning nil and errEtcdNotAvailable error.
func GetEtcdBackend(ctx context.Context, etcdConfig *EtcdConfig) (
func GetEtcdBackend(ctx context.Context, etcdConfig *etcd.Config) (
Backend, error) {
return nil, errEtcdNotAvailable

@ -6,6 +6,7 @@ import (
"time"
"github.com/lightningnetwork/lnd/channeldb/kvdb"
"github.com/lightningnetwork/lnd/channeldb/kvdb/etcd"
)
const (
@ -21,7 +22,7 @@ type DB struct {
BatchCommitInterval time.Duration `long:"batch-commit-interval" description:"The maximum duration the channel graph batch schedulers will wait before attempting to commit a batch of pending updates. This can be tradeoff database contenion for commit latency."`
Etcd *kvdb.EtcdConfig `group:"etcd" namespace:"etcd" description:"Etcd settings."`
Etcd *etcd.Config `group:"etcd" namespace:"etcd" description:"Etcd settings."`
Bolt *kvdb.BoltConfig `group:"bolt" namespace:"bolt" description:"Bolt settings."`
}