Skip to content

feat(api): associate VPC prefixes with its SitePrefix "parents" - #4634

Merged
chet merged 1 commit into
NVIDIA:mainfrom
chet:gh-issue-3886
Aug 8, 2026
Merged

feat(api): associate VPC prefixes with its SitePrefix "parents"#4634
chet merged 1 commit into
NVIDIA:mainfrom
chet:gh-issue-3886

Conversation

@chet

@chet chet commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Historically, SitePrefixes were only a configured containment guardrail. When Core created a VpcPrefix, it only needed to ask whether the CIDR fit inside any configured site root. VPC prefixes were globally unique, so there was no reason to retain which root matched.

Tenant-managed SitePrefixes change that. Different tenants may own separate SitePrefix resources with the same RFC1918 CIDR, and an operator-managed root may overlap a tenant-managed one. Containment can tell us that a VpcPrefix fits, but it can no longer tell us which SitePrefix authorized it.

This adds an optional site_prefix_id relationship to VpcPrefix persistence and gRPC. Core locks and validates the selected SitePrefix for tenant ownership, readiness, routing scope, containment, and FNN compatibility. NetworkPrefix allocation also uses the exact vpc_prefix_id instead of rediscovering its VpcPrefix from the CIDR.

The rollout remains additive:

  • Existing rows adopt an operator-managed SitePrefix only when exactly one contains them; missing or ambiguous matches stay visible for startup preflight instead of being guessed.
  • Existing callers may omit site_prefix_id and automatically use one uniquely matching operator-managed root.
  • Sites without persisted SitePrefix authority keep the existing configuration-backed containment path.
  • site_prefix_id remains nullable in PostgreSQL and optional in protobuf for mixed-version rollouts.
  • Global VPC and segment overlap constraints remain unchanged.

This establishes the SitePrefix -> VpcPrefix -> NetworkPrefix relationship needed by the tenant-managed workflow. It does not relax overlap, change peering or routing policy, or complete tenant-managed SitePrefix enablement by itself.

It also makes an IPv4 /31 VpcPrefix represent one usable generated linknet, without allowing directly created /31 NetworkSegments.

Related issues

This supports #3886.

It is part of #3883.

Type of Change

  • Add - New feature or capability
  • Change - Changes in existing functionality
  • Fix - Bug fixes
  • Remove - Removed features or deprecated functionality
  • Internal - Internal changes (refactoring, tests, docs, etc.)

Breaking Changes

  • This PR contains breaking changes

Testing

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing performed
  • No testing required (docs, internal refactor, etc.)

Tested with:

  • cargo test -p carbide-api-db -p carbide-api-core --lib -- 1,599 runnable Core tests passed with 4 ignored, and all 357 database tests passed.
  • cargo make check-format-nightly
  • cargo make clippy
  • cargo make carbide-lints
  • cargo make --no-workspace generate-rest-core-proto
  • go test ./proto/core/gen/v1 from rest-api/
  • bash scripts/check-migration-filenames.sh --base origin/main

@chet
chet requested a review from a team as a code owner August 5, 2026 22:43
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Summary by CodeRabbit

  • New Features
    • VPC prefixes now support optional SitePrefix associations and search filtering.
    • Creation validates SitePrefix ownership, readiness, routing, containment, and compatibility.
    • IPv4 /31 VPC prefixes can now be allocated where applicable.
  • Bug Fixes
    • Prevented virtualization changes that would disconnect tenant-managed address space.
    • Added startup checks to detect unresolved VPC prefix lineage.
  • Tests
    • Expanded coverage for allocation limits, SitePrefix validation, concurrency, migration, and virtualization safeguards.

Walkthrough

VPC prefixes now support optional SitePrefix lineage across storage, APIs, CLI, and REST. Creation validates attachment and concurrency rules. Startup verifies lineage. Allocation supports IPv4 /31 roots with lineage-aware occupancy. Virtualization preserves tenant-managed FNN requirements.

Changes

VPC prefix lineage

Layer / File(s) Summary
Lineage contracts and persistence
crates/api-db/migrations/..., crates/api-db/src/vpc_prefix.rs, crates/api-model/..., crates/rpc/..., rest-api/proto/...
Adds nullable site_prefix_id storage, migration backfill, model fields, API fields, search filtering, persistence, and RPC conversion.
Attachment resolution and lifecycle validation
crates/api-db/src/site_prefix.rs, crates/api-core/src/handlers/vpc_prefix.rs, crates/api-core/src/handlers/vpc.rs, crates/api-core/src/setup.rs, crates/api-core/src/tests/vpc_prefix.rs
Adds locked SitePrefix resolution, ownership and lifecycle checks, legacy parent inference, lineage backfill, startup validation, virtualization safeguards, and concurrency coverage.
Lineage-aware allocation and occupancy
crates/api-db/src/network_prefix.rs, crates/api-db/src/vpc_prefix.rs, crates/api-core/src/network_segment/allocate.rs, crates/api-core/src/tests/instance.rs
Uses exact-parent and unassigned occupancy queries, centralizes interval calculations, and permits the IPv4 /31 root allocation case.
Compatibility wiring
crates/admin-cli/..., crates/api-core/src/tests/instance_config_update.rs, crates/api-web/..., crates/vpc-prefix-controller/...
Adds CLI parsing and request forwarding. Existing callers and fixtures explicitly set omitted SitePrefix identifiers to None.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant VpcPrefixHandler
  participant SitePrefixQueries
  participant VpcPrefixDatabase
  Client->>VpcPrefixHandler: create VPC prefix with site_prefix_id
  VpcPrefixHandler->>SitePrefixQueries: validate and lock SitePrefix
  SitePrefixQueries->>VpcPrefixDatabase: persist lineage and prefix
  VpcPrefixDatabase-->>VpcPrefixHandler: return created VPC prefix
  VpcPrefixHandler-->>Client: return created prefix
Loading

Possibly related PRs

Suggested labels: networking, api, rest-api

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
Title check ✅ Passed The title clearly identifies the main change: associating VPC prefixes with SitePrefix parents.
Description check ✅ Passed The description directly explains SitePrefix association, validation, migration, compatibility, allocation changes, and testing.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@chet

chet commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

@chet I will perform a complete review of #4634. 🐇

✅ Action performed

Full review finished.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

🔐 TruffleHog Secret Scan

No secrets or credentials found!

Your code has been scanned for 700+ types of secrets and credentials. All clear! 🎉

🔗 View scan details

🕐 Last updated: 2026-08-05 22:46:52 UTC | Commit: bf79e9b

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
crates/api-db/migrations/20260805131227_associate_vpc_prefix_site_prefix.sql (1)

12-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider splitting the VALIDATE CONSTRAINT into a separate migration.

sqlx::migrate!() runs each migration in one transaction. Adding the foreign key as NOT VALID and validating it in the same transaction produces the same lock profile as adding a validated constraint directly, so the two-step form provides no benefit here. This repository already applies the split pattern: 20260804153414_remove_secondary_vtep_data.sql performs the data change and 20260804171948_validate_secondary_vtep_constraint.sql validates afterwards.

Two options are acceptable:

  • Move line 38-39 into a follow-up migration, matching the existing precedent.
  • Drop NOT VALID and add the constraint as validated, which makes the single-transaction lock behaviour explicit.

The current table size makes either choice safe today; the concern is clarity for future readers and larger deployments.

🤖 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 `@crates/api-db/migrations/20260805131227_associate_vpc_prefix_site_prefix.sql`
around lines 12 - 39, Separate validation of
network_vpc_prefixes_site_prefix_id_fkey from the migration that adds and
backfills it, creating a follow-up migration that runs VALIDATE CONSTRAINT and
removing the in-transaction validation from the current migration. Follow the
existing split pattern used by the secondary VTEP constraint migrations.

Source: Linters/SAST tools

crates/api-core/src/setup.rs (1)

327-337: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Pass the pool directly to the read-only preflight.

find_unassigned_vpc_prefix_site_prefix_ids accepts impl DbReader<'_>, so this read-only check does not need a transaction. The current form opens a transaction, and on the eyre::ensure! failure path it never reaches txn.rollback(), relying on drop instead. Using the pool removes both the transaction and that asymmetric cleanup path.

Consider also gating on carbide_config.listen_only instead of seed_data.is_none(), which states the intent directly.

♻️ Proposed simplification
-    if seed_data.is_none() && !carbide_config.site_fabric_prefixes.is_empty() {
-        let mut txn = Transaction::begin(&db_pool).await?;
-        let unassigned =
-            db::site_prefix::find_unassigned_vpc_prefix_site_prefix_ids(&mut txn).await?;
+    if carbide_config.listen_only && !carbide_config.site_fabric_prefixes.is_empty() {
+        let unassigned =
+            db::site_prefix::find_unassigned_vpc_prefix_site_prefix_ids(&db_pool).await?;
         eyre::ensure!(
             unassigned.is_empty(),
             "VpcPrefix SitePrefix lineage preflight failed: unassigned VpcPrefix IDs: {:?}",
             unassigned,
         );
-        txn.rollback().await?;
     }

As per coding guidelines: "For read-only database functions, accept impl DbReader so callers can use a pool without unnecessarily opening and committing a transaction."

🤖 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 `@crates/api-core/src/setup.rs` around lines 327 - 337, Update the read-only
preflight around find_unassigned_vpc_prefix_site_prefix_ids to pass db_pool
directly, removing the explicit Transaction::begin, rollback, and related
mutable transaction handling. Gate this check on carbide_config.listen_only
instead of seed_data.is_none() to express the intended mode directly, while
preserving the existing unassigned-ID validation and error behavior.

Source: Coding guidelines

🤖 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.

Inline comments:
In `@crates/admin-cli/src/vpc_prefix/create/args.rs`:
- Line 113: Add an optional site_prefix_id argument to the create command’s
argument definitions and pass it through instead of hardcoding site_prefix_id:
None. Document the new --site-prefix-id option in the EXAMPLES section and
regenerate the CLI reference using the project’s standard generation workflow.

---

Nitpick comments:
In `@crates/api-core/src/setup.rs`:
- Around line 327-337: Update the read-only preflight around
find_unassigned_vpc_prefix_site_prefix_ids to pass db_pool directly, removing
the explicit Transaction::begin, rollback, and related mutable transaction
handling. Gate this check on carbide_config.listen_only instead of
seed_data.is_none() to express the intended mode directly, while preserving the
existing unassigned-ID validation and error behavior.

In
`@crates/api-db/migrations/20260805131227_associate_vpc_prefix_site_prefix.sql`:
- Around line 12-39: Separate validation of
network_vpc_prefixes_site_prefix_id_fkey from the migration that adds and
backfills it, creating a follow-up migration that runs VALIDATE CONSTRAINT and
removing the in-transaction validation from the current migration. Follow the
existing split pattern used by the secondary VTEP constraint migrations.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f34a69c0-f978-4353-bcd9-ba25aba0fd6f

📥 Commits

Reviewing files that changed from the base of the PR and between 424d38e and bf79e9b.

⛔ Files ignored due to path filters (1)
  • rest-api/proto/core/gen/v1/nico_nico.pb.go is excluded by !**/*.pb.go, !**/gen/**, !rest-api/**/*.pb.go
📒 Files selected for processing (20)
  • crates/admin-cli/src/devenv/config/apply/cmd.rs
  • crates/admin-cli/src/vpc_prefix/create/args.rs
  • crates/api-core/src/handlers/vpc.rs
  • crates/api-core/src/handlers/vpc_prefix.rs
  • crates/api-core/src/network_segment/allocate.rs
  • crates/api-core/src/setup.rs
  • crates/api-core/src/tests/instance.rs
  • crates/api-core/src/tests/instance_config_update.rs
  • crates/api-core/src/tests/vpc_prefix.rs
  • crates/api-db/migrations/20260805131227_associate_vpc_prefix_site_prefix.sql
  • crates/api-db/src/migrations/mod.rs
  • crates/api-db/src/network_prefix.rs
  • crates/api-db/src/site_prefix.rs
  • crates/api-db/src/vpc_prefix.rs
  • crates/api-model/src/vpc_prefix.rs
  • crates/api-web/src/tests/vpc.rs
  • crates/rpc/proto/forge.proto
  • crates/rpc/src/model/vpc_prefix.rs
  • crates/vpc-prefix-controller/src/io.rs
  • rest-api/proto/core/src/v1/nico_nico.proto

Comment thread crates/admin-cli/src/vpc_prefix/create/args.rs Outdated

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (7)
crates/api-db/src/vpc_prefix.rs (1)

583-586: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Accept impl DbReader for this read-only lookup.

has_tenant_managed_site_prefix performs a single EXISTS read. The api-db convention reserves &mut PgConnection for writes and mutation-scoped reads, so a read-only helper should accept impl DbReader<'_>. The signature still accepts &mut *txn from the existing caller in crates/api-core/src/handlers/vpc.rs, which keeps the locked-snapshot semantics intact, while a pool-based caller becomes possible without opening a transaction.

Note that impl DbReader<'_> consumes the executor, so the caller must pass txn.as_mut() or &mut *txn rather than &mut txn.

♻️ Proposed signature change
 pub async fn has_tenant_managed_site_prefix(
-    txn: &mut PgConnection,
+    txn: impl DbReader<'_>,
     vpc_id: VpcId,
 ) -> Result<bool, DatabaseError> {

As per coding guidelines: "For read-only database functions, accept impl DbReader so callers can use a pool without unnecessarily opening and committing a transaction."

🤖 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 `@crates/api-db/src/vpc_prefix.rs` around lines 583 - 586, Update the signature
of has_tenant_managed_site_prefix to accept impl DbReader<'_> instead of &mut
PgConnection, preserving its read-only EXISTS behavior and Result<bool,
DatabaseError> return type. Adjust the existing caller to pass the executor as
txn.as_mut() or &mut *txn, not &mut txn, so locked-snapshot semantics remain
unchanged while pool-based callers are supported.

Source: Coding guidelines

crates/rpc/src/model/vpc_prefix.rs (1)

313-368: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Strong contract guard. Consider extending it to the proto mirror.

This test pins the forge tag numbers, the retained reserved 5 range, and the legacy tenant_prefix_id = 2. That is the right level of enforcement for wire compatibility.

The same three tags also exist in rest-api/proto/core/src/v1/nico_nico.proto at 11, 8, and 7. That file is not part of this crate's descriptor set, so nothing here prevents the two definitions from drifting apart. If a mirror-consistency check does not already exist elsewhere, consider adding one so a future tag change to one file cannot silently diverge from the other.

🤖 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 `@crates/rpc/src/model/vpc_prefix.rs` around lines 313 - 368, Extend the
contract coverage around the test site_prefix_fields_use_fresh_protobuf_tags to
validate the corresponding VpcPrefix, VpcPrefixCreationRequest, and
VpcPrefixSearchQuery definitions in the mirrored nico_nico.proto schema. Ensure
their site_prefix_id tags remain 11, 8, and 7 respectively, and fail when the
mirror diverges from the forge definitions; reuse any existing proto
mirror-consistency mechanism if available.
crates/api-db/migrations/20260805131227_associate_vpc_prefix_site_prefix.sql (1)

38-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider splitting VALIDATE CONSTRAINT into a follow-up migration.

This migration adds the constraint as NOT VALID and validates it in the same transaction. The repository already established the split pattern with 20260804153414_remove_secondary_vtep_data.sql and 20260804171948_validate_secondary_vtep_constraint.sql.

The practical impact is limited here. The preceding ALTER TABLE ... ADD COLUMN already holds ACCESS EXCLUSIVE on network_vpc_prefixes until commit, and network_vpc_prefixes is a small inventory table. Treat this as consistency with the established convention rather than a lock-duration fix.

🤖 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 `@crates/api-db/migrations/20260805131227_associate_vpc_prefix_site_prefix.sql`
around lines 38 - 39, Remove the VALIDATE CONSTRAINT statement from this
migration’s network_vpc_prefixes changes, leaving the foreign key added as NOT
VALID. Add a subsequent migration dedicated to validating
network_vpc_prefixes_site_prefix_id_fkey, following the established split
pattern used by the secondary VTEP migrations.

Source: Linters/SAST tools

crates/api-core/src/setup.rs (1)

328-336: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the needless transaction around a read-only query.

db::site_prefix::find_unassigned_vpc_prefix_site_prefix_ids accepts impl DbReader<'_>. Pass &db_pool directly. The current code opens a transaction and then rolls it back, and the early return on the eyre::ensure! failure path relies on Drop to release it.

♻️ Proposed simplification
     if seed_data.is_none() && !carbide_config.site_fabric_prefixes.is_empty() {
-        let mut txn = Transaction::begin(&db_pool).await?;
-        let unassigned =
-            db::site_prefix::find_unassigned_vpc_prefix_site_prefix_ids(&mut txn).await?;
+        let unassigned =
+            db::site_prefix::find_unassigned_vpc_prefix_site_prefix_ids(&db_pool).await?;
         eyre::ensure!(
             unassigned.is_empty(),
             "VpcPrefix SitePrefix lineage preflight failed: unassigned VpcPrefix IDs: {:?}",
             unassigned,
         );
-        txn.rollback().await?;
     }

As per coding guidelines: "For read-only database functions, accept impl DbReader so callers can use a pool without unnecessarily opening and committing a transaction."

🤖 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 `@crates/api-core/src/setup.rs` around lines 328 - 336, Remove the unnecessary
Transaction::begin and rollback flow around the read-only preflight query; call
db::site_prefix::find_unassigned_vpc_prefix_site_prefix_ids directly with
&db_pool, preserving the existing eyre::ensure! validation and error message.

Source: Coding guidelines

crates/api-core/src/tests/vpc_prefix.rs (2)

1164-1389: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split this test so a failure identifies the broken property.

This single test now asserts eight independent contracts: successful attachment, get and search round-tripping, tenant-ownership rejection, omitted-ID rejection under a tenant root, three lifecycle rejections, containment rejection, non-FNN rejection, /31 allocation and exhaustion, and legacy operator-root precedence. The function spans about 225 lines and shares one env across all of them.

Two consequences. First, the first failing assertion hides every later contract, so one regression masks the rest. Second, a reported failure names only a line number, not the property.

Extract at least the two self-contained tails into their own tests: the /31 allocation case at lines 1322-1354 and the legacy operator-root precedence case at lines 1356-1386. Keep the existing table at lines 1253-1282 as it is; it already groups the lifecycle cases well.

🤖 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 `@crates/api-core/src/tests/vpc_prefix.rs` around lines 1164 - 1389, Split the
`/31` allocation and exhaustion scenario into a separate test, and move the
legacy operator-root precedence scenario into another self-contained test with
its own environment and fixture setup. Keep the existing lifecycle-state table
in `exact_site_prefix_attachment_enforces_lineage_and_round_trips` unchanged,
while preserving each extracted test’s assertions and required setup.

140-170: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Narrow the lock-wait match so the synchronisation stays trustworthy.

wait_until_query_is_blocked_by matches the blocked statement with ILIKE '%' || $2 || '%'. Two callers pass very broad fragments: "site_prefixes" at line 1597 and "vpcs" at line 1721. Today only the spawned request can wait on those locks, so the tests are correct. If a later change introduces any other lock wait touching those tables, the helper returns the wrong pid and the test continues before the intended statement blocks. The failure would then be a confusing assertion mismatch rather than a clear timeout.

Pass the distinctive statement text instead, for example the FOR UPDATE/FOR SHARE fragment or the advisory-lock function name that the code under test actually issues. The other two callers already do this with "site_prefixes:tenant:" and "pg_advisory_xact_lock_shared".

🤖 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 `@crates/api-core/src/tests/vpc_prefix.rs` around lines 140 - 170, Narrow the
query fragments passed to wait_until_query_is_blocked_by at the callers
currently using broad "site_prefixes" and "vpcs" matches. Replace them with
distinctive SQL text issued by the blocked statements, such as their FOR
UPDATE/FOR SHARE clause or advisory-lock function name, while preserving the
existing specific fragments used by the other callers.
crates/vpc-prefix-controller/src/io.rs (1)

55-62: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add list_objects coverage for both lineage cases. site_prefix_id: None is intentionally unfiltered because search adds that predicate only for Some(...). An integration assertion should ensure reconciliation includes prefixes with and without site_prefix_id.

🤖 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 `@crates/vpc-prefix-controller/src/io.rs` around lines 55 - 62, Add integration
coverage for list_objects reconciliation that includes two VPC prefixes: one
with a site_prefix_id and one without it. Assert both lineage cases are
returned, preserving site_prefix_id: None as an intentionally unfiltered search
input in the db::vpc_prefix::search call.
🤖 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 `@crates/api-core/src/setup.rs`:
- Around line 328-336: Remove the unnecessary Transaction::begin and rollback
flow around the read-only preflight query; call
db::site_prefix::find_unassigned_vpc_prefix_site_prefix_ids directly with
&db_pool, preserving the existing eyre::ensure! validation and error message.

In `@crates/api-core/src/tests/vpc_prefix.rs`:
- Around line 1164-1389: Split the `/31` allocation and exhaustion scenario into
a separate test, and move the legacy operator-root precedence scenario into
another self-contained test with its own environment and fixture setup. Keep the
existing lifecycle-state table in
`exact_site_prefix_attachment_enforces_lineage_and_round_trips` unchanged, while
preserving each extracted test’s assertions and required setup.
- Around line 140-170: Narrow the query fragments passed to
wait_until_query_is_blocked_by at the callers currently using broad
"site_prefixes" and "vpcs" matches. Replace them with distinctive SQL text
issued by the blocked statements, such as their FOR UPDATE/FOR SHARE clause or
advisory-lock function name, while preserving the existing specific fragments
used by the other callers.

In
`@crates/api-db/migrations/20260805131227_associate_vpc_prefix_site_prefix.sql`:
- Around line 38-39: Remove the VALIDATE CONSTRAINT statement from this
migration’s network_vpc_prefixes changes, leaving the foreign key added as NOT
VALID. Add a subsequent migration dedicated to validating
network_vpc_prefixes_site_prefix_id_fkey, following the established split
pattern used by the secondary VTEP migrations.

In `@crates/api-db/src/vpc_prefix.rs`:
- Around line 583-586: Update the signature of has_tenant_managed_site_prefix to
accept impl DbReader<'_> instead of &mut PgConnection, preserving its read-only
EXISTS behavior and Result<bool, DatabaseError> return type. Adjust the existing
caller to pass the executor as txn.as_mut() or &mut *txn, not &mut txn, so
locked-snapshot semantics remain unchanged while pool-based callers are
supported.

In `@crates/rpc/src/model/vpc_prefix.rs`:
- Around line 313-368: Extend the contract coverage around the test
site_prefix_fields_use_fresh_protobuf_tags to validate the corresponding
VpcPrefix, VpcPrefixCreationRequest, and VpcPrefixSearchQuery definitions in the
mirrored nico_nico.proto schema. Ensure their site_prefix_id tags remain 11, 8,
and 7 respectively, and fail when the mirror diverges from the forge
definitions; reuse any existing proto mirror-consistency mechanism if available.

In `@crates/vpc-prefix-controller/src/io.rs`:
- Around line 55-62: Add integration coverage for list_objects reconciliation
that includes two VPC prefixes: one with a site_prefix_id and one without it.
Assert both lineage cases are returned, preserving site_prefix_id: None as an
intentionally unfiltered search input in the db::vpc_prefix::search call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f43a2c95-b3db-48d0-9a46-622c6c5b0d57

📥 Commits

Reviewing files that changed from the base of the PR and between 424d38e and bf79e9b.

⛔ Files ignored due to path filters (1)
  • rest-api/proto/core/gen/v1/nico_nico.pb.go is excluded by !**/*.pb.go, !**/gen/**, !rest-api/**/*.pb.go
📒 Files selected for processing (20)
  • crates/admin-cli/src/devenv/config/apply/cmd.rs
  • crates/admin-cli/src/vpc_prefix/create/args.rs
  • crates/api-core/src/handlers/vpc.rs
  • crates/api-core/src/handlers/vpc_prefix.rs
  • crates/api-core/src/network_segment/allocate.rs
  • crates/api-core/src/setup.rs
  • crates/api-core/src/tests/instance.rs
  • crates/api-core/src/tests/instance_config_update.rs
  • crates/api-core/src/tests/vpc_prefix.rs
  • crates/api-db/migrations/20260805131227_associate_vpc_prefix_site_prefix.sql
  • crates/api-db/src/migrations/mod.rs
  • crates/api-db/src/network_prefix.rs
  • crates/api-db/src/site_prefix.rs
  • crates/api-db/src/vpc_prefix.rs
  • crates/api-model/src/vpc_prefix.rs
  • crates/api-web/src/tests/vpc.rs
  • crates/rpc/proto/forge.proto
  • crates/rpc/src/model/vpc_prefix.rs
  • crates/vpc-prefix-controller/src/io.rs
  • rest-api/proto/core/src/v1/nico_nico.proto

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@chet

chet commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

@chet I will perform a complete review of #4634.

🐇

✅ Action performed

Full review finished.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
crates/api-db/src/site_prefix.rs (1)

592-621: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider a single set-based update instead of one statement per row.

The loop issues one UPDATE ... RETURNING round trip per resolvable VpcPrefix. On a site with many legacy rows this multiplies startup latency inside an already open transaction. A single statement with UNNEST($1::uuid[], $2::uuid[]) would assign all unique matches at once and return the assigned IDs.

🤖 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 `@crates/api-db/src/site_prefix.rs` around lines 592 - 621, The lineage
assignment currently performs one database update per resolvable VPC prefix;
replace the per-row loop and `update_query` execution with a single set-based
update using `UNNEST` over paired VPC-prefix and site-prefix ID arrays,
preserving the existing `site_prefix_id IS NULL` guard and collecting all
returned IDs into `report.assigned_vpc_prefix_ids`. Keep missing and ambiguous
candidate reporting unchanged.
🤖 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 `@crates/api-db/src/site_prefix.rs`:
- Around line 592-621: The lineage assignment currently performs one database
update per resolvable VPC prefix; replace the per-row loop and `update_query`
execution with a single set-based update using `UNNEST` over paired VPC-prefix
and site-prefix ID arrays, preserving the existing `site_prefix_id IS NULL`
guard and collecting all returned IDs into `report.assigned_vpc_prefix_ids`.
Keep missing and ambiguous candidate reporting unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: ea8094de-2575-4748-861b-78531b452afe

📥 Commits

Reviewing files that changed from the base of the PR and between 5af64fc and 10ed3e2.

⛔ Files ignored due to path filters (1)
  • rest-api/proto/core/gen/v1/nico_nico.pb.go is excluded by !**/*.pb.go, !**/gen/**, !rest-api/**/*.pb.go
📒 Files selected for processing (21)
  • crates/admin-cli/src/devenv/config/apply/cmd.rs
  • crates/admin-cli/src/vpc_prefix/create/args.rs
  • crates/admin-cli/src/vpc_prefix/tests.rs
  • crates/api-core/src/handlers/vpc.rs
  • crates/api-core/src/handlers/vpc_prefix.rs
  • crates/api-core/src/network_segment/allocate.rs
  • crates/api-core/src/setup.rs
  • crates/api-core/src/tests/instance.rs
  • crates/api-core/src/tests/instance_config_update.rs
  • crates/api-core/src/tests/vpc_prefix.rs
  • crates/api-db/migrations/20260805131227_associate_vpc_prefix_site_prefix.sql
  • crates/api-db/src/migrations/mod.rs
  • crates/api-db/src/network_prefix.rs
  • crates/api-db/src/site_prefix.rs
  • crates/api-db/src/vpc_prefix.rs
  • crates/api-model/src/vpc_prefix.rs
  • crates/api-web/src/tests/vpc.rs
  • crates/rpc/proto/forge.proto
  • crates/rpc/src/model/vpc_prefix.rs
  • crates/vpc-prefix-controller/src/io.rs
  • rest-api/proto/core/src/v1/nico_nico.proto
🚧 Files skipped from review as they are similar to previous changes (17)
  • crates/vpc-prefix-controller/src/io.rs
  • crates/api-web/src/tests/vpc.rs
  • crates/api-core/src/handlers/vpc.rs
  • crates/api-core/src/setup.rs
  • crates/rpc/proto/forge.proto
  • crates/api-core/src/handlers/vpc_prefix.rs
  • crates/rpc/src/model/vpc_prefix.rs
  • crates/api-model/src/vpc_prefix.rs
  • crates/api-db/src/migrations/mod.rs
  • crates/api-core/src/tests/instance.rs
  • rest-api/proto/core/src/v1/nico_nico.proto
  • crates/api-core/src/tests/instance_config_update.rs
  • crates/api-db/src/vpc_prefix.rs
  • crates/api-db/src/network_prefix.rs
  • crates/api-core/src/tests/vpc_prefix.rs
  • crates/api-core/src/network_segment/allocate.rs
  • crates/admin-cli/src/devenv/config/apply/cmd.rs

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (5)
crates/admin-cli/src/vpc_prefix/create/args.rs (1)

52-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the extended explanation into long_help.

The help value contains three sentences. Clap renders help inline in the --help table, so this argument will produce a very wide row next to the single-line help of every sibling argument. Clap renders long_help only for --help on the subcommand in long form, which is the correct place for the selection rules.

♻️ Proposed split
     #[clap(
         long,
         name = "site-prefix-id",
         value_name = "SitePrefixId",
-        help = "The exact parent SitePrefix ID. Required for tenant-managed SitePrefixes or when multiple operator-managed SitePrefixes contain this VPC prefix. When omitted, Core selects the unique containing operator-managed SitePrefix when one exists"
+        help = "The exact parent SitePrefix ID",
+        long_help = "The exact parent SitePrefix ID. Required for tenant-managed SitePrefixes, or when multiple operator-managed SitePrefixes contain this VPC prefix. When omitted, Core selects the unique containing operator-managed SitePrefix if one exists"
     )]
     site_prefix_id: Option<SitePrefixId>,

As per path instructions: "Review CLI changes for clap behavior, actionable operator-facing error messages, realistic help examples".

🤖 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 `@crates/admin-cli/src/vpc_prefix/create/args.rs` around lines 52 - 58, Update
the site_prefix_id clap attribute so help contains only a concise single-line
description suitable for the inline options table, and move the full parent
SitePrefix selection rules into long_help. Preserve the existing argument name,
type, and semantics while ensuring detailed guidance appears only in the
subcommand’s long-form help.

Source: Path instructions

crates/api-core/src/setup.rs (1)

300-309: 🩺 Stability & Availability | 🔵 Trivial

Emit the unresolved lineage before the process aborts.

The writer path fails startup when any VPC prefix remains unresolved. The rollback discards the partial assignments, so the next boot repeats the same work. The only signal is the eyre chain, which is easy to truncate in aggregated logs.

Log the report at error level with structured fields before eyre::ensure!, so operators can identify the affected rows without re-running startup. Example fields: unresolved_vpc_prefix_count, missing_vpc_prefix_ids, ambiguous_vpc_prefix_count.

🤖 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 `@crates/api-core/src/setup.rs` around lines 300 - 309, The site-prefix lineage
preflight must emit unresolved details before aborting. In the setup block
around backfill_vpc_prefix_site_prefix_lineage, when
unresolved_vpc_prefix_count() is nonzero, log an error with structured fields
for the unresolved count, missing_vpc_prefix_ids, and ambiguous VPC prefix
count, then retain the existing eyre::ensure! failure behavior.
crates/api-core/src/tests/vpc_prefix.rs (2)

1592-1602: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a more specific blocked-query fragment.

The fragment "site_prefixes" matches any blocked statement that mentions the table. In this test the creation path issues both an advisory-lock call and a row read against site_prefixes. The helper can therefore return as soon as the first of them waits, which is not necessarily the lock this test intends to observe. That makes the ordering assertion sensitive to future changes in the handler.

Match the exact statement the test targets, as the sibling tests already do with "site_prefixes:tenant:" and "pg_advisory_xact_lock_shared".

♻️ Suggested tightening
-    wait_until_query_is_blocked_by(&env.pool, blocker_pid, "site_prefixes").await;
+    // The handler resolves an explicit parent with a shared row lock.
+    wait_until_query_is_blocked_by(&env.pool, blocker_pid, "FROM site_prefixes WHERE id =").await;
🤖 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 `@crates/api-core/src/tests/vpc_prefix.rs` around lines 1592 - 1602, Update the
wait_until_query_is_blocked_by call in this VPC prefix creation test to use a
specific fragment identifying the intended blocked advisory-lock statement,
rather than the broad "site_prefixes" table fragment. Match the existing
sibling-test convention, such as the tenant-specific site-prefix fragment
together with "pg_advisory_xact_lock_shared", while leaving the retirement and
assertion flow unchanged.

1164-1389: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Split this test into focused cases.

exact_site_prefix_attachment_enforces_lineage_and_round_trips covers eight independent behaviors: successful attachment, persistence, search filtering, tenant ownership, omitted-ID rejection, lifecycle admission, containment, virtualization type, /31 allocation, and legacy operator-root inference. A single failure reports one test name, so the failing behavior is not identifiable from the test result alone.

The lifecycle loop at lines 1253-1282 already uses the table form. Extend that approach: keep one test per behavior, or move the remaining single-assertion cases into value_scenarios!/scenarios! tables. At minimum, add a short comment above the block at lines 1284-1294, which currently relies on the reader noticing that 10.46.1.0/24 is outside 10.40.0.0/16.

As per coding guidelines: "Use table-driven tests for operations mapping inputs to outputs or errors."

🤖 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 `@crates/api-core/src/tests/vpc_prefix.rs` around lines 1164 - 1389, Split
exact_site_prefix_attachment_enforces_lineage_and_round_trips into focused tests
or table-driven scenarios so each attachment behavior reports independently,
covering successful round-trip/search, tenant ownership, omitted IDs, lifecycle
states, containment, virtualization type, /31 allocation, and legacy
operator-root inference. Preserve the existing assertions and setup semantics;
at minimum, add a concise comment explaining the out-of-range 10.46.1.0/24
containment case if it remains in a combined block.

Source: Coding guidelines

crates/api-core/src/handlers/vpc_prefix.rs (1)

129-185: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the legacy resolution branch into a named function.

The else block spans roughly 55 lines and nests a three-arm match with two lock acquisitions, a VPC lookup, and two early returns inside a let binding. create is already a long function, and this block is the densest part of it.

An extraction such as resolve_legacy_site_prefix(txn, &new_prefix) -> CarbideResult<Option<SitePrefix>> would keep create linear and give the legacy compatibility rule a name that matches the comment at Line 173. The behavior is correct as written, so this is a readability improvement only.

🤖 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 `@crates/api-core/src/handlers/vpc_prefix.rs` around lines 129 - 185, Extract
the entire legacy-resolution else branch from create into a named helper such as
resolve_legacy_site_prefix, accepting the transaction and new prefix and
returning CarbideResult<Option<SitePrefix>>. Move its locks, VPC lookup,
tenant-managed containment check, candidate matching, and existing errors
unchanged; have create call the helper so its control flow remains linear.
🤖 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 `@crates/admin-cli/src/vpc_prefix/create/args.rs`:
- Around line 52-58: Update the site_prefix_id clap attribute so help contains
only a concise single-line description suitable for the inline options table,
and move the full parent SitePrefix selection rules into long_help. Preserve the
existing argument name, type, and semantics while ensuring detailed guidance
appears only in the subcommand’s long-form help.

In `@crates/api-core/src/handlers/vpc_prefix.rs`:
- Around line 129-185: Extract the entire legacy-resolution else branch from
create into a named helper such as resolve_legacy_site_prefix, accepting the
transaction and new prefix and returning CarbideResult<Option<SitePrefix>>. Move
its locks, VPC lookup, tenant-managed containment check, candidate matching, and
existing errors unchanged; have create call the helper so its control flow
remains linear.

In `@crates/api-core/src/setup.rs`:
- Around line 300-309: The site-prefix lineage preflight must emit unresolved
details before aborting. In the setup block around
backfill_vpc_prefix_site_prefix_lineage, when unresolved_vpc_prefix_count() is
nonzero, log an error with structured fields for the unresolved count,
missing_vpc_prefix_ids, and ambiguous VPC prefix count, then retain the existing
eyre::ensure! failure behavior.

In `@crates/api-core/src/tests/vpc_prefix.rs`:
- Around line 1592-1602: Update the wait_until_query_is_blocked_by call in this
VPC prefix creation test to use a specific fragment identifying the intended
blocked advisory-lock statement, rather than the broad "site_prefixes" table
fragment. Match the existing sibling-test convention, such as the
tenant-specific site-prefix fragment together with
"pg_advisory_xact_lock_shared", while leaving the retirement and assertion flow
unchanged.
- Around line 1164-1389: Split
exact_site_prefix_attachment_enforces_lineage_and_round_trips into focused tests
or table-driven scenarios so each attachment behavior reports independently,
covering successful round-trip/search, tenant ownership, omitted IDs, lifecycle
states, containment, virtualization type, /31 allocation, and legacy
operator-root inference. Preserve the existing assertions and setup semantics;
at minimum, add a concise comment explaining the out-of-range 10.46.1.0/24
containment case if it remains in a combined block.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 1afdef69-5dd7-461b-8b53-3d0b71d76776

📥 Commits

Reviewing files that changed from the base of the PR and between 5af64fc and 10ed3e2.

⛔ Files ignored due to path filters (1)
  • rest-api/proto/core/gen/v1/nico_nico.pb.go is excluded by !**/*.pb.go, !**/gen/**, !rest-api/**/*.pb.go
📒 Files selected for processing (21)
  • crates/admin-cli/src/devenv/config/apply/cmd.rs
  • crates/admin-cli/src/vpc_prefix/create/args.rs
  • crates/admin-cli/src/vpc_prefix/tests.rs
  • crates/api-core/src/handlers/vpc.rs
  • crates/api-core/src/handlers/vpc_prefix.rs
  • crates/api-core/src/network_segment/allocate.rs
  • crates/api-core/src/setup.rs
  • crates/api-core/src/tests/instance.rs
  • crates/api-core/src/tests/instance_config_update.rs
  • crates/api-core/src/tests/vpc_prefix.rs
  • crates/api-db/migrations/20260805131227_associate_vpc_prefix_site_prefix.sql
  • crates/api-db/src/migrations/mod.rs
  • crates/api-db/src/network_prefix.rs
  • crates/api-db/src/site_prefix.rs
  • crates/api-db/src/vpc_prefix.rs
  • crates/api-model/src/vpc_prefix.rs
  • crates/api-web/src/tests/vpc.rs
  • crates/rpc/proto/forge.proto
  • crates/rpc/src/model/vpc_prefix.rs
  • crates/vpc-prefix-controller/src/io.rs
  • rest-api/proto/core/src/v1/nico_nico.proto

CIDR containment was enough to find a VpcPrefix's SitePrefix while every prefix was globally unique. Tenant-managed SitePrefixes change that: the same address space can belong to different tenants, so the exact resource ID needs to be part of the relationship.

So, this adds a nullable `site_prefix_id` to VpcPrefix persistence and the gRPC surface, backfills rows only when they have one operator-managed match, and makes startup yell about missing or ambiguous relationships. New create requests lock and validate the selected SitePrefix; legacy callers that omit the ID attach automatically when there is one operator-managed match, while sites without any SitePrefix authority keep their old behavior.

NetworkPrefix allocation and capacity now use the exact `vpc_prefix_id`. Unassigned NetworkPrefixes still reserve space globally until NVIDIA#3892 relaxes the global exclusion constraint, and an IPv4 `/31` VpcPrefix now represents one usable generated linknet.

This supports NVIDIA#3886

Signed-off-by: Chet Nichols III <chetn@nvidia.com>
@chet

chet commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full_review, thanks!

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

@chet I will perform a complete review of #4634.

⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 27 minutes.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

@chet chet changed the title feat(api): associate VPC prefixes with exact SitePrefix parents feat(api): associate VPC prefixes with its SitePrefix "parents" Aug 7, 2026
@chet
chet merged commit d77bf0f into NVIDIA:main Aug 8, 2026
122 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