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

* Commands for alloc status and job status

* -ui flag for most commands

* url hints for variables

* url hints for job dispatch, evals, and deployments

* agent config ui.cli_url_links to disable

* Fix an issue where path prefix was presumed for variables

* driver uncomment and general cleanup

* -ui flag on the generic status endpoint

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

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

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

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

* Parallel outside of test instances

* Browser-opening test, sorta

* Env var for disabling/enabling CLI hints

* Addressing a few PR comments

* CLI docs available flags now all have -ui

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

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

* isTerminal check and parseBool on command option

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

306 lines
7.4 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"errors"
"fmt"
"os"
"sort"
"strings"
"github.com/hashicorp/nomad/api"
"github.com/posener/complete"
)
const (
msgWarnFilterPerformance = "Filter queries require a full scan of the data; use prefix searching where possible"
)
type VarListCommand struct {
prefix string
outFmt string
tmpl string
Meta
}
func (c *VarListCommand) Help() string {
helpText := `
Usage: nomad var list [options] <prefix>
List is used to list available variables. Supplying an optional prefix,
filters the list to variables having a path starting with the prefix.
When using pagination, the next page token is provided in the JSON output
or as a message to standard error to leave standard output for the listed
variables from that page.
If ACLs are enabled, this command will only return variables stored in
namespaces and paths where the token has the 'variables:list' capability.
General Options:
` + generalOptionsUsage(usageOptsDefault) + `
List Options:
-per-page
How many results to show per page.
-page-token
Where to start pagination.
-filter
Specifies an expression used to filter query results. Queries using this
option are less efficient than using the prefix parameter; therefore,
the prefix parameter should be used whenever possible.
-out (go-template | json | table | terse )
Format to render created or updated variable. Defaults to "none" when
stdout is a terminal and "json" when the output is redirected. The "terse"
format outputs as little information as possible to uniquely identify a
variable depending on whether or not the wildcard namespace was passed.
-template
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)
}
func (c *VarListCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-out": complete.PredictSet("go-template", "json", "terse", "table"),
"-template": complete.PredictAnything,
"-ui": complete.PredictNothing,
},
)
}
func (c *VarListCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictNothing
}
func (c *VarListCommand) Synopsis() string {
return "List variable metadata"
}
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()) }
flags.StringVar(&c.tmpl, "template", "", "")
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", "")
} else {
flags.StringVar(&c.outFmt, "out", "json", "")
}
if err := flags.Parse(args); err != nil {
return 1
}
// Check that we got no arguments
args = flags.Args()
if l := len(args); l > 1 {
c.Ui.Error("This command takes flags and either no arguments or one: <prefix>")
c.Ui.Error(commandErrorText(c))
return 1
}
if len(args) == 1 {
prefix = args[0]
}
if err := c.validateOutputFlag(); err != nil {
c.Ui.Error(err.Error())
c.Ui.Error(commandErrorText(c))
return 1
}
// Get the HTTP client
client, err := c.Meta.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}
if filter != "" {
c.Ui.Warn(msgWarnFilterPerformance)
}
qo := &api.QueryOptions{
Filter: filter,
PerPage: int32(perPage),
NextToken: pageToken,
Params: map[string]string{},
}
vars, qm, err := client.Variables().PrefixList(prefix, qo)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error retrieving vars: %s", err))
return 1
}
switch c.outFmt {
case "json":
// obj and items enable us to rework the output before sending it
// to the Format method for transformation into JSON.
var obj, items interface{}
obj = vars
items = vars
// If the response is paginated, we need to provide a means for the
// caller to get to the pagination information. Wrapping the list
// in a struct for the special case allows this extra data without
// adding unnecessary structure in the non-paginated case.
if perPage > 0 {
obj = struct {
Data interface{}
QueryMeta *api.QueryMeta
}{
items,
qm,
}
}
// By this point, the output is ready to be transformed to JSON via
// the Format func.
out, err := Format(true, "", obj)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
c.Ui.Output(out)
// Since the JSON formatting deals with the pagination information
// itself, exit the command here so that it doesn't double print.
return 0
case "terse":
c.Ui.Output(
formatList(
dataToQuietStringSlice(vars, c.Meta.namespace)))
case "go-template":
out, err := Format(false, c.tmpl, vars)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
c.Ui.Output(out)
default:
c.Ui.Output(formatVarStubs(vars))
}
if qm.NextToken != "" {
// This uses Ui.Warn to output the next page token to stderr
// so that scripts consuming paths from stdout will not have
// to special case the output.
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
}
func formatVarStubs(vars []*api.VariableMetadata) string {
if len(vars) == 0 {
return errNoMatchingVariables
}
// Sort the output by variable namespace, path
sort.Slice(vars, func(i, j int) bool {
if vars[i].Namespace == vars[j].Namespace {
return vars[i].Path < vars[j].Path
}
return vars[i].Namespace < vars[j].Namespace
})
rows := make([]string, len(vars)+1)
rows[0] = "Namespace|Path|Last Updated"
for i, sv := range vars {
rows[i+1] = fmt.Sprintf("%s|%s|%s",
sv.Namespace,
sv.Path,
formatUnixNanoTime(sv.ModifyTime),
)
}
return formatList(rows)
}
func dataToQuietStringSlice(vars []*api.VariableMetadata, ns string) []string {
// If ns is the wildcard namespace, we have to provide namespace
// as part of the quiet output, otherwise it can be a simple list
// of paths.
toPathStr := func(v *api.VariableMetadata) string {
if ns == "*" {
return fmt.Sprintf("%s|%s", v.Namespace, v.Path)
}
return v.Path
}
// Reduce the items slice to a string slice containing only the
// variable paths.
pList := make([]string, len(vars))
for i, sv := range vars {
pList[i] = toPathStr(sv)
}
return pList
}
func (c *VarListCommand) validateOutputFlag() error {
if c.outFmt != "go-template" && c.tmpl != "" {
return errors.New(errUnexpectedTemplate)
}
switch c.outFmt {
case "json", "terse", "table":
return nil
case "go-template":
if c.tmpl == "" {
return errors.New(errMissingTemplate)
}
return nil
default:
return errors.New(errInvalidListOutFormat)
}
}