command/server: add config loading

This commit is contained in:
Mitchell Hashimoto 2015-03-12 15:21:11 -07:00
parent 39b42bb862
commit d88c20e293
11 changed files with 544 additions and 0 deletions

63
command/server.go Normal file
View File

@ -0,0 +1,63 @@
package command
import (
"strings"
"github.com/hashicorp/vault/helper/flag-slice"
)
// ServerCommand is a Command that starts the Vault server.
type ServerCommand struct {
Meta
}
func (c *ServerCommand) Run(args []string) int {
var configPath []string
flags := c.Meta.FlagSet("server", FlagSetDefault)
flags.Usage = func() { c.Ui.Error(c.Help()) }
flags.Var((*sliceflag.StringFlag)(&configPath), "config", "config")
if err := flags.Parse(args); err != nil {
return 1
}
// Validation
if len(configPath) == 0 {
c.Ui.Error("At least one config path must be specified with -config")
flags.Usage()
return 1
}
// Load the configuration
return 0
}
func (c *ServerCommand) Synopsis() string {
return "Start a Vault server"
}
func (c *ServerCommand) Help() string {
helpText := `
Usage: vault server [options]
Start a Vault server.
This command starts a Vault server that responds to API requests.
Vault will start in a "sealed" state. The Vault must be unsealed
with "vault unseal" or the API before this server can respond to requests.
This must be done for every server.
If the server is being started against a storage backend that has
brand new (no existing Vault data in it), it must be initialized with
"vault init" or the API first.
General Options:
-config=<path> Path to the configuration file or directory. This can be
specified multiple times. If it is a directory, all
files with a ".hcl" or ".json" suffix will be loaded.
`
return strings.TrimSpace(helpText)
}

265
command/server/config.go Normal file
View File

@ -0,0 +1,265 @@
package server
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/hashicorp/hcl"
hclobj "github.com/hashicorp/hcl/hcl"
)
// Config is the configuration for the vault server.
type Config struct {
Listeners []*Listener
Backend *Backend
}
// Listener is the listener configuration for the server.
type Listener struct {
Type string
Config map[string]interface{}
}
func (l *Listener) GoString() string {
return fmt.Sprintf("*%#v", *l)
}
// Backend is the backend configuration for the server.
type Backend struct {
Type string
Config map[string]interface{}
}
func (b *Backend) GoString() string {
return fmt.Sprintf("*%#v", *b)
}
// 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
}
return result
}
// 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 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
}
}
return &result, nil
}
// 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]interface{}
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]interface{}
if err := hcl.DecodeObject(&config, obj); err != nil {
return nil, fmt.Errorf(
"Error reading config for backend %s: %s",
result.Type,
err)
}
result.Config = config
return &result, nil
}

View File

@ -0,0 +1,118 @@
package server
import (
"reflect"
"testing"
)
func TestLoadConfigFile(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config.hcl")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &Config{
Listeners: []*Listener{
&Listener{
Type: "tcp",
Config: map[string]interface{}{
"address": "127.0.0.1:443",
},
},
},
Backend: &Backend{
Type: "consul",
Config: map[string]interface{}{
"foo": "bar",
},
},
}
if !reflect.DeepEqual(config, expected) {
t.Fatalf("bad: %#v", config)
}
}
func TestLoadConfigFile_json(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config.hcl.json")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &Config{
Listeners: []*Listener{
&Listener{
Type: "tcp",
Config: map[string]interface{}{
"address": "127.0.0.1:443",
},
},
},
Backend: &Backend{
Type: "consul",
Config: map[string]interface{}{
"foo": "bar",
},
},
}
if !reflect.DeepEqual(config, expected) {
t.Fatalf("bad: %#v", config)
}
}
func TestLoadConfigFile_json2(t *testing.T) {
config, err := LoadConfigFile("./test-fixtures/config2.hcl.json")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &Config{
Listeners: []*Listener{
&Listener{
Type: "tcp",
Config: map[string]interface{}{
"address": "127.0.0.1:443",
},
},
},
Backend: &Backend{
Type: "consul",
Config: map[string]interface{}{
"foo": "bar",
},
},
}
if !reflect.DeepEqual(config, expected) {
t.Fatalf("bad: %#v", config)
}
}
func TestLoadConfigDir(t *testing.T) {
config, err := LoadConfigDir("./test-fixtures/config-dir")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &Config{
Listeners: []*Listener{
&Listener{
Type: "tcp",
Config: map[string]interface{}{
"address": "127.0.0.1:443",
},
},
},
Backend: &Backend{
Type: "consul",
Config: map[string]interface{}{
"foo": "bar",
},
},
}
if !reflect.DeepEqual(config, expected) {
t.Fatalf("bad: %#v", config)
}
}

View File

@ -0,0 +1,7 @@
{
"listener": {
"tcp": {
"address": "127.0.0.1:443"
}
}
}

View File

@ -0,0 +1,3 @@
backend "consul" {
foo = "bar"
}

View File

@ -0,0 +1,7 @@
listener "tcp" {
address = "127.0.0.1:443"
}
backend "consul" {
foo = "bar"
}

View File

@ -0,0 +1,13 @@
{
"listener": {
"tcp": {
"address": "127.0.0.1:443"
}
},
"backend": {
"consul": {
"foo": "bar"
}
}
}

View File

@ -0,0 +1,13 @@
{
"listener": {
"tcp": [{
"address": "127.0.0.1:443"
}]
},
"backend": {
"consul": {
"foo": "bar"
}
}
}

View File

@ -48,6 +48,12 @@ func init() {
}, nil
},
"server": func() (cli.Command, error) {
return &command.ServerCommand{
Meta: meta,
}, nil
},
"version": func() (cli.Command, error) {
ver := Version
rel := VersionPrerelease

16
helper/flag-slice/flag.go Normal file
View File

@ -0,0 +1,16 @@
package sliceflag
import "strings"
// StringFlag implements the flag.Value interface and allows multiple
// calls to the same variable to append a list.
type StringFlag []string
func (s *StringFlag) String() string {
return strings.Join(*s, ",")
}
func (s *StringFlag) Set(value string) error {
*s = append(*s, value)
return nil
}

View File

@ -0,0 +1,33 @@
package sliceflag
import (
"flag"
"reflect"
"testing"
)
func TestStringFlag_implements(t *testing.T) {
var raw interface{}
raw = new(StringFlag)
if _, ok := raw.(flag.Value); !ok {
t.Fatalf("StringFlag should be a Value")
}
}
func TestStringFlagSet(t *testing.T) {
sv := new(StringFlag)
err := sv.Set("foo")
if err != nil {
t.Fatalf("err: %s", err)
}
err = sv.Set("bar")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := []string{"foo", "bar"}
if !reflect.DeepEqual([]string(*sv), expected) {
t.Fatalf("Bad: %#v", sv)
}
}