From 4621212c49f04e46a708152e96bd37769464c503 Mon Sep 17 00:00:00 2001 From: Giulio Micheloni Date: Tue, 14 Jul 2026 15:19:33 +0200 Subject: [PATCH 1/3] [EN-1038] feat(db): add template disk entitlement schema 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 --- ...4091414_add_template_disk_entitlements.sql | 42 +++++++ .../20260714091415_create_team_limits_v2.sql | 40 +++++++ .../tests/disk_entitlements_migration_test.go | 113 ++++++++++++++++++ packages/db/pkg/testutils/queries/models.go | 16 +++ packages/db/sqlc.yaml | 9 ++ 5 files changed, 220 insertions(+) create mode 100644 packages/db/migrations/20260714091414_add_template_disk_entitlements.sql create mode 100644 packages/db/migrations/20260714091415_create_team_limits_v2.sql create mode 100644 packages/db/pkg/tests/disk_entitlements_migration_test.go diff --git a/packages/db/migrations/20260714091414_add_template_disk_entitlements.sql b/packages/db/migrations/20260714091414_add_template_disk_entitlements.sql new file mode 100644 index 0000000000..5571f9d1be --- /dev/null +++ b/packages/db/migrations/20260714091414_add_template_disk_entitlements.sql @@ -0,0 +1,42 @@ +-- +goose Up +-- +goose StatementBegin + +ALTER TABLE "public"."tiers" + -- Free-rootfs target in the existing MiB convention. + ADD COLUMN "default_free_disk_size_mb" bigint, + -- Total logical-rootfs ceiling before active add-ons. + ADD COLUMN "max_disk_size_mb" bigint; + +ALTER TABLE "public"."addons" + -- Total-ceiling increment in the existing MiB convention. + ADD COLUMN "extra_max_disk_size_mb" bigint; + +ALTER TABLE "public"."tiers" + ADD CONSTRAINT "tiers_default_free_disk_size_mb_check" + CHECK (default_free_disk_size_mb >= 0), + ADD CONSTRAINT "tiers_max_disk_size_mb_check" + CHECK (max_disk_size_mb > 0), + ADD CONSTRAINT "tiers_default_free_disk_size_lte_max_check" + CHECK (default_free_disk_size_mb <= max_disk_size_mb); + +ALTER TABLE "public"."addons" + ADD CONSTRAINT "addons_extra_max_disk_size_mb_check" + CHECK (extra_max_disk_size_mb >= 0); + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +ALTER TABLE "public"."tiers" + DROP CONSTRAINT IF EXISTS "tiers_default_free_disk_size_lte_max_check", + DROP CONSTRAINT IF EXISTS "tiers_max_disk_size_mb_check", + DROP CONSTRAINT IF EXISTS "tiers_default_free_disk_size_mb_check", + DROP COLUMN IF EXISTS "max_disk_size_mb", + DROP COLUMN IF EXISTS "default_free_disk_size_mb"; + +ALTER TABLE "public"."addons" + DROP CONSTRAINT IF EXISTS "addons_extra_max_disk_size_mb_check", + DROP COLUMN IF EXISTS "extra_max_disk_size_mb"; + +-- +goose StatementEnd diff --git a/packages/db/migrations/20260714091415_create_team_limits_v2.sql b/packages/db/migrations/20260714091415_create_team_limits_v2.sql new file mode 100644 index 0000000000..d6889e3f9d --- /dev/null +++ b/packages/db/migrations/20260714091415_create_team_limits_v2.sql @@ -0,0 +1,40 @@ +-- +goose Up +-- +goose StatementBegin + +CREATE VIEW "public"."team_limits_v2" +WITH (security_invoker=on) AS +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 +FROM "public"."teams" t +JOIN "public"."tiers" tier ON t.tier = tier.id +LEFT JOIN LATERAL ( + SELECT COALESCE(SUM(extra_concurrent_sandboxes), 0)::bigint AS extra_concurrent_sandboxes, + COALESCE(SUM(extra_concurrent_template_builds), 0)::bigint AS extra_concurrent_template_builds, + COALESCE(SUM(extra_max_vcpu), 0)::bigint AS extra_max_vcpu, + COALESCE(SUM(extra_max_ram_mb), 0)::bigint AS extra_max_ram_mb, + COALESCE(SUM(extra_disk_mb), 0)::bigint AS extra_disk_mb, + COALESCE(SUM(extra_events_ttl_days), 0)::bigint AS extra_events_ttl_days, + COALESCE(SUM(COALESCE(extra_max_disk_size_mb, extra_disk_mb)), 0)::bigint AS extra_max_disk_size_mb + FROM "public"."addons" addon + WHERE addon.team_id = t.id + AND addon.valid_from <= now() + AND (addon.valid_to IS NULL OR addon.valid_to > now()) +) a ON true; + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +DROP VIEW IF EXISTS "public"."team_limits_v2"; + +-- +goose StatementEnd diff --git a/packages/db/pkg/tests/disk_entitlements_migration_test.go b/packages/db/pkg/tests/disk_entitlements_migration_test.go new file mode 100644 index 0000000000..3bbae52f6e --- /dev/null +++ b/packages/db/pkg/tests/disk_entitlements_migration_test.go @@ -0,0 +1,113 @@ +package tests + +import ( + "database/sql" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/e2b-dev/infra/packages/db/pkg/testutils" + testqueries "github.com/e2b-dev/infra/packages/db/pkg/testutils/queries" +) + +func TestDiskEntitlementsMigration(t *testing.T) { + t.Parallel() + + db := testutils.SetupDatabase(t) + ctx := t.Context() + + sqlDB, err := sql.Open("pgx", db.ConnStr()) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, sqlDB.Close()) }) + + var columnCount int64 + var nullableWithoutDefaults bool + err = sqlDB.QueryRowContext(ctx, ` + SELECT COUNT(*), BOOL_AND(is_nullable = 'YES' AND column_default IS NULL) + FROM information_schema.columns + WHERE table_schema = 'public' + AND (table_name, column_name) IN ( + ('tiers', 'default_free_disk_size_mb'), + ('tiers', 'max_disk_size_mb'), + ('addons', 'extra_max_disk_size_mb') + ) + `).Scan(&columnCount, &nullableWithoutDefaults) + require.NoError(t, err) + require.Equal(t, int64(3), columnCount) + require.True(t, nullableWithoutDefaults) + + var v1Columns, v2Columns string + err = sqlDB.QueryRowContext(ctx, ` + SELECT string_agg(column_name, ',' ORDER BY ordinal_position) + FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'team_limits' + `).Scan(&v1Columns) + require.NoError(t, err) + err = sqlDB.QueryRowContext(ctx, ` + SELECT string_agg(column_name, ',' ORDER BY ordinal_position) + FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'team_limits_v2' + `).Scan(&v2Columns) + require.NoError(t, err) + + const legacyColumns = "id,max_length_hours,concurrent_sandboxes,concurrent_template_builds," + + "max_vcpu,max_ram_mb,disk_mb,events_ttl_days" + require.Equal(t, legacyColumns, v1Columns) + require.Equal(t, legacyColumns+",default_free_disk_size_mb,max_disk_size_mb", v2Columns) + + var securityInvoker bool + err = sqlDB.QueryRowContext(ctx, ` + SELECT COALESCE(reloptions @> ARRAY['security_invoker=on']::text[], false) + FROM pg_class + WHERE oid = 'public.team_limits_v2'::regclass + `).Scan(&securityInvoker) + require.NoError(t, err) + require.True(t, securityInvoker) + + _, err = sqlDB.ExecContext(ctx, ` + INSERT INTO public.tiers ( + id, name, disk_mb, concurrent_instances, max_length_hours, + default_free_disk_size_mb, max_disk_size_mb + ) + VALUES ('en-1038-test', 'EN-1038 test', 10240, 1, 24, 8000, 30000) + `) + require.NoError(t, err) + + teamID := uuid.New() + err = db.TestQueries.InsertTestTeam(ctx, testqueries.InsertTestTeamParams{ + ID: teamID, + Name: "EN-1038 migration test", + Tier: "en-1038-test", + Email: "en-1038-migration@example.com", + Slug: "en-1038-migration", + }) + require.NoError(t, err) + + _, err = sqlDB.ExecContext(ctx, ` + INSERT INTO public.addons ( + team_id, name, extra_disk_mb, extra_max_disk_size_mb, added_by + ) + VALUES ($1, 'EN-1038 test add-on', 1000, 3000, + '00000000-0000-0000-0000-000000000000') + `, teamID) + require.NoError(t, err) + + var legacyDisk int64 + err = sqlDB.QueryRowContext(ctx, ` + SELECT disk_mb FROM public.team_limits WHERE id = $1 + `, teamID).Scan(&legacyDisk) + require.NoError(t, err) + require.Equal(t, int64(11240), legacyDisk) + + var v2Disk, defaultFree, maximum int64 + err = sqlDB.QueryRowContext(ctx, ` + SELECT disk_mb, default_free_disk_size_mb, max_disk_size_mb + FROM public.team_limits_v2 + WHERE id = $1 + `, teamID).Scan(&v2Disk, &defaultFree, &maximum) + require.NoError(t, err) + require.Equal(t, int64(11240), v2Disk) + require.Equal(t, int64(9000), defaultFree) + require.Equal(t, int64(33000), maximum) +} diff --git a/packages/db/pkg/testutils/queries/models.go b/packages/db/pkg/testutils/queries/models.go index 9fecdc27a4..9a80f47443 100644 --- a/packages/db/pkg/testutils/queries/models.go +++ b/packages/db/pkg/testutils/queries/models.go @@ -61,6 +61,7 @@ type Addon struct { AddedBy uuid.UUID IdempotencyKey pgtype.Text ExtraEventsTtlDays int64 + ExtraMaxDiskSizeMb pgtype.Int8 } type AuthUser struct { @@ -223,6 +224,19 @@ type TeamLimit struct { EventsTtlDays int32 } +type TeamLimitsV2 struct { + ID uuid.UUID + MaxLengthHours int64 + ConcurrentSandboxes int32 + ConcurrentTemplateBuilds int32 + MaxVcpu int32 + MaxRamMb int32 + DiskMb int32 + EventsTtlDays int32 + DefaultFreeDiskSizeMb *int64 + MaxDiskSizeMb *int64 +} + type Tier struct { ID string Name string @@ -235,6 +249,8 @@ type Tier struct { // The number of concurrent template builds the team can run ConcurrentTemplateBuilds int64 EventsTtlDays int64 + DefaultFreeDiskSizeMb pgtype.Int8 + MaxDiskSizeMb pgtype.Int8 } type User struct { diff --git a/packages/db/sqlc.yaml b/packages/db/sqlc.yaml index 0604fb5af5..8bf069e49e 100644 --- a/packages/db/sqlc.yaml +++ b/packages/db/sqlc.yaml @@ -21,6 +21,15 @@ overrides: import: "time" type: "Time" pointer: true + # sqlc does not infer nullability for these view expressions. + - column: "public.team_limits_v2.default_free_disk_size_mb" + go_type: + type: "int64" + pointer: true + - column: "public.team_limits_v2.max_disk_size_mb" + go_type: + type: "int64" + pointer: true sql: - engine: "postgresql" From c1406f8a87d87086186422e20a0ae9ea15f58bd8 Mon Sep 17 00:00:00 2001 From: Giulio Micheloni Date: Tue, 14 Jul 2026 18:45:12 +0200 Subject: [PATCH 2/3] [EN-1038] feat: add LD-gated template disk enforcement 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. --- docs/ARCHITECTURE.md | 30 +- packages/api/internal/db/teams.go | 4 +- .../deprecated_template_start_build.go | 1 + .../handlers/template_start_build_v2.go | 1 + .../template-manager/create_template.go | 4 +- .../api/internal/template/register_build.go | 2 +- packages/auth/pkg/auth/auth_store.go | 6 +- packages/auth/pkg/auth/middleware_test.go | 2 +- .../auth/pkg/auth/team_middleware_test.go | 20 +- packages/auth/pkg/auth/team_state_test.go | 8 +- packages/auth/pkg/types/limits.go | 5 +- packages/auth/pkg/types/teams.go | 20 +- packages/db/pkg/auth/queries/get_team.sql.go | 106 ++++--- packages/db/pkg/auth/queries/models.go | 4 +- .../pkg/auth/sql_queries/teams/get_team.sql | 8 +- .../pkg/template/build/builder.go | 53 +++- .../pkg/template/build/config/config.go | 4 + .../pkg/template/server/create_template.go | 1 + packages/orchestrator/template-manager.proto | 4 + packages/shared/pkg/featureflags/flags.go | 4 + .../template-manager/template-manager.pb.go | 278 +++++++++--------- 21 files changed, 335 insertions(+), 230 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b9b113e84e..ec5203308a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -290,22 +290,36 @@ sequenceDiagram DRP->>DRP: swap credentials, rewrite path → artifact registry end C->>API: POST /v3/templates (register build: cpu, ram) → Postgres env_builds + API->>API: resolve team default free-disk target C->>API: POST /v2/templates/{id}/builds/{buildID} (recipe: steps, start/ready cmd) - API->>TM: gRPC TemplateCreate(TemplateConfig) + API->>TM: gRPC TemplateCreate(internal build config + build-start max disk size) TM->>TM: pull image → inject envd/provisioning → extract ext4 rootfs TM->>FC: boot VM per phase: provision → user steps - TM->>TM: resize disk on host + opt free-disk restoration enabled + TM->>TM: resize disk on host + end TM->>FC: boot VM per phase: finalize → optimize TM->>OS: upload layers + final {buildID}/memfile, rootfs.ext4, snapfile, metadata - API->>TM: poll TemplateBuildStatus - API->>API: mark build ready in Postgres + TM->>TM: read final rootfs header and evaluate team maximum + alt maximum exceeded and enforcement enabled + TM->>OS: delete failed build artifacts + TM-->>API: build failed + else within limit or observe-only + API->>TM: poll TemplateBuildStatus + API->>API: persist floored total size and mark build ready in Postgres + end ``` Builds are **layered** (`pkg/template/build/phases/`): base → user → one layer per recipe step → -resize disk → finalize → optimize. Each layer is hashed and cached, so rebuilds only re-run changed -steps. Resize disk grows the quiescent rootfs on the host; the other non-cached phases run in a real -Firecracker VM and their pause-diffs become layers. The optimize phase records which memory pages a -fresh resume touches, producing prefetch hints that speed up future sandbox starts. +optional free-disk restoration → finalize → optimize. Each layer is hashed and cached, so rebuilds +only re-run changed steps. Free-disk restoration grows the quiescent rootfs on the host. After all +phases and uploads complete, the builder reads the final rootfs header once. It uses the exact logical +byte size both for the team-maximum decision and to derive the floored MiB value returned for +`env_builds.total_disk_size_mb`. The check adds attributes to the existing build span and rejects an +excess only when `build-enforce-max-disk-size` is enabled; rejection follows the normal failed-build +path and deletes the uploaded build prefix. The non-cached VM phases produce pause-diffs as layers. +The optimize phase records which memory pages a fresh resume touches, producing prefetch hints that +speed up future sandbox starts. ## Deployment topology diff --git a/packages/api/internal/db/teams.go b/packages/api/internal/db/teams.go index c89981bb47..ee6ae0bfdd 100644 --- a/packages/api/internal/db/teams.go +++ b/packages/api/internal/db/teams.go @@ -16,7 +16,7 @@ func GetTeamByID(ctx context.Context, db *authdb.Client, teamID uuid.UUID) (*typ return nil, fmt.Errorf("failed to get team by ID: %w", err) } - return types.NewTeam(&result.Team, &result.TeamLimit), nil + return types.NewTeam(&result.Team, &result.TeamLimitsV2), nil } func GetTeamsByUser(ctx context.Context, db *authdb.Client, userID uuid.UUID) ([]*types.TeamWithDefault, error) { @@ -28,7 +28,7 @@ func GetTeamsByUser(ctx context.Context, db *authdb.Client, userID uuid.UUID) ([ teamsWithLimits := make([]*types.TeamWithDefault, 0, len(teams)) for _, team := range teams { teamsWithLimits = append(teamsWithLimits, &types.TeamWithDefault{ - Team: types.NewTeam(&team.Team, &team.TeamLimit), + Team: types.NewTeam(&team.Team, &team.TeamLimitsV2), IsDefault: team.IsDefault, }) } diff --git a/packages/api/internal/handlers/deprecated_template_start_build.go b/packages/api/internal/handlers/deprecated_template_start_build.go index 034445242e..a5377ca64d 100644 --- a/packages/api/internal/handlers/deprecated_template_start_build.go +++ b/packages/api/internal/handlers/deprecated_template_start_build.go @@ -211,6 +211,7 @@ func (a *APIStore) PostTemplatesTemplateIDBuildsBuildID(c *gin.Context, template build.StartCmd, build.Vcpu, build.FreeDiskSizeMb, + &team.Limits.MaxDiskSizeMb, build.RamMb, build.ReadyCmd, &fromImage, diff --git a/packages/api/internal/handlers/template_start_build_v2.go b/packages/api/internal/handlers/template_start_build_v2.go index 037e49333f..af7216be12 100644 --- a/packages/api/internal/handlers/template_start_build_v2.go +++ b/packages/api/internal/handlers/template_start_build_v2.go @@ -174,6 +174,7 @@ func (a *APIStore) PostV2TemplatesTemplateIDBuildsBuildID(c *gin.Context, templa body.StartCmd, build.Vcpu, build.FreeDiskSizeMb, + &team.Limits.MaxDiskSizeMb, build.RamMb, body.ReadyCmd, body.FromImage, diff --git a/packages/api/internal/template-manager/create_template.go b/packages/api/internal/template-manager/create_template.go index bbea8805ff..99c7ac1f78 100644 --- a/packages/api/internal/template-manager/create_template.go +++ b/packages/api/internal/template-manager/create_template.go @@ -44,7 +44,8 @@ func (tm *TemplateManager) CreateTemplate( firecrackerVersion string, startCommand *string, vCpuCount, - diskSizeMB, + diskSizeMB int64, + maxDiskSizeMB *int64, memoryMB int64, readyCommand *string, fromImage *string, @@ -118,6 +119,7 @@ func (tm *TemplateManager) CreateTemplate( VCpuCount: int32(vCpuCount), MemoryMB: int32(memoryMB), DiskSizeMB: int32(diskSizeMB), + MaxDiskSizeMB: maxDiskSizeMB, KernelVersion: kernelVersion, FirecrackerVersion: firecrackerVersion, HugePages: features.HasHugePages(), diff --git a/packages/api/internal/template/register_build.go b/packages/api/internal/template/register_build.go index 4908f8fb3e..e1525ba132 100644 --- a/packages/api/internal/template/register_build.go +++ b/packages/api/internal/template/register_build.go @@ -223,7 +223,7 @@ func RegisterBuild( Vcpu: cpuCount, KernelVersion: data.KernelVersion, FirecrackerVersion: data.FirecrackerVersion, - FreeDiskSizeMb: data.Team.Limits.DiskMb, + FreeDiskSizeMb: data.Team.Limits.DefaultFreeDiskSizeMb, StartCmd: data.StartCmd, ReadyCmd: data.ReadyCmd, Dockerfile: new(data.Dockerfile), diff --git a/packages/auth/pkg/auth/auth_store.go b/packages/auth/pkg/auth/auth_store.go index af31b11a49..8f6e8fe3f8 100644 --- a/packages/auth/pkg/auth/auth_store.go +++ b/packages/auth/pkg/auth/auth_store.go @@ -48,7 +48,7 @@ func (s *authStoreImpl) GetTeamByHashedAPIKey(ctx context.Context, hashedKey str } }() - team := types.NewTeam(&result.Team, &result.TeamLimit) + team := types.NewTeam(&result.Team, &result.TeamLimitsV2) return team, nil } @@ -66,7 +66,7 @@ func (s *authStoreImpl) GetTeamByID(ctx context.Context, teamID uuid.UUID) (*typ return nil, err } - team := types.NewTeam(&result.Team, &result.TeamLimit) + team := types.NewTeam(&result.Team, &result.TeamLimitsV2) return team, nil } @@ -92,7 +92,7 @@ func (s *authStoreImpl) GetTeamByIDAndUserID(ctx context.Context, userID uuid.UU return nil, err } - team := types.NewTeam(&result.Team, &result.TeamLimit) + team := types.NewTeam(&result.Team, &result.TeamLimitsV2) return team, nil } diff --git a/packages/auth/pkg/auth/middleware_test.go b/packages/auth/pkg/auth/middleware_test.go index 9c49d391b9..7703037203 100644 --- a/packages/auth/pkg/auth/middleware_test.go +++ b/packages/auth/pkg/auth/middleware_test.go @@ -41,7 +41,7 @@ func TestAdminTeamAuthenticatorSetsTeamContext(t *testing.T) { ctx := context.Background() teamID := uuid.New() - team := types.NewTeam(&authqueries.Team{ID: teamID}, &authqueries.TeamLimit{}) + team := types.NewTeam(&authqueries.Team{ID: teamID}, &authqueries.TeamLimitsV2{}) req := httptest.NewRequestWithContext(ctx, http.MethodGet, "/", nil) req.Header.Set(HeaderTeamID, teamID.String()) diff --git a/packages/auth/pkg/auth/team_middleware_test.go b/packages/auth/pkg/auth/team_middleware_test.go index 39a6a27bd5..b2524c50ea 100644 --- a/packages/auth/pkg/auth/team_middleware_test.go +++ b/packages/auth/pkg/auth/team_middleware_test.go @@ -44,7 +44,7 @@ func TestEnforceBlockedTeam(t *testing.T) { name: "not blocked passes", method: http.MethodPost, fullPath: "/sandboxes", - team: types.NewTeam(&authqueries.Team{IsBlocked: false}, &authqueries.TeamLimit{}), + team: types.NewTeam(&authqueries.Team{IsBlocked: false}, &authqueries.TeamLimitsV2{}), wantHandlerRan: true, wantStatus: http.StatusOK, }, @@ -52,7 +52,7 @@ func TestEnforceBlockedTeam(t *testing.T) { name: "blocked and allowlisted get passes", method: http.MethodGet, fullPath: "/sandboxes", - team: types.NewTeam(&authqueries.Team{IsBlocked: true, BlockedReason: &reason}, &authqueries.TeamLimit{}), + team: types.NewTeam(&authqueries.Team{IsBlocked: true, BlockedReason: &reason}, &authqueries.TeamLimitsV2{}), wantHandlerRan: true, wantStatus: http.StatusOK, }, @@ -60,7 +60,7 @@ func TestEnforceBlockedTeam(t *testing.T) { name: "blocked and allowlisted delete passes", method: http.MethodDelete, fullPath: "/sandboxes/:sandboxID", - team: types.NewTeam(&authqueries.Team{IsBlocked: true, BlockedReason: &reason}, &authqueries.TeamLimit{}), + team: types.NewTeam(&authqueries.Team{IsBlocked: true, BlockedReason: &reason}, &authqueries.TeamLimitsV2{}), wantHandlerRan: true, wantStatus: http.StatusOK, }, @@ -68,7 +68,7 @@ func TestEnforceBlockedTeam(t *testing.T) { name: "blocked and non-allowlisted post denied", method: http.MethodPost, fullPath: "/sandboxes", - team: types.NewTeam(&authqueries.Team{IsBlocked: true, BlockedReason: &reason}, &authqueries.TeamLimit{}), + team: types.NewTeam(&authqueries.Team{IsBlocked: true, BlockedReason: &reason}, &authqueries.TeamLimitsV2{}), wantHandlerRan: false, wantStatus: http.StatusForbidden, wantBodyHas: "team is blocked: " + reason, @@ -77,7 +77,7 @@ func TestEnforceBlockedTeam(t *testing.T) { name: "blocked and method mismatch on allowlisted path denied", method: http.MethodPost, fullPath: "/sandboxes/:sandboxID", - team: types.NewTeam(&authqueries.Team{IsBlocked: true}, &authqueries.TeamLimit{}), + team: types.NewTeam(&authqueries.Team{IsBlocked: true}, &authqueries.TeamLimitsV2{}), wantHandlerRan: false, wantStatus: http.StatusForbidden, wantBodyHas: "team is blocked", @@ -102,7 +102,7 @@ func TestEnforceBlockedTeam(t *testing.T) { func TestEnforceBlockedTeam_AllowlistParameterization(t *testing.T) { t.Parallel() - team := types.NewTeam(&authqueries.Team{IsBlocked: true}, &authqueries.TeamLimit{}) + team := types.NewTeam(&authqueries.Team{IsBlocked: true}, &authqueries.TeamLimitsV2{}) const method, path = http.MethodGet, "/teams/:teamID/members" permissive := BlockedTeamAllowlist{method: {path: {}}} @@ -125,10 +125,10 @@ func TestCheckTeamAccess(t *testing.T) { http.MethodGet: {"/sandboxes": {}}, } - cleanTeam := types.NewTeam(&authqueries.Team{}, &authqueries.TeamLimit{}) - bannedTeam := types.NewTeam(&authqueries.Team{IsBanned: true}, &authqueries.TeamLimit{}) - blockedTeam := types.NewTeam(&authqueries.Team{IsBlocked: true, BlockedReason: &reason}, &authqueries.TeamLimit{}) - bannedAndBlockedTeam := types.NewTeam(&authqueries.Team{IsBanned: true, IsBlocked: true, BlockedReason: &reason}, &authqueries.TeamLimit{}) + cleanTeam := types.NewTeam(&authqueries.Team{}, &authqueries.TeamLimitsV2{}) + bannedTeam := types.NewTeam(&authqueries.Team{IsBanned: true}, &authqueries.TeamLimitsV2{}) + blockedTeam := types.NewTeam(&authqueries.Team{IsBlocked: true, BlockedReason: &reason}, &authqueries.TeamLimitsV2{}) + bannedAndBlockedTeam := types.NewTeam(&authqueries.Team{IsBanned: true, IsBlocked: true, BlockedReason: &reason}, &authqueries.TeamLimitsV2{}) wantForbidden := func(t *testing.T, err error) { t.Helper() diff --git a/packages/auth/pkg/auth/team_state_test.go b/packages/auth/pkg/auth/team_state_test.go index 1ca4da9efd..0f53a4c79c 100644 --- a/packages/auth/pkg/auth/team_state_test.go +++ b/packages/auth/pkg/auth/team_state_test.go @@ -71,24 +71,24 @@ func TestCheckTeamBlocked(t *testing.T) { }, { name: "not blocked", - team: types.NewTeam(&authqueries.Team{IsBlocked: false}, &authqueries.TeamLimit{}), + team: types.NewTeam(&authqueries.Team{IsBlocked: false}, &authqueries.TeamLimitsV2{}), wantErr: false, }, { name: "blocked without reason", - team: types.NewTeam(&authqueries.Team{IsBlocked: true}, &authqueries.TeamLimit{}), + team: types.NewTeam(&authqueries.Team{IsBlocked: true}, &authqueries.TeamLimitsV2{}), wantErr: true, wantMsgHas: "team is blocked", }, { name: "blocked with reason", - team: types.NewTeam(&authqueries.Team{IsBlocked: true, BlockedReason: &reason}, &authqueries.TeamLimit{}), + team: types.NewTeam(&authqueries.Team{IsBlocked: true, BlockedReason: &reason}, &authqueries.TeamLimitsV2{}), wantErr: true, wantMsgHas: reason, }, { name: "blocked with empty reason pointer", - team: types.NewTeam(&authqueries.Team{IsBlocked: true, BlockedReason: &empty}, &authqueries.TeamLimit{}), + team: types.NewTeam(&authqueries.Team{IsBlocked: true, BlockedReason: &empty}, &authqueries.TeamLimitsV2{}), wantErr: true, wantMsgHas: "team is blocked", }, diff --git a/packages/auth/pkg/types/limits.go b/packages/auth/pkg/types/limits.go index 28d39f6e11..db2fe0bc94 100644 --- a/packages/auth/pkg/types/limits.go +++ b/packages/auth/pkg/types/limits.go @@ -7,7 +7,10 @@ type TeamLimits struct { MaxVcpu int64 MaxRamMb int64 - DiskMb int64 + // Deprecated: use DefaultFreeDiskSizeMb or MaxDiskSizeMb according to the operation. + DiskMb int64 + DefaultFreeDiskSizeMb int64 + MaxDiskSizeMb int64 EventsTTLDays int64 } diff --git a/packages/auth/pkg/types/teams.go b/packages/auth/pkg/types/teams.go index f8b383bea4..de0841735f 100644 --- a/packages/auth/pkg/types/teams.go +++ b/packages/auth/pkg/types/teams.go @@ -15,22 +15,24 @@ func (t *Team) TeamID() string { } func newTeamLimits( - teamLimits *authqueries.TeamLimit, + teamLimits *authqueries.TeamLimitsV2, ) *TeamLimits { 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), } } func NewTeam( team *authqueries.Team, - teamLimits *authqueries.TeamLimit, + teamLimits *authqueries.TeamLimitsV2, ) *Team { return &Team{ Team: team, diff --git a/packages/db/pkg/auth/queries/get_team.sql.go b/packages/db/pkg/auth/queries/get_team.sql.go index 931cb3b611..1fae5fd37d 100644 --- a/packages/db/pkg/auth/queries/get_team.sql.go +++ b/packages/db/pkg/auth/queries/get_team.sql.go @@ -60,16 +60,16 @@ func (q *Queries) GetAutoJoinTeamsBySSOOrganizationID(ctx context.Context, ssoOr } const getTeamWithTierByAPIKey = `-- name: GetTeamWithTierByAPIKey :one -SELECT t.id, t.created_at, t.is_blocked, t.name, t.tier, t.email, t.is_banned, t.blocked_reason, t.cluster_id, t.sandbox_scheduling_labels, t.sso_organization_id, t.sso_auto_join, t.slug, tl.id, tl.max_length_hours, tl.concurrent_sandboxes, tl.concurrent_template_builds, tl.max_vcpu, tl.max_ram_mb, tl.disk_mb, tl.events_ttl_days FROM "public"."team_api_keys" tak +SELECT t.id, t.created_at, t.is_blocked, t.name, t.tier, t.email, t.is_banned, t.blocked_reason, t.cluster_id, t.sandbox_scheduling_labels, t.sso_organization_id, t.sso_auto_join, t.slug, tl.id, tl.max_length_hours, tl.concurrent_sandboxes, tl.concurrent_template_builds, tl.max_vcpu, tl.max_ram_mb, tl.disk_mb, tl.events_ttl_days, tl.default_free_disk_size_mb, tl.max_disk_size_mb FROM "public"."team_api_keys" tak JOIN "public"."teams" t ON tak.team_id = t.id -JOIN "public"."team_limits" tl on tl.id = t.id +JOIN "public"."team_limits_v2" tl on tl.id = t.id WHERE tak.team_id = t.id AND tak.api_key_hash = $1 ` type GetTeamWithTierByAPIKeyRow struct { - Team Team - TeamLimit TeamLimit + Team Team + TeamLimitsV2 TeamLimitsV2 } func (q *Queries) GetTeamWithTierByAPIKey(ctx context.Context, apiKeyHash string) (GetTeamWithTierByAPIKeyRow, error) { @@ -89,23 +89,25 @@ func (q *Queries) GetTeamWithTierByAPIKey(ctx context.Context, apiKeyHash string &i.Team.SsoOrganizationID, &i.Team.SsoAutoJoin, &i.Team.Slug, - &i.TeamLimit.ID, - &i.TeamLimit.MaxLengthHours, - &i.TeamLimit.ConcurrentSandboxes, - &i.TeamLimit.ConcurrentTemplateBuilds, - &i.TeamLimit.MaxVcpu, - &i.TeamLimit.MaxRamMb, - &i.TeamLimit.DiskMb, - &i.TeamLimit.EventsTtlDays, + &i.TeamLimitsV2.ID, + &i.TeamLimitsV2.MaxLengthHours, + &i.TeamLimitsV2.ConcurrentSandboxes, + &i.TeamLimitsV2.ConcurrentTemplateBuilds, + &i.TeamLimitsV2.MaxVcpu, + &i.TeamLimitsV2.MaxRamMb, + &i.TeamLimitsV2.DiskMb, + &i.TeamLimitsV2.EventsTtlDays, + &i.TeamLimitsV2.DefaultFreeDiskSizeMb, + &i.TeamLimitsV2.MaxDiskSizeMb, ) return i, err } const getTeamWithTierByTeamAndUser = `-- name: GetTeamWithTierByTeamAndUser :one -SELECT t.id, t.created_at, t.is_blocked, t.name, t.tier, t.email, t.is_banned, t.blocked_reason, t.cluster_id, t.sandbox_scheduling_labels, t.sso_organization_id, t.sso_auto_join, t.slug, tl.id, tl.max_length_hours, tl.concurrent_sandboxes, tl.concurrent_template_builds, tl.max_vcpu, tl.max_ram_mb, tl.disk_mb, tl.events_ttl_days +SELECT t.id, t.created_at, t.is_blocked, t.name, t.tier, t.email, t.is_banned, t.blocked_reason, t.cluster_id, t.sandbox_scheduling_labels, t.sso_organization_id, t.sso_auto_join, t.slug, tl.id, tl.max_length_hours, tl.concurrent_sandboxes, tl.concurrent_template_builds, tl.max_vcpu, tl.max_ram_mb, tl.disk_mb, tl.events_ttl_days, tl.default_free_disk_size_mb, tl.max_disk_size_mb FROM "public"."teams" t JOIN "public"."users_teams" ut ON ut.team_id = t.id - JOIN "public"."team_limits" tl on tl.id = t.id + JOIN "public"."team_limits_v2" tl on tl.id = t.id WHERE ut.user_id = $1 AND t.id = $2 ` @@ -115,8 +117,8 @@ type GetTeamWithTierByTeamAndUserParams struct { } type GetTeamWithTierByTeamAndUserRow struct { - Team Team - TeamLimit TeamLimit + Team Team + TeamLimitsV2 TeamLimitsV2 } func (q *Queries) GetTeamWithTierByTeamAndUser(ctx context.Context, arg GetTeamWithTierByTeamAndUserParams) (GetTeamWithTierByTeamAndUserRow, error) { @@ -136,28 +138,30 @@ func (q *Queries) GetTeamWithTierByTeamAndUser(ctx context.Context, arg GetTeamW &i.Team.SsoOrganizationID, &i.Team.SsoAutoJoin, &i.Team.Slug, - &i.TeamLimit.ID, - &i.TeamLimit.MaxLengthHours, - &i.TeamLimit.ConcurrentSandboxes, - &i.TeamLimit.ConcurrentTemplateBuilds, - &i.TeamLimit.MaxVcpu, - &i.TeamLimit.MaxRamMb, - &i.TeamLimit.DiskMb, - &i.TeamLimit.EventsTtlDays, + &i.TeamLimitsV2.ID, + &i.TeamLimitsV2.MaxLengthHours, + &i.TeamLimitsV2.ConcurrentSandboxes, + &i.TeamLimitsV2.ConcurrentTemplateBuilds, + &i.TeamLimitsV2.MaxVcpu, + &i.TeamLimitsV2.MaxRamMb, + &i.TeamLimitsV2.DiskMb, + &i.TeamLimitsV2.EventsTtlDays, + &i.TeamLimitsV2.DefaultFreeDiskSizeMb, + &i.TeamLimitsV2.MaxDiskSizeMb, ) return i, err } const getTeamWithTierByTeamID = `-- name: GetTeamWithTierByTeamID :one -SELECT t.id, t.created_at, t.is_blocked, t.name, t.tier, t.email, t.is_banned, t.blocked_reason, t.cluster_id, t.sandbox_scheduling_labels, t.sso_organization_id, t.sso_auto_join, t.slug, tl.id, tl.max_length_hours, tl.concurrent_sandboxes, tl.concurrent_template_builds, tl.max_vcpu, tl.max_ram_mb, tl.disk_mb, tl.events_ttl_days +SELECT t.id, t.created_at, t.is_blocked, t.name, t.tier, t.email, t.is_banned, t.blocked_reason, t.cluster_id, t.sandbox_scheduling_labels, t.sso_organization_id, t.sso_auto_join, t.slug, tl.id, tl.max_length_hours, tl.concurrent_sandboxes, tl.concurrent_template_builds, tl.max_vcpu, tl.max_ram_mb, tl.disk_mb, tl.events_ttl_days, tl.default_free_disk_size_mb, tl.max_disk_size_mb FROM "public"."teams" t - JOIN "public"."team_limits" tl on tl.id = t.id + JOIN "public"."team_limits_v2" tl on tl.id = t.id WHERE t.id = $1 ` type GetTeamWithTierByTeamIDRow struct { - Team Team - TeamLimit TeamLimit + Team Team + TeamLimitsV2 TeamLimitsV2 } func (q *Queries) GetTeamWithTierByTeamID(ctx context.Context, id uuid.UUID) (GetTeamWithTierByTeamIDRow, error) { @@ -177,30 +181,32 @@ func (q *Queries) GetTeamWithTierByTeamID(ctx context.Context, id uuid.UUID) (Ge &i.Team.SsoOrganizationID, &i.Team.SsoAutoJoin, &i.Team.Slug, - &i.TeamLimit.ID, - &i.TeamLimit.MaxLengthHours, - &i.TeamLimit.ConcurrentSandboxes, - &i.TeamLimit.ConcurrentTemplateBuilds, - &i.TeamLimit.MaxVcpu, - &i.TeamLimit.MaxRamMb, - &i.TeamLimit.DiskMb, - &i.TeamLimit.EventsTtlDays, + &i.TeamLimitsV2.ID, + &i.TeamLimitsV2.MaxLengthHours, + &i.TeamLimitsV2.ConcurrentSandboxes, + &i.TeamLimitsV2.ConcurrentTemplateBuilds, + &i.TeamLimitsV2.MaxVcpu, + &i.TeamLimitsV2.MaxRamMb, + &i.TeamLimitsV2.DiskMb, + &i.TeamLimitsV2.EventsTtlDays, + &i.TeamLimitsV2.DefaultFreeDiskSizeMb, + &i.TeamLimitsV2.MaxDiskSizeMb, ) return i, err } const getTeamsWithUsersTeamsWithTier = `-- name: GetTeamsWithUsersTeamsWithTier :many -SELECT t.id, t.created_at, t.is_blocked, t.name, t.tier, t.email, t.is_banned, t.blocked_reason, t.cluster_id, t.sandbox_scheduling_labels, t.sso_organization_id, t.sso_auto_join, t.slug, ut.is_default, tl.id, tl.max_length_hours, tl.concurrent_sandboxes, tl.concurrent_template_builds, tl.max_vcpu, tl.max_ram_mb, tl.disk_mb, tl.events_ttl_days +SELECT t.id, t.created_at, t.is_blocked, t.name, t.tier, t.email, t.is_banned, t.blocked_reason, t.cluster_id, t.sandbox_scheduling_labels, t.sso_organization_id, t.sso_auto_join, t.slug, ut.is_default, tl.id, tl.max_length_hours, tl.concurrent_sandboxes, tl.concurrent_template_builds, tl.max_vcpu, tl.max_ram_mb, tl.disk_mb, tl.events_ttl_days, tl.default_free_disk_size_mb, tl.max_disk_size_mb FROM "public"."teams" t JOIN "public"."users_teams" ut ON ut.team_id = t.id - JOIN "public"."team_limits" tl on tl.id = t.id + JOIN "public"."team_limits_v2" tl on tl.id = t.id WHERE ut.user_id = $1 ` type GetTeamsWithUsersTeamsWithTierRow struct { - Team Team - IsDefault bool - TeamLimit TeamLimit + Team Team + IsDefault bool + TeamLimitsV2 TeamLimitsV2 } func (q *Queries) GetTeamsWithUsersTeamsWithTier(ctx context.Context, userID uuid.UUID) ([]GetTeamsWithUsersTeamsWithTierRow, error) { @@ -227,14 +233,16 @@ func (q *Queries) GetTeamsWithUsersTeamsWithTier(ctx context.Context, userID uui &i.Team.SsoAutoJoin, &i.Team.Slug, &i.IsDefault, - &i.TeamLimit.ID, - &i.TeamLimit.MaxLengthHours, - &i.TeamLimit.ConcurrentSandboxes, - &i.TeamLimit.ConcurrentTemplateBuilds, - &i.TeamLimit.MaxVcpu, - &i.TeamLimit.MaxRamMb, - &i.TeamLimit.DiskMb, - &i.TeamLimit.EventsTtlDays, + &i.TeamLimitsV2.ID, + &i.TeamLimitsV2.MaxLengthHours, + &i.TeamLimitsV2.ConcurrentSandboxes, + &i.TeamLimitsV2.ConcurrentTemplateBuilds, + &i.TeamLimitsV2.MaxVcpu, + &i.TeamLimitsV2.MaxRamMb, + &i.TeamLimitsV2.DiskMb, + &i.TeamLimitsV2.EventsTtlDays, + &i.TeamLimitsV2.DefaultFreeDiskSizeMb, + &i.TeamLimitsV2.MaxDiskSizeMb, ); err != nil { return nil, err } diff --git a/packages/db/pkg/auth/queries/models.go b/packages/db/pkg/auth/queries/models.go index 0caf3f6c8a..d2e9b7f818 100644 --- a/packages/db/pkg/auth/queries/models.go +++ b/packages/db/pkg/auth/queries/models.go @@ -42,7 +42,7 @@ type TeamApiKey struct { ApiKeyMaskSuffix string } -type TeamLimit struct { +type TeamLimitsV2 struct { ID uuid.UUID MaxLengthHours int64 ConcurrentSandboxes int32 @@ -51,4 +51,6 @@ type TeamLimit struct { MaxRamMb int32 DiskMb int32 EventsTtlDays int32 + DefaultFreeDiskSizeMb int64 + MaxDiskSizeMb int64 } diff --git a/packages/db/pkg/auth/sql_queries/teams/get_team.sql b/packages/db/pkg/auth/sql_queries/teams/get_team.sql index 59998bbe2c..505c386410 100644 --- a/packages/db/pkg/auth/sql_queries/teams/get_team.sql +++ b/packages/db/pkg/auth/sql_queries/teams/get_team.sql @@ -1,7 +1,7 @@ -- name: GetTeamWithTierByAPIKey :one SELECT sqlc.embed(t), sqlc.embed(tl) FROM "public"."team_api_keys" tak JOIN "public"."teams" t ON tak.team_id = t.id -JOIN "public"."team_limits" tl on tl.id = t.id +JOIN "public"."team_limits_v2" tl on tl.id = t.id WHERE tak.team_id = t.id AND tak.api_key_hash = $1; @@ -9,13 +9,13 @@ WHERE tak.team_id = t.id SELECT sqlc.embed(t), sqlc.embed(tl) FROM "public"."teams" t JOIN "public"."users_teams" ut ON ut.team_id = t.id - JOIN "public"."team_limits" tl on tl.id = t.id + JOIN "public"."team_limits_v2" tl on tl.id = t.id WHERE ut.user_id = $1 AND t.id = $2; -- name: GetTeamWithTierByTeamID :one SELECT sqlc.embed(t), sqlc.embed(tl) FROM "public"."teams" t - JOIN "public"."team_limits" tl on tl.id = t.id + JOIN "public"."team_limits_v2" tl on tl.id = t.id WHERE t.id = $1; -- name: GetAutoJoinTeamsBySSOOrganizationID :many @@ -31,5 +31,5 @@ ORDER BY t.created_at ASC; SELECT sqlc.embed(t), ut.is_default, sqlc.embed(tl) FROM "public"."teams" t JOIN "public"."users_teams" ut ON ut.team_id = t.id - JOIN "public"."team_limits" tl on tl.id = t.id + JOIN "public"."team_limits_v2" tl on tl.id = t.id WHERE ut.user_id = $1; diff --git a/packages/orchestrator/pkg/template/build/builder.go b/packages/orchestrator/pkg/template/build/builder.go index e460f4a82c..ac6930486b 100644 --- a/packages/orchestrator/pkg/template/build/builder.go +++ b/packages/orchestrator/pkg/template/build/builder.go @@ -6,6 +6,7 @@ import ( "context" "errors" "fmt" + "math" "time" "go.opentelemetry.io/otel" @@ -415,21 +416,65 @@ func runBuild( return nil, err } - // Ensure the base layer is uploaded before getting the rootfs size + // Ensure queued layer uploads finish before reading the final rootfs header. err = bc.UploadErrGroup.Wait() if err != nil { return nil, fmt.Errorf("error waiting for layers upload: %w", err) } - // Get the base rootfs size from the template files - // This is the size of the rootfs after provisioning and before building the layers - // (as they don't change the rootfs size) + // Read the final rootfs header once, then use its exact logical size for both + // enforcement and the floored MiB result returned to the API. + enforceMaxDiskSize := builder.featureFlags.BoolFlag(ctx, featureflags.BuildEnforceMaxDiskSize) rootfsSize, err := getRootfsSize(ctx, builder.templateStorage, storage.Paths{BuildID: lastLayerResult.Metadata.Template.BuildID}) if err != nil { return nil, fmt.Errorf("error getting rootfs size: %w", err) } logger.L().Info(ctx, "rootfs size", zap.Uint64("size", rootfsSize)) + var maxDiskSizeMB int64 + if bc.Config.MaxDiskSizeMB != nil { + maxDiskSizeMB = *bc.Config.MaxDiskSizeMB + } + validMaxDiskSize := maxDiskSizeMB > 0 && maxDiskSizeMB <= units.BytesToMB(math.MaxInt64) + exceededMaxDiskSize := validMaxDiskSize && rootfsSize > uint64(units.MBToBytes(maxDiskSizeMB)) + rejected := exceededMaxDiskSize && enforceMaxDiskSize + span.SetAttributes( + attribute.Int64("template.disk.limit.max_mb", maxDiskSizeMB), + attribute.Int64("template.disk.limit.final_total_bytes", int64(rootfsSize)), + attribute.Bool("template.disk.limit.ld_enabled", enforceMaxDiskSize), + attribute.Int64("template.disk.requested_free_mb", bc.Config.DiskSizeMB), + attribute.Bool("template.disk.limit.rejected", rejected), + ) + if exceededMaxDiskSize { + builder.logger.Warn(ctx, "template build exceeds total disk size limit", + logger.WithTeamID(bc.Config.TeamID), + logger.WithTemplateID(bc.Config.TemplateID), + logger.WithBuildID(bc.Template.BuildID), + zap.Int64("template.disk.limit.max_mb", maxDiskSizeMB), + zap.Uint64("template.disk.limit.final_total_bytes", rootfsSize), + zap.Bool("template.disk.limit.ld_enabled", enforceMaxDiskSize), + zap.Int64("template.disk.requested_free_mb", bc.Config.DiskSizeMB), + zap.Bool("template.disk.limit.rejected", rejected), + ) + } + + if rejected { + displayRootfsSizeMB := rootfsSize >> units.MBShift + if rootfsSize&((uint64(1)< Date: Tue, 14 Jul 2026 16:54:51 +0000 Subject: [PATCH 3/3] chore: auto-commit generated changes --- packages/db/pkg/auth/queries/models.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/db/pkg/auth/queries/models.go b/packages/db/pkg/auth/queries/models.go index d2e9b7f818..adf9800615 100644 --- a/packages/db/pkg/auth/queries/models.go +++ b/packages/db/pkg/auth/queries/models.go @@ -51,6 +51,6 @@ type TeamLimitsV2 struct { MaxRamMb int32 DiskMb int32 EventsTtlDays int32 - DefaultFreeDiskSizeMb int64 - MaxDiskSizeMb int64 + DefaultFreeDiskSizeMb *int64 + MaxDiskSizeMb *int64 }