diff --git a/command/var.go b/command/var.go index 455827095..898d22e3c 100644 --- a/command/var.go +++ b/command/var.go @@ -322,8 +322,10 @@ 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` + errNoMatchingVariables = `No matching variables 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]` + errInvalidListOutFormat = `Invalid value for "-out"; valid values are [go-template, json, table, terse]` errWildcardNamespaceNotAllowed = `The wildcard namespace ("*") is not valid for this command.` msgfmtCASMismatch = ` diff --git a/command/var_list.go b/command/var_list.go index fc45c7091..78335f674 100644 --- a/command/var_list.go +++ b/command/var_list.go @@ -1,7 +1,9 @@ package command import ( + "errors" "fmt" + "os" "sort" "strings" @@ -10,12 +12,13 @@ import ( ) const ( - msgVariableNotFound = "No matching variables found" msgWarnFilterPerformance = "Filter queries require a full scan of the data; use prefix searching where possible" ) type VarListCommand struct { - Prefix string + prefix string + outFmt string + tmpl string Meta } @@ -25,6 +28,9 @@ Usage: nomad var list [options] List is used to list available variables. Supplying an optional prefix, filters the list to variables having a path starting with the prefix. + When using pagination, the next page token is provided in the JSON output + or as a message to standard error to leave standard output for the listed + variables from that page. If ACLs are enabled, this command will only return variables stored in namespaces where the token has the 'variables:list' capability. @@ -46,15 +52,16 @@ List Options: option are less efficient than using the prefix parameter; therefore, the prefix parameter should be used whenever possible. - -json - Output the variables in JSON format. + -out (go-template | json | table | terse ) + Format to render created or updated variable. Defaults to "none" when + stdout is a terminal and "json" when the output is redirected. The "terse" + format outputs as little information as possible to uniquely identify a + variable depending on whether or not the wildcard namespace was passed. - -t - Format and display the variables using a Go template. + -template + Template to render output with. Required when format is "go-template", + invalid for other formats. - -q - Output matching variable paths with no additional information. - This option overrides the '-t' option. ` return strings.TrimSpace(helpText) } @@ -62,8 +69,8 @@ List Options: func (c *VarListCommand) AutocompleteFlags() complete.Flags { return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), complete.Flags{ - "-json": complete.PredictNothing, - "-t": complete.PredictAnything, + "-out": complete.PredictSet("go-template", "json", "terse", "table"), + "-template": complete.PredictAnything, }, ) } @@ -78,19 +85,23 @@ func (c *VarListCommand) Synopsis() string { func (c *VarListCommand) Name() string { return "var list" } func (c *VarListCommand) Run(args []string) int { - var json, quiet bool var perPage int - var tmpl, pageToken, filter, prefix string + var pageToken, filter, prefix string flags := c.Meta.FlagSet(c.Name(), FlagSetClient) flags.Usage = func() { c.Ui.Output(c.Help()) } - flags.BoolVar(&quiet, "q", false, "") - flags.BoolVar(&json, "json", false, "") - flags.StringVar(&tmpl, "t", "", "") + flags.StringVar(&c.tmpl, "template", "", "") + flags.IntVar(&perPage, "per-page", 0, "") flags.StringVar(&pageToken, "page-token", "", "") flags.StringVar(&filter, "filter", "", "") + if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 { + flags.StringVar(&c.outFmt, "out", "table", "") + } else { + flags.StringVar(&c.outFmt, "out", "json", "") + } + if err := flags.Parse(args); err != nil { return 1 } @@ -107,6 +118,12 @@ func (c *VarListCommand) Run(args []string) int { prefix = args[0] } + if err := c.validateOutputFlag(); err != nil { + c.Ui.Error(err.Error()) + c.Ui.Error(commandErrorText(c)) + return 1 + } + // Get the HTTP client client, err := c.Meta.Client() if err != nil { @@ -131,26 +148,19 @@ func (c *VarListCommand) Run(args []string) int { return 1 } - switch { - case json: - + switch c.outFmt { + case "json": // obj and items enable us to rework the output before sending it // to the Format method for transformation into JSON. var obj, items interface{} obj = vars items = vars - if quiet { - items = dataToQuietJSONReadySlice(vars, c.Meta.namespace) - obj = items - } - // If the response is paginated, we need to provide a means for the // caller to get to the pagination information. Wrapping the list // in a struct for the special case allows this extra data without // adding unnecessary structure in the non-paginated case. if perPage > 0 { - obj = struct { Data interface{} QueryMeta *api.QueryMeta @@ -162,7 +172,7 @@ func (c *VarListCommand) Run(args []string) int { // By this point, the output is ready to be transformed to JSON via // the Format func. - out, err := Format(json, tmpl, obj) + out, err := Format(true, "", obj) if err != nil { c.Ui.Error(err.Error()) return 1 @@ -174,18 +184,17 @@ func (c *VarListCommand) Run(args []string) int { // itself, exit the command here so that it doesn't double print. return 0 - case quiet: + case "terse": c.Ui.Output( formatList( dataToQuietStringSlice(vars, c.Meta.namespace))) - case len(tmpl) > 0: - out, err := Format(json, tmpl, vars) + case "go-template": + out, err := Format(false, c.tmpl, vars) if err != nil { c.Ui.Error(err.Error()) return 1 } - c.Ui.Output(out) default: @@ -204,7 +213,7 @@ func (c *VarListCommand) Run(args []string) int { func formatVarStubs(vars []*api.VariableMetadata) string { if len(vars) == 0 { - return msgVariableNotFound + return errNoMatchingVariables } // Sort the output by variable namespace, path @@ -248,28 +257,19 @@ func dataToQuietStringSlice(vars []*api.VariableMetadata, ns string) []string { return pList } -func dataToQuietJSONReadySlice(vars []*api.VariableMetadata, ns string) interface{} { - // If ns is the wildcard namespace, we have to provide namespace - // as part of the quiet output, otherwise it can be a simple list - // of paths. - if ns == "*" { - type pTuple struct { - Namespace string - Path string - } - pList := make([]*pTuple, len(vars)) - for i, sv := range vars { - pList[i] = &pTuple{sv.Namespace, sv.Path} - } - return pList +func (c *VarListCommand) validateOutputFlag() error { + if c.outFmt != "go-template" && c.tmpl != "" { + return errors.New(errUnexpectedTemplate) } - - // Reduce the items slice to a string slice containing only the - // variable paths. - pList := make([]string, len(vars)) - for i, sv := range vars { - pList[i] = sv.Path + switch c.outFmt { + case "json", "terse", "table": + return nil + case "go-template": + if c.tmpl == "" { + return errors.New(errMissingTemplate) + } + return nil + default: + return errors.New(errInvalidListOutFormat) } - - return pList } diff --git a/command/var_list_test.go b/command/var_list_test.go index ceee8d3b2..e0da26a98 100644 --- a/command/var_list_test.go +++ b/command/var_list_test.go @@ -50,6 +50,24 @@ func TestVarListCommand_Offline(t *testing.T) { exitCode: 1, expectStdErrPrefix: "Error initializing client: invalid address", }, + { + name: "missing template", + args: []string{`-out=go-template`, "foo"}, + exitCode: 1, + expectStdErrPrefix: errMissingTemplate, + }, + { + name: "unexpected_template", + args: []string{`-out=json`, `-template="bad"`, "foo"}, + exitCode: 1, + expectStdErrPrefix: errUnexpectedTemplate, + }, + { + name: "bad out", + args: []string{`-out=bad`, "foo"}, + exitCode: 1, + expectStdErrPrefix: errInvalidListOutFormat, + }, } for _, tC := range testCases { t.Run(tC.name, func(t *testing.T) { @@ -109,13 +127,6 @@ func TestVarListCommand_Online(t *testing.T) { nsList := []string{api.DefaultNamespace, "ns1"} pathList := []string{"a/b/c", "a/b/c/d", "z/y", "z/y/x"} - toJSON := func(in interface{}) string { - b, err := json.MarshalIndent(in, "", " ") - if err != nil { - return "" - } - return strings.TrimSpace(string(b)) - } variables := setupTestVariables(client, nsList, pathList) testTmpl := `{{ range $i, $e := . }}{{if ne $i 0}}{{print "•"}}{{end}}{{printf "%v\t%v" .Namespace .Path}}{{end}}` @@ -150,12 +161,12 @@ func TestVarListCommand_Online(t *testing.T) { testCases := []testVarListTestCase{ { name: "plaintext/not found", - args: []string{"does/not/exist"}, - expectStdOut: msgVariableNotFound, + args: []string{"-out=table", "does/not/exist"}, + expectStdOut: errNoMatchingVariables, }, { name: "plaintext/single variable", - args: []string{"a/b/c/d"}, + args: []string{"-out=table", "a/b/c/d"}, expectStdOut: formatList([]string{ "Namespace|Path|Last Updated", fmt.Sprintf( @@ -166,41 +177,41 @@ func TestVarListCommand_Online(t *testing.T) { ), }, { - name: "plaintext/quiet", - args: []string{"-q"}, + name: "plaintext/terse", + args: []string{"-out=terse"}, expectStdOut: strings.Join(variables.HavingNamespace(api.DefaultNamespace).Strings(), "\n"), }, { - name: "plaintext/quiet/prefix", - args: []string{"-q", "a/b/c"}, + name: "plaintext/terse/prefix", + args: []string{"-out=terse", "a/b/c"}, expectStdOut: strings.Join(variables.HavingNSPrefix(api.DefaultNamespace, "a/b/c").Strings(), "\n"), }, { - name: "plaintext/quiet/filter", - args: []string{"-q", "-filter", "VariableMetadata.Path == \"a/b/c\""}, + name: "plaintext/terse/filter", + args: []string{"-out=terse", "-filter", "VariableMetadata.Path == \"a/b/c\""}, expectStdOut: "a/b/c", expectStdErrPrefix: msgWarnFilterPerformance, }, { - name: "plaintext/quiet/paginated", - args: []string{"-q", "-per-page=1"}, + name: "plaintext/terse/paginated", + args: []string{"-out=terse", "-per-page=1"}, expectStdOut: "a/b/c", expectStdErrPrefix: "Next page token", }, { - name: "plaintext/quiet/prefix/wildcard ns", - args: []string{"-q", "-namespace", "*", "a/b/c/d"}, + name: "plaintext/terse/prefix/wildcard ns", + args: []string{"-out=terse", "-namespace", "*", "a/b/c/d"}, expectStdOut: strings.Join(variables.HavingPrefix("a/b/c/d").Strings(), "\n"), }, { - name: "plaintext/quiet/paginated/prefix/wildcard ns", - args: []string{"-q", "-per-page=1", "-namespace", "*", "a/b/c/d"}, + name: "plaintext/terse/paginated/prefix/wildcard ns", + args: []string{"-out=terse", "-per-page=1", "-namespace", "*", "a/b/c/d"}, expectStdOut: variables.HavingPrefix("a/b/c/d").Strings()[0], expectStdErrPrefix: "Next page token", }, { name: "json/not found", - args: []string{"-json", "does/not/exist"}, + args: []string{"-out=json", "does/not/exist"}, jsonTest: &testVarListJSONTest{ jsonDest: &SVMSlice{}, expectFns: []testVarListJSONTestExpectFn{ @@ -210,7 +221,7 @@ func TestVarListCommand_Online(t *testing.T) { }, { name: "json/prefix", - args: []string{"-json", "a"}, + args: []string{"-out=json", "a"}, jsonTest: &testVarListJSONTest{ jsonDest: &SVMSlice{}, expectFns: []testVarListJSONTestExpectFn{ @@ -220,7 +231,7 @@ func TestVarListCommand_Online(t *testing.T) { }, { name: "json/paginated", - args: []string{"-json", "-per-page", "1"}, + args: []string{"-out=json", "-per-page", "1"}, jsonTest: &testVarListJSONTest{ jsonDest: &PaginatedSVMSlice{}, expectFns: []testVarListJSONTestExpectFn{ @@ -228,68 +239,32 @@ func TestVarListCommand_Online(t *testing.T) { }, }, }, - { - name: "json/quiet", - args: []string{"-q", "-json"}, - expectStdOut: toJSON(variables.HavingNamespace(api.DefaultNamespace).Strings()), - }, - { - name: "json/quiet/paginated", - args: []string{"-q", "-json", "-per-page", "1"}, - jsonTest: &testVarListJSONTest{ - jsonDest: &PaginatedSVQuietSlice{}, - expectFns: []testVarListJSONTestExpectFn{ - hasLength(t, 1), - }, - }, - }, - { - name: "json/quiet/wildcard-ns", - args: []string{"-q", "-json", "-namespace", "*"}, - jsonTest: &testVarListJSONTest{ - jsonDest: &SVMSlice{}, - expectFns: []testVarListJSONTestExpectFn{ - hasLength(t, variables.Len()), - pathsEqual(t, variables), - }, - }, - }, - { - name: "json/quiet/paginated/wildcard-ns", - args: []string{"-q", "-json", "-per-page=1", "-namespace", "*"}, - jsonTest: &testVarListJSONTest{ - jsonDest: &PaginatedSVMSlice{}, - expectFns: []testVarListJSONTestExpectFn{ - hasLength(t, 1), - pathsEqual(t, SVMSlice{variables[0]}), - }, - }, - }, + { name: "template/not found", - args: []string{"-t", testTmpl, "does/not/exist"}, + args: []string{"-out=go-template", "-template", testTmpl, "does/not/exist"}, expectStdOut: "", }, { name: "template/prefix", - args: []string{"-t", testTmpl, "a/b/c/d"}, + args: []string{"-out=go-template", "-template", testTmpl, "a/b/c/d"}, expectStdOut: "default\ta/b/c/d", }, { name: "template/filter", - args: []string{"-t", testTmpl, "-filter", "VariableMetadata.Path == \"a/b/c\""}, + args: []string{"-out=go-template", "-template", testTmpl, "-filter", "VariableMetadata.Path == \"a/b/c\""}, expectStdOut: "default\ta/b/c", expectStdErrPrefix: msgWarnFilterPerformance, }, { name: "template/paginated", - args: []string{"-t", testTmpl, "-per-page=1"}, + args: []string{"-out=go-template", "-template", testTmpl, "-per-page=1"}, expectStdOut: "default\ta/b/c", expectStdErrPrefix: "Next page token", }, { name: "template/prefix/wildcard namespace", - args: []string{"-namespace", "*", "-t", testTmpl, "a/b/c/d"}, + args: []string{"-namespace", "*", "-out=go-template", "-template", testTmpl, "a/b/c/d"}, expectStdOut: "default\ta/b/c/d•ns1\ta/b/c/d", }, } @@ -385,11 +360,14 @@ func setupTestVariables(c *api.Client, nsList, pathList []string) SVMSlice { return out } -func setupTestVariable(c *api.Client, ns, p string, out *SVMSlice) { - testVar := &api.Variable{Items: map[string]string{"k": "v"}} - c.Raw().Write("/v1/var/"+p, testVar, nil, &api.WriteOptions{Namespace: ns}) - v, _, _ := c.Variables().Read(p, &api.QueryOptions{Namespace: ns}) +func setupTestVariable(c *api.Client, ns, p string, out *SVMSlice) error { + testVar := &api.Variable{ + Namespace: ns, + Path: p, + Items: map[string]string{"k": "v"}} + v, _, err := c.Variables().Create(testVar, &api.WriteOptions{Namespace: ns}) *out = append(*out, *v.Metadata()) + return err } type NSPather interface {