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  )