Skip to content

Commit

Permalink
Merge pull request #4732 from hashicorp/f-service-cli
Browse files Browse the repository at this point in the history
cli: add `services register` and  `deregister`
  • Loading branch information
mitchellh authored Oct 2, 2018
2 parents f6810d5 + 657682d commit 868cb6d
Show file tree
Hide file tree
Showing 20 changed files with 1,546 additions and 90 deletions.
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.
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",
},
},
},
},
}

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

0 comments on commit 868cb6d

Please sign in to comment.