open-vault/physical/etcd.go

170 lines
4.4 KiB
Go

package physical
import (
"encoding/base64"
"errors"
"path/filepath"
"strings"
"time"
"github.com/armon/go-metrics"
"github.com/coreos/go-etcd/etcd"
)
const (
// Ideally, this prefix would match the "_" used in the file backend, but
// that prefix has special meaining in etcd. Specifically, it excludes those
// entries from directory listings.
EtcdNodeFilePrefix = "."
// The delimiter is the same as the `-C` flag of etcdctl.
EtcdMachineDelimiter = ","
)
var (
EtcdSyncClusterError = errors.New("client setup failed: unable to sync etcd cluster")
)
// errorIsMissingKey returns true if the given error is an etcd error with an
// error code corresponding to a missing key.
func errorIsMissingKey(err error) bool {
etcdErr, ok := err.(*etcd.EtcdError)
return ok && etcdErr.ErrorCode == 100
}
// EtcdBackend is a physical backend that stores data at specific
// prefix within Etcd. It is used for most production situations as
// it allows Vault to run on multiple machines in a highly-available manner.
type EtcdBackend struct {
path string
client *etcd.Client
}
// newEtcdBackend constructs a etcd backend using a given machine address.
func newEtcdBackend(conf map[string]string) (Backend, error) {
// Get the etcd path form the configuration.
path, ok := conf["path"]
if !ok {
path = "/vault"
}
// Ensure path is prefixed.
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
// Set a default machines list and check for an overriding address value.
machines := "http://128.0.0.1:4001"
if address, ok := conf["address"]; ok {
machines = address
}
// Create a new client from the supplied addres and attempt to sync with the
// cluster.
client := etcd.NewClient(strings.Split(machines, EtcdMachineDelimiter))
if !client.SyncCluster() {
return nil, EtcdSyncClusterError
}
// Setup the backend
return &EtcdBackend{
path: path,
client: client,
}, nil
}
// Put is used to insert or update an entry.
func (c *EtcdBackend) Put(entry *Entry) error {
defer metrics.MeasureSince([]string{"etcd", "put"}, time.Now())
value := base64.StdEncoding.EncodeToString(entry.Value)
_, err := c.client.Set(c.nodePath(entry.Key), value, 0)
return err
}
// Get is used to fetch an entry.
func (c *EtcdBackend) Get(key string) (*Entry, error) {
defer metrics.MeasureSince([]string{"etcd", "get"}, time.Now())
response, err := c.client.Get(c.nodePath(key), false, false)
if err != nil {
if errorIsMissingKey(err) {
return nil, nil
}
return nil, err
}
// Decode the stored value from base-64.
value, err := base64.StdEncoding.DecodeString(response.Node.Value)
if err != nil {
return nil, err
}
// Construct and return a new entry.
return &Entry{
Key: key,
Value: value,
}, nil
}
// Delete is used to permanently delete an entry.
func (c *EtcdBackend) Delete(key string) error {
defer metrics.MeasureSince([]string{"etcd", "delete"}, time.Now())
// Remove the key, non-recursively.
_, err := c.client.Delete(c.nodePath(key), false)
if err != nil && !errorIsMissingKey(err) {
return err
}
return nil
}
// List is used to list all the keys under a given prefix, up to the next
// prefix.
func (c *EtcdBackend) List(prefix string) ([]string, error) {
defer metrics.MeasureSince([]string{"etcd", "list"}, time.Now())
// Set a directory path from the given prefix.
path := c.nodePathDir(prefix)
// Get the directory, non-recursively, from etcd. If the directory is
// missing, we just return an empty list of contents.
response, err := c.client.Get(path, true, false)
if err != nil {
if errorIsMissingKey(err) {
return []string{}, nil
}
return nil, err
}
out := make([]string, len(response.Node.Nodes))
for i, node := range response.Node.Nodes {
// etcd keys include the full path, so let's trim the prefix directory
// path.
name := strings.TrimPrefix(node.Key, path)
// Check if this node is itself a directory. If it is, add a trailing
// slash; if it isn't remove the node file prefix.
if node.Dir {
out[i] = name + "/"
} else {
out[i] = name[1:]
}
}
return out, nil
}
// nodePath returns an etcd filepath based on the given key.
func (b *EtcdBackend) nodePath(key string) string {
return filepath.Join(b.path, filepath.Dir(key), EtcdNodeFilePrefix+filepath.Base(key))
}
// nodePathDir returns an etcd directory path based on the given key.
func (b *EtcdBackend) nodePathDir(key string) string {
return filepath.Join(b.path, key) + "/"
}