Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli: add services register and deregister #4732

Merged
merged 12 commits into from
Oct 2, 2018
Merged
6 changes: 6 additions & 0 deletions command/commands_oss.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 })
Expand Down
100 changes: 100 additions & 0 deletions command/services/config.go
Original file line number Diff line number Diff line change
@@ -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.
mitchellh marked this conversation as resolved.
Show resolved Hide resolved
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
}
}
105 changes: 105 additions & 0 deletions command/services/config_test.go
Original file line number Diff line number Diff line change
@@ -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",
},
},
},
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When f-envoy lands it might be worth adding explicit test cases for connect proxys (including upstreams etc) and sidecar services.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely, that's also why I prepped the table test. :D

}

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 }
114 changes: 114 additions & 0 deletions command/services/deregister/deregister.go
Original file line number Diff line number Diff line change
@@ -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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why %q for a string? Not a big deal just wondering.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes for cleaner output to the user to see a quoted string, it also helps in cases where there are spaces (a service name shouldn't have spaces but just in general). So usually when erroring out to the user or even logging (w/o hclog) I use %q in places I'd otherwise use %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.
`
Loading