diff --git a/.changelog/16792.txt b/.changelog/16792.txt new file mode 100644 index 000000000000..31e54883900c --- /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 a6820c778e9d..813bff7f7028 100644 --- a/nomad/deployment_endpoint.go +++ b/nomad/deployment_endpoint.go @@ -447,20 +447,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 @@ -488,8 +501,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) @@ -500,7 +519,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) @@ -519,7 +538,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 17d4b99806e1..b6a0727c58eb 100644 --- a/nomad/deployment_endpoint_test.go +++ b/nomad/deployment_endpoint_test.go @@ -13,6 +13,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" ) @@ -971,7 +972,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() @@ -979,8 +979,8 @@ func TestDeploymentEndpoint_List(t *testing.T) { d.JobID = j.ID state := s1.fsm.State() - assert.Nil(state.UpsertJob(structs.MsgTypeTestSetup, 999, j), "UpsertJob") - assert.Nil(state.UpsertDeployment(1000, d), "UpsertDeployment") + must.Nil(t, state.UpsertJob(structs.MsgTypeTestSetup, 999, j), must.Sprint("UpsertJob")) + must.Nil(t, state.UpsertDeployment(1000, d), must.Sprint("UpsertDeployment")) // Lookup the deployments get := &structs.DeploymentListRequest{ @@ -990,10 +990,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{ @@ -1005,21 +1005,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, 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, j2), must.Sprint("UpsertJob")) + must.Nil(t, state.UpsertDeployment(1003, d2), must.Sprint("UpsertDeployment")) // Lookup the deployments with wildcard namespace get = &structs.DeploymentListRequest{ @@ -1028,9 +1027,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) { @@ -1055,17 +1085,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) @@ -1078,19 +1108,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) { @@ -1105,19 +1135,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) }) } @@ -1128,65 +1158,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, 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})) - - get := &structs.DeploymentListRequest{ - QueryOptions: structs.QueryOptions{ - Region: "global", - Namespace: structs.DefaultNamespace, + devToken := mock.CreatePolicyAndToken(t, state, 1004, "test-dev", + mock.NamespacePolicy("dev", "", []string{acl.NamespaceCapabilityReadJob})) + + 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}, }, - } - - // 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()) - } - // 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()) + { + 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, + }, + } - // 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") - } + var resp structs.DeploymentListResponse + err := msgpackrpc.CallWithCodec(codec, "Deployment.List", get, &resp) - // 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") + 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) + + got := make([]string, len(resp.Deployments)) + for i, eval := range resp.Deployments { + got[i] = eval.ID + } + require.ElementsMatch(t, got, tc.expectedDeployments) + } + }) } } @@ -1198,18 +1267,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, j), "UpsertJob") + must.Nil(t, state.UpsertJob(structs.MsgTypeTestSetup, 999, 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{ @@ -1221,31 +1289,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) { @@ -1255,6 +1320,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 @@ -1264,14 +1335,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 } @@ -1291,7 +1362,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", @@ -1429,6 +1500,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 { @@ -1447,10 +1530,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 } @@ -1458,8 +1541,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 2b77ada95786..90e5b38ec7ac 100644 --- a/nomad/state/state_store.go +++ b/nomad/state/state_store.go @@ -676,7 +676,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 579cf780c1ee..237a6f104ea5 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -9780,6 +9780,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