diff --git a/command/commands_oss.go b/command/commands_oss.go index 8e95282aab2f..46f328b7fd2c 100644 --- a/command/commands_oss.go +++ b/command/commands_oss.go @@ -44,6 +44,9 @@ import ( operraftremove "github.com/hashicorp/consul/command/operator/raft/removepeer" "github.com/hashicorp/consul/command/reload" "github.com/hashicorp/consul/command/rtt" + "github.com/hashicorp/consul/command/services" + svcsderegister "github.com/hashicorp/consul/command/services/deregister" + svcsregister "github.com/hashicorp/consul/command/services/register" "github.com/hashicorp/consul/command/snapshot" snapinspect "github.com/hashicorp/consul/command/snapshot/inspect" snaprestore "github.com/hashicorp/consul/command/snapshot/restore" @@ -107,6 +110,9 @@ func init() { Register("operator raft remove-peer", func(ui cli.Ui) (cli.Command, error) { return operraftremove.New(ui), nil }) Register("reload", func(ui cli.Ui) (cli.Command, error) { return reload.New(ui), nil }) Register("rtt", func(ui cli.Ui) (cli.Command, error) { return rtt.New(ui), nil }) + Register("services", func(cli.Ui) (cli.Command, error) { return services.New(), nil }) + Register("services register", func(ui cli.Ui) (cli.Command, error) { return svcsregister.New(ui), nil }) + Register("services deregister", func(ui cli.Ui) (cli.Command, error) { return svcsderegister.New(ui), nil }) Register("snapshot", func(cli.Ui) (cli.Command, error) { return snapshot.New(), nil }) Register("snapshot inspect", func(ui cli.Ui) (cli.Command, error) { return snapinspect.New(ui), nil }) Register("snapshot restore", func(ui cli.Ui) (cli.Command, error) { return snaprestore.New(ui), nil }) diff --git a/command/services/config.go b/command/services/config.go new file mode 100644 index 000000000000..2241c90ecad9 --- /dev/null +++ b/command/services/config.go @@ -0,0 +1,100 @@ +package services + +import ( + "reflect" + "time" + + "github.com/hashicorp/consul/agent/config" + "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/api" + "github.com/mitchellh/mapstructure" +) + +// ServicesFromFiles returns the list of agent service registration structs +// from a set of file arguments. +func ServicesFromFiles(files []string) ([]*api.AgentServiceRegistration, error) { + // We set devMode to true so we can get the basic valid default + // configuration. devMode doesn't set any services by default so this + // is okay since we only look at services. + devMode := true + b, err := config.NewBuilder(config.Flags{ + ConfigFiles: files, + DevMode: &devMode, + }) + if err != nil { + return nil, err + } + + cfg, err := b.BuildAndValidate() + if err != nil { + return nil, err + } + + // The services are now in "structs.ServiceDefinition" form and we need + // them in "api.AgentServiceRegistration" form so do the conversion. + result := make([]*api.AgentServiceRegistration, 0, len(cfg.Services)) + for _, svc := range cfg.Services { + apiSvc, err := serviceToAgentService(svc) + if err != nil { + return nil, err + } + + result = append(result, apiSvc) + } + + return result, nil +} + +// serviceToAgentService converts a ServiceDefinition struct to an +// AgentServiceRegistration API struct. +func serviceToAgentService(svc *structs.ServiceDefinition) (*api.AgentServiceRegistration, error) { + // mapstructure can do this for us, but we encapsulate it in this + // helper function in case we need to change the logic in the future. + var result api.AgentServiceRegistration + d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: &result, + DecodeHook: timeDurationToStringHookFunc(), + WeaklyTypedInput: true, + }) + if err != nil { + return nil, err + } + if err := d.Decode(svc); err != nil { + return nil, err + } + + // The structs version has non-pointer checks and the destination + // has pointers, so we need to set the destination to nil if there + // is no check ID set. + if result.Check != nil && result.Check.Name == "" { + result.Check = nil + } + if len(result.Checks) == 1 && result.Checks[0].Name == "" { + result.Checks = nil + } + + return &result, nil +} + +// timeDurationToStringHookFunc returns a DecodeHookFunc that converts +// time.Duration to string. +func timeDurationToStringHookFunc() mapstructure.DecodeHookFunc { + return func( + f reflect.Type, + t reflect.Type, + data interface{}) (interface{}, error) { + dur, ok := data.(time.Duration) + if !ok { + return data, nil + } + if t.Kind() != reflect.String { + return data, nil + } + if dur == 0 { + return "", nil + } + + // Convert it by parsing + return data.(time.Duration).String(), nil + } +} diff --git a/command/services/config_test.go b/command/services/config_test.go new file mode 100644 index 000000000000..238c286e412d --- /dev/null +++ b/command/services/config_test.go @@ -0,0 +1,105 @@ +package services + +import ( + "testing" + + "github.com/hashicorp/consul/agent/config" + "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/api" + "github.com/stretchr/testify/require" +) + +// This test ensures that dev mode doesn't register services by default. +// We depend on this behavior for ServiesFromFiles so we want to fail +// tests if that ever changes. +func TestDevModeHasNoServices(t *testing.T) { + t.Parallel() + require := require.New(t) + + devMode := true + b, err := config.NewBuilder(config.Flags{ + DevMode: &devMode, + }) + require.NoError(err) + + cfg, err := b.BuildAndValidate() + require.NoError(err) + require.Empty(cfg.Services) +} + +func TestStructsToAgentService(t *testing.T) { + t.Parallel() + + cases := []struct { + Name string + Input *structs.ServiceDefinition + Output *api.AgentServiceRegistration + }{ + { + "Basic service with port", + &structs.ServiceDefinition{ + Name: "web", + Tags: []string{"leader"}, + Port: 1234, + }, + &api.AgentServiceRegistration{ + Name: "web", + Tags: []string{"leader"}, + Port: 1234, + }, + }, + { + "Service with a check", + &structs.ServiceDefinition{ + Name: "web", + Check: structs.CheckType{ + Name: "ping", + }, + }, + &api.AgentServiceRegistration{ + Name: "web", + Check: &api.AgentServiceCheck{ + Name: "ping", + }, + }, + }, + { + "Service with checks", + &structs.ServiceDefinition{ + Name: "web", + Checks: structs.CheckTypes{ + &structs.CheckType{ + Name: "ping", + }, + &structs.CheckType{ + Name: "pong", + }, + }, + }, + &api.AgentServiceRegistration{ + Name: "web", + Checks: api.AgentServiceChecks{ + &api.AgentServiceCheck{ + Name: "ping", + }, + &api.AgentServiceCheck{ + Name: "pong", + }, + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + require := require.New(t) + actual, err := serviceToAgentService(tc.Input) + require.NoError(err) + require.Equal(tc.Output, actual) + }) + } +} + +func intPtr(v int) *int { return &v } +func strPtr(v string) *string { return &v } diff --git a/command/services/deregister/deregister.go b/command/services/deregister/deregister.go new file mode 100644 index 000000000000..85a63ab419d5 --- /dev/null +++ b/command/services/deregister/deregister.go @@ -0,0 +1,114 @@ +package deregister + +import ( + "flag" + "fmt" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/command/flags" + "github.com/hashicorp/consul/command/services" + "github.com/mitchellh/cli" +) + +func New(ui cli.Ui) *cmd { + c := &cmd{UI: ui} + c.init() + return c +} + +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + flagId string +} + +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + c.flags.StringVar(&c.flagId, "id", "", + "ID to delete. This must not be set if arguments are given.") + + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.ServerFlags()) + c.help = flags.Usage(help, c.flags) +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + return 1 + } + + // Check for arg validation + args = c.flags.Args() + if len(args) == 0 && c.flagId == "" { + c.UI.Error("Service deregistration requires at least one argument or -id.") + return 1 + } else if len(args) > 0 && c.flagId != "" { + c.UI.Error("Service deregistration requires arguments or -id, not both.") + return 1 + } + + svcs := []*api.AgentServiceRegistration{&api.AgentServiceRegistration{ + ID: c.flagId}} + if len(args) > 0 { + var err error + svcs, err = services.ServicesFromFiles(args) + if err != nil { + c.UI.Error(fmt.Sprintf("Error: %s", err)) + return 1 + } + } + + // Create and test the HTTP client + client, err := c.http.APIClient() + if err != nil { + c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) + return 1 + } + + // Create all the services + for _, svc := range svcs { + id := svc.ID + if id == "" { + id = svc.Name + } + if id == "" { + continue + } + + if err := client.Agent().ServiceDeregister(id); err != nil { + c.UI.Error(fmt.Sprintf("Error registering service %q: %s", + svc.Name, err)) + return 1 + } + } + + return 0 +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return c.help +} + +const synopsis = "Deregister services with the local agent" +const help = ` +Usage: consul services deregister [options] [FILE...] + + Deregister one or more services that were previously registered with + the local agent. + + $ consul services deregister web.json db.json + + The -id flag may be used to deregister a single service by ID: + + $ consul services deregister -id=web + + Services are deregistered from the local agent catalog. This command must + be run against the same agent where the service was registered. +` diff --git a/command/services/deregister/deregister_test.go b/command/services/deregister/deregister_test.go new file mode 100644 index 000000000000..d4dab538400d --- /dev/null +++ b/command/services/deregister/deregister_test.go @@ -0,0 +1,186 @@ +package deregister + +import ( + "os" + "strings" + "testing" + + "github.com/hashicorp/consul/agent" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/testutil" + "github.com/mitchellh/cli" + "github.com/stretchr/testify/require" +) + +func TestCommand_noTabs(t *testing.T) { + t.Parallel() + if strings.ContainsRune(New(nil).Help(), '\t') { + t.Fatal("help has tabs") + } +} + +func TestCommand_Validation(t *testing.T) { + t.Parallel() + + ui := cli.NewMockUi() + c := New(ui) + + cases := map[string]struct { + args []string + output string + }{ + "no args or id": { + []string{}, + "at least one", + }, + "args and -id": { + []string{"-id", "web", "foo.json"}, + "not both", + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + require := require.New(t) + + c.init() + + // Ensure our buffer is always clear + if ui.ErrorWriter != nil { + ui.ErrorWriter.Reset() + } + if ui.OutputWriter != nil { + ui.OutputWriter.Reset() + } + + require.Equal(1, c.Run(tc.args)) + output := ui.ErrorWriter.String() + require.Contains(output, tc.output) + }) + } +} + +func TestCommand_File_id(t *testing.T) { + t.Parallel() + + require := require.New(t) + a := agent.NewTestAgent(t.Name(), ``) + defer a.Shutdown() + client := a.Client() + + // Register a service + require.NoError(client.Agent().ServiceRegister(&api.AgentServiceRegistration{ + Name: "web"})) + require.NoError(client.Agent().ServiceRegister(&api.AgentServiceRegistration{ + Name: "db"})) + + ui := cli.NewMockUi() + c := New(ui) + + contents := `{ "Service": { "ID": "web", "Name": "foo" } }` + f := testFile(t, "json") + defer os.Remove(f.Name()) + if _, err := f.WriteString(contents); err != nil { + t.Fatalf("err: %#v", err) + } + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + f.Name(), + } + + require.Equal(0, c.Run(args), ui.ErrorWriter.String()) + + svcs, err := client.Agent().Services() + require.NoError(err) + require.Len(svcs, 1) + require.NotNil(svcs["db"]) +} + +func TestCommand_File_nameOnly(t *testing.T) { + t.Parallel() + + require := require.New(t) + a := agent.NewTestAgent(t.Name(), ``) + defer a.Shutdown() + client := a.Client() + + // Register a service + require.NoError(client.Agent().ServiceRegister(&api.AgentServiceRegistration{ + Name: "web"})) + require.NoError(client.Agent().ServiceRegister(&api.AgentServiceRegistration{ + Name: "db"})) + + ui := cli.NewMockUi() + c := New(ui) + + contents := `{ "Service": { "Name": "web" } }` + f := testFile(t, "json") + defer os.Remove(f.Name()) + if _, err := f.WriteString(contents); err != nil { + t.Fatalf("err: %#v", err) + } + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + f.Name(), + } + + require.Equal(0, c.Run(args), ui.ErrorWriter.String()) + + svcs, err := client.Agent().Services() + require.NoError(err) + require.Len(svcs, 1) + require.NotNil(svcs["db"]) +} + +func TestCommand_Flag(t *testing.T) { + t.Parallel() + + require := require.New(t) + a := agent.NewTestAgent(t.Name(), ``) + defer a.Shutdown() + client := a.Client() + + // Register a service + require.NoError(client.Agent().ServiceRegister(&api.AgentServiceRegistration{ + Name: "web"})) + require.NoError(client.Agent().ServiceRegister(&api.AgentServiceRegistration{ + Name: "db"})) + + ui := cli.NewMockUi() + c := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-id", "web", + } + + require.Equal(0, c.Run(args), ui.ErrorWriter.String()) + + svcs, err := client.Agent().Services() + require.NoError(err) + require.Len(svcs, 1) + require.NotNil(svcs["db"]) +} + +func testFile(t *testing.T, suffix string) *os.File { + f := testutil.TempFile(t, "register-test-file") + if err := f.Close(); err != nil { + t.Fatalf("err: %s", err) + } + + newName := f.Name() + "." + suffix + if err := os.Rename(f.Name(), newName); err != nil { + os.Remove(f.Name()) + t.Fatalf("err: %s", err) + } + + f, err := os.Create(newName) + if err != nil { + os.Remove(newName) + t.Fatalf("err: %s", err) + } + + return f +} diff --git a/command/services/register/register.go b/command/services/register/register.go new file mode 100644 index 000000000000..ecd453c71f25 --- /dev/null +++ b/command/services/register/register.go @@ -0,0 +1,131 @@ +package register + +import ( + "flag" + "fmt" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/command/flags" + "github.com/hashicorp/consul/command/services" + "github.com/mitchellh/cli" +) + +func New(ui cli.Ui) *cmd { + c := &cmd{UI: ui} + c.init() + return c +} + +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + + // flags + flagId string + flagName string + flagAddress string + flagPort int + flagTags []string + flagMeta map[string]string +} + +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + c.flags.StringVar(&c.flagId, "id", "", + "ID of the service to register for arg-based registration. If this "+ + "isn't set, it will default to the -name value.") + c.flags.StringVar(&c.flagName, "name", "", + "Name of the service to register for arg-based registration.") + c.flags.StringVar(&c.flagAddress, "address", "", + "Address of the service to register for arg-based registration.") + c.flags.IntVar(&c.flagPort, "port", 0, + "Port of the service to register for arg-based registration.") + c.flags.Var((*flags.FlagMapValue)(&c.flagMeta), "meta", + "Metadata to set on the intention, formatted as key=value. This flag "+ + "may be specified multiple times to set multiple meta fields.") + c.flags.Var((*flags.AppendSliceValue)(&c.flagTags), "tag", + "Tag to add to the service. This flag can be specified multiple "+ + "times to set multiple tags.") + + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.ServerFlags()) + c.help = flags.Usage(help, c.flags) +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + return 1 + } + + svcs := []*api.AgentServiceRegistration{&api.AgentServiceRegistration{ + ID: c.flagId, + Name: c.flagName, + Address: c.flagAddress, + Port: c.flagPort, + Tags: c.flagTags, + Meta: c.flagMeta, + }} + + // Check for arg validation + args = c.flags.Args() + if len(args) == 0 && c.flagName == "" { + c.UI.Error("Service registration requires at least one argument or flags.") + return 1 + } else if len(args) > 0 && c.flagName != "" { + c.UI.Error("Service registration requires arguments or -id, not both.") + return 1 + } + + if len(args) > 0 { + var err error + svcs, err = services.ServicesFromFiles(args) + if err != nil { + c.UI.Error(fmt.Sprintf("Error: %s", err)) + return 1 + } + } + + // Create and test the HTTP client + client, err := c.http.APIClient() + if err != nil { + c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) + return 1 + } + + // Create all the services + for _, svc := range svcs { + if err := client.Agent().ServiceRegister(svc); err != nil { + c.UI.Error(fmt.Sprintf("Error registering service %q: %s", + svc.Name, err)) + return 1 + } + } + + return 0 +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return c.help +} + +const synopsis = "Register services with the local agent" +const help = ` +Usage: consul services register [options] [FILE...] + + Register one or more services using the local agent API. Services can + be registered from standard Consul configuration files (HCL or JSON) or + using flags. The service is registered and the command returns. The caller + must remember to call "consul services deregister" or a similar API to + deregister the service when complete. + + $ consul services register web.json + + Additional flags and more advanced use cases are detailed below. +` diff --git a/command/services/register/register_test.go b/command/services/register/register_test.go new file mode 100644 index 000000000000..fac96ab580e5 --- /dev/null +++ b/command/services/register/register_test.go @@ -0,0 +1,140 @@ +package register + +import ( + "os" + "strings" + "testing" + + "github.com/hashicorp/consul/agent" + "github.com/hashicorp/consul/testutil" + "github.com/mitchellh/cli" + "github.com/stretchr/testify/require" +) + +func TestCommand_noTabs(t *testing.T) { + t.Parallel() + if strings.ContainsRune(New(nil).Help(), '\t') { + t.Fatal("help has tabs") + } +} + +func TestCommand_Validation(t *testing.T) { + t.Parallel() + + ui := cli.NewMockUi() + c := New(ui) + + cases := map[string]struct { + args []string + output string + }{ + "no args or id": { + []string{}, + "at least one", + }, + "args and -name": { + []string{"-name", "web", "foo.json"}, + "not both", + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + require := require.New(t) + + c.init() + + // Ensure our buffer is always clear + if ui.ErrorWriter != nil { + ui.ErrorWriter.Reset() + } + if ui.OutputWriter != nil { + ui.OutputWriter.Reset() + } + + require.Equal(1, c.Run(tc.args)) + output := ui.ErrorWriter.String() + require.Contains(output, tc.output) + }) + } +} + +func TestCommand_File(t *testing.T) { + t.Parallel() + + require := require.New(t) + a := agent.NewTestAgent(t.Name(), ``) + defer a.Shutdown() + client := a.Client() + + ui := cli.NewMockUi() + c := New(ui) + + contents := `{ "Service": { "Name": "web" } }` + f := testFile(t, "json") + defer os.Remove(f.Name()) + if _, err := f.WriteString(contents); err != nil { + t.Fatalf("err: %#v", err) + } + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + f.Name(), + } + + require.Equal(0, c.Run(args), ui.ErrorWriter.String()) + + svcs, err := client.Agent().Services() + require.NoError(err) + require.Len(svcs, 1) + + svc := svcs["web"] + require.NotNil(svc) +} + +func TestCommand_Flags(t *testing.T) { + t.Parallel() + + require := require.New(t) + a := agent.NewTestAgent(t.Name(), ``) + defer a.Shutdown() + client := a.Client() + + ui := cli.NewMockUi() + c := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-name", "web", + } + + require.Equal(0, c.Run(args), ui.ErrorWriter.String()) + + svcs, err := client.Agent().Services() + require.NoError(err) + require.Len(svcs, 1) + + svc := svcs["web"] + require.NotNil(svc) +} + +func testFile(t *testing.T, suffix string) *os.File { + f := testutil.TempFile(t, "register-test-file") + if err := f.Close(); err != nil { + t.Fatalf("err: %s", err) + } + + newName := f.Name() + "." + suffix + if err := os.Rename(f.Name(), newName); err != nil { + os.Remove(f.Name()) + t.Fatalf("err: %s", err) + } + + f, err := os.Create(newName) + if err != nil { + os.Remove(newName) + t.Fatalf("err: %s", err) + } + + return f +} diff --git a/command/services/services.go b/command/services/services.go new file mode 100644 index 000000000000..0e050d77d3e4 --- /dev/null +++ b/command/services/services.go @@ -0,0 +1,35 @@ +package services + +import ( + "github.com/hashicorp/consul/command/flags" + "github.com/mitchellh/cli" +) + +func New() *cmd { + return &cmd{} +} + +type cmd struct{} + +func (c *cmd) Run(args []string) int { + return cli.RunResultHelp +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return flags.Usage(help, nil) +} + +const synopsis = "Interact with services" +const help = ` +Usage: consul services [options] [args] + + This command has subcommands for interacting with services. The subcommands + default to working with services registered with the local agent. Please see + the "consul catalog" command for interacting with the entire catalog. + + For more examples, ask for subcommand help or view the documentation. +` diff --git a/command/services/services_test.go b/command/services/services_test.go new file mode 100644 index 000000000000..c7521718a33a --- /dev/null +++ b/command/services/services_test.go @@ -0,0 +1,13 @@ +package services + +import ( + "strings" + "testing" +) + +func TestCommand_noTabs(t *testing.T) { + t.Parallel() + if strings.ContainsRune(New().Help(), '\t') { + t.Fatal("help has tabs") + } +} diff --git a/vendor/github.com/mitchellh/mapstructure/CHANGELOG.md b/vendor/github.com/mitchellh/mapstructure/CHANGELOG.md new file mode 100644 index 000000000000..fb0f46c84b0b --- /dev/null +++ b/vendor/github.com/mitchellh/mapstructure/CHANGELOG.md @@ -0,0 +1,12 @@ +## 1.1.0 (September 30, 2018) + +* Added `StringToIPHookFunc` to convert `string` to `net.IP` and `net.IPNet` [GH-133] +* Support struct to struct decoding [GH-137] +* If source map value is nil, then destination map value is nil (instead of empty) +* If source slice value is nil, then destination slice value is nil (instead of empty) +* If source pointer is nil, then destination pointer is set to nil (instead of + allocated zero value of type) + +## 1.0.0 + +* Initial tagged stable release. diff --git a/vendor/github.com/mitchellh/mapstructure/README.md b/vendor/github.com/mitchellh/mapstructure/README.md index 659d6885fc7e..0018dc7d9f94 100644 --- a/vendor/github.com/mitchellh/mapstructure/README.md +++ b/vendor/github.com/mitchellh/mapstructure/README.md @@ -1,4 +1,4 @@ -# mapstructure +# mapstructure [![Godoc](https://godoc.org/github.com/mitchellh/mapstructure?status.svg)](https://godoc.org/github.com/mitchellh/mapstructure) mapstructure is a Go library for decoding generic map values to structures and vice versa, while providing helpful error handling. diff --git a/vendor/github.com/mitchellh/mapstructure/decode_hooks.go b/vendor/github.com/mitchellh/mapstructure/decode_hooks.go index afcfd5eed69c..1f0abc65ab7e 100644 --- a/vendor/github.com/mitchellh/mapstructure/decode_hooks.go +++ b/vendor/github.com/mitchellh/mapstructure/decode_hooks.go @@ -2,6 +2,8 @@ package mapstructure import ( "errors" + "fmt" + "net" "reflect" "strconv" "strings" @@ -115,6 +117,69 @@ func StringToTimeDurationHookFunc() DecodeHookFunc { } } +// StringToIPHookFunc returns a DecodeHookFunc that converts +// strings to net.IP +func StringToIPHookFunc() DecodeHookFunc { + return func( + f reflect.Type, + t reflect.Type, + data interface{}) (interface{}, error) { + if f.Kind() != reflect.String { + return data, nil + } + if t != reflect.TypeOf(net.IP{}) { + return data, nil + } + + // Convert it by parsing + ip := net.ParseIP(data.(string)) + if ip == nil { + return net.IP{}, fmt.Errorf("failed parsing ip %v", data) + } + + return ip, nil + } +} + +// StringToIPNetHookFunc returns a DecodeHookFunc that converts +// strings to net.IPNet +func StringToIPNetHookFunc() DecodeHookFunc { + return func( + f reflect.Type, + t reflect.Type, + data interface{}) (interface{}, error) { + if f.Kind() != reflect.String { + return data, nil + } + if t != reflect.TypeOf(net.IPNet{}) { + return data, nil + } + + // Convert it by parsing + _, net, err := net.ParseCIDR(data.(string)) + return net, err + } +} + +// StringToTimeHookFunc returns a DecodeHookFunc that converts +// strings to time.Time. +func StringToTimeHookFunc(layout string) DecodeHookFunc { + return func( + f reflect.Type, + t reflect.Type, + data interface{}) (interface{}, error) { + if f.Kind() != reflect.String { + return data, nil + } + if t != reflect.TypeOf(time.Time{}) { + return data, nil + } + + // Convert it by parsing + return time.Parse(layout, data.(string)) + } +} + // WeaklyTypedHook is a DecodeHookFunc which adds support for weak typing to // the decoder. // diff --git a/vendor/github.com/mitchellh/mapstructure/go.mod b/vendor/github.com/mitchellh/mapstructure/go.mod new file mode 100644 index 000000000000..d2a712562082 --- /dev/null +++ b/vendor/github.com/mitchellh/mapstructure/go.mod @@ -0,0 +1 @@ +module github.com/mitchellh/mapstructure diff --git a/vendor/github.com/mitchellh/mapstructure/mapstructure.go b/vendor/github.com/mitchellh/mapstructure/mapstructure.go index 30a9957c65db..95eb686565fe 100644 --- a/vendor/github.com/mitchellh/mapstructure/mapstructure.go +++ b/vendor/github.com/mitchellh/mapstructure/mapstructure.go @@ -114,12 +114,12 @@ type Metadata struct { Unused []string } -// Decode takes a map and uses reflection to convert it into the -// given Go native structure. val must be a pointer to a struct. -func Decode(m interface{}, rawVal interface{}) error { +// Decode takes an input structure and uses reflection to translate it to +// the output structure. output must be a pointer to a map or struct. +func Decode(input interface{}, output interface{}) error { config := &DecoderConfig{ Metadata: nil, - Result: rawVal, + Result: output, } decoder, err := NewDecoder(config) @@ -127,7 +127,7 @@ func Decode(m interface{}, rawVal interface{}) error { return err } - return decoder.Decode(m) + return decoder.Decode(input) } // WeakDecode is the same as Decode but is shorthand to enable @@ -147,6 +147,40 @@ func WeakDecode(input, output interface{}) error { return decoder.Decode(input) } +// DecodeMetadata is the same as Decode, but is shorthand to +// enable metadata collection. See DecoderConfig for more info. +func DecodeMetadata(input interface{}, output interface{}, metadata *Metadata) error { + config := &DecoderConfig{ + Metadata: metadata, + Result: output, + } + + decoder, err := NewDecoder(config) + if err != nil { + return err + } + + return decoder.Decode(input) +} + +// WeakDecodeMetadata is the same as Decode, but is shorthand to +// enable both WeaklyTypedInput and metadata collection. See +// DecoderConfig for more info. +func WeakDecodeMetadata(input interface{}, output interface{}, metadata *Metadata) error { + config := &DecoderConfig{ + Metadata: metadata, + Result: output, + WeaklyTypedInput: true, + } + + decoder, err := NewDecoder(config) + if err != nil { + return err + } + + return decoder.Decode(input) +} + // NewDecoder returns a new decoder for the given configuration. Once // a decoder has been returned, the same configuration must not be used // again. @@ -184,68 +218,91 @@ func NewDecoder(config *DecoderConfig) (*Decoder, error) { // Decode decodes the given raw interface to the target pointer specified // by the configuration. -func (d *Decoder) Decode(raw interface{}) error { - return d.decode("", raw, reflect.ValueOf(d.config.Result).Elem()) +func (d *Decoder) Decode(input interface{}) error { + return d.decode("", input, reflect.ValueOf(d.config.Result).Elem()) } // Decodes an unknown data type into a specific reflection value. -func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error { - if data == nil { - // If the data is nil, then we don't set anything. +func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) error { + var inputVal reflect.Value + if input != nil { + inputVal = reflect.ValueOf(input) + + // We need to check here if input is a typed nil. Typed nils won't + // match the "input == nil" below so we check that here. + if inputVal.Kind() == reflect.Ptr && inputVal.IsNil() { + input = nil + } + } + + if input == nil { + // If the data is nil, then we don't set anything, unless ZeroFields is set + // to true. + if d.config.ZeroFields { + outVal.Set(reflect.Zero(outVal.Type())) + + if d.config.Metadata != nil && name != "" { + d.config.Metadata.Keys = append(d.config.Metadata.Keys, name) + } + } return nil } - dataVal := reflect.ValueOf(data) - if !dataVal.IsValid() { - // If the data value is invalid, then we just set the value + if !inputVal.IsValid() { + // If the input value is invalid, then we just set the value // to be the zero value. - val.Set(reflect.Zero(val.Type())) + outVal.Set(reflect.Zero(outVal.Type())) + if d.config.Metadata != nil && name != "" { + d.config.Metadata.Keys = append(d.config.Metadata.Keys, name) + } return nil } if d.config.DecodeHook != nil { - // We have a DecodeHook, so let's pre-process the data. + // We have a DecodeHook, so let's pre-process the input. var err error - data, err = DecodeHookExec( + input, err = DecodeHookExec( d.config.DecodeHook, - dataVal.Type(), val.Type(), data) + inputVal.Type(), outVal.Type(), input) if err != nil { return fmt.Errorf("error decoding '%s': %s", name, err) } } var err error - dataKind := getKind(val) - switch dataKind { + outputKind := getKind(outVal) + switch outputKind { case reflect.Bool: - err = d.decodeBool(name, data, val) + err = d.decodeBool(name, input, outVal) case reflect.Interface: - err = d.decodeBasic(name, data, val) + err = d.decodeBasic(name, input, outVal) case reflect.String: - err = d.decodeString(name, data, val) + err = d.decodeString(name, input, outVal) case reflect.Int: - err = d.decodeInt(name, data, val) + err = d.decodeInt(name, input, outVal) case reflect.Uint: - err = d.decodeUint(name, data, val) + err = d.decodeUint(name, input, outVal) case reflect.Float32: - err = d.decodeFloat(name, data, val) + err = d.decodeFloat(name, input, outVal) case reflect.Struct: - err = d.decodeStruct(name, data, val) + err = d.decodeStruct(name, input, outVal) case reflect.Map: - err = d.decodeMap(name, data, val) + err = d.decodeMap(name, input, outVal) case reflect.Ptr: - err = d.decodePtr(name, data, val) + err = d.decodePtr(name, input, outVal) case reflect.Slice: - err = d.decodeSlice(name, data, val) + err = d.decodeSlice(name, input, outVal) + case reflect.Array: + err = d.decodeArray(name, input, outVal) case reflect.Func: - err = d.decodeFunc(name, data, val) + err = d.decodeFunc(name, input, outVal) default: // If we reached this point then we weren't able to decode it - return fmt.Errorf("%s: unsupported type: %s", name, dataKind) + return fmt.Errorf("%s: unsupported type: %s", name, outputKind) } // If we reached here, then we successfully decoded SOMETHING, so - // mark the key as used if we're tracking metadata. + // mark the key as used if we're tracking metainput. if d.config.Metadata != nil && name != "" { d.config.Metadata.Keys = append(d.config.Metadata.Keys, name) } @@ -256,7 +313,10 @@ func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error // This decodes a basic type (bool, int, string, etc.) and sets the // value to "data" of that type. func (d *Decoder) decodeBasic(name string, data interface{}, val reflect.Value) error { - dataVal := reflect.ValueOf(data) + if val.IsValid() && val.Elem().IsValid() { + return d.decode(name, data, val.Elem()) + } + dataVal := reflect.Indirect(reflect.ValueOf(data)) if !dataVal.IsValid() { dataVal = reflect.Zero(val.Type()) } @@ -273,7 +333,7 @@ func (d *Decoder) decodeBasic(name string, data interface{}, val reflect.Value) } func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) error { - dataVal := reflect.ValueOf(data) + dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) converted := true @@ -292,12 +352,22 @@ func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) val.SetString(strconv.FormatUint(dataVal.Uint(), 10)) case dataKind == reflect.Float32 && d.config.WeaklyTypedInput: val.SetString(strconv.FormatFloat(dataVal.Float(), 'f', -1, 64)) - case dataKind == reflect.Slice && d.config.WeaklyTypedInput: + case dataKind == reflect.Slice && d.config.WeaklyTypedInput, + dataKind == reflect.Array && d.config.WeaklyTypedInput: dataType := dataVal.Type() elemKind := dataType.Elem().Kind() - switch { - case elemKind == reflect.Uint8: - val.SetString(string(dataVal.Interface().([]uint8))) + switch elemKind { + case reflect.Uint8: + var uints []uint8 + if dataKind == reflect.Array { + uints = make([]uint8, dataVal.Len(), dataVal.Len()) + for i := range uints { + uints[i] = dataVal.Index(i).Interface().(uint8) + } + } else { + uints = dataVal.Interface().([]uint8) + } + val.SetString(string(uints)) default: converted = false } @@ -315,7 +385,7 @@ func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) } func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) error { - dataVal := reflect.ValueOf(data) + dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) dataType := dataVal.Type() @@ -357,7 +427,7 @@ func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) er } func (d *Decoder) decodeUint(name string, data interface{}, val reflect.Value) error { - dataVal := reflect.ValueOf(data) + dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) switch { @@ -400,7 +470,7 @@ func (d *Decoder) decodeUint(name string, data interface{}, val reflect.Value) e } func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) error { - dataVal := reflect.ValueOf(data) + dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) switch { @@ -431,7 +501,7 @@ func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) e } func (d *Decoder) decodeFloat(name string, data interface{}, val reflect.Value) error { - dataVal := reflect.ValueOf(data) + dataVal := reflect.Indirect(reflect.ValueOf(data)) dataKind := getKind(dataVal) dataType := dataVal.Type() @@ -487,38 +557,68 @@ func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) er valMap = reflect.MakeMap(mapType) } - // Check input type + // Check input type and based on the input type jump to the proper func dataVal := reflect.Indirect(reflect.ValueOf(data)) - if dataVal.Kind() != reflect.Map { - // In weak mode, we accept a slice of maps as an input... - if d.config.WeaklyTypedInput { - switch dataVal.Kind() { - case reflect.Array, reflect.Slice: - // Special case for BC reasons (covered by tests) - if dataVal.Len() == 0 { - val.Set(valMap) - return nil - } + switch dataVal.Kind() { + case reflect.Map: + return d.decodeMapFromMap(name, dataVal, val, valMap) - for i := 0; i < dataVal.Len(); i++ { - err := d.decode( - fmt.Sprintf("%s[%d]", name, i), - dataVal.Index(i).Interface(), val) - if err != nil { - return err - } - } + case reflect.Struct: + return d.decodeMapFromStruct(name, dataVal, val, valMap) - return nil - } + case reflect.Array, reflect.Slice: + if d.config.WeaklyTypedInput { + return d.decodeMapFromSlice(name, dataVal, val, valMap) } + fallthrough + + default: return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind()) } +} + +func (d *Decoder) decodeMapFromSlice(name string, dataVal reflect.Value, val reflect.Value, valMap reflect.Value) error { + // Special case for BC reasons (covered by tests) + if dataVal.Len() == 0 { + val.Set(valMap) + return nil + } + + for i := 0; i < dataVal.Len(); i++ { + err := d.decode( + fmt.Sprintf("%s[%d]", name, i), + dataVal.Index(i).Interface(), val) + if err != nil { + return err + } + } + + return nil +} + +func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val reflect.Value, valMap reflect.Value) error { + valType := val.Type() + valKeyType := valType.Key() + valElemType := valType.Elem() // Accumulate errors errors := make([]string, 0) + // If the input data is empty, then we just match what the input data is. + if dataVal.Len() == 0 { + if dataVal.IsNil() { + if !val.IsNil() { + val.Set(dataVal) + } + } else { + // Set to empty allocated value + val.Set(valMap) + } + + return nil + } + for _, k := range dataVal.MapKeys() { fieldName := fmt.Sprintf("%s[%s]", name, k) @@ -551,22 +651,128 @@ func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) er return nil } +func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val reflect.Value, valMap reflect.Value) error { + typ := dataVal.Type() + for i := 0; i < typ.NumField(); i++ { + // Get the StructField first since this is a cheap operation. If the + // field is unexported, then ignore it. + f := typ.Field(i) + if f.PkgPath != "" { + continue + } + + // Next get the actual value of this field and verify it is assignable + // to the map value. + v := dataVal.Field(i) + if !v.Type().AssignableTo(valMap.Type().Elem()) { + return fmt.Errorf("cannot assign type '%s' to map value field of type '%s'", v.Type(), valMap.Type().Elem()) + } + + tagValue := f.Tag.Get(d.config.TagName) + tagParts := strings.Split(tagValue, ",") + + // Determine the name of the key in the map + keyName := f.Name + if tagParts[0] != "" { + if tagParts[0] == "-" { + continue + } + keyName = tagParts[0] + } + + // If "squash" is specified in the tag, we squash the field down. + squash := false + for _, tag := range tagParts[1:] { + if tag == "squash" { + squash = true + break + } + } + if squash && v.Kind() != reflect.Struct { + return fmt.Errorf("cannot squash non-struct type '%s'", v.Type()) + } + + switch v.Kind() { + // this is an embedded struct, so handle it differently + case reflect.Struct: + x := reflect.New(v.Type()) + x.Elem().Set(v) + + vType := valMap.Type() + vKeyType := vType.Key() + vElemType := vType.Elem() + mType := reflect.MapOf(vKeyType, vElemType) + vMap := reflect.MakeMap(mType) + + err := d.decode(keyName, x.Interface(), vMap) + if err != nil { + return err + } + + if squash { + for _, k := range vMap.MapKeys() { + valMap.SetMapIndex(k, vMap.MapIndex(k)) + } + } else { + valMap.SetMapIndex(reflect.ValueOf(keyName), vMap) + } + + default: + valMap.SetMapIndex(reflect.ValueOf(keyName), v) + } + } + + if val.CanAddr() { + val.Set(valMap) + } + + return nil +} + func (d *Decoder) decodePtr(name string, data interface{}, val reflect.Value) error { + // If the input data is nil, then we want to just set the output + // pointer to be nil as well. + isNil := data == nil + if !isNil { + switch v := reflect.Indirect(reflect.ValueOf(data)); v.Kind() { + case reflect.Chan, + reflect.Func, + reflect.Interface, + reflect.Map, + reflect.Ptr, + reflect.Slice: + isNil = v.IsNil() + } + } + if isNil { + if !val.IsNil() && val.CanSet() { + nilValue := reflect.New(val.Type()).Elem() + val.Set(nilValue) + } + + return nil + } + // Create an element of the concrete (non pointer) type and decode // into that. Then set the value of the pointer to this type. valType := val.Type() valElemType := valType.Elem() + if val.CanSet() { + realVal := val + if realVal.IsNil() || d.config.ZeroFields { + realVal = reflect.New(valElemType) + } - realVal := val - if realVal.IsNil() || d.config.ZeroFields { - realVal = reflect.New(valElemType) - } + if err := d.decode(name, data, reflect.Indirect(realVal)); err != nil { + return err + } - if err := d.decode(name, data, reflect.Indirect(realVal)); err != nil { - return err + val.Set(realVal) + } else { + if err := d.decode(name, data, reflect.Indirect(val)); err != nil { + return err + } } - - val.Set(realVal) return nil } @@ -592,22 +798,101 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) valSlice := val if valSlice.IsNil() || d.config.ZeroFields { + if d.config.WeaklyTypedInput { + switch { + // Slice and array we use the normal logic + case dataValKind == reflect.Slice, dataValKind == reflect.Array: + break + + // Empty maps turn into empty slices + case dataValKind == reflect.Map: + if dataVal.Len() == 0 { + val.Set(reflect.MakeSlice(sliceType, 0, 0)) + return nil + } + // Create slice of maps of other sizes + return d.decodeSlice(name, []interface{}{data}, val) + + case dataValKind == reflect.String && valElemType.Kind() == reflect.Uint8: + return d.decodeSlice(name, []byte(dataVal.String()), val) + + // All other types we try to convert to the slice type + // and "lift" it into it. i.e. a string becomes a string slice. + default: + // Just re-try this function with data as a slice. + return d.decodeSlice(name, []interface{}{data}, val) + } + } + + // Check input type + if dataValKind != reflect.Array && dataValKind != reflect.Slice { + return fmt.Errorf( + "'%s': source data must be an array or slice, got %s", name, dataValKind) + + } + + // If the input value is empty, then don't allocate since non-nil != nil + if dataVal.Len() == 0 { + return nil + } + + // Make a new slice to hold our result, same size as the original data. + valSlice = reflect.MakeSlice(sliceType, dataVal.Len(), dataVal.Len()) + } + + // Accumulate any errors + errors := make([]string, 0) + + for i := 0; i < dataVal.Len(); i++ { + currentData := dataVal.Index(i).Interface() + for valSlice.Len() <= i { + valSlice = reflect.Append(valSlice, reflect.Zero(valElemType)) + } + currentField := valSlice.Index(i) + + fieldName := fmt.Sprintf("%s[%d]", name, i) + if err := d.decode(fieldName, currentData, currentField); err != nil { + errors = appendErrors(errors, err) + } + } + + // Finally, set the value to the slice we built up + val.Set(valSlice) + + // If there were errors, we return those + if len(errors) > 0 { + return &Error{errors} + } + + return nil +} + +func (d *Decoder) decodeArray(name string, data interface{}, val reflect.Value) error { + dataVal := reflect.Indirect(reflect.ValueOf(data)) + dataValKind := dataVal.Kind() + valType := val.Type() + valElemType := valType.Elem() + arrayType := reflect.ArrayOf(valType.Len(), valElemType) + + valArray := val + + if valArray.Interface() == reflect.Zero(valArray.Type()).Interface() || d.config.ZeroFields { // Check input type if dataValKind != reflect.Array && dataValKind != reflect.Slice { if d.config.WeaklyTypedInput { switch { - // Empty maps turn into empty slices + // Empty maps turn into empty arrays case dataValKind == reflect.Map: if dataVal.Len() == 0 { - val.Set(reflect.MakeSlice(sliceType, 0, 0)) + val.Set(reflect.Zero(arrayType)) return nil } - // All other types we try to convert to the slice type - // and "lift" it into it. i.e. a string becomes a string slice. + // All other types we try to convert to the array type + // and "lift" it into it. i.e. a string becomes a string array. default: // Just re-try this function with data as a slice. - return d.decodeSlice(name, []interface{}{data}, val) + return d.decodeArray(name, []interface{}{data}, val) } } @@ -615,9 +900,14 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) "'%s': source data must be an array or slice, got %s", name, dataValKind) } + if dataVal.Len() > arrayType.Len() { + return fmt.Errorf( + "'%s': expected source data to have length less or equal to %d, got %d", name, arrayType.Len(), dataVal.Len()) - // Make a new slice to hold our result, same size as the original data. - valSlice = reflect.MakeSlice(sliceType, dataVal.Len(), dataVal.Len()) + } + + // Make a new array to hold our result, same size as the original data. + valArray = reflect.New(arrayType).Elem() } // Accumulate any errors @@ -625,10 +915,7 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) for i := 0; i < dataVal.Len(); i++ { currentData := dataVal.Index(i).Interface() - for valSlice.Len() <= i { - valSlice = reflect.Append(valSlice, reflect.Zero(valElemType)) - } - currentField := valSlice.Index(i) + currentField := valArray.Index(i) fieldName := fmt.Sprintf("%s[%d]", name, i) if err := d.decode(fieldName, currentData, currentField); err != nil { @@ -636,8 +923,8 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) } } - // Finally, set the value to the slice we built up - val.Set(valSlice) + // Finally, set the value to the array we built up + val.Set(valArray) // If there were errors, we return those if len(errors) > 0 { @@ -658,10 +945,29 @@ func (d *Decoder) decodeStruct(name string, data interface{}, val reflect.Value) } dataValKind := dataVal.Kind() - if dataValKind != reflect.Map { - return fmt.Errorf("'%s' expected a map, got '%s'", name, dataValKind) + switch dataValKind { + case reflect.Map: + return d.decodeStructFromMap(name, dataVal, val) + + case reflect.Struct: + // Not the most efficient way to do this but we can optimize later if + // we want to. To convert from struct to struct we go to map first + // as an intermediary. + m := make(map[string]interface{}) + mval := reflect.Indirect(reflect.ValueOf(&m)) + if err := d.decodeMapFromStruct(name, dataVal, mval, mval); err != nil { + return err + } + + result := d.decodeStructFromMap(name, mval, val) + return result + + default: + return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind()) } +} +func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) error { dataValType := dataVal.Type() if kind := dataValType.Key().Kind(); kind != reflect.String && kind != reflect.Interface { return fmt.Errorf( @@ -716,7 +1022,7 @@ func (d *Decoder) decodeStruct(name string, data interface{}, val reflect.Value) errors = appendErrors(errors, fmt.Errorf("%s: unsupported type for squash: %s", fieldType.Name, fieldKind)) } else { - structs = append(structs, val.FieldByName(fieldType.Name)) + structs = append(structs, structVal.FieldByName(fieldType.Name)) } continue } diff --git a/vendor/vendor.json b/vendor/vendor.json index 031342cfdcde..2b922bbba0ce 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -208,7 +208,7 @@ {"path":"github.com/mitchellh/go-homedir","checksumSHA1":"V/quM7+em2ByJbWBLOsEwnY3j/Q=","revision":"b8bc1bf767474819792c23f32d8286a45736f1c6","revisionTime":"2016-12-03T19:45:07Z"}, {"path":"github.com/mitchellh/go-testing-interface","checksumSHA1":"bDdhmDk8q6utWrccBhEOa6IoGkE=","revision":"a61a99592b77c9ba629d254a693acffaeb4b7e28","revisionTime":"2017-10-04T22:19:16Z"}, {"path":"github.com/mitchellh/hashstructure","checksumSHA1":"tWUjKyFOGJtYExocPWVYiXBYsfE=","revision":"2bca23e0e452137f789efbc8610126fd8b94f73b","revisionTime":"2017-06-09T04:59:27Z"}, - {"path":"github.com/mitchellh/mapstructure","checksumSHA1":"gILp4IL+xwXLH6tJtRLrnZ56F24=","revision":"06020f85339e21b2478f756a78e295255ffa4d6a","revisionTime":"2017-10-17T17:18:08Z"}, + {"path":"github.com/mitchellh/mapstructure","checksumSHA1":"7F5KalhUJ/sCH5bU44MMgw8tqNo=","revision":"5a380f224700b8a6c4eaad048804f5bff514cb35","revisionTime":"2018-10-01T02:14:42Z"}, {"path":"github.com/mitchellh/reflectwalk","checksumSHA1":"AMU63CNOg4XmIhVR/S/Xttt1/f0=","revision":"63d60e9d0dbc60cf9164e6510889b0db6683d98c","revisionTime":"2017-07-26T20:21:17Z"}, {"path":"github.com/modern-go/concurrent","checksumSHA1":"ZTcgWKWHsrX0RXYVXn5Xeb8Q0go=","revision":"bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94","revisionTime":"2018-03-06T01:26:44Z"}, {"path":"github.com/modern-go/reflect2","checksumSHA1":"qvH48wzTIV3QKSDqI0dLFtVjaDI=","revision":"94122c33edd36123c84d5368cfb2b69df93a0ec8","revisionTime":"2018-07-18T01:23:57Z"}, diff --git a/website/source/docs/commands/index.html.md b/website/source/docs/commands/index.html.md index 68c55a5b1401..f242ecbf55bd 100644 --- a/website/source/docs/commands/index.html.md +++ b/website/source/docs/commands/index.html.md @@ -28,16 +28,17 @@ Usage: consul [--version] [--help] [] Available commands are: agent Runs a Consul agent catalog Interact with the catalog + connect Interact with Consul Connect event Fire a new event exec Executes a command on Consul nodes force-leave Forces a member of the cluster to enter the "left" state info Provides debugging information for operators. + intention Interact with Connect service intentions join Tell Consul agent to join cluster keygen Generates a new encryption key keyring Manages gossip layer encryption keys kv Interact with the key-value store leave Gracefully leaves the Consul cluster and shuts down - license Get/Put the Consul Enterprise license (Enterprise-only) lock Execute a command holding a lock maint Controls node or service maintenance mode members Lists the members of a Consul cluster @@ -45,6 +46,7 @@ Available commands are: operator Provides cluster-level tools for Consul operators reload Triggers the agent to reload configuration files rtt Estimates network round trip time between nodes + services Interact with services snapshot Saves, restores and inspects snapshots of Consul server state validate Validate config files/directories version Prints the Consul version diff --git a/website/source/docs/commands/services.html.md b/website/source/docs/commands/services.html.md new file mode 100644 index 000000000000..f80e4b7f7bf9 --- /dev/null +++ b/website/source/docs/commands/services.html.md @@ -0,0 +1,66 @@ +--- +layout: "docs" +page_title: "Commands: Services" +sidebar_current: "docs-commands-services" +--- + +# Consul Agent Services + +Command: `consul services` + +The `services` command has subcommands for interacting with Consul services +registered with the [local agent](/docs/agent/basics.html). These provide +useful commands such as `register` and `deregister` for easily registering +services in scripts, dev mode, etc. +To view all services in the catalog, instead of only agent-local services, +see the [`catalog services`](/docs/commands/catalog/services.html) command. + +## Usage + +Usage: `consul services ` + +For the exact documentation for your Consul version, run `consul services -h` to +view the complete list of subcommands. + +```text +Usage: consul services [options] [args] + + ... + +Subcommands: + deregister Deregister services with the local agent + register Register services with the local agent +``` + +For more information, examples, and usage about a subcommand, click on the name +of the subcommand in the sidebar. + +## Basic Examples + +To create a simple service: + +```text +$ consul services register -name=web +``` + +To create a service from a configuration file: + +```text +$ cat web.json +{ + "Service": { + "Name": "web" + } +} + +$ consul services register web.json +``` + +To deregister a service: + +```sh +# Either style works: +$ consul services deregister web.json + +$ consul services deregister -id web +``` diff --git a/website/source/docs/commands/services/deregister.html.md b/website/source/docs/commands/services/deregister.html.md new file mode 100644 index 000000000000..87316383cfde --- /dev/null +++ b/website/source/docs/commands/services/deregister.html.md @@ -0,0 +1,63 @@ +--- +layout: "docs" +page_title: "Commands: Services Deregister" +sidebar_current: "docs-commands-services-deregister" +--- + +# Consul Agent Service Deregistration + +Command: `consul services deregister` + +The `services deregister` command deregisters a service with the local agent. +Note that this command can only deregister services that were registered +with the agent specified (defaults to the local agent) and is meant to +be paired with `services register`. + +This is just one method for service deregistration. If the service was +registered with a configuration file, then deleting that file and +[reloading](/docs/commands/reload.html) Consul is the correct method to +deregister. See [Service Definition](/docs/agent/services.html) for more +information about registering services generally. + +## Usage + +Usage: `consul services deregister [options] [FILE...]` + +This command can deregister either a single service using the `-id` flag +documented below, or one or more services using service definition files +in HCL or JSON format. +This flexibility makes it easy to pair the command with the +`services register` command since the argument syntax is the same. + +#### API Options + +<%= partial "docs/commands/http_api_options_client" %> + +#### Service Deregistration Flags + +The flags below should only be set if _no arguments_ are given. If no +arguments are given, the flags below can be used to deregister a single +service. + +* `-id` - The ID of the service. + +## Examples + +To deregister by ID: + +```text +$ consul services deregister -id=web +``` + +To deregister from a configuration file: + +```text +$ cat web.json +{ + "Service": { + "Name": "web" + } +} + +$ consul services deregister web.json +``` diff --git a/website/source/docs/commands/services/register.html.md b/website/source/docs/commands/services/register.html.md new file mode 100644 index 000000000000..9fd69feb1471 --- /dev/null +++ b/website/source/docs/commands/services/register.html.md @@ -0,0 +1,99 @@ +--- +layout: "docs" +page_title: "Commands: Services Register" +sidebar_current: "docs-commands-services-register" +--- + +# Consul Agent Service Registration + +Command: `consul services register` + +The `services register` command registers a service with the local agent. +This command returns after registration and must be paired with explicit +service deregistration. This command simplifies service registration from +scripts, in dev mode, etc. + +This is just one method of service registration. Services can also be +registered by placing a [service definition](/docs/agent/services.html) +in the Consul agent configuration directory and issuing a +[reload](/docs/commands/reload.html). This approach is easiest for +configuration management systems that other systems that have access to +the configuration directory. Clients may also use the +[HTTP API](/api/agent/service.html) directly. + +## Usage + +Usage: `consul services register [options] [FILE...]` + +This command can register either a single service using flags documented +below, or one or more services using service definition files in HCL +or JSON format. The service is registered against the specified Consul +agent (defaults to the local agent). This agent will execute all registered +health checks. + +This command returns after registration succeeds. It must be paired with +a deregistration command or API call to remove the service. To ensure that +services are properly deregistered, it is **highly recommended** that +a check is created with the +[`DeregisterCriticalServiceAfter`](/api/agent/check.html#deregistercriticalserviceafter) +configuration set. This will ensure that even if deregistration failed for +any reason, the agent will automatically deregister the service instance after +it is unhealthy for the specified period of time. + +Registered services are persisted in the agent state directory. If the +state directory remains unmodified, registered services will persist across +restarts. + +~> **Warning for Consul operators:** The Consul agent persists registered +services in the local state directory. If this state directory is deleted +or lost, services registered with this command will need to be reregistered. + +#### API Options + +<%= partial "docs/commands/http_api_options_client" %> + +#### Service Registration Flags + +The flags below should only be set if _no arguments_ are given. If no +arguments are given, the flags below can be used to register a single +service. + +Note that the behavior of each of the fields below is exactly the same +as when constructing a standard [service definition](/docs/agent/services.html). +Please refer to that documentation for full details. + +* `-id` - The ID of the service. This will default to `-name` if not set. + +* `-name` - The name of the service to register. + +* `-address` - The address of the service. If this isn't specified, + it will default to the address registered with the local agent. + +* `-port` - The port of the service. + +* `-meta key=value` - Specify arbitrary KV metadata to associate with the + service instance. This can be specified multiple times. + +* `-tag value` - Associate a tag with the service instance. This flag can + be specified multiples times. + +## Examples + +To create a simple service: + +```text +$ consul services register -name=web +``` + +To create a service from a configuration file: + +```text +$ cat web.json +{ + "Service": { + "Name": "web" + } +} + +$ consul services register web.json +``` diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 372f8de59b16..1731c80ca63d 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -185,6 +185,18 @@ rtt + > + services + + + > snapshot