mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-02 11:38:02 +00:00
command/server: add config loading
This commit is contained in:
63
command/server.go
Normal file
63
command/server.go
Normal 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
265
command/server/config.go
Normal 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
|
||||||
|
}
|
||||||
118
command/server/config_test.go
Normal file
118
command/server/config_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
command/server/test-fixtures/config-dir/bar.json
Normal file
7
command/server/test-fixtures/config-dir/bar.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"listener": {
|
||||||
|
"tcp": {
|
||||||
|
"address": "127.0.0.1:443"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
command/server/test-fixtures/config-dir/foo.hcl
Normal file
3
command/server/test-fixtures/config-dir/foo.hcl
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
backend "consul" {
|
||||||
|
foo = "bar"
|
||||||
|
}
|
||||||
7
command/server/test-fixtures/config.hcl
Normal file
7
command/server/test-fixtures/config.hcl
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
listener "tcp" {
|
||||||
|
address = "127.0.0.1:443"
|
||||||
|
}
|
||||||
|
|
||||||
|
backend "consul" {
|
||||||
|
foo = "bar"
|
||||||
|
}
|
||||||
13
command/server/test-fixtures/config.hcl.json
Normal file
13
command/server/test-fixtures/config.hcl.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"listener": {
|
||||||
|
"tcp": {
|
||||||
|
"address": "127.0.0.1:443"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"backend": {
|
||||||
|
"consul": {
|
||||||
|
"foo": "bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
command/server/test-fixtures/config2.hcl.json
Normal file
13
command/server/test-fixtures/config2.hcl.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"listener": {
|
||||||
|
"tcp": [{
|
||||||
|
"address": "127.0.0.1:443"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
|
||||||
|
"backend": {
|
||||||
|
"consul": {
|
||||||
|
"foo": "bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,12 @@ func init() {
|
|||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"server": func() (cli.Command, error) {
|
||||||
|
return &command.ServerCommand{
|
||||||
|
Meta: meta,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
|
||||||
"version": func() (cli.Command, error) {
|
"version": func() (cli.Command, error) {
|
||||||
ver := Version
|
ver := Version
|
||||||
rel := VersionPrerelease
|
rel := VersionPrerelease
|
||||||
|
|||||||
16
helper/flag-slice/flag.go
Normal file
16
helper/flag-slice/flag.go
Normal 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
|
||||||
|
}
|
||||||
33
helper/flag-slice/flag_test.go
Normal file
33
helper/flag-slice/flag_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user