diff --git a/command/server.go b/command/server.go new file mode 100644 index 0000000000..5c952533e2 --- /dev/null +++ b/command/server.go @@ -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 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) +} diff --git a/command/server/config.go b/command/server/config.go new file mode 100644 index 0000000000..614aad0f2b --- /dev/null +++ b/command/server/config.go @@ -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 +} diff --git a/command/server/config_test.go b/command/server/config_test.go new file mode 100644 index 0000000000..4a60ac8b31 --- /dev/null +++ b/command/server/config_test.go @@ -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) + } +} diff --git a/command/server/test-fixtures/config-dir/bar.json b/command/server/test-fixtures/config-dir/bar.json new file mode 100644 index 0000000000..3d76653564 --- /dev/null +++ b/command/server/test-fixtures/config-dir/bar.json @@ -0,0 +1,7 @@ +{ + "listener": { + "tcp": { + "address": "127.0.0.1:443" + } + } +} diff --git a/command/server/test-fixtures/config-dir/foo.hcl b/command/server/test-fixtures/config-dir/foo.hcl new file mode 100644 index 0000000000..5ab1d72b92 --- /dev/null +++ b/command/server/test-fixtures/config-dir/foo.hcl @@ -0,0 +1,3 @@ +backend "consul" { + foo = "bar" +} diff --git a/command/server/test-fixtures/config.hcl b/command/server/test-fixtures/config.hcl new file mode 100644 index 0000000000..31dcb3e143 --- /dev/null +++ b/command/server/test-fixtures/config.hcl @@ -0,0 +1,7 @@ +listener "tcp" { + address = "127.0.0.1:443" +} + +backend "consul" { + foo = "bar" +} diff --git a/command/server/test-fixtures/config.hcl.json b/command/server/test-fixtures/config.hcl.json new file mode 100644 index 0000000000..221e444261 --- /dev/null +++ b/command/server/test-fixtures/config.hcl.json @@ -0,0 +1,13 @@ +{ + "listener": { + "tcp": { + "address": "127.0.0.1:443" + } + }, + + "backend": { + "consul": { + "foo": "bar" + } + } +} diff --git a/command/server/test-fixtures/config2.hcl.json b/command/server/test-fixtures/config2.hcl.json new file mode 100644 index 0000000000..1478d4f557 --- /dev/null +++ b/command/server/test-fixtures/config2.hcl.json @@ -0,0 +1,13 @@ +{ + "listener": { + "tcp": [{ + "address": "127.0.0.1:443" + }] + }, + + "backend": { + "consul": { + "foo": "bar" + } + } +} diff --git a/commands.go b/commands.go index ff24e0ab65..341d1c3520 100644 --- a/commands.go +++ b/commands.go @@ -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 diff --git a/helper/flag-slice/flag.go b/helper/flag-slice/flag.go new file mode 100644 index 0000000000..da75149dc4 --- /dev/null +++ b/helper/flag-slice/flag.go @@ -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 +} diff --git a/helper/flag-slice/flag_test.go b/helper/flag-slice/flag_test.go new file mode 100644 index 0000000000..f72e1d9605 --- /dev/null +++ b/helper/flag-slice/flag_test.go @@ -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) + } +}