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  )