mirror of https://github.com/hashicorp/consul
Merge pull request #4732 from hashicorp/f-service-cli
cli: add `services register` and `deregister`pull/4745/head
commit
868cb6d84f
|
@ -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 })
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 }
|
|
@ -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.
|
||||
`
|
|
@ -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
|
||||
}
|
|
@ -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.
|
||||
`
|
|
@ -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
|
||||
}
|
|
@ -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.
|
||||
`
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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.
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
//
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
module github.com/mitchellh/mapstructure
|
|
@ -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)
|
||||
|
@ -510,15 +595,30 @@ 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
|
||||
}
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
```
|
|
@ -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
|
||||
```
|
|
@ -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
|
||||
```
|
|
@ -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">
|
||||
|
|
Loading…
Reference in New Issue