279 lines
6.8 KiB
Go
279 lines
6.8 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
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.
|
|
|
|
`
|
|
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,
|
|
},
|
|
)
|
|
}
|
|
|
|
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
|
|
|
|
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", "", "")
|
|
|
|
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))
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|