github.com/hernad/nomad@v1.6.112/command/var_list.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package command 5 6 import ( 7 "errors" 8 "fmt" 9 "os" 10 "sort" 11 "strings" 12 13 "github.com/hernad/nomad/api" 14 "github.com/posener/complete" 15 ) 16 17 const ( 18 msgWarnFilterPerformance = "Filter queries require a full scan of the data; use prefix searching where possible" 19 ) 20 21 type VarListCommand struct { 22 prefix string 23 outFmt string 24 tmpl string 25 Meta 26 } 27 28 func (c *VarListCommand) Help() string { 29 helpText := ` 30 Usage: nomad var list [options] <prefix> 31 32 List is used to list available variables. Supplying an optional prefix, 33 filters the list to variables having a path starting with the prefix. 34 When using pagination, the next page token is provided in the JSON output 35 or as a message to standard error to leave standard output for the listed 36 variables from that page. 37 38 If ACLs are enabled, this command will only return variables stored in 39 namespaces and paths where the token has the 'variables:list' capability. 40 41 General Options: 42 43 ` + generalOptionsUsage(usageOptsDefault) + ` 44 45 List Options: 46 47 -per-page 48 How many results to show per page. 49 50 -page-token 51 Where to start pagination. 52 53 -filter 54 Specifies an expression used to filter query results. Queries using this 55 option are less efficient than using the prefix parameter; therefore, 56 the prefix parameter should be used whenever possible. 57 58 -out (go-template | json | table | terse ) 59 Format to render created or updated variable. Defaults to "none" when 60 stdout is a terminal and "json" when the output is redirected. The "terse" 61 format outputs as little information as possible to uniquely identify a 62 variable depending on whether or not the wildcard namespace was passed. 63 64 -template 65 Template to render output with. Required when format is "go-template", 66 invalid for other formats. 67 68 ` 69 return strings.TrimSpace(helpText) 70 } 71 72 func (c *VarListCommand) AutocompleteFlags() complete.Flags { 73 return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), 74 complete.Flags{ 75 "-out": complete.PredictSet("go-template", "json", "terse", "table"), 76 "-template": complete.PredictAnything, 77 }, 78 ) 79 } 80 81 func (c *VarListCommand) AutocompleteArgs() complete.Predictor { 82 return complete.PredictNothing 83 } 84 85 func (c *VarListCommand) Synopsis() string { 86 return "List variable metadata" 87 } 88 89 func (c *VarListCommand) Name() string { return "var list" } 90 func (c *VarListCommand) Run(args []string) int { 91 var perPage int 92 var pageToken, filter, prefix string 93 94 flags := c.Meta.FlagSet(c.Name(), FlagSetClient) 95 flags.Usage = func() { c.Ui.Output(c.Help()) } 96 flags.StringVar(&c.tmpl, "template", "", "") 97 98 flags.IntVar(&perPage, "per-page", 0, "") 99 flags.StringVar(&pageToken, "page-token", "", "") 100 flags.StringVar(&filter, "filter", "", "") 101 102 if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 { 103 flags.StringVar(&c.outFmt, "out", "table", "") 104 } else { 105 flags.StringVar(&c.outFmt, "out", "json", "") 106 } 107 108 if err := flags.Parse(args); err != nil { 109 return 1 110 } 111 112 // Check that we got no arguments 113 args = flags.Args() 114 if l := len(args); l > 1 { 115 c.Ui.Error("This command takes flags and either no arguments or one: <prefix>") 116 c.Ui.Error(commandErrorText(c)) 117 return 1 118 } 119 120 if len(args) == 1 { 121 prefix = args[0] 122 } 123 124 if err := c.validateOutputFlag(); err != nil { 125 c.Ui.Error(err.Error()) 126 c.Ui.Error(commandErrorText(c)) 127 return 1 128 } 129 130 // Get the HTTP client 131 client, err := c.Meta.Client() 132 if err != nil { 133 c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) 134 return 1 135 } 136 137 if filter != "" { 138 c.Ui.Warn(msgWarnFilterPerformance) 139 } 140 141 qo := &api.QueryOptions{ 142 Filter: filter, 143 PerPage: int32(perPage), 144 NextToken: pageToken, 145 Params: map[string]string{}, 146 } 147 148 vars, qm, err := client.Variables().PrefixList(prefix, qo) 149 if err != nil { 150 c.Ui.Error(fmt.Sprintf("Error retrieving vars: %s", err)) 151 return 1 152 } 153 154 switch c.outFmt { 155 case "json": 156 // obj and items enable us to rework the output before sending it 157 // to the Format method for transformation into JSON. 158 var obj, items interface{} 159 obj = vars 160 items = vars 161 162 // If the response is paginated, we need to provide a means for the 163 // caller to get to the pagination information. Wrapping the list 164 // in a struct for the special case allows this extra data without 165 // adding unnecessary structure in the non-paginated case. 166 if perPage > 0 { 167 obj = struct { 168 Data interface{} 169 QueryMeta *api.QueryMeta 170 }{ 171 items, 172 qm, 173 } 174 } 175 176 // By this point, the output is ready to be transformed to JSON via 177 // the Format func. 178 out, err := Format(true, "", obj) 179 if err != nil { 180 c.Ui.Error(err.Error()) 181 return 1 182 } 183 184 c.Ui.Output(out) 185 186 // Since the JSON formatting deals with the pagination information 187 // itself, exit the command here so that it doesn't double print. 188 return 0 189 190 case "terse": 191 c.Ui.Output( 192 formatList( 193 dataToQuietStringSlice(vars, c.Meta.namespace))) 194 195 case "go-template": 196 out, err := Format(false, c.tmpl, vars) 197 if err != nil { 198 c.Ui.Error(err.Error()) 199 return 1 200 } 201 c.Ui.Output(out) 202 203 default: 204 c.Ui.Output(formatVarStubs(vars)) 205 } 206 207 if qm.NextToken != "" { 208 // This uses Ui.Warn to output the next page token to stderr 209 // so that scripts consuming paths from stdout will not have 210 // to special case the output. 211 c.Ui.Warn(fmt.Sprintf("Next page token: %s", qm.NextToken)) 212 } 213 214 return 0 215 } 216 217 func formatVarStubs(vars []*api.VariableMetadata) string { 218 if len(vars) == 0 { 219 return errNoMatchingVariables 220 } 221 222 // Sort the output by variable namespace, path 223 sort.Slice(vars, func(i, j int) bool { 224 if vars[i].Namespace == vars[j].Namespace { 225 return vars[i].Path < vars[j].Path 226 } 227 return vars[i].Namespace < vars[j].Namespace 228 }) 229 230 rows := make([]string, len(vars)+1) 231 rows[0] = "Namespace|Path|Last Updated" 232 for i, sv := range vars { 233 rows[i+1] = fmt.Sprintf("%s|%s|%s", 234 sv.Namespace, 235 sv.Path, 236 formatUnixNanoTime(sv.ModifyTime), 237 ) 238 } 239 return formatList(rows) 240 } 241 242 func dataToQuietStringSlice(vars []*api.VariableMetadata, ns string) []string { 243 // If ns is the wildcard namespace, we have to provide namespace 244 // as part of the quiet output, otherwise it can be a simple list 245 // of paths. 246 toPathStr := func(v *api.VariableMetadata) string { 247 if ns == "*" { 248 return fmt.Sprintf("%s|%s", v.Namespace, v.Path) 249 } 250 return v.Path 251 } 252 253 // Reduce the items slice to a string slice containing only the 254 // variable paths. 255 pList := make([]string, len(vars)) 256 for i, sv := range vars { 257 pList[i] = toPathStr(sv) 258 } 259 260 return pList 261 } 262 263 func (c *VarListCommand) validateOutputFlag() error { 264 if c.outFmt != "go-template" && c.tmpl != "" { 265 return errors.New(errUnexpectedTemplate) 266 } 267 switch c.outFmt { 268 case "json", "terse", "table": 269 return nil 270 case "go-template": 271 if c.tmpl == "" { 272 return errors.New(errMissingTemplate) 273 } 274 return nil 275 default: 276 return errors.New(errInvalidListOutFormat) 277 } 278 }