From afd2b9499e369633d175be9a3d59c09f323873e2 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Wed, 13 Nov 2024 10:42:32 -0500 Subject: [PATCH 01/18] Basic implementation for server members and node status --- command/meta.go | 93 ++++++++++++++++++++++++++ command/node_status.go | 28 ++++++++ command/server_members.go | 10 +++ helper/pluginutils/catalog/register.go | 14 ++-- 4 files changed, 139 insertions(+), 6 deletions(-) diff --git a/command/meta.go b/command/meta.go index da641896abe..00f5a7d3ba9 100644 --- a/command/meta.go +++ b/command/meta.go @@ -427,3 +427,96 @@ 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 +} + +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", + }, +} + +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.Replace(path, fmt.Sprintf(":%s", k), v, -1) + } + + 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 + } + + return m.formatUIHint(url, route.Description), nil +} diff --git a/command/node_status.go b/command/node_status.go index 9538e90622b..17c1c96c5d9 100644 --- a/command/node_status.go +++ b/command/node_status.go @@ -312,6 +312,13 @@ 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(), + }) + if hint != "" { + c.Ui.Output(hint) + } + return 0 } @@ -369,6 +376,7 @@ Results have been paginated. To get the next page run: } return c.formatNode(client, node) + } func nodeDrivers(n *api.Node) []string { @@ -504,6 +512,16 @@ 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, + }, + }) + if hint != "" { + c.Ui.Output(hint) + } + // Output alloc info if err := c.outputAllocInfo(node, nodeAllocs); err != nil { c.Ui.Error(fmt.Sprintf("%s", err)) @@ -591,6 +609,16 @@ 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, + }, + }) + if hint != "" { + c.Ui.Output(hint) + } + return 0 } diff --git a/command/server_members.go b/command/server_members.go index a7f0f174afb..8df6ce07afd 100644 --- a/command/server_members.go +++ b/command/server_members.go @@ -146,6 +146,16 @@ func (c *ServerMembersCommand) Run(args []string) int { // Dump the list c.Ui.Output(columnize.SimpleFormat(out)) + // c.Meta.showUIPath(UIHintContext{ + // Command: "server members", + // }) + hint, _ := c.Meta.showUIPath(UIHintContext{ + Command: c.Name(), + }) + if hint != "" { + c.Ui.Output(hint) + } + // If there were leader errors display a warning if leaderErr != nil { c.Ui.Output("") diff --git a/helper/pluginutils/catalog/register.go b/helper/pluginutils/catalog/register.go index a44f998fde8..e583a049b66 100644 --- a/helper/pluginutils/catalog/register.go +++ b/helper/pluginutils/catalog/register.go @@ -3,11 +3,13 @@ package catalog +// TODO: changes made to get mac buildin' again. + import ( "github.com/hashicorp/nomad/drivers/docker" - "github.com/hashicorp/nomad/drivers/exec" - "github.com/hashicorp/nomad/drivers/java" - "github.com/hashicorp/nomad/drivers/qemu" + // "github.com/hashicorp/nomad/drivers/exec" + // "github.com/hashicorp/nomad/drivers/java" + // "github.com/hashicorp/nomad/drivers/qemu" "github.com/hashicorp/nomad/drivers/rawexec" ) @@ -16,8 +18,8 @@ import ( // register_XXX.go file. func init() { RegisterDeferredConfig(rawexec.PluginID, rawexec.PluginConfig, rawexec.PluginLoader) - Register(exec.PluginID, exec.PluginConfig) - Register(qemu.PluginID, qemu.PluginConfig) - Register(java.PluginID, java.PluginConfig) + // Register(exec.PluginID, exec.PluginConfig) + // Register(qemu.PluginID, qemu.PluginConfig) + // Register(java.PluginID, java.PluginConfig) RegisterDeferredConfig(docker.PluginID, docker.PluginConfig, docker.PluginLoader) } From 2104c82ad213c0d527031cb17fe164d6565ab359 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Wed, 13 Nov 2024 11:08:27 -0500 Subject: [PATCH 02/18] Commands for alloc status and job status --- command/alloc_status.go | 9 +++++++++ command/job_status.go | 31 +++++++++++++++++++++++++++++++ command/meta.go | 14 +++++++++++++- 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/command/alloc_status.go b/command/alloc_status.go index 1b15bc89af5..eefab9f9db2 100644 --- a/command/alloc_status.go +++ b/command/alloc_status.go @@ -237,6 +237,15 @@ 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, + }, + }) + if hint != "" { + c.Ui.Output(hint) + } return 0 } diff --git a/command/job_status.go b/command/job_status.go index 0f444646c87..5b01a2bfef3 100644 --- a/command/job_status.go +++ b/command/job_status.go @@ -159,6 +159,12 @@ 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", + }) + if hint != "" { + c.Ui.Output(hint) + } } else { if c.json || len(c.tmpl) > 0 { pairs := make([]NamespacedID, len(jobs)) @@ -182,6 +188,12 @@ 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", + }) + if hint != "" { + c.Ui.Output(hint) + } } } return 0 @@ -271,6 +283,15 @@ 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, + }, + }) + if hint != "" { + c.Ui.Output(hint) + } return 0 } @@ -292,6 +313,16 @@ func (c *JobStatusCommand) Run(args []string) int { } } + hint, _ := c.Meta.showUIPath(UIHintContext{ + Command: "job status single", + PathParams: map[string]string{ + "jobID": *job.ID, + }, + }) + if hint != "" { + c.Ui.Output(hint) + } + return 0 } diff --git a/command/meta.go b/command/meta.go index 00f5a7d3ba9..aca39a9508a 100644 --- a/command/meta.go +++ b/command/meta.go @@ -463,6 +463,18 @@ var CommandUIRoutes = map[string]UIRoute{ 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", + Description: "View job details and metrics", + }, + "alloc status": { + Path: "/allocations/:allocID", + Description: "View allocation details", + }, } func (m *Meta) formatUIHint(url string, description string) string { @@ -501,7 +513,7 @@ func (m *Meta) buildUIPath(route UIRoute, params map[string]string) (string, err path := route.Path for k, v := range params { - path = strings.Replace(path, fmt.Sprintf(":%s", k), v, -1) + path = strings.ReplaceAll(path, fmt.Sprintf(":%s", k), v) } return fmt.Sprintf("%s/ui%s", client.Address(), path), nil From 907054913413f258e513364ec112d826f8aa2cc8 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Wed, 13 Nov 2024 16:53:55 -0500 Subject: [PATCH 03/18] -ui flag for most commands --- command/alloc_status.go | 8 +++++++- command/job_run.go | 42 ++++++++++++++++++++++++++++++++++++++- command/job_status.go | 10 ++++++++++ command/meta.go | 15 ++++++++++++++ command/node_status.go | 9 +++++++++ command/server_members.go | 11 ++++++---- 6 files changed, 89 insertions(+), 6 deletions(-) diff --git a/command/alloc_status.go b/command/alloc_status.go index eefab9f9db2..e54903f54af 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 @@ -242,6 +247,7 @@ func (c *AllocStatusCommand) Run(args []string) int { PathParams: map[string]string{ "allocID": alloc.ID, }, + OpenURL: openURL, }) if hint != "" { c.Ui.Output(hint) diff --git a/command/job_run.go b/command/job_run.go index 1a9a216ce33..e99f013d24f 100644 --- a/command/job_run.go +++ b/command/job_run.go @@ -106,6 +106,9 @@ Run Options: Output the JSON that would be submitted to the HTTP API without submitting the job. + -ui + Open the job run page in the browser. + -policy-override Sets the flag to force override any soft mandatory Sentinel policies. @@ -176,6 +179,7 @@ func (c *JobRunCommand) AutocompleteFlags() complete.Flags { "-var": complete.PredictAnything, "-var-file": complete.PredictFiles("*.var"), "-eval-priority": complete.PredictNothing, + "-ui": complete.PredictNothing, }) } @@ -190,7 +194,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, consulToken, consulNamespace, vaultToken, vaultNamespace string var evalPriority int @@ -211,6 +215,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 @@ -305,6 +310,18 @@ func (c *JobRunCommand) Run(args []string) int { } c.Ui.Output(string(buf)) + + hint, _ := c.Meta.showUIPath(UIHintContext{ + Command: "job run", + PathParams: map[string]string{ + "jobID": *job.ID, + }, + OpenURL: openURL, + }) + if hint != "" { + c.Ui.Output(hint) + } + return 0 } @@ -372,10 +389,33 @@ 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, + }, + OpenURL: openURL, + }) + if hint != "" { + c.Ui.Output(hint) + } + return 0 } // Detach was not specified, so start monitoring + // Set hint to open the browser + hint, _ := c.Meta.showUIPath(UIHintContext{ + Command: "job run", + PathParams: map[string]string{ + "jobID": *job.ID, + }, + OpenURL: openURL, + }) + if hint != "" { + c.Ui.Output(hint) + } + mon := newMonitor(c.Ui, client, length) return mon.monitor(evalID) diff --git a/command/job_status.go b/command/job_status.go index 5b01a2bfef3..376bffa82d3 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 @@ -161,6 +167,7 @@ func (c *JobStatusCommand) Run(args []string) int { c.Ui.Output("No running jobs") hint, _ := c.Meta.showUIPath(UIHintContext{ Command: "job status", + OpenURL: c.openURL, }) if hint != "" { c.Ui.Output(hint) @@ -190,6 +197,7 @@ func (c *JobStatusCommand) Run(args []string) int { c.Ui.Output(createStatusListOutput(jobs, allNamespaces)) hint, _ := c.Meta.showUIPath(UIHintContext{ Command: "job status", + OpenURL: c.openURL, }) if hint != "" { c.Ui.Output(hint) @@ -288,6 +296,7 @@ func (c *JobStatusCommand) Run(args []string) int { PathParams: map[string]string{ "jobID": *job.ID, }, + OpenURL: c.openURL, }) if hint != "" { c.Ui.Output(hint) @@ -318,6 +327,7 @@ func (c *JobStatusCommand) Run(args []string) int { PathParams: map[string]string{ "jobID": *job.ID, }, + OpenURL: c.openURL, }) if hint != "" { c.Ui.Output(hint) diff --git a/command/meta.go b/command/meta.go index aca39a9508a..6910805b4b1 100644 --- a/command/meta.go +++ b/command/meta.go @@ -10,7 +10,11 @@ import ( "reflect" "strings" +<<<<<<< HEAD "github.com/hashicorp/cli" +======= + "github.com/hashicorp/cap/util" +>>>>>>> c48bff6996 (-ui flag for most commands) "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/helper/pointer" colorable "github.com/mattn/go-colorable" @@ -436,6 +440,7 @@ type UIRoute struct { type UIHintContext struct { Command string PathParams map[string]string + OpenURL bool } const ( @@ -471,6 +476,10 @@ var CommandUIRoutes = map[string]UIRoute{ Path: "/jobs/:jobID", Description: "View job details and metrics", }, + "job run": { + Path: "/jobs/:jobID", + Description: "View this job in the Web UI", + }, "alloc status": { Path: "/allocations/:allocID", Description: "View allocation details", @@ -530,5 +539,11 @@ func (m *Meta) showUIPath(ctx UIHintContext) (string, error) { return "", err } + if ctx.OpenURL { + if err := util.OpenURL(url); err != nil { + m.Ui.Warn(fmt.Sprintf("Failed to open browser: %v", err)) + } + } + return m.formatUIHint(url, route.Description), nil } diff --git a/command/node_status.go b/command/node_status.go index 17c1c96c5d9..bbc980c71a5 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 @@ -314,6 +320,7 @@ Results have been paginated. To get the next page run: hint, _ := c.Meta.showUIPath(UIHintContext{ Command: c.Name(), + OpenURL: c.openURL, }) if hint != "" { c.Ui.Output(hint) @@ -517,6 +524,7 @@ func (c *NodeStatusCommand) formatNode(client *api.Client, node *api.Node) int { PathParams: map[string]string{ "nodeID": node.ID, }, + OpenURL: c.openURL, }) if hint != "" { c.Ui.Output(hint) @@ -614,6 +622,7 @@ func (c *NodeStatusCommand) formatNode(client *api.Client, node *api.Node) int { PathParams: map[string]string{ "nodeID": node.ID, }, + OpenURL: c.openURL, }) if hint != "" { c.Ui.Output(hint) diff --git a/command/server_members.go b/command/server_members.go index 8df6ce07afd..46ab1019feb 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,11 +151,9 @@ func (c *ServerMembersCommand) Run(args []string) int { // Dump the list c.Ui.Output(columnize.SimpleFormat(out)) - // c.Meta.showUIPath(UIHintContext{ - // Command: "server members", - // }) hint, _ := c.Meta.showUIPath(UIHintContext{ Command: c.Name(), + OpenURL: openURL, }) if hint != "" { c.Ui.Output(hint) From 9063f7b30f358cb0a83f73651830f3c3d9edfb3c Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Thu, 14 Nov 2024 09:51:29 -0500 Subject: [PATCH 04/18] url hints for variables --- command/meta.go | 14 +++++++++++++- command/var_get.go | 32 +++++++++++++++++++++++++++++++- command/var_list.go | 17 +++++++++++++++++ command/var_put.go | 41 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 100 insertions(+), 4 deletions(-) diff --git a/command/meta.go b/command/meta.go index 6910805b4b1..1257c60e525 100644 --- a/command/meta.go +++ b/command/meta.go @@ -478,12 +478,24 @@ var CommandUIRoutes = map[string]UIRoute{ }, "job run": { Path: "/jobs/:jobID", - Description: "View this job in the Web UI", + Description: "View this job", }, "alloc status": { Path: "/allocations/:allocID", Description: "View allocation details", }, + "var list": { + Path: "/variables/path/:prefix", + Description: "View Nomad variables", + }, + "var get": { + Path: "/variables/var/:path@:namespace", + Description: "View variable details", + }, + "var put": { + Path: "/variables/var/:path@:namespace", + Description: "View variable details", + }, } func (m *Meta) formatUIHint(url string, description string) string { diff --git a/command/var_get.go b/command/var_get.go index c101cf48f02..04d9aa1399a 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 get 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.Output(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.Output(hint) + } return 0 } diff --git a/command/var_list.go b/command/var_list.go index 141937f4a5a..dfceef4146c 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,17 @@ func (c *VarListCommand) Run(args []string) int { c.Ui.Warn(fmt.Sprintf("Next page token: %s", qm.NextToken)) } + hint, _ := c.Meta.showUIPath(UIHintContext{ + Command: "var list", + PathParams: map[string]string{ + "prefix": prefix, + }, + OpenURL: openURL, + }) + if hint != "" { + c.Ui.Output(hint) + } + return 0 } diff --git a/command/var_put.go b/command/var_put.go index c54a8934430..3492f2d14db 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 put 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.Output(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.Output(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.Output(hint) + } return 0 } From e50666a8c23b06f2af489efd279f5433453e69c6 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Thu, 14 Nov 2024 10:29:00 -0500 Subject: [PATCH 05/18] url hints for job dispatch, evals, and deployments --- command/deployment_status.go | 33 +++++++++++++++++++++++++++++++-- command/eval_list.go | 15 ++++++++++++++- command/eval_status.go | 19 +++++++++++++++++-- command/job_dispatch.go | 24 +++++++++++++++++++++++- command/job_run.go | 2 ++ command/meta.go | 16 ++++++++++++++++ command/plugin_status.go | 2 ++ 7 files changed, 105 insertions(+), 6 deletions(-) diff --git a/command/deployment_status.go b/command/deployment_status.go index 318b7f898b9..c42f63d5195 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,34 @@ func (c *DeploymentStatusCommand) Run(args []string) int { formatTime(time.Now()), limit(deploy.ID, length))) c.monitor(client, deploy.ID, meta.LastIndex, wait, verbose) + // Hint here + hint, _ := c.Meta.showUIPath(UIHintContext{ + Command: "deployment status", + PathParams: map[string]string{ + "jobID": deploy.JobID, + }, + OpenURL: openURL, + }) + if hint != "" { + c.Ui.Output(hint) + // Because this is before monitor, newline so we don't scrunch + c.Ui.Output("\n") + } + 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.Output(hint) + } return 0 } diff --git a/command/eval_list.go b/command/eval_list.go index 35e495675a3..24b8c181e30 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 evaluation 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.Output(hint) + } + return 0 } diff --git a/command/eval_status.go b/command/eval_status.go index 99936591e30..d7896d330cf 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.Output(hint) + } + return 0 } diff --git a/command/job_dispatch.go b/command/job_dispatch.go index 4b14fc059c7..1f2b0e9a681 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,21 @@ 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, + }, + OpenURL: openURL, + }) + if hint != "" { + c.Ui.Output(hint) + // Because this is before monitor, newline so we don't scrunch + c.Ui.Output("\n") + } return mon.monitor(resp.EvalID) } diff --git a/command/job_run.go b/command/job_run.go index e99f013d24f..1caaef0581e 100644 --- a/command/job_run.go +++ b/command/job_run.go @@ -414,6 +414,8 @@ func (c *JobRunCommand) Run(args []string) int { }) if hint != "" { c.Ui.Output(hint) + // Because this is before monitor, newline so we don't scrunch + c.Ui.Output("\n") } mon := newMonitor(c.Ui, client, length) diff --git a/command/meta.go b/command/meta.go index 1257c60e525..392529dafa7 100644 --- a/command/meta.go +++ b/command/meta.go @@ -496,6 +496,22 @@ var CommandUIRoutes = map[string]UIRoute{ Path: "/variables/var/:path@:namespace", Description: "View variable details", }, + "job dispatch": { + Path: "/jobs/:dispatchID", + 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 { diff --git a/command/plugin_status.go b/command/plugin_status.go index 92dbdc7f26b..cf0de31aa43 100644 --- a/command/plugin_status.go +++ b/command/plugin_status.go @@ -146,5 +146,7 @@ func (c *PluginStatusCommand) Run(args []string) int { // Extend this section with other plugin implementations + // TODO: add UI Hints + return 0 } From d3889a860123aaf2d965491de0327de4334a0aa6 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Thu, 14 Nov 2024 15:48:51 -0500 Subject: [PATCH 06/18] agent config ui.cli_url_links to disable --- command/meta.go | 22 ++++++++++++++++++++++ nomad/structs/config/ui.go | 11 ++++++++++- website/content/docs/configuration/ui.mdx | 7 ++++++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/command/meta.go b/command/meta.go index 392529dafa7..08b327dd449 100644 --- a/command/meta.go +++ b/command/meta.go @@ -573,5 +573,27 @@ func (m *Meta) showUIPath(ctx UIHintContext) (string, error) { } } + if m.uiHintsDisabled() { + return "", nil + } + return m.formatUIHint(url, route.Description), nil } + +func (m *Meta) uiHintsDisabled() bool { + client, err := m.Client() + if err != nil { + return true + } + agent, err := client.Agent().Self() + if err != nil { + return true + } + agentConfig := agent.Config + uiConfig, ok := agentConfig["UI"].(map[string]interface{}) + if !ok { + return true + } + + return !uiConfig["CLIURLLinks"].(bool) +} diff --git a/nomad/structs/config/ui.go b/nomad/structs/config/ui.go index b201acfccae..10fd2750a73 100644 --- a/nomad/structs/config/ui.go +++ b/nomad/structs/config/ui.go @@ -28,6 +28,9 @@ type UIConfig struct { // Label configures UI label styles Label *LabelUIConfig `hcl:"label"` + + // CLIURLLinks controls whether CLI commands that return URLs will output that url as a hint + CLIURLLinks *bool `hcl:"cli_url_links"` } // only covers the elements of @@ -136,12 +139,14 @@ type LabelUIConfig struct { // DefaultUIConfig returns the canonical defaults for the Nomad // `ui` configuration. func DefaultUIConfig() *UIConfig { + enabled := true return &UIConfig{ - Enabled: true, + Enabled: enabled, Consul: &ConsulUIConfig{}, Vault: &VaultUIConfig{}, Label: &LabelUIConfig{}, ContentSecurityPolicy: DefaultCSPConfig(), + CLIURLLinks: &enabled, } } @@ -177,6 +182,10 @@ func (old *UIConfig) Merge(other *UIConfig) *UIConfig { result.Label = result.Label.Merge(other.Label) result.ContentSecurityPolicy = result.ContentSecurityPolicy.Merge(other.ContentSecurityPolicy) + if other.CLIURLLinks != nil { + result.CLIURLLinks = other.CLIURLLinks + } + return result } diff --git a/website/content/docs/configuration/ui.mdx b/website/content/docs/configuration/ui.mdx index a51c9125827..71b1f4d905f 100644 --- a/website/content/docs/configuration/ui.mdx +++ b/website/content/docs/configuration/ui.mdx @@ -58,6 +58,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. +- `cli_url_links` `(bool: true)` - Controls whether CLI commands display hints + about equivalent UI pages. For example, when running `nomad server members`, + the CLI will show 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 @@ -106,7 +111,7 @@ 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 will default 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. From 86d12699ec44a512baa380f36953ed694cb5ac4c Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Thu, 14 Nov 2024 16:18:25 -0500 Subject: [PATCH 07/18] Fix an issue where path prefix was presumed for variables --- command/meta.go | 6 +++++- command/var_list.go | 28 +++++++++++++++++++--------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/command/meta.go b/command/meta.go index 08b327dd449..b470b4c4139 100644 --- a/command/meta.go +++ b/command/meta.go @@ -485,9 +485,13 @@ var CommandUIRoutes = map[string]UIRoute{ Description: "View allocation details", }, "var list": { - Path: "/variables/path/:prefix", + 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", diff --git a/command/var_list.go b/command/var_list.go index dfceef4146c..71a1a5155cf 100644 --- a/command/var_list.go +++ b/command/var_list.go @@ -217,15 +217,25 @@ func (c *VarListCommand) Run(args []string) int { c.Ui.Warn(fmt.Sprintf("Next page token: %s", qm.NextToken)) } - hint, _ := c.Meta.showUIPath(UIHintContext{ - Command: "var list", - PathParams: map[string]string{ - "prefix": prefix, - }, - OpenURL: openURL, - }) - if hint != "" { - c.Ui.Output(hint) + if prefix != "" { + hint, _ := c.Meta.showUIPath(UIHintContext{ + Command: "var list prefix", + PathParams: map[string]string{ + "prefix": prefix, + }, + OpenURL: openURL, + }) + if hint != "" { + c.Ui.Output(hint) + } + } else { + hint, _ := c.Meta.showUIPath(UIHintContext{ + Command: "var list", + OpenURL: openURL, + }) + if hint != "" { + c.Ui.Output(hint) + } } return 0 From 01e70733d5bf1cbf92dd4ae69e480c206312a41b Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Sun, 24 Nov 2024 09:27:30 -0500 Subject: [PATCH 08/18] driver uncomment and general cleanup --- command/deployment_status.go | 1 - command/job_run.go | 1 - helper/pluginutils/catalog/register.go | 14 ++++++-------- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/command/deployment_status.go b/command/deployment_status.go index c42f63d5195..da144d84274 100644 --- a/command/deployment_status.go +++ b/command/deployment_status.go @@ -187,7 +187,6 @@ func (c *DeploymentStatusCommand) Run(args []string) int { formatTime(time.Now()), limit(deploy.ID, length))) c.monitor(client, deploy.ID, meta.LastIndex, wait, verbose) - // Hint here hint, _ := c.Meta.showUIPath(UIHintContext{ Command: "deployment status", PathParams: map[string]string{ diff --git a/command/job_run.go b/command/job_run.go index 1caaef0581e..e83fe6c2155 100644 --- a/command/job_run.go +++ b/command/job_run.go @@ -404,7 +404,6 @@ func (c *JobRunCommand) Run(args []string) int { } // Detach was not specified, so start monitoring - // Set hint to open the browser hint, _ := c.Meta.showUIPath(UIHintContext{ Command: "job run", PathParams: map[string]string{ diff --git a/helper/pluginutils/catalog/register.go b/helper/pluginutils/catalog/register.go index e583a049b66..a44f998fde8 100644 --- a/helper/pluginutils/catalog/register.go +++ b/helper/pluginutils/catalog/register.go @@ -3,13 +3,11 @@ package catalog -// TODO: changes made to get mac buildin' again. - import ( "github.com/hashicorp/nomad/drivers/docker" - // "github.com/hashicorp/nomad/drivers/exec" - // "github.com/hashicorp/nomad/drivers/java" - // "github.com/hashicorp/nomad/drivers/qemu" + "github.com/hashicorp/nomad/drivers/exec" + "github.com/hashicorp/nomad/drivers/java" + "github.com/hashicorp/nomad/drivers/qemu" "github.com/hashicorp/nomad/drivers/rawexec" ) @@ -18,8 +16,8 @@ import ( // register_XXX.go file. func init() { RegisterDeferredConfig(rawexec.PluginID, rawexec.PluginConfig, rawexec.PluginLoader) - // Register(exec.PluginID, exec.PluginConfig) - // Register(qemu.PluginID, qemu.PluginConfig) - // Register(java.PluginID, java.PluginConfig) + Register(exec.PluginID, exec.PluginConfig) + Register(qemu.PluginID, qemu.PluginConfig) + Register(java.PluginID, java.PluginConfig) RegisterDeferredConfig(docker.PluginID, docker.PluginConfig, docker.PluginLoader) } From 47f861292ec00185481623393dcc1fc23940399b Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Fri, 13 Dec 2024 11:20:33 -0500 Subject: [PATCH 09/18] -ui flag on the generic status endpoint --- command/status.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/command/status.go b/command/status.go index 4c175d6160e..906ad1cf4b7 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 From 5632de36b1e3b7a2b924344583e3d47faa88c5ea Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Mon, 16 Dec 2024 10:20:52 -0500 Subject: [PATCH 10/18] Job run command gets namespaces, and no longer gets ui hints for --output flag --- command/job_run.go | 22 +++++++++------------- command/job_status.go | 6 ++++-- command/meta.go | 4 ++-- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/command/job_run.go b/command/job_run.go index e83fe6c2155..642237f9800 100644 --- a/command/job_run.go +++ b/command/job_run.go @@ -311,17 +311,6 @@ func (c *JobRunCommand) Run(args []string) int { c.Ui.Output(string(buf)) - hint, _ := c.Meta.showUIPath(UIHintContext{ - Command: "job run", - PathParams: map[string]string{ - "jobID": *job.ID, - }, - OpenURL: openURL, - }) - if hint != "" { - c.Ui.Output(hint) - } - return 0 } @@ -370,6 +359,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") @@ -392,7 +386,8 @@ func (c *JobRunCommand) Run(args []string) int { hint, _ := c.Meta.showUIPath(UIHintContext{ Command: "job run", PathParams: map[string]string{ - "jobID": *job.ID, + "jobID": *job.ID, + "namespace": jobNamespace, }, OpenURL: openURL, }) @@ -407,7 +402,8 @@ func (c *JobRunCommand) Run(args []string) int { hint, _ := c.Meta.showUIPath(UIHintContext{ Command: "job run", PathParams: map[string]string{ - "jobID": *job.ID, + "jobID": *job.ID, + "namespace": jobNamespace, }, OpenURL: openURL, }) diff --git a/command/job_status.go b/command/job_status.go index 376bffa82d3..876f78a03a4 100644 --- a/command/job_status.go +++ b/command/job_status.go @@ -294,7 +294,8 @@ func (c *JobStatusCommand) Run(args []string) int { hint, _ := c.Meta.showUIPath(UIHintContext{ Command: "job status single", PathParams: map[string]string{ - "jobID": *job.ID, + "jobID": *job.ID, + "namespace": *job.Namespace, }, OpenURL: c.openURL, }) @@ -325,7 +326,8 @@ func (c *JobStatusCommand) Run(args []string) int { hint, _ := c.Meta.showUIPath(UIHintContext{ Command: "job status single", PathParams: map[string]string{ - "jobID": *job.ID, + "jobID": *job.ID, + "namespace": *job.Namespace, }, OpenURL: c.openURL, }) diff --git a/command/meta.go b/command/meta.go index b470b4c4139..58b25abb57a 100644 --- a/command/meta.go +++ b/command/meta.go @@ -473,11 +473,11 @@ var CommandUIRoutes = map[string]UIRoute{ Description: "View and manage Nomad jobs", }, "job status single": { - Path: "/jobs/:jobID", + Path: "/jobs/:jobID@:namespace", Description: "View job details and metrics", }, "job run": { - Path: "/jobs/:jobID", + Path: "/jobs/:jobID@:namespace", Description: "View this job", }, "alloc status": { From b17637cb723b0963f5d9b9f9962dcf703dddf0d9 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Mon, 16 Dec 2024 17:00:31 -0500 Subject: [PATCH 11/18] Dispatch command hints get a namespace, and bunch o tests --- command/job_dispatch.go | 1 + command/meta.go | 2 +- command/meta_test.go | 201 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 1 deletion(-) diff --git a/command/job_dispatch.go b/command/job_dispatch.go index 1f2b0e9a681..453ebb68542 100644 --- a/command/job_dispatch.go +++ b/command/job_dispatch.go @@ -234,6 +234,7 @@ func (c *JobDispatchCommand) Run(args []string) int { Command: "job dispatch", PathParams: map[string]string{ "dispatchID": dispatchID, + "namespace": namespace, }, OpenURL: openURL, }) diff --git a/command/meta.go b/command/meta.go index 58b25abb57a..a2551cb92cf 100644 --- a/command/meta.go +++ b/command/meta.go @@ -501,7 +501,7 @@ var CommandUIRoutes = map[string]UIRoute{ Description: "View variable details", }, "job dispatch": { - Path: "/jobs/:dispatchID", + Path: "/jobs/:dispatchID@:namespace", Description: "View this job", }, "eval list": { diff --git a/command/meta_test.go b/command/meta_test.go index 208dd15cb22..3c66aa8b746 100644 --- a/command/meta_test.go +++ b/command/meta_test.go @@ -5,6 +5,7 @@ package command import ( "flag" + "fmt" "os" "reflect" "sort" @@ -250,3 +251,203 @@ func TestMeta_JobByPrefix(t *testing.T) { }) } } + +func TestMeta_ShowUIPath(t *testing.T) { + ci.Parallel(t) + + cases := []struct { + name string + context UIHintContext + expectedURL string + expectedOpened bool + }{ + { + name: "server members", + context: UIHintContext{ + Command: "server members", + }, + expectedURL: "http://127.0.0.1:4646/ui/servers", + }, + { + name: "node status (many)", + context: UIHintContext{ + Command: "node status", + }, + expectedURL: "http://127.0.0.1:4646/ui/clients", + }, + { + name: "node status (single)", + context: UIHintContext{ + Command: "node status single", + PathParams: map[string]string{ + "nodeID": "node-1", + }, + }, + expectedURL: "http://127.0.0.1:4646/ui/clients/node-1", + }, + { + name: "job status (many)", + context: UIHintContext{ + Command: "job status", + }, + expectedURL: "http://127.0.0.1:4646/ui/jobs", + }, + { + name: "job status (single)", + context: UIHintContext{ + Command: "job status single", + PathParams: map[string]string{ + "jobID": "example-job", + "namespace": "default", + }, + }, + expectedURL: "http://127.0.0.1:4646/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: "http://127.0.0.1:4646/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: "http://127.0.0.1:4646/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: "http://127.0.0.1:4646/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: "http://127.0.0.1:4646/ui/jobs/dispatch-1@toronto", + }, + { + name: "eval list", + context: UIHintContext{ + Command: "eval list", + }, + expectedURL: "http://127.0.0.1:4646/ui/evaluations", + }, + { + name: "eval status", + context: UIHintContext{ + Command: "eval status", + PathParams: map[string]string{ + "evalID": "eval-1", + }, + }, + expectedURL: "http://127.0.0.1:4646/ui/evaluations?currentEval=eval-1", + }, + { + name: "deployment status", + context: UIHintContext{ + Command: "deployment status", + PathParams: map[string]string{ + "jobID": "example-job", + }, + }, + expectedURL: "http://127.0.0.1:4646/ui/jobs/example-job/deployments", + }, + { + name: "var list (root)", + context: UIHintContext{ + Command: "var list", + }, + expectedURL: "http://127.0.0.1:4646/ui/variables", + }, + { + name: "var list (path)", + context: UIHintContext{ + Command: "var list prefix", + PathParams: map[string]string{ + "prefix": "foo", + }, + }, + expectedURL: "http://127.0.0.1:4646/ui/variables/path/foo", + }, + { + name: "var get", + context: UIHintContext{ + Command: "var get", + PathParams: map[string]string{ + "path": "foo", + "namespace": "default", + }, + }, + expectedURL: "http://127.0.0.1:4646/ui/variables/var/foo@default", + }, + { + name: "var put", + context: UIHintContext{ + Command: "var put", + PathParams: map[string]string{ + "path": "foo", + "namespace": "default", + }, + }, + expectedURL: "http://127.0.0.1:4646/ui/variables/var/foo@default", + }, + { + name: "alloc status", + context: UIHintContext{ + Command: "alloc status", + PathParams: map[string]string{ + "allocID": "alloc-1", + }, + }, + expectedURL: "http://127.0.0.1:4646/ui/allocations/alloc-1", + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + route := CommandUIRoutes[tc.context.Command] + expectedHint := fmt.Sprintf("\n\n==> %s in the Web UI: %s", route.Description, tc.expectedURL) + + m := &Meta{ + Ui: cli.NewMockUi(), + } + + hint, err := m.showUIPath(tc.context) + must.NoError(t, err) + must.Eq(t, expectedHint, hint) + }) + } + + // TODO: invalid/edge cases + // - unknown command + // - missing required params + // - invalid path params + // - --output flag on job run + + // TODO: browser opening tests +} From d8de6dfefdeed855337f6ecfb9d9c54d5a133a52 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Mon, 16 Dec 2024 21:07:20 -0500 Subject: [PATCH 12/18] Lots of tests depend on specific output, so let's not mess with them --- command/testing_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/command/testing_test.go b/command/testing_test.go index 66f70e58f31..c2e7659940d 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.CLIURLLinks = pointer.Of(false) + if cb != nil { cb(config) } From b506f7532b5bb8c91bc1bfcf7915a08392c204ca Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Mon, 16 Dec 2024 23:26:22 -0500 Subject: [PATCH 13/18] figured out what flagAddress is all about for testServer, oof --- command/meta_test.go | 109 ++++++++++++++++++++++++++++++++----------- 1 file changed, 82 insertions(+), 27 deletions(-) diff --git a/command/meta_test.go b/command/meta_test.go index 3c66aa8b746..3fe85fa2d19 100644 --- a/command/meta_test.go +++ b/command/meta_test.go @@ -15,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" ) @@ -255,6 +256,18 @@ 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.CLIURLLinks = pointer.Of(true) + }) + defer server.Shutdown() + waitForNodes(t, client) + + m := &Meta{ + Ui: cli.NewMockUi(), + flagAddress: url, + } + cases := []struct { name string context UIHintContext @@ -266,14 +279,14 @@ func TestMeta_ShowUIPath(t *testing.T) { context: UIHintContext{ Command: "server members", }, - expectedURL: "http://127.0.0.1:4646/ui/servers", + expectedURL: url + "/ui/servers", }, { name: "node status (many)", context: UIHintContext{ Command: "node status", }, - expectedURL: "http://127.0.0.1:4646/ui/clients", + expectedURL: url + "/ui/clients", }, { name: "node status (single)", @@ -283,14 +296,14 @@ func TestMeta_ShowUIPath(t *testing.T) { "nodeID": "node-1", }, }, - expectedURL: "http://127.0.0.1:4646/ui/clients/node-1", + expectedURL: url + "/ui/clients/node-1", }, { name: "job status (many)", context: UIHintContext{ Command: "job status", }, - expectedURL: "http://127.0.0.1:4646/ui/jobs", + expectedURL: url + "/ui/jobs", }, { name: "job status (single)", @@ -301,7 +314,7 @@ func TestMeta_ShowUIPath(t *testing.T) { "namespace": "default", }, }, - expectedURL: "http://127.0.0.1:4646/ui/jobs/example-job@default", + expectedURL: url + "/ui/jobs/example-job@default", }, { name: "job run (default ns)", @@ -312,7 +325,7 @@ func TestMeta_ShowUIPath(t *testing.T) { "namespace": "default", }, }, - expectedURL: "http://127.0.0.1:4646/ui/jobs/example-job@default", + expectedURL: url + "/ui/jobs/example-job@default", }, { name: "job run (non-default ns)", @@ -323,7 +336,7 @@ func TestMeta_ShowUIPath(t *testing.T) { "namespace": "prod", }, }, - expectedURL: "http://127.0.0.1:4646/ui/jobs/example-job@prod", + expectedURL: url + "/ui/jobs/example-job@prod", }, { name: "job dispatch (default ns)", @@ -334,7 +347,7 @@ func TestMeta_ShowUIPath(t *testing.T) { "namespace": "default", }, }, - expectedURL: "http://127.0.0.1:4646/ui/jobs/dispatch-1@default", + expectedURL: url + "/ui/jobs/dispatch-1@default", }, { name: "job dispatch (non-default ns)", @@ -345,14 +358,14 @@ func TestMeta_ShowUIPath(t *testing.T) { "namespace": "toronto", }, }, - expectedURL: "http://127.0.0.1:4646/ui/jobs/dispatch-1@toronto", + expectedURL: url + "/ui/jobs/dispatch-1@toronto", }, { name: "eval list", context: UIHintContext{ Command: "eval list", }, - expectedURL: "http://127.0.0.1:4646/ui/evaluations", + expectedURL: url + "/ui/evaluations", }, { name: "eval status", @@ -362,7 +375,7 @@ func TestMeta_ShowUIPath(t *testing.T) { "evalID": "eval-1", }, }, - expectedURL: "http://127.0.0.1:4646/ui/evaluations?currentEval=eval-1", + expectedURL: url + "/ui/evaluations?currentEval=eval-1", }, { name: "deployment status", @@ -372,14 +385,14 @@ func TestMeta_ShowUIPath(t *testing.T) { "jobID": "example-job", }, }, - expectedURL: "http://127.0.0.1:4646/ui/jobs/example-job/deployments", + expectedURL: url + "/ui/jobs/example-job/deployments", }, { name: "var list (root)", context: UIHintContext{ Command: "var list", }, - expectedURL: "http://127.0.0.1:4646/ui/variables", + expectedURL: url + "/ui/variables", }, { name: "var list (path)", @@ -389,7 +402,7 @@ func TestMeta_ShowUIPath(t *testing.T) { "prefix": "foo", }, }, - expectedURL: "http://127.0.0.1:4646/ui/variables/path/foo", + expectedURL: url + "/ui/variables/path/foo", }, { name: "var get", @@ -400,7 +413,7 @@ func TestMeta_ShowUIPath(t *testing.T) { "namespace": "default", }, }, - expectedURL: "http://127.0.0.1:4646/ui/variables/var/foo@default", + expectedURL: url + "/ui/variables/var/foo@default", }, { name: "var put", @@ -411,7 +424,7 @@ func TestMeta_ShowUIPath(t *testing.T) { "namespace": "default", }, }, - expectedURL: "http://127.0.0.1:4646/ui/variables/var/foo@default", + expectedURL: url + "/ui/variables/var/foo@default", }, { name: "alloc status", @@ -421,7 +434,7 @@ func TestMeta_ShowUIPath(t *testing.T) { "allocID": "alloc-1", }, }, - expectedURL: "http://127.0.0.1:4646/ui/allocations/alloc-1", + expectedURL: url + "/ui/allocations/alloc-1", }, } @@ -433,21 +446,63 @@ func TestMeta_ShowUIPath(t *testing.T) { route := CommandUIRoutes[tc.context.Command] expectedHint := fmt.Sprintf("\n\n==> %s in the Web UI: %s", route.Description, tc.expectedURL) - m := &Meta{ - Ui: cli.NewMockUi(), - } - hint, err := m.showUIPath(tc.context) must.NoError(t, err) must.Eq(t, expectedHint, hint) }) } +} + +func TestMeta_ShowUIPath_CLIURLLinksEnabled(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.CLIURLLinks = 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_CLIURLLinksDisabled(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.CLIURLLinks = pointer.Of(false) + }) + defer server.Shutdown() + waitForNodes(t, client) + + m := &Meta{ + Ui: cli.NewMockUi(), + flagAddress: url, + } - // TODO: invalid/edge cases - // - unknown command - // - missing required params - // - invalid path params - // - --output flag on job run + hint, err := m.showUIPath(UIHintContext{ + Command: "job status", + }) + must.NoError(t, err) - // TODO: browser opening tests + must.StrNotContains(t, hint, url+"/ui/jobs") } + +// TODO: invalid/edge cases +// - unknown command +// - missing required params +// - invalid path params +// - --output flag on job run + +// TODO: browser opening tests From 819742b1e63c23bf20ffc2a8b0547b47e36cdf28 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Tue, 17 Dec 2024 00:20:53 -0500 Subject: [PATCH 14/18] Parallel outside of test instances --- command/meta_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/command/meta_test.go b/command/meta_test.go index 3fe85fa2d19..64aa5d8b3b9 100644 --- a/command/meta_test.go +++ b/command/meta_test.go @@ -441,8 +441,6 @@ func TestMeta_ShowUIPath(t *testing.T) { for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { - t.Parallel() - route := CommandUIRoutes[tc.context.Command] expectedHint := fmt.Sprintf("\n\n==> %s in the Web UI: %s", route.Description, tc.expectedURL) From c42a2969cd2a3ea84a017a8bcb0440bace74bfbe Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Tue, 17 Dec 2024 00:34:30 -0500 Subject: [PATCH 15/18] Browser-opening test, sorta --- .changelog/24454.txt | 3 +++ command/meta_test.go | 32 ++++++++++++++++++++++++++------ command/plugin_status.go | 2 -- 3 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 .changelog/24454.txt diff --git a/.changelog/24454.txt b/.changelog/24454.txt new file mode 100644 index 00000000000..1a140ae967f --- /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/meta_test.go b/command/meta_test.go index 64aa5d8b3b9..fb70fa92bac 100644 --- a/command/meta_test.go +++ b/command/meta_test.go @@ -497,10 +497,30 @@ func TestMeta_ShowUIPath_CLIURLLinksDisabled(t *testing.T) { must.StrNotContains(t, hint, url+"/ui/jobs") } -// TODO: invalid/edge cases -// - unknown command -// - missing required params -// - invalid path params -// - --output flag on job run +func TestMeta_ShowUIPath_BrowserOpening(t *testing.T) { + ci.Parallel(t) + + server, client, url := testServer(t, true, func(c *agent.Config) { + c.UI.CLIURLLinks = 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") -// TODO: browser opening tests + // 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/plugin_status.go b/command/plugin_status.go index cf0de31aa43..92dbdc7f26b 100644 --- a/command/plugin_status.go +++ b/command/plugin_status.go @@ -146,7 +146,5 @@ func (c *PluginStatusCommand) Run(args []string) int { // Extend this section with other plugin implementations - // TODO: add UI Hints - return 0 } From c1de8177980b4b62f22f5f33fce38d048001a964 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Wed, 8 Jan 2025 11:23:48 -0500 Subject: [PATCH 16/18] Env var for disabling/enabling CLI hints --- command/commands.go | 3 + command/meta.go | 43 ++++++++++-- command/meta_test.go | 82 +++++++++++++++++++++-- command/testing_test.go | 2 +- nomad/structs/config/ui.go | 10 +-- website/content/docs/commands/index.mdx | 2 + website/content/docs/configuration/ui.mdx | 2 +- 7 files changed, 125 insertions(+), 19 deletions(-) diff --git a/command/commands.go b/command/commands.go index a29e97ea726..e7aad2444a9 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` + + // EnvNomadShowCLIHints is an env var that toggles CLI hints. + EnvNomadShowCLIHints = `NOMAD_SHOW_CLI_HINTS` ) // DeprecatedCommand is a command that wraps an existing command and prints a diff --git a/command/meta.go b/command/meta.go index a2551cb92cf..c26e76e7dec 100644 --- a/command/meta.go +++ b/command/meta.go @@ -10,11 +10,8 @@ import ( "reflect" "strings" -<<<<<<< HEAD - "github.com/hashicorp/cli" -======= "github.com/hashicorp/cap/util" ->>>>>>> c48bff6996 (-ui flag for most commands) + "github.com/hashicorp/cli" "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/helper/pointer" colorable "github.com/mattn/go-colorable" @@ -62,6 +59,8 @@ type Meta struct { // token is used for ACLs to access privileged information token string + showCLIHints *bool + caCert string caPath string clientCert string @@ -252,6 +251,14 @@ func (m *Meta) SetupUi(args []string) { Ui: m.Ui, } } + + // Check to see if the user has disabled hints via env var. + showCLIHints := os.Getenv(EnvNomadShowCLIHints) + if showCLIHints == "false" { + m.showCLIHints = pointer.Of(false) + } else if showCLIHints == "true" { + m.showCLIHints = pointer.Of(true) + } } // FormatWarnings returns a string with the warnings formatted for CLI output. @@ -585,19 +592,43 @@ func (m *Meta) showUIPath(ctx UIHintContext) (string, error) { } 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 - uiConfig, ok := agentConfig["UI"].(map[string]interface{}) + agentUIConfig, ok := agentConfig["UI"].(map[string]interface{}) + 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 !uiConfig["CLIURLLinks"].(bool) + return false } diff --git a/command/meta_test.go b/command/meta_test.go index fb70fa92bac..df094a3a658 100644 --- a/command/meta_test.go +++ b/command/meta_test.go @@ -258,7 +258,7 @@ func TestMeta_ShowUIPath(t *testing.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.CLIURLLinks = pointer.Of(true) + c.UI.ShowCLIHints = pointer.Of(true) }) defer server.Shutdown() waitForNodes(t, client) @@ -451,12 +451,12 @@ func TestMeta_ShowUIPath(t *testing.T) { } } -func TestMeta_ShowUIPath_CLIURLLinksEnabled(t *testing.T) { +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.CLIURLLinks = pointer.Of(true) + c.UI.ShowCLIHints = pointer.Of(true) }) defer server.Shutdown() waitForNodes(t, client) @@ -474,12 +474,12 @@ func TestMeta_ShowUIPath_CLIURLLinksEnabled(t *testing.T) { must.StrContains(t, hint, url+"/ui/jobs") } -func TestMeta_ShowUIPath_CLIURLLinksDisabled(t *testing.T) { +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.CLIURLLinks = pointer.Of(false) + c.UI.ShowCLIHints = pointer.Of(false) }) defer server.Shutdown() waitForNodes(t, client) @@ -497,11 +497,81 @@ func TestMeta_ShowUIPath_CLIURLLinksDisabled(t *testing.T) { 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_SHOW_CLI_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.CLIURLLinks = pointer.Of(true) + c.UI.ShowCLIHints = pointer.Of(true) }) defer server.Shutdown() waitForNodes(t, client) diff --git a/command/testing_test.go b/command/testing_test.go index c2e7659940d..ae5e6186242 100644 --- a/command/testing_test.go +++ b/command/testing_test.go @@ -26,7 +26,7 @@ func testServer(t *testing.T, runClient bool, cb func(*agent.Config)) (*agent.Te config.Client.Enabled = runClient // Disable UI hints in test by default - config.UI.CLIURLLinks = pointer.Of(false) + config.UI.ShowCLIHints = pointer.Of(false) if cb != nil { cb(config) diff --git a/nomad/structs/config/ui.go b/nomad/structs/config/ui.go index 10fd2750a73..9c10c9940ad 100644 --- a/nomad/structs/config/ui.go +++ b/nomad/structs/config/ui.go @@ -29,8 +29,8 @@ type UIConfig struct { // Label configures UI label styles Label *LabelUIConfig `hcl:"label"` - // CLIURLLinks controls whether CLI commands that return URLs will output that url as a hint - CLIURLLinks *bool `hcl:"cli_url_links"` + // 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 @@ -146,7 +146,7 @@ func DefaultUIConfig() *UIConfig { Vault: &VaultUIConfig{}, Label: &LabelUIConfig{}, ContentSecurityPolicy: DefaultCSPConfig(), - CLIURLLinks: &enabled, + ShowCLIHints: &enabled, } } @@ -182,8 +182,8 @@ func (old *UIConfig) Merge(other *UIConfig) *UIConfig { result.Label = result.Label.Merge(other.Label) result.ContentSecurityPolicy = result.ContentSecurityPolicy.Merge(other.ContentSecurityPolicy) - if other.CLIURLLinks != nil { - result.CLIURLLinks = other.CLIURLLinks + if other.ShowCLIHints != nil { + result.ShowCLIHints = other.ShowCLIHints } return result diff --git a/website/content/docs/commands/index.mdx b/website/content/docs/commands/index.mdx index 0617239944b..54234243b0b 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_SHOW_CLI_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/configuration/ui.mdx b/website/content/docs/configuration/ui.mdx index 71b1f4d905f..5c33b0dd259 100644 --- a/website/content/docs/configuration/ui.mdx +++ b/website/content/docs/configuration/ui.mdx @@ -58,7 +58,7 @@ and the configuration is individual to each agent. - `label` ([Label]: nil) - Configures a user-defined label to display in the Nomad Web UI header. -- `cli_url_links` `(bool: true)` - Controls whether CLI commands display hints +- `show_cli_hints` `(bool: true)` - Controls whether CLI commands display hints about equivalent UI pages. For example, when running `nomad server members`, the CLI will show a message indicating where to find server information in the web UI. Set to `false` to disable these hints. From b03325ea0f4b71fc154b4f806c1b578616f04f68 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Wed, 8 Jan 2025 15:01:15 -0500 Subject: [PATCH 17/18] Addressing a few PR comments --- .changelog/24454.txt | 2 +- nomad/structs/config/ui.go | 7 ++++--- website/content/docs/configuration/ui.mdx | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.changelog/24454.txt b/.changelog/24454.txt index 1a140ae967f..33bac43bf33 100644 --- a/.changelog/24454.txt +++ b/.changelog/24454.txt @@ -1,3 +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 +cli: Added UI URL hints to the end of common CLI commands and a `-ui` flag to auto-open them ``` diff --git a/nomad/structs/config/ui.go b/nomad/structs/config/ui.go index 9c10c9940ad..0cd547e562d 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 @@ -139,14 +141,13 @@ type LabelUIConfig struct { // DefaultUIConfig returns the canonical defaults for the Nomad // `ui` configuration. func DefaultUIConfig() *UIConfig { - enabled := true return &UIConfig{ - Enabled: enabled, + Enabled: true, Consul: &ConsulUIConfig{}, Vault: &VaultUIConfig{}, Label: &LabelUIConfig{}, ContentSecurityPolicy: DefaultCSPConfig(), - ShowCLIHints: &enabled, + ShowCLIHints: pointer.Of(true), } } diff --git a/website/content/docs/configuration/ui.mdx b/website/content/docs/configuration/ui.mdx index 5c33b0dd259..29b67304a4c 100644 --- a/website/content/docs/configuration/ui.mdx +++ b/website/content/docs/configuration/ui.mdx @@ -60,7 +60,7 @@ and the configuration is individual to each agent. - `show_cli_hints` `(bool: true)` - Controls whether CLI commands display hints about equivalent UI pages. For example, when running `nomad server members`, - the CLI will show a message indicating where to find server information in + 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 @@ -111,10 +111,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. From 32bd07387f9432689eb26621de2bb9fb926f5c78 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Wed, 8 Jan 2025 16:53:59 -0500 Subject: [PATCH 18/18] CLI docs available flags now all have -ui --- command/eval_list.go | 2 +- command/job_run.go | 2 +- command/var_get.go | 2 +- command/var_put.go | 2 +- website/content/docs/commands/alloc/status.mdx | 1 + website/content/docs/commands/deployment/status.mdx | 7 ++++--- website/content/docs/commands/eval/list.mdx | 1 + website/content/docs/commands/eval/status.mdx | 1 + website/content/docs/commands/job/dispatch.mdx | 2 ++ website/content/docs/commands/job/run.mdx | 2 ++ website/content/docs/commands/job/status.mdx | 4 +++- website/content/docs/commands/node/status.mdx | 2 ++ website/content/docs/commands/server/members.mdx | 4 +++- website/content/docs/commands/var/get.mdx | 2 ++ website/content/docs/commands/var/list.mdx | 2 ++ website/content/docs/commands/var/put.mdx | 2 ++ 16 files changed, 29 insertions(+), 9 deletions(-) diff --git a/command/eval_list.go b/command/eval_list.go index 24b8c181e30..7cb00290ac5 100644 --- a/command/eval_list.go +++ b/command/eval_list.go @@ -54,7 +54,7 @@ Eval List Options: Format and display evaluation using a Go template. -ui - Open the evaluation in the browser. + Open the evaluations page in the browser. ` return strings.TrimSpace(helpText) diff --git a/command/job_run.go b/command/job_run.go index 642237f9800..8fdeae8aef6 100644 --- a/command/job_run.go +++ b/command/job_run.go @@ -107,7 +107,7 @@ Run Options: the job. -ui - Open the job run page in the browser. + Open the job page in the browser. -policy-override Sets the flag to force override any soft mandatory Sentinel policies. diff --git a/command/var_get.go b/command/var_get.go index 04d9aa1399a..d831e044e2e 100644 --- a/command/var_get.go +++ b/command/var_get.go @@ -50,7 +50,7 @@ Get Options: Template to render output with. Required when output is "go-template". -ui - Open the variable get page in the browser. + Open the variable page in the browser. ` return strings.TrimSpace(helpText) diff --git a/command/var_put.go b/command/var_put.go index 3492f2d14db..b026b8bf095 100644 --- a/command/var_put.go +++ b/command/var_put.go @@ -103,7 +103,7 @@ Var put Options: output (stdout) for redirected output. -ui - Open the variable put page in the browser. + Open the variable page in the browser. ` return strings.TrimSpace(helpText) diff --git a/website/content/docs/commands/alloc/status.mdx b/website/content/docs/commands/alloc/status.mdx index 966bbf97315..0aa7066fe99 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 86542733e2f..0f4ec79ad1b 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 1081d2f529a..17f63ac0bd1 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 663c3a562c5..94078b9bb96 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/job/dispatch.mdx b/website/content/docs/commands/job/dispatch.mdx index 235cc952c32..b52c7926934 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 b8dad76e8e3..30d986ccbf2 100644 --- a/website/content/docs/commands/job/run.mdx +++ b/website/content/docs/commands/job/run.mdx @@ -123,6 +123,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 7706775ca02..3b6a94f572e 100644 --- a/website/content/docs/commands/job/status.mdx +++ b/website/content/docs/commands/job/status.mdx @@ -21,7 +21,7 @@ the specific job is queried and displayed. Otherwise, a list of matching jobs and information will be displayed. If the ID is omitted, the command lists out all of the existing jobs and a few -of the most useful status fields for each. Alloc status also shows allocation +of the most useful status fields for each. Alloc status also shows allocation modification time in addition to create time. When the `-verbose` flag is not set, allocation creation and modify times are shown in a shortened relative time format like `5m ago`. @@ -51,6 +51,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 d6c2488c106..daa67d3559d 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 3ace4d8db82..c68421476ff 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 3639942887c..06189a6e2fc 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 d8974ffff2c..93ec0499aa2 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 34a361ad3a7..824a20a7224 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":