diff --git a/.changelog/11648.txt b/.changelog/11648.txt new file mode 100644 index 000000000..ce58748aa --- /dev/null +++ b/.changelog/11648.txt @@ -0,0 +1,3 @@ +```release-note:improvement +api: Add pagination and filtering to Evaluations List API +``` diff --git a/api/api.go b/api/api.go index 140089e4c..8719cab2c 100644 --- a/api/api.go +++ b/api/api.go @@ -69,8 +69,10 @@ type QueryOptions struct { // paginated lists. PerPage int32 - // NextToken is the token used indicate where to start paging for queries - // that support paginated lists. + // NextToken is the token used to indicate where to start paging + // for queries that support paginated lists. This token should be + // the ID of the next object after the last one seen in the + // previous response. NextToken string // ctx is an optional context pass through to the underlying HTTP @@ -113,6 +115,11 @@ type QueryMeta struct { // How long did the request take RequestTime time.Duration + + // NextToken is the token used to indicate where to start paging + // for queries that support paginated lists. To resume paging from + // this point, pass this token in the next request's QueryOptions + NextToken string } // WriteMeta is used to return meta data about a write @@ -574,6 +581,12 @@ func (r *request) setQueryOptions(q *QueryOptions) { if q.Prefix != "" { r.params.Set("prefix", q.Prefix) } + if q.PerPage != 0 { + r.params.Set("per_page", fmt.Sprint(q.PerPage)) + } + if q.NextToken != "" { + r.params.Set("next_token", q.NextToken) + } for k, v := range q.Params { r.params.Set(k, v) } @@ -958,6 +971,7 @@ func parseQueryMeta(resp *http.Response, q *QueryMeta) error { return fmt.Errorf("Failed to parse X-Nomad-LastContact: %v", err) } q.LastContact = time.Duration(last) * time.Millisecond + q.NextToken = header.Get("X-Nomad-NextToken") // Parse the X-Nomad-KnownLeader switch header.Get("X-Nomad-KnownLeader") { diff --git a/api/evaluations_test.go b/api/evaluations_test.go index 41f389435..a734e5705 100644 --- a/api/evaluations_test.go +++ b/api/evaluations_test.go @@ -5,6 +5,8 @@ import ( "sort" "strings" "testing" + + "github.com/hashicorp/nomad/api/internal/testutil" ) func TestEvaluations_List(t *testing.T) { @@ -43,10 +45,38 @@ func TestEvaluations_List(t *testing.T) { // if the eval fails fast there can be more than 1 // but they are in order of most recent first, so look at the last one + if len(result) == 0 { + t.Fatalf("expected eval (%s), got none", resp.EvalID) + } idx := len(result) - 1 - if len(result) == 0 || result[idx].ID != resp.EvalID { + if result[idx].ID != resp.EvalID { t.Fatalf("expected eval (%s), got: %#v", resp.EvalID, result[idx]) } + + // wait until the 2nd eval shows up before we try paging + results := []*Evaluation{} + testutil.WaitForResult(func() (bool, error) { + results, _, err = e.List(nil) + if len(results) < 2 || err != nil { + return false, err + } + return true, nil + }, func(err error) { + t.Fatalf("err: %s", err) + }) + + // Check the evaluations again with paging; note that while this + // package sorts by timestamp, the actual HTTP API sorts by ID + // so we need to use that for the NextToken + ids := []string{results[0].ID, results[1].ID} + sort.Strings(ids) + result, qm, err = e.List(&QueryOptions{PerPage: int32(1), NextToken: ids[1]}) + if err != nil { + t.Fatalf("err: %s", err) + } + if len(result) != 1 { + t.Fatalf("expected no evals after last one but got %v", result[0]) + } } func TestEvaluations_PrefixList(t *testing.T) { diff --git a/command/agent/eval_endpoint.go b/command/agent/eval_endpoint.go index 56c646bd2..a51c9e940 100644 --- a/command/agent/eval_endpoint.go +++ b/command/agent/eval_endpoint.go @@ -17,6 +17,10 @@ func (s *HTTPServer) EvalsRequest(resp http.ResponseWriter, req *http.Request) ( return nil, nil } + query := req.URL.Query() + args.FilterEvalStatus = query.Get("status") + args.FilterJobID = query.Get("job") + var out structs.EvalListResponse if err := s.agent.RPC("Eval.List", &args, &out); err != nil { return nil, err diff --git a/command/agent/eval_endpoint_test.go b/command/agent/eval_endpoint_test.go index 6f82cbc8f..357ef58b4 100644 --- a/command/agent/eval_endpoint_test.go +++ b/command/agent/eval_endpoint_test.go @@ -1,10 +1,13 @@ package agent import ( + "fmt" "net/http" "net/http/httptest" "testing" + "github.com/stretchr/testify/require" + "github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/structs" ) @@ -17,39 +20,42 @@ func TestHTTP_EvalList(t *testing.T) { eval1 := mock.Eval() eval2 := mock.Eval() err := state.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{eval1, eval2}) - if err != nil { - t.Fatalf("err: %v", err) - } + require.NoError(t, err) - // Make the HTTP request + // simple list request req, err := http.NewRequest("GET", "/v1/evaluations", nil) - if err != nil { - t.Fatalf("err: %v", err) - } + require.NoError(t, err) respW := httptest.NewRecorder() - - // Make the request obj, err := s.Server.EvalsRequest(respW, req) - if err != nil { - t.Fatalf("err: %v", err) - } + require.NoError(t, err) - // Check for the index - if respW.HeaderMap.Get("X-Nomad-Index") == "" { - t.Fatalf("missing index") - } - if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" { - t.Fatalf("missing known leader") - } - if respW.HeaderMap.Get("X-Nomad-LastContact") == "" { - t.Fatalf("missing last contact") - } + // check headers and response body + require.NotEqual(t, "", respW.HeaderMap.Get("X-Nomad-Index"), "missing index") + require.Equal(t, "true", respW.HeaderMap.Get("X-Nomad-KnownLeader"), "missing known leader") + require.NotEqual(t, "", respW.HeaderMap.Get("X-Nomad-LastContact"), "missing last contact") + require.Len(t, obj.([]*structs.Evaluation), 2, "expected 2 evals") + + // paginated list request + req, err = http.NewRequest("GET", "/v1/evaluations?per_page=1", nil) + require.NoError(t, err) + respW = httptest.NewRecorder() + obj, err = s.Server.EvalsRequest(respW, req) + require.NoError(t, err) + + // check response body + require.Len(t, obj.([]*structs.Evaluation), 1, "expected 1 eval") + + // filtered list request + req, err = http.NewRequest("GET", + fmt.Sprintf("/v1/evaluations?per_page=10&job=%s", eval2.JobID), nil) + require.NoError(t, err) + respW = httptest.NewRecorder() + obj, err = s.Server.EvalsRequest(respW, req) + require.NoError(t, err) + + // check response body + require.Len(t, obj.([]*structs.Evaluation), 1, "expected 1 eval") - // Check the eval - e := obj.([]*structs.Evaluation) - if len(e) != 2 { - t.Fatalf("bad: %#v", e) - } }) } diff --git a/command/agent/http.go b/command/agent/http.go index f56215f72..0b656f959 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -608,11 +608,19 @@ func setLastContact(resp http.ResponseWriter, last time.Duration) { resp.Header().Set("X-Nomad-LastContact", strconv.FormatUint(lastMsec, 10)) } +// setNextToken is used to set the next token header for pagination +func setNextToken(resp http.ResponseWriter, nextToken string) { + if nextToken != "" { + resp.Header().Set("X-Nomad-NextToken", nextToken) + } +} + // setMeta is used to set the query response meta data func setMeta(resp http.ResponseWriter, m *structs.QueryMeta) { setIndex(resp, m.Index) setLastContact(resp, m.LastContact) setKnownLeader(resp, m.KnownLeader) + setNextToken(resp, m.NextToken) } // setHeaders is used to set canonical response header fields @@ -746,8 +754,7 @@ func parsePagination(req *http.Request, b *structs.QueryOptions) { } } - nextToken := query.Get("next_token") - b.NextToken = nextToken + b.NextToken = query.Get("next_token") } // parseWriteRequest is a convenience method for endpoints that need to parse a diff --git a/nomad/eval_endpoint.go b/nomad/eval_endpoint.go index e40c31c97..e3a14946d 100644 --- a/nomad/eval_endpoint.go +++ b/nomad/eval_endpoint.go @@ -349,32 +349,39 @@ func (e *Eval) List(args *structs.EvalListRequest, opts := blockingOptions{ queryOpts: &args.QueryOptions, queryMeta: &reply.QueryMeta, - run: func(ws memdb.WatchSet, state *state.StateStore) error { + run: func(ws memdb.WatchSet, store *state.StateStore) error { // Scan all the evaluations var err error var iter memdb.ResultIterator if prefix := args.QueryOptions.Prefix; prefix != "" { - iter, err = state.EvalsByIDPrefix(ws, args.RequestNamespace(), prefix) + iter, err = store.EvalsByIDPrefix(ws, args.RequestNamespace(), prefix) } else { - iter, err = state.EvalsByNamespace(ws, args.RequestNamespace()) + iter, err = store.EvalsByNamespace(ws, args.RequestNamespace()) } if err != nil { return err } - var evals []*structs.Evaluation - for { - raw := iter.Next() - if raw == nil { - break + iter = memdb.NewFilterIterator(iter, func(raw interface{}) bool { + if eval := raw.(*structs.Evaluation); eval != nil { + return args.ShouldBeFiltered(eval) } - eval := raw.(*structs.Evaluation) - evals = append(evals, eval) - } + return false + }) + + var evals []*structs.Evaluation + paginator := state.NewPaginator(iter, args.QueryOptions, + func(raw interface{}) { + eval := raw.(*structs.Evaluation) + evals = append(evals, eval) + }) + + nextToken := paginator.Page() + reply.QueryMeta.NextToken = nextToken reply.Evaluations = evals // Use the last index that affected the jobs table - index, err := state.Index("evals") + index, err := store.Index("evals") if err != nil { return err } diff --git a/nomad/eval_endpoint_test.go b/nomad/eval_endpoint_test.go index a95a898b6..ef142a9d9 100644 --- a/nomad/eval_endpoint_test.go +++ b/nomad/eval_endpoint_test.go @@ -851,6 +851,225 @@ func TestEvalEndpoint_List_Blocking(t *testing.T) { } } +func TestEvalEndpoint_List_PaginationFiltering(t *testing.T) { + t.Parallel() + s1, _, cleanupS1 := TestACLServer(t, nil) + defer cleanupS1() + codec := rpcClient(t, s1) + testutil.WaitForLeader(t, s1.RPC) + + // create a set of evals and field values to filter on. these are + // in the order that the state store will return them from the + // iterator (sorted by key), for ease of writing tests + mocks := []struct { + id string + namespace string + jobID string + status string + }{ + {id: "aaaa1111-3350-4b4b-d185-0e1992ed43e9", jobID: "example"}, + {id: "aaaaaa22-3350-4b4b-d185-0e1992ed43e9", jobID: "example"}, + {id: "aaaaaa33-3350-4b4b-d185-0e1992ed43e9", namespace: "non-default"}, + {id: "aaaaaaaa-3350-4b4b-d185-0e1992ed43e9", jobID: "example", status: "blocked"}, + {id: "aaaaaabb-3350-4b4b-d185-0e1992ed43e9"}, + {id: "aaaaaacc-3350-4b4b-d185-0e1992ed43e9"}, + {id: "aaaaaadd-3350-4b4b-d185-0e1992ed43e9", jobID: "example"}, + {id: "aaaaaaee-3350-4b4b-d185-0e1992ed43e9", jobID: "example"}, + {id: "aaaaaaff-3350-4b4b-d185-0e1992ed43e9"}, + } + + mockEvals := []*structs.Evaluation{} + for _, m := range mocks { + eval := mock.Eval() + eval.ID = m.id + if m.namespace != "" { // defaults to "default" + eval.Namespace = m.namespace + } + if m.jobID != "" { // defaults to some random UUID + eval.JobID = m.jobID + } + if m.status != "" { // defaults to "pending" + eval.Status = m.status + } + mockEvals = append(mockEvals, eval) + } + + state := s1.fsm.State() + require.NoError(t, state.UpsertEvals(structs.MsgTypeTestSetup, 1000, mockEvals)) + + aclToken := mock.CreatePolicyAndToken(t, state, 1100, "test-valid-read", + mock.NamespacePolicy(structs.DefaultNamespace, "read", nil)). + SecretID + + cases := []struct { + name string + namespace string + prefix string + nextToken string + filterJobID string + filterStatus string + pageSize int32 + expectedNextToken string + expectedIDs []string + }{ + { + name: "test01 size-2 page-1 default NS", + pageSize: 2, + expectedNextToken: "aaaaaaaa-3350-4b4b-d185-0e1992ed43e9", + expectedIDs: []string{ + "aaaa1111-3350-4b4b-d185-0e1992ed43e9", + "aaaaaa22-3350-4b4b-d185-0e1992ed43e9", + }, + }, + { + name: "test02 size-2 page-1 default NS with prefix", + prefix: "aaaa", + pageSize: 2, + expectedNextToken: "aaaaaaaa-3350-4b4b-d185-0e1992ed43e9", + expectedIDs: []string{ + "aaaa1111-3350-4b4b-d185-0e1992ed43e9", + "aaaaaa22-3350-4b4b-d185-0e1992ed43e9", + }, + }, + { + name: "test03 size-2 page-2 default NS", + pageSize: 2, + nextToken: "aaaaaaaa-3350-4b4b-d185-0e1992ed43e9", + expectedNextToken: "aaaaaacc-3350-4b4b-d185-0e1992ed43e9", + expectedIDs: []string{ + "aaaaaaaa-3350-4b4b-d185-0e1992ed43e9", + "aaaaaabb-3350-4b4b-d185-0e1992ed43e9", + }, + }, + { + name: "test04 size-2 page-2 default NS with prefix", + prefix: "aaaa", + pageSize: 2, + nextToken: "aaaaaabb-3350-4b4b-d185-0e1992ed43e9", + expectedNextToken: "aaaaaadd-3350-4b4b-d185-0e1992ed43e9", + expectedIDs: []string{ + "aaaaaabb-3350-4b4b-d185-0e1992ed43e9", + "aaaaaacc-3350-4b4b-d185-0e1992ed43e9", + }, + }, + { + name: "test05 size-2 page-1 with filters default NS", + pageSize: 2, + filterJobID: "example", + filterStatus: "pending", + // aaaaaaaa, bb, and cc are filtered by status + expectedNextToken: "aaaaaadd-3350-4b4b-d185-0e1992ed43e9", + expectedIDs: []string{ + "aaaa1111-3350-4b4b-d185-0e1992ed43e9", + "aaaaaa22-3350-4b4b-d185-0e1992ed43e9", + }, + }, + { + name: "test06 size-2 page-1 with filters default NS with short prefix", + prefix: "aaaa", + pageSize: 2, + filterJobID: "example", + filterStatus: "pending", + // aaaaaaaa, bb, and cc are filtered by status + expectedNextToken: "aaaaaadd-3350-4b4b-d185-0e1992ed43e9", + expectedIDs: []string{ + "aaaa1111-3350-4b4b-d185-0e1992ed43e9", + "aaaaaa22-3350-4b4b-d185-0e1992ed43e9", + }, + }, + { + name: "test07 size-2 page-1 with filters default NS with longer prefix", + prefix: "aaaaaa", + pageSize: 2, + filterJobID: "example", + filterStatus: "pending", + expectedNextToken: "aaaaaaee-3350-4b4b-d185-0e1992ed43e9", + expectedIDs: []string{ + "aaaaaa22-3350-4b4b-d185-0e1992ed43e9", + "aaaaaadd-3350-4b4b-d185-0e1992ed43e9", + }, + }, + { + name: "test08 size-2 page-2 filter skip nextToken", + pageSize: 3, // reads off the end + filterJobID: "example", + filterStatus: "pending", + nextToken: "aaaaaaaa-3350-4b4b-d185-0e1992ed43e9", + expectedNextToken: "", + expectedIDs: []string{ + "aaaaaadd-3350-4b4b-d185-0e1992ed43e9", + "aaaaaaee-3350-4b4b-d185-0e1992ed43e9", + }, + }, + { + name: "test09 size-2 page-2 filters skip nextToken with prefix", + prefix: "aaaaaa", + pageSize: 3, // reads off the end + filterJobID: "example", + filterStatus: "pending", + nextToken: "aaaaaaaa-3350-4b4b-d185-0e1992ed43e9", + expectedNextToken: "", + expectedIDs: []string{ + "aaaaaadd-3350-4b4b-d185-0e1992ed43e9", + "aaaaaaee-3350-4b4b-d185-0e1992ed43e9", + }, + }, + { + name: "test10 no valid results with filters", + pageSize: 2, + filterJobID: "whatever", + nextToken: "", + expectedIDs: []string{}, + }, + { + name: "test11 no valid results with filters and prefix", + prefix: "aaaa", + pageSize: 2, + filterJobID: "whatever", + nextToken: "", + expectedIDs: []string{}, + }, + { + name: "test12 no valid results with filters page-2", + filterJobID: "whatever", + nextToken: "aaaaaa11-3350-4b4b-d185-0e1992ed43e9", + expectedIDs: []string{}, + }, + { + name: "test13 no valid results with filters page-2 with prefix", + prefix: "aaaa", + filterJobID: "whatever", + nextToken: "aaaaaa11-3350-4b4b-d185-0e1992ed43e9", + expectedIDs: []string{}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + req := &structs.EvalListRequest{ + FilterJobID: tc.filterJobID, + FilterEvalStatus: tc.filterStatus, + QueryOptions: structs.QueryOptions{ + Region: "global", + Namespace: tc.namespace, + Prefix: tc.prefix, + PerPage: tc.pageSize, + NextToken: tc.nextToken, + }, + } + req.AuthToken = aclToken + var resp structs.EvalListResponse + require.NoError(t, msgpackrpc.CallWithCodec(codec, "Eval.List", req, &resp)) + gotIDs := []string{} + for _, eval := range resp.Evaluations { + gotIDs = append(gotIDs, eval.ID) + } + require.Equal(t, tc.expectedIDs, gotIDs, "unexpected page of evals") + require.Equal(t, tc.expectedNextToken, resp.QueryMeta.NextToken, "unexpected NextToken") + }) + } +} + func TestEvalEndpoint_Allocations(t *testing.T) { t.Parallel() diff --git a/nomad/state/paginator.go b/nomad/state/paginator.go new file mode 100644 index 000000000..ecf131b52 --- /dev/null +++ b/nomad/state/paginator.go @@ -0,0 +1,88 @@ +package state + +import ( + memdb "github.com/hashicorp/go-memdb" + "github.com/hashicorp/nomad/nomad/structs" +) + +// Paginator is an iterator over a memdb.ResultIterator that returns +// only the expected number of pages. +type Paginator struct { + iter memdb.ResultIterator + perPage int32 + itemCount int32 + seekingToken string + nextToken string + nextTokenFound bool + + // appendFunc is the function the caller should use to append raw + // entries to the results set. The object is guaranteed to be + // non-nil. + appendFunc func(interface{}) +} + +func NewPaginator(iter memdb.ResultIterator, opts structs.QueryOptions, appendFunc func(interface{})) *Paginator { + return &Paginator{ + iter: iter, + perPage: opts.PerPage, + seekingToken: opts.NextToken, + nextTokenFound: opts.NextToken == "", + appendFunc: appendFunc, + } +} + +// Page populates a page by running the append function +// over all results. Returns the next token +func (p *Paginator) Page() string { +DONE: + for { + raw, andThen := p.next() + switch andThen { + case paginatorInclude: + p.appendFunc(raw) + case paginatorSkip: + continue + case paginatorComplete: + break DONE + } + } + return p.nextToken +} + +func (p *Paginator) next() (interface{}, paginatorState) { + raw := p.iter.Next() + if raw == nil { + p.nextToken = "" + return nil, paginatorComplete + } + + // have we found the token we're seeking (if any)? + id := raw.(IDGetter).GetID() + p.nextToken = id + if !p.nextTokenFound && id < p.seekingToken { + return nil, paginatorSkip + } + p.nextTokenFound = true + + // have we produced enough results for this page? + p.itemCount++ + if p.perPage != 0 && p.itemCount > p.perPage { + return raw, paginatorComplete + } + + return raw, paginatorInclude +} + +// IDGetter must be implemented for the results of any iterator we +// want to paginate +type IDGetter interface { + GetID() string +} + +type paginatorState int + +const ( + paginatorInclude paginatorState = iota + paginatorSkip + paginatorComplete +) diff --git a/nomad/state/paginator_test.go b/nomad/state/paginator_test.go new file mode 100644 index 000000000..cae606783 --- /dev/null +++ b/nomad/state/paginator_test.go @@ -0,0 +1,112 @@ +package state + +import ( + "testing" + + "github.com/stretchr/testify/require" + + memdb "github.com/hashicorp/go-memdb" + "github.com/hashicorp/nomad/nomad/structs" +) + +func TestPaginator(t *testing.T) { + t.Parallel() + ids := []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"} + + cases := []struct { + name string + perPage int32 + nextToken string + expected []string + expectedNextToken string + }{ + { + name: "size-3 page-1", + perPage: 3, + expected: []string{"0", "1", "2"}, + expectedNextToken: "3", + }, + { + name: "size-5 page-2 stop before end", + perPage: 5, + nextToken: "3", + expected: []string{"3", "4", "5", "6", "7"}, + expectedNextToken: "8", + }, + { + name: "page-2 reading off the end", + perPage: 10, + nextToken: "5", + expected: []string{"5", "6", "7", "8", "9"}, + expectedNextToken: "", + }, + { + name: "starting off the end", + perPage: 5, + nextToken: "a", + expected: []string{}, + expectedNextToken: "", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + + iter := newTestIterator(ids) + results := []string{} + + paginator := NewPaginator(iter, + structs.QueryOptions{ + PerPage: tc.perPage, NextToken: tc.nextToken, + }, + func(raw interface{}) { + result := raw.(*mockObject) + results = append(results, result.GetID()) + }, + ) + + nextToken := paginator.Page() + require.Equal(t, tc.expected, results) + require.Equal(t, tc.expectedNextToken, nextToken) + }) + } + +} + +// helpers for pagination tests + +// implements memdb.ResultIterator interface +type testResultIterator struct { + results chan interface{} + idx int +} + +func (i testResultIterator) Next() interface{} { + select { + case result := <-i.results: + return result + default: + return nil + } +} + +// not used, but required to implement memdb.ResultIterator +func (i testResultIterator) WatchCh() <-chan struct{} { + return make(<-chan struct{}) +} + +type mockObject struct { + id string +} + +func (m *mockObject) GetID() string { + return m.id +} + +func newTestIterator(ids []string) memdb.ResultIterator { + iter := testResultIterator{results: make(chan interface{}, 20)} + for _, id := range ids { + iter.results <- &mockObject{id: id} + } + return iter +} diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index 50f9f640b..48c978228 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -277,8 +277,10 @@ type QueryOptions struct { // paginated lists. PerPage int32 - // NextToken is the token used indicate where to start paging for queries - // that support paginated lists. + // NextToken is the token used to indicate where to start paging + // for queries that support paginated lists. This token should be + // the ID of the next object after the last one seen in the + // previous response. NextToken string InternalRpcInfo @@ -436,6 +438,11 @@ type QueryMeta struct { // Used to indicate if there is a known leader node KnownLeader bool + + // NextToken is the token returned with queries that support + // paginated lists. To resume paging from this point, pass + // this token in the next request's QueryOptions. + NextToken string } // WriteMeta allows a write response to include potentially @@ -844,9 +851,23 @@ type EvalDequeueRequest struct { // EvalListRequest is used to list the evaluations type EvalListRequest struct { + FilterJobID string + FilterEvalStatus string QueryOptions } +// ShouldBeFiltered indicates that the eval should be filtered (that +// is, removed) from the results +func (req *EvalListRequest) ShouldBeFiltered(e *Evaluation) bool { + if req.FilterJobID != "" && req.FilterJobID != e.JobID { + return true + } + if req.FilterEvalStatus != "" && req.FilterEvalStatus != e.Status { + return true + } + return false +} + // PlanRequest is used to submit an allocation plan to the leader type PlanRequest struct { Plan *Plan @@ -10391,6 +10412,14 @@ type Evaluation struct { ModifyTime int64 } +// GetID implements the IDGetter interface, required for pagination +func (e *Evaluation) GetID() string { + if e == nil { + return "" + } + return e.ID +} + // TerminalStatus returns if the current status is terminal and // will no longer transition. func (e *Evaluation) TerminalStatus() bool { diff --git a/website/content/api-docs/evaluations.mdx b/website/content/api-docs/evaluations.mdx index 4f5ece72a..9711140ed 100644 --- a/website/content/api-docs/evaluations.mdx +++ b/website/content/api-docs/evaluations.mdx @@ -31,6 +31,24 @@ The table below shows this endpoint's support for even number of hexadecimal characters (0-9a-f). This is specified as a query string parameter. +- `next_token` `(string: "")` - This endpoint supports paging. The + `next_token` parameter accepts a string which is the `ID` field of + the next expected evaluation. This value can be obtained from the + `X-Nomad-NextToken` header from the previous response. + +- `per_page` `(int: 0)` - Specifies a maximum number of evaluations to + return for this request. If omitted, the response is not + paginated. The `ID` of the last evaluation in the response can be + used as the `last_token` of the next request to fetch additional + pages. + +- `job` `(string: "")` - Filter the list of evaluations to a specific + job ID. + +- `status` `(string: "")` - Filter the list of evaluations to a + specific evaluation status (one of `blocked`, `pending`, `complete`, + `failed`, or `canceled`). + ### Sample Request ```shell-session