Files
nomad/command/status.go
Phil Renaud 35e1ea4328 [cli] UI URL hints for common CLI commands (#24454)
* Basic implementation for server members and node status

* Commands for alloc status and job status

* -ui flag for most commands

* url hints for variables

* url hints for job dispatch, evals, and deployments

* agent config ui.cli_url_links to disable

* Fix an issue where path prefix was presumed for variables

* driver uncomment and general cleanup

* -ui flag on the generic status endpoint

* Job run command gets namespaces, and no longer gets ui hints for --output flag

* Dispatch command hints get a namespace, and bunch o tests

* Lots of tests depend on specific output, so let's not mess with them

* figured out what flagAddress is all about for testServer, oof

* Parallel outside of test instances

* Browser-opening test, sorta

* Env var for disabling/enabling CLI hints

* Addressing a few PR comments

* CLI docs available flags now all have -ui

* PR comments addressed; switched the env var to be consistent and scrunched monitor-adjacent hints a bit more

* ui.Output -> ui.Warn; moves hints from stdout to stderr

* isTerminal check and parseBool on command option

* terminal.IsTerminal check removed for test-runner-not-being-terminal reasons
2025-03-07 13:23:35 -05:00

201 lines
4.7 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"fmt"
"strings"
"github.com/hashicorp/cli"
"github.com/hashicorp/nomad/api/contexts"
"github.com/posener/complete"
)
type StatusCommand struct {
Meta
// Placeholder bool to allow passing of verbose flags to subcommands.
verbose bool
openURL bool
}
func (c *StatusCommand) Help() string {
helpText := `
Usage: nomad status [options] <identifier>
Display the status output for any given resource. The command will
detect the type of resource being queried and display the appropriate
status output.
If no arguments are provided, the command will fallback to "nomad job status",
which will list all jobs.
General Options:
` + generalOptionsUsage(usageOptsDefault) + `
Status Options:
-verbose
Display full information.
-ui
Open the status page in the browser.
`
return strings.TrimSpace(helpText)
}
func (c *StatusCommand) Synopsis() string {
return "Display the status output for a resource"
}
func (c *StatusCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-verbose": complete.PredictNothing,
"-ui": complete.PredictNothing,
})
}
func (c *StatusCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictFunc(func(a complete.Args) []string {
client, err := c.Meta.Client()
if err != nil {
return nil
}
resp, _, err := client.Search().PrefixSearch(a.Last, contexts.All, nil)
if err != nil {
return []string{}
}
final := make([]string, 0)
for _, matches := range resp.Matches {
if len(matches) == 0 {
continue
}
final = append(final, matches...)
}
return final
})
}
func (c *StatusCommand) Run(args []string) int {
flags := c.Meta.FlagSet("status", FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.BoolVar(&c.verbose, "verbose", false, "")
flags.BoolVar(&c.openURL, "ui", false, "")
if err := flags.Parse(args); err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing arguments: %q", err))
return 1
}
// Store the original arguments so we can pass them to the routed command
argsCopy := args
// Check that we got exactly one evaluation ID
args = flags.Args()
// Get the HTTP client
client, err := c.Meta.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing client: %q", err))
return 1
}
// If no identifier is provided, default to listing jobs
if len(args) == 0 {
cmd := &JobStatusCommand{Meta: c.Meta}
return cmd.Run(argsCopy)
}
id := args[len(args)-1]
// Query for the context associated with the id
res, _, err := client.Search().PrefixSearch(id, contexts.All, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying search with id: %q", err))
return 1
}
if res.Matches == nil {
c.Ui.Error(fmt.Sprintf("No matches returned for query: %q", err))
return 1
}
var match contexts.Context
exactMatches := 0
for ctx, vers := range res.Matches {
if len(vers) > 0 && vers[0] == id {
match = ctx
exactMatches++
}
}
if exactMatches > 1 {
c.logMultiMatchError(id, res.Matches)
return 1
} else if exactMatches == 0 {
matchCount := 0
for ctx, vers := range res.Matches {
l := len(vers)
if l == 1 {
match = ctx
matchCount++
}
// Only a single result should return, as this is a match against a full id
if matchCount > 1 || l > 1 {
c.logMultiMatchError(id, res.Matches)
return 1
}
}
}
var cmd cli.Command
switch match {
case contexts.Evals:
cmd = &EvalStatusCommand{Meta: c.Meta}
case contexts.Nodes:
cmd = &NodeStatusCommand{Meta: c.Meta}
case contexts.Allocs:
cmd = &AllocStatusCommand{Meta: c.Meta}
case contexts.Jobs:
cmd = &JobStatusCommand{Meta: c.Meta}
case contexts.Deployments:
cmd = &DeploymentStatusCommand{Meta: c.Meta}
case contexts.Namespaces:
cmd = &NamespaceStatusCommand{Meta: c.Meta}
case contexts.Quotas:
cmd = &QuotaStatusCommand{Meta: c.Meta}
case contexts.Plugins:
cmd = &PluginStatusCommand{Meta: c.Meta}
case contexts.Volumes:
cmd = &VolumeStatusCommand{Meta: c.Meta}
default:
c.Ui.Error(fmt.Sprintf("Unable to resolve ID: %q", id))
return 1
}
return cmd.Run(argsCopy)
}
// logMultiMatchError is used to log an error message when multiple matches are
// found. The error message logged displays the matched IDs per context.
func (c *StatusCommand) logMultiMatchError(id string, matches map[contexts.Context][]string) {
c.Ui.Error(fmt.Sprintf("Multiple matches found for id %q", id))
for ctx, vers := range matches {
if len(vers) == 0 {
continue
}
c.Ui.Error(fmt.Sprintf("\n%s:", strings.Title(string(ctx))))
c.Ui.Error(strings.Join(vers, ", "))
}
}