Skip to content

[Fix] Enforce project quota shrink against real usage#210

Merged
HiranAdikari merged 1 commit into
controlplanefrom
fix/project-usage-quota-guard
Jul 3, 2026
Merged

[Fix] Enforce project quota shrink against real usage#210
HiranAdikari merged 1 commit into
controlplanefrom
fix/project-usage-quota-guard

Conversation

@HiranAdikari

@HiranAdikari HiranAdikari commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What this fixes

The project-quota shrink guard computed a project's in-use CPU/memory by reading resources.metadata and resources.machine_pool_size — columns that nothing on the create path ever writes. Every VM and cluster therefore summed to zero usage, and a project owner could shrink a project's quota below what its running resources actually consume without the guard firing. Quota enforcement is a stated core principle of the API, so this was a real correctness bug rather than a cosmetic one.

The fix

sumProjectResourceUsageTx now derives usage from data the create path really persists: each VM's size label and each cluster's node-pool rows (size × count, all roles), resolved through the models.Sizes catalog, which is passed into the query as unnest arrays. Bastions keep their fixed 2 CPU / 4 GB footprint, and storage stays 0 until a volumes table exists (pre-existing TODO).

Why it's safe

It is a single query rewrite inside the existing UpdateProjectQuota transaction — no schema change, no API change, no handler change. Because size labels and node-pool rows have always been persisted, the fix is retroactively correct for every existing row; no backfill is needed. A row with an unknown size label contributes 0, same as before.

Tests

The covering test previously masked the bug: it seeded metadata with a raw SQL INSERT, so it passed while the production path wrote nothing. It now creates the VM and cluster through Repository.Create (the production insert) plus real cluster_node_pools rows, so it fails if the create path ever stops writing the column the guard reads. The TestProjectCap_ suite (5 tests) now also runs in the cluster-free authz CI job on every PR. A -run pattern in that job that matches no existing test was dropped, along with its comment references.

Verified

go build ./..., go vet ./..., the full unit suite, and the nop-mode integration run (real Postgres via testcontainers) are green: 5/5 TestProjectCap_ PASS including the de-masked shrink-below-usage rejection (asserts both in_use.cpu_cores = 26 and in_use.memory_gb = 52 from an xlarge VM + small×1 and medium×2 pools). Self-reviewed: diff-scoped scanners (golangci-lint, govulncheck, gitleaks, actionlint) + multi-lens adversarial review; the adversarial pass found no defects in the query (multi-tenancy isolation, NULL/empty-project handling, and the CI filter's full match set were each verified by execution).

Not included

Storage usage still sums to 0 (needs the volumes table, pre-existing TODO). Databases and key vaults are not yet counted — their instance classes have no cpu/memory catalog to resolve against; that follow-up is now documented in the function comment. A node-pool row whose own status is failed still counts toward usage (conservative over-count; pool rows can't distinguish "never provisioned" from "running but a later update failed"). The unused resources.metadata/machine_pool_size columns are left in place — the migrator is additive-only.

Summary by CodeRabbit

  • Bug Fixes

    • Improved project quota usage calculations so project limits now reflect both virtual machine and cluster node-pool consumption more accurately.
    • Requests that reduce a project’s quota below current usage now return the correct error with updated in-use totals.
  • Tests

    • Updated authorization and project-capacity integration coverage to match the current test scope and the revised usage calculations.

The project quota shrink guard summed VM and cluster usage from
resources.metadata and resources.machine_pool_size, but nothing on
the create path ever writes those columns, so usage always computed
to zero and a project quota could be shrunk below what its running
resources actually consume.

Derive usage from data the create path really persists: the size
label on VM rows and each cluster's node-pool rows (size x count),
resolved through the models.Sizes catalog passed into the query as
unnest arrays. Bastions keep their fixed 2 CPU / 4 GB footprint and
storage stays 0 until a volumes table exists.

The covering test previously masked the bug by seeding metadata with
a raw INSERT; it now creates resources through Repository.Create so
it fails if the create path stops writing the column the guard
reads. The capacity suite now also runs in the cluster-free CI job,
and a test-name pattern that matches no test was dropped from its
filter.
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR changes how project capacity usage is calculated: sumProjectResourceUsageTx now derives CPU/memory from a size-label catalog joined against VM resources and cluster node pools, rather than metadata JSON fields. Integration tests and the authz CI workflow were updated accordingly.

Changes

Capacity usage recalculation

Layer / File(s) Summary
Size-spec based usage query
dc-api/internal/db/projects.go
sumProjectResourceUsageTx rewritten to build a size_specs CTE from models.Sizes, computing VM usage via resources.size joins and cluster usage via cluster_node_pools joins with LEFT JOIN semantics for unknown sizes; formatting cleanup in UpdateProjectQuota.
Integration test updates for composite usage
dc-api/test/integration/projects_cap_test.go, dc-api/test/integration/nop_env_test.go
Tests now create ACTIVE VM and cluster resources with node pools to derive expected in-use totals (26 CPU/52 GB), assert quota_below_usage, and update cleanup and comments accordingly.
Authz workflow test selection
.github/workflows/authz.yaml
CI comments and go test -run pattern updated to drop TestOptionD_ and include TestProjectCap_.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Test as ProjectsCapTest
  participant API as UpdateProjectQuota
  participant Calc as sumProjectResourceUsageTx
  participant DB as Database

  Test->>API: PATCH project CPU quota to 10
  API->>Calc: compute in-use CPU/memory
  Calc->>DB: query size_specs CTE joined with VM resources
  Calc->>DB: query size_specs CTE joined with cluster_node_pools
  DB-->>Calc: aggregated usage (26 CPU, 52 GB)
  Calc-->>API: TenantCapUsage
  API-->>Test: 400 quota_below_usage with in_use totals
Loading

Metadata

  • Related issues: None found
  • Related PRs: None found
  • Suggested labels: None found
  • Suggested reviewers: None found

🐰 A catalog of sizes, both small and grand,
Now guides the counting of CPU and RAM,
No more digging through metadata's sand,
Just pools and specs, computed calm.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: enforcing project quota shrink against actual resource usage.
Description check ✅ Passed The description covers the problem, fix, safety, and testing, though it doesn't use the template's Summary/Changes/Checklist headings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/project-usage-quota-guard

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
dc-api/internal/db/projects.go (1)

347-354: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a named constant for the bastion 2 CPU / 4 GB footprint.

The fixed bastion allocation is duplicated as magic numbers here and restated in the doc comment (Lines 291, 348-349). Extracting a small constant (or a models.Sizes["bastion"]-style entry) would keep the two in sync if this value ever changes.

♻️ Suggested refactor
+// bastionCPU/bastionMemGB is the fixed footprint counted per bastion resource.
+const bastionCPU = 2
+const bastionMemGB = 4
+
 		bast AS (
-			SELECT COUNT(*) * 2 AS cpu,
-			       COUNT(*) * 4 AS mem
+			SELECT COUNT(*) * $5 AS cpu,
+			       COUNT(*) * $6 AS mem
 			FROM   resources r
 			WHERE  r.project_uuid = $1
 			  AND  r.status IN ('PENDING', 'ACTIVE')
 			  AND  r.type = 'BASTION'
 		)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dc-api/internal/db/projects.go` around lines 347 - 354, The bastion resource
footprint is hardcoded as magic numbers in the allocation query, so it can drift
from the documented value. Update the logic in the project sizing query around
the bastion aggregation to use a named constant or shared size entry (for
example a dedicated bastion size in the existing sizing model) instead of
repeating 2 CPU / 4 GB inline. Make sure the same source is used by the related
doc comment and any helper that computes project sizes so the bastion allocation
stays consistent if it changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@dc-api/internal/db/projects.go`:
- Around line 347-354: The bastion resource footprint is hardcoded as magic
numbers in the allocation query, so it can drift from the documented value.
Update the logic in the project sizing query around the bastion aggregation to
use a named constant or shared size entry (for example a dedicated bastion size
in the existing sizing model) instead of repeating 2 CPU / 4 GB inline. Make
sure the same source is used by the related doc comment and any helper that
computes project sizes so the bastion allocation stays consistent if it changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 31c41562-5cb0-4b38-a465-aa1fee03576e

📥 Commits

Reviewing files that changed from the base of the PR and between 896dc6e and 3db8813.

📒 Files selected for processing (4)
  • .github/workflows/authz.yaml
  • dc-api/internal/db/projects.go
  • dc-api/test/integration/nop_env_test.go
  • dc-api/test/integration/projects_cap_test.go

@HiranAdikari
HiranAdikari merged commit d43b63a into controlplane Jul 3, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants