// 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] 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: ") 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) } }