Merge pull request #4732 from hashicorp/f-service-cli

cli: add `services register` and  `deregister`
pull/4745/head
Mitchell Hashimoto 2018-10-02 12:45:58 -07:00 committed by GitHub
commit 868cb6d84f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1553 additions and 97 deletions

View File

@ -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 })

100
command/services/config.go Normal file
View File

@ -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
}
}

View File

@ -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 }

View File

@ -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.
`

View File

@ -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
}

View File

@ -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.
`

View File

@ -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
}

View File

@ -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 <subcommand> [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.
`

View File

@ -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")
}
}

12
vendor/github.com/mitchellh/mapstructure/CHANGELOG.md generated vendored Normal file
View File

@ -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.

View File

@ -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.

View File

@ -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.
//

1
vendor/github.com/mitchellh/mapstructure/go.mod generated vendored Normal file
View File

@ -0,0 +1 @@
module github.com/mitchellh/mapstructure

View File

@ -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,13 +557,28 @@ 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.Map:
return d.decodeMapFromMap(name, dataVal, val, valMap)
case reflect.Struct:
return d.decodeMapFromStruct(name, dataVal, val, valMap)
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)
@ -511,14 +596,29 @@ func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) er
return nil
}
}
return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind())
}
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,12 +651,113 @@ 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)
@ -567,6 +768,11 @@ func (d *Decoder) decodePtr(name string, data interface{}, val reflect.Value) er
}
val.Set(realVal)
} else {
if err := d.decode(name, data, reflect.Indirect(val)); err != nil {
return err
}
}
return nil
}
@ -592,16 +798,23 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value)
valSlice := val
if valSlice.IsNil() || d.config.ZeroFields {
// Check input type
if dataValKind != reflect.Array && dataValKind != reflect.Slice {
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.
@ -611,11 +824,18 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value)
}
}
// 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())
}
@ -647,6 +867,73 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value)
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 arrays
case dataValKind == reflect.Map:
if dataVal.Len() == 0 {
val.Set(reflect.Zero(arrayType))
return nil
}
// 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.decodeArray(name, []interface{}{data}, val)
}
}
return fmt.Errorf(
"'%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 array to hold our result, same size as the original data.
valArray = reflect.New(arrayType).Elem()
}
// Accumulate any errors
errors := make([]string, 0)
for i := 0; i < dataVal.Len(); i++ {
currentData := dataVal.Index(i).Interface()
currentField := valArray.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 array we built up
val.Set(valArray)
// If there were errors, we return those
if len(errors) > 0 {
return &Error{errors}
}
return nil
}
func (d *Decoder) decodeStruct(name string, data interface{}, val reflect.Value) error {
dataVal := reflect.Indirect(reflect.ValueOf(data))
@ -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
}

2
vendor/vendor.json vendored
View File

@ -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"},

View File

@ -28,16 +28,17 @@ Usage: consul [--version] [--help] <command> [<args>]
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

View File

@ -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 <subcommand>`
For the exact documentation for your Consul version, run `consul services -h` to
view the complete list of subcommands.
```text
Usage: consul services <subcommand> [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
```

View File

@ -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
```

View File

@ -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
```

View File

@ -185,6 +185,18 @@
<a href="/docs/commands/rtt.html">rtt</a>
</li>
<li<%= sidebar_current("docs-commands-services") %>>
<a href="/docs/commands/services.html">services</a>
<ul class="nav">
<li<%= sidebar_current("docs-commands-services-register") %>>
<a href="/docs/commands/services/register.html">register</a>
</li>
<li<%= sidebar_current("docs-commands-services-deregister") %>>
<a href="/docs/commands/services/deregister.html">deregister</a>
</li>
</ul>
</li>
<li<%= sidebar_current("docs-commands-snapshot") %>>
<a href="/docs/commands/snapshot.html">snapshot</a>
<ul class="nav">