diff --git a/command/commands.go b/command/commands.go index 7175332f4..2418f1033 100644 --- a/command/commands.go +++ b/command/commands.go @@ -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(), diff --git a/command/var.go b/command/var.go new file mode 100644 index 000000000..e4541ed30 --- /dev/null +++ b/command/var.go @@ -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 [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 + + Examine a secure variable: + + $ nomad var get + + List existing secure variables: + + $ nomad var list + + 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] + }) +} diff --git a/command/var_list.go b/command/var_list.go new file mode 100644 index 000000000..28b6e38c3 --- /dev/null +++ b/command/var_list.go @@ -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] + + 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: ") + 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 +} diff --git a/command/var_list_test.go b/command/var_list_test.go new file mode 100644 index 000000000..15fd44cd9 --- /dev/null +++ b/command/var_list_test.go @@ -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: ", + }, + { + 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 +} diff --git a/nomad/secure_variables_endpoint.go b/nomad/secure_variables_endpoint.go index 35eecb785..39cdd3580 100644 --- a/nomad/secure_variables_endpoint.go +++ b/nomad/secure_variables_endpoint.go @@ -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