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:
Charlie Voiselle 2022-07-12 12:49:39 -04:00 committed by GitHub
parent 8054b3c9e6
commit 6be7a41351
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 865 additions and 4 deletions

View File

@ -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(),

74
command/var.go Normal file
View File

@ -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]
})
}

276
command/var_list.go Normal file
View File

@ -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
}

489
command/var_list_test.go Normal file
View File

@ -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
}

View File

@ -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