open-nomad/command/var_list.go
Charlie Voiselle 6be7a41351
SV: CLI: var list command (#13707)
* SV CLI: var list
* Fix wildcard prefix filtering

Co-authored-by: Tim Gross <tgross@hashicorp.com>
2022-07-12 12:49:39 -04:00

277 lines
6.6 KiB
Go

package command
import (
"fmt"
"sort"
"strings"
"time"
"github.com/hashicorp/nomad/api"
"github.com/posener/complete"
)
const (
msgSecureVariableNotFound = "No matching secure variables found"
msgWarnFilterPerformance = "Filter queries require a full scan of the data; use prefix searching where possible"
)
type VarListCommand struct {
Prefix string
Meta
}
func (c *VarListCommand) Help() string {
helpText := `
Usage: nomad var list [options] <prefix>
List is used to list available secure variables. Supplying an optional prefix,
filters the list to variables having a path starting with the prefix.
If ACLs are enabled, this command will return only secure variables stored at
namespaced paths where the token has the ` + "`read`" + ` 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.
-json
Output the secure variables in JSON format.
-t
Format and display the secure variables using a Go template.
-q
Output matching secure variable paths with no additional information.
This option overrides the ` + "`-t`" + ` option.
`
return strings.TrimSpace(helpText)
}
func (c *VarListCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-json": complete.PredictNothing,
"-t": complete.PredictAnything,
},
)
}
func (c *VarListCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictNothing
}
func (c *VarListCommand) Synopsis() string {
return "List secure variable metadata"
}
func (c *VarListCommand) Name() string { return "var list" }
func (c *VarListCommand) Run(args []string) int {
var json, quiet bool
var perPage int
var tmpl, pageToken, filter, prefix string
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.BoolVar(&quiet, "q", false, "")
flags.BoolVar(&json, "json", false, "")
flags.StringVar(&tmpl, "t", "", "")
flags.IntVar(&perPage, "per-page", 0, "")
flags.StringVar(&pageToken, "page-token", "", "")
flags.StringVar(&filter, "filter", "", "")
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]
}
// 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.SecureVariables().PrefixList(prefix, qo)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error retrieving vars: %s", err))
return 1
}
switch {
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 quiet {
items = dataToQuietJSONReadySlice(vars, c.Meta.namespace)
obj = items
}
// 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(json, tmpl, 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 quiet:
c.Ui.Output(
formatList(
dataToQuietStringSlice(vars, c.Meta.namespace)))
case len(tmpl) > 0:
out, err := Format(json, 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.SecureVariableMetadata) string {
if len(vars) == 0 {
return msgSecureVariableNotFound
}
// 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,
time.Unix(0, sv.ModifyTime),
)
}
return formatList(rows)
}
func dataToQuietStringSlice(vars []*api.SecureVariableMetadata, 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.SecureVariableMetadata) 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 dataToQuietJSONReadySlice(vars []*api.SecureVariableMetadata, ns string) interface{} {
// 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.
if ns == "*" {
type pTuple struct {
Namespace string
Path string
}
pList := make([]*pTuple, len(vars))
for i, sv := range vars {
pList[i] = &pTuple{sv.Namespace, sv.Path}
}
return pList
}
// 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] = sv.Path
}
return pList
}