Skip to content

Commit

Permalink
Vars: CLI commands for var get, var put, var purge (#14400)
Browse files Browse the repository at this point in the history
* Includes updates to `var init`
  • Loading branch information
angrycub authored Sep 9, 2022
1 parent 8ff79d8 commit 39a3fd6
Show file tree
Hide file tree
Showing 15 changed files with 1,791 additions and 67 deletions.
26 changes: 13 additions & 13 deletions api/variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,36 +313,36 @@ func (sv *Variables) writeChecked(endpoint string, in *Variable, out *Variable,
// encrypted Nomad backend.
type Variable struct {
// Namespace is the Nomad namespace associated with the variable
Namespace string
Namespace string `hcl:"namespace"`
// Path is the path to the variable
Path string
Path string `hcl:"path"`

// Raft indexes to track creation and modification
CreateIndex uint64
ModifyIndex uint64
CreateIndex uint64 `hcl:"create_index"`
ModifyIndex uint64 `hcl:"modify_index"`

// Times provided as a convenience for operators expressed time.UnixNanos
CreateTime int64
ModifyTime int64
CreateTime int64 `hcl:"create_time"`
ModifyTime int64 `hcl:"modify_time"`

Items VariableItems
Items VariableItems `hcl:"items"`
}

// VariableMetadata specifies the metadata for a variable and
// is used as the list object
type VariableMetadata struct {
// Namespace is the Nomad namespace associated with the variable
Namespace string
Namespace string `hcl:"namespace"`
// Path is the path to the variable
Path string
Path string `hcl:"path"`

// Raft indexes to track creation and modification
CreateIndex uint64
ModifyIndex uint64
CreateIndex uint64 `hcl:"create_index"`
ModifyIndex uint64 `hcl:"modify_index"`

// Times provided as a convenience for operators expressed time.UnixNanos
CreateTime int64
ModifyTime int64
CreateTime int64 `hcl:"create_time"`
ModifyTime int64 `hcl:"modify_time"`
}

type VariableItems map[string]string
Expand Down
19 changes: 17 additions & 2 deletions command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -936,8 +936,8 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory {
Meta: meta,
}, nil
},
"var list": func() (cli.Command, error) {
return &VarListCommand{
"var purge": func() (cli.Command, error) {
return &VarPurgeCommand{
Meta: meta,
}, nil
},
Expand All @@ -946,6 +946,21 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory {
Meta: meta,
}, nil
},
"var list": func() (cli.Command, error) {
return &VarListCommand{
Meta: meta,
}, nil
},
"var put": func() (cli.Command, error) {
return &VarPutCommand{
Meta: meta,
}, nil
},
"var get": func() (cli.Command, error) {
return &VarGetCommand{
Meta: meta,
}, nil
},
"version": func() (cli.Command, error) {
return &VersionCommand{
Version: version.GetVersion(),
Expand Down
265 changes: 265 additions & 0 deletions command/var.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
package command

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"sort"
"strings"
"text/template"
"time"

"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/api/contexts"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
"github.com/mitchellh/mapstructure"
"github.com/posener/complete"
)

Expand Down Expand Up @@ -41,6 +54,10 @@ Usage: nomad var <subcommand> [options] [args]
$ nomad var list <prefix>
Purge a variable:
$ nomad var purge <path>
Please see the individual subcommand help for detailed usage information.
`

Expand Down Expand Up @@ -72,3 +89,251 @@ func VariablePathPredictor(factory ApiClientFactory) complete.Predictor {
return resp.Matches[contexts.Variables]
})
}

type VarUI interface {
GetConcurrentUI() cli.ConcurrentUi
Colorize() *colorstring.Colorize
}

// renderSVAsUiTable prints a variable as a table. It needs access to the
// command to get access to colorize and the UI itself. Commands that call it
// need to implement the VarUI interface.
func renderSVAsUiTable(sv *api.Variable, c VarUI) {
meta := []string{
fmt.Sprintf("Namespace|%s", sv.Namespace),
fmt.Sprintf("Path|%s", sv.Path),
fmt.Sprintf("Create Time|%v", formatUnixNanoTime(sv.ModifyTime)),
}
if sv.CreateTime != sv.ModifyTime {
meta = append(meta, fmt.Sprintf("Modify Time|%v", time.Unix(0, sv.ModifyTime)))
}
meta = append(meta, fmt.Sprintf("Check Index|%v", sv.ModifyIndex))
ui := c.GetConcurrentUI()
ui.Output(formatKV(meta))
ui.Output(c.Colorize().Color("\n[bold]Items[reset]"))
items := make([]string, 0, len(sv.Items))

keys := make([]string, 0, len(sv.Items))
for k := range sv.Items {
keys = append(keys, k)
}
sort.Strings(keys)

for _, k := range keys {
items = append(items, fmt.Sprintf("%s|%s", k, sv.Items[k]))
}
ui.Output(formatKV(items))
}

func renderAsHCL(sv *api.Variable) string {
const tpl = `
namespace = "{{.Namespace}}"
path = "{{.Path}}"
create_index = {{.CreateIndex}} # Set by server
modify_index = {{.ModifyIndex}} # Set by server; consulted for check-and-set
create_time = {{.CreateTime}} # Set by server
modify_time = {{.ModifyTime}} # Set by server
items = {
{{- $PAD := 0 -}}{{- range $k,$v := .Items}}{{if gt (len $k) $PAD}}{{$PAD = (len $k)}}{{end}}{{end -}}
{{- $FMT := printf " %%%vs = %%q\n" $PAD}}
{{range $k,$v := .Items}}{{printf $FMT $k $v}}{{ end -}}
}
`
out, err := renderWithGoTemplate(sv, tpl)
if err != nil {
// Any errors in this should be caught as test panics.
// If we ship with one, the worst case is that it panics a single
// run of the CLI and only for output of variables in HCL.
panic(err)
}
return out
}

func renderWithGoTemplate(sv *api.Variable, tpl string) (string, error) {
//TODO: Enhance this to take a template as an @-aliased filename too
t := template.Must(template.New("var").Parse(tpl))
var out bytes.Buffer
if err := t.Execute(&out, sv); err != nil {
return "", err
}

result := out.String()
return result, nil
}

// KVBuilder is a struct to build a key/value mapping based on a list
// of "k=v" pairs, where the value might come from stdin, a file, etc.
type KVBuilder struct {
Stdin io.Reader

result map[string]interface{}
stdin bool
}

// Map returns the built map.
func (b *KVBuilder) Map() map[string]interface{} {
return b.result
}

// Add adds to the mapping with the given args.
func (b *KVBuilder) Add(args ...string) error {
for _, a := range args {
if err := b.add(a); err != nil {
return fmt.Errorf("invalid key/value pair %q: %w", a, err)
}
}

return nil
}

func (b *KVBuilder) add(raw string) error {
// Regardless of validity, make sure we make our result
if b.result == nil {
b.result = make(map[string]interface{})
}

// Empty strings are fine, just ignored
if raw == "" {
return nil
}

// Split into key/value
parts := strings.SplitN(raw, "=", 2)

// If the arg is exactly "-", then we need to read from stdin
// and merge the results into the resulting structure.
if len(parts) == 1 {
if raw == "-" {
if b.Stdin == nil {
return fmt.Errorf("stdin is not supported")
}
if b.stdin {
return fmt.Errorf("stdin already consumed")
}

b.stdin = true
return b.addReader(b.Stdin)
}

// If the arg begins with "@" then we need to read a file directly
if raw[0] == '@' {
f, err := os.Open(raw[1:])
if err != nil {
return err
}
defer f.Close()

return b.addReader(f)
}
}

if len(parts) != 2 {
return fmt.Errorf("format must be key=value")
}
key, value := parts[0], parts[1]

if len(value) > 0 {
if value[0] == '@' {
contents, err := ioutil.ReadFile(value[1:])
if err != nil {
return fmt.Errorf("error reading file: %w", err)
}

value = string(contents)
} else if value[0] == '\\' && value[1] == '@' {
value = value[1:]
} else if value == "-" {
if b.Stdin == nil {
return fmt.Errorf("stdin is not supported")
}
if b.stdin {
return fmt.Errorf("stdin already consumed")
}
b.stdin = true

var buf bytes.Buffer
if _, err := io.Copy(&buf, b.Stdin); err != nil {
return err
}

value = buf.String()
}
}

// Repeated keys will be converted into a slice
if existingValue, ok := b.result[key]; ok {
var sliceValue []interface{}
if err := mapstructure.WeakDecode(existingValue, &sliceValue); err != nil {
return err
}
sliceValue = append(sliceValue, value)
b.result[key] = sliceValue
return nil
}

b.result[key] = value
return nil
}

func (b *KVBuilder) addReader(r io.Reader) error {
if r == nil {
return fmt.Errorf("'io.Reader' being decoded is nil")
}

dec := json.NewDecoder(r)
// While decoding JSON values, interpret the integer values as
// `json.Number`s instead of `float64`.
dec.UseNumber()

return dec.Decode(&b.result)
}

// handleCASError provides consistent output for operations that result in a
// check-and-set error
func handleCASError(err error, c VarUI) (handled bool) {
ui := c.GetConcurrentUI()
var cErr api.ErrCASConflict
if errors.As(err, &cErr) {
lastUpdate := ""
if cErr.Conflict.ModifyIndex > 0 {
lastUpdate = fmt.Sprintf(
tidyRawString(msgfmtCASConflictLastAccess),
formatUnixNanoTime(cErr.Conflict.ModifyTime))
}
ui.Error(c.Colorize().Color("\n[bold][underline]Check-and-Set conflict[reset]\n"))
ui.Warn(
wrapAndPrepend(
c.Colorize().Color(
fmt.Sprintf(
tidyRawString(msgfmtCASMismatch),
cErr.CheckIndex,
cErr.Conflict.ModifyIndex,
lastUpdate),
),
80, " ") + "\n",
)
handled = true
}
return
}

const (
errMissingTemplate = `A template must be supplied using '-template' when using go-template formatting`
errUnexpectedTemplate = `The '-template' flag is only valid when using 'go-template' formatting`
errVariableNotFound = `Variable not found`
errInvalidInFormat = `Invalid value for "-in"; valid values are [hcl, json]`
errInvalidOutFormat = `Invalid value for "-out"; valid values are [go-template, hcl, json, none, table]`
errWildcardNamespaceNotAllowed = `The wildcard namespace ("*") is not valid for this command.`

msgfmtCASMismatch = `
Your provided check-index [green](%v)[yellow] does not match the
server-side index [green](%v)[yellow].
%s
If you are sure you want to perform this operation, add the [green]-force[yellow] or
[green]-check-index=%[2]v[yellow] flag before the positional arguments.`

msgfmtCASConflictLastAccess = `
The server-side item was last updated on [green]%s[yellow].
`
)
Loading

0 comments on commit 39a3fd6

Please sign in to comment.