From ed011a8fed04be64959164c50f128e7fba54b1e8 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:08:23 +0200 Subject: [PATCH 1/7] feat(api): filter snapshots by name Add an optional `name` query parameter to `GET /snapshots`, mirroring sandbox metadata filtering. The name is parsed (namespace/tag stripped) and matched against the snapshot's alias, scoped to the requesting team. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/api/internal/api/api.gen.go | 85 ++++++++++++------- .../handlers/snapshot_template_list.go | 24 +++++- .../list_team_snapshot_templates.sql.go | 15 +++- .../list_team_snapshot_templates.sql | 9 +- spec/openapi.yml | 6 ++ 5 files changed, 103 insertions(+), 36 deletions(-) diff --git a/packages/api/internal/api/api.gen.go b/packages/api/internal/api/api.gen.go index aec0f0cc9b..afb74722ba 100644 --- a/packages/api/internal/api/api.gen.go +++ b/packages/api/internal/api/api.gen.go @@ -1568,6 +1568,9 @@ type GetSandboxesSandboxIDMetricsParams struct { type GetSnapshotsParams struct { SandboxID *string `form:"sandboxID,omitempty" json:"sandboxID,omitempty"` + // Name Filter snapshots by name (e.g. "my-snapshot" or "my-team/my-snapshot"). + Name *string `form:"name,omitempty" json:"name,omitempty"` + // Limit Maximum number of items to return per page Limit *PaginationLimit `form:"limit,omitempty" json:"limit,omitempty"` @@ -4414,6 +4417,18 @@ func NewGetSnapshotsRequest(server string, params *GetSnapshotsParams) (*http.Re } + if params.Name != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "name", *params.Name, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + } + if params.Limit != nil { if queryFrag, err := runtime.StyleParamWithOptions("form", true, "limit", *params.Limit, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: "int32"}); err != nil { @@ -12474,6 +12489,14 @@ func (siw *ServerInterfaceWrapper) GetSnapshots(c *gin.Context) { return } + // ------------- Optional query parameter "name" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "name", c.Request.URL.Query(), ¶ms.Name, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandler(c, fmt.Errorf("Invalid format for parameter name: %w", err), http.StatusBadRequest) + return + } + // ------------- Optional query parameter "limit" ------------- err = runtime.BindQueryParameterWithOptions("form", true, false, "limit", c.Request.URL.Query(), ¶ms.Limit, runtime.BindQueryParameterOptions{Type: "integer", Format: "int32"}) @@ -13900,37 +13923,37 @@ var swaggerSpec = []string{ "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", - "rv9/AAAA//8=", + "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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/packages/api/internal/handlers/snapshot_template_list.go b/packages/api/internal/handlers/snapshot_template_list.go index 904fa735cc..2e3873bfbc 100644 --- a/packages/api/internal/handlers/snapshot_template_list.go +++ b/packages/api/internal/handlers/snapshot_template_list.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "net/http" "time" @@ -25,7 +26,8 @@ const ( func (a *APIStore) GetSnapshots(c *gin.Context, params api.GetSnapshotsParams) { ctx := c.Request.Context() - teamID := auth.MustGetTeamID(c) + teamInfo := auth.MustGetTeamInfo(c) + teamID := teamInfo.Team.ID span := trace.SpanFromContext(ctx) traceID := span.SpanContext().TraceID().String() @@ -64,9 +66,29 @@ func (a *APIStore) GetSnapshots(c *gin.Context, params api.GetSnapshotsParams) { sandboxIDFilter = &short } + var aliasFilter *string + if params.Name != nil { + identifier, _, err := id.ParseName(*params.Name) + if err != nil { + a.sendAPIStoreError(c, http.StatusBadRequest, fmt.Sprintf("Invalid name: %s", err)) + + return + } + + if err := id.ValidateNamespaceMatchesTeam(identifier, teamInfo.Slug); err != nil { + a.sendAPIStoreError(c, http.StatusBadRequest, err.Error()) + + return + } + + alias := id.ExtractAlias(identifier) + aliasFilter = &alias + } + snapshots, err := a.sqlcDB.ListTeamSnapshotTemplates(ctx, queries.ListTeamSnapshotTemplatesParams{ TeamID: teamID, SandboxID: sandboxIDFilter, + Alias: aliasFilter, CursorTime: pagination.CursorTime(), CursorID: pagination.CursorID(), PageLimit: pagination.QueryLimit(), diff --git a/packages/db/queries/list_team_snapshot_templates.sql.go b/packages/db/queries/list_team_snapshot_templates.sql.go index b3e9936238..3b618824fc 100644 --- a/packages/db/queries/list_team_snapshot_templates.sql.go +++ b/packages/db/queries/list_team_snapshot_templates.sql.go @@ -47,17 +47,25 @@ LEFT JOIN LATERAL ( WHERE e.team_id = $1 AND e.source = 'snapshot_template' AND ( - $2::text IS NULL + $2::text IS NULL OR st.sandbox_id = $2::text ) -AND (e.created_at, e.id) < ($3, $4::text) +AND ( + $3::text IS NULL + OR EXISTS ( + SELECT 1 FROM "public"."env_aliases" af + WHERE af.env_id = e.id AND af.alias = $3::text + ) +) +AND (e.created_at, e.id) < ($4, $5::text) ORDER BY e.created_at DESC, e.id DESC -LIMIT $5 +LIMIT $6 ` type ListTeamSnapshotTemplatesParams struct { TeamID uuid.UUID SandboxID *string + Alias *string CursorTime time.Time CursorID string PageLimit int32 @@ -86,6 +94,7 @@ func (q *Queries) ListTeamSnapshotTemplates(ctx context.Context, arg ListTeamSna rows, err := q.db.Query(ctx, listTeamSnapshotTemplates, arg.TeamID, arg.SandboxID, + arg.Alias, arg.CursorTime, arg.CursorID, arg.PageLimit, diff --git a/packages/db/queries/snapshots/list_team_snapshot_templates.sql b/packages/db/queries/snapshots/list_team_snapshot_templates.sql index b985886b25..9e16ef0696 100644 --- a/packages/db/queries/snapshots/list_team_snapshot_templates.sql +++ b/packages/db/queries/snapshots/list_team_snapshot_templates.sql @@ -35,9 +35,16 @@ LEFT JOIN LATERAL ( WHERE e.team_id = @team_id AND e.source = 'snapshot_template' AND ( - sqlc.narg(sandbox_id)::text IS NULL + sqlc.narg(sandbox_id)::text IS NULL OR st.sandbox_id = sqlc.narg(sandbox_id)::text ) +AND ( + sqlc.narg(alias)::text IS NULL + OR EXISTS ( + SELECT 1 FROM "public"."env_aliases" af + WHERE af.env_id = e.id AND af.alias = sqlc.narg(alias)::text + ) +) AND (e.created_at, e.id) < (@cursor_time, @cursor_id::text) ORDER BY e.created_at DESC, e.id DESC LIMIT @page_limit; diff --git a/spec/openapi.yml b/spec/openapi.yml index 07f527506a..87ec43d159 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -2711,6 +2711,12 @@ paths: schema: type: string description: Filter snapshots by source sandbox ID + - name: name + in: query + description: Filter snapshots by name (e.g. "my-snapshot" or "my-team/my-snapshot"). + required: false + schema: + type: string - $ref: "#/components/parameters/paginationLimit" - $ref: "#/components/parameters/paginationNextToken" responses: From 3883eabcdf9ebbbcb14190175678d24df68c1166 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 2 Jul 2026 13:27:05 +0000 Subject: [PATCH 2/7] chore: auto-commit generated changes --- tests/integration/internal/api/generated.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/integration/internal/api/generated.go b/tests/integration/internal/api/generated.go index 2817438a3a..d6dcda6a1c 100644 --- a/tests/integration/internal/api/generated.go +++ b/tests/integration/internal/api/generated.go @@ -1563,6 +1563,9 @@ type GetSandboxesSandboxIDMetricsParams struct { type GetSnapshotsParams struct { SandboxID *string `form:"sandboxID,omitempty" json:"sandboxID,omitempty"` + // Name Filter snapshots by name (e.g. "my-snapshot" or "my-team/my-snapshot"). + Name *string `form:"name,omitempty" json:"name,omitempty"` + // Limit Maximum number of items to return per page Limit *PaginationLimit `form:"limit,omitempty" json:"limit,omitempty"` @@ -4409,6 +4412,18 @@ func NewGetSnapshotsRequest(server string, params *GetSnapshotsParams) (*http.Re } + if params.Name != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "name", *params.Name, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + } + if params.Limit != nil { if queryFrag, err := runtime.StyleParamWithOptions("form", true, "limit", *params.Limit, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: "int32"}); err != nil { From 53dfda9b08f604a5b0b32a3778bfd7445dd33032 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:31:09 +0200 Subject: [PATCH 3/7] fix(api): scope snapshot name filter to team namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bugbot: the name filter matched env_aliases.alias regardless of namespace. Mirror AliasCache.Resolve instead — a bare alias matches the team namespace and falls back to promoted (NULL) aliases, while an explicit "team/alias" matches only the team namespace. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../handlers/snapshot_template_list.go | 22 +++++++++----- .../list_team_snapshot_templates.sql.go | 30 +++++++++++++------ .../list_team_snapshot_templates.sql | 10 ++++++- 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/packages/api/internal/handlers/snapshot_template_list.go b/packages/api/internal/handlers/snapshot_template_list.go index 2e3873bfbc..80298ff5c3 100644 --- a/packages/api/internal/handlers/snapshot_template_list.go +++ b/packages/api/internal/handlers/snapshot_template_list.go @@ -67,6 +67,11 @@ func (a *APIStore) GetSnapshots(c *gin.Context, params api.GetSnapshotsParams) { } var aliasFilter *string + // Namespace matching mirrors alias resolution (see AliasCache.Resolve): a bare + // alias matches the team namespace and falls back to promoted (NULL) aliases, + // while an explicit "team/alias" matches only the team namespace. + aliasNamespace := teamInfo.Slug + matchNullNamespace := false if params.Name != nil { identifier, _, err := id.ParseName(*params.Name) if err != nil { @@ -81,17 +86,20 @@ func (a *APIStore) GetSnapshots(c *gin.Context, params api.GetSnapshotsParams) { return } - alias := id.ExtractAlias(identifier) + namespace, alias := id.SplitIdentifier(identifier) aliasFilter = &alias + matchNullNamespace = namespace == nil } snapshots, err := a.sqlcDB.ListTeamSnapshotTemplates(ctx, queries.ListTeamSnapshotTemplatesParams{ - TeamID: teamID, - SandboxID: sandboxIDFilter, - Alias: aliasFilter, - CursorTime: pagination.CursorTime(), - CursorID: pagination.CursorID(), - PageLimit: pagination.QueryLimit(), + TeamID: teamID, + SandboxID: sandboxIDFilter, + Alias: aliasFilter, + AliasNamespace: aliasNamespace, + MatchNullNamespace: matchNullNamespace, + CursorTime: pagination.CursorTime(), + CursorID: pagination.CursorID(), + PageLimit: pagination.QueryLimit(), }) if err != nil { telemetry.ReportCriticalError(ctx, "Error listing snapshot templates", err, telemetry.WithTeamID(teamID.String())) diff --git a/packages/db/queries/list_team_snapshot_templates.sql.go b/packages/db/queries/list_team_snapshot_templates.sql.go index 3b618824fc..9579672785 100644 --- a/packages/db/queries/list_team_snapshot_templates.sql.go +++ b/packages/db/queries/list_team_snapshot_templates.sql.go @@ -53,22 +53,32 @@ AND ( AND ( $3::text IS NULL OR EXISTS ( + -- Mirror alias resolution: match the team namespace, and for bare aliases + -- also match promoted (NULL namespace) aliases. Explicit "team/alias" only + -- matches the team namespace. SELECT 1 FROM "public"."env_aliases" af - WHERE af.env_id = e.id AND af.alias = $3::text + WHERE af.env_id = e.id + AND af.alias = $3::text + AND ( + af.namespace = $4::text + OR ($5::bool AND af.namespace IS NULL) + ) ) ) -AND (e.created_at, e.id) < ($4, $5::text) +AND (e.created_at, e.id) < ($6, $7::text) ORDER BY e.created_at DESC, e.id DESC -LIMIT $6 +LIMIT $8 ` type ListTeamSnapshotTemplatesParams struct { - TeamID uuid.UUID - SandboxID *string - Alias *string - CursorTime time.Time - CursorID string - PageLimit int32 + TeamID uuid.UUID + SandboxID *string + Alias *string + AliasNamespace string + MatchNullNamespace bool + CursorTime time.Time + CursorID string + PageLimit int32 } type ListTeamSnapshotTemplatesRow struct { @@ -95,6 +105,8 @@ func (q *Queries) ListTeamSnapshotTemplates(ctx context.Context, arg ListTeamSna arg.TeamID, arg.SandboxID, arg.Alias, + arg.AliasNamespace, + arg.MatchNullNamespace, arg.CursorTime, arg.CursorID, arg.PageLimit, diff --git a/packages/db/queries/snapshots/list_team_snapshot_templates.sql b/packages/db/queries/snapshots/list_team_snapshot_templates.sql index 9e16ef0696..8d5f69dfb0 100644 --- a/packages/db/queries/snapshots/list_team_snapshot_templates.sql +++ b/packages/db/queries/snapshots/list_team_snapshot_templates.sql @@ -41,8 +41,16 @@ AND ( AND ( sqlc.narg(alias)::text IS NULL OR EXISTS ( + -- Mirror alias resolution: match the team namespace, and for bare aliases + -- also match promoted (NULL namespace) aliases. Explicit "team/alias" only + -- matches the team namespace. SELECT 1 FROM "public"."env_aliases" af - WHERE af.env_id = e.id AND af.alias = sqlc.narg(alias)::text + WHERE af.env_id = e.id + AND af.alias = sqlc.narg(alias)::text + AND ( + af.namespace = @alias_namespace::text + OR (@match_null_namespace::bool AND af.namespace IS NULL) + ) ) ) AND (e.created_at, e.id) < (@cursor_time, @cursor_id::text) From b6dcca247e6168fadf92afecce15345ba8e2c3e1 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:54:02 +0200 Subject: [PATCH 4/7] fix(api): resolve snapshot name filter via alias cache and honor tag Resolve the name to a template ID with templateCache.ResolveAlias (the same path every other name-accepting endpoint uses) instead of mirroring namespace-resolution rules in SQL. This makes ?name= work for unnamed snapshots, removes the dead NULL-namespace branch, and filters by the previously discarded tag component. Co-Authored-By: Claude Fable 5 --- packages/api/internal/api/api.gen.go | 379 +++++++++--------- .../handlers/snapshot_template_list.go | 49 ++- .../list_team_snapshot_templates.sql.go | 44 +- .../list_team_snapshot_templates.sql | 22 +- spec/openapi.yml | 2 +- tests/integration/internal/api/generated.go | 2 +- 6 files changed, 255 insertions(+), 243 deletions(-) diff --git a/packages/api/internal/api/api.gen.go b/packages/api/internal/api/api.gen.go index afb74722ba..4fec097373 100644 --- a/packages/api/internal/api/api.gen.go +++ b/packages/api/internal/api/api.gen.go @@ -1568,7 +1568,7 @@ type GetSandboxesSandboxIDMetricsParams struct { type GetSnapshotsParams struct { SandboxID *string `form:"sandboxID,omitempty" json:"sandboxID,omitempty"` - // Name Filter snapshots by name (e.g. "my-snapshot" or "my-team/my-snapshot"). + // Name Filter snapshots by name or ID, optionally tag-qualified (e.g. "my-snapshot", "my-team/my-snapshot" or "my-snapshot:v1"). Name *string `form:"name,omitempty" json:"name,omitempty"` // Limit Maximum number of items to return per page @@ -13765,195 +13765,196 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "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", + "AwAA//8=", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/packages/api/internal/handlers/snapshot_template_list.go b/packages/api/internal/handlers/snapshot_template_list.go index 80298ff5c3..d1a3c52951 100644 --- a/packages/api/internal/handlers/snapshot_template_list.go +++ b/packages/api/internal/handlers/snapshot_template_list.go @@ -1,6 +1,7 @@ package handlers import ( + "errors" "fmt" "net/http" "time" @@ -9,6 +10,7 @@ import ( "go.opentelemetry.io/otel/trace" "github.com/e2b-dev/infra/packages/api/internal/api" + templatecache "github.com/e2b-dev/infra/packages/api/internal/cache/templates" "github.com/e2b-dev/infra/packages/api/internal/utils" "github.com/e2b-dev/infra/packages/auth/pkg/auth" "github.com/e2b-dev/infra/packages/db/queries" @@ -66,14 +68,9 @@ func (a *APIStore) GetSnapshots(c *gin.Context, params api.GetSnapshotsParams) { sandboxIDFilter = &short } - var aliasFilter *string - // Namespace matching mirrors alias resolution (see AliasCache.Resolve): a bare - // alias matches the team namespace and falls back to promoted (NULL) aliases, - // while an explicit "team/alias" matches only the team namespace. - aliasNamespace := teamInfo.Slug - matchNullNamespace := false + var envIDFilter, tagFilter *string if params.Name != nil { - identifier, _, err := id.ParseName(*params.Name) + identifier, tag, err := id.ParseName(*params.Name) if err != nil { a.sendAPIStoreError(c, http.StatusBadRequest, fmt.Sprintf("Invalid name: %s", err)) @@ -86,20 +83,36 @@ func (a *APIStore) GetSnapshots(c *gin.Context, params api.GetSnapshotsParams) { return } - namespace, alias := id.SplitIdentifier(identifier) - aliasFilter = &alias - matchNullNamespace = namespace == nil + tagFilter = tag + + // Resolve alias using the cache — same pattern as template builds. Team + // ownership is enforced by the query's team_id predicate, so a name that + // resolves to another team's template yields an empty list. + aliasInfo, err := a.templateCache.ResolveAlias(ctx, identifier, teamInfo.Slug) + switch { + case err == nil: + envIDFilter = &aliasInfo.TemplateID + case errors.Is(err, templatecache.ErrTemplateNotFound): + c.JSON(http.StatusOK, []api.SnapshotInfo{}) + + return + default: + apiErr := templatecache.ErrorToAPIError(err, identifier) + a.sendAPIStoreError(c, apiErr.Code, apiErr.ClientMsg) + telemetry.ReportCriticalError(ctx, "error resolving snapshot template alias", apiErr.Err) + + return + } } snapshots, err := a.sqlcDB.ListTeamSnapshotTemplates(ctx, queries.ListTeamSnapshotTemplatesParams{ - TeamID: teamID, - SandboxID: sandboxIDFilter, - Alias: aliasFilter, - AliasNamespace: aliasNamespace, - MatchNullNamespace: matchNullNamespace, - CursorTime: pagination.CursorTime(), - CursorID: pagination.CursorID(), - PageLimit: pagination.QueryLimit(), + TeamID: teamID, + SandboxID: sandboxIDFilter, + EnvID: envIDFilter, + Tag: tagFilter, + CursorTime: pagination.CursorTime(), + CursorID: pagination.CursorID(), + PageLimit: pagination.QueryLimit(), }) if err != nil { telemetry.ReportCriticalError(ctx, "Error listing snapshot templates", err, telemetry.WithTeamID(teamID.String())) diff --git a/packages/db/queries/list_team_snapshot_templates.sql.go b/packages/db/queries/list_team_snapshot_templates.sql.go index 9579672785..0331360702 100644 --- a/packages/db/queries/list_team_snapshot_templates.sql.go +++ b/packages/db/queries/list_team_snapshot_templates.sql.go @@ -52,33 +52,32 @@ AND ( ) AND ( $3::text IS NULL + OR e.id = $3::text +) +AND ( + $4::text IS NULL OR EXISTS ( - -- Mirror alias resolution: match the team namespace, and for bare aliases - -- also match promoted (NULL namespace) aliases. Explicit "team/alias" only - -- matches the team namespace. - SELECT 1 FROM "public"."env_aliases" af - WHERE af.env_id = e.id - AND af.alias = $3::text - AND ( - af.namespace = $4::text - OR ($5::bool AND af.namespace IS NULL) - ) + SELECT 1 + FROM "public"."env_build_assignments" tba + JOIN "public"."env_builds" tb ON tb.id = tba.build_id + WHERE tba.env_id = e.id + AND tba.tag = $4::text + AND tb.status IN ('success', 'uploaded', 'ready') ) ) -AND (e.created_at, e.id) < ($6, $7::text) +AND (e.created_at, e.id) < ($5, $6::text) ORDER BY e.created_at DESC, e.id DESC -LIMIT $8 +LIMIT $7 ` type ListTeamSnapshotTemplatesParams struct { - TeamID uuid.UUID - SandboxID *string - Alias *string - AliasNamespace string - MatchNullNamespace bool - CursorTime time.Time - CursorID string - PageLimit int32 + TeamID uuid.UUID + SandboxID *string + EnvID *string + Tag *string + CursorTime time.Time + CursorID string + PageLimit int32 } type ListTeamSnapshotTemplatesRow struct { @@ -104,9 +103,8 @@ func (q *Queries) ListTeamSnapshotTemplates(ctx context.Context, arg ListTeamSna rows, err := q.db.Query(ctx, listTeamSnapshotTemplates, arg.TeamID, arg.SandboxID, - arg.Alias, - arg.AliasNamespace, - arg.MatchNullNamespace, + arg.EnvID, + arg.Tag, arg.CursorTime, arg.CursorID, arg.PageLimit, diff --git a/packages/db/queries/snapshots/list_team_snapshot_templates.sql b/packages/db/queries/snapshots/list_team_snapshot_templates.sql index 8d5f69dfb0..6dcf97a47f 100644 --- a/packages/db/queries/snapshots/list_team_snapshot_templates.sql +++ b/packages/db/queries/snapshots/list_team_snapshot_templates.sql @@ -39,18 +39,18 @@ AND ( OR st.sandbox_id = sqlc.narg(sandbox_id)::text ) AND ( - sqlc.narg(alias)::text IS NULL + sqlc.narg(env_id)::text IS NULL + OR e.id = sqlc.narg(env_id)::text +) +AND ( + sqlc.narg(tag)::text IS NULL OR EXISTS ( - -- Mirror alias resolution: match the team namespace, and for bare aliases - -- also match promoted (NULL namespace) aliases. Explicit "team/alias" only - -- matches the team namespace. - SELECT 1 FROM "public"."env_aliases" af - WHERE af.env_id = e.id - AND af.alias = sqlc.narg(alias)::text - AND ( - af.namespace = @alias_namespace::text - OR (@match_null_namespace::bool AND af.namespace IS NULL) - ) + SELECT 1 + FROM "public"."env_build_assignments" tba + JOIN "public"."env_builds" tb ON tb.id = tba.build_id + WHERE tba.env_id = e.id + AND tba.tag = sqlc.narg(tag)::text + AND tb.status IN ('success', 'uploaded', 'ready') ) ) AND (e.created_at, e.id) < (@cursor_time, @cursor_id::text) diff --git a/spec/openapi.yml b/spec/openapi.yml index 87ec43d159..18431862ae 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -2713,7 +2713,7 @@ paths: description: Filter snapshots by source sandbox ID - name: name in: query - description: Filter snapshots by name (e.g. "my-snapshot" or "my-team/my-snapshot"). + description: Filter snapshots by name or ID, optionally tag-qualified (e.g. "my-snapshot", "my-team/my-snapshot" or "my-snapshot:v1"). required: false schema: type: string diff --git a/tests/integration/internal/api/generated.go b/tests/integration/internal/api/generated.go index d6dcda6a1c..3661a9b6a5 100644 --- a/tests/integration/internal/api/generated.go +++ b/tests/integration/internal/api/generated.go @@ -1563,7 +1563,7 @@ type GetSandboxesSandboxIDMetricsParams struct { type GetSnapshotsParams struct { SandboxID *string `form:"sandboxID,omitempty" json:"sandboxID,omitempty"` - // Name Filter snapshots by name (e.g. "my-snapshot" or "my-team/my-snapshot"). + // Name Filter snapshots by name or ID, optionally tag-qualified (e.g. "my-snapshot", "my-team/my-snapshot" or "my-snapshot:v1"). Name *string `form:"name,omitempty" json:"name,omitempty"` // Limit Maximum number of items to return per page From 6dbbc6fc4e5b3321c949840c8b5268af52e7ec97 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:55:10 +0200 Subject: [PATCH 5/7] test(integration): cover snapshot list name filter Co-Authored-By: Claude Fable 5 --- .../api/sandboxes/snapshot_template_test.go | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/integration/internal/tests/api/sandboxes/snapshot_template_test.go b/tests/integration/internal/tests/api/sandboxes/snapshot_template_test.go index 0a6c58fd31..6cd34d85aa 100644 --- a/tests/integration/internal/tests/api/sandboxes/snapshot_template_test.go +++ b/tests/integration/internal/tests/api/sandboxes/snapshot_template_test.go @@ -156,6 +156,63 @@ func TestSnapshotTemplateList(t *testing.T) { assert.NotEmpty(t, snap.SnapshotID) } }) + + t.Run("list snapshots filtered by name", func(t *testing.T) { + t.Parallel() + sbx := utils.SetupSandboxWithCleanup(t, c, utils.WithAutoPause(false)) + + name := "list-by-name-" + sbx.SandboxID + snapshot := createSnapshotTemplateWithCleanup(t, c, sbx.SandboxID, &name) + + listSnapshots := func(filter string) []api.SnapshotInfo { + listResp, err := c.GetSnapshotsWithResponse(t.Context(), &api.GetSnapshotsParams{ + Name: &filter, + }, setup.WithAPIKey()) + require.NoError(t, err) + require.Equal(t, http.StatusOK, listResp.StatusCode()) + require.NotNil(t, listResp.JSON200) + + return *listResp.JSON200 + } + + snapshots := listSnapshots(name) + require.Len(t, snapshots, 1) + assert.Equal(t, snapshot.SnapshotID, snapshots[0].SnapshotID) + + // The namespaced form returned in Names should also match + require.NotEmpty(t, snapshot.Names) + snapshots = listSnapshots(snapshot.Names[0]) + require.Len(t, snapshots, 1) + assert.Equal(t, snapshot.SnapshotID, snapshots[0].SnapshotID) + + // A name that doesn't resolve returns an empty list + assert.Empty(t, listSnapshots("missing-"+sbx.SandboxID)) + + // A tag that has no build on the snapshot returns an empty list + assert.Empty(t, listSnapshots(name+":missing-tag")) + + // The default tag has a build, so it matches + snapshots = listSnapshots(name + ":default") + require.Len(t, snapshots, 1) + assert.Equal(t, snapshot.SnapshotID, snapshots[0].SnapshotID) + }) + + t.Run("list unnamed snapshot filtered by its ID", func(t *testing.T) { + t.Parallel() + sbx := utils.SetupSandboxWithCleanup(t, c, utils.WithAutoPause(false)) + + snapshot := createSnapshotTemplateWithCleanup(t, c, sbx.SandboxID, nil) + templateID, _, _ := strings.Cut(snapshot.SnapshotID, ":") + + listResp, err := c.GetSnapshotsWithResponse(t.Context(), &api.GetSnapshotsParams{ + Name: &templateID, + }, setup.WithAPIKey()) + require.NoError(t, err) + require.Equal(t, http.StatusOK, listResp.StatusCode()) + require.NotNil(t, listResp.JSON200) + require.Len(t, *listResp.JSON200, 1) + assert.Equal(t, snapshot.SnapshotID, (*listResp.JSON200)[0].SnapshotID) + }) } func TestSnapshotTemplateDelete(t *testing.T) { From c4349bf3870d77fd06dbc8c63a45a94a0498163f Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:59:16 +0200 Subject: [PATCH 6/7] fix(api): return the tag-matched build when filtering snapshots by tag Move the tag condition into the lateral build join so a tag-qualified name filter both excludes snapshots without a successful build for that tag and reports that build's tag in the response, instead of always showing the newest build. Addresses Cursor review on the list endpoint. Co-Authored-By: Claude Fable 5 --- .../list_team_snapshot_templates.sql.go | 27 +++++++------------ .../list_team_snapshot_templates.sql | 17 ++++-------- .../api/sandboxes/snapshot_template_test.go | 24 +++++++++++++++++ 3 files changed, 39 insertions(+), 29 deletions(-) diff --git a/packages/db/queries/list_team_snapshot_templates.sql.go b/packages/db/queries/list_team_snapshot_templates.sql.go index 0331360702..2886a4dc33 100644 --- a/packages/db/queries/list_team_snapshot_templates.sql.go +++ b/packages/db/queries/list_team_snapshot_templates.sql.go @@ -31,10 +31,14 @@ SELECT FROM "public"."active_envs" e JOIN "public"."snapshot_templates" st ON st.env_id = e.id JOIN LATERAL ( + -- When a tag filter is set, pick the newest build with that tag; the inner + -- join then also drops snapshots with no successful build for the tag. SELECT b.id, b.created_at, b.updated_at, b.finished_at, b.status, b.dockerfile, b.start_cmd, b.vcpu, b.ram_mb, b.free_disk_size_mb, b.total_disk_size_mb, b.kernel_version, b.firecracker_version, b.env_id, b.envd_version, b.ready_cmd, b.cluster_node_id, b.reason, b.version, b.cpu_architecture, b.cpu_family, b.cpu_model, b.cpu_model_name, b.cpu_flags, b.status_group, b.team_id, ba.tag FROM "public"."env_build_assignments" ba JOIN "public"."env_builds" b ON b.id = ba.build_id - WHERE ba.env_id = e.id AND b.status IN ('success', 'uploaded', 'ready') + WHERE ba.env_id = e.id + AND b.status IN ('success', 'uploaded', 'ready') + AND ($1::text IS NULL OR ba.tag = $1::text) ORDER BY ba.created_at DESC LIMIT 1 ) eb ON TRUE @@ -44,26 +48,15 @@ LEFT JOIN LATERAL ( FROM "public"."env_aliases" WHERE env_id = e.id ) ea ON TRUE -WHERE e.team_id = $1 +WHERE e.team_id = $2 AND e.source = 'snapshot_template' -AND ( - $2::text IS NULL - OR st.sandbox_id = $2::text -) AND ( $3::text IS NULL - OR e.id = $3::text + OR st.sandbox_id = $3::text ) AND ( $4::text IS NULL - OR EXISTS ( - SELECT 1 - FROM "public"."env_build_assignments" tba - JOIN "public"."env_builds" tb ON tb.id = tba.build_id - WHERE tba.env_id = e.id - AND tba.tag = $4::text - AND tb.status IN ('success', 'uploaded', 'ready') - ) + OR e.id = $4::text ) AND (e.created_at, e.id) < ($5, $6::text) ORDER BY e.created_at DESC, e.id DESC @@ -71,10 +64,10 @@ LIMIT $7 ` type ListTeamSnapshotTemplatesParams struct { + Tag *string TeamID uuid.UUID SandboxID *string EnvID *string - Tag *string CursorTime time.Time CursorID string PageLimit int32 @@ -101,10 +94,10 @@ type ListTeamSnapshotTemplatesRow struct { // Snapshot templates are envs with source='snapshot_template'. func (q *Queries) ListTeamSnapshotTemplates(ctx context.Context, arg ListTeamSnapshotTemplatesParams) ([]ListTeamSnapshotTemplatesRow, error) { rows, err := q.db.Query(ctx, listTeamSnapshotTemplates, + arg.Tag, arg.TeamID, arg.SandboxID, arg.EnvID, - arg.Tag, arg.CursorTime, arg.CursorID, arg.PageLimit, diff --git a/packages/db/queries/snapshots/list_team_snapshot_templates.sql b/packages/db/queries/snapshots/list_team_snapshot_templates.sql index 6dcf97a47f..c8479d99cf 100644 --- a/packages/db/queries/snapshots/list_team_snapshot_templates.sql +++ b/packages/db/queries/snapshots/list_team_snapshot_templates.sql @@ -19,10 +19,14 @@ SELECT FROM "public"."active_envs" e JOIN "public"."snapshot_templates" st ON st.env_id = e.id JOIN LATERAL ( + -- When a tag filter is set, pick the newest build with that tag; the inner + -- join then also drops snapshots with no successful build for the tag. SELECT b.*, ba.tag FROM "public"."env_build_assignments" ba JOIN "public"."env_builds" b ON b.id = ba.build_id - WHERE ba.env_id = e.id AND b.status IN ('success', 'uploaded', 'ready') + WHERE ba.env_id = e.id + AND b.status IN ('success', 'uploaded', 'ready') + AND (sqlc.narg(tag)::text IS NULL OR ba.tag = sqlc.narg(tag)::text) ORDER BY ba.created_at DESC LIMIT 1 ) eb ON TRUE @@ -42,17 +46,6 @@ AND ( sqlc.narg(env_id)::text IS NULL OR e.id = sqlc.narg(env_id)::text ) -AND ( - sqlc.narg(tag)::text IS NULL - OR EXISTS ( - SELECT 1 - FROM "public"."env_build_assignments" tba - JOIN "public"."env_builds" tb ON tb.id = tba.build_id - WHERE tba.env_id = e.id - AND tba.tag = sqlc.narg(tag)::text - AND tb.status IN ('success', 'uploaded', 'ready') - ) -) AND (e.created_at, e.id) < (@cursor_time, @cursor_id::text) ORDER BY e.created_at DESC, e.id DESC LIMIT @page_limit; diff --git a/tests/integration/internal/tests/api/sandboxes/snapshot_template_test.go b/tests/integration/internal/tests/api/sandboxes/snapshot_template_test.go index 6cd34d85aa..a3ee7aadee 100644 --- a/tests/integration/internal/tests/api/sandboxes/snapshot_template_test.go +++ b/tests/integration/internal/tests/api/sandboxes/snapshot_template_test.go @@ -197,6 +197,30 @@ func TestSnapshotTemplateList(t *testing.T) { assert.Equal(t, snapshot.SnapshotID, snapshots[0].SnapshotID) }) + t.Run("list snapshots filtered by older tag shows that tag", func(t *testing.T) { + t.Parallel() + sbx := utils.SetupSandboxWithCleanup(t, c, utils.WithAutoPause(false)) + + base := "list-by-tag-" + sbx.SandboxID + nameV1 := base + ":v1" + snapV1 := createSnapshotTemplateWithCleanup(t, c, sbx.SandboxID, &nameV1) + + nameV2 := base + ":v2" + resp2 := createSnapshotTemplate(t, c, sbx.SandboxID, &nameV2) + require.Equal(t, http.StatusCreated, resp2.StatusCode()) + require.NotNil(t, resp2.JSON201) + + // Filtering by the older tag must match and show that tag, not the newest + listResp, err := c.GetSnapshotsWithResponse(t.Context(), &api.GetSnapshotsParams{ + Name: &nameV1, + }, setup.WithAPIKey()) + require.NoError(t, err) + require.Equal(t, http.StatusOK, listResp.StatusCode()) + require.NotNil(t, listResp.JSON200) + require.Len(t, *listResp.JSON200, 1) + assert.Equal(t, snapV1.SnapshotID, (*listResp.JSON200)[0].SnapshotID) + }) + t.Run("list unnamed snapshot filtered by its ID", func(t *testing.T) { t.Parallel() sbx := utils.SetupSandboxWithCleanup(t, c, utils.WithAutoPause(false)) From 8e3aee3ba3dba2b6f3e35b468bcee689d338df88 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Thu, 2 Jul 2026 19:19:24 +0200 Subject: [PATCH 7/7] fix(api): honor explicit default tag in snapshot name filter ParseName normalizes ":default" to a nil tag, so the list filter treated "name:default" like a bare name and matched builds of any tag. Re-check the raw input to filter by the default tag when it is given explicitly. Co-Authored-By: Claude Fable 5 --- .../handlers/snapshot_template_list.go | 8 ++++ .../api/sandboxes/snapshot_template_test.go | 38 +++++++++++++------ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/packages/api/internal/handlers/snapshot_template_list.go b/packages/api/internal/handlers/snapshot_template_list.go index d1a3c52951..e4b1c78a38 100644 --- a/packages/api/internal/handlers/snapshot_template_list.go +++ b/packages/api/internal/handlers/snapshot_template_list.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/http" + "strings" "time" "github.com/gin-gonic/gin" @@ -83,7 +84,14 @@ func (a *APIStore) GetSnapshots(c *gin.Context, params api.GetSnapshotsParams) { return } + // ParseName normalizes an explicit ":default" tag to nil; re-check the raw + // input so "name:default" filters by the default tag, while a bare "name" + // matches builds of any tag. tagFilter = tag + if _, _, hasTag := strings.Cut(*params.Name, id.TagSeparator); hasTag && tagFilter == nil { + defaultTag := id.DefaultTag + tagFilter = &defaultTag + } // Resolve alias using the cache — same pattern as template builds. Team // ownership is enforced by the query's team_id predicate, so a name that diff --git a/tests/integration/internal/tests/api/sandboxes/snapshot_template_test.go b/tests/integration/internal/tests/api/sandboxes/snapshot_template_test.go index a3ee7aadee..29f8ab9f53 100644 --- a/tests/integration/internal/tests/api/sandboxes/snapshot_template_test.go +++ b/tests/integration/internal/tests/api/sandboxes/snapshot_template_test.go @@ -202,23 +202,39 @@ func TestSnapshotTemplateList(t *testing.T) { sbx := utils.SetupSandboxWithCleanup(t, c, utils.WithAutoPause(false)) base := "list-by-tag-" + sbx.SandboxID - nameV1 := base + ":v1" - snapV1 := createSnapshotTemplateWithCleanup(t, c, sbx.SandboxID, &nameV1) + snapDefault := createSnapshotTemplateWithCleanup(t, c, sbx.SandboxID, &base) + require.Contains(t, snapDefault.SnapshotID, ":default") nameV2 := base + ":v2" resp2 := createSnapshotTemplate(t, c, sbx.SandboxID, &nameV2) require.Equal(t, http.StatusCreated, resp2.StatusCode()) require.NotNil(t, resp2.JSON201) - // Filtering by the older tag must match and show that tag, not the newest - listResp, err := c.GetSnapshotsWithResponse(t.Context(), &api.GetSnapshotsParams{ - Name: &nameV1, - }, setup.WithAPIKey()) - require.NoError(t, err) - require.Equal(t, http.StatusOK, listResp.StatusCode()) - require.NotNil(t, listResp.JSON200) - require.Len(t, *listResp.JSON200, 1) - assert.Equal(t, snapV1.SnapshotID, (*listResp.JSON200)[0].SnapshotID) + listSnapshots := func(filter string) []api.SnapshotInfo { + listResp, err := c.GetSnapshotsWithResponse(t.Context(), &api.GetSnapshotsParams{ + Name: &filter, + }, setup.WithAPIKey()) + require.NoError(t, err) + require.Equal(t, http.StatusOK, listResp.StatusCode()) + require.NotNil(t, listResp.JSON200) + + return *listResp.JSON200 + } + + // Filtering by an older tag (here the explicit default) must match and + // show that tag's build, not the newest one + snapshots := listSnapshots(base + ":default") + require.Len(t, snapshots, 1) + assert.Equal(t, snapDefault.SnapshotID, snapshots[0].SnapshotID) + + snapshots = listSnapshots(nameV2) + require.Len(t, snapshots, 1) + assert.Equal(t, resp2.JSON201.SnapshotID, snapshots[0].SnapshotID) + + // A bare name matches builds of any tag and shows the newest + snapshots = listSnapshots(base) + require.Len(t, snapshots, 1) + assert.Equal(t, resp2.JSON201.SnapshotID, snapshots[0].SnapshotID) }) t.Run("list unnamed snapshot filtered by its ID", func(t *testing.T) {