command/server: add config loading
This commit is contained in:
parent
39b42bb862
commit
d88c20e293
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"listener": {
|
||||
"tcp": {
|
||||
"address": "127.0.0.1:443"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
backend "consul" {
|
||||
foo = "bar"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
listener "tcp" {
|
||||
address = "127.0.0.1:443"
|
||||
}
|
||||
|
||||
backend "consul" {
|
||||
foo = "bar"
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"listener": {
|
||||
"tcp": {
|
||||
"address": "127.0.0.1:443"
|
||||
}
|
||||
},
|
||||
|
||||
"backend": {
|
||||
"consul": {
|
||||
"foo": "bar"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"listener": {
|
||||
"tcp": [{
|
||||
"address": "127.0.0.1:443"
|
||||
}]
|
||||
},
|
||||
|
||||
"backend": {
|
||||
"consul": {
|
||||
"foo": "bar"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue