open-vault/command/server/config.go

399 lines
8.6 KiB
Go

package server
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/hashicorp/hcl"
hclobj "github.com/hashicorp/hcl/hcl"
)
// Config is the configuration for the vault server.
type Config struct {
Listeners []*Listener `hcl:"-"`
Backend *Backend `hcl:"-"`
DisableCache bool `hcl:"disable_cache"`
DisableMlock bool `hcl:"disable_mlock"`
Telemetry *Telemetry `hcl:"telemetry"`
MaxLeaseTTL time.Duration `hcl:"-"`
MaxLeaseTTLRaw string `hcl:"max_lease_ttl"`
DefaultLeaseTTL time.Duration `hcl:"-"`
DefaultLeaseTTLRaw string `hcl:"default_lease_ttl"`
}
// DevConfig is a Config that is used for dev mode of Vault.
func DevConfig() *Config {
return &Config{
DisableCache: false,
DisableMlock: true,
Backend: &Backend{
Type: "inmem",
},
Listeners: []*Listener{
&Listener{
Type: "tcp",
Config: map[string]string{
"tls_disable": "1",
},
},
},
Telemetry: &Telemetry{},
MaxLeaseTTL: 30 * 24 * time.Hour,
DefaultLeaseTTL: 30 * 24 * time.Hour,
}
}
// Listener is the listener configuration for the server.
type Listener struct {
Type string
Config map[string]string
}
func (l *Listener) GoString() string {
return fmt.Sprintf("*%#v", *l)
}
// Backend is the backend configuration for the server.
type Backend struct {
Type string
AdvertiseAddr string
Config map[string]string
}
func (b *Backend) GoString() string {
return fmt.Sprintf("*%#v", *b)
}
// Telemetry is the telemetry configuration for the server
type Telemetry struct {
StatsiteAddr string `hcl:"statsite_address"`
StatsdAddr string `hcl:"statsd_address"`
DisableHostname bool `hcl:"disable_hostname"`
}
func (s *Telemetry) GoString() string {
return fmt.Sprintf("*%#v", *s)
}
// Merge merges two configurations.
func (c *Config) Merge(c2 *Config) *Config {
result := new(Config)
for _, l := range c.Listeners {
result.Listeners = append(result.Listeners, l)
}
for _, l := range c2.Listeners {
result.Listeners = append(result.Listeners, l)
}
result.Backend = c.Backend
if c2.Backend != nil {
result.Backend = c2.Backend
}
result.Telemetry = c.Telemetry
if c2.Telemetry != nil {
result.Telemetry = c2.Telemetry
}
// merging these booleans via an OR operation
result.DisableCache = c.DisableCache
if c2.DisableCache {
result.DisableCache = c2.DisableCache
}
result.DisableMlock = c.DisableMlock
if c2.DisableMlock {
result.DisableMlock = c2.DisableMlock
}
// merge these integers via a MAX operation
result.MaxLeaseTTL = c.MaxLeaseTTL
if c2.MaxLeaseTTL > result.MaxLeaseTTL {
result.MaxLeaseTTL = c2.MaxLeaseTTL
}
result.DefaultLeaseTTL = c.DefaultLeaseTTL
if c2.DefaultLeaseTTL > result.DefaultLeaseTTL {
result.DefaultLeaseTTL = c2.DefaultLeaseTTL
}
return result
}
// LoadConfig loads the configuration at the given path, regardless if
// its a file or directory.
func LoadConfig(path string) (*Config, error) {
fi, err := os.Stat(path)
if err != nil {
return nil, err
}
if fi.IsDir() {
return LoadConfigDir(path)
} else {
return LoadConfigFile(path)
}
}
// LoadConfigFile loads the configuration from the given file.
func LoadConfigFile(path string) (*Config, error) {
// Read the file
d, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
// Parse!
obj, err := hcl.Parse(string(d))
if err != nil {
return nil, err
}
// Start building the result
var result Config
if err := hcl.DecodeObject(&result, obj); err != nil {
return nil, err
}
if result.MaxLeaseTTLRaw != "" {
if result.MaxLeaseTTL, err = time.ParseDuration(result.MaxLeaseTTLRaw); err != nil {
return nil, err
}
}
if result.DefaultLeaseTTLRaw != "" {
if result.DefaultLeaseTTL, err = time.ParseDuration(result.DefaultLeaseTTLRaw); err != nil {
return nil, err
}
}
if objs := obj.Get("listener", false); objs != nil {
result.Listeners, err = loadListeners(objs)
if err != nil {
return nil, err
}
}
if objs := obj.Get("backend", false); objs != nil {
result.Backend, err = loadBackend(objs)
if err != nil {
return nil, err
}
}
// A little hacky but upgrades the old stats config directives to the new way
if result.Telemetry == nil {
statsdAddr := obj.Get("statsd_addr", false)
statsiteAddr := obj.Get("statsite_addr", false)
if statsdAddr != nil || statsiteAddr != nil {
result.Telemetry = &Telemetry{
StatsdAddr: getString(statsdAddr),
StatsiteAddr: getString(statsiteAddr),
}
}
}
return &result, nil
}
func getString(o *hclobj.Object) string {
if o == nil || o.Type != hclobj.ValueTypeString {
return ""
}
return o.Value.(string)
}
// LoadConfigDir loads all the configurations in the given directory
// in alphabetical order.
func LoadConfigDir(dir string) (*Config, error) {
f, err := os.Open(dir)
if err != nil {
return nil, err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return nil, err
}
if !fi.IsDir() {
return nil, fmt.Errorf(
"configuration path must be a directory: %s",
dir)
}
var files []string
err = nil
for err != io.EOF {
var fis []os.FileInfo
fis, err = f.Readdir(128)
if err != nil && err != io.EOF {
return nil, err
}
for _, fi := range fis {
// Ignore directories
if fi.IsDir() {
continue
}
// Only care about files that are valid to load.
name := fi.Name()
skip := true
if strings.HasSuffix(name, ".hcl") {
skip = false
} else if strings.HasSuffix(name, ".json") {
skip = false
}
if skip || isTemporaryFile(name) {
continue
}
path := filepath.Join(dir, name)
files = append(files, path)
}
}
var result *Config
for _, f := range files {
config, err := LoadConfigFile(f)
if err != nil {
return nil, fmt.Errorf("Error loading %s: %s", f, err)
}
if result == nil {
result = config
} else {
result = result.Merge(config)
}
}
return result, nil
}
// isTemporaryFile returns true or false depending on whether the
// provided file name is a temporary file for the following editors:
// emacs or vim.
func isTemporaryFile(name string) bool {
return strings.HasSuffix(name, "~") || // vim
strings.HasPrefix(name, ".#") || // emacs
(strings.HasPrefix(name, "#") && strings.HasSuffix(name, "#")) // emacs
}
func loadListeners(os *hclobj.Object) ([]*Listener, error) {
var allNames []*hclobj.Object
// Really confusing iteration. The key is the false/true parameter
// of whether we're expanding or not. We first iterate over all
// the "listeners"
for _, o1 := range os.Elem(false) {
// Iterate expand to get the list of types
for _, o2 := range o1.Elem(true) {
switch o2.Type {
case hclobj.ValueTypeList:
// This switch is for JSON, to allow them to do this:
//
// "tcp": [{ ... }, { ... }]
//
// To configure multiple listeners of the same type.
for _, o3 := range o2.Elem(true) {
o3.Key = o2.Key
allNames = append(allNames, o3)
}
case hclobj.ValueTypeObject:
// This is for the standard `listener "tcp" { ... }` syntax
allNames = append(allNames, o2)
}
}
}
if len(allNames) == 0 {
return nil, nil
}
// Now go over all the types and their children in order to get
// all of the actual resources.
result := make([]*Listener, 0, len(allNames))
for _, obj := range allNames {
k := obj.Key
var config map[string]string
if err := hcl.DecodeObject(&config, obj); err != nil {
return nil, fmt.Errorf(
"Error reading config for %s: %s",
k,
err)
}
result = append(result, &Listener{
Type: k,
Config: config,
})
}
return result, nil
}
func loadBackend(os *hclobj.Object) (*Backend, error) {
var allNames []*hclobj.Object
// See loadListeners
for _, o1 := range os.Elem(false) {
// Iterate expand to get the list of types
for _, o2 := range o1.Elem(true) {
// Iterate non-expand to get the full list of types
for _, o3 := range o2.Elem(false) {
allNames = append(allNames, o3)
}
}
}
if len(allNames) == 0 {
return nil, nil
}
if len(allNames) > 1 {
keys := make([]string, 0, len(allNames))
for _, o := range allNames {
keys = append(keys, o.Key)
}
return nil, fmt.Errorf(
"Multiple backends declared. Only one is allowed: %v", keys)
}
// Now go over all the types and their children in order to get
// all of the actual resources.
var result Backend
obj := allNames[0]
result.Type = obj.Key
var config map[string]string
if err := hcl.DecodeObject(&config, obj); err != nil {
return nil, fmt.Errorf(
"Error reading config for backend %s: %s",
result.Type,
err)
}
if v, ok := config["advertise_addr"]; ok {
result.AdvertiseAddr = v
delete(config, "advertise_addr")
}
result.Config = config
return &result, nil
}