cli: add service commands for list, info, and delete.
This commit is contained in:
parent
042bf0fa57
commit
c0a2e493c3
|
@ -768,6 +768,26 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory {
|
|||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"service": func() (cli.Command, error) {
|
||||
return &ServiceCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"service list": func() (cli.Command, error) {
|
||||
return &ServiceListCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"service info": func() (cli.Command, error) {
|
||||
return &ServiceInfoCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"service delete": func() (cli.Command, error) {
|
||||
return &ServiceDeleteCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"status": func() (cli.Command, error) {
|
||||
return &StatusCommand{
|
||||
Meta: meta,
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
type ServiceCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *ServiceCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad service <subcommand> [options]
|
||||
|
||||
This command groups subcommands for interacting with the services API.
|
||||
|
||||
List services:
|
||||
|
||||
$ nomad service list
|
||||
|
||||
Detail an individual service:
|
||||
|
||||
$ nomad service info <service_name>
|
||||
|
||||
Delete an individual service registration:
|
||||
|
||||
$ nomad service delete <service_name> <service_id>
|
||||
|
||||
Please see the individual subcommand help for detailed usage information.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *ServiceCommand) Name() string { return "service" }
|
||||
|
||||
func (c *ServiceCommand) Synopsis() string { return "Interact with registered services" }
|
||||
|
||||
func (c *ServiceCommand) Run(_ []string) int { return cli.RunResultHelp }
|
|
@ -0,0 +1,68 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
type ServiceDeleteCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (s *ServiceDeleteCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad service delete [options] <service_name> <service_id>
|
||||
|
||||
Delete is used to deregister the specified service registration. It should be
|
||||
used with caution and can only remove a single registration, via the service
|
||||
name and service ID, at a time.
|
||||
|
||||
When ACLs are enabled, this command requires a token with the 'submit-job'
|
||||
capability for the service registration namespace.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage(usageOptsDefault)
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (s *ServiceDeleteCommand) Name() string { return "service delete" }
|
||||
|
||||
func (s *ServiceDeleteCommand) Synopsis() string { return "Deregister a registered service" }
|
||||
|
||||
func (s *ServiceDeleteCommand) AutocompleteFlags() complete.Flags {
|
||||
return mergeAutocompleteFlags(s.Meta.AutocompleteFlags(FlagSetClient),
|
||||
complete.Flags{})
|
||||
}
|
||||
|
||||
func (s *ServiceDeleteCommand) Run(args []string) int {
|
||||
|
||||
flags := s.Meta.FlagSet(s.Name(), FlagSetClient)
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
args = flags.Args()
|
||||
|
||||
if len(args) != 2 {
|
||||
s.Ui.Error("This command takes two arguments: <service_name> and <service_id>")
|
||||
s.Ui.Error(commandErrorText(s))
|
||||
return 1
|
||||
}
|
||||
|
||||
client, err := s.Meta.Client()
|
||||
if err != nil {
|
||||
s.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
if _, err := client.ServiceRegistrations().Delete(args[0], args[1], nil); err != nil {
|
||||
s.Ui.Error(fmt.Sprintf("Error deleting service registration: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
s.Ui.Output("Successfully deleted service registration")
|
||||
return 0
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServiceDeleteCommand_Run(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
srv, client, url := testServer(t, true, nil)
|
||||
defer srv.Shutdown()
|
||||
|
||||
// Wait until our test node is ready.
|
||||
testutil.WaitForResult(func() (bool, error) {
|
||||
nodes, _, err := client.Nodes().List(nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(nodes) == 0 {
|
||||
return false, fmt.Errorf("missing node")
|
||||
}
|
||||
if _, ok := nodes[0].Drivers["mock_driver"]; !ok {
|
||||
return false, fmt.Errorf("mock_driver not ready")
|
||||
}
|
||||
return true, nil
|
||||
}, func(err error) {
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &ServiceDeleteCommand{
|
||||
Meta: Meta{
|
||||
Ui: ui,
|
||||
flagAddress: url,
|
||||
},
|
||||
}
|
||||
|
||||
// Run the command without any arguments to ensure we are performing this
|
||||
// check.
|
||||
require.Equal(t, 1, cmd.Run([]string{"-address=" + url}))
|
||||
require.Contains(t, ui.ErrorWriter.String(),
|
||||
"This command takes two arguments: <service_name> and <service_id>")
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Create a test job with a Nomad service.
|
||||
testJob := testJob("service-discovery-nomad-delete")
|
||||
testJob.TaskGroups[0].Tasks[0].Services = []*api.Service{
|
||||
{Name: "service-discovery-nomad-delete", Provider: "nomad"}}
|
||||
|
||||
// Register that job.
|
||||
regResp, _, err := client.Jobs().Register(testJob, nil)
|
||||
require.NoError(t, err)
|
||||
registerCode := waitForSuccess(ui, client, fullId, t, regResp.EvalID)
|
||||
require.Equal(t, 0, registerCode)
|
||||
|
||||
// Detail the service as we need the ID.
|
||||
serviceList, _, err := client.ServiceRegistrations().Get("service-discovery-nomad-delete", nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, serviceList, 1)
|
||||
|
||||
// Attempt to manually delete the service registration.
|
||||
code := cmd.Run([]string{"-address=" + url, "service-discovery-nomad-delete", serviceList[0].ID})
|
||||
require.Equal(t, 0, code)
|
||||
require.Contains(t, ui.OutputWriter.String(), "Successfully deleted service registration")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
// Ensure ServiceInfoCommand satisfies the cli.Command interface.
|
||||
var _ cli.Command = &ServiceInfoCommand{}
|
||||
|
||||
// ServiceInfoCommand implements cli.Command.
|
||||
type ServiceInfoCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
// Help satisfies the cli.Command Help function.
|
||||
func (s *ServiceInfoCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad service info [options] <service_name>
|
||||
|
||||
Info is used to read the services registered to a single service name.
|
||||
|
||||
When ACLs are enabled, this command requires a token with the 'read-job'
|
||||
capability for the service namespace.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage(usageOptsDefault) + `
|
||||
|
||||
Service Info Options:
|
||||
|
||||
-verbose
|
||||
Display full information.
|
||||
|
||||
-json
|
||||
Output the service in JSON format.
|
||||
|
||||
-t
|
||||
Format and display the service using a Go template.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
// Synopsis satisfies the cli.Command Synopsis function.
|
||||
func (s *ServiceInfoCommand) Synopsis() string {
|
||||
return "Display an individual Nomad service registration"
|
||||
}
|
||||
|
||||
func (s *ServiceInfoCommand) AutocompleteFlags() complete.Flags {
|
||||
return mergeAutocompleteFlags(s.Meta.AutocompleteFlags(FlagSetClient),
|
||||
complete.Flags{
|
||||
"-json": complete.PredictNothing,
|
||||
"-t": complete.PredictAnything,
|
||||
"-verbose": complete.PredictNothing,
|
||||
})
|
||||
}
|
||||
|
||||
// Name returns the name of this command.
|
||||
func (s *ServiceInfoCommand) Name() string { return "service info" }
|
||||
|
||||
// Run satisfies the cli.Command Run function.
|
||||
func (s *ServiceInfoCommand) Run(args []string) int {
|
||||
var (
|
||||
json, verbose bool
|
||||
tmpl string
|
||||
)
|
||||
|
||||
flags := s.Meta.FlagSet(s.Name(), FlagSetClient)
|
||||
flags.Usage = func() { s.Ui.Output(s.Help()) }
|
||||
flags.BoolVar(&json, "json", false, "")
|
||||
flags.BoolVar(&verbose, "verbose", false, "")
|
||||
flags.StringVar(&tmpl, "t", "", "")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
args = flags.Args()
|
||||
|
||||
if len(args) != 1 {
|
||||
s.Ui.Error("This command takes one argument: <service_name>")
|
||||
s.Ui.Error(commandErrorText(s))
|
||||
return 1
|
||||
}
|
||||
|
||||
client, err := s.Meta.Client()
|
||||
if err != nil {
|
||||
s.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
serviceInfo, _, err := client.ServiceRegistrations().Get(args[0], nil)
|
||||
if err != nil {
|
||||
s.Ui.Error(fmt.Sprintf("Error listing service registrations: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
if len(serviceInfo) == 0 {
|
||||
s.Ui.Output("No service registrations found")
|
||||
return 0
|
||||
}
|
||||
|
||||
if json || len(tmpl) > 0 {
|
||||
out, err := Format(json, tmpl, serviceInfo)
|
||||
if err != nil {
|
||||
s.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
s.Ui.Output(out)
|
||||
return 0
|
||||
}
|
||||
|
||||
// It is possible for multiple jobs to register a service with the same
|
||||
// name. In order to provide consistency, sort the output by job ID.
|
||||
sortedJobID := []string{}
|
||||
jobIDServices := make(map[string][]*api.ServiceRegistration)
|
||||
|
||||
// Populate the objects, ensuring we do not add duplicate job IDs to the
|
||||
// array which will be sorted.
|
||||
for _, service := range serviceInfo {
|
||||
if _, ok := jobIDServices[service.JobID]; ok {
|
||||
jobIDServices[service.JobID] = append(jobIDServices[service.JobID], service)
|
||||
} else {
|
||||
jobIDServices[service.JobID] = []*api.ServiceRegistration{service}
|
||||
sortedJobID = append(sortedJobID, service.JobID)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the jobIDs.
|
||||
sort.Strings(sortedJobID)
|
||||
|
||||
if verbose {
|
||||
s.formatVerboseOutput(sortedJobID, jobIDServices)
|
||||
} else {
|
||||
s.formatOutput(sortedJobID, jobIDServices)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// formatOutput produces the non-verbose output of service registration info
|
||||
// for a specific service by its name.
|
||||
func (s *ServiceInfoCommand) formatOutput(jobIDs []string, jobServices map[string][]*api.ServiceRegistration) {
|
||||
|
||||
// Create the output table header.
|
||||
outputTable := []string{"Job ID|Address|Tags|Node ID|Alloc ID"}
|
||||
|
||||
// Populate the list.
|
||||
for _, jobID := range jobIDs {
|
||||
for _, service := range jobServices[jobID] {
|
||||
outputTable = append(outputTable, fmt.Sprintf(
|
||||
"%s|%s|[%s]|%s|%s",
|
||||
service.JobID,
|
||||
fmt.Sprintf("%s:%v", service.Address, service.Port),
|
||||
strings.Join(service.Tags, ","),
|
||||
limit(service.NodeID, shortId),
|
||||
limit(service.AllocID, shortId),
|
||||
))
|
||||
}
|
||||
}
|
||||
s.Ui.Output(formatList(outputTable))
|
||||
}
|
||||
|
||||
// formatOutput produces the verbose output of service registration info for a
|
||||
// specific service by its name.
|
||||
func (s *ServiceInfoCommand) formatVerboseOutput(jobIDs []string, jobServices map[string][]*api.ServiceRegistration) {
|
||||
for _, jobID := range jobIDs {
|
||||
for _, service := range jobServices[jobID] {
|
||||
out := []string{
|
||||
fmt.Sprintf("ID|%s", service.ID),
|
||||
fmt.Sprintf("Service Name|%s", service.ServiceName),
|
||||
fmt.Sprintf("Namespace|%s", service.Namespace),
|
||||
fmt.Sprintf("Job ID|%s", service.JobID),
|
||||
fmt.Sprintf("Alloc ID|%s", service.AllocID),
|
||||
fmt.Sprintf("Node ID|%s", service.NodeID),
|
||||
fmt.Sprintf("Datacenter|%s", service.Datacenter),
|
||||
fmt.Sprintf("Address|%v", fmt.Sprintf("%s:%v", service.Address, service.Port)),
|
||||
fmt.Sprintf("Tags|[%s]\n", strings.Join(service.Tags, ",")),
|
||||
}
|
||||
s.Ui.Output(formatKV(out))
|
||||
s.Ui.Output("")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServiceInfoCommand_Run(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
srv, client, url := testServer(t, true, nil)
|
||||
defer srv.Shutdown()
|
||||
|
||||
// Wait until our test node is ready.
|
||||
testutil.WaitForResult(func() (bool, error) {
|
||||
nodes, _, err := client.Nodes().List(nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(nodes) == 0 {
|
||||
return false, fmt.Errorf("missing node")
|
||||
}
|
||||
if _, ok := nodes[0].Drivers["mock_driver"]; !ok {
|
||||
return false, fmt.Errorf("mock_driver not ready")
|
||||
}
|
||||
return true, nil
|
||||
}, func(err error) {
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &ServiceInfoCommand{
|
||||
Meta: Meta{
|
||||
Ui: ui,
|
||||
flagAddress: url,
|
||||
},
|
||||
}
|
||||
|
||||
// Run the command without any arguments to ensure we are performing this
|
||||
// check.
|
||||
require.Equal(t, 1, cmd.Run([]string{"-address=" + url}))
|
||||
require.Contains(t, ui.ErrorWriter.String(),
|
||||
"This command takes one argument: <service_name>")
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Create a test job with a Nomad service.
|
||||
testJob := testJob("service-discovery-nomad-info")
|
||||
testJob.TaskGroups[0].Tasks[0].Services = []*api.Service{
|
||||
{Name: "service-discovery-nomad-info", Provider: "nomad", Tags: []string{"foo", "bar"}}}
|
||||
|
||||
// Register that job.
|
||||
regResp, _, err := client.Jobs().Register(testJob, nil)
|
||||
require.NoError(t, err)
|
||||
registerCode := waitForSuccess(ui, client, fullId, t, regResp.EvalID)
|
||||
require.Equal(t, 0, registerCode)
|
||||
|
||||
// Reset the output writer, otherwise we will have additional information here.
|
||||
ui.OutputWriter.Reset()
|
||||
|
||||
// Job register doesn't assure the service registration has completed. It
|
||||
// therefore needs this wrapper to account for eventual service
|
||||
// registration. One this has completed, we can perform lookups without
|
||||
// similar wraps.
|
||||
require.Eventually(t, func() bool {
|
||||
|
||||
defer ui.OutputWriter.Reset()
|
||||
|
||||
// Perform a standard lookup.
|
||||
if code := cmd.Run([]string{"-address=" + url, "service-discovery-nomad-info"}); code != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Test each header and data entry.
|
||||
s := ui.OutputWriter.String()
|
||||
if !assert.Contains(t, s, "Job ID") {
|
||||
return false
|
||||
}
|
||||
if !assert.Contains(t, s, "Address") {
|
||||
return false
|
||||
}
|
||||
if !assert.Contains(t, s, "Node ID") {
|
||||
return false
|
||||
}
|
||||
if !assert.Contains(t, s, "Alloc ID") {
|
||||
return false
|
||||
}
|
||||
if !assert.Contains(t, s, "service-discovery-nomad-info") {
|
||||
return false
|
||||
}
|
||||
if !assert.Contains(t, s, ":0") {
|
||||
return false
|
||||
}
|
||||
if !assert.Contains(t, s, "[foo,bar]") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
|
||||
// Perform a verbose lookup.
|
||||
code := cmd.Run([]string{"-address=" + url, "-verbose", "service-discovery-nomad-info"})
|
||||
require.Equal(t, 0, code)
|
||||
|
||||
// Test KV entries.
|
||||
s := ui.OutputWriter.String()
|
||||
require.Contains(t, s, "Service Name = service-discovery-nomad-info")
|
||||
require.Contains(t, s, "Namespace = default")
|
||||
require.Contains(t, s, "Job ID = service-discovery-nomad-info")
|
||||
require.Contains(t, s, "Datacenter = dc1")
|
||||
require.Contains(t, s, "Address = :0")
|
||||
require.Contains(t, s, "Tags = [foo,bar]")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
// Ensure ServiceListCommand satisfies the cli.Command interface.
|
||||
var _ cli.Command = &ServiceListCommand{}
|
||||
|
||||
// ServiceListCommand implements cli.Command.
|
||||
type ServiceListCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
// Help satisfies the cli.Command Help function.
|
||||
func (s *ServiceListCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad service list [options]
|
||||
|
||||
List is used to list the currently registered services.
|
||||
|
||||
If ACLs are enabled, this command requires a token with the 'read-job'
|
||||
capabilities for the namespace of all services. Any namespaces that the token
|
||||
does not have access to will have its services filtered from the results.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage(usageOptsDefault) + `
|
||||
|
||||
Service List Options:
|
||||
|
||||
-json
|
||||
Output the services in JSON format.
|
||||
|
||||
-t
|
||||
Format and display the services using a Go template.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
// Synopsis satisfies the cli.Command Synopsis function.
|
||||
func (s *ServiceListCommand) Synopsis() string {
|
||||
return "Display all registered Nomad services"
|
||||
}
|
||||
|
||||
func (s *ServiceListCommand) AutocompleteFlags() complete.Flags {
|
||||
return mergeAutocompleteFlags(s.Meta.AutocompleteFlags(FlagSetClient),
|
||||
complete.Flags{
|
||||
"-json": complete.PredictNothing,
|
||||
"-t": complete.PredictAnything,
|
||||
})
|
||||
}
|
||||
|
||||
// Name returns the name of this command.
|
||||
func (s *ServiceListCommand) Name() string { return "service list" }
|
||||
|
||||
// Run satisfies the cli.Command Run function.
|
||||
func (s *ServiceListCommand) Run(args []string) int {
|
||||
|
||||
var (
|
||||
json bool
|
||||
tmpl, name string
|
||||
)
|
||||
|
||||
flags := s.Meta.FlagSet(s.Name(), FlagSetClient)
|
||||
flags.Usage = func() { s.Ui.Output(s.Help()) }
|
||||
flags.BoolVar(&json, "json", false, "")
|
||||
flags.StringVar(&name, "name", "", "")
|
||||
flags.StringVar(&tmpl, "t", "", "")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
if args = flags.Args(); len(args) > 0 {
|
||||
s.Ui.Error("This command takes no arguments")
|
||||
s.Ui.Error(commandErrorText(s))
|
||||
return 1
|
||||
}
|
||||
|
||||
client, err := s.Meta.Client()
|
||||
if err != nil {
|
||||
s.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
list, _, err := client.ServiceRegistrations().List(nil)
|
||||
if err != nil {
|
||||
s.Ui.Error(fmt.Sprintf("Error listing service registrations: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
s.Ui.Output("No service registrations found")
|
||||
return 0
|
||||
}
|
||||
|
||||
if json || len(tmpl) > 0 {
|
||||
out, err := Format(json, tmpl, list)
|
||||
if err != nil {
|
||||
s.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
s.Ui.Output(out)
|
||||
return 0
|
||||
}
|
||||
|
||||
s.formatOutput(list)
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *ServiceListCommand) formatOutput(regs []*api.ServiceRegistrationListStub) {
|
||||
|
||||
// Create objects to hold sorted a sorted namespace array and a mapping, so
|
||||
// we can perform service lookups on a namespace basis.
|
||||
sortedNamespaces := make([]string, len(regs))
|
||||
namespacedServices := make(map[string][]*api.ServiceRegistrationStub)
|
||||
|
||||
for i, namespaceServices := range regs {
|
||||
sortedNamespaces[i] = namespaceServices.Namespace
|
||||
namespacedServices[namespaceServices.Namespace] = namespaceServices.Services
|
||||
}
|
||||
|
||||
// Sort the namespaces.
|
||||
sort.Strings(sortedNamespaces)
|
||||
|
||||
// The table always starts with the service name.
|
||||
outputTable := []string{"Service Name"}
|
||||
|
||||
// If the request was made using the wildcard namespace, include this in
|
||||
// the output.
|
||||
if s.Meta.namespace == api.AllNamespacesNamespace {
|
||||
outputTable[0] += "|Namespace"
|
||||
}
|
||||
|
||||
// The tags come last and are always present.
|
||||
outputTable[0] += "|Tags"
|
||||
|
||||
for _, ns := range sortedNamespaces {
|
||||
|
||||
// Grab the services belonging to this namespace.
|
||||
services := namespacedServices[ns]
|
||||
|
||||
// Create objects to hold sorted a sorted service name array and a
|
||||
// mapping, so we can perform service tag lookups on a name basis.
|
||||
sortedNames := make([]string, len(services))
|
||||
serviceTags := make(map[string][]string)
|
||||
|
||||
for i, service := range services {
|
||||
sortedNames[i] = service.ServiceName
|
||||
serviceTags[service.ServiceName] = service.Tags
|
||||
}
|
||||
|
||||
// Sort the service names.
|
||||
sort.Strings(sortedNames)
|
||||
|
||||
for _, serviceName := range sortedNames {
|
||||
|
||||
// Grab the service tags, and sort these for good measure.
|
||||
tags := serviceTags[serviceName]
|
||||
sort.Strings(tags)
|
||||
|
||||
// Build the output array entry.
|
||||
regOutput := serviceName
|
||||
|
||||
if s.Meta.namespace == api.AllNamespacesNamespace {
|
||||
regOutput += "|" + ns
|
||||
}
|
||||
regOutput += "|" + fmt.Sprintf("[%s]", strings.Join(tags, ","))
|
||||
outputTable = append(outputTable, regOutput)
|
||||
}
|
||||
}
|
||||
|
||||
s.Ui.Output(formatList(outputTable))
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServiceListCommand_Run(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
srv, client, url := testServer(t, true, nil)
|
||||
defer srv.Shutdown()
|
||||
|
||||
// Wait until our test node is ready.
|
||||
testutil.WaitForResult(func() (bool, error) {
|
||||
nodes, _, err := client.Nodes().List(nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(nodes) == 0 {
|
||||
return false, fmt.Errorf("missing node")
|
||||
}
|
||||
if _, ok := nodes[0].Drivers["mock_driver"]; !ok {
|
||||
return false, fmt.Errorf("mock_driver not ready")
|
||||
}
|
||||
return true, nil
|
||||
}, func(err error) {
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &ServiceListCommand{
|
||||
Meta: Meta{
|
||||
Ui: ui,
|
||||
flagAddress: url,
|
||||
},
|
||||
}
|
||||
|
||||
// Run the command with some random arguments to ensure we are performing
|
||||
// this check.
|
||||
require.Equal(t, 1, cmd.Run([]string{"-address=" + url, "pretty-please"}))
|
||||
require.Contains(t, ui.ErrorWriter.String(), "This command takes no arguments")
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Create a test job with a Nomad service.
|
||||
testJob := testJob("service-discovery-nomad-list")
|
||||
testJob.TaskGroups[0].Tasks[0].Services = []*api.Service{
|
||||
{Name: "service-discovery-nomad-list", Provider: "nomad", Tags: []string{"foo", "bar"}}}
|
||||
|
||||
// Register that job.
|
||||
regResp, _, err := client.Jobs().Register(testJob, nil)
|
||||
require.NoError(t, err)
|
||||
registerCode := waitForSuccess(ui, client, fullId, t, regResp.EvalID)
|
||||
require.Equal(t, 0, registerCode)
|
||||
|
||||
// Reset the output writer, otherwise we will have additional information here.
|
||||
ui.OutputWriter.Reset()
|
||||
|
||||
// Job register doesn't assure the service registration has completed. It
|
||||
// therefore needs this wrapper to account for eventual service
|
||||
// registration. One this has completed, we can perform lookups without
|
||||
// similar wraps.
|
||||
require.Eventually(t, func() bool {
|
||||
|
||||
defer ui.OutputWriter.Reset()
|
||||
|
||||
// Perform a standard lookup.
|
||||
if code := cmd.Run([]string{"-address=" + url}); code != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Test each header and data entry.
|
||||
s := ui.OutputWriter.String()
|
||||
if !assert.Contains(t, s, "Service Name") {
|
||||
return false
|
||||
}
|
||||
if !assert.Contains(t, s, "Tags") {
|
||||
return false
|
||||
}
|
||||
if !assert.Contains(t, s, "service-discovery-nomad-list") {
|
||||
return false
|
||||
}
|
||||
if !assert.Contains(t, s, "[bar,foo]") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
|
||||
// Perform a wildcard namespace lookup.
|
||||
code := cmd.Run([]string{"-address=" + url, "-namespace", "*"})
|
||||
require.Equal(t, 0, code)
|
||||
|
||||
// Test each header and data entry.
|
||||
s := ui.OutputWriter.String()
|
||||
require.Contains(t, s, "Service Name")
|
||||
require.Contains(t, s, "Namespace")
|
||||
require.Contains(t, s, "Tags")
|
||||
require.Contains(t, s, "service-discovery-nomad-list")
|
||||
require.Contains(t, s, "default")
|
||||
require.Contains(t, s, "[bar,foo]")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
}
|
Loading…
Reference in New Issue