diff --git a/packages/api/internal/api/api.gen.go b/packages/api/internal/api/api.gen.go index a682dd1ddd..8964625a8d 100644 --- a/packages/api/internal/api/api.gen.go +++ b/packages/api/internal/api/api.gen.go @@ -98,6 +98,9 @@ type ServerInterface interface { // (DELETE /templates/{templateID}) DeleteTemplatesTemplateID(c *gin.Context, templateID TemplateID) + // (GET /templates/{templateID}) + GetTemplatesTemplateID(c *gin.Context, templateID TemplateID, params GetTemplatesTemplateIDParams) + // (PATCH /templates/{templateID}) PatchTemplatesTemplateID(c *gin.Context, templateID TemplateID) @@ -964,6 +967,55 @@ func (siw *ServerInterfaceWrapper) DeleteTemplatesTemplateID(c *gin.Context) { siw.Handler.DeleteTemplatesTemplateID(c, templateID) } +// GetTemplatesTemplateID operation middleware +func (siw *ServerInterfaceWrapper) GetTemplatesTemplateID(c *gin.Context) { + + var err error + + // ------------- Path parameter "templateID" ------------- + var templateID TemplateID + + err = runtime.BindStyledParameterWithOptions("simple", "templateID", c.Param("templateID"), &templateID, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter templateID: %w", err), http.StatusBadRequest) + return + } + + c.Set(ApiKeyAuthScopes, []string{}) + + c.Set(Supabase1TokenAuthScopes, []string{}) + + c.Set(Supabase2TeamAuthScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params GetTemplatesTemplateIDParams + + // ------------- Optional query parameter "nextToken" ------------- + + err = runtime.BindQueryParameter("form", true, false, "nextToken", c.Request.URL.Query(), ¶ms.NextToken) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter nextToken: %w", err), http.StatusBadRequest) + return + } + + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameter("form", true, false, "limit", c.Request.URL.Query(), ¶ms.Limit) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter limit: %w", err), http.StatusBadRequest) + return + } + + for _, middleware := range siw.HandlerMiddlewares { + middleware(c) + if c.IsAborted() { + return + } + } + + siw.Handler.GetTemplatesTemplateID(c, templateID, params) +} + // PatchTemplatesTemplateID operation middleware func (siw *ServerInterfaceWrapper) PatchTemplatesTemplateID(c *gin.Context) { @@ -1338,6 +1390,7 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options router.GET(options.BaseURL+"/templates", wrapper.GetTemplates) router.POST(options.BaseURL+"/templates", wrapper.PostTemplates) router.DELETE(options.BaseURL+"/templates/:templateID", wrapper.DeleteTemplatesTemplateID) + router.GET(options.BaseURL+"/templates/:templateID", wrapper.GetTemplatesTemplateID) router.PATCH(options.BaseURL+"/templates/:templateID", wrapper.PatchTemplatesTemplateID) router.POST(options.BaseURL+"/templates/:templateID", wrapper.PostTemplatesTemplateID) router.POST(options.BaseURL+"/templates/:templateID/builds/:buildID", wrapper.PostTemplatesTemplateIDBuildsBuildID) diff --git a/packages/api/internal/api/spec.gen.go b/packages/api/internal/api/spec.gen.go index 76ad6c5256..e54d9d2705 100644 --- a/packages/api/internal/api/spec.gen.go +++ b/packages/api/internal/api/spec.gen.go @@ -19,114 +19,117 @@ import ( var swaggerSpec = []string{ "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/packages/api/internal/api/types.gen.go b/packages/api/internal/api/types.gen.go index 043f87eead..ed7549cbcb 100644 --- a/packages/api/internal/api/types.gen.go +++ b/packages/api/internal/api/types.gen.go @@ -711,20 +711,31 @@ type Template struct { // TemplateBuild defines model for TemplateBuild. type TemplateBuild struct { // BuildID Identifier of the build - BuildID string `json:"buildID"` + BuildID openapi_types.UUID `json:"buildID"` - // LogEntries Build logs structured - LogEntries []BuildLogEntry `json:"logEntries"` + // CpuCount CPU cores for the sandbox + CpuCount CPUCount `json:"cpuCount"` - // Logs Build logs - Logs []string `json:"logs"` - Reason *BuildStatusReason `json:"reason,omitempty"` + // CreatedAt Time when the build was created + CreatedAt time.Time `json:"createdAt"` + + // DiskSizeMB Disk size for the sandbox in MiB + DiskSizeMB *DiskSizeMB `json:"diskSizeMB,omitempty"` + + // EnvdVersion Version of the envd running in the sandbox + EnvdVersion *EnvdVersion `json:"envdVersion,omitempty"` + + // FinishedAt Time when the build was finished + FinishedAt *time.Time `json:"finishedAt,omitempty"` + + // MemoryMB Memory for the sandbox in MiB + MemoryMB MemoryMB `json:"memoryMB"` // Status Status of the template build Status TemplateBuildStatus `json:"status"` - // TemplateID Identifier of the template - TemplateID string `json:"templateID"` + // UpdatedAt Time when the build was last updated + UpdatedAt time.Time `json:"updatedAt"` } // TemplateBuildFileUpload defines model for TemplateBuildFileUpload. @@ -736,6 +747,25 @@ type TemplateBuildFileUpload struct { Url *string `json:"url,omitempty"` } +// TemplateBuildInfo defines model for TemplateBuildInfo. +type TemplateBuildInfo struct { + // BuildID Identifier of the build + BuildID string `json:"buildID"` + + // LogEntries Build logs structured + LogEntries []BuildLogEntry `json:"logEntries"` + + // Logs Build logs + Logs []string `json:"logs"` + Reason *BuildStatusReason `json:"reason,omitempty"` + + // Status Status of the template build + Status TemplateBuildStatus `json:"status"` + + // TemplateID Identifier of the template + TemplateID string `json:"templateID"` +} + // TemplateBuildRequest defines model for TemplateBuildRequest. type TemplateBuildRequest struct { // Alias Alias of the template @@ -894,6 +924,33 @@ type TemplateUpdateRequest struct { Public *bool `json:"public,omitempty"` } +// TemplateWithBuilds defines model for TemplateWithBuilds. +type TemplateWithBuilds struct { + // Aliases Aliases of the template + Aliases []string `json:"aliases"` + + // Builds List of builds for the template + Builds []TemplateBuild `json:"builds"` + + // CreatedAt Time when the template was created + CreatedAt time.Time `json:"createdAt"` + + // LastSpawnedAt Time when the template was last used + LastSpawnedAt *time.Time `json:"lastSpawnedAt"` + + // Public Whether the template is public or only accessible by the team + Public bool `json:"public"` + + // SpawnCount Number of times the template was used + SpawnCount int64 `json:"spawnCount"` + + // TemplateID Identifier of the template + TemplateID string `json:"templateID"` + + // UpdatedAt Time when the template was last updated + UpdatedAt time.Time `json:"updatedAt"` +} + // UpdateTeamAPIKey defines model for UpdateTeamAPIKey. type UpdateTeamAPIKey struct { // Name New name for the API key @@ -912,6 +969,12 @@ type BuildID = string // NodeID defines model for nodeID. type NodeID = string +// PaginationLimit defines model for paginationLimit. +type PaginationLimit = int32 + +// PaginationNextToken defines model for paginationNextToken. +type PaginationNextToken = string + // SandboxID defines model for sandboxID. type SandboxID = string @@ -1010,6 +1073,15 @@ type GetTemplatesParams struct { TeamID *string `form:"teamID,omitempty" json:"teamID,omitempty"` } +// GetTemplatesTemplateIDParams defines parameters for GetTemplatesTemplateID. +type GetTemplatesTemplateIDParams struct { + // NextToken Cursor to start the list from + NextToken *PaginationNextToken `form:"nextToken,omitempty" json:"nextToken,omitempty"` + + // Limit Maximum number of items to return per page + Limit *PaginationLimit `form:"limit,omitempty" json:"limit,omitempty"` +} + // GetTemplatesTemplateIDBuildsBuildIDStatusParams defines parameters for GetTemplatesTemplateIDBuildsBuildIDStatus. type GetTemplatesTemplateIDBuildsBuildIDStatusParams struct { // LogsOffset Index of the starting build log that should be returned with the template @@ -1026,10 +1098,10 @@ type GetV2SandboxesParams struct { State *[]SandboxState `form:"state,omitempty" json:"state,omitempty"` // NextToken Cursor to start the list from - NextToken *string `form:"nextToken,omitempty" json:"nextToken,omitempty"` + NextToken *PaginationNextToken `form:"nextToken,omitempty" json:"nextToken,omitempty"` // Limit Maximum number of items to return per page - Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"` + Limit *PaginationLimit `form:"limit,omitempty" json:"limit,omitempty"` } // PostAccessTokensJSONRequestBody defines body for PostAccessTokens for application/json ContentType. diff --git a/packages/api/internal/handlers/sandboxes_list.go b/packages/api/internal/handlers/sandboxes_list.go index 798dc9e48e..1bfbc0f106 100644 --- a/packages/api/internal/handlers/sandboxes_list.go +++ b/packages/api/internal/handlers/sandboxes_list.go @@ -27,13 +27,11 @@ import ( ) const ( - maxSandboxListLimit int32 = 100 - defaultSandboxListLimit int32 = 100 + sandboxesDefaultLimit = int32(100) + sandboxesMaxLimit = int32(100) ) -func (a *APIStore) getPausedSandboxes(ctx context.Context, teamID uuid.UUID, runningSandboxesIDs []string, metadataFilter *map[string]string, limit int32, cursorTime time.Time, cursorID string) ([]utils.PaginatedSandbox, error) { - // Apply limit + 1 to check if there are more results - queryLimit := limit + 1 +func (a *APIStore) getPausedSandboxes(ctx context.Context, teamID uuid.UUID, runningSandboxesIDs []string, metadataFilter *map[string]string, queryLimit int32, cursorTime time.Time, cursorID string) ([]utils.PaginatedSandbox, error) { queryMetadata := dbtypes.JSONBStringMap{} if metadataFilter != nil { queryMetadata = *metadataFilter @@ -115,14 +113,23 @@ func (a *APIStore) GetV2Sandboxes(c *gin.Context, params api.GetV2SandboxesParam states = append(states, *params.State...) } - limit := defaultSandboxListLimit - if params.Limit != nil { - limit = *params.Limit - } + // Initialize pagination + pagination, err := utils.NewPagination[utils.PaginatedSandbox]( + utils.PaginationParams{ + Limit: params.Limit, + NextToken: params.NextToken, + }, + utils.PaginationConfig{ + DefaultLimit: sandboxesDefaultLimit, + MaxLimit: sandboxesMaxLimit, + DefaultID: utils.MaxSandboxID, + }, + ) + if err != nil { + telemetry.ReportError(ctx, "error parsing pagination cursor", err) + a.sendAPIStoreError(c, http.StatusBadRequest, "Invalid next token") - // Clip limit to max - if limit > maxSandboxListLimit { - limit = maxSandboxListLimit + return } metadataFilter, err := utils.ParseMetadata(params.Metadata) @@ -136,15 +143,6 @@ func (a *APIStore) GetV2Sandboxes(c *gin.Context, params api.GetV2SandboxesParam // Get sandboxes with pagination sandboxes := make([]utils.PaginatedSandbox, 0) - // Parse the next token to offset sandboxes for pagination - cursorTime, cursorID, err := utils.ParseNextToken(params.NextToken) - if err != nil { - zap.L().Error("Error parsing cursor", zap.Error(err)) - a.sendAPIStoreError(c, http.StatusBadRequest, "Invalid next token") - - return - } - allSandboxes := a.orchestrator.GetSandboxes(ctx, team.ID, []sandbox.State{sandbox.StateRunning, sandbox.StatePausing}) runningSandboxes := sharedUtils.Filter(allSandboxes, func(sbx sandbox.Sandbox) bool { return sbx.State == sandbox.StateRunning @@ -163,7 +161,7 @@ func (a *APIStore) GetV2Sandboxes(c *gin.Context, params api.GetV2SandboxesParam c.Header("X-Total-Running", strconv.Itoa(len(runningSandboxList))) // Filter based on cursor - runningSandboxList = utils.FilterBasedOnCursor(runningSandboxList, cursorTime, cursorID) + runningSandboxList = utils.FilterBasedOnCursor(runningSandboxList, pagination.CursorTime(), pagination.CursorID()) sandboxes = append(sandboxes, runningSandboxList...) } @@ -178,7 +176,7 @@ func (a *APIStore) GetV2Sandboxes(c *gin.Context, params api.GetV2SandboxesParam runningSandboxesIDs = append(runningSandboxesIDs, utils.ShortID(info.SandboxID)) } - pausedSandboxList, err := a.getPausedSandboxes(ctx, team.ID, runningSandboxesIDs, metadataFilter, limit, cursorTime, cursorID) + pausedSandboxList, err := a.getPausedSandboxes(ctx, team.ID, runningSandboxesIDs, metadataFilter, pagination.QueryLimit(), pagination.CursorTime(), pagination.CursorID()) if err != nil { zap.L().Error("Error getting paused sandboxes", zap.Error(err)) a.sendAPIStoreError(c, http.StatusInternalServerError, "Error getting paused sandboxes") @@ -188,7 +186,7 @@ func (a *APIStore) GetV2Sandboxes(c *gin.Context, params api.GetV2SandboxesParam pausingSandboxList := instanceInfoToPaginatedSandboxes(pausingSandboxes) pausingSandboxList = utils.FilterSandboxesOnMetadata(pausingSandboxList, metadataFilter) - pausingSandboxList = utils.FilterBasedOnCursor(pausingSandboxList, cursorTime, cursorID) + pausingSandboxList = utils.FilterBasedOnCursor(pausingSandboxList, pagination.CursorTime(), pagination.CursorID()) sandboxes = append(sandboxes, pausedSandboxList...) sandboxes = append(sandboxes, pausingSandboxList...) @@ -197,21 +195,9 @@ func (a *APIStore) GetV2Sandboxes(c *gin.Context, params api.GetV2SandboxesParam // We need to sort again after merging running and paused sandboxes utils.SortPaginatedSandboxesDesc(sandboxes) - var nextToken *string - if len(sandboxes) > int(limit) { - // We have more results than the limit, so we need to set the nextToken - lastSandbox := sandboxes[limit-1] - cursor := lastSandbox.GenerateCursor() - nextToken = &cursor - - // Trim to the requested limit - sandboxes = sandboxes[:limit] - } - - // Add pagination info to headers - if nextToken != nil { - c.Header("X-Next-Token", *nextToken) - } + sandboxes = pagination.ProcessResultsWithHeader(c, sandboxes, func(s utils.PaginatedSandbox) (time.Time, string) { + return s.PaginationTimestamp, s.SandboxID + }) c.JSON(http.StatusOK, sandboxes) } diff --git a/packages/api/internal/handlers/store.go b/packages/api/internal/handlers/store.go index 9216f5caba..620f2e7daf 100644 --- a/packages/api/internal/handlers/store.go +++ b/packages/api/internal/handlers/store.go @@ -39,6 +39,8 @@ import ( // This is a security measure to prevent the use of weak secrets (like empty). const minSupabaseJWTSecretLength = 16 +var _ api.ServerInterface = (*APIStore)(nil) + type APIStore struct { Healthy bool config cfg.Config diff --git a/packages/api/internal/handlers/template_build_status.go b/packages/api/internal/handlers/template_build_status.go index da2667b461..97ff4a863e 100644 --- a/packages/api/internal/handlers/template_build_status.go +++ b/packages/api/internal/handlers/template_build_status.go @@ -65,7 +65,7 @@ func (a *APIStore) GetTemplatesTemplateIDBuildsBuildIDStatus(c *gin.Context, tem // early return if still waiting for build start if buildInfo.BuildStatus == envbuild.StatusWaiting { - result := api.TemplateBuild{ + result := api.TemplateBuildInfo{ LogEntries: make([]api.BuildLogEntry, 0), Logs: make([]string, 0), TemplateID: templateID, @@ -79,7 +79,7 @@ func (a *APIStore) GetTemplatesTemplateIDBuildsBuildIDStatus(c *gin.Context, tem } // Needs to be before logs request so the status is not set to done too early - result := api.TemplateBuild{ + result := api.TemplateBuildInfo{ LogEntries: nil, Logs: nil, TemplateID: templateID, diff --git a/packages/api/internal/handlers/template_get.go b/packages/api/internal/handlers/template_get.go new file mode 100644 index 0000000000..707957a4a5 --- /dev/null +++ b/packages/api/internal/handlers/template_get.go @@ -0,0 +1,122 @@ +package handlers + +import ( + "fmt" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + + "github.com/e2b-dev/infra/packages/api/internal/api" + "github.com/e2b-dev/infra/packages/api/internal/utils" + "github.com/e2b-dev/infra/packages/db/dberrors" + "github.com/e2b-dev/infra/packages/db/queries" + "github.com/e2b-dev/infra/packages/shared/pkg/telemetry" + sharedUtils "github.com/e2b-dev/infra/packages/shared/pkg/utils" +) + +const ( + templateBuildsDefaultLimit = int32(100) + templateBuildsMaxLimit = int32(100) +) + +var maxBuildID = uuid.Max.String() + +func (a *APIStore) GetTemplatesTemplateID(c *gin.Context, templateID api.TemplateID, params api.GetTemplatesTemplateIDParams) { + ctx := c.Request.Context() + + team, apiErr := a.GetTeam(ctx, c, nil) + if apiErr != nil { + a.sendAPIStoreError(c, apiErr.Code, apiErr.ClientMsg) + telemetry.ReportCriticalError(ctx, "error when getting team and tier", apiErr.Err) + + return + } + + telemetry.SetAttributes(ctx, + telemetry.WithTeamID(team.ID.String()), + ) + + template, err := a.sqlcDB.GetTemplateByIDWithAliases(ctx, templateID) + switch { + case dberrors.IsNotFoundError(err): + a.sendAPIStoreError(c, http.StatusNotFound, fmt.Sprintf("Template %s not found", templateID)) + telemetry.ReportError(ctx, "template not found", err, telemetry.WithTemplateID(templateID)) + + return + case err != nil: + a.sendAPIStoreError(c, http.StatusInternalServerError, "Error when getting template") + telemetry.ReportCriticalError(ctx, "error when getting template", err) + + return + case template.TeamID != team.ID: + telemetry.ReportError(ctx, "user doesn't have access to the template", nil, telemetry.WithTemplateID(templateID)) + a.sendAPIStoreError(c, http.StatusForbidden, fmt.Sprintf("You don't have access to this sandbox template (%s)", templateID)) + + return + } + + // Initialize pagination + pagination, err := utils.NewPagination[queries.EnvBuild]( + utils.PaginationParams{ + Limit: params.Limit, + NextToken: params.NextToken, + }, + utils.PaginationConfig{ + DefaultLimit: templateBuildsDefaultLimit, + MaxLimit: templateBuildsMaxLimit, + DefaultID: maxBuildID, + }, + ) + if err != nil { + telemetry.ReportError(ctx, "error parsing pagination cursor", err) + a.sendAPIStoreError(c, http.StatusBadRequest, "Invalid next token") + + return + } + + builds, err := a.sqlcDB.GetTemplateBuilds(ctx, queries.GetTemplateBuildsParams{ + TemplateID: templateID, + CursorTime: pagination.CursorTime(), + CursorID: pagination.CursorID(), + BuildLimit: pagination.QueryLimit(), + }) + if err != nil { + a.sendAPIStoreError(c, http.StatusInternalServerError, "Error when getting builds") + telemetry.ReportCriticalError(ctx, "error when getting builds", err) + + return + } + + builds = pagination.ProcessResultsWithHeader(c, builds, func(b queries.EnvBuild) (time.Time, string) { + return b.CreatedAt, b.ID.String() + }) + + res := api.TemplateWithBuilds{ + TemplateID: template.ID, + Public: template.Public, + Aliases: template.Aliases, + CreatedAt: template.CreatedAt, + UpdatedAt: template.UpdatedAt, + LastSpawnedAt: template.LastSpawnedAt, + SpawnCount: template.SpawnCount, + Builds: make([]api.TemplateBuild, 0, len(builds)), + } + + for _, item := range builds { + res.Builds = append(res.Builds, api.TemplateBuild{ + BuildID: item.ID, + Status: api.TemplateBuildStatus(item.Status), + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + FinishedAt: item.FinishedAt, + CpuCount: api.CPUCount(item.Vcpu), + MemoryMB: api.MemoryMB(item.RamMb), + DiskSizeMB: sharedUtils.CastPtr(item.TotalDiskSizeMb, func(v int64) api.DiskSizeMB { return api.DiskSizeMB(v) }), + EnvdVersion: item.EnvdVersion, + }) + } + + c.JSON(http.StatusOK, res) +} diff --git a/packages/api/internal/utils/pagination.go b/packages/api/internal/utils/pagination.go new file mode 100644 index 0000000000..216c4d394f --- /dev/null +++ b/packages/api/internal/utils/pagination.go @@ -0,0 +1,147 @@ +package utils + +import ( + "encoding/base64" + "fmt" + "time" + + "github.com/gin-gonic/gin" +) + +// generateCursor generates a cursor token from a timestamp and ID. +// The cursor format is base64-encoded "{RFC3339Nano_timestamp}__{id}". +func generateCursor(timestamp time.Time, id string) string { + cursor := fmt.Sprintf("%s__%s", timestamp.Format(time.RFC3339Nano), id) + + return base64.URLEncoding.EncodeToString([]byte(cursor)) +} + +// PaginationParams holds pagination parameters from the API request +type PaginationParams struct { + Limit *int32 + NextToken *string +} + +// PaginationConfig holds configuration for pagination behavior +type PaginationConfig struct { + DefaultLimit int32 + MaxLimit int32 + DefaultID string // Default cursor ID when no token is provided (e.g., max UUID or max sandbox ID) +} + +// Cursor represents a parsed pagination cursor +type Cursor struct { + Time time.Time + ID string +} + +// Pagination handles pagination logic for list endpoints +type Pagination[T any] struct { + config PaginationConfig + limit int32 + cursor Cursor + nextToken *string +} + +// NewPagination creates a new pagination instance from request parameters +func NewPagination[T any](params PaginationParams, config PaginationConfig) (*Pagination[T], error) { + p := &Pagination[T]{ + config: config, + } + + // Parse and validate limit + p.limit = config.DefaultLimit + if params.Limit != nil { + p.limit = *params.Limit + } + if p.limit > config.MaxLimit { + p.limit = config.MaxLimit + } + + // Parse cursor token + var err error + p.cursor, err = parseCursorToken(params.NextToken, config.DefaultID) + if err != nil { + return nil, fmt.Errorf("invalid next token: %w", err) + } + + return p, nil +} + +// QueryLimit returns the limit to use for database queries (limit + 1 to detect more results) +func (p *Pagination[T]) QueryLimit() int32 { + return p.limit + 1 +} + +// CursorTime returns the cursor timestamp +func (p *Pagination[T]) CursorTime() time.Time { + return p.cursor.Time +} + +// CursorID returns the cursor ID +func (p *Pagination[T]) CursorID() string { + return p.cursor.ID +} + +// setNextToken sets the next token from the last item in the results +func (p *Pagination[T]) setNextToken(timestamp time.Time, id string) { + cursor := generateCursor(timestamp, id) + p.nextToken = &cursor +} + +// hasMore checks if there are more results based on the result count +func (p *Pagination[T]) hasMore(resultCount int) bool { + return resultCount > int(p.limit) +} + +// trimResults trims the results to the requested limit if there are more +func (p *Pagination[T]) trimResults(results []T) []T { + if p.hasMore(len(results)) { + return results[:p.limit] + } + + return results +} + +// processResults handles pagination: checks for more results, sets next token from last item, and trims results. +// The getTimestampAndID function extracts the timestamp and ID from each result item. +func (p *Pagination[T]) processResults(results []T, getTimestampAndID func(T) (time.Time, string)) []T { + if p.hasMore(len(results)) { + lastItem := results[p.limit-1] + timestamp, id := getTimestampAndID(lastItem) + p.setNextToken(timestamp, id) + } + + return p.trimResults(results) +} + +// ProcessResultsWithHeader handles pagination and sets the X-Next-Token header in one call. +// This is a convenience method that combines ProcessResults and SetHeader. +func (p *Pagination[T]) ProcessResultsWithHeader(c *gin.Context, results []T, getTimestampAndID func(T) (time.Time, string)) []T { + trimmed := p.processResults(results, getTimestampAndID) + p.setHeader(c) + + return trimmed +} + +// setHeader sets the X-Next-Token header if there are more results +func (p *Pagination[T]) setHeader(c *gin.Context) { + if p.nextToken != nil { + c.Header("X-Next-Token", *p.nextToken) + } +} + +// parseCursorToken parses a cursor token, returning default values if token is nil/empty +func parseCursorToken(token *string, defaultID string) (Cursor, error) { + if token != nil && *token != "" { + cursorTime, cursorID, err := ParseCursor(*token) + if err != nil { + return Cursor{}, err + } + + return Cursor{Time: cursorTime, ID: cursorID}, nil + } + + // Default to current time and provided default ID to get the first page + return Cursor{Time: time.Now(), ID: defaultID}, nil +} diff --git a/packages/api/internal/utils/pagination_test.go b/packages/api/internal/utils/pagination_test.go new file mode 100644 index 0000000000..7d330b4221 --- /dev/null +++ b/packages/api/internal/utils/pagination_test.go @@ -0,0 +1,471 @@ +package utils + +import ( + "encoding/base64" + "fmt" + "net/http/httptest" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testItem struct { + ID string + Timestamp time.Time +} + +func TestGenerateCursor(t *testing.T) { + timestamp := time.Date(2024, 1, 15, 10, 30, 45, 123456789, time.UTC) + id := "test-id-123" + + cursor := generateCursor(timestamp, id) + + // Decode and verify + decoded, err := base64.URLEncoding.DecodeString(cursor) + require.NoError(t, err) + + expected := fmt.Sprintf("%s__%s", timestamp.Format(time.RFC3339Nano), id) + assert.Equal(t, expected, string(decoded)) +} + +func TestNewPagination(t *testing.T) { + config := PaginationConfig{ + DefaultLimit: 10, + MaxLimit: 100, + DefaultID: "default-id", + } + + t.Run("with default limit", func(t *testing.T) { + p, err := NewPagination[testItem](PaginationParams{}, config) + require.NoError(t, err) + assert.Equal(t, int32(10), p.limit) + assert.Equal(t, int32(11), p.QueryLimit()) + assert.Equal(t, config.DefaultID, p.CursorID()) + }) + + t.Run("with custom limit", func(t *testing.T) { + limit := int32(25) + p, err := NewPagination[testItem]( + PaginationParams{Limit: &limit}, + config, + ) + require.NoError(t, err) + assert.Equal(t, int32(25), p.limit) + assert.Equal(t, int32(26), p.QueryLimit()) + }) + + t.Run("with limit exceeding max", func(t *testing.T) { + limit := int32(150) + p, err := NewPagination[testItem]( + PaginationParams{Limit: &limit}, + config, + ) + require.NoError(t, err) + assert.Equal(t, int32(100), p.limit) + assert.Equal(t, int32(101), p.QueryLimit()) + }) + + t.Run("with valid next token", func(t *testing.T) { + timestamp := time.Date(2024, 1, 15, 10, 30, 45, 123456789, time.UTC) + id := "test-id-123" + token := generateCursor(timestamp, id) + + p, err := NewPagination[testItem]( + PaginationParams{NextToken: &token}, + config, + ) + require.NoError(t, err) + assert.Equal(t, timestamp, p.CursorTime()) + assert.Equal(t, id, p.CursorID()) + }) + + t.Run("with invalid next token", func(t *testing.T) { + invalidToken := "invalid-token" + _, err := NewPagination[testItem]( + PaginationParams{NextToken: &invalidToken}, + config, + ) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid next token") + }) + + t.Run("with empty next token", func(t *testing.T) { + emptyToken := "" + p, err := NewPagination[testItem]( + PaginationParams{NextToken: &emptyToken}, + config, + ) + require.NoError(t, err) + assert.Equal(t, config.DefaultID, p.CursorID()) + }) + + t.Run("with nil next token", func(t *testing.T) { + p, err := NewPagination[testItem]( + PaginationParams{NextToken: nil}, + config, + ) + require.NoError(t, err) + assert.Equal(t, config.DefaultID, p.CursorID()) + }) +} + +func TestPagination_Limit(t *testing.T) { + config := PaginationConfig{ + DefaultLimit: 20, + MaxLimit: 100, + DefaultID: "default-id", + } + + p, err := NewPagination[testItem](PaginationParams{}, config) + require.NoError(t, err) + assert.Equal(t, int32(20), p.limit) +} + +func TestPagination_QueryLimit(t *testing.T) { + config := PaginationConfig{ + DefaultLimit: 20, + MaxLimit: 100, + DefaultID: "default-id", + } + + p, err := NewPagination[testItem](PaginationParams{}, config) + require.NoError(t, err) + assert.Equal(t, int32(21), p.QueryLimit()) +} + +func TestPagination_Cursor(t *testing.T) { + config := PaginationConfig{ + DefaultLimit: 10, + MaxLimit: 100, + DefaultID: "default-id", + } + + timestamp := time.Date(2024, 1, 15, 10, 30, 45, 123456789, time.UTC) + id := "test-id-123" + token := generateCursor(timestamp, id) + + p, err := NewPagination[testItem]( + PaginationParams{NextToken: &token}, + config, + ) + require.NoError(t, err) + + cursor := p.cursor + assert.Equal(t, timestamp, cursor.Time) + assert.Equal(t, id, cursor.ID) +} + +func TestPagination_CursorTime(t *testing.T) { + config := PaginationConfig{ + DefaultLimit: 10, + MaxLimit: 100, + DefaultID: "default-id", + } + + timestamp := time.Date(2024, 1, 15, 10, 30, 45, 123456789, time.UTC) + id := "test-id-123" + token := generateCursor(timestamp, id) + + p, err := NewPagination[testItem]( + PaginationParams{NextToken: &token}, + config, + ) + require.NoError(t, err) + + assert.Equal(t, timestamp, p.CursorTime()) +} + +func TestPagination_CursorID(t *testing.T) { + config := PaginationConfig{ + DefaultLimit: 10, + MaxLimit: 100, + DefaultID: "default-id", + } + + timestamp := time.Date(2024, 1, 15, 10, 30, 45, 123456789, time.UTC) + id := "test-id-123" + token := generateCursor(timestamp, id) + + p, err := NewPagination[testItem]( + PaginationParams{NextToken: &token}, + config, + ) + require.NoError(t, err) + + assert.Equal(t, id, p.CursorID()) +} + +func TestPagination_SetNextToken(t *testing.T) { + config := PaginationConfig{ + DefaultLimit: 10, + MaxLimit: 100, + DefaultID: "default-id", + } + + p, err := NewPagination[testItem](PaginationParams{}, config) + require.NoError(t, err) + + timestamp := time.Date(2024, 1, 15, 10, 30, 45, 123456789, time.UTC) + id := "test-id-123" + p.setNextToken(timestamp, id) + + // Verify the token was set correctly by checking it can be parsed + require.NotNil(t, p.nextToken) + parsedTime, parsedID, err := ParseCursor(*p.nextToken) + require.NoError(t, err) + assert.Equal(t, timestamp, parsedTime) + assert.Equal(t, id, parsedID) +} + +func TestPagination_HasMore(t *testing.T) { + config := PaginationConfig{ + DefaultLimit: 10, + MaxLimit: 100, + DefaultID: "default-id", + } + + p, err := NewPagination[testItem](PaginationParams{}, config) + require.NoError(t, err) + + t.Run("no more results", func(t *testing.T) { + assert.False(t, p.hasMore(5)) + assert.False(t, p.hasMore(10)) + }) + + t.Run("has more results", func(t *testing.T) { + assert.True(t, p.hasMore(11)) + assert.True(t, p.hasMore(15)) + }) +} + +func TestPagination_TrimResults(t *testing.T) { + config := PaginationConfig{ + DefaultLimit: 5, + MaxLimit: 100, + DefaultID: "default-id", + } + + p, err := NewPagination[testItem](PaginationParams{}, config) + require.NoError(t, err) + + t.Run("no trimming needed", func(t *testing.T) { + results := []testItem{ + {ID: "1", Timestamp: time.Now()}, + {ID: "2", Timestamp: time.Now()}, + {ID: "3", Timestamp: time.Now()}, + } + trimmed := p.trimResults(results) + assert.Len(t, trimmed, 3) + assert.Equal(t, results, trimmed) + }) + + t.Run("trimming needed", func(t *testing.T) { + results := []testItem{ + {ID: "1", Timestamp: time.Now()}, + {ID: "2", Timestamp: time.Now()}, + {ID: "3", Timestamp: time.Now()}, + {ID: "4", Timestamp: time.Now()}, + {ID: "5", Timestamp: time.Now()}, + {ID: "6", Timestamp: time.Now()}, + {ID: "7", Timestamp: time.Now()}, + } + trimmed := p.trimResults(results) + assert.Len(t, trimmed, 5) + assert.Equal(t, results[:5], trimmed) + }) + + t.Run("exact limit", func(t *testing.T) { + results := []testItem{ + {ID: "1", Timestamp: time.Now()}, + {ID: "2", Timestamp: time.Now()}, + {ID: "3", Timestamp: time.Now()}, + {ID: "4", Timestamp: time.Now()}, + {ID: "5", Timestamp: time.Now()}, + } + trimmed := p.trimResults(results) + assert.Len(t, trimmed, 5) + assert.Equal(t, results, trimmed) + }) +} + +func TestPagination_ProcessResults(t *testing.T) { + config := PaginationConfig{ + DefaultLimit: 5, + MaxLimit: 100, + DefaultID: "default-id", + } + + getTimestampAndID := func(item testItem) (time.Time, string) { + return item.Timestamp, item.ID + } + + t.Run("no more results", func(t *testing.T) { + p, err := NewPagination[testItem](PaginationParams{}, config) + require.NoError(t, err) + + results := []testItem{ + {ID: "1", Timestamp: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)}, + {ID: "2", Timestamp: time.Date(2024, 1, 15, 11, 0, 0, 0, time.UTC)}, + {ID: "3", Timestamp: time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC)}, + } + + processed := p.processResults(results, getTimestampAndID) + assert.Len(t, processed, 3) + assert.Nil(t, p.nextToken) // No next token should be set + }) + + t.Run("has more results", func(t *testing.T) { + p, err := NewPagination[testItem](PaginationParams{}, config) + require.NoError(t, err) + + results := []testItem{ + {ID: "1", Timestamp: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)}, + {ID: "2", Timestamp: time.Date(2024, 1, 15, 11, 0, 0, 0, time.UTC)}, + {ID: "3", Timestamp: time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC)}, + {ID: "4", Timestamp: time.Date(2024, 1, 15, 13, 0, 0, 0, time.UTC)}, + {ID: "5", Timestamp: time.Date(2024, 1, 15, 14, 0, 0, 0, time.UTC)}, + {ID: "6", Timestamp: time.Date(2024, 1, 15, 15, 0, 0, 0, time.UTC)}, + } + + processed := p.processResults(results, getTimestampAndID) + assert.Len(t, processed, 5) + assert.NotNil(t, p.nextToken) // Next token should be set + + // Verify next token is based on the 5th item (index 4) + parsedTime, parsedID, err := ParseCursor(*p.nextToken) + require.NoError(t, err) + assert.Equal(t, results[4].Timestamp, parsedTime) + assert.Equal(t, results[4].ID, parsedID) + }) +} + +func TestPagination_ProcessResultsWithGin(t *testing.T) { + config := PaginationConfig{ + DefaultLimit: 5, + MaxLimit: 100, + DefaultID: "default-id", + } + + getTimestampAndID := func(item testItem) (time.Time, string) { + return item.Timestamp, item.ID + } + + gin.SetMode(gin.TestMode) + + t.Run("sets header when has more results", func(t *testing.T) { + p, err := NewPagination[testItem](PaginationParams{}, config) + require.NoError(t, err) + + results := []testItem{ + {ID: "1", Timestamp: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)}, + {ID: "2", Timestamp: time.Date(2024, 1, 15, 11, 0, 0, 0, time.UTC)}, + {ID: "3", Timestamp: time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC)}, + {ID: "4", Timestamp: time.Date(2024, 1, 15, 13, 0, 0, 0, time.UTC)}, + {ID: "5", Timestamp: time.Date(2024, 1, 15, 14, 0, 0, 0, time.UTC)}, + {ID: "6", Timestamp: time.Date(2024, 1, 15, 15, 0, 0, 0, time.UTC)}, + } + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + processed := p.ProcessResultsWithHeader(c, results, getTimestampAndID) + assert.Len(t, processed, 5) + assert.NotNil(t, p.nextToken) + + // Verify header was set + assert.Equal(t, *p.nextToken, c.Writer.Header().Get("X-Next-Token")) + }) + + t.Run("no header when no more results", func(t *testing.T) { + p, err := NewPagination[testItem](PaginationParams{}, config) + require.NoError(t, err) + + results := []testItem{ + {ID: "1", Timestamp: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)}, + {ID: "2", Timestamp: time.Date(2024, 1, 15, 11, 0, 0, 0, time.UTC)}, + {ID: "3", Timestamp: time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC)}, + } + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + processed := p.ProcessResultsWithHeader(c, results, getTimestampAndID) + assert.Len(t, processed, 3) + assert.Nil(t, p.nextToken) + + // Verify header was not set + assert.Empty(t, c.Writer.Header().Get("X-Next-Token")) + }) +} + +func TestPagination_SetHeader(t *testing.T) { + config := PaginationConfig{ + DefaultLimit: 10, + MaxLimit: 100, + DefaultID: "default-id", + } + + gin.SetMode(gin.TestMode) + + t.Run("sets header when next token exists", func(t *testing.T) { + p, err := NewPagination[testItem](PaginationParams{}, config) + require.NoError(t, err) + + timestamp := time.Date(2024, 1, 15, 10, 30, 45, 123456789, time.UTC) + id := "test-id-123" + p.setNextToken(timestamp, id) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + p.setHeader(c) + assert.Equal(t, *p.nextToken, c.Writer.Header().Get("X-Next-Token")) + }) + + t.Run("does not set header when next token is nil", func(t *testing.T) { + p, err := NewPagination[testItem](PaginationParams{}, config) + require.NoError(t, err) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + p.setHeader(c) + assert.Empty(t, c.Writer.Header().Get("X-Next-Token")) + }) +} + +func TestParseCursor(t *testing.T) { + t.Run("valid cursor", func(t *testing.T) { + timestamp := time.Date(2024, 1, 15, 10, 30, 45, 123456789, time.UTC) + id := "test-id-123" + cursor := generateCursor(timestamp, id) + + parsedTime, parsedID, err := ParseCursor(cursor) + require.NoError(t, err) + assert.Equal(t, timestamp, parsedTime) + assert.Equal(t, id, parsedID) + }) + + t.Run("invalid base64 encoding", func(t *testing.T) { + _, _, err := ParseCursor("invalid-base64!") + require.Error(t, err) + assert.Contains(t, err.Error(), "error decoding cursor") + }) + + t.Run("invalid format - missing separator", func(t *testing.T) { + invalid := base64.URLEncoding.EncodeToString([]byte("not-valid-format")) + _, _, err := ParseCursor(invalid) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid cursor format") + }) + + t.Run("invalid timestamp format", func(t *testing.T) { + invalid := base64.URLEncoding.EncodeToString([]byte("invalid-timestamp__test-id")) + _, _, err := ParseCursor(invalid) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid timestamp format") + }) +} diff --git a/packages/api/internal/utils/sandboxes_list.go b/packages/api/internal/utils/sandboxes_list.go index 5235f22c7e..9bbe1e8f43 100644 --- a/packages/api/internal/utils/sandboxes_list.go +++ b/packages/api/internal/utils/sandboxes_list.go @@ -13,21 +13,17 @@ import ( "github.com/e2b-dev/infra/packages/api/internal/api" ) -const maxSandboxID = "zzzzzzzzzzzzzzzzzzzz" +const ( + MaxSandboxID = "zzzzzzzzzzzzzzzzzzzz" +) -// extend the api.ListedSandbox with a timestamp to use for pagination +// PaginatedSandbox extends the api.ListedSandbox with a timestamp to use for pagination type PaginatedSandbox struct { api.ListedSandbox PaginationTimestamp time.Time `json:"-"` } -func (p *PaginatedSandbox) GenerateCursor() string { - cursor := fmt.Sprintf("%s__%s", p.PaginationTimestamp.Format(time.RFC3339Nano), p.SandboxID) - - return base64.URLEncoding.EncodeToString([]byte(cursor)) -} - func ParseNextToken(token *string) (time.Time, string, error) { if token != nil && *token != "" { cursorTime, cursorID, err := ParseCursor(*token) @@ -39,7 +35,7 @@ func ParseNextToken(token *string) (time.Time, string, error) { } // default to all sandboxes (older than now) and always lexically after any sandbox ID (the sort is descending) - return time.Now(), maxSandboxID, nil + return time.Now(), MaxSandboxID, nil } func ParseMetadata(metadata *string) (*map[string]string, error) { diff --git a/packages/db/queries/get_template_by_id.sql b/packages/db/queries/get_template_by_id.sql index ee3cd6bd25..7130592b3d 100644 --- a/packages/db/queries/get_template_by_id.sql +++ b/packages/db/queries/get_template_by_id.sql @@ -1,4 +1,24 @@ -- name: GetTemplateByID :one SELECT t.* FROM "public"."envs" t -WHERE t.id = $1; +WHERE t.id = $1 + AND NOT EXISTS ( + SELECT 1 + FROM public.snapshots AS s + WHERE s.env_id = t.id + ); + +-- name: GetTemplateByIDWithAliases :one +SELECT e.*, al.aliases +FROM "public"."envs" e +CROSS JOIN LATERAL ( + SELECT array_agg(alias)::text[] AS aliases + FROM public.env_aliases + WHERE env_id = e.id +) AS al +WHERE e.id = $1 + AND NOT EXISTS ( + SELECT 1 + FROM public.snapshots AS s + WHERE s.env_id = e.id + ); diff --git a/packages/db/queries/get_template_by_id.sql.go b/packages/db/queries/get_template_by_id.sql.go index bb511a24d3..fecbe0842b 100644 --- a/packages/db/queries/get_template_by_id.sql.go +++ b/packages/db/queries/get_template_by_id.sql.go @@ -7,12 +7,20 @@ package queries import ( "context" + "time" + + "github.com/google/uuid" ) const getTemplateByID = `-- name: GetTemplateByID :one SELECT t.id, t.created_at, t.updated_at, t.public, t.build_count, t.spawn_count, t.last_spawned_at, t.team_id, t.created_by, t.cluster_id FROM "public"."envs" t WHERE t.id = $1 + AND NOT EXISTS ( + SELECT 1 + FROM public.snapshots AS s + WHERE s.env_id = t.id + ) ` func (q *Queries) GetTemplateByID(ctx context.Context, id string) (Env, error) { @@ -32,3 +40,52 @@ func (q *Queries) GetTemplateByID(ctx context.Context, id string) (Env, error) { ) return i, err } + +const getTemplateByIDWithAliases = `-- name: GetTemplateByIDWithAliases :one +SELECT e.id, e.created_at, e.updated_at, e.public, e.build_count, e.spawn_count, e.last_spawned_at, e.team_id, e.created_by, e.cluster_id, al.aliases +FROM "public"."envs" e +CROSS JOIN LATERAL ( + SELECT array_agg(alias)::text[] AS aliases + FROM public.env_aliases + WHERE env_id = e.id +) AS al +WHERE e.id = $1 + AND NOT EXISTS ( + SELECT 1 + FROM public.snapshots AS s + WHERE s.env_id = e.id + ) +` + +type GetTemplateByIDWithAliasesRow struct { + ID string + CreatedAt time.Time + UpdatedAt time.Time + Public bool + BuildCount int32 + SpawnCount int64 + LastSpawnedAt *time.Time + TeamID uuid.UUID + CreatedBy *uuid.UUID + ClusterID *uuid.UUID + Aliases []string +} + +func (q *Queries) GetTemplateByIDWithAliases(ctx context.Context, id string) (GetTemplateByIDWithAliasesRow, error) { + row := q.db.QueryRow(ctx, getTemplateByIDWithAliases, id) + var i GetTemplateByIDWithAliasesRow + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Public, + &i.BuildCount, + &i.SpawnCount, + &i.LastSpawnedAt, + &i.TeamID, + &i.CreatedBy, + &i.ClusterID, + &i.Aliases, + ) + return i, err +} diff --git a/packages/db/queries/get_template_with_builds.sql b/packages/db/queries/get_template_with_builds.sql new file mode 100644 index 0000000000..5920d53d9d --- /dev/null +++ b/packages/db/queries/get_template_with_builds.sql @@ -0,0 +1,7 @@ +-- name: GetTemplateBuilds :many +SELECT eb.* +FROM public.env_builds eb +WHERE eb.env_id = sqlc.arg(template_id) + AND (eb.created_at, eb.id::text) < (@cursor_time, @cursor_id::text) +ORDER BY eb.created_at DESC, eb.id DESC +LIMIT @build_limit; diff --git a/packages/db/queries/get_template_with_builds.sql.go b/packages/db/queries/get_template_with_builds.sql.go new file mode 100644 index 0000000000..3c010878e6 --- /dev/null +++ b/packages/db/queries/get_template_with_builds.sql.go @@ -0,0 +1,72 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: get_template_with_builds.sql + +package queries + +import ( + "context" + "time" +) + +const getTemplateBuilds = `-- name: GetTemplateBuilds :many +SELECT eb.id, eb.created_at, eb.updated_at, eb.finished_at, eb.status, eb.dockerfile, eb.start_cmd, eb.vcpu, eb.ram_mb, eb.free_disk_size_mb, eb.total_disk_size_mb, eb.kernel_version, eb.firecracker_version, eb.env_id, eb.envd_version, eb.ready_cmd, eb.cluster_node_id, eb.reason, eb.version +FROM public.env_builds eb +WHERE eb.env_id = $1 + AND (eb.created_at, eb.id::text) < ($2, $3::text) +ORDER BY eb.created_at DESC, eb.id DESC +LIMIT $4 +` + +type GetTemplateBuildsParams struct { + TemplateID string + CursorTime time.Time + CursorID string + BuildLimit int32 +} + +func (q *Queries) GetTemplateBuilds(ctx context.Context, arg GetTemplateBuildsParams) ([]EnvBuild, error) { + rows, err := q.db.Query(ctx, getTemplateBuilds, + arg.TemplateID, + arg.CursorTime, + arg.CursorID, + arg.BuildLimit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []EnvBuild + for rows.Next() { + var i EnvBuild + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.FinishedAt, + &i.Status, + &i.Dockerfile, + &i.StartCmd, + &i.Vcpu, + &i.RamMb, + &i.FreeDiskSizeMb, + &i.TotalDiskSizeMb, + &i.KernelVersion, + &i.FirecrackerVersion, + &i.EnvID, + &i.EnvdVersion, + &i.ReadyCmd, + &i.ClusterNodeID, + &i.Reason, + &i.Version, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/packages/shared/pkg/utils/ptr.go b/packages/shared/pkg/utils/ptr.go index 2ec179d19e..ec2390834b 100644 --- a/packages/shared/pkg/utils/ptr.go +++ b/packages/shared/pkg/utils/ptr.go @@ -31,3 +31,13 @@ func DerefOrDefault[T any](s *T, defaultValue T) T { return *s } + +func CastPtr[S any, T any](s *S, castFunc func(S) T) *T { + if s == nil { + return nil + } + + t := castFunc(*s) + + return &t +} diff --git a/spec/openapi.yml b/spec/openapi.yml index 10a6870a3c..c966436a80 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -75,6 +75,24 @@ components: required: true schema: type: string + paginationLimit: + name: limit + in: query + description: Maximum number of items to return per page + required: false + schema: + type: integer + format: int32 + minimum: 1 + default: 100 + maximum: 100 + paginationNextToken: + name: nextToken + in: query + description: Cursor to start the list from + required: false + schema: + type: string responses: "400": @@ -690,6 +708,87 @@ components: envdVersion: $ref: "#/components/schemas/EnvdVersion" + TemplateBuild: + required: + - buildID + - status + - createdAt + - updatedAt + - cpuCount + - memoryMB + properties: + buildID: + type: string + format: uuid + description: Identifier of the build + status: + $ref: "#/components/schemas/TemplateBuildStatus" + createdAt: + type: string + format: date-time + description: Time when the build was created + updatedAt: + type: string + format: date-time + description: Time when the build was last updated + finishedAt: + type: string + format: date-time + description: Time when the build was finished + cpuCount: + $ref: "#/components/schemas/CPUCount" + memoryMB: + $ref: "#/components/schemas/MemoryMB" + diskSizeMB: + $ref: "#/components/schemas/DiskSizeMB" + envdVersion: + $ref: "#/components/schemas/EnvdVersion" + + TemplateWithBuilds: + required: + - templateID + - public + - aliases + - createdAt + - updatedAt + - lastSpawnedAt + - spawnCount + - builds + properties: + templateID: + type: string + description: Identifier of the template + public: + type: boolean + description: Whether the template is public or only accessible by the team + aliases: + type: array + description: Aliases of the template + items: + type: string + createdAt: + type: string + format: date-time + description: Time when the template was created + updatedAt: + type: string + format: date-time + description: Time when the template was last updated + lastSpawnedAt: + type: string + nullable: true + format: date-time + description: Time when the template was last used + spawnCount: + type: integer + format: int64 + description: Number of times the template was used + builds: + type: array + description: List of builds for the template + items: + $ref: "#/components/schemas/TemplateBuild" + TemplateBuildRequest: required: - dockerfile @@ -925,7 +1024,7 @@ components: - ready - error - TemplateBuild: + TemplateBuildInfo: required: - templateID - buildID @@ -1517,21 +1616,8 @@ paths: $ref: "#/components/schemas/SandboxState" style: form explode: false - - name: nextToken - in: query - description: Cursor to start the list from - required: false - schema: - type: string - - name: limit - in: query - description: Maximum number of items to return per page - required: false - schema: - type: integer - format: int32 - minimum: 1 - default: 100 + - $ref: "#/components/parameters/paginationNextToken" + - $ref: "#/components/parameters/paginationLimit" responses: "200": description: Successfully returned all running sandboxes @@ -2012,6 +2098,28 @@ paths: $ref: "#/components/responses/500" /templates/{templateID}: + get: + description: List all builds for a template + tags: [templates] + security: + - ApiKeyAuth: [] + - Supabase1TokenAuth: [] + Supabase2TeamAuth: [] + parameters: + - $ref: "#/components/parameters/templateID" + - $ref: "#/components/parameters/paginationNextToken" + - $ref: "#/components/parameters/paginationLimit" + responses: + "200": + description: Successfully returned the template with its builds + content: + application/json: + schema: + $ref: "#/components/schemas/TemplateWithBuilds" + "401": + $ref: "#/components/responses/401" + "500": + $ref: "#/components/responses/500" post: description: Rebuild an template deprecated: true @@ -2152,7 +2260,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/TemplateBuild" + $ref: "#/components/schemas/TemplateBuildInfo" "401": $ref: "#/components/responses/401" "404": diff --git a/tests/integration/internal/api/client.gen.go b/tests/integration/internal/api/client.gen.go index a7968d3a5c..59ece6ba76 100644 --- a/tests/integration/internal/api/client.gen.go +++ b/tests/integration/internal/api/client.gen.go @@ -193,6 +193,9 @@ type ClientInterface interface { // DeleteTemplatesTemplateID request DeleteTemplatesTemplateID(ctx context.Context, templateID TemplateID, reqEditors ...RequestEditorFn) (*http.Response, error) + // GetTemplatesTemplateID request + GetTemplatesTemplateID(ctx context.Context, templateID TemplateID, params *GetTemplatesTemplateIDParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // PatchTemplatesTemplateIDWithBody request with any body PatchTemplatesTemplateIDWithBody(ctx context.Context, templateID TemplateID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -687,6 +690,18 @@ func (c *Client) DeleteTemplatesTemplateID(ctx context.Context, templateID Templ return c.Client.Do(req) } +func (c *Client) GetTemplatesTemplateID(ctx context.Context, templateID TemplateID, params *GetTemplatesTemplateIDParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetTemplatesTemplateIDRequest(c.Server, templateID, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) PatchTemplatesTemplateIDWithBody(ctx context.Context, templateID TemplateID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewPatchTemplatesTemplateIDRequestWithBody(c.Server, templateID, contentType, body) if err != nil { @@ -2108,6 +2123,78 @@ func NewDeleteTemplatesTemplateIDRequest(server string, templateID TemplateID) ( return req, nil } +// NewGetTemplatesTemplateIDRequest generates requests for GetTemplatesTemplateID +func NewGetTemplatesTemplateIDRequest(server string, templateID TemplateID, params *GetTemplatesTemplateIDParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "templateID", runtime.ParamLocationPath, templateID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/templates/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.NextToken != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "nextToken", runtime.ParamLocationQuery, *params.NextToken); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Limit != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "limit", runtime.ParamLocationQuery, *params.Limit); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewPatchTemplatesTemplateIDRequest calls the generic PatchTemplatesTemplateID builder with application/json body func NewPatchTemplatesTemplateIDRequest(server string, templateID TemplateID, body PatchTemplatesTemplateIDJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -2741,6 +2828,9 @@ type ClientWithResponsesInterface interface { // DeleteTemplatesTemplateIDWithResponse request DeleteTemplatesTemplateIDWithResponse(ctx context.Context, templateID TemplateID, reqEditors ...RequestEditorFn) (*DeleteTemplatesTemplateIDResponse, error) + // GetTemplatesTemplateIDWithResponse request + GetTemplatesTemplateIDWithResponse(ctx context.Context, templateID TemplateID, params *GetTemplatesTemplateIDParams, reqEditors ...RequestEditorFn) (*GetTemplatesTemplateIDResponse, error) + // PatchTemplatesTemplateIDWithBodyWithResponse request with any body PatchTemplatesTemplateIDWithBodyWithResponse(ctx context.Context, templateID TemplateID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PatchTemplatesTemplateIDResponse, error) @@ -3466,6 +3556,30 @@ func (r DeleteTemplatesTemplateIDResponse) StatusCode() int { return 0 } +type GetTemplatesTemplateIDResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *TemplateWithBuilds + JSON401 *N401 + JSON500 *N500 +} + +// Status returns HTTPResponse.Status +func (r GetTemplatesTemplateIDResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetTemplatesTemplateIDResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type PatchTemplatesTemplateIDResponse struct { Body []byte HTTPResponse *http.Response @@ -3540,7 +3654,7 @@ func (r PostTemplatesTemplateIDBuildsBuildIDResponse) StatusCode() int { type GetTemplatesTemplateIDBuildsBuildIDStatusResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *TemplateBuild + JSON200 *TemplateBuildInfo JSON401 *N401 JSON404 *N404 JSON500 *N500 @@ -4018,6 +4132,15 @@ func (c *ClientWithResponses) DeleteTemplatesTemplateIDWithResponse(ctx context. return ParseDeleteTemplatesTemplateIDResponse(rsp) } +// GetTemplatesTemplateIDWithResponse request returning *GetTemplatesTemplateIDResponse +func (c *ClientWithResponses) GetTemplatesTemplateIDWithResponse(ctx context.Context, templateID TemplateID, params *GetTemplatesTemplateIDParams, reqEditors ...RequestEditorFn) (*GetTemplatesTemplateIDResponse, error) { + rsp, err := c.GetTemplatesTemplateID(ctx, templateID, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetTemplatesTemplateIDResponse(rsp) +} + // PatchTemplatesTemplateIDWithBodyWithResponse request with arbitrary body returning *PatchTemplatesTemplateIDResponse func (c *ClientWithResponses) PatchTemplatesTemplateIDWithBodyWithResponse(ctx context.Context, templateID TemplateID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PatchTemplatesTemplateIDResponse, error) { rsp, err := c.PatchTemplatesTemplateIDWithBody(ctx, templateID, contentType, body, reqEditors...) @@ -5364,6 +5487,46 @@ func ParseDeleteTemplatesTemplateIDResponse(rsp *http.Response) (*DeleteTemplate return response, nil } +// ParseGetTemplatesTemplateIDResponse parses an HTTP response from a GetTemplatesTemplateIDWithResponse call +func ParseGetTemplatesTemplateIDResponse(rsp *http.Response) (*GetTemplatesTemplateIDResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetTemplatesTemplateIDResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest TemplateWithBuilds + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest N401 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest N500 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParsePatchTemplatesTemplateIDResponse parses an HTTP response from a PatchTemplatesTemplateIDWithResponse call func ParsePatchTemplatesTemplateIDResponse(rsp *http.Response) (*PatchTemplatesTemplateIDResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -5492,7 +5655,7 @@ func ParseGetTemplatesTemplateIDBuildsBuildIDStatusResponse(rsp *http.Response) switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest TemplateBuild + var dest TemplateBuildInfo if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } diff --git a/tests/integration/internal/api/models.gen.go b/tests/integration/internal/api/models.gen.go index 043f87eead..ed7549cbcb 100644 --- a/tests/integration/internal/api/models.gen.go +++ b/tests/integration/internal/api/models.gen.go @@ -711,20 +711,31 @@ type Template struct { // TemplateBuild defines model for TemplateBuild. type TemplateBuild struct { // BuildID Identifier of the build - BuildID string `json:"buildID"` + BuildID openapi_types.UUID `json:"buildID"` - // LogEntries Build logs structured - LogEntries []BuildLogEntry `json:"logEntries"` + // CpuCount CPU cores for the sandbox + CpuCount CPUCount `json:"cpuCount"` - // Logs Build logs - Logs []string `json:"logs"` - Reason *BuildStatusReason `json:"reason,omitempty"` + // CreatedAt Time when the build was created + CreatedAt time.Time `json:"createdAt"` + + // DiskSizeMB Disk size for the sandbox in MiB + DiskSizeMB *DiskSizeMB `json:"diskSizeMB,omitempty"` + + // EnvdVersion Version of the envd running in the sandbox + EnvdVersion *EnvdVersion `json:"envdVersion,omitempty"` + + // FinishedAt Time when the build was finished + FinishedAt *time.Time `json:"finishedAt,omitempty"` + + // MemoryMB Memory for the sandbox in MiB + MemoryMB MemoryMB `json:"memoryMB"` // Status Status of the template build Status TemplateBuildStatus `json:"status"` - // TemplateID Identifier of the template - TemplateID string `json:"templateID"` + // UpdatedAt Time when the build was last updated + UpdatedAt time.Time `json:"updatedAt"` } // TemplateBuildFileUpload defines model for TemplateBuildFileUpload. @@ -736,6 +747,25 @@ type TemplateBuildFileUpload struct { Url *string `json:"url,omitempty"` } +// TemplateBuildInfo defines model for TemplateBuildInfo. +type TemplateBuildInfo struct { + // BuildID Identifier of the build + BuildID string `json:"buildID"` + + // LogEntries Build logs structured + LogEntries []BuildLogEntry `json:"logEntries"` + + // Logs Build logs + Logs []string `json:"logs"` + Reason *BuildStatusReason `json:"reason,omitempty"` + + // Status Status of the template build + Status TemplateBuildStatus `json:"status"` + + // TemplateID Identifier of the template + TemplateID string `json:"templateID"` +} + // TemplateBuildRequest defines model for TemplateBuildRequest. type TemplateBuildRequest struct { // Alias Alias of the template @@ -894,6 +924,33 @@ type TemplateUpdateRequest struct { Public *bool `json:"public,omitempty"` } +// TemplateWithBuilds defines model for TemplateWithBuilds. +type TemplateWithBuilds struct { + // Aliases Aliases of the template + Aliases []string `json:"aliases"` + + // Builds List of builds for the template + Builds []TemplateBuild `json:"builds"` + + // CreatedAt Time when the template was created + CreatedAt time.Time `json:"createdAt"` + + // LastSpawnedAt Time when the template was last used + LastSpawnedAt *time.Time `json:"lastSpawnedAt"` + + // Public Whether the template is public or only accessible by the team + Public bool `json:"public"` + + // SpawnCount Number of times the template was used + SpawnCount int64 `json:"spawnCount"` + + // TemplateID Identifier of the template + TemplateID string `json:"templateID"` + + // UpdatedAt Time when the template was last updated + UpdatedAt time.Time `json:"updatedAt"` +} + // UpdateTeamAPIKey defines model for UpdateTeamAPIKey. type UpdateTeamAPIKey struct { // Name New name for the API key @@ -912,6 +969,12 @@ type BuildID = string // NodeID defines model for nodeID. type NodeID = string +// PaginationLimit defines model for paginationLimit. +type PaginationLimit = int32 + +// PaginationNextToken defines model for paginationNextToken. +type PaginationNextToken = string + // SandboxID defines model for sandboxID. type SandboxID = string @@ -1010,6 +1073,15 @@ type GetTemplatesParams struct { TeamID *string `form:"teamID,omitempty" json:"teamID,omitempty"` } +// GetTemplatesTemplateIDParams defines parameters for GetTemplatesTemplateID. +type GetTemplatesTemplateIDParams struct { + // NextToken Cursor to start the list from + NextToken *PaginationNextToken `form:"nextToken,omitempty" json:"nextToken,omitempty"` + + // Limit Maximum number of items to return per page + Limit *PaginationLimit `form:"limit,omitempty" json:"limit,omitempty"` +} + // GetTemplatesTemplateIDBuildsBuildIDStatusParams defines parameters for GetTemplatesTemplateIDBuildsBuildIDStatus. type GetTemplatesTemplateIDBuildsBuildIDStatusParams struct { // LogsOffset Index of the starting build log that should be returned with the template @@ -1026,10 +1098,10 @@ type GetV2SandboxesParams struct { State *[]SandboxState `form:"state,omitempty" json:"state,omitempty"` // NextToken Cursor to start the list from - NextToken *string `form:"nextToken,omitempty" json:"nextToken,omitempty"` + NextToken *PaginationNextToken `form:"nextToken,omitempty" json:"nextToken,omitempty"` // Limit Maximum number of items to return per page - Limit *int32 `form:"limit,omitempty" json:"limit,omitempty"` + Limit *PaginationLimit `form:"limit,omitempty" json:"limit,omitempty"` } // PostAccessTokensJSONRequestBody defines body for PostAccessTokens for application/json ContentType.