Skip to content

Allow configuring disk sizes per template en 1038 v2#3288

Draft
gm-e2b wants to merge 3 commits into
mainfrom
allow-configuring-disk-sizes-per-template-en-1038-v2
Draft

Allow configuring disk sizes per template en 1038 v2#3288
gm-e2b wants to merge 3 commits into
mainfrom
allow-configuring-disk-sizes-per-template-en-1038-v2

Conversation

@gm-e2b

@gm-e2b gm-e2b commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

No description provided.

gm-e2b added 2 commits July 14, 2026 17:46
Separate the default template free-space target from the maximum total
root filesystem size that a team may build.

Add nullable default_free_disk_size_mb and max_disk_size_mb columns to
tiers, plus extra_max_disk_size_mb to add-ons. Keep extra_disk_mb as the
add-on contribution to default free space and add constraints preventing
invalid negative or inverted tier values.

Keep the existing team_limits view unchanged for current consumers and
introduce team_limits_v2 with the new effective default and maximum
fields. Preserve compatibility with legacy add-on writers by treating a
null extra maximum as equal to extra_disk_mb.

Leave data population and runtime adoption to later rollout steps so
this commit remains an expand-only schema change.

Regenerate the database models and add focused migration coverage for
the new columns, the unchanged V1 view, the V2 contract, security
invoker behavior, and effective tier-plus-add-on calculations.

Part of: EN-1038
Read template disk defaults and maximums from team_limits_v2, and use the
new default when registering builds.

Propagate the build-start maximum through the internal TemplateCreate RPC
for both build endpoints. At build completion, compare the exact final
rootfs size with the maximum, emit disk-limit telemetry, and reject
oversized builds when build-enforce-max-disk-size is enabled.

Keep missing or invalid maximums fail-open and document the updated build
flow.
@linear-code

linear-code Bot commented Jul 14, 2026

Copy link
Copy Markdown

EN-1038

@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

❌ 16 Tests Failed:

Tests completed Failed Passed Skipped
2603 16 2587 6
View the top 3 failed test(s) by shortest run time
::TestMain
Stack Traces | 0s run time
FAIL	github..../api/internal/middleware [build failed]
::TestMain
Stack Traces | 0s run time
FAIL	github..../api/internal/clusters [build failed]
::TestMain
Stack Traces | 0s run time
FAIL	github..../api/internal/api [build failed]
::TestMain
Stack Traces | 0s run time
FAIL	github..../api/internal/template [build failed]
::TestMain
Stack Traces | 0s run time
FAIL	github..../internal/orchestrator/nodemanager [build failed]
::TestMain
Stack Traces | 0s run time
FAIL	github..../api/internal/orchestrator [build failed]
::TestMain
Stack Traces | 0s run time
FAIL	github..../api/internal/team [build failed]
::TestMain
Stack Traces | 0s run time
FAIL	github..../internal/orchestrator/placement [build failed]
::TestMain
Stack Traces | 0s run time
FAIL	github..../api/internal/utils [build failed]
::TestMain
Stack Traces | 0s run time
FAIL	github..../api/internal/cfg [build failed]
::TestMain
Stack Traces | 0s run time
FAIL	github..../api/internal/analytics_collector [build failed]
::TestMain
Stack Traces | 0s run time
FAIL	github..../api/internal/db [build failed]
::TestMain
Stack Traces | 0s run time
FAIL	github..../internal/middleware/ratelimit [build failed]
::TestMain
Stack Traces | 0s run time
FAIL	github..../api/internal/template-manager [build failed]
::TestMain
Stack Traces | 0s run time
FAIL	github..../api/internal/handlers [build failed]
::TestMain
Stack Traces | 0s run time
FAIL	github..../infra/packages/api [build failed]

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@gm-e2b gm-e2b marked this pull request as draft July 14, 2026 16:50
(tier.max_ram_mb + a.extra_max_ram_mb) AS max_ram_mb,
(tier.disk_mb + a.extra_disk_mb) AS disk_mb,
(tier.events_ttl_days + a.extra_events_ttl_days) AS events_ttl_days,
(tier.default_free_disk_size_mb + a.extra_disk_mb)::bigint AS default_free_disk_size_mb,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Agentic Security Review
Severity: HIGH

team_limits_v2.default_free_disk_size_mb is computed from nullable tiers.default_free_disk_size_mb (NULL + extra_disk_mb stays NULL), while downstream auth queries scan this column into non-null int64 fields. For teams on existing tier rows without a backfill/default, that scan can fail and break team resolution on authenticated request paths.

Impact: authenticated traffic for affected teams can fail in auth/team-loading paths (availability/DoS) until data is backfilled or the view expression is made NULL-safe.

Fix in Cursor Fix in Web

Reviewed by Cursor Security Reviewer for commit c1406f8. Configure here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces team-level disk entitlements and maximum disk size limits for template builds. It adds database migrations for new columns on tiers and addons, creates a new team_limits_v2 view, and updates the orchestrator and template-manager to enforce the resolved maximum disk size limit during builds under a new feature flag. The review feedback highlights a critical issue where the generated database model fields DefaultFreeDiskSizeMb and MaxDiskSizeMb are typed as int64 instead of *int64, which will cause runtime database scan errors when encountering NULL values. Additionally, the feedback suggests safely dereferencing these fields in newTeamLimits to prevent potential nil-pointer panics.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/db/pkg/auth/queries/models.go Outdated
Comment on lines +54 to +55
DefaultFreeDiskSizeMb int64
MaxDiskSizeMb int64

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The fields DefaultFreeDiskSizeMb and MaxDiskSizeMb are generated as int64 instead of *int64. Since the corresponding database columns default_free_disk_size_mb and max_disk_size_mb are nullable and do not have default values, fetching any team on an existing tier (where these columns are NULL) will result in a runtime database scan error: sql: Scan error on column index ..., name ...: converting NULL to int64 is unsupported.

Please update these fields to be pointers (*int64) to safely handle NULL values from the database.

Suggested change
DefaultFreeDiskSizeMb int64
MaxDiskSizeMb int64
DefaultFreeDiskSizeMb *int64
MaxDiskSizeMb *int64

Comment on lines 20 to 30
return &TeamLimits{
SandboxConcurrency: int64(teamLimits.ConcurrentSandboxes),
BuildConcurrency: int64(teamLimits.ConcurrentTemplateBuilds),
MaxLengthHours: teamLimits.MaxLengthHours,
MaxVcpu: int64(teamLimits.MaxVcpu),
MaxRamMb: int64(teamLimits.MaxRamMb),
DiskMb: int64(teamLimits.DiskMb),
EventsTTLDays: int64(teamLimits.EventsTtlDays),
SandboxConcurrency: int64(teamLimits.ConcurrentSandboxes),
BuildConcurrency: int64(teamLimits.ConcurrentTemplateBuilds),
MaxLengthHours: teamLimits.MaxLengthHours,
MaxVcpu: int64(teamLimits.MaxVcpu),
MaxRamMb: int64(teamLimits.MaxRamMb),
DiskMb: int64(teamLimits.DiskMb),
DefaultFreeDiskSizeMb: teamLimits.DefaultFreeDiskSizeMb,
MaxDiskSizeMb: teamLimits.MaxDiskSizeMb,
EventsTTLDays: int64(teamLimits.EventsTtlDays),
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

To support the nullable DefaultFreeDiskSizeMb and MaxDiskSizeMb fields (which should be pointers *int64 in authqueries.TeamLimitsV2), update newTeamLimits to safely dereference them and default to 0 if they are nil. This prevents potential nil-pointer dereference panics and ensures backward compatibility for existing tiers.

	var defaultFreeDiskSizeMb int64
	if teamLimits.DefaultFreeDiskSizeMb != nil {
		defaultFreeDiskSizeMb = *teamLimits.DefaultFreeDiskSizeMb
	}
	var maxDiskSizeMb int64
	if teamLimits.MaxDiskSizeMb != nil {
		maxDiskSizeMb = *teamLimits.MaxDiskSizeMb
	}

	return &TeamLimits{
		SandboxConcurrency:    int64(teamLimits.ConcurrentSandboxes),
		BuildConcurrency:      int64(teamLimits.ConcurrentTemplateBuilds),
		MaxLengthHours:        teamLimits.MaxLengthHours,
		MaxVcpu:               int64(teamLimits.MaxVcpu),
		MaxRamMb:              int64(teamLimits.MaxRamMb),
		DiskMb:                int64(teamLimits.DiskMb),
		DefaultFreeDiskSizeMb: defaultFreeDiskSizeMb,
		MaxDiskSizeMb:         maxDiskSizeMb,
		EventsTTLDays:         int64(teamLimits.EventsTtlDays),
	}

Comment on lines +6 to +16
SELECT
t.id,
tier.max_length_hours,
(tier.concurrent_instances + a.extra_concurrent_sandboxes) AS concurrent_sandboxes,
(tier.concurrent_template_builds + a.extra_concurrent_template_builds) AS concurrent_template_builds,
(tier.max_vcpu + a.extra_max_vcpu) AS max_vcpu,
(tier.max_ram_mb + a.extra_max_ram_mb) AS max_ram_mb,
(tier.disk_mb + a.extra_disk_mb) AS disk_mb,
(tier.events_ttl_days + a.extra_events_ttl_days) AS events_ttl_days,
(tier.default_free_disk_size_mb + a.extra_disk_mb)::bigint AS default_free_disk_size_mb,
(tier.max_disk_size_mb + a.extra_max_disk_size_mb)::bigint AS max_disk_size_mb

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Migration 20260714091414 adds tiers.default_free_disk_size_mb and tiers.max_disk_size_mb as nullable bigint columns with no DEFAULT and no backfill for existing rows. Because the team_limits_v2 view (20260714091415) does not COALESCE the tier-side value before adding the addon delta, every pre-existing tier yields NULL for these two columns, and since the auth-path generated model (packages/db/pkg/auth/queries/models.go) types them as non-pointer int64 while get_team.sql.go scans directly into that field, pgx will error scanning NULL into it. This breaks GetTeamWithTierByAPIKey/ByTeamID/ByTeamAndUser/GetTeamsWithUsersTeamsWithTier for every existing team as soon as this migration deploys, taking down API-key auth and team lookups system-wide until every tier row is manually backfilled.

Extended reasoning...

The bug: Migration 20260714091414_add_template_disk_entitlements.sql adds tiers.default_free_disk_size_mb and tiers.max_disk_size_mb as bigint columns with no DEFAULT and no backfill UPDATE for existing rows. The only guardrails are CHECK constraints (>= 0, > 0, <= max), and in SQL a CHECK evaluates to UNKNOWN (and therefore passes) when the operand is NULL. Every tier row that existed before this migration — i.e. every tier in every production/self-hosted deployment — therefore keeps these two columns as NULL after the migration runs. Nothing else in this PR (or in packages/db/migrations/) issues a backfill for them.

Where it manifests: The very next migration, 20260714091415_create_team_limits_v2.sql, defines the team_limits_v2 view as:

(tier.default_free_disk_size_mb + a.extra_disk_mb)::bigint AS default_free_disk_size_mb,
(tier.max_disk_size_mb + a.extra_max_disk_size_mb)::bigint AS max_disk_size_mb

The addon-side operand (a.extra_disk_mb, a.extra_max_disk_size_mb) is wrapped in COALESCE(..., 0) inside the lateral join, but the tier-side operand is not. In SQL, NULL + n = NULL, so for any team on a non-backfilled tier this view produces NULL in both columns — even when the team has zero addons.

Why the generated code can't absorb a NULL: The sqlc.yaml override comment even acknowledges the nullability problem ("sqlc does not infer nullability for these view expressions") and forces these two columns to *int64 — but that override only applies to the testutils sqlc config, not the authqueries config used by the real auth path. Concretely:

  • packages/db/pkg/testutils/queries/models.go: DefaultFreeDiskSizeMb *int64, MaxDiskSizeMb *int64 (correct, pointer).
  • packages/db/pkg/auth/queries/models.go (TeamLimitsV2, used by real auth): DefaultFreeDiskSizeMb int64, MaxDiskSizeMb int64 (non-pointer).

And get_team.sql.go scans directly into the non-pointer field in all four generated queries: GetTeamWithTierByAPIKey, GetTeamWithTierByTeamAndUser, GetTeamWithTierByTeamID, GetTeamsWithUsersTeamsWithTier (e.g. &i.TeamLimitsV2.DefaultFreeDiskSizeMb). pgx v5 returns a scan error when the destination is a non-pointer int64 and the column value is SQL NULL.

This non-pointer typing isn't just a stale codegen artifact that a re-run would silently fix either: packages/auth/pkg/types/teams.go's newTeamLimits assigns teamLimits.DefaultFreeDiskSizeMb straight into TeamLimits.DefaultFreeDiskSizeMb (also int64, no conversion), so the code depends on it staying a plain int64 to compile.

Step-by-step proof:

  1. Deploy runs migration 20260714091414: existing tier row base_v1 (from migration 20231220094836) gets default_free_disk_size_mb = NULL, max_disk_size_mb = NULL. The CHECK constraints pass because the operands are NULL.
  2. Migration 20260714091415 creates team_limits_v2. For any team on base_v1, evaluating the view: tier.default_free_disk_size_mb is NULL, a.extra_disk_mb is COALESCE(SUM(...), 0) = 0 (no addons) → NULL + 0 = NULL. Same for max_disk_size_mb.
  3. A request hits an authenticated endpoint. authStoreImpl.GetTeamByHashedAPIKey calls s.authDB.Read.GetTeamWithTierByAPIKey(ctx, hashedKey), which runs the generated query and executes row.Scan(..., &i.TeamLimitsV2.DefaultFreeDiskSizeMb, &i.TeamLimitsV2.MaxDiskSizeMb).
  4. pgx tries to write SQL NULL into a non-pointer int64 destination and returns an error (e.g. "cannot scan NULL into *int64" is fine, but this is a non-pointer int64, which errors as an invalid Scan target). The error propagates up through GetTeamByHashedAPIKeyfmt.Errorf("failed to get team from API key: %w", err).
  5. Every API-key-authenticated request for that team now fails at the auth layer. Because there is no backfill, this affects every pre-existing team as soon as the migration deploys — not just an edge case.

Why this isn't caught by the PR's own test: disk_entitlements_migration_test.go inserts a brand-new tier row with default_free_disk_size_mb and max_disk_size_mb explicitly populated (8000/30000), so it never exercises the NULL/pre-existing-tier path that production will hit.

Fix: Either (a) add COALESCE(tier.default_free_disk_size_mb, <sentinel>) / COALESCE(tier.max_disk_size_mb, <sentinel>) in the view (matching whatever sentinel makes sense, e.g. falling back to the legacy disk_mb), or (b) add a backfill UPDATE ... WHERE default_free_disk_size_mb IS NULL (and same for max_disk_size_mb) followed by SET NOT NULL, consistent with how prior tier-column additions in this repo (e.g. migration 20240219190940) handle backfill. Either fix should be paired with correcting the authqueries sqlc override so the Go type reflects the DB's actual nullability guarantee.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant