2017-08-03 17:24:27 +00:00
package etcd
2015-05-24 16:38:49 +00:00
import (
2017-01-26 22:19:13 +00:00
"context"
2015-05-24 16:38:49 +00:00
"errors"
2017-04-04 15:50:44 +00:00
"fmt"
"net/url"
2015-12-17 11:48:13 +00:00
"os"
2017-04-04 15:50:44 +00:00
"strings"
2015-05-24 16:38:49 +00:00
2017-01-26 22:19:13 +00:00
"github.com/coreos/etcd/client"
"github.com/coreos/go-semver/semver"
2017-08-03 17:24:27 +00:00
"github.com/hashicorp/vault/physical"
2016-08-19 20:45:17 +00:00
log "github.com/mgutz/logxi/v1"
2015-05-24 16:38:49 +00:00
)
var (
2016-01-11 18:56:58 +00:00
EtcdSyncConfigError = errors . New ( "client setup failed: unable to parse etcd sync field in config" )
2015-05-31 19:02:11 +00:00
EtcdSyncClusterError = errors . New ( "client setup failed: unable to sync etcd cluster" )
2017-04-04 15:50:44 +00:00
EtcdMultipleBootstrapError = errors . New ( "client setup failed: multiple discovery or bootstrap flags specified, use either \"address\" or \"discovery_srv\"" )
2015-11-16 22:30:02 +00:00
EtcdAddressError = errors . New ( "client setup failed: address must be valid URL (ex. 'scheme://host:port')" )
2015-05-31 19:02:11 +00:00
EtcdSemaphoreKeysEmptyError = errors . New ( "lock queue is empty" )
EtcdLockHeldError = errors . New ( "lock already held" )
EtcdLockNotHeldError = errors . New ( "lock not held" )
2018-03-20 18:54:10 +00:00
EtcdSemaphoreKeyRemovedError = errors . New ( "semaphore key removed before lock acquisition" )
2017-08-03 17:24:27 +00:00
EtcdVersionUnknown = errors . New ( "etcd: unknown API version" )
2015-05-24 16:38:49 +00:00
)
2017-08-03 17:24:27 +00:00
// NewEtcdBackend constructs a etcd backend using a given machine address.
func NewEtcdBackend ( conf map [ string ] string , logger log . Logger ) ( physical . Backend , error ) {
2017-01-03 19:43:46 +00:00
var (
apiVersion string
ok bool
)
2015-11-06 07:37:01 +00:00
2017-01-26 22:19:13 +00:00
// v2 client can talk to both etcd2 and etcd3 thought API v2
c , err := newEtcdV2Client ( conf )
if err != nil {
return nil , errors . New ( "failed to create etcd client: " + err . Error ( ) )
}
remoteAPIVersion , err := getEtcdAPIVersion ( c )
if err != nil {
return nil , errors . New ( "failed to get etcd API version: " + err . Error ( ) )
}
2017-01-03 19:43:46 +00:00
if apiVersion , ok = conf [ "etcd_api" ] ; ! ok {
apiVersion = os . Getenv ( "ETCD_API" )
2015-11-16 22:30:02 +00:00
}
2017-01-26 22:19:13 +00:00
2017-01-03 19:43:46 +00:00
if apiVersion == "" {
2017-01-26 22:19:13 +00:00
path , ok := conf [ "path" ]
if ! ok {
path = "/vault"
}
kAPI := client . NewKeysAPI ( c )
2015-11-16 06:12:06 +00:00
2017-01-26 22:19:13 +00:00
// keep using v2 if vault data exists in v2 and user does not explicitly
// ask for v3.
_ , err := kAPI . Get ( context . Background ( ) , path , & client . GetOptions { } )
if errorIsMissingKey ( err ) {
apiVersion = remoteAPIVersion
} else if err == nil {
apiVersion = "2"
} else {
return nil , errors . New ( "failed to check etcd status: " + err . Error ( ) )
}
}
2015-12-17 11:48:13 +00:00
2017-01-03 19:43:46 +00:00
switch apiVersion {
case "2" , "etcd2" , "v2" :
return newEtcd2Backend ( conf , logger )
case "3" , "etcd3" , "v3" :
2017-01-26 22:19:13 +00:00
if remoteAPIVersion == "2" {
return nil , errors . New ( "etcd3 is required: etcd2 is running" )
}
2017-01-03 19:43:46 +00:00
return newEtcd3Backend ( conf , logger )
2016-01-27 22:15:52 +00:00
default :
2017-08-03 17:24:27 +00:00
return nil , EtcdVersionUnknown
2015-05-31 19:02:11 +00:00
}
}
2017-01-26 22:19:13 +00:00
// getEtcdAPIVersion gets the latest supported API version.
// If etcd cluster version >= 3.1, "3" will be returned.
// Otherwise, "2" will be returned.
func getEtcdAPIVersion ( c client . Client ) ( string , error ) {
v , err := c . GetVersion ( context . Background ( ) )
if err != nil {
return "" , err
}
sv , err := semver . NewVersion ( v . Cluster )
if err != nil {
return "" , nil
}
if sv . LessThan ( * semver . Must ( semver . NewVersion ( "3.1.0" ) ) ) {
return "2" , nil
}
return "3" , nil
}
2017-04-04 15:50:44 +00:00
// Retrieves the config option in order of priority:
// 1. The named environment variable if it exist
// 2. The key in the config map
func getEtcdOption ( conf map [ string ] string , confKey , envVar string ) ( string , bool ) {
confVal , inConf := conf [ confKey ]
envVal , inEnv := os . LookupEnv ( envVar )
if inEnv {
return envVal , true
}
return confVal , inConf
}
func getEtcdEndpoints ( conf map [ string ] string ) ( [ ] string , error ) {
address , staticBootstrap := getEtcdOption ( conf , "address" , "ETCD_ADDR" )
domain , useSrv := getEtcdOption ( conf , "discovery_srv" , "ETCD_DISCOVERY_SRV" )
if useSrv && staticBootstrap {
return nil , EtcdMultipleBootstrapError
}
if staticBootstrap {
endpoints := strings . Split ( address , Etcd2MachineDelimiter )
// Verify that the machines are valid URLs
for _ , e := range endpoints {
u , urlErr := url . Parse ( e )
if urlErr != nil || u . Scheme == "" {
return nil , EtcdAddressError
}
}
return endpoints , nil
}
if useSrv {
discoverer := client . NewSRVDiscover ( )
endpoints , err := discoverer . Discover ( domain )
if err != nil {
return nil , fmt . Errorf ( "failed to discover etcd endpoints through SRV discovery: %v" , err )
}
return endpoints , nil
}
// Set a default endpoints list if no option was set
return [ ] string { "http://127.0.0.1:2379" } , nil
}