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