-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4732 from hashicorp/f-service-cli
cli: add `services register` and `deregister`
- Loading branch information
Showing
20 changed files
with
1,546 additions
and
90 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
` |
Oops, something went wrong.