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