Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cli/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,7 @@ func QueryConvertWorkspaces[T any](ctx context.Context, client *codersdk.Client,
}
return converted, nil
}

// pageLimit is the number of rows requested per page when the CLI fetches a
// list endpoint to exhaustion.
const pageLimit = codersdk.MaxPaginationLimit
2 changes: 1 addition & 1 deletion cli/templatepull.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (r *RootCmd) templatePull() *serpent.Command {
{
// Determine the latest template version and compare with the
// active version. If they aren't the same, warn the user.
versions, err := client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{
versions, err := allTemplateVersions(ctx, client, codersdk.TemplateVersionsByTemplateRequest{
TemplateID: template.ID,
})
if err != nil {
Expand Down
21 changes: 20 additions & 1 deletion cli/templateversions.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"context"
"fmt"
"strings"
"time"
Expand All @@ -14,6 +15,24 @@ import (
"github.com/coder/serpent"
)

// allTemplateVersions fetches template versions page by page until a page
// returns fewer rows than requested, which marks the end of the result set.
func allTemplateVersions(ctx context.Context, client *codersdk.Client, req codersdk.TemplateVersionsByTemplateRequest) ([]codersdk.TemplateVersion, error) {
req.Limit = pageLimit
var versions []codersdk.TemplateVersion
for {
page, err := client.TemplateVersionsByTemplate(ctx, req)
if err != nil {
return nil, err
}
versions = append(versions, page...)
if len(page) < pageLimit {
return versions, nil
}
req.Offset += len(page)
}
}

func (r *RootCmd) templateVersions() *serpent.Command {
cmd := &serpent.Command{
Use: "versions",
Expand Down Expand Up @@ -110,7 +129,7 @@ func (r *RootCmd) templateVersionsList() *serpent.Command {
IncludeArchived: includeArchived.Value(),
}

versions, err := client.TemplateVersionsByTemplate(inv.Context(), req)
versions, err := allTemplateVersions(inv.Context(), client, req)
if err != nil {
return xerrors.Errorf("get template versions by template: %w", err)
}
Expand Down
19 changes: 15 additions & 4 deletions cli/userlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,23 @@ func (r *RootCmd) userList() *serpent.Command {
req.Search = fmt.Sprintf("github_com_user_id:%d", githubUserID)
}

res, err := client.Users(inv.Context(), req)
if err != nil {
return err
// Fetch pages until a page returns fewer rows than requested,
// which marks the end of the result set.
req.Limit = pageLimit
users := []codersdk.User{}
for {
res, err := client.Users(inv.Context(), req)
if err != nil {
return err
}
users = append(users, res.Users...)
if len(res.Users) < pageLimit {
break
}
req.Offset += len(res.Users)
}

out, err := formatter.Format(inv.Context(), res.Users)
out, err := formatter.Format(inv.Context(), users)
if err != nil {
return err
}
Expand Down
8 changes: 3 additions & 5 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 3 additions & 5 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion coderd/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const auditLogCountCap = 2000
// @Produce json
// @Tags Audit
// @Param q query string false "Search query"
// @Param limit query int true "Page limit"
// @Param limit query int false "Page limit"
// @Param offset query int false "Page offset"
// @Success 200 {object} codersdk.AuditLogResponse
// @Router /api/v2/audit [get]
Expand Down
2 changes: 1 addition & 1 deletion coderd/members.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ func (api *API) listMembers(rw http.ResponseWriter, r *http.Request) {
// @Param organization path string true "Organization ID"
// @Param q query string false "Member search query"
// @Param after_id query string false "After ID" format(uuid)
// @Param limit query int false "Page limit, if 0 returns all members"
// @Param limit query int false "Page limit"
// @Param offset query int false "Page offset"
// @Success 200 {object} []codersdk.PaginatedMembersResponse
// @Router /api/v2/organizations/{organization}/paginated-members [get]
Expand Down
21 changes: 17 additions & 4 deletions coderd/pagination.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package coderd

import (
"fmt"
"net/http"

"github.com/google/uuid"
Expand All @@ -17,10 +18,22 @@ func ParsePagination(w http.ResponseWriter, r *http.Request) (p codersdk.Paginat
parser := httpapi.NewQueryParamParser()
params := codersdk.Pagination{
AfterID: parser.UUID(queryParams, uuid.Nil, "after_id"),
// A limit of 0 should be interpreted by the SQL query as "null" or
// "no limit". Do not make this value anything besides 0.
Limit: int(parser.PositiveInt32(queryParams, 0, "limit")),
Offset: int(parser.PositiveInt32(queryParams, 0, "offset")),
Offset: int(parser.PositiveInt32(queryParams, 0, "offset")),
}
limitErrsBefore := len(parser.Errors)
params.Limit = int(parser.PositiveInt32(queryParams, 0, "limit"))
limitParsed := len(parser.Errors) == limitErrsBefore
// An omitted limit resolves to codersdk.MaxPaginationLimit so the downstream
// query is never unbounded. A limit that is set must be a positive integer no
// greater than that maximum; otherwise it is rejected rather than clamped.
switch {
case queryParams.Get("limit") == "":
params.Limit = codersdk.MaxPaginationLimit
case limitParsed && (params.Limit < 1 || params.Limit > codersdk.MaxPaginationLimit):
parser.Errors = append(parser.Errors, codersdk.ValidationError{
Field: "limit",
Detail: fmt.Sprintf("Query param \"limit\" must be a positive integer no greater than %d.", codersdk.MaxPaginationLimit),
})
}
if len(parser.Errors) > 0 {
httpapi.Write(ctx, w, http.StatusBadRequest, codersdk.Response{
Expand Down
31 changes: 31 additions & 0 deletions coderd/pagination_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"strconv"
"testing"

"github.com/google/uuid"
Expand Down Expand Up @@ -72,6 +73,11 @@ func TestPagination(t *testing.T) {
Offset: "-1",
ExpectedError: invalidValues,
},
{
Name: "LimitAboveMax",
Limit: strconv.Itoa(codersdk.MaxPaginationLimit + 1),
ExpectedError: invalidValues,
},

// Valid values
{
Expand All @@ -93,11 +99,35 @@ func TestPagination(t *testing.T) {
Limit: 50,
},
},
{
// An omitted limit resolves to the maximum page size so the
// query is never unbounded.
Name: "DefaultsToMaxWhenAbsent",
ExpectedParams: codersdk.Pagination{
AfterID: uuid.Nil,
Limit: codersdk.MaxPaginationLimit,
},
},
{
// An explicit limit must be positive; 0 is rejected.
Name: "ZeroLimitRejected",
Limit: "0",
ExpectedError: invalidValues,
},
{
Name: "LimitAtMax",
Limit: strconv.Itoa(codersdk.MaxPaginationLimit),
ExpectedParams: codersdk.Pagination{
AfterID: uuid.Nil,
Limit: codersdk.MaxPaginationLimit,
},
},
{
Name: "ValidOffset",
Offset: "150",
ExpectedParams: codersdk.Pagination{
AfterID: uuid.Nil,
Limit: codersdk.MaxPaginationLimit,
Offset: 150,
},
},
Expand All @@ -106,6 +136,7 @@ func TestPagination(t *testing.T) {
AfterID: "5f2005fc-acc4-4e5e-a7fa-be017359c60b",
ExpectedParams: codersdk.Pagination{
AfterID: uuid.MustParse("5f2005fc-acc4-4e5e-a7fa-be017359c60b"),
Limit: codersdk.MaxPaginationLimit,
},
},
}
Expand Down
13 changes: 10 additions & 3 deletions codersdk/pagination.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import (
"github.com/google/uuid"
)

// MaxPaginationLimit is the largest page size the server accepts. An omitted
// limit resolves to this value. A limit that is set must be a positive integer
// no greater than this, so the resulting query is always bounded.
const MaxPaginationLimit = 100

// Pagination sets pagination options for the endpoints that support it.
type Pagination struct {
// AfterID returns all or up to Limit results after the given
Expand All @@ -15,9 +20,11 @@ type Pagination struct {
// set AfterID to the last UUID returned by the previous
// request.
AfterID uuid.UUID `json:"after_id,omitempty" format:"uuid"`
// Limit sets the maximum number of users to be returned
// in a single page. If the limit is <= 0, there is no limit
// and all users are returned.
// Limit sets the maximum number of results returned in a single page.
// When set, it must be a positive integer no greater than
// MaxPaginationLimit, otherwise the request is rejected. A value of 0
// omits the query parameter entirely, and the server resolves the page
// size to MaxPaginationLimit.
Limit int `json:"limit,omitempty"`
// Offset is used to indicate which page to return. An offset of 0
// returns the first 'limit' number of users.
Expand Down
2 changes: 1 addition & 1 deletion codersdk/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ func (c *Client) Workspaces(ctx context.Context, filter WorkspaceFilter) (Worksp
}

// WorkspacePageSize is the number of rows AllWorkspaces requests per page.
const WorkspacePageSize = 100
const WorkspacePageSize = MaxPaginationLimit

// AllWorkspaces requests successive pages of workspaces matching the filter and
// returns every row. Limit and Offset on the filter are ignored.
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/api/audit.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions docs/reference/api/enterprise.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions docs/reference/api/members.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading