From 8302085384d2e45485fb6fd34eef3fb6abcb3e17 Mon Sep 17 00:00:00 2001 From: Juana De La Cuesta Date: Wed, 12 Apr 2023 11:02:14 +0200 Subject: [PATCH] Deployment Status Command Does Not Respect -namespace Wildcard (#16792) * func: add namespace support for list deployment * func: add wildcard to namespace filter for deployments * Update deployment_endpoint.go * style: use must instead of require or asseert * style: rename paginator to avoid clash with import * style: add changelog entry * fix: add missing parameter for upsert jobs --- .changelog/16792.txt | 3 + nomad/deployment_endpoint.go | 33 +++- nomad/deployment_endpoint_test.go | 317 +++++++++++++++++++----------- nomad/state/state_store.go | 3 +- nomad/structs/structs.go | 8 + 5 files changed, 240 insertions(+), 124 deletions(-) create mode 100644 .changelog/16792.txt diff --git a/.changelog/16792.txt b/.changelog/16792.txt new file mode 100644 index 000000000..31e548839 --- /dev/null +++ b/.changelog/16792.txt @@ -0,0 +1,3 @@ +```release-note:bug +core: the deployment's list endpoint now supports look up by prefix using the wildcard for namespace +``` diff --git a/nomad/deployment_endpoint.go b/nomad/deployment_endpoint.go index 0924d05a8..193ef7c43 100644 --- a/nomad/deployment_endpoint.go +++ b/nomad/deployment_endpoint.go @@ -450,20 +450,33 @@ func (d *Deployment) List(args *structs.DeploymentListRequest, reply *structs.De // Check namespace read-job permissions against request namespace since // results are filtered by request namespace. - if aclObj, err := d.srv.ResolveACL(args); err != nil { + aclObj, err := d.srv.ResolveACL(args) + if err != nil { return err - } else if aclObj != nil && !aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadJob) { + } + + if aclObj != nil && !aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadJob) { return structs.ErrPermissionDenied } + allow := aclObj.AllowNsOpFunc(acl.NamespaceCapabilityReadJob) + // Setup the blocking query sort := state.SortOption(args.Reverse) opts := blockingOptions{ queryOpts: &args.QueryOptions, queryMeta: &reply.QueryMeta, run: func(ws memdb.WatchSet, store *state.StateStore) error { + allowableNamespaces, err := allowedNSes(aclObj, store, allow) + if err != nil { + if err == structs.ErrPermissionDenied { + reply.Deployments = make([]*structs.Deployment, 0) + return nil + } + return err + } + // Capture all the deployments - var err error var iter memdb.ResultIterator var opts paginator.StructsTokenizerOptions @@ -491,8 +504,14 @@ func (d *Deployment) List(args *structs.DeploymentListRequest, reply *structs.De tokenizer := paginator.NewStructsTokenizer(iter, opts) + filters := []paginator.Filter{ + paginator.NamespaceFilter{ + AllowableNamespaces: allowableNamespaces, + }, + } + var deploys []*structs.Deployment - paginator, err := paginator.NewPaginator(iter, tokenizer, nil, args.QueryOptions, + pnator, err := paginator.NewPaginator(iter, tokenizer, filters, args.QueryOptions, func(raw interface{}) error { deploy := raw.(*structs.Deployment) deploys = append(deploys, deploy) @@ -503,7 +522,7 @@ func (d *Deployment) List(args *structs.DeploymentListRequest, reply *structs.De http.StatusBadRequest, "failed to create result paginator: %v", err) } - nextToken, err := paginator.Page() + nextToken, err := pnator.Page() if err != nil { return structs.NewErrRPCCodedf( http.StatusBadRequest, "failed to read result page: %v", err) @@ -522,7 +541,9 @@ func (d *Deployment) List(args *structs.DeploymentListRequest, reply *structs.De // Set the query response d.srv.setQueryMeta(&reply.QueryMeta) return nil - }} + }, + } + return d.srv.blockingRPC(&opts) } diff --git a/nomad/deployment_endpoint_test.go b/nomad/deployment_endpoint_test.go index 94551e44a..1847a1f14 100644 --- a/nomad/deployment_endpoint_test.go +++ b/nomad/deployment_endpoint_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/testutil" + "github.com/shoenig/test/must" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -974,7 +975,6 @@ func TestDeploymentEndpoint_List(t *testing.T) { defer cleanupS1() codec := rpcClient(t, s1) testutil.WaitForLeader(t, s1.RPC) - assert := assert.New(t) // Create the register request j := mock.Job() @@ -982,8 +982,8 @@ func TestDeploymentEndpoint_List(t *testing.T) { d.JobID = j.ID state := s1.fsm.State() - assert.Nil(state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, j), "UpsertJob") - assert.Nil(state.UpsertDeployment(1000, d), "UpsertDeployment") + must.Nil(t, state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, j), must.Sprint("UpsertJob")) + must.Nil(t, state.UpsertDeployment(1000, d), must.Sprint("UpsertDeployment")) // Lookup the deployments get := &structs.DeploymentListRequest{ @@ -993,10 +993,10 @@ func TestDeploymentEndpoint_List(t *testing.T) { }, } var resp structs.DeploymentListResponse - assert.Nil(msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp), "RPC") - assert.EqualValues(resp.Index, 1000, "Wrong Index") - assert.Len(resp.Deployments, 1, "Deployments") - assert.Equal(resp.Deployments[0].ID, d.ID, "Deployment ID") + must.Nil(t, msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp), must.Sprint("RPC")) + must.Eq(t, resp.Index, 1000, must.Sprint("Wrong Index")) + must.Len(t, 1, resp.Deployments, must.Sprint("Deployments")) + must.StrContains(t, resp.Deployments[0].ID, d.ID, must.Sprint("Deployment ID")) // Lookup the deploys by prefix get = &structs.DeploymentListRequest{ @@ -1008,21 +1008,20 @@ func TestDeploymentEndpoint_List(t *testing.T) { } var resp2 structs.DeploymentListResponse - assert.Nil(msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp2), "RPC") - assert.EqualValues(resp.Index, 1000, "Wrong Index") - assert.Len(resp2.Deployments, 1, "Deployments") - assert.Equal(resp2.Deployments[0].ID, d.ID, "Deployment ID") + must.Nil(t, msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp2), must.Sprint("RPC")) + must.Eq(t, resp.Index, 1000, must.Sprint("Wrong Index")) + must.Len(t, 1, resp2.Deployments, must.Sprint("Deployments")) + must.Eq(t, resp2.Deployments[0].ID, d.ID, must.Sprint("Deployment ID")) // add another deployment in another namespace - j2 := mock.Job() d2 := mock.Deployment() j2.Namespace = "prod" d2.Namespace = "prod" d2.JobID = j2.ID - assert.Nil(state.UpsertNamespaces(1001, []*structs.Namespace{{Name: "prod"}})) - assert.Nil(state.UpsertJob(structs.MsgTypeTestSetup, 1002, nil, j2), "UpsertJob") - assert.Nil(state.UpsertDeployment(1003, d2), "UpsertDeployment") + must.Nil(t, state.UpsertNamespaces(1001, []*structs.Namespace{{Name: "prod"}})) + must.Nil(t, state.UpsertJob(structs.MsgTypeTestSetup, 1002, nil, j2), must.Sprint("UpsertJob")) + must.Nil(t, state.UpsertDeployment(1003, d2), must.Sprint("UpsertDeployment")) // Lookup the deployments with wildcard namespace get = &structs.DeploymentListRequest{ @@ -1031,9 +1030,40 @@ func TestDeploymentEndpoint_List(t *testing.T) { Namespace: structs.AllNamespacesSentinel, }, } - assert.Nil(msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp), "RPC") - assert.EqualValues(resp.Index, 1003, "Wrong Index") - assert.Len(resp.Deployments, 2, "Deployments") + must.Nil(t, msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp), must.Sprint("RPC")) + must.Eq(t, resp.Index, 1003, must.Sprint("Wrong Index")) + must.Len(t, 2, resp.Deployments, must.Sprint("Deployments")) + + // Lookup a deployment with wildcard namespace and prefix + var resp3 structs.DeploymentListResponse + get = &structs.DeploymentListRequest{ + QueryOptions: structs.QueryOptions{ + Region: "global", + Prefix: d.ID[:4], + Namespace: structs.AllNamespacesSentinel, + }, + } + + must.Nil(t, msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp3), must.Sprint("RPC")) + must.Eq(t, resp3.Index, 1003, must.Sprint("Wrong Index")) + must.Len(t, 1, resp3.Deployments, must.Sprint("Deployments")) + must.StrContains(t, resp3.Deployments[0].ID, d.ID, must.Sprint("Deployment ID")) + + // Lookup the other deployments with wildcard namespace and prefix + var resp4 structs.DeploymentListResponse + get = &structs.DeploymentListRequest{ + QueryOptions: structs.QueryOptions{ + Region: "global", + Prefix: d2.ID[:4], + Namespace: structs.AllNamespacesSentinel, + }, + } + + must.Nil(t, msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp4), must.Sprint("RPC")) + must.Eq(t, resp4.Index, 1003, must.Sprint("Wrong Index")) + must.Len(t, 1, resp4.Deployments, must.Sprint("Deployments")) + must.StrContains(t, resp4.Deployments[0].ID, d2.ID, must.Sprint("Deployment ID")) + } func TestDeploymentEndpoint_List_order(t *testing.T) { @@ -1058,17 +1088,17 @@ func TestDeploymentEndpoint_List_order(t *testing.T) { dep3.ID = uuid3 err := s1.fsm.State().UpsertDeployment(1000, dep1) - require.NoError(t, err) + must.NoError(t, err) err = s1.fsm.State().UpsertDeployment(1001, dep2) - require.NoError(t, err) + must.NoError(t, err) err = s1.fsm.State().UpsertDeployment(1002, dep3) - require.NoError(t, err) + must.NoError(t, err) // update dep2 again so we can later assert create index order did not change err = s1.fsm.State().UpsertDeployment(1003, dep2) - require.NoError(t, err) + must.NoError(t, err) t.Run("default", func(t *testing.T) { // Lookup the deployments in chronological order (oldest first) @@ -1081,19 +1111,19 @@ func TestDeploymentEndpoint_List_order(t *testing.T) { var resp structs.DeploymentListResponse err = msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp) - require.NoError(t, err) - require.Equal(t, uint64(1003), resp.Index) - require.Len(t, resp.Deployments, 3) + must.NoError(t, err) + must.Eq(t, uint64(1003), resp.Index) + must.Len(t, 3, resp.Deployments) // Assert returned order is by CreateIndex (ascending) - require.Equal(t, uint64(1000), resp.Deployments[0].CreateIndex) - require.Equal(t, uuid1, resp.Deployments[0].ID) + must.Eq(t, uint64(1000), resp.Deployments[0].CreateIndex) + must.Eq(t, uuid1, resp.Deployments[0].ID) - require.Equal(t, uint64(1001), resp.Deployments[1].CreateIndex) - require.Equal(t, uuid2, resp.Deployments[1].ID) + must.Eq(t, uint64(1001), resp.Deployments[1].CreateIndex) + must.Eq(t, uuid2, resp.Deployments[1].ID) - require.Equal(t, uint64(1002), resp.Deployments[2].CreateIndex) - require.Equal(t, uuid3, resp.Deployments[2].ID) + must.Eq(t, uint64(1002), resp.Deployments[2].CreateIndex) + must.Eq(t, uuid3, resp.Deployments[2].ID) }) t.Run("reverse", func(t *testing.T) { @@ -1108,19 +1138,19 @@ func TestDeploymentEndpoint_List_order(t *testing.T) { var resp structs.DeploymentListResponse err = msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp) - require.NoError(t, err) - require.Equal(t, uint64(1003), resp.Index) - require.Len(t, resp.Deployments, 3) + must.NoError(t, err) + must.Eq(t, uint64(1003), resp.Index) + must.Len(t, 3, resp.Deployments) // Assert returned order is by CreateIndex (descending) - require.Equal(t, uint64(1002), resp.Deployments[0].CreateIndex) - require.Equal(t, uuid3, resp.Deployments[0].ID) + must.Eq(t, uint64(1002), resp.Deployments[0].CreateIndex) + must.Eq(t, uuid3, resp.Deployments[0].ID) - require.Equal(t, uint64(1001), resp.Deployments[1].CreateIndex) - require.Equal(t, uuid2, resp.Deployments[1].ID) + must.Eq(t, uint64(1001), resp.Deployments[1].CreateIndex) + must.Eq(t, uuid2, resp.Deployments[1].ID) - require.Equal(t, uint64(1000), resp.Deployments[2].CreateIndex) - require.Equal(t, uuid1, resp.Deployments[2].ID) + must.Eq(t, uint64(1000), resp.Deployments[2].CreateIndex) + must.Eq(t, uuid1, resp.Deployments[2].ID) }) } @@ -1131,65 +1161,104 @@ func TestDeploymentEndpoint_List_ACL(t *testing.T) { defer cleanupS1() codec := rpcClient(t, s1) testutil.WaitForLeader(t, s1.RPC) - assert := assert.New(t) + //assert := assert.New(t) + + // Create dev namespace + devNS := mock.Namespace() + devNS.Name = "dev" + err := s1.fsm.State().UpsertNamespaces(999, []*structs.Namespace{devNS}) + require.NoError(t, err) // Create the register request - j := mock.Job() - d := mock.Deployment() - d.JobID = j.ID + d1 := mock.Deployment() + d2 := mock.Deployment() + d2.Namespace = devNS.Name state := s1.fsm.State() - assert.Nil(state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, j), "UpsertJob") - assert.Nil(state.UpsertDeployment(1000, d), "UpsertDeployment") + must.NoError(t, state.UpsertDeployment(1000, d1), must.Sprint("Upsert Deployment failed")) + must.NoError(t, state.UpsertDeployment(1001, d2), must.Sprint("Upsert Deployment failed")) // Create the namespace policy and tokens - validToken := mock.CreatePolicyAndToken(t, state, 1001, "test-valid", + validToken := mock.CreatePolicyAndToken(t, state, 1002, "test-valid", mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob})) - invalidToken := mock.CreatePolicyAndToken(t, state, 1003, "test-invalid", + invalidToken := mock.CreatePolicyAndToken(t, state, 1001, "test-invalid", mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs})) + devToken := mock.CreatePolicyAndToken(t, state, 1004, "test-dev", + mock.NamespacePolicy("dev", "", []string{acl.NamespaceCapabilityReadJob})) - get := &structs.DeploymentListRequest{ - QueryOptions: structs.QueryOptions{ - Region: "global", - Namespace: structs.DefaultNamespace, + testCases := []struct { + name string + namespace string + token string + expectedDeployments []string + expectedError string + prefix string + }{ + { + name: "no token", + token: "", + namespace: structs.DefaultNamespace, + expectedError: structs.ErrPermissionDenied.Error(), + }, + { + name: "invalid token", + token: invalidToken.SecretID, + namespace: structs.DefaultNamespace, + expectedError: structs.ErrPermissionDenied.Error(), + }, + { + name: "valid token", + token: validToken.SecretID, + namespace: structs.DefaultNamespace, + expectedDeployments: []string{d1.ID}, + }, + { + name: "root token all namespaces", + token: root.SecretID, + namespace: structs.AllNamespacesSentinel, + expectedDeployments: []string{d1.ID, d2.ID}, + }, + + { + name: "root token default namespace", + token: root.SecretID, + namespace: structs.DefaultNamespace, + expectedDeployments: []string{d1.ID}, + }, + { + name: "dev token all namespaces", + token: devToken.SecretID, + namespace: structs.AllNamespacesSentinel, + expectedDeployments: []string{d2.ID}, }, } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + get := &structs.EvalListRequest{ + QueryOptions: structs.QueryOptions{ + AuthToken: tc.token, + Region: "global", + Namespace: tc.namespace, + Prefix: tc.prefix, + }, + } - // Try with no token and expect permission denied - { - var resp structs.DeploymentUpdateResponse - err := msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp) - assert.NotNil(err) - assert.Equal(err.Error(), structs.ErrPermissionDenied.Error()) - } + var resp structs.DeploymentListResponse + err := msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp) - // Try with an invalid token - { - get.AuthToken = invalidToken.SecretID - var resp structs.DeploymentUpdateResponse - err := msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp) - assert.NotNil(err) - assert.Equal(err.Error(), structs.ErrPermissionDenied.Error()) - } + if tc.expectedError != "" { + must.ErrorContains(t, err, tc.expectedError) + } else { + must.NoError(t, err) + require.Equal(t, uint64(1001), resp.Index, "Bad index: %d %d", resp.Index, 1001) - // Lookup the deployments with a root token - { - get.AuthToken = root.SecretID - var resp structs.DeploymentListResponse - assert.Nil(msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp), "RPC") - assert.EqualValues(resp.Index, 1000, "Wrong Index") - assert.Len(resp.Deployments, 1, "Deployments") - assert.Equal(resp.Deployments[0].ID, d.ID, "Deployment ID") - } - - // Lookup the deployments with a valid token - { - get.AuthToken = validToken.SecretID - var resp structs.DeploymentListResponse - assert.Nil(msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp), "RPC") - assert.EqualValues(resp.Index, 1000, "Wrong Index") - assert.Len(resp.Deployments, 1, "Deployments") - assert.Equal(resp.Deployments[0].ID, d.ID, "Deployment ID") + got := make([]string, len(resp.Deployments)) + for i, eval := range resp.Deployments { + got[i] = eval.ID + } + require.ElementsMatch(t, got, tc.expectedDeployments) + } + }) } } @@ -1201,18 +1270,17 @@ func TestDeploymentEndpoint_List_Blocking(t *testing.T) { state := s1.fsm.State() codec := rpcClient(t, s1) testutil.WaitForLeader(t, s1.RPC) - assert := assert.New(t) // Create the deployment j := mock.Job() d := mock.Deployment() d.JobID = j.ID - assert.Nil(state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, j), "UpsertJob") + must.Nil(t, state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, j), must.Sprint("UpsertJob")) // Upsert alloc triggers watches time.AfterFunc(100*time.Millisecond, func() { - assert.Nil(state.UpsertDeployment(3, d), "UpsertDeployment") + must.Nil(t, state.UpsertDeployment(3, d), must.Sprint("UpsertDeployment")) }) req := &structs.DeploymentListRequest{ @@ -1224,31 +1292,28 @@ func TestDeploymentEndpoint_List_Blocking(t *testing.T) { } start := time.Now() var resp structs.DeploymentListResponse - assert.Nil(msgpackrpc.CallWithCodec(codec, "Deployment.List", req, &resp), "RPC") - assert.EqualValues(resp.Index, 3, "Wrong Index") - assert.Len(resp.Deployments, 1, "Deployments") - assert.Equal(resp.Deployments[0].ID, d.ID, "Deployment ID") - if elapsed := time.Since(start); elapsed < 100*time.Millisecond { - t.Fatalf("should block (returned in %s) %#v", elapsed, resp) - } + must.Nil(t, msgpackrpc.CallWithCodec(codec, "Deployment.List", req, &resp), must.Sprint("RPC")) + must.Eq(t, resp.Index, 3, must.Sprint("Wrong Index")) + must.Len(t, 1, resp.Deployments, must.Sprint("Deployments")) + must.Eq(t, resp.Deployments[0].ID, d.ID, must.Sprint("Deployment ID")) + elapsed := time.Since(start) + must.Greater(t, 100*time.Millisecond, elapsed, must.Sprintf("should block (returned in %s) %#v", elapsed, resp)) // Deployment updates trigger watches d2 := d.Copy() d2.Status = structs.DeploymentStatusPaused time.AfterFunc(100*time.Millisecond, func() { - assert.Nil(state.UpsertDeployment(5, d2), "UpsertDeployment") + must.Nil(t, state.UpsertDeployment(5, d2), must.Sprint("UpsertDeployment")) }) req.MinQueryIndex = 3 start = time.Now() var resp2 structs.DeploymentListResponse - assert.Nil(msgpackrpc.CallWithCodec(codec, "Deployment.List", req, &resp2), "RPC") - assert.EqualValues(5, resp2.Index, "Wrong Index") - assert.Len(resp2.Deployments, 1, "Deployments") - assert.Equal(d2.ID, resp2.Deployments[0].ID, "Deployment ID") - if elapsed := time.Since(start); elapsed < 100*time.Millisecond { - t.Fatalf("should block (returned in %s) %#v", elapsed, resp2) - } + must.Nil(t, msgpackrpc.CallWithCodec(codec, "Deployment.List", req, &resp2), must.Sprint("RPC")) + must.Eq(t, 5, resp2.Index, must.Sprint("Wrong Index")) + must.Len(t, 1, resp2.Deployments, must.Sprint("Deployments")) + must.StrContains(t, d2.ID, resp2.Deployments[0].ID, must.Sprint("Deployment ID")) + must.Greater(t, 100*time.Millisecond, elapsed, must.Sprintf("should block (returned in %s) %#v", elapsed, resp2)) } func TestDeploymentEndpoint_List_Pagination(t *testing.T) { @@ -1258,6 +1323,12 @@ func TestDeploymentEndpoint_List_Pagination(t *testing.T) { codec := rpcClient(t, s1) testutil.WaitForLeader(t, s1.RPC) + // Create dev namespace + devNS := mock.Namespace() + devNS.Name = "non-default" + err := s1.fsm.State().UpsertNamespaces(999, []*structs.Namespace{devNS}) + must.NoError(t, err) + // create a set of deployments. these are in the order that the // state store will return them from the iterator (sorted by key), // for ease of writing tests @@ -1267,14 +1338,14 @@ func TestDeploymentEndpoint_List_Pagination(t *testing.T) { jobID string status string }{ - {id: "aaaa1111-3350-4b4b-d185-0e1992ed43e9"}, // 0 - {id: "aaaaaa22-3350-4b4b-d185-0e1992ed43e9"}, // 1 - {id: "aaaaaa33-3350-4b4b-d185-0e1992ed43e9", namespace: "non-default"}, // 2 - {id: "aaaaaaaa-3350-4b4b-d185-0e1992ed43e9"}, // 3 - {id: "aaaaaabb-3350-4b4b-d185-0e1992ed43e9"}, // 4 - {id: "aaaaaacc-3350-4b4b-d185-0e1992ed43e9"}, // 5 - {id: "aaaaaadd-3350-4b4b-d185-0e1992ed43e9"}, // 6 - {id: "00000111-3350-4b4b-d185-0e1992ed43e9"}, // 7 + {id: "aaaa1111-3350-4b4b-d185-0e1992ed43e9"}, // 0 + {id: "aaaaaa22-3350-4b4b-d185-0e1992ed43e9"}, // 1 + {id: "aaaaaa33-3350-4b4b-d185-0e1992ed43e9", namespace: devNS.Name}, // 2 + {id: "aaaaaaaa-3350-4b4b-d185-0e1992ed43e9"}, // 3 + {id: "aaaaaabb-3350-4b4b-d185-0e1992ed43e9"}, // 4 + {id: "aaaaaacc-3350-4b4b-d185-0e1992ed43e9"}, // 5 + {id: "aaaaaadd-3350-4b4b-d185-0e1992ed43e9"}, // 6 + {id: "00000111-3350-4b4b-d185-0e1992ed43e9"}, // 7 {}, // 8, index missing {id: "bbbb1111-3350-4b4b-d185-0e1992ed43e9"}, // 9 } @@ -1294,7 +1365,7 @@ func TestDeploymentEndpoint_List_Pagination(t *testing.T) { if m.namespace != "" { // defaults to "default" deployment.Namespace = m.namespace } - require.NoError(t, state.UpsertDeployment(index, deployment)) + must.NoError(t, state.UpsertDeployment(index, deployment)) } aclToken := mock.CreatePolicyAndToken(t, state, 1100, "test-valid-read", @@ -1432,6 +1503,18 @@ func TestDeploymentEndpoint_List_Pagination(t *testing.T) { "bbbb1111-3350-4b4b-d185-0e1992ed43e9", }, }, + { + name: "test15 size-2 page-2 all namespaces with prefix", + namespace: "*", + prefix: "aaaa", + pageSize: 2, + nextToken: "aaaaaa33-3350-4b4b-d185-0e1992ed43e9", + expectedNextToken: "aaaaaabb-3350-4b4b-d185-0e1992ed43e9", + expectedIDs: []string{ + "aaaaaa33-3350-4b4b-d185-0e1992ed43e9", + "aaaaaaaa-3350-4b4b-d185-0e1992ed43e9", + }, + }, } for _, tc := range cases { @@ -1450,10 +1533,10 @@ func TestDeploymentEndpoint_List_Pagination(t *testing.T) { var resp structs.DeploymentListResponse err := msgpackrpc.CallWithCodec(codec, "Deployment.List", req, &resp) if tc.expectedError == "" { - require.NoError(t, err) + must.NoError(t, err) } else { - require.Error(t, err) - require.Contains(t, err.Error(), tc.expectedError) + must.Error(t, err) + must.ErrorContains(t, err, tc.expectedError) return } @@ -1461,8 +1544,8 @@ func TestDeploymentEndpoint_List_Pagination(t *testing.T) { for _, deployment := range resp.Deployments { gotIDs = append(gotIDs, deployment.ID) } - require.Equal(t, tc.expectedIDs, gotIDs, "unexpected page of deployments") - require.Equal(t, tc.expectedNextToken, resp.QueryMeta.NextToken, "unexpected NextToken") + must.Eq(t, tc.expectedIDs, gotIDs, must.Sprint("unexpected page of deployments")) + must.Eq(t, tc.expectedNextToken, resp.QueryMeta.NextToken, must.Sprint("unexpected NextToken")) }) } } diff --git a/nomad/state/state_store.go b/nomad/state/state_store.go index 0549ea372..7d1c7d1d2 100644 --- a/nomad/state/state_store.go +++ b/nomad/state/state_store.go @@ -681,7 +681,8 @@ func deploymentNamespaceFilter(namespace string) func(interface{}) bool { return true } - return d.Namespace != namespace + return namespace != structs.AllNamespacesSentinel && + d.Namespace != namespace } } diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index caa3de67c..52d0af2df 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -9858,6 +9858,14 @@ func (d *Deployment) GoString() string { return base } +// GetNamespace implements the NamespaceGetter interface, required for pagination. +func (d *Deployment) GetNamespace() string { + if d == nil { + return "" + } + return d.Namespace +} + // DeploymentState tracks the state of a deployment for a given task group. type DeploymentState struct { // AutoRevert marks whether the task group has indicated the job should be