diff --git a/.changelog/24454.txt b/.changelog/24454.txt new file mode 100644 index 000000000..33bac43bf --- /dev/null +++ b/.changelog/24454.txt @@ -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 +``` diff --git a/command/alloc_status.go b/command/alloc_status.go index 1b15bc89a..b9301e8c8 100644 --- a/command/alloc_status.go +++ b/command/alloc_status.go @@ -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 } diff --git a/command/commands.go b/command/commands.go index 9b078de20..983f0299a 100644 --- a/command/commands.go +++ b/command/commands.go @@ -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 diff --git a/command/deployment_status.go b/command/deployment_status.go index 318b7f898..af11fb6ed 100644 --- a/command/deployment_status.go +++ b/command/deployment_status.go @@ -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 } diff --git a/command/eval_list.go b/command/eval_list.go index 35e495675..305b03a1d 100644 --- a/command/eval_list.go +++ b/command/eval_list.go @@ -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 } diff --git a/command/eval_status.go b/command/eval_status.go index 99936591e..2cc1fcd61 100644 --- a/command/eval_status.go +++ b/command/eval_status.go @@ -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 } diff --git a/command/job_dispatch.go b/command/job_dispatch.go index 4b14fc059..2b6f66210 100644 --- a/command/job_dispatch.go +++ b/command/job_dispatch.go @@ -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) } diff --git a/command/job_run.go b/command/job_run.go index b1c47baaa..21a3b1623 100644 --- a/command/job_run.go +++ b/command/job_run.go @@ -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) diff --git a/command/job_status.go b/command/job_status.go index 3ebf483e1..a23013ca4 100644 --- a/command/job_status.go +++ b/command/job_status.go @@ -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 } diff --git a/command/meta.go b/command/meta.go index 85b058e74..f1eb47148 100644 --- a/command/meta.go +++ b/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 +} diff --git a/command/meta_test.go b/command/meta_test.go index 208dd15cb..758b69fb6 100644 --- a/command/meta_test.go +++ b/command/meta_test.go @@ -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") +} diff --git a/command/node_status.go b/command/node_status.go index 9538e9062..7125c0af4 100644 --- a/command/node_status.go +++ b/command/node_status.go @@ -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 } diff --git a/command/server_members.go b/command/server_members.go index a7f0f174a..90887ee56 100644 --- a/command/server_members.go +++ b/command/server_members.go @@ -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("") diff --git a/command/status.go b/command/status.go index 4c175d616..906ad1cf4 100644 --- a/command/status.go +++ b/command/status.go @@ -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 diff --git a/command/testing_test.go b/command/testing_test.go index 66f70e58f..ae5e61862 100644 --- a/command/testing_test.go +++ b/command/testing_test.go @@ -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) } diff --git a/command/var_get.go b/command/var_get.go index c101cf48f..be6c7992b 100644 --- a/command/var_get.go +++ b/command/var_get.go @@ -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 } diff --git a/command/var_list.go b/command/var_list.go index 141937f4a..0443963cc 100644 --- a/command/var_list.go +++ b/command/var_list.go @@ -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 } diff --git a/command/var_put.go b/command/var_put.go index c54a89344..265901a7d 100644 --- a/command/var_put.go +++ b/command/var_put.go @@ -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 } diff --git a/nomad/structs/config/ui.go b/nomad/structs/config/ui.go index b201acfcc..0cd547e56 100644 --- a/nomad/structs/config/ui.go +++ b/nomad/structs/config/ui.go @@ -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 } diff --git a/website/content/docs/commands/alloc/status.mdx b/website/content/docs/commands/alloc/status.mdx index 966bbf973..0aa7066fe 100644 --- a/website/content/docs/commands/alloc/status.mdx +++ b/website/content/docs/commands/alloc/status.mdx @@ -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 diff --git a/website/content/docs/commands/deployment/status.mdx b/website/content/docs/commands/deployment/status.mdx index 86542733e..0f4ec79ad 100644 --- a/website/content/docs/commands/deployment/status.mdx +++ b/website/content/docs/commands/deployment/status.mdx @@ -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 \ No newline at end of file +[`auto_revert`]: /nomad/docs/job-specification/update#auto_revert diff --git a/website/content/docs/commands/eval/list.mdx b/website/content/docs/commands/eval/list.mdx index 1081d2f52..17f63ac0b 100644 --- a/website/content/docs/commands/eval/list.mdx +++ b/website/content/docs/commands/eval/list.mdx @@ -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 diff --git a/website/content/docs/commands/eval/status.mdx b/website/content/docs/commands/eval/status.mdx index 663c3a562..94078b9bb 100644 --- a/website/content/docs/commands/eval/status.mdx +++ b/website/content/docs/commands/eval/status.mdx @@ -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 diff --git a/website/content/docs/commands/index.mdx b/website/content/docs/commands/index.mdx index 061723994..cd19bcd07 100644 --- a/website/content/docs/commands/index.mdx +++ b/website/content/docs/commands/index.mdx @@ -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 diff --git a/website/content/docs/commands/job/dispatch.mdx b/website/content/docs/commands/job/dispatch.mdx index 235cc952c..b52c79269 100644 --- a/website/content/docs/commands/job/dispatch.mdx +++ b/website/content/docs/commands/job/dispatch.mdx @@ -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 diff --git a/website/content/docs/commands/job/run.mdx b/website/content/docs/commands/job/run.mdx index 8c0b556ae..f657bfb7d 100644 --- a/website/content/docs/commands/job/run.mdx +++ b/website/content/docs/commands/job/run.mdx @@ -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: diff --git a/website/content/docs/commands/job/status.mdx b/website/content/docs/commands/job/status.mdx index 9cb1ec4c8..5861a8462 100644 --- a/website/content/docs/commands/job/status.mdx +++ b/website/content/docs/commands/job/status.mdx @@ -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: diff --git a/website/content/docs/commands/node/status.mdx b/website/content/docs/commands/node/status.mdx index d6c2488c1..daa67d355 100644 --- a/website/content/docs/commands/node/status.mdx +++ b/website/content/docs/commands/node/status.mdx @@ -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: diff --git a/website/content/docs/commands/server/members.mdx b/website/content/docs/commands/server/members.mdx index 3ace4d8db..c68421476 100644 --- a/website/content/docs/commands/server/members.mdx +++ b/website/content/docs/commands/server/members.mdx @@ -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 -``` \ No newline at end of file +``` diff --git a/website/content/docs/commands/var/get.mdx b/website/content/docs/commands/var/get.mdx index 363994288..06189a6e2 100644 --- a/website/content/docs/commands/var/get.mdx +++ b/website/content/docs/commands/var/get.mdx @@ -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": diff --git a/website/content/docs/commands/var/list.mdx b/website/content/docs/commands/var/list.mdx index d8974ffff..93ec0499a 100644 --- a/website/content/docs/commands/var/list.mdx +++ b/website/content/docs/commands/var/list.mdx @@ -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": diff --git a/website/content/docs/commands/var/put.mdx b/website/content/docs/commands/var/put.mdx index 34a361ad3..824a20a72 100644 --- a/website/content/docs/commands/var/put.mdx +++ b/website/content/docs/commands/var/put.mdx @@ -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": diff --git a/website/content/docs/configuration/ui.mdx b/website/content/docs/configuration/ui.mdx index 303e25709..63e74375a 100644 --- a/website/content/docs/configuration/ui.mdx +++ b/website/content/docs/configuration/ui.mdx @@ -60,6 +60,11 @@ and the configuration is individual to each agent. - `label` ([Label]: nil) - 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.