SV: CLI: var list command (#13707)
* SV CLI: var list * Fix wildcard prefix filtering Co-authored-by: Tim Gross <tgross@hashicorp.com>
This commit is contained in:
parent
8054b3c9e6
commit
6be7a41351
|
@ -901,6 +901,16 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory {
|
|||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"var": func() (cli.Command, error) {
|
||||
return &VarCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"var list": func() (cli.Command, error) {
|
||||
return &VarListCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"version": func() (cli.Command, error) {
|
||||
return &VersionCommand{
|
||||
Version: version.GetVersion(),
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/api/contexts"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
type VarCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (f *VarCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad var <subcommand> [options] [args]
|
||||
|
||||
This command groups subcommands for interacting with secure variables. Secure
|
||||
variables allow operators to provide credentials and otherwise sensitive
|
||||
material to Nomad jobs at runtime via the template stanza or directly through
|
||||
the Nomad API and CLI.
|
||||
|
||||
Users can create new secure variables; list, inspect, and delete existing
|
||||
secure variables, and more. For a full guide on secure variables see:
|
||||
https://www.nomadproject.io/guides/vars.html
|
||||
|
||||
Create a secure variable specification file:
|
||||
|
||||
$ nomad var init
|
||||
|
||||
Upsert a secure variable:
|
||||
|
||||
$ nomad var put <path>
|
||||
|
||||
Examine a secure variable:
|
||||
|
||||
$ nomad var get <path>
|
||||
|
||||
List existing secure variables:
|
||||
|
||||
$ nomad var list <prefix>
|
||||
|
||||
Please see the individual subcommand help for detailed usage information.
|
||||
`
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (f *VarCommand) Synopsis() string {
|
||||
return "Interact with secure variables"
|
||||
}
|
||||
|
||||
func (f *VarCommand) Name() string { return "var" }
|
||||
|
||||
func (f *VarCommand) Run(args []string) int {
|
||||
return cli.RunResultHelp
|
||||
}
|
||||
|
||||
// SecureVariablePathPredictor returns a var predictor
|
||||
func SecureVariablePathPredictor(factory ApiClientFactory) complete.Predictor {
|
||||
return complete.PredictFunc(func(a complete.Args) []string {
|
||||
client, err := factory()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, _, err := client.Search().PrefixSearch(a.Last, contexts.SecureVariables, nil)
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
return resp.Matches[contexts.SecureVariables]
|
||||
})
|
||||
}
|
|
@ -0,0 +1,276 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
const (
|
||||
msgSecureVariableNotFound = "No matching secure variables found"
|
||||
msgWarnFilterPerformance = "Filter queries require a full scan of the data; use prefix searching where possible"
|
||||
)
|
||||
|
||||
type VarListCommand struct {
|
||||
Prefix string
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *VarListCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad var list [options] <prefix>
|
||||
|
||||
List is used to list available secure variables. Supplying an optional prefix,
|
||||
filters the list to variables having a path starting with the prefix.
|
||||
|
||||
If ACLs are enabled, this command will return only secure variables stored at
|
||||
namespaced paths where the token has the ` + "`read`" + ` capability.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage(usageOptsDefault) + `
|
||||
|
||||
List Options:
|
||||
|
||||
-per-page
|
||||
How many results to show per page.
|
||||
|
||||
-page-token
|
||||
Where to start pagination.
|
||||
|
||||
-filter
|
||||
Specifies an expression used to filter query results. Queries using this
|
||||
option are less efficient than using the prefix parameter; therefore,
|
||||
the prefix parameter should be used whenever possible.
|
||||
|
||||
-json
|
||||
Output the secure variables in JSON format.
|
||||
|
||||
-t
|
||||
Format and display the secure variables using a Go template.
|
||||
|
||||
-q
|
||||
Output matching secure variable paths with no additional information.
|
||||
This option overrides the ` + "`-t`" + ` option.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *VarListCommand) AutocompleteFlags() complete.Flags {
|
||||
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
|
||||
complete.Flags{
|
||||
"-json": complete.PredictNothing,
|
||||
"-t": complete.PredictAnything,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (c *VarListCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *VarListCommand) Synopsis() string {
|
||||
return "List secure variable metadata"
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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.IntVar(&perPage, "per-page", 0, "")
|
||||
flags.StringVar(&pageToken, "page-token", "", "")
|
||||
flags.StringVar(&filter, "filter", "", "")
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that we got no arguments
|
||||
args = flags.Args()
|
||||
if l := len(args); l > 1 {
|
||||
c.Ui.Error("This command takes flags and either no arguments or one: <prefix>")
|
||||
c.Ui.Error(commandErrorText(c))
|
||||
return 1
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
prefix = args[0]
|
||||
}
|
||||
|
||||
// Get the HTTP client
|
||||
client, err := c.Meta.Client()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
if filter != "" {
|
||||
c.Ui.Warn(msgWarnFilterPerformance)
|
||||
}
|
||||
|
||||
qo := &api.QueryOptions{
|
||||
Filter: filter,
|
||||
PerPage: int32(perPage),
|
||||
NextToken: pageToken,
|
||||
Params: map[string]string{},
|
||||
}
|
||||
|
||||
vars, qm, err := client.SecureVariables().PrefixList(prefix, qo)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error retrieving vars: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
switch {
|
||||
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
|
||||
}{
|
||||
items,
|
||||
qm,
|
||||
}
|
||||
}
|
||||
|
||||
// By this point, the output is ready to be transformed to JSON via
|
||||
// the Format func.
|
||||
out, err := Format(json, tmpl, obj)
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
c.Ui.Output(out)
|
||||
|
||||
// Since the JSON formatting deals with the pagination information
|
||||
// itself, exit the command here so that it doesn't double print.
|
||||
return 0
|
||||
|
||||
case quiet:
|
||||
c.Ui.Output(
|
||||
formatList(
|
||||
dataToQuietStringSlice(vars, c.Meta.namespace)))
|
||||
|
||||
case len(tmpl) > 0:
|
||||
out, err := Format(json, tmpl, vars)
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
c.Ui.Output(out)
|
||||
|
||||
default:
|
||||
c.Ui.Output(formatVarStubs(vars))
|
||||
}
|
||||
|
||||
if qm.NextToken != "" {
|
||||
// This uses Ui.Warn to output the next page token to stderr
|
||||
// so that scripts consuming paths from stdout will not have
|
||||
// to special case the output.
|
||||
c.Ui.Warn(fmt.Sprintf("Next page token: %s", qm.NextToken))
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func formatVarStubs(vars []*api.SecureVariableMetadata) string {
|
||||
if len(vars) == 0 {
|
||||
return msgSecureVariableNotFound
|
||||
}
|
||||
|
||||
// Sort the output by variable namespace, path
|
||||
sort.Slice(vars, func(i, j int) bool {
|
||||
if vars[i].Namespace == vars[j].Namespace {
|
||||
return vars[i].Path < vars[j].Path
|
||||
}
|
||||
return vars[i].Namespace < vars[j].Namespace
|
||||
})
|
||||
|
||||
rows := make([]string, len(vars)+1)
|
||||
rows[0] = "Namespace|Path|Last Updated"
|
||||
for i, sv := range vars {
|
||||
rows[i+1] = fmt.Sprintf("%s|%s|%s",
|
||||
sv.Namespace,
|
||||
sv.Path,
|
||||
time.Unix(0, sv.ModifyTime),
|
||||
)
|
||||
}
|
||||
return formatList(rows)
|
||||
}
|
||||
|
||||
func dataToQuietStringSlice(vars []*api.SecureVariableMetadata, ns string) []string {
|
||||
// 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.
|
||||
toPathStr := func(v *api.SecureVariableMetadata) string {
|
||||
if ns == "*" {
|
||||
return fmt.Sprintf("%s|%s", v.Namespace, v.Path)
|
||||
}
|
||||
return v.Path
|
||||
}
|
||||
|
||||
// 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] = toPathStr(sv)
|
||||
}
|
||||
|
||||
return pList
|
||||
}
|
||||
|
||||
func dataToQuietJSONReadySlice(vars []*api.SecureVariableMetadata, 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
return pList
|
||||
}
|
|
@ -0,0 +1,489 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVarListCommand_Implements(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
var _ cli.Command = &VarListCommand{}
|
||||
}
|
||||
|
||||
// TestVarListCommand_Offline contains all of the tests that do not require a
|
||||
// testagent to complete
|
||||
func TestVarListCommand_Offline(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &VarListCommand{Meta: Meta{Ui: ui}}
|
||||
|
||||
testCases := []testVarListTestCase{
|
||||
{
|
||||
name: "help",
|
||||
args: []string{"-help"},
|
||||
exitCode: 1,
|
||||
expectUsage: true,
|
||||
},
|
||||
{
|
||||
name: "bad args",
|
||||
args: []string{"some", "bad", "args"},
|
||||
exitCode: 1,
|
||||
expectUsageError: true,
|
||||
expectStdErrPrefix: "This command takes flags and either no arguments or one: <prefix>",
|
||||
},
|
||||
{
|
||||
name: "bad address",
|
||||
args: []string{"-address", "nope"},
|
||||
exitCode: 1,
|
||||
expectStdErrPrefix: "Error retrieving vars",
|
||||
},
|
||||
{
|
||||
name: "unparsable address",
|
||||
args: []string{"-address", "http://10.0.0.1:bad"},
|
||||
exitCode: 1,
|
||||
expectStdErrPrefix: "Error initializing client: invalid address",
|
||||
},
|
||||
}
|
||||
for _, tC := range testCases {
|
||||
t.Run(tC.name, func(t *testing.T) {
|
||||
tC := tC
|
||||
ec := cmd.Run(tC.args)
|
||||
stdOut := ui.OutputWriter.String()
|
||||
errOut := ui.ErrorWriter.String()
|
||||
defer resetUiWriters(ui)
|
||||
|
||||
require.Equal(t, tC.exitCode, ec,
|
||||
"Expected exit code %v; got: %v\nstdout: %s\nstderr: %s",
|
||||
tC.exitCode, ec, stdOut, errOut,
|
||||
)
|
||||
if tC.expectUsage {
|
||||
help := cmd.Help()
|
||||
require.Equal(t, help, strings.TrimSpace(stdOut))
|
||||
// Test that stdout ends with a linefeed since we trim them for
|
||||
// convenience in the equality tests.
|
||||
require.True(t, strings.HasSuffix(stdOut, "\n"),
|
||||
"stdout does not end with a linefeed")
|
||||
}
|
||||
if tC.expectUsageError {
|
||||
require.Contains(t, errOut, commandErrorText(cmd))
|
||||
}
|
||||
if tC.expectStdOut != "" {
|
||||
require.Equal(t, tC.expectStdOut, strings.TrimSpace(stdOut))
|
||||
// Test that stdout ends with a linefeed since we trim them for
|
||||
// convenience in the equality tests.
|
||||
require.True(t, strings.HasSuffix(stdOut, "\n"),
|
||||
"stdout does not end with a linefeed")
|
||||
}
|
||||
if tC.expectStdErrPrefix != "" {
|
||||
require.True(t, strings.HasPrefix(errOut, tC.expectStdErrPrefix),
|
||||
"Expected stderr to start with %q; got %s",
|
||||
tC.expectStdErrPrefix, errOut)
|
||||
// Test that stderr ends with a linefeed since we trim them for
|
||||
// convenience in the equality tests.
|
||||
require.True(t, strings.HasSuffix(errOut, "\n"),
|
||||
"stderr does not end with a linefeed")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestVarListCommand_Online contains all of the tests that use a testagent.
|
||||
// They reuse the same testagent so that they can run in parallel and minimize
|
||||
// test startup time costs.
|
||||
func TestVarListCommand_Online(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Create a server
|
||||
srv, client, url := testServer(t, true, nil)
|
||||
defer srv.Shutdown()
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &VarListCommand{Meta: Meta{Ui: ui}}
|
||||
|
||||
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}}`
|
||||
|
||||
pathsEqual := func(t *testing.T, expect any) testVarListJSONTestExpectFn {
|
||||
out := func(t *testing.T, check any) {
|
||||
|
||||
expect := expect
|
||||
exp, ok := expect.(NSPather)
|
||||
require.True(t, ok, "expect is not an NSPather, got %T", expect)
|
||||
in, ok := check.(NSPather)
|
||||
require.True(t, ok, "check is not an NSPather, got %T", check)
|
||||
require.ElementsMatch(t, exp.NSPaths(), in.NSPaths())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
hasLength := func(t *testing.T, length int) testVarListJSONTestExpectFn {
|
||||
out := func(t *testing.T, check any) {
|
||||
|
||||
length := length
|
||||
in, ok := check.(NSPather)
|
||||
require.True(t, ok, "check is not an NSPather, got %T", check)
|
||||
inLen := in.NSPaths().Len()
|
||||
require.Equal(t, length, inLen,
|
||||
"expected length of %v, got %v. \nvalues: %v",
|
||||
length, inLen, in.NSPaths())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
testCases := []testVarListTestCase{
|
||||
{
|
||||
name: "plaintext/not found",
|
||||
args: []string{"does/not/exist"},
|
||||
expectStdOut: msgSecureVariableNotFound,
|
||||
},
|
||||
{
|
||||
name: "plaintext/single variable",
|
||||
args: []string{"a/b/c/d"},
|
||||
expectStdOut: formatList([]string{
|
||||
"Namespace|Path|Last Updated",
|
||||
fmt.Sprintf(
|
||||
"default|a/b/c/d|%s",
|
||||
time.Unix(0, variables.HavingPrefix("a/b/c/d")[0].ModifyTime),
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "plaintext/quiet",
|
||||
args: []string{"-q"},
|
||||
expectStdOut: strings.Join(variables.HavingNamespace(api.DefaultNamespace).Strings(), "\n"),
|
||||
},
|
||||
{
|
||||
name: "plaintext/quiet/prefix",
|
||||
args: []string{"-q", "a/b/c"},
|
||||
expectStdOut: strings.Join(variables.HavingNSPrefix(api.DefaultNamespace, "a/b/c").Strings(), "\n"),
|
||||
},
|
||||
{
|
||||
name: "plaintext/quiet/filter",
|
||||
args: []string{"-q", "-filter", "SecureVariableMetadata.Path == \"a/b/c\""},
|
||||
expectStdOut: "a/b/c",
|
||||
expectStdErrPrefix: msgWarnFilterPerformance,
|
||||
},
|
||||
{
|
||||
name: "plaintext/quiet/paginated",
|
||||
args: []string{"-q", "-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"},
|
||||
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"},
|
||||
expectStdOut: variables.HavingPrefix("a/b/c/d").Strings()[0],
|
||||
expectStdErrPrefix: "Next page token",
|
||||
},
|
||||
{
|
||||
name: "json/not found",
|
||||
args: []string{"-json", "does/not/exist"},
|
||||
jsonTest: &testVarListJSONTest{
|
||||
jsonDest: &SVMSlice{},
|
||||
expectFns: []testVarListJSONTestExpectFn{
|
||||
hasLength(t, 0),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json/prefix",
|
||||
args: []string{"-json", "a"},
|
||||
jsonTest: &testVarListJSONTest{
|
||||
jsonDest: &SVMSlice{},
|
||||
expectFns: []testVarListJSONTestExpectFn{
|
||||
pathsEqual(t, variables.HavingNSPrefix(api.DefaultNamespace, "a")),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json/paginated",
|
||||
args: []string{"-json", "-per-page", "1"},
|
||||
jsonTest: &testVarListJSONTest{
|
||||
jsonDest: &PaginatedSVMSlice{},
|
||||
expectFns: []testVarListJSONTestExpectFn{
|
||||
hasLength(t, 1),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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"},
|
||||
expectStdOut: "",
|
||||
},
|
||||
{
|
||||
name: "template/prefix",
|
||||
args: []string{"-t", testTmpl, "a/b/c/d"},
|
||||
expectStdOut: "default\ta/b/c/d",
|
||||
},
|
||||
{
|
||||
name: "template/filter",
|
||||
args: []string{"-t", testTmpl, "-filter", "SecureVariableMetadata.Path == \"a/b/c\""},
|
||||
expectStdOut: "default\ta/b/c",
|
||||
expectStdErrPrefix: msgWarnFilterPerformance,
|
||||
},
|
||||
{
|
||||
name: "template/paginated",
|
||||
args: []string{"-t", 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"},
|
||||
expectStdOut: "default\ta/b/c/d•ns1\ta/b/c/d",
|
||||
},
|
||||
}
|
||||
for _, tC := range testCases {
|
||||
t.Run(tC.name, func(t *testing.T) {
|
||||
tC := tC
|
||||
// address always needs to be provided and since the test cases
|
||||
// might pass a positional parameter, we need to jam it in the
|
||||
// front.
|
||||
tcArgs := append([]string{"-address=" + url}, tC.args...)
|
||||
|
||||
code := cmd.Run(tcArgs)
|
||||
stdOut := ui.OutputWriter.String()
|
||||
errOut := ui.ErrorWriter.String()
|
||||
defer resetUiWriters(ui)
|
||||
|
||||
require.Equal(t, tC.exitCode, code,
|
||||
"Expected exit code %v; got: %v\nstdout: %s\nstderr: %s",
|
||||
tC.exitCode, code, stdOut, errOut)
|
||||
|
||||
if tC.expectStdOut != "" {
|
||||
require.Equal(t, tC.expectStdOut, strings.TrimSpace(stdOut))
|
||||
|
||||
// Test that stdout ends with a linefeed since we trim them for
|
||||
// convenience in the equality tests.
|
||||
require.True(t, strings.HasSuffix(stdOut, "\n"),
|
||||
"stdout does not end with a linefeed")
|
||||
}
|
||||
|
||||
if tC.expectStdErrPrefix != "" {
|
||||
require.True(t, strings.HasPrefix(errOut, tC.expectStdErrPrefix),
|
||||
"Expected stderr to start with %q; got %s",
|
||||
tC.expectStdErrPrefix, errOut)
|
||||
|
||||
// Test that stderr ends with a linefeed since this test only
|
||||
// considers prefixes.
|
||||
require.True(t, strings.HasSuffix(stdOut, "\n"),
|
||||
"stderr does not end with a linefeed")
|
||||
}
|
||||
|
||||
if tC.jsonTest != nil {
|
||||
jtC := tC.jsonTest
|
||||
err := json.Unmarshal([]byte(stdOut), &jtC.jsonDest)
|
||||
require.NoError(t, err, "stdout: %s", stdOut)
|
||||
|
||||
for _, fn := range jtC.expectFns {
|
||||
fn(t, jtC.jsonDest)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func resetUiWriters(ui *cli.MockUi) {
|
||||
ui.ErrorWriter.Reset()
|
||||
ui.OutputWriter.Reset()
|
||||
}
|
||||
|
||||
type testVarListTestCase struct {
|
||||
name string
|
||||
args []string
|
||||
exitCode int
|
||||
expectUsage bool
|
||||
expectUsageError bool
|
||||
expectStdOut string
|
||||
expectStdErrPrefix string
|
||||
jsonTest *testVarListJSONTest
|
||||
}
|
||||
|
||||
type testVarListJSONTest struct {
|
||||
jsonDest interface{}
|
||||
expectFns []testVarListJSONTestExpectFn
|
||||
}
|
||||
|
||||
type testVarListJSONTestExpectFn func(*testing.T, interface{})
|
||||
|
||||
type testSVNamespacePath struct {
|
||||
Namespace string
|
||||
Path string
|
||||
}
|
||||
|
||||
func setupTestVariables(c *api.Client, nsList, pathList []string) SVMSlice {
|
||||
|
||||
out := make(SVMSlice, 0, len(nsList)*len(pathList))
|
||||
|
||||
for _, ns := range nsList {
|
||||
c.Namespaces().Register(&api.Namespace{Name: ns}, nil)
|
||||
for _, p := range pathList {
|
||||
setupTestVariable(c, ns, p, &out)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func setupTestVariable(c *api.Client, ns, p string, out *SVMSlice) {
|
||||
testVar := &api.SecureVariable{Items: map[string]string{"k": "v"}}
|
||||
c.Raw().Write("/v1/var/"+p, testVar, nil, &api.WriteOptions{Namespace: ns})
|
||||
v, _, _ := c.SecureVariables().Read(p, &api.QueryOptions{Namespace: ns})
|
||||
*out = append(*out, *v.Metadata())
|
||||
}
|
||||
|
||||
type NSPather interface {
|
||||
Len() int
|
||||
NSPaths() testSVNamespacePaths
|
||||
}
|
||||
|
||||
type testSVNamespacePaths []testSVNamespacePath
|
||||
|
||||
func (ps testSVNamespacePaths) Len() int { return len(ps) }
|
||||
func (ps testSVNamespacePaths) NSPaths() testSVNamespacePaths {
|
||||
return ps
|
||||
}
|
||||
|
||||
type SVMSlice []api.SecureVariableMetadata
|
||||
|
||||
func (s SVMSlice) Len() int { return len(s) }
|
||||
func (s SVMSlice) NSPaths() testSVNamespacePaths {
|
||||
|
||||
out := make(testSVNamespacePaths, len(s))
|
||||
for i, v := range s {
|
||||
out[i] = testSVNamespacePath{v.Namespace, v.Path}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (ps SVMSlice) Strings() []string {
|
||||
ns := make(map[string]struct{})
|
||||
outNS := make([]string, len(ps))
|
||||
out := make([]string, len(ps))
|
||||
for i, p := range ps {
|
||||
out[i] = p.Path
|
||||
outNS[i] = p.Namespace + "|" + p.Path
|
||||
ns[p.Namespace] = struct{}{}
|
||||
}
|
||||
if len(ns) > 1 {
|
||||
return strings.Split(formatList(outNS), "\n")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (ps *SVMSlice) HavingNamespace(ns string) SVMSlice {
|
||||
return *ps.having("namespace", ns)
|
||||
}
|
||||
|
||||
func (ps *SVMSlice) HavingPrefix(prefix string) SVMSlice {
|
||||
return *ps.having("prefix", prefix)
|
||||
}
|
||||
|
||||
func (ps *SVMSlice) HavingNSPrefix(ns, p string) SVMSlice {
|
||||
return *ps.having("namespace", ns).having("prefix", p)
|
||||
}
|
||||
|
||||
func (ps SVMSlice) having(field, val string) *SVMSlice {
|
||||
|
||||
out := make(SVMSlice, 0, len(ps))
|
||||
for _, p := range ps {
|
||||
if field == "namespace" && p.Namespace == val {
|
||||
out = append(out, p)
|
||||
}
|
||||
if field == "prefix" && strings.HasPrefix(p.Path, val) {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return &out
|
||||
}
|
||||
|
||||
type PaginatedSVMSlice struct {
|
||||
Data SVMSlice
|
||||
QueryMeta api.QueryMeta
|
||||
}
|
||||
|
||||
func (s *PaginatedSVMSlice) Len() int { return len(s.Data) }
|
||||
func (s *PaginatedSVMSlice) NSPaths() testSVNamespacePaths {
|
||||
|
||||
out := make(testSVNamespacePaths, len(s.Data))
|
||||
for i, v := range s.Data {
|
||||
out[i] = testSVNamespacePath{v.Namespace, v.Path}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type PaginatedSVQuietSlice struct {
|
||||
Data []string
|
||||
QueryMeta api.QueryMeta
|
||||
}
|
||||
|
||||
func (ps PaginatedSVQuietSlice) Len() int { return len(ps.Data) }
|
||||
func (s *PaginatedSVQuietSlice) NSPaths() testSVNamespacePaths {
|
||||
|
||||
out := make(testSVNamespacePaths, len(s.Data))
|
||||
for i, v := range s.Data {
|
||||
out[i] = testSVNamespacePath{"", v}
|
||||
}
|
||||
return out
|
||||
}
|
|
@ -364,14 +364,26 @@ func (s *SecureVariables) listAllSecureVariables(
|
|||
},
|
||||
)
|
||||
|
||||
// Wrap the SecureVariables iterator with a FilterIterator to
|
||||
// eliminate invalid values before sending them to the paginator.
|
||||
fltrIter := memdb.NewFilterIterator(iter, func(raw interface{}) bool {
|
||||
|
||||
// Values are filtered when the func returns true.
|
||||
sv := raw.(*structs.SecureVariableEncrypted)
|
||||
if allowedNSes != nil && !allowedNSes[sv.Namespace] {
|
||||
return true
|
||||
}
|
||||
if !strings.HasPrefix(sv.Path, args.Prefix) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// Build the paginator. This includes the function that is
|
||||
// responsible for appending a variable to the stubs array.
|
||||
paginatorImpl, err := paginator.NewPaginator(iter, tokenizer, nil, args.QueryOptions,
|
||||
paginatorImpl, err := paginator.NewPaginator(fltrIter, tokenizer, nil, args.QueryOptions,
|
||||
func(raw interface{}) error {
|
||||
sv := raw.(*structs.SecureVariableEncrypted)
|
||||
if allowedNSes != nil && !allowedNSes[sv.Namespace] {
|
||||
return nil
|
||||
}
|
||||
svStub := sv.SecureVariableMetadata
|
||||
svs = append(svs, &svStub)
|
||||
return nil
|
||||
|
|
Loading…
Reference in New Issue