mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
[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
This commit is contained in:
3
.changelog/24454.txt
Normal file
3
.changelog/24454.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:improvement
|
||||
cli: Added UI URL hints to the end of common CLI commands and a `-ui` flag to auto-open them
|
||||
```
|
||||
@@ -49,6 +49,9 @@ Alloc Status Options:
|
||||
-verbose
|
||||
Show full information.
|
||||
|
||||
-ui
|
||||
Open the allocation status page in the browser.
|
||||
|
||||
-json
|
||||
Output the allocation in its JSON format.
|
||||
|
||||
@@ -70,6 +73,7 @@ func (c *AllocStatusCommand) AutocompleteFlags() complete.Flags {
|
||||
"-verbose": complete.PredictNothing,
|
||||
"-json": complete.PredictNothing,
|
||||
"-t": complete.PredictAnything,
|
||||
"-ui": complete.PredictNothing,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -91,7 +95,7 @@ func (c *AllocStatusCommand) AutocompleteArgs() complete.Predictor {
|
||||
func (c *AllocStatusCommand) Name() string { return "alloc status" }
|
||||
|
||||
func (c *AllocStatusCommand) Run(args []string) int {
|
||||
var short, displayStats, verbose, json bool
|
||||
var short, displayStats, verbose, json, openURL bool
|
||||
var tmpl string
|
||||
|
||||
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
|
||||
@@ -101,6 +105,7 @@ func (c *AllocStatusCommand) Run(args []string) int {
|
||||
flags.BoolVar(&displayStats, "stats", false, "")
|
||||
flags.BoolVar(&json, "json", false, "")
|
||||
flags.StringVar(&tmpl, "t", "", "")
|
||||
flags.BoolVar(&openURL, "ui", false, "")
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
@@ -237,6 +242,16 @@ func (c *AllocStatusCommand) Run(args []string) int {
|
||||
c.Ui.Output(formatAllocMetrics(alloc.Metrics, true, " "))
|
||||
}
|
||||
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "alloc status",
|
||||
PathParams: map[string]string{
|
||||
"allocID": alloc.ID,
|
||||
},
|
||||
OpenURL: openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ const (
|
||||
|
||||
// EnvNomadCLIForceColor is an env var that forces colored UI output.
|
||||
EnvNomadCLIForceColor = `NOMAD_CLI_FORCE_COLOR`
|
||||
|
||||
// EnvNomadCLIShowHints is an env var that toggles CLI hints.
|
||||
EnvNomadCLIShowHints = `NOMAD_CLI_SHOW_HINTS`
|
||||
)
|
||||
|
||||
// DeprecatedCommand is a command that wraps an existing command and prints a
|
||||
|
||||
@@ -55,6 +55,9 @@ Status Options:
|
||||
How long to wait before polling an update, used in conjunction with monitor
|
||||
mode. Defaults to 2s.
|
||||
|
||||
-ui
|
||||
Open the deployment in the browser.
|
||||
|
||||
-t
|
||||
Format and display deployment using a Go template.
|
||||
`
|
||||
@@ -72,6 +75,7 @@ func (c *DeploymentStatusCommand) AutocompleteFlags() complete.Flags {
|
||||
"-json": complete.PredictNothing,
|
||||
"-monitor": complete.PredictNothing,
|
||||
"-t": complete.PredictAnything,
|
||||
"-ui": complete.PredictNothing,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -93,7 +97,7 @@ func (c *DeploymentStatusCommand) AutocompleteArgs() complete.Predictor {
|
||||
func (c *DeploymentStatusCommand) Name() string { return "deployment status" }
|
||||
|
||||
func (c *DeploymentStatusCommand) Run(args []string) int {
|
||||
var json, verbose, monitor bool
|
||||
var json, verbose, monitor, openURL bool
|
||||
var wait time.Duration
|
||||
var tmpl string
|
||||
|
||||
@@ -104,7 +108,7 @@ func (c *DeploymentStatusCommand) Run(args []string) int {
|
||||
flags.BoolVar(&monitor, "monitor", false, "")
|
||||
flags.StringVar(&tmpl, "t", "", "")
|
||||
flags.DurationVar(&wait, "wait", 2*time.Second, "")
|
||||
|
||||
flags.BoolVar(&openURL, "ui", false, "")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
@@ -183,9 +187,33 @@ func (c *DeploymentStatusCommand) Run(args []string) int {
|
||||
formatTime(time.Now()), limit(deploy.ID, length)))
|
||||
c.monitor(client, deploy.ID, meta.LastIndex, wait, verbose)
|
||||
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "deployment status",
|
||||
PathParams: map[string]string{
|
||||
"jobID": deploy.JobID,
|
||||
},
|
||||
OpenURL: openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
// Because this is before monitor, newline so we don't scrunch
|
||||
c.Ui.Warn("")
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
c.Ui.Output(c.Colorize().Color(formatDeployment(client, deploy, length)))
|
||||
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "deployment status",
|
||||
PathParams: map[string]string{
|
||||
"jobID": deploy.JobID,
|
||||
},
|
||||
OpenURL: openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,9 @@ Eval List Options:
|
||||
|
||||
-t
|
||||
Format and display evaluation using a Go template.
|
||||
|
||||
-ui
|
||||
Open the evaluations page in the browser.
|
||||
`
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
@@ -72,6 +75,7 @@ func (c *EvalListCommand) AutocompleteFlags() complete.Flags {
|
||||
"-status": complete.PredictAnything,
|
||||
"-per-page": complete.PredictAnything,
|
||||
"-page-token": complete.PredictAnything,
|
||||
"-ui": complete.PredictNothing,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -93,7 +97,7 @@ func (c *EvalListCommand) AutocompleteArgs() complete.Predictor {
|
||||
func (c *EvalListCommand) Name() string { return "eval list" }
|
||||
|
||||
func (c *EvalListCommand) Run(args []string) int {
|
||||
var monitor, verbose, json bool
|
||||
var monitor, verbose, json, openURL bool
|
||||
var perPage int
|
||||
var tmpl, pageToken, filter, filterJobID, filterStatus string
|
||||
|
||||
@@ -103,6 +107,7 @@ func (c *EvalListCommand) Run(args []string) int {
|
||||
flags.BoolVar(&verbose, "verbose", false, "")
|
||||
flags.BoolVar(&json, "json", false, "")
|
||||
flags.StringVar(&tmpl, "t", "", "")
|
||||
flags.BoolVar(&openURL, "ui", false, "")
|
||||
flags.IntVar(&perPage, "per-page", 0, "")
|
||||
flags.StringVar(&pageToken, "page-token", "", "")
|
||||
flags.StringVar(&filter, "filter", "", "")
|
||||
@@ -173,6 +178,14 @@ Results have been paginated. To get the next page run:
|
||||
%s -page-token %s`, argsWithoutPageToken(os.Args), qm.NextToken))
|
||||
}
|
||||
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "eval list",
|
||||
OpenURL: openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,9 @@ Eval Status Options:
|
||||
|
||||
-t
|
||||
Format and display evaluation using a Go template.
|
||||
|
||||
-ui
|
||||
Open the evaluation in the browser.
|
||||
`
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
@@ -59,6 +62,7 @@ func (c *EvalStatusCommand) AutocompleteFlags() complete.Flags {
|
||||
"-monitor": complete.PredictNothing,
|
||||
"-t": complete.PredictAnything,
|
||||
"-verbose": complete.PredictNothing,
|
||||
"-ui": complete.PredictNothing,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -84,7 +88,7 @@ func (c *EvalStatusCommand) AutocompleteArgs() complete.Predictor {
|
||||
func (c *EvalStatusCommand) Name() string { return "eval status" }
|
||||
|
||||
func (c *EvalStatusCommand) Run(args []string) int {
|
||||
var monitor, verbose, json bool
|
||||
var monitor, verbose, json, openURL bool
|
||||
var tmpl string
|
||||
|
||||
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
|
||||
@@ -93,7 +97,7 @@ func (c *EvalStatusCommand) Run(args []string) int {
|
||||
flags.BoolVar(&verbose, "verbose", false, "")
|
||||
flags.BoolVar(&json, "json", false, "")
|
||||
flags.StringVar(&tmpl, "t", "", "")
|
||||
|
||||
flags.BoolVar(&openURL, "ui", false, "")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
@@ -247,6 +251,17 @@ func (c *EvalStatusCommand) Run(args []string) int {
|
||||
}
|
||||
}
|
||||
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "eval status",
|
||||
PathParams: map[string]string{
|
||||
"evalID": eval.ID,
|
||||
},
|
||||
OpenURL: openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ package command
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -68,6 +69,9 @@ Dispatch Options:
|
||||
|
||||
-verbose
|
||||
Display full information.
|
||||
|
||||
-ui
|
||||
Open the dispatched job in the browser.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
@@ -83,6 +87,7 @@ func (c *JobDispatchCommand) AutocompleteFlags() complete.Flags {
|
||||
"-detach": complete.PredictNothing,
|
||||
"-idempotency-token": complete.PredictAnything,
|
||||
"-verbose": complete.PredictNothing,
|
||||
"-ui": complete.PredictNothing,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -113,7 +118,7 @@ func (c *JobDispatchCommand) AutocompleteArgs() complete.Predictor {
|
||||
func (c *JobDispatchCommand) Name() string { return "job dispatch" }
|
||||
|
||||
func (c *JobDispatchCommand) Run(args []string) int {
|
||||
var detach, verbose bool
|
||||
var detach, verbose, openURL bool
|
||||
var idempotencyToken string
|
||||
var meta []string
|
||||
var idPrefixTemplate string
|
||||
@@ -125,6 +130,7 @@ func (c *JobDispatchCommand) Run(args []string) int {
|
||||
flags.StringVar(&idempotencyToken, "idempotency-token", "", "")
|
||||
flags.Var((*flaghelper.StringFlag)(&meta), "meta", "")
|
||||
flags.StringVar(&idPrefixTemplate, "id-prefix-template", "", "")
|
||||
flags.BoolVar(&openURL, "ui", false, "")
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
@@ -220,5 +226,22 @@ func (c *JobDispatchCommand) Run(args []string) int {
|
||||
|
||||
c.Ui.Output("")
|
||||
mon := newMonitor(c.Ui, client, length)
|
||||
|
||||
// for hint purposes, need the dispatchedJobID to be escaped ("/" becomes "%2F")
|
||||
dispatchID := url.PathEscape(resp.DispatchedJobID)
|
||||
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "job dispatch",
|
||||
PathParams: map[string]string{
|
||||
"dispatchID": dispatchID,
|
||||
"namespace": namespace,
|
||||
},
|
||||
OpenURL: openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
// Because this is before monitor, newline so we don't scrunch
|
||||
c.Ui.Warn("")
|
||||
}
|
||||
return mon.monitor(resp.EvalID)
|
||||
}
|
||||
|
||||
@@ -97,6 +97,9 @@ Run Options:
|
||||
Output the JSON that would be submitted to the HTTP API without submitting
|
||||
the job.
|
||||
|
||||
-ui
|
||||
Open the job page in the browser.
|
||||
|
||||
-policy-override
|
||||
Sets the flag to force override any soft mandatory Sentinel policies.
|
||||
|
||||
@@ -147,6 +150,7 @@ func (c *JobRunCommand) AutocompleteFlags() complete.Flags {
|
||||
"-var": complete.PredictAnything,
|
||||
"-var-file": complete.PredictFiles("*.var"),
|
||||
"-eval-priority": complete.PredictNothing,
|
||||
"-ui": complete.PredictNothing,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -161,7 +165,7 @@ func (c *JobRunCommand) AutocompleteArgs() complete.Predictor {
|
||||
func (c *JobRunCommand) Name() string { return "job run" }
|
||||
|
||||
func (c *JobRunCommand) Run(args []string) int {
|
||||
var detach, verbose, output, override, preserveCounts bool
|
||||
var detach, verbose, output, override, preserveCounts, openURL bool
|
||||
var checkIndexStr, consulNamespace, vaultNamespace string
|
||||
var evalPriority int
|
||||
|
||||
@@ -180,6 +184,7 @@ func (c *JobRunCommand) Run(args []string) int {
|
||||
flagSet.Var(&c.JobGetter.Vars, "var", "")
|
||||
flagSet.Var(&c.JobGetter.VarFiles, "var-file", "")
|
||||
flagSet.IntVar(&evalPriority, "eval-priority", 0, "")
|
||||
flagSet.BoolVar(&openURL, "ui", false, "")
|
||||
|
||||
if err := flagSet.Parse(args); err != nil {
|
||||
return 1
|
||||
@@ -254,6 +259,7 @@ func (c *JobRunCommand) Run(args []string) int {
|
||||
}
|
||||
|
||||
c.Ui.Output(string(buf))
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -302,6 +308,11 @@ func (c *JobRunCommand) Run(args []string) int {
|
||||
|
||||
evalID := resp.EvalID
|
||||
|
||||
jobNamespace := c.Meta.namespace
|
||||
if jobNamespace == "" {
|
||||
jobNamespace = "default"
|
||||
}
|
||||
|
||||
// Check if we should enter monitor mode
|
||||
if detach || periodic || paramjob || multiregion {
|
||||
c.Ui.Output("Job registration successful")
|
||||
@@ -321,10 +332,36 @@ func (c *JobRunCommand) Run(args []string) int {
|
||||
c.Ui.Output("Evaluation ID: " + evalID)
|
||||
}
|
||||
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "job run",
|
||||
PathParams: map[string]string{
|
||||
"jobID": *job.ID,
|
||||
"namespace": jobNamespace,
|
||||
},
|
||||
OpenURL: openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// Detach was not specified, so start monitoring
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "job run",
|
||||
PathParams: map[string]string{
|
||||
"jobID": *job.ID,
|
||||
"namespace": jobNamespace,
|
||||
},
|
||||
OpenURL: openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
// Because this is before monitor, newline so we don't scrunch
|
||||
c.Ui.Warn("")
|
||||
}
|
||||
|
||||
mon := newMonitor(c.Ui, client, length)
|
||||
return mon.monitor(evalID)
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ type JobStatusCommand struct {
|
||||
verbose bool
|
||||
json bool
|
||||
tmpl string
|
||||
openURL bool
|
||||
}
|
||||
|
||||
// NamespacedID is a tuple of an ID and a namespace
|
||||
@@ -73,6 +74,9 @@ Status Options:
|
||||
|
||||
-verbose
|
||||
Display full information.
|
||||
|
||||
-ui
|
||||
Open the job status page in the browser.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
@@ -88,6 +92,7 @@ func (c *JobStatusCommand) AutocompleteFlags() complete.Flags {
|
||||
"-evals": complete.PredictNothing,
|
||||
"-short": complete.PredictNothing,
|
||||
"-verbose": complete.PredictNothing,
|
||||
"-ui": complete.PredictNothing,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -119,6 +124,7 @@ func (c *JobStatusCommand) Run(args []string) int {
|
||||
flags.BoolVar(&c.json, "json", false, "")
|
||||
flags.StringVar(&c.tmpl, "t", "", "")
|
||||
flags.BoolVar(&c.verbose, "verbose", false, "")
|
||||
flags.BoolVar(&c.openURL, "ui", false, "")
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
@@ -159,6 +165,13 @@ func (c *JobStatusCommand) Run(args []string) int {
|
||||
if len(jobs) == 0 {
|
||||
// No output if we have no jobs
|
||||
c.Ui.Output("No running jobs")
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "job status",
|
||||
OpenURL: c.openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
} else {
|
||||
if c.json || len(c.tmpl) > 0 {
|
||||
pairs := make([]NamespacedID, len(jobs))
|
||||
@@ -182,6 +195,13 @@ func (c *JobStatusCommand) Run(args []string) int {
|
||||
c.Ui.Output(out)
|
||||
} else {
|
||||
c.Ui.Output(createStatusListOutput(jobs, allNamespaces))
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "job status",
|
||||
OpenURL: c.openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
@@ -271,6 +291,17 @@ func (c *JobStatusCommand) Run(args []string) int {
|
||||
|
||||
// Exit early
|
||||
if short {
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "job status single",
|
||||
PathParams: map[string]string{
|
||||
"jobID": *job.ID,
|
||||
"namespace": *job.Namespace,
|
||||
},
|
||||
OpenURL: c.openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -292,6 +323,18 @@ func (c *JobStatusCommand) Run(args []string) int {
|
||||
}
|
||||
}
|
||||
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "job status single",
|
||||
PathParams: map[string]string{
|
||||
"jobID": *job.ID,
|
||||
"namespace": *job.Namespace,
|
||||
},
|
||||
OpenURL: c.openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
208
command/meta.go
208
command/meta.go
@@ -8,8 +8,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/cap/util"
|
||||
"github.com/hashicorp/cli"
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
@@ -58,6 +60,8 @@ type Meta struct {
|
||||
// token is used for ACLs to access privileged information
|
||||
token string
|
||||
|
||||
showCLIHints *bool
|
||||
|
||||
caCert string
|
||||
caPath string
|
||||
clientCert string
|
||||
@@ -272,6 +276,16 @@ func (m *Meta) SetupUi(args []string) {
|
||||
Ui: m.Ui,
|
||||
}
|
||||
}
|
||||
|
||||
// Check to see if the user has disabled hints via env var.
|
||||
showCLIHints := os.Getenv(EnvNomadCLIShowHints)
|
||||
if showCLIHints != "" {
|
||||
if show, err := strconv.ParseBool(showCLIHints); err == nil {
|
||||
m.showCLIHints = pointer.Of(show)
|
||||
} else {
|
||||
m.Ui.Warn(fmt.Sprintf("Invalid value %q for %s: %v", showCLIHints, EnvNomadCLIShowHints, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FormatWarnings returns a string with the warnings formatted for CLI output.
|
||||
@@ -451,3 +465,197 @@ type funcVar func(s string) error
|
||||
func (f funcVar) Set(s string) error { return f(s) }
|
||||
func (f funcVar) String() string { return "" }
|
||||
func (f funcVar) IsBoolFlag() bool { return false }
|
||||
|
||||
type UIRoute struct {
|
||||
Path string
|
||||
Description string
|
||||
}
|
||||
|
||||
type UIHintContext struct {
|
||||
Command string
|
||||
PathParams map[string]string
|
||||
OpenURL bool
|
||||
}
|
||||
|
||||
const (
|
||||
// Colors and styles
|
||||
resetter = "\033[0m"
|
||||
magenta = "\033[35m"
|
||||
blue = "\033[34m"
|
||||
bold = "\033[1m"
|
||||
|
||||
// Output formatting
|
||||
uiHintDelimiter = "\n\n==> "
|
||||
defaultHint = "See more in the Web UI:"
|
||||
)
|
||||
|
||||
var CommandUIRoutes = map[string]UIRoute{
|
||||
"server members": {
|
||||
Path: "/servers",
|
||||
Description: "View and manage Nomad servers",
|
||||
},
|
||||
"node status": {
|
||||
Path: "/clients",
|
||||
Description: "View and manage Nomad clients",
|
||||
},
|
||||
"node status single": {
|
||||
Path: "/clients/:nodeID",
|
||||
Description: "View client details and metrics",
|
||||
},
|
||||
"job status": {
|
||||
Path: "/jobs",
|
||||
Description: "View and manage Nomad jobs",
|
||||
},
|
||||
"job status single": {
|
||||
Path: "/jobs/:jobID@:namespace",
|
||||
Description: "View job details and metrics",
|
||||
},
|
||||
"job run": {
|
||||
Path: "/jobs/:jobID@:namespace",
|
||||
Description: "View this job",
|
||||
},
|
||||
"alloc status": {
|
||||
Path: "/allocations/:allocID",
|
||||
Description: "View allocation details",
|
||||
},
|
||||
"var list": {
|
||||
Path: "/variables",
|
||||
Description: "View Nomad variables",
|
||||
},
|
||||
"var list prefix": {
|
||||
Path: "/variables/path/:prefix",
|
||||
Description: "View Nomad variables at this path",
|
||||
},
|
||||
"var get": {
|
||||
Path: "/variables/var/:path@:namespace",
|
||||
Description: "View variable details",
|
||||
},
|
||||
"var put": {
|
||||
Path: "/variables/var/:path@:namespace",
|
||||
Description: "View variable details",
|
||||
},
|
||||
"job dispatch": {
|
||||
Path: "/jobs/:dispatchID@:namespace",
|
||||
Description: "View this job",
|
||||
},
|
||||
"eval list": {
|
||||
Path: "/evaluations",
|
||||
Description: "View evaluations",
|
||||
},
|
||||
"eval status": {
|
||||
Path: "/evaluations?currentEval=:evalID",
|
||||
Description: "View evaluation details",
|
||||
},
|
||||
"deployment status": {
|
||||
Path: "/jobs/:jobID/deployments",
|
||||
Description: "View all deployments for this job",
|
||||
},
|
||||
}
|
||||
|
||||
func (m *Meta) formatUIHint(url string, description string) string {
|
||||
if description == "" {
|
||||
description = defaultHint
|
||||
}
|
||||
|
||||
description = fmt.Sprintf("%s in the Web UI:", description)
|
||||
|
||||
// Basic version without colors
|
||||
hint := fmt.Sprintf("%s%s %s", uiHintDelimiter, description, url)
|
||||
|
||||
// If colors are disabled, return basic version
|
||||
_, coloredUi := m.Ui.(*cli.ColoredUi)
|
||||
if m.noColor || !coloredUi {
|
||||
return hint
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%[1]s%[2]s%[3]s%[4]s%[5]s %[6]s%[7]s%[8]s",
|
||||
bold,
|
||||
magenta,
|
||||
uiHintDelimiter[1:], // "==> "
|
||||
description,
|
||||
resetter,
|
||||
blue,
|
||||
url,
|
||||
resetter,
|
||||
)
|
||||
}
|
||||
|
||||
func (m *Meta) buildUIPath(route UIRoute, params map[string]string) (string, error) {
|
||||
client, err := m.Client()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error getting client config: %v", err)
|
||||
}
|
||||
|
||||
path := route.Path
|
||||
for k, v := range params {
|
||||
path = strings.ReplaceAll(path, fmt.Sprintf(":%s", k), v)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/ui%s", client.Address(), path), nil
|
||||
}
|
||||
|
||||
func (m *Meta) showUIPath(ctx UIHintContext) (string, error) {
|
||||
route, exists := CommandUIRoutes[ctx.Command]
|
||||
if !exists {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
url, err := m.buildUIPath(route, ctx.PathParams)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if ctx.OpenURL {
|
||||
if err := util.OpenURL(url); err != nil {
|
||||
m.Ui.Warn(fmt.Sprintf("Failed to open browser: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
if m.uiHintsDisabled() {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return m.formatUIHint(url, route.Description), nil
|
||||
}
|
||||
|
||||
func (m *Meta) uiHintsDisabled() bool {
|
||||
// Either the local env var is set to false,
|
||||
// or the agent config is set to false nad the local config isn't set to true
|
||||
|
||||
// First check if the user/env var is set to false. If it is, return early.
|
||||
if m.showCLIHints != nil && !*m.showCLIHints {
|
||||
return true
|
||||
}
|
||||
|
||||
// Next, check if the agent config is set to false. If it is, return early.
|
||||
client, err := m.Client()
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
agent, err := client.Agent().Self()
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
agentConfig := agent.Config
|
||||
agentUIConfig, ok := agentConfig["UI"].(map[string]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
agentShowCLIHints, ok := agentUIConfig["ShowCLIHints"].(bool)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if !agentShowCLIHints {
|
||||
// check to see if env var is set to true, overriding the agent setting
|
||||
if m.showCLIHints != nil && *m.showCLIHints {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package command
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/hashicorp/cli"
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
@@ -250,3 +252,345 @@ func TestMeta_JobByPrefix(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeta_ShowUIPath(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Create a test server with UI enabled but CLI URL links disabled
|
||||
server, client, url := testServer(t, true, func(c *agent.Config) {
|
||||
c.UI.ShowCLIHints = pointer.Of(true)
|
||||
})
|
||||
defer server.Shutdown()
|
||||
waitForNodes(t, client)
|
||||
|
||||
m := &Meta{
|
||||
Ui: cli.NewMockUi(),
|
||||
flagAddress: url,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
context UIHintContext
|
||||
expectedURL string
|
||||
expectedOpened bool
|
||||
}{
|
||||
{
|
||||
name: "server members",
|
||||
context: UIHintContext{
|
||||
Command: "server members",
|
||||
},
|
||||
expectedURL: url + "/ui/servers",
|
||||
},
|
||||
{
|
||||
name: "node status (many)",
|
||||
context: UIHintContext{
|
||||
Command: "node status",
|
||||
},
|
||||
expectedURL: url + "/ui/clients",
|
||||
},
|
||||
{
|
||||
name: "node status (single)",
|
||||
context: UIHintContext{
|
||||
Command: "node status single",
|
||||
PathParams: map[string]string{
|
||||
"nodeID": "node-1",
|
||||
},
|
||||
},
|
||||
expectedURL: url + "/ui/clients/node-1",
|
||||
},
|
||||
{
|
||||
name: "job status (many)",
|
||||
context: UIHintContext{
|
||||
Command: "job status",
|
||||
},
|
||||
expectedURL: url + "/ui/jobs",
|
||||
},
|
||||
{
|
||||
name: "job status (single)",
|
||||
context: UIHintContext{
|
||||
Command: "job status single",
|
||||
PathParams: map[string]string{
|
||||
"jobID": "example-job",
|
||||
"namespace": "default",
|
||||
},
|
||||
},
|
||||
expectedURL: url + "/ui/jobs/example-job@default",
|
||||
},
|
||||
{
|
||||
name: "job run (default ns)",
|
||||
context: UIHintContext{
|
||||
Command: "job run",
|
||||
PathParams: map[string]string{
|
||||
"jobID": "example-job",
|
||||
"namespace": "default",
|
||||
},
|
||||
},
|
||||
expectedURL: url + "/ui/jobs/example-job@default",
|
||||
},
|
||||
{
|
||||
name: "job run (non-default ns)",
|
||||
context: UIHintContext{
|
||||
Command: "job run",
|
||||
PathParams: map[string]string{
|
||||
"jobID": "example-job",
|
||||
"namespace": "prod",
|
||||
},
|
||||
},
|
||||
expectedURL: url + "/ui/jobs/example-job@prod",
|
||||
},
|
||||
{
|
||||
name: "job dispatch (default ns)",
|
||||
context: UIHintContext{
|
||||
Command: "job dispatch",
|
||||
PathParams: map[string]string{
|
||||
"dispatchID": "dispatch-1",
|
||||
"namespace": "default",
|
||||
},
|
||||
},
|
||||
expectedURL: url + "/ui/jobs/dispatch-1@default",
|
||||
},
|
||||
{
|
||||
name: "job dispatch (non-default ns)",
|
||||
context: UIHintContext{
|
||||
Command: "job dispatch",
|
||||
PathParams: map[string]string{
|
||||
"dispatchID": "dispatch-1",
|
||||
"namespace": "toronto",
|
||||
},
|
||||
},
|
||||
expectedURL: url + "/ui/jobs/dispatch-1@toronto",
|
||||
},
|
||||
{
|
||||
name: "eval list",
|
||||
context: UIHintContext{
|
||||
Command: "eval list",
|
||||
},
|
||||
expectedURL: url + "/ui/evaluations",
|
||||
},
|
||||
{
|
||||
name: "eval status",
|
||||
context: UIHintContext{
|
||||
Command: "eval status",
|
||||
PathParams: map[string]string{
|
||||
"evalID": "eval-1",
|
||||
},
|
||||
},
|
||||
expectedURL: url + "/ui/evaluations?currentEval=eval-1",
|
||||
},
|
||||
{
|
||||
name: "deployment status",
|
||||
context: UIHintContext{
|
||||
Command: "deployment status",
|
||||
PathParams: map[string]string{
|
||||
"jobID": "example-job",
|
||||
},
|
||||
},
|
||||
expectedURL: url + "/ui/jobs/example-job/deployments",
|
||||
},
|
||||
{
|
||||
name: "var list (root)",
|
||||
context: UIHintContext{
|
||||
Command: "var list",
|
||||
},
|
||||
expectedURL: url + "/ui/variables",
|
||||
},
|
||||
{
|
||||
name: "var list (path)",
|
||||
context: UIHintContext{
|
||||
Command: "var list prefix",
|
||||
PathParams: map[string]string{
|
||||
"prefix": "foo",
|
||||
},
|
||||
},
|
||||
expectedURL: url + "/ui/variables/path/foo",
|
||||
},
|
||||
{
|
||||
name: "var get",
|
||||
context: UIHintContext{
|
||||
Command: "var get",
|
||||
PathParams: map[string]string{
|
||||
"path": "foo",
|
||||
"namespace": "default",
|
||||
},
|
||||
},
|
||||
expectedURL: url + "/ui/variables/var/foo@default",
|
||||
},
|
||||
{
|
||||
name: "var put",
|
||||
context: UIHintContext{
|
||||
Command: "var put",
|
||||
PathParams: map[string]string{
|
||||
"path": "foo",
|
||||
"namespace": "default",
|
||||
},
|
||||
},
|
||||
expectedURL: url + "/ui/variables/var/foo@default",
|
||||
},
|
||||
{
|
||||
name: "alloc status",
|
||||
context: UIHintContext{
|
||||
Command: "alloc status",
|
||||
PathParams: map[string]string{
|
||||
"allocID": "alloc-1",
|
||||
},
|
||||
},
|
||||
expectedURL: url + "/ui/allocations/alloc-1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
route := CommandUIRoutes[tc.context.Command]
|
||||
expectedHint := fmt.Sprintf("\n\n==> %s in the Web UI: %s", route.Description, tc.expectedURL)
|
||||
|
||||
hint, err := m.showUIPath(tc.context)
|
||||
must.NoError(t, err)
|
||||
must.Eq(t, expectedHint, hint)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeta_ShowUIPath_ShowCLIHintsEnabled(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Create a test server with UI enabled and CLI URL links enabled
|
||||
server, client, url := testServer(t, true, func(c *agent.Config) {
|
||||
c.UI.ShowCLIHints = pointer.Of(true)
|
||||
})
|
||||
defer server.Shutdown()
|
||||
waitForNodes(t, client)
|
||||
|
||||
m := &Meta{
|
||||
Ui: cli.NewMockUi(),
|
||||
flagAddress: url,
|
||||
}
|
||||
|
||||
hint, err := m.showUIPath(UIHintContext{
|
||||
Command: "job status",
|
||||
})
|
||||
must.NoError(t, err)
|
||||
|
||||
must.StrContains(t, hint, url+"/ui/jobs")
|
||||
}
|
||||
|
||||
func TestMeta_ShowUIPath_ShowCLIHintsDisabled(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Create a test server with UI enabled and CLI URL links disabled
|
||||
server, client, url := testServer(t, true, func(c *agent.Config) {
|
||||
c.UI.ShowCLIHints = pointer.Of(false)
|
||||
})
|
||||
defer server.Shutdown()
|
||||
waitForNodes(t, client)
|
||||
|
||||
m := &Meta{
|
||||
Ui: cli.NewMockUi(),
|
||||
flagAddress: url,
|
||||
}
|
||||
|
||||
hint, err := m.showUIPath(UIHintContext{
|
||||
Command: "job status",
|
||||
})
|
||||
must.NoError(t, err)
|
||||
|
||||
must.StrNotContains(t, hint, url+"/ui/jobs")
|
||||
}
|
||||
|
||||
func TestMeta_ShowUIPath_EnvVarOverride(t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
envValue string
|
||||
serverEnabled bool
|
||||
expectHints bool
|
||||
}{
|
||||
{
|
||||
name: "env var true overrides server false",
|
||||
envValue: "true",
|
||||
serverEnabled: false,
|
||||
expectHints: true,
|
||||
},
|
||||
{
|
||||
name: "env var false overrides server true",
|
||||
envValue: "false",
|
||||
serverEnabled: true,
|
||||
expectHints: false,
|
||||
},
|
||||
{
|
||||
name: "empty env var falls back to server true",
|
||||
envValue: "",
|
||||
serverEnabled: true,
|
||||
expectHints: true,
|
||||
},
|
||||
{
|
||||
name: "empty env var falls back to server false",
|
||||
envValue: "",
|
||||
serverEnabled: false,
|
||||
expectHints: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc // capture range variable
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
// Set environment variable
|
||||
if tc.envValue != "" {
|
||||
t.Setenv("NOMAD_CLI_SHOW_HINTS", tc.envValue)
|
||||
}
|
||||
|
||||
// Create a test server with UI enabled and CLI hints as per test case
|
||||
server, client, url := testServer(t, true, func(c *agent.Config) {
|
||||
c.UI.ShowCLIHints = pointer.Of(tc.serverEnabled)
|
||||
})
|
||||
defer server.Shutdown()
|
||||
waitForNodes(t, client)
|
||||
|
||||
m := &Meta{
|
||||
Ui: cli.NewMockUi(),
|
||||
flagAddress: url,
|
||||
}
|
||||
m.SetupUi([]string{})
|
||||
|
||||
hint, err := m.showUIPath(UIHintContext{
|
||||
Command: "job status",
|
||||
})
|
||||
must.NoError(t, err)
|
||||
|
||||
if tc.expectHints {
|
||||
must.StrContains(t, hint, url+"/ui/jobs")
|
||||
} else {
|
||||
must.StrNotContains(t, hint, url+"/ui/jobs")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeta_ShowUIPath_BrowserOpening(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
server, client, url := testServer(t, true, func(c *agent.Config) {
|
||||
c.UI.ShowCLIHints = pointer.Of(true)
|
||||
})
|
||||
defer server.Shutdown()
|
||||
waitForNodes(t, client)
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
|
||||
m := &Meta{
|
||||
Ui: ui,
|
||||
flagAddress: url,
|
||||
}
|
||||
|
||||
hint, err := m.showUIPath(UIHintContext{
|
||||
Command: "job status",
|
||||
OpenURL: true,
|
||||
})
|
||||
must.NoError(t, err)
|
||||
must.StrContains(t, hint, url+"/ui/jobs")
|
||||
|
||||
// Not a perfect test, but it's a start: make sure showUIPath isn't warning about
|
||||
// being unable to open the browser.
|
||||
must.StrNotContains(t, ui.ErrorWriter.String(), "Failed to open browser")
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ type NodeStatusCommand struct {
|
||||
pageToken string
|
||||
filter string
|
||||
tmpl string
|
||||
openURL bool
|
||||
}
|
||||
|
||||
func (c *NodeStatusCommand) Help() string {
|
||||
@@ -92,6 +93,9 @@ Node Status Options:
|
||||
-filter
|
||||
Specifies an expression used to filter query results.
|
||||
|
||||
-ui
|
||||
Open the node status page in the browser.
|
||||
|
||||
-os
|
||||
Display operating system name.
|
||||
|
||||
@@ -126,6 +130,7 @@ func (c *NodeStatusCommand) AutocompleteFlags() complete.Flags {
|
||||
"-os": complete.PredictAnything,
|
||||
"-quiet": complete.PredictAnything,
|
||||
"-verbose": complete.PredictNothing,
|
||||
"-ui": complete.PredictNothing,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -166,6 +171,7 @@ func (c *NodeStatusCommand) Run(args []string) int {
|
||||
flags.StringVar(&c.filter, "filter", "", "")
|
||||
flags.IntVar(&c.perPage, "per-page", 0, "")
|
||||
flags.StringVar(&c.pageToken, "page-token", "", "")
|
||||
flags.BoolVar(&c.openURL, "ui", false, "")
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
@@ -312,6 +318,14 @@ Results have been paginated. To get the next page run:
|
||||
%s -page-token %s`, argsWithoutPageToken(os.Args), qm.NextToken))
|
||||
}
|
||||
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: c.Name(),
|
||||
OpenURL: c.openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -369,6 +383,7 @@ Results have been paginated. To get the next page run:
|
||||
}
|
||||
|
||||
return c.formatNode(client, node)
|
||||
|
||||
}
|
||||
|
||||
func nodeDrivers(n *api.Node) []string {
|
||||
@@ -504,6 +519,17 @@ func (c *NodeStatusCommand) formatNode(client *api.Client, node *api.Node) int {
|
||||
basic = append(basic, fmt.Sprintf("Drivers|%s", strings.Join(nodeDrivers(node), ",")))
|
||||
c.Ui.Output(c.Colorize().Color(formatKV(basic)))
|
||||
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "node status single",
|
||||
PathParams: map[string]string{
|
||||
"nodeID": node.ID,
|
||||
},
|
||||
OpenURL: c.openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
|
||||
// Output alloc info
|
||||
if err := c.outputAllocInfo(node, nodeAllocs); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("%s", err))
|
||||
@@ -591,6 +617,17 @@ func (c *NodeStatusCommand) formatNode(client *api.Client, node *api.Node) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "node status single",
|
||||
PathParams: map[string]string{
|
||||
"nodeID": node.ID,
|
||||
},
|
||||
OpenURL: c.openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ Server Members Options:
|
||||
Show detailed information about each member. This dumps a raw set of tags
|
||||
which shows more information than the default output format.
|
||||
|
||||
-ui
|
||||
Open the servers page in the browser.
|
||||
|
||||
-json
|
||||
Output the latest information about each member in a JSON format.
|
||||
|
||||
@@ -55,6 +58,7 @@ func (c *ServerMembersCommand) AutocompleteFlags() complete.Flags {
|
||||
"-verbose": complete.PredictNothing,
|
||||
"-json": complete.PredictNothing,
|
||||
"-t": complete.PredictAnything,
|
||||
"-ui": complete.PredictNothing,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -69,7 +73,7 @@ func (c *ServerMembersCommand) Synopsis() string {
|
||||
func (c *ServerMembersCommand) Name() string { return "server members" }
|
||||
|
||||
func (c *ServerMembersCommand) Run(args []string) int {
|
||||
var detailed, verbose, json bool
|
||||
var detailed, verbose, json, openURL bool
|
||||
var tmpl string
|
||||
|
||||
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
|
||||
@@ -77,6 +81,7 @@ func (c *ServerMembersCommand) Run(args []string) int {
|
||||
flags.BoolVar(&detailed, "detailed", false, "Show detailed output")
|
||||
flags.BoolVar(&verbose, "verbose", false, "Show detailed output")
|
||||
flags.BoolVar(&json, "json", false, "")
|
||||
flags.BoolVar(&openURL, "ui", false, "Open the servers page in the browser")
|
||||
flags.StringVar(&tmpl, "t", "", "")
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
@@ -146,6 +151,14 @@ func (c *ServerMembersCommand) Run(args []string) int {
|
||||
// Dump the list
|
||||
c.Ui.Output(columnize.SimpleFormat(out))
|
||||
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: c.Name(),
|
||||
OpenURL: openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
|
||||
// If there were leader errors display a warning
|
||||
if leaderErr != nil {
|
||||
c.Ui.Output("")
|
||||
|
||||
@@ -17,6 +17,7 @@ type StatusCommand struct {
|
||||
|
||||
// Placeholder bool to allow passing of verbose flags to subcommands.
|
||||
verbose bool
|
||||
openURL bool
|
||||
}
|
||||
|
||||
func (c *StatusCommand) Help() string {
|
||||
@@ -38,6 +39,9 @@ Status Options:
|
||||
|
||||
-verbose
|
||||
Display full information.
|
||||
|
||||
-ui
|
||||
Open the status page in the browser.
|
||||
`
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
@@ -51,6 +55,7 @@ func (c *StatusCommand) AutocompleteFlags() complete.Flags {
|
||||
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
|
||||
complete.Flags{
|
||||
"-verbose": complete.PredictNothing,
|
||||
"-ui": complete.PredictNothing,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -84,7 +89,7 @@ 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
|
||||
|
||||
@@ -25,6 +25,9 @@ func testServer(t *testing.T, runClient bool, cb func(*agent.Config)) (*agent.Te
|
||||
a := agent.NewTestAgent(t, t.Name(), func(config *agent.Config) {
|
||||
config.Client.Enabled = runClient
|
||||
|
||||
// Disable UI hints in test by default
|
||||
config.UI.ShowCLIHints = pointer.Of(false)
|
||||
|
||||
if cb != nil {
|
||||
cb(config)
|
||||
}
|
||||
|
||||
@@ -49,6 +49,9 @@ Get Options:
|
||||
-template
|
||||
Template to render output with. Required when output is "go-template".
|
||||
|
||||
-ui
|
||||
Open the variable page in the browser.
|
||||
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
@@ -58,6 +61,7 @@ func (c *VarGetCommand) AutocompleteFlags() complete.Flags {
|
||||
complete.Flags{
|
||||
"-out": complete.PredictSet("go-template", "hcl", "json", "none", "table"),
|
||||
"-template": complete.PredictAnything,
|
||||
"-ui": complete.PredictNothing,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -74,12 +78,13 @@ func (c *VarGetCommand) Name() string { return "var get" }
|
||||
|
||||
func (c *VarGetCommand) Run(args []string) int {
|
||||
var out, item string
|
||||
|
||||
var openURL bool
|
||||
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
|
||||
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||
|
||||
flags.StringVar(&item, "item", "", "")
|
||||
flags.StringVar(&c.tmpl, "template", "", "")
|
||||
flags.BoolVar(&openURL, "ui", false, "")
|
||||
|
||||
if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
|
||||
flags.StringVar(&c.outFmt, "out", "table", "")
|
||||
@@ -161,10 +166,35 @@ func (c *VarGetCommand) Run(args []string) int {
|
||||
default:
|
||||
// the renderSVAsUiTable func writes directly to the ui and doesn't error.
|
||||
renderSVAsUiTable(sv, c)
|
||||
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "var put",
|
||||
PathParams: map[string]string{
|
||||
"path": path,
|
||||
"namespace": sv.Namespace,
|
||||
},
|
||||
OpenURL: openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
c.Ui.Output(out)
|
||||
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "var get",
|
||||
PathParams: map[string]string{
|
||||
"path": path,
|
||||
"namespace": sv.Namespace,
|
||||
},
|
||||
OpenURL: openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,9 @@ List Options:
|
||||
Template to render output with. Required when format is "go-template",
|
||||
invalid for other formats.
|
||||
|
||||
-ui
|
||||
Open the variable list page in the browser.
|
||||
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
@@ -74,6 +77,7 @@ func (c *VarListCommand) AutocompleteFlags() complete.Flags {
|
||||
complete.Flags{
|
||||
"-out": complete.PredictSet("go-template", "json", "terse", "table"),
|
||||
"-template": complete.PredictAnything,
|
||||
"-ui": complete.PredictNothing,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -90,6 +94,7 @@ func (c *VarListCommand) Name() string { return "var list" }
|
||||
func (c *VarListCommand) Run(args []string) int {
|
||||
var perPage int
|
||||
var pageToken, filter, prefix string
|
||||
var openURL bool
|
||||
|
||||
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
|
||||
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||
@@ -98,6 +103,7 @@ func (c *VarListCommand) Run(args []string) int {
|
||||
flags.IntVar(&perPage, "per-page", 0, "")
|
||||
flags.StringVar(&pageToken, "page-token", "", "")
|
||||
flags.StringVar(&filter, "filter", "", "")
|
||||
flags.BoolVar(&openURL, "ui", false, "")
|
||||
|
||||
if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
|
||||
flags.StringVar(&c.outFmt, "out", "table", "")
|
||||
@@ -211,6 +217,27 @@ func (c *VarListCommand) Run(args []string) int {
|
||||
c.Ui.Warn(fmt.Sprintf("Next page token: %s", qm.NextToken))
|
||||
}
|
||||
|
||||
if prefix != "" {
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "var list prefix",
|
||||
PathParams: map[string]string{
|
||||
"prefix": prefix,
|
||||
},
|
||||
OpenURL: openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
} else {
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "var list",
|
||||
OpenURL: openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,9 @@ Var put Options:
|
||||
Provides additional information via standard error to preserve standard
|
||||
output (stdout) for redirected output.
|
||||
|
||||
-ui
|
||||
Open the variable page in the browser.
|
||||
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
@@ -111,6 +114,7 @@ func (c *VarPutCommand) AutocompleteFlags() complete.Flags {
|
||||
complete.Flags{
|
||||
"-in": complete.PredictSet("hcl", "json"),
|
||||
"-out": complete.PredictSet("none", "hcl", "json", "go-template", "table"),
|
||||
"-ui": complete.PredictNothing,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -126,7 +130,7 @@ func (c *VarPutCommand) Synopsis() string {
|
||||
func (c *VarPutCommand) Name() string { return "var put" }
|
||||
|
||||
func (c *VarPutCommand) Run(args []string) int {
|
||||
var force, enforce, doVerbose bool
|
||||
var force, enforce, doVerbose, openURL bool
|
||||
var path, checkIndexStr string
|
||||
var checkIndex uint64
|
||||
var err error
|
||||
@@ -139,7 +143,7 @@ func (c *VarPutCommand) Run(args []string) int {
|
||||
flags.StringVar(&checkIndexStr, "check-index", "", "")
|
||||
flags.StringVar(&c.inFmt, "in", "json", "")
|
||||
flags.StringVar(&c.tmpl, "template", "", "")
|
||||
|
||||
flags.BoolVar(&openURL, "ui", false, "")
|
||||
if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
|
||||
flags.StringVar(&c.outFmt, "out", "none", "")
|
||||
} else {
|
||||
@@ -355,13 +359,46 @@ func (c *VarPutCommand) Run(args []string) int {
|
||||
// the renderSVAsUiTable func writes directly to the ui and doesn't error.
|
||||
verbose(successMsg)
|
||||
renderSVAsUiTable(sv, c)
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "var put",
|
||||
PathParams: map[string]string{
|
||||
"path": path,
|
||||
"namespace": sv.Namespace,
|
||||
},
|
||||
OpenURL: openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
return 0
|
||||
default:
|
||||
c.Ui.Output(successMsg)
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "var put",
|
||||
PathParams: map[string]string{
|
||||
"path": path,
|
||||
"namespace": sv.Namespace,
|
||||
},
|
||||
OpenURL: openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
verbose(successMsg)
|
||||
c.Ui.Output(out)
|
||||
hint, _ := c.Meta.showUIPath(UIHintContext{
|
||||
Command: "var put",
|
||||
PathParams: map[string]string{
|
||||
"path": path,
|
||||
"namespace": sv.Namespace,
|
||||
},
|
||||
OpenURL: openURL,
|
||||
})
|
||||
if hint != "" {
|
||||
c.Ui.Warn(hint)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
)
|
||||
|
||||
// UIConfig contains the operator configuration of the web UI
|
||||
@@ -28,6 +30,9 @@ type UIConfig struct {
|
||||
|
||||
// Label configures UI label styles
|
||||
Label *LabelUIConfig `hcl:"label"`
|
||||
|
||||
// ShowCLIHints controls whether CLI commands that return URLs will output that url as a hint
|
||||
ShowCLIHints *bool `hcl:"show_cli_hints"`
|
||||
}
|
||||
|
||||
// only covers the elements of
|
||||
@@ -142,6 +147,7 @@ func DefaultUIConfig() *UIConfig {
|
||||
Vault: &VaultUIConfig{},
|
||||
Label: &LabelUIConfig{},
|
||||
ContentSecurityPolicy: DefaultCSPConfig(),
|
||||
ShowCLIHints: pointer.Of(true),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +183,10 @@ func (old *UIConfig) Merge(other *UIConfig) *UIConfig {
|
||||
result.Label = result.Label.Merge(other.Label)
|
||||
result.ContentSecurityPolicy = result.ContentSecurityPolicy.Merge(other.ContentSecurityPolicy)
|
||||
|
||||
if other.ShowCLIHints != nil {
|
||||
result.ShowCLIHints = other.ShowCLIHints
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ When ACLs are enabled, this command requires a token with the `read-job` and
|
||||
- `-verbose`: Show full information.
|
||||
- `-json` : Output the allocation in its JSON format.
|
||||
- `-t` : Format and display the allocation using a Go template.
|
||||
- `-ui` : Open the allocation status page in the browser.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ capability for the deployment's namespace.
|
||||
- `-monitor`: Enter monitor mode to poll for updates to the deployment status.
|
||||
- `-wait`: How long to wait before polling an update, used in conjunction with monitor
|
||||
mode. Defaults to 2s.
|
||||
- `-ui`: Open the deployment page in the browser.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -135,8 +136,8 @@ $ nomad deployment status -monitor e45
|
||||
web false 1 1 1 0 2021-06-09T15:59:58-07:00
|
||||
```
|
||||
|
||||
**Please note**: The library used for updating terminal output in place currently isn't fully
|
||||
Windows compatible so there may be some formatting differences (different margins, no spinner
|
||||
**Please note**: The library used for updating terminal output in place currently isn't fully
|
||||
Windows compatible so there may be some formatting differences (different margins, no spinner
|
||||
indicating deployment is in progress).
|
||||
|
||||
[`auto_revert`]: /nomad/docs/job-specification/update#auto_revert
|
||||
[`auto_revert`]: /nomad/docs/job-specification/update#auto_revert
|
||||
|
||||
@@ -34,6 +34,7 @@ capability for the requested namespace.
|
||||
- `-status`: Only show evaluations with this status.
|
||||
- `-json`: Output the evaluation in its JSON format.
|
||||
- `-t`: Format and display evaluation using a Go template.
|
||||
- `-ui`: Open the evaluations page in the browser.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ indicated by exit code 1.
|
||||
-json`. In Nomad 1.4.0 the behavior of this option will change to
|
||||
output only the selected evaluation in JSON.
|
||||
- `-t` : Format and display evaluation using a Go template.
|
||||
- `-ui`: Open the evaluation in the browser.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -94,6 +94,8 @@ flags.
|
||||
|
||||
- `NOMAD_CLI_NO_COLOR` - Disables colored command output.
|
||||
|
||||
- `NOMAD_CLI_SHOW_HINTS` - Enables ui-hints in common CLI command output.
|
||||
|
||||
#### mTLS Environment Variables
|
||||
|
||||
- `NOMAD_CLIENT_CERT` - Path to a PEM encoded client certificate for TLS
|
||||
|
||||
@@ -76,6 +76,8 @@ dispatching parameterized jobs.
|
||||
|
||||
- `-verbose`: Show full information.
|
||||
|
||||
- `-ui`: Open the dispatched job in the browser.
|
||||
|
||||
## Examples
|
||||
|
||||
Dispatch against a parameterized job with the ID "video-encode" and
|
||||
|
||||
@@ -97,6 +97,8 @@ that volume.
|
||||
|
||||
- `-verbose`: Show full information.
|
||||
|
||||
- `-ui`: Open the job page in the browser.
|
||||
|
||||
## Examples
|
||||
|
||||
Schedule the job contained in the file `example.nomad.hcl`, monitoring placement and deployment:
|
||||
|
||||
@@ -53,6 +53,8 @@ run the command with a job prefix instead of the exact job ID.
|
||||
- `-verbose`: Show full information. Allocation create and modify times are
|
||||
shown in `yyyy/mm/dd hh:mm:ss` format.
|
||||
|
||||
- `-ui`: Open the job status page in the browser.
|
||||
|
||||
## Examples
|
||||
|
||||
List of all jobs:
|
||||
|
||||
@@ -61,6 +61,8 @@ capability.
|
||||
|
||||
- `-t` : Format and display node using a Go template.
|
||||
|
||||
- `-ui` : Open the node status page in the browser
|
||||
|
||||
## Examples
|
||||
|
||||
List view:
|
||||
|
||||
@@ -40,6 +40,8 @@ capability.
|
||||
|
||||
- `-t`: Format and display the server memebers using a Go template.
|
||||
|
||||
- `-ui`: Open the servers page in the browser.
|
||||
|
||||
## Examples
|
||||
|
||||
Default view:
|
||||
@@ -101,4 +103,4 @@ Or use the `-t` flag to format and display the server members information using
|
||||
```shell-session
|
||||
$ nomad server members -t '{{range .}}{{printf "%s: %s" .Name .Status }}{{end}}'
|
||||
bacon-mac.global: alive
|
||||
```
|
||||
```
|
||||
|
||||
@@ -39,6 +39,8 @@ documentation for details.
|
||||
- `-template` `(string: "")` Template to render output with. Required when
|
||||
output is "go-template".
|
||||
|
||||
- `-ui`: Open the variable page in the browser.
|
||||
|
||||
## Examples
|
||||
|
||||
Retrieve the variable stored at path "secret/creds":
|
||||
|
||||
@@ -52,6 +52,8 @@ documentation for details.
|
||||
- `-template` `(string: "")` Template to render output with. Required when
|
||||
output is "go-template".
|
||||
|
||||
- `-ui`: Open the variable list page in the browser.
|
||||
|
||||
## Examples
|
||||
|
||||
List values under the key "nomad/jobs":
|
||||
|
||||
@@ -83,6 +83,8 @@ taking the sum of the length in bytes of all of the unencrypted keys and values.
|
||||
- `-verbose`: Provides additional information via standard error to preserve
|
||||
standard output (stdout) for redirected output.
|
||||
|
||||
- `-ui`: Open the variable page in the browser.
|
||||
|
||||
## Examples
|
||||
|
||||
Writes the data to the path "secret/creds":
|
||||
|
||||
@@ -60,6 +60,11 @@ and the configuration is individual to each agent.
|
||||
- `label` <code>([Label]: nil)</code> - Configures a user-defined
|
||||
label to display in the Nomad Web UI header.
|
||||
|
||||
- `show_cli_hints` `(bool: true)` - Controls whether CLI commands display hints
|
||||
about equivalent UI pages. For example, when running `nomad server members`,
|
||||
the CLI shows a message indicating where to find server information in
|
||||
the web UI. Set to `false` to disable these hints.
|
||||
|
||||
## `content_security_policy` Parameters
|
||||
|
||||
The `content_security_policy` block configures the HTTP
|
||||
@@ -108,10 +113,10 @@ header sent with the web UI response.
|
||||
- `text` `(string: "")` - Specifies the text of the label that will be
|
||||
displayed in the header of the Web UI.
|
||||
- `background_color` `(string: "")` - The background color of the label to
|
||||
be displayed. The Web UI will default to a black background. HEX values
|
||||
be displayed. The Web UI defaults to a black background. HEX values
|
||||
may be used.
|
||||
- `text_color` `(string: "")` - The text color of the label to be displayed.
|
||||
The Web UI will default to white text. HEX values may be used.
|
||||
The Web UI defaults to white text. HEX values may be used.
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user