github.com/hernad/nomad@v1.6.112/command/var.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package command 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "io" 12 "os" 13 "sort" 14 "strings" 15 "text/template" 16 "time" 17 18 "github.com/hernad/nomad/api" 19 "github.com/hernad/nomad/api/contexts" 20 "github.com/mitchellh/cli" 21 "github.com/mitchellh/colorstring" 22 "github.com/mitchellh/mapstructure" 23 "github.com/posener/complete" 24 ) 25 26 type VarCommand struct { 27 Meta 28 } 29 30 func (f *VarCommand) Help() string { 31 helpText := ` 32 Usage: nomad var <subcommand> [options] [args] 33 34 This command groups subcommands for interacting with variables. Variables 35 allow operators to provide credentials and otherwise sensitive material to 36 Nomad jobs at runtime via the template block or directly through 37 the Nomad API and CLI. 38 39 Users can create new variables; list, inspect, and delete existing 40 variables, and more. For a full guide on variables see: 41 https://www.nomadproject.io/docs/concepts/variables 42 43 Create a variable specification file: 44 45 $ nomad var init 46 47 Upsert a variable: 48 49 $ nomad var put <path> 50 51 Examine a variable: 52 53 $ nomad var get <path> 54 55 List existing variables: 56 57 $ nomad var list <prefix> 58 59 Purge a variable: 60 61 $ nomad var purge <path> 62 63 Please see the individual subcommand help for detailed usage information. 64 ` 65 66 return strings.TrimSpace(helpText) 67 } 68 69 func (f *VarCommand) Synopsis() string { 70 return "Interact with variables" 71 } 72 73 func (f *VarCommand) Name() string { return "var" } 74 75 func (f *VarCommand) Run(args []string) int { 76 return cli.RunResultHelp 77 } 78 79 // VariablePathPredictor returns a var predictor 80 func VariablePathPredictor(factory ApiClientFactory) complete.Predictor { 81 return complete.PredictFunc(func(a complete.Args) []string { 82 client, err := factory() 83 if err != nil { 84 return nil 85 } 86 87 resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Variables, nil) 88 if err != nil { 89 return []string{} 90 } 91 return resp.Matches[contexts.Variables] 92 }) 93 } 94 95 type VarUI interface { 96 GetConcurrentUI() cli.ConcurrentUi 97 Colorize() *colorstring.Colorize 98 } 99 100 // renderSVAsUiTable prints a variable as a table. It needs access to the 101 // command to get access to colorize and the UI itself. Commands that call it 102 // need to implement the VarUI interface. 103 func renderSVAsUiTable(sv *api.Variable, c VarUI) { 104 meta := []string{ 105 fmt.Sprintf("Namespace|%s", sv.Namespace), 106 fmt.Sprintf("Path|%s", sv.Path), 107 fmt.Sprintf("Create Time|%v", formatUnixNanoTime(sv.ModifyTime)), 108 } 109 if sv.CreateTime != sv.ModifyTime { 110 meta = append(meta, fmt.Sprintf("Modify Time|%v", time.Unix(0, sv.ModifyTime))) 111 } 112 meta = append(meta, fmt.Sprintf("Check Index|%v", sv.ModifyIndex)) 113 ui := c.GetConcurrentUI() 114 ui.Output(formatKV(meta)) 115 ui.Output(c.Colorize().Color("\n[bold]Items[reset]")) 116 items := make([]string, 0, len(sv.Items)) 117 118 keys := make([]string, 0, len(sv.Items)) 119 for k := range sv.Items { 120 keys = append(keys, k) 121 } 122 sort.Strings(keys) 123 124 for _, k := range keys { 125 items = append(items, fmt.Sprintf("%s|%s", k, sv.Items[k])) 126 } 127 ui.Output(formatKV(items)) 128 } 129 130 func renderAsHCL(sv *api.Variable) string { 131 const tpl = ` 132 namespace = "{{.Namespace}}" 133 path = "{{.Path}}" 134 create_index = {{.CreateIndex}} # Set by server 135 modify_index = {{.ModifyIndex}} # Set by server; consulted for check-and-set 136 create_time = {{.CreateTime}} # Set by server 137 modify_time = {{.ModifyTime}} # Set by server 138 139 items = { 140 {{- $PAD := 0 -}}{{- range $k,$v := .Items}}{{if gt (len $k) $PAD}}{{$PAD = (len $k)}}{{end}}{{end -}} 141 {{- $FMT := printf " %%%vs = %%q\n" $PAD}} 142 {{range $k,$v := .Items}}{{printf $FMT $k $v}}{{ end -}} 143 } 144 ` 145 out, err := renderWithGoTemplate(sv, tpl) 146 if err != nil { 147 // Any errors in this should be caught as test panics. 148 // If we ship with one, the worst case is that it panics a single 149 // run of the CLI and only for output of variables in HCL. 150 panic(err) 151 } 152 return out 153 } 154 155 func renderWithGoTemplate(sv *api.Variable, tpl string) (string, error) { 156 //TODO: Enhance this to take a template as an @-aliased filename too 157 t := template.Must(template.New("var").Parse(tpl)) 158 var out bytes.Buffer 159 if err := t.Execute(&out, sv); err != nil { 160 return "", err 161 } 162 163 result := out.String() 164 return result, nil 165 } 166 167 // KVBuilder is a struct to build a key/value mapping based on a list 168 // of "k=v" pairs, where the value might come from stdin, a file, etc. 169 type KVBuilder struct { 170 Stdin io.Reader 171 172 result map[string]interface{} 173 stdin bool 174 } 175 176 // Map returns the built map. 177 func (b *KVBuilder) Map() map[string]interface{} { 178 return b.result 179 } 180 181 // Add adds to the mapping with the given args. 182 func (b *KVBuilder) Add(args ...string) error { 183 for _, a := range args { 184 if err := b.add(a); err != nil { 185 return fmt.Errorf("invalid key/value pair %q: %w", a, err) 186 } 187 } 188 189 return nil 190 } 191 192 func (b *KVBuilder) add(raw string) error { 193 // Regardless of validity, make sure we make our result 194 if b.result == nil { 195 b.result = make(map[string]interface{}) 196 } 197 198 // Empty strings are fine, just ignored 199 if raw == "" { 200 return nil 201 } 202 203 // Split into key/value 204 parts := strings.SplitN(raw, "=", 2) 205 206 // If the arg is exactly "-", then we need to read from stdin 207 // and merge the results into the resulting structure. 208 if len(parts) == 1 { 209 if raw == "-" { 210 if b.Stdin == nil { 211 return fmt.Errorf("stdin is not supported") 212 } 213 if b.stdin { 214 return fmt.Errorf("stdin already consumed") 215 } 216 217 b.stdin = true 218 return b.addReader(b.Stdin) 219 } 220 221 // If the arg begins with "@" then we need to read a file directly 222 if raw[0] == '@' { 223 f, err := os.Open(raw[1:]) 224 if err != nil { 225 return err 226 } 227 defer f.Close() 228 229 return b.addReader(f) 230 } 231 } 232 233 if len(parts) != 2 { 234 return fmt.Errorf("format must be key=value") 235 } 236 key, value := parts[0], parts[1] 237 238 if len(value) > 0 { 239 if value[0] == '@' { 240 contents, err := os.ReadFile(value[1:]) 241 if err != nil { 242 return fmt.Errorf("error reading file: %w", err) 243 } 244 245 value = string(contents) 246 } else if value[0] == '\\' && value[1] == '@' { 247 value = value[1:] 248 } else if value == "-" { 249 if b.Stdin == nil { 250 return fmt.Errorf("stdin is not supported") 251 } 252 if b.stdin { 253 return fmt.Errorf("stdin already consumed") 254 } 255 b.stdin = true 256 257 var buf bytes.Buffer 258 if _, err := io.Copy(&buf, b.Stdin); err != nil { 259 return err 260 } 261 262 value = buf.String() 263 } 264 } 265 266 // Repeated keys will be converted into a slice 267 if existingValue, ok := b.result[key]; ok { 268 var sliceValue []interface{} 269 if err := mapstructure.WeakDecode(existingValue, &sliceValue); err != nil { 270 return err 271 } 272 sliceValue = append(sliceValue, value) 273 b.result[key] = sliceValue 274 return nil 275 } 276 277 b.result[key] = value 278 return nil 279 } 280 281 func (b *KVBuilder) addReader(r io.Reader) error { 282 if r == nil { 283 return fmt.Errorf("'io.Reader' being decoded is nil") 284 } 285 286 dec := json.NewDecoder(r) 287 // While decoding JSON values, interpret the integer values as 288 // `json.Number`s instead of `float64`. 289 dec.UseNumber() 290 291 return dec.Decode(&b.result) 292 } 293 294 // handleCASError provides consistent output for operations that result in a 295 // check-and-set error 296 func handleCASError(err error, c VarUI) (handled bool) { 297 ui := c.GetConcurrentUI() 298 var cErr api.ErrCASConflict 299 if errors.As(err, &cErr) { 300 lastUpdate := "" 301 if cErr.Conflict.ModifyIndex > 0 { 302 lastUpdate = fmt.Sprintf( 303 tidyRawString(msgfmtCASConflictLastAccess), 304 formatUnixNanoTime(cErr.Conflict.ModifyTime)) 305 } 306 ui.Error(c.Colorize().Color("\n[bold][underline]Check-and-Set conflict[reset]\n")) 307 ui.Warn( 308 wrapAndPrepend( 309 c.Colorize().Color( 310 fmt.Sprintf( 311 tidyRawString(msgfmtCASMismatch), 312 cErr.CheckIndex, 313 cErr.Conflict.ModifyIndex, 314 lastUpdate), 315 ), 316 80, " ") + "\n", 317 ) 318 handled = true 319 } 320 return 321 } 322 323 const ( 324 errMissingTemplate = `A template must be supplied using '-template' when using go-template formatting` 325 errUnexpectedTemplate = `The '-template' flag is only valid when using 'go-template' formatting` 326 errVariableNotFound = `Variable not found` 327 errNoMatchingVariables = `No matching variables found` 328 errInvalidInFormat = `Invalid value for "-in"; valid values are [hcl, json]` 329 errInvalidOutFormat = `Invalid value for "-out"; valid values are [go-template, hcl, json, none, table]` 330 errInvalidListOutFormat = `Invalid value for "-out"; valid values are [go-template, json, table, terse]` 331 errWildcardNamespaceNotAllowed = `The wildcard namespace ("*") is not valid for this command.` 332 333 msgfmtCASMismatch = ` 334 Your provided check-index [green](%v)[yellow] does not match the 335 server-side index [green](%v)[yellow]. 336 %s 337 If you are sure you want to perform this operation, add the [green]-force[yellow] or 338 [green]-check-index=%[2]v[yellow] flag before the positional arguments.` 339 340 msgfmtCASConflictLastAccess = ` 341 The server-side item was last updated on [green]%s[yellow]. 342 ` 343 )