Skip to content

Commit

Permalink
cli: add service commands for list, info, and delete.
Browse files Browse the repository at this point in the history
  • Loading branch information
jrasell committed Mar 21, 2022
1 parent f0be952 commit 0112be6
Show file tree
Hide file tree
Showing 8 changed files with 755 additions and 0 deletions.
20 changes: 20 additions & 0 deletions command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
40 changes: 40 additions & 0 deletions command/service.go
Original file line number Diff line number Diff line change
@@ -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 }
68 changes: 68 additions & 0 deletions command/service_delete.go
Original file line number Diff line number Diff line change
@@ -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
}
74 changes: 74 additions & 0 deletions command/service_delete_test.go
Original file line number Diff line number Diff line change
@@ -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()
}
186 changes: 186 additions & 0 deletions command/service_info.go
Original file line number Diff line number Diff line change
@@ -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("")
}
}
}
Loading

0 comments on commit 0112be6

Please sign in to comment.