Skip to content

Conversation

@bcmmbaga
Copy link
Contributor

@bcmmbaga bcmmbaga commented Nov 24, 2025

Describe your changes

Issue ticket number and link

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

Summary by CodeRabbit

  • New Features

    • Full DNS Zones CRUD and DNS Records (A/AAAA/CNAME) management with REST endpoints and client support.
    • Client library APIs for DNS zones and records; store/schema additions to persist zones and records.
    • Activity events emitted for zone/record create/update/delete.
  • Bug Fixes

    • Added validation to prevent DNS domain conflicts with account settings.
  • Tests

    • Extensive unit and integration tests for zones, records, store, and client.
  • Documentation

    • OpenAPI spec and generated API types updated for DNS Zones and DNS Records.

✏️ Tip: You can customize this high-level summary in your review settings.

Signed-off-by: bcmmbaga <[email protected]>
Signed-off-by: bcmmbaga <[email protected]>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 24, 2025

Walkthrough

Adds DNS zones and DNS records: new domain/record models, CRUD HTTP APIs and REST client, store persistence, managers with permissions/validation/events, integration into network-map/account flows to apply per-account zones and peer filtering, plus tests and OpenAPI additions.

Changes

Cohort / File(s) Summary
Zones: entity & API
management/internals/modules/zones/zone.go, management/internals/modules/zones/interface.go, management/internals/modules/zones/manager/api.go
New Zone type and Manager interface; HTTP handlers to list/create/get/update/delete zones.
Zones: manager implementation
management/internals/modules/zones/manager/manager.go, management/internals/modules/zones/manager/manager_test.go
Concrete manager implementation with permission checks, transactions, domain-conflict validation, events, background peer updates; comprehensive unit tests.
DNS Records: entity & API
management/internals/modules/zones/records/record.go, management/internals/modules/zones/records/interface.go, management/internals/modules/zones/records/manager/api.go
New Record type (A/AAAA/CNAME), validation, Manager interface, HTTP handlers for record CRUD.
DNS Records: manager implementation & tests
management/internals/modules/zones/records/manager/manager.go, management/internals/modules/zones/records/manager/manager_test.go
Record manager with conflict checks, transactions, event logging, and tests covering create/update/delete and conflict cases.
Store: interface & SQL
management/server/store/store.go, management/server/store/sql_store.go, management/server/store/sql_store_test.go
Store interface extended and SQL implementation added for zones and records (CRUD, queries, locking); tests for lifecycle and record queries.
Network-map controller & repository
management/internals/controllers/network_map/controller/controller.go, management/internals/controllers/network_map/controller/repository.go
Controller and repo plumbing to obtain account zones via GetAccountZones and propagate peersCustomZone + accountZones through network-map generation; added filter helper.
Network-map builder & types
management/server/types/networkmapbuilder.go, management/server/types/networkmap.go, management/server/types/account.go, management/server/types/networkmap.go, management/server/types/networkmap_golden_test.go, management/server/types/account_test.go
Builder and Account changes to accept accountZones, append filtered account zones into DNS config, and add filterPeerAppliedZones; tests updated/added.
Account settings validation
management/server/account.go, management/server/account_test.go
validateSettingsUpdate now takes transaction and checks for DNS domain conflicts with existing zones; tests added.
Server wiring & HTTP handler
management/internals/server/boot.go, management/internals/server/modules.go, management/server/http/handler.go, management/server/http/testing/testing_tools/channel/channel.go
Added ZonesManager() and RecordsManager() factories; NewAPIHandler now receives zone and record managers; test wiring updated.
Peers HTTP handler
management/server/http/handlers/peers/peers_handler.go
Adjusted GetAccessiblePeers call sites to pass empty custom zone and nil account zones.
GRPC conversion & util
management/internals/shared/grpc/conversion.go, management/server/util/util.go
convertToProtoCustomZone now includes SearchDomainDisabled; added IsValidDomain(regex) helper.
Activity codes & errors
management/server/activity/codes.go, shared/management/status/error.go
Added activity codes for zone/record events and NotFound error helpers for zones and DNS records.
REST client & tests
shared/management/client/rest/client.go, shared/management/client/rest/dns_zones.go, shared/management/client/rest/dns_zones_test.go
REST client DNSZonesAPI added with full CRUD for zones and records; integration-style client tests added.
OpenAPI & generated types
shared/management/http/api/openapi.yml, shared/management/http/api/types.gen.go
OpenAPI entries and generated types for Zone, ZoneRequest, DNSRecord, DNSRecordRequest, DNSRecordType and the new endpoints.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API_Server as HTTP API
    participant ZonesManager
    participant Store
    participant AccountMgr as AccountManager
    Note over Client,API_Server: Create DNS Zone request
    Client->>API_Server: POST /api/dns/zones (ZoneRequest)
    API_Server->>ZonesManager: CreateZone(ctx, accountID, userID, Zone)
    ZonesManager->>Store: Begin Transaction
    ZonesManager->>Store: GetZoneByDomain(lock) -> maybe NotFound
    ZonesManager->>Store: CreateZone(zone)
    ZonesManager->>Store: Commit Transaction
    ZonesManager->>AccountMgr: StoreEvent(DNSZoneCreated)
    ZonesManager->>AccountMgr: Trigger background peer updates (async)
    AccountMgr-->>ZonesManager: ack
    ZonesManager-->>API_Server: Zone (created)
    API_Server-->>Client: 201 Created (Zone)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Pay special attention to:
    • zones/manager/manager.go and records/manager/manager.go (transactions, event ordering, background goroutines).
    • network-map signature/flow changes (controller → account → builder) and consistent parameter ordering.
    • SQL store methods for correct locking/preload semantics and error wrapping.
    • Validation logic: domain regex, record-type validations, and CNAME/A/AAAA conflict handling.
    • Tests that rely on new behavior (manager tests, store tests, account tests) for flakiness or missing mocks.

Possibly related PRs

Suggested reviewers

  • pascal-fischer
  • mlsmaycon

Poem

🐰 I tunneled through code and found new zones,
I nibbled records, A to CNAME tones,
I hopped events, permissions in a line,
Peers bloom with filtered DNS just fine,
Hooray — the warren’s routing now refines! 🌿

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description contains only a template with uncompleted sections; critical information about the changes, issue reference, and implementation details is missing or unfilled. Complete the description template by explaining what changes were made, linking relevant issue tickets, and describing the implementation approach and rationale.
Docstring Coverage ⚠️ Warning Docstring coverage is 9.89% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title '[management] Add custom dns zones' clearly summarizes the main feature addition to the codebase, indicating a feature related to custom DNS zones in the management module.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/custom-dns-zones

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 and usage tips.

@bcmmbaga bcmmbaga marked this pull request as ready for review November 28, 2025 09:10
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
management/server/types/account.go (1)

269-279: Based on my thorough verification of the codebase, I can now provide the final assessment:

Both issues in the review comment are confirmed and valid

The code inspection reveals:

  1. Flag dropping is confirmed: Both Account.GetPeerNetworkMap and NetworkMapBuilder.assembleNetworkMap create a new CustomZone with only Domain and Records fields:

    customZones[i] = nbdns.CustomZone{
        Domain:  customZone.Domain,
        Records: records,
    }

    This leaves SearchDomainDisabled and SkipPTRProcess zero-initialized (false), dropping the original values.

  2. In-place mutation is confirmed: The input customZones slice is modified directly in-place. While current call patterns typically pass fresh or empty slices, the function signature permits slice reuse, and any such reuse would result in cumulative filtering across multiple peers.

The CustomZone struct in dns/dns.go definitively has all four fields (Domain, Records, SearchDomainDisabled, SkipPTRProcess), confirming the impact of this code pattern.


Preserve CustomZone flags and avoid surprising slice mutation in GetPeerNetworkMap

Two issues in this block:

  1. Flags are dropped for the account DNS zone

    When a zone matches dns.Fqdn(dnsDomain), the code replaces the entire element:

    customZones[i] = nbdns.CustomZone{
        Domain:  customZone.Domain,
        Records: records,
    }

    This zeroes SearchDomainDisabled and SkipPTRProcess, changing resolver behavior for that zone.

    At minimum, keep the original flags:

  •   records := filterZoneRecordsForPeers(peer, customZone, peersToConnectIncludingRouters, expiredPeers)
    
  •   customZones[i] = nbdns.CustomZone{
    
  •       Domain:  customZone.Domain,
    
  •       Records: records,
    
  •   }
    
  •   records := filterZoneRecordsForPeers(peer, customZone, peersToConnectIncludingRouters, expiredPeers)
    
  •   customZones[i].Records = records
    
    
    
  1. In‑place mutation can corrupt subsequent calls

    GetPeerNetworkMap mutates the passed customZones slice. If callers reuse a single slice for multiple peers, records are progressively intersected (peer 2 sees only a subset derived from peer 1's view).

    Consider copying before filtering to make the function side‑effect‑free for callers:

  • if dnsManagementStatus {
  •   for i, customZone := range customZones {
    
  • if dnsManagementStatus {
  •   filteredZones := slices.Clone(customZones)
    
  •   for i, customZone := range filteredZones {
           if customZone.Domain == dns.Fqdn(dnsDomain) {
    
  •           records := filterZoneRecordsForPeers(peer, customZone, peersToConnectIncludingRouters, expiredPeers)
    
  •           customZones[i].Records = records
    
  •           filteredZones[i].Records = filterZoneRecordsForPeers(peer, customZone, peersToConnectIncludingRouters, expiredPeers)
           }
    
  •   }
    
  •   dnsUpdate.CustomZones = customZones
    
  •   }
    
  •   dnsUpdate.CustomZones = filteredZones
    
    
    

Also applies to: NetworkMapBuilder.assembleNetworkMap

🧹 Nitpick comments (8)
management/server/util/util.go (1)

3-6: Domain validation helper is fine; just confirm label policy

IsValidDomain and the compiled domainRegex give a clear, centralized domain check and cover common cases (including *.example.com).

Be aware of two behavioral details:

  • Labels with underscores (e.g. _acme-challenge.example.com) are rejected.
  • Labels starting/ending with - are accepted, even though they’re not valid hostnames per RFCs.

If that matches your product’s intended constraints, this is good to go; otherwise you may want to tighten/relax the regex accordingly.

Also applies to: 58-63

management/server/http/handlers/peers/peers_handler.go (1)

13-14: Avoid unnecessary dns.CustomZone dependency in handler

GetAccessiblePeers doesn’t consume CustomZones from the returned network map, so you can avoid importing github.com/netbirdio/netbird/dns and allocating an empty slice. Passing nil is simpler and keeps DNS concerns encapsulated deeper in the stack.

-import (
+import (
   ...
-  "github.com/netbirdio/netbird/dns"
   ...
 )
 ...
- netMap := account.GetPeerNetworkMap(r.Context(), peerID, dnsDomain, []dns.CustomZone{}, validPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil)
+ netMap := account.GetPeerNetworkMap(r.Context(), peerID, dnsDomain, nil, validPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil)

Also applies to: 324-327

shared/management/http/api/openapi.yml (2)

1765-1786: Clarify whether DNS record name is FQDN or zone‑relative

DNSRecordRequest.name is documented with example "www", which suggests a label relative to the zone:

    DNSRecordRequest:
      properties:
        name:
          description: DNS record name
          example: www

However, the server‑side validation in management/internals/modules/zones/records/manager/manager.go currently requires record.Name to HasSuffix(zone.Domain), and the integration test creates records with full FQDNs like "api.test.example.com" for a zone domain "test.example.com".

This mismatch can confuse API consumers.

Suggestion

Either:

  • Keep the current server behavior (full FQDN) and update the description/example to something like "www.example.com" / "api.test.example.com", or
  • Change server validation to accept zone‑relative labels and append the zone domain internally.

1816-1834: Extend activity_code enum with new DNS zone/record activities

The Event.activity_code enum doesn’t list the new DNS activities (dns.zone.create/update/delete, dns.zone.record.create/update/delete) you introduced in management/server/activity/codes.go. As a result, documented values are out of sync with what the backend can emit.

Consider appending these new codes to the enum so API clients see the full set of possible activity_code values.

management/internals/modules/zones/interface.go (1)

7-12: Zone Manager interface is fine; consider tightening GetZone naming

The CRUD interface here is consistent with the Zone model and store APIs. For readability and consistency with DeleteZone(..., zoneID string), consider renaming the last parameter of GetZone from zone to zoneID in implementations and usages.

management/internals/modules/zones/zone.go (1)

60-77: Consider adding a maximum length check for the domain field.

While util.IsValidDomain validates the format, DNS domains have a maximum length of 253 characters. Consider adding a length check similar to the name field validation for defense in depth.

 	if !util.IsValidDomain(z.Domain) {
 		return errors.New("invalid zone domain format")
 	}
+
+	if len(z.Domain) > 253 {
+		return errors.New("zone domain exceeds maximum length of 253 characters")
+	}
management/server/types/networkmapbuilder.go (1)

13-13: Normalize domains consistently and avoid dropping CustomZone fields

The new DNS zone filtering logic is sound, but two details are worth tightening up:

  • The comparison customZone.Domain == dns.Fqdn(dnsDomain) will only match if customZone.Domain is already in fully‑qualified form (trailing dot). If Zone.Domain is stored without a trailing dot, this will silently skip zones. Consider normalizing both sides (e.g., dns.Fqdn on both or a shared helper) to avoid mismatches.

  • When you rebuild a matching zone as nbdns.CustomZone{Domain: customZone.Domain, Records: records}, any other fields on nbdns.CustomZone are zeroed out. To future‑proof this, it’s safer to copy the existing struct and only override Records (e.g., z := customZone; z.Records = records; customZones[i] = z).

Also applies to: 939-942, 978-982, 1028-1040

management/server/store/sql_store.go (1)

113-119: Zone/DNS record store methods mostly align; consider association scope and error detail

The new zone/DNS‑record CRUD methods look consistent with existing store patterns, but a couple of refinements would make them safer and easier to debug:

  • UpdateZone (and to a lesser extent UpdateDNSRecord) uses Select("*").Save(zone). Given zones.Zone declares Records []*records.Record \gorm:"foreignKey:ZoneID;references:ID"`inmanagement/internals/modules/zones/zone.golines 12–21, this can implicitly upsert associated records when callers only intend to update zone metadata. If records are supposed to be managed exclusively via the DNS record methods, it’s safer toOmit(clause.Associations)or explicitly select just the zone fields to avoid accidental writes toRecords`.

  • The new log messages are quite generic (e.g., "failed to create to store" / "failed to delete dns record from store"). Including the relevant IDs (accountID, zoneID, recordID) in the log and/or wrapping error text will make production debugging a lot easier.

These are behavioural/observability tweaks; the core implementation and account scoping look correct.

Also applies to: 4131-4294

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aca0398 and 0e644d2.

📒 Files selected for processing (31)
  • management/internals/controllers/network_map/controller/controller.go (13 hunks)
  • management/internals/controllers/network_map/controller/repository.go (3 hunks)
  • management/internals/modules/zones/interface.go (1 hunks)
  • management/internals/modules/zones/manager/api.go (1 hunks)
  • management/internals/modules/zones/manager/manager.go (1 hunks)
  • management/internals/modules/zones/records/interface.go (1 hunks)
  • management/internals/modules/zones/records/manager/api.go (1 hunks)
  • management/internals/modules/zones/records/manager/manager.go (1 hunks)
  • management/internals/modules/zones/records/record.go (1 hunks)
  • management/internals/modules/zones/zone.go (1 hunks)
  • management/internals/server/boot.go (1 hunks)
  • management/internals/server/modules.go (2 hunks)
  • management/internals/shared/grpc/conversion.go (1 hunks)
  • management/server/account_test.go (1 hunks)
  • management/server/activity/codes.go (2 hunks)
  • management/server/http/handler.go (3 hunks)
  • management/server/http/handlers/peers/peers_handler.go (2 hunks)
  • management/server/http/testing/testing_tools/channel/channel.go (2 hunks)
  • management/server/store/sql_store.go (3 hunks)
  • management/server/store/store.go (2 hunks)
  • management/server/types/account.go (2 hunks)
  • management/server/types/networkmap.go (1 hunks)
  • management/server/types/networkmap_golden_test.go (18 hunks)
  • management/server/types/networkmapbuilder.go (5 hunks)
  • management/server/util/util.go (2 hunks)
  • shared/management/client/rest/client.go (2 hunks)
  • shared/management/client/rest/dns_zones.go (1 hunks)
  • shared/management/client/rest/dns_zones_test.go (1 hunks)
  • shared/management/http/api/openapi.yml (3 hunks)
  • shared/management/http/api/types.gen.go (4 hunks)
  • shared/management/status/error.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (17)
shared/management/client/rest/client.go (2)
shared/management/client/rest/dns.go (1)
  • DNSAPI (12-14)
shared/management/client/rest/dns_zones.go (1)
  • DNSZonesAPI (12-14)
management/internals/modules/zones/records/interface.go (1)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
management/internals/modules/zones/interface.go (2)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
shared/management/http/api/types.gen.go (1)
  • Zone (1910-1928)
management/server/http/handler.go (4)
management/internals/modules/zones/records/interface.go (1)
  • Manager (7-13)
management/internals/modules/zones/interface.go (1)
  • Manager (7-13)
management/internals/modules/zones/manager/api.go (1)
  • RegisterEndpoints (20-30)
management/internals/modules/zones/records/manager/api.go (1)
  • RegisterEndpoints (20-30)
management/internals/shared/grpc/conversion.go (1)
shared/management/proto/management.pb.go (3)
  • SimpleRecord (2752-2762)
  • SimpleRecord (2777-2777)
  • SimpleRecord (2792-2794)
management/server/types/networkmap.go (2)
management/server/telemetry/accountmanager_metrics.go (1)
  • AccountManagerMetrics (11-17)
management/server/types/networkmapbuilder.go (1)
  • NetworkMapCache (33-59)
management/internals/modules/zones/records/manager/manager.go (9)
management/server/store/store.go (1)
  • Store (52-222)
management/internals/modules/zones/records/interface.go (1)
  • Manager (7-13)
management/internals/modules/zones/manager/manager.go (1)
  • NewManager (23-29)
management/internals/modules/zones/records/record.go (3)
  • Record (21-29)
  • NewRecord (31-41)
  • RecordTypeCNAME (18-18)
management/server/permissions/modules/module.go (1)
  • Dns (11-11)
management/server/permissions/operations/operation.go (2)
  • Read (7-7)
  • Delete (9-9)
shared/management/status/error.go (6)
  • NewPermissionValidationError (213-215)
  • NewPermissionDeniedError (209-211)
  • Type (46-46)
  • Errorf (70-75)
  • InvalidArgument (27-27)
  • AlreadyExists (30-30)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
route/route.go (1)
  • ID (48-48)
management/server/types/account.go (2)
dns/dns.go (1)
  • CustomZone (43-52)
shared/management/proto/management.pb.go (3)
  • CustomZone (2680-2689)
  • CustomZone (2704-2704)
  • CustomZone (2719-2721)
management/internals/server/boot.go (1)
management/server/http/handler.go (1)
  • NewAPIHandler (57-145)
shared/management/client/rest/dns_zones_test.go (4)
shared/management/http/api/types.gen.go (8)
  • DNSRecord (418-433)
  • DNSRecordTypeA (17-17)
  • PostApiDnsZonesJSONRequestBody (2027-2027)
  • PutApiDnsZonesZoneIdJSONRequestBody (2030-2030)
  • PostApiDnsZonesZoneIdRecordsJSONRequestBody (2033-2033)
  • PutApiDnsZonesZoneIdRecordsRecordIdJSONRequestBody (2036-2036)
  • ZoneRequest (1931-1946)
  • DNSRecordRequest (436-448)
shared/management/http/util/util.go (1)
  • ErrorResponse (21-24)
management/server/activity/codes.go (1)
  • Code (9-12)
shared/management/status/error.go (1)
  • Error (54-57)
management/internals/controllers/network_map/controller/repository.go (1)
management/server/store/store.go (1)
  • LockingStrengthNone (49-49)
management/internals/modules/zones/records/record.go (2)
shared/management/http/api/types.gen.go (3)
  • DNSRecord (418-433)
  • DNSRecordType (451-451)
  • DNSRecordRequest (436-448)
management/server/util/util.go (1)
  • IsValidDomain (58-63)
management/internals/controllers/network_map/controller/controller.go (4)
management/internals/controllers/network_map/interface.go (1)
  • Controller (23-39)
management/server/types/account.go (1)
  • LookupMap (57-57)
management/internals/modules/zones/records/record.go (3)
  • RecordTypeA (16-16)
  • RecordTypeAAAA (17-17)
  • RecordTypeCNAME (18-18)
dns/dns.go (1)
  • DefaultClass (21-21)
management/server/store/store.go (3)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
shared/management/http/api/types.gen.go (1)
  • Zone (1910-1928)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
management/server/account_test.go (2)
dns/dns.go (1)
  • CustomZone (43-52)
shared/management/proto/management.pb.go (3)
  • CustomZone (2680-2689)
  • CustomZone (2704-2704)
  • CustomZone (2719-2721)
management/server/store/sql_store.go (4)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
shared/management/status/error.go (4)
  • Error (54-57)
  • Errorf (70-75)
  • NewZoneNotFoundError (257-259)
  • NewDNSRecordNotFoundError (262-264)
management/server/store/store.go (2)
  • LockingStrength (42-42)
  • LockingStrengthNone (49-49)
management/internals/modules/zones/zone.go (3)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
shared/management/http/api/types.gen.go (2)
  • Zone (1910-1928)
  • ZoneRequest (1931-1946)
management/server/util/util.go (1)
  • IsValidDomain (58-63)
🔇 Additional comments (47)
management/internals/modules/zones/manager/api.go (1)

16-160: Zones HTTP handlers follow existing patterns cleanly

Auth extraction, validation, manager delegation, and JSON responses look consistent and correct; no issues spotted in routing or parameter handling.

management/internals/shared/grpc/conversion.go (1)

329-345: gRPC custom zone conversion correctly propagates SearchDomainDisabled

The added SearchDomainDisabled field is wired through alongside Domain and preallocated Records; conversion logic remains consistent.

management/server/http/testing/testing_tools/channel/channel.go (1)

12-15: Test HTTP channel correctly wires zones and records managers

Using real zonesManager.NewManager and recordsManager.NewManager with the test store and passing them into NewAPIHandler keeps the black-box tests exercising the new DNS zones/records API surface as expected.

Also applies to: 92-101

management/internals/server/modules.go (1)

9-12: Zones/records manager accessors align with existing server module patterns

ZonesManager() and RecordsManager() follow the same lazy-initialization pattern as other managers and use the expected dependencies (Store, AccountManager, PermissionsManager), so the wiring looks correct.

Also applies to: 123-133

management/server/types/networkmap.go (1)

24-34: Network map API cleanly extended for multi‑zone DNS

Extending GetPeerNetworkMapExp with dnsDomain and customZones and forwarding them to NetworkMapCache.GetPeerNetworkMap is a minimal, coherent change; cache initialization semantics remain unchanged.

shared/management/client/rest/client.go (1)

60-67: REST client DNSZones API wiring is consistent and complete

Adding the DNSZones field and initializing it in initialize() mirrors the existing DNS API pattern and should make the new endpoints immediately usable via the client.

Also applies to: 107-121

shared/management/status/error.go (1)

256-264: LGTM!

The new error constructors follow the established pattern in this file and are consistent with other NotFound error types.

management/server/http/handler.go (3)

17-20: LGTM!

Import aliases are appropriately named to avoid conflicts with the interface types.


72-73: LGTM!

New manager parameters follow the established injection pattern.


141-142: LGTM!

Endpoint registration follows the established pattern and maintains logical grouping with DNS-related handlers.

management/internals/controllers/network_map/controller/repository.go (1)

6-6: LGTM!

The new GetAccountZones method follows the established repository pattern, using LockingStrengthNone consistent with other read methods in this file.

Also applies to: 16-16, 42-45

shared/management/http/api/types.gen.go (1)

1-4: Generated code - no manual changes required.

This file is auto-generated by oapi-codegen. The new DNS zone and record types appear correctly structured with appropriate field types and optionality for request/response patterns.

management/server/account_test.go (1)

397-399: Test correctly adapts to updated GetPeerNetworkMap signature with domain and zones slice parameters.

The test at lines 397-399 properly implements the new signature:

  • Passes "netbird.io" as the dnsDomain string parameter
  • Wraps customZone in []nbdns.CustomZone{customZone} slice to support multiple zones
  • All other parameters (context, peerID, validatedPeers, policies, routers, metrics) match the function signature

The implementation aligns with the underlying function that iterates over the zones slice to filter records per domain.

management/internals/server/boot.go (1)

96-96: Perfect! All verifications are complete. The signature shows:

  • Parameter 15: zManager zones.Manager
  • Parameter 16: rManager records.Manager
  • Parameter 17: networkMapController network_map.Controller

These match the ZonesManager() and RecordsManager() methods that return zones.Manager and records.Manager respectively. The code change is correctly implemented.


LGTM!

Both ZonesManager() and RecordsManager() methods exist on BaseServer and return the correct types (zones.Manager and records.Manager). The NewAPIHandler signature correctly accepts these parameters at positions 15-16, and they are properly passed from the call site.

management/server/activity/codes.go (1)

183-189: DNS zone/record activity codes and mappings look consistent

New DNS activity constants and their activityMap entries follow the existing naming and coding conventions; no functional issues spotted here.

Also applies to: 300-306

management/internals/modules/zones/records/interface.go (1)

7-13: Records Manager interface shape is reasonable

The CRUD interface is coherent (context-first, explicit account/user/zone IDs) and matches the underlying Record model and store APIs.

management/server/types/account.go (1)

1755-1778: Per‑peer DNS record filtering helper looks correct

filterZoneRecordsForPeers builds a set of IPs for the target peer, its reachable peers, and expired peers, and filters zone records by RData membership. The logic is straightforward and matches the goal of scoping DNS responses to the peer’s connectivity set.

shared/management/http/api/openapi.yml (1)

1710-1756: Zone schemas look consistent with generated types

ZoneRequest / Zone schemas (fields, required sets, and examples) line up with the generated api.Zone / api.ZoneRequest structs and the domain model (zones.Zone). The use of Enabled *bool in the request and required enabled in the response is a sensible pattern for create/update semantics.

management/internals/modules/zones/records/manager/manager.go (1)

33-55: Permissions + transactional flow for DNS records look solid

  • Permission checks for read/create/update/delete use the DNS module and appropriate operations and map errors via status.NewPermissionValidationError / NewPermissionDeniedError.
  • All mutating operations run inside ExecuteInTransaction, lock the zone (LockingStrengthUpdate), update the record, and increment the network serial before emitting events.

This pattern matches the rest of the codebase’s account/DNS workflows.

Also applies to: 101-205

shared/management/client/rest/dns_zones_test.go (3)

40-213: Mocked DNS zones client tests provide good coverage

The mocked tests for zone CRUD operations verify HTTP methods, request body shapes (via unmarshalling), and error handling for 4xx responses. This should catch most regressions in the REST client layer.


215-390: Mocked DNS record client tests are comprehensive

Similarly, record CRUD tests validate correct endpoints, methods, and JSON payloads, plus propagate server error responses into Go errors, which is exactly what the client should be doing.


392-460: End‑to‑end integration test nicely validates the full DNS zones/records flow

TestDNSZones_Integration walks through create/list/get/update/delete for both zones and records against a black‑box server, asserting realistic values (full FQDN for record name, distribution groups, etc.). This is a valuable high‑level guardrail for the new feature.

management/internals/modules/zones/records/manager/api.go (6)

16-30: LGTM!

The handler struct and route registration are well-structured. The RESTful endpoints follow proper conventions for DNS record CRUD operations.


32-57: LGTM!

The getAllRecords handler correctly validates authentication, zone ID, and properly converts internal records to API responses.


59-93: LGTM!

The createRecord handler follows the standard pattern: decode request, convert to domain model, validate, and delegate to manager. The error handling is consistent.


95-121: LGTM!

The getRecord handler correctly validates both zone and record IDs before delegating to the manager.


123-164: LGTM!

The updateRecord handler correctly uses the path parameter for the record ID, ensuring the URL is the source of truth for the resource identifier.


166-191: LGTM!

The deleteRecord handler follows proper REST conventions, returning an empty object on successful deletion.

management/internals/modules/zones/zone.go (5)

13-22: LGTM!

The Zone struct is well-defined with appropriate GORM tags. The JSON serializer for DistributionGroups and the foreign key relationship for Records are correctly configured.


24-34: LGTM!

The constructor properly initializes all fields and generates a unique ID using xid.


36-45: LGTM!

The API response mapping is complete and correctly maps all Zone fields to the API representation.


47-58: LGTM!

The FromAPIRequest method correctly handles the optional Enabled field by defaulting to true when not provided, which is a sensible default for new zones.


79-81: LGTM!

The EventMeta method provides appropriate metadata for event tracking.

management/internals/controllers/network_map/controller/controller.go (6)

174-178: LGTM!

Proper error handling for zone retrieval with logging and early return on failure.


831-888: LGTM!

The filterPeerAppliedZones function correctly:

  • Filters zones by peer group membership
  • Skips disabled zones and zones without records
  • Maps record types to DNS type constants
  • Logs warnings for unknown record types instead of failing
  • Properly inverts EnableSearchDomain to SearchDomainDisabled

320-328: LGTM!

The zone retrieval and filtering logic in UpdateAccountPeer is consistent with the pattern used in other methods.


459-470: LGTM!

Zone retrieval and filtering follows the established pattern in the controller.


789-800: LGTM!

The zone handling in GetNetworkMap is consistent with other controller methods.


500-518: LGTM!

The signature update to accept customZones []nbdns.CustomZone properly supports the multi-zone architecture.

management/server/types/networkmap_golden_test.go (2)

107-108: LGTM!

The test signature updates are consistent across all test functions and benchmarks.


72-72: Golden tests intentionally use empty zones; CustomZone functionality is tested separately.

The golden tests pass empty []dns.CustomZone{} by design—they validate baseline network map structure rather than DNS zone filtering. Comprehensive CustomZone testing already exists in management/server/account_test.go (line 398), where GetPeersCustomZone() is called with actual zone data, and in management/server/types/account_test.go (lines 1119–1209), which covers zone record filtering scenarios. No additional coverage is needed in these golden tests.

management/internals/modules/zones/records/record.go (5)

13-29: LGTM!

The RecordType constants and Record struct are well-defined with appropriate GORM tags for indexing and primary key.


31-41: LGTM!

The constructor properly initializes all fields and generates a unique ID.


43-59: LGTM!

The API conversion methods correctly map between internal and external record representations.


61-96: LGTM!

The validation logic is comprehensive:

  • Name format validation using domain regex
  • Type-specific content validation (IPv4, IPv6, domain)
  • Non-negative TTL check
  • Clear error messages for each validation failure

98-107: LGTM!

The EventMeta method appropriately includes both record details and zone context for comprehensive event tracking.

management/server/store/store.go (1)

26-28: DNS zone and DNS record methods on Store look consistent

The new zone and DNS record operations on Store follow existing patterns (account‑scoped IDs, read methods with LockingStrength) and match the SqlStore implementations. No issues from the interface design side.

Also applies to: 209-221

Signed-off-by: bcmmbaga <[email protected]>
Signed-off-by: bcmmbaga <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
management/internals/modules/zones/manager/manager.go (1)

167-172: Fix closure capture of loop variable record.

This issue was flagged in a previous review and remains unfixed. The closures built inside the loop capture record by reference, so when they execute (lines 184-186), all of them will reference the last record from the loop. This causes incorrect event IDs and metadata for all deleted records except the last one.

Apply this diff to capture the current record value in each iteration:

-	for _, record := range records {
-		eventsToStore = append(eventsToStore, func() {
-			meta := record.EventMeta(zone.ID, zone.Name)
-			m.accountManager.StoreEvent(ctx, userID, record.ID, accountID, activity.DNSRecordDeleted, meta)
-		})
-	}
+	for _, record := range records {
+		rec := record
+		eventsToStore = append(eventsToStore, func() {
+			meta := rec.EventMeta(zone.ID, zone.Name)
+			m.accountManager.StoreEvent(ctx, userID, rec.ID, accountID, activity.DNSRecordDeleted, meta)
+		})
+	}
🧹 Nitpick comments (2)
management/server/store/sql_store_test.go (1)

3859-3879: Consider testing cascading delete behavior.

While the current tests verify zone deletion works correctly, it would be helpful to optionally add a test that verifies whether DNS records are automatically deleted when their parent zone is deleted (if cascading delete is implemented), or if they need to be deleted separately.

Example test structure:

func TestSqlStore_DeleteZoneCascade(t *testing.T) {
	// Create zone
	// Create DNS records in zone
	// Delete zone
	// Verify records are also deleted (or handle accordingly)
}
management/internals/modules/zones/manager/manager.go (1)

188-188: Consider using a detached context for background peer updates.

The goroutine receives the request context ctx, which may be canceled when the HTTP request completes. If UpdateAccountPeers is a long-running operation, it could be interrupted prematurely.

If peer updates should complete regardless of client disconnection, consider using a detached context:

go m.accountManager.UpdateAccountPeers(context.Background(), accountID)

However, if cancellation is intentional (e.g., to respect request timeouts), the current implementation is fine.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e644d2 and 25c9cb2.

📒 Files selected for processing (7)
  • management/internals/controllers/network_map/controller/controller.go (13 hunks)
  • management/internals/controllers/network_map/controller/controller_test.go (2 hunks)
  • management/internals/modules/zones/manager/manager.go (1 hunks)
  • management/internals/modules/zones/records/manager/manager.go (1 hunks)
  • management/internals/modules/zones/records/record.go (1 hunks)
  • management/server/store/sql_store_test.go (2 hunks)
  • shared/management/http/api/openapi.yml (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
management/internals/modules/zones/records/record.go (2)
shared/management/http/api/types.gen.go (3)
  • DNSRecord (418-433)
  • DNSRecordType (451-451)
  • DNSRecordRequest (436-448)
management/server/util/util.go (1)
  • IsValidDomain (58-63)
management/internals/modules/zones/records/manager/manager.go (8)
management/server/store/store.go (1)
  • Store (52-222)
management/internals/modules/zones/records/interface.go (1)
  • Manager (7-13)
management/internals/modules/zones/manager/manager.go (1)
  • NewManager (23-29)
management/internals/modules/zones/records/record.go (3)
  • Record (21-29)
  • NewRecord (31-41)
  • RecordTypeCNAME (18-18)
management/server/permissions/modules/module.go (1)
  • Dns (11-11)
management/server/permissions/operations/operation.go (2)
  • Read (7-7)
  • Delete (9-9)
shared/management/status/error.go (4)
  • NewPermissionValidationError (213-215)
  • NewPermissionDeniedError (209-211)
  • Type (46-46)
  • Errorf (70-75)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (13)
  • GitHub Check: release_ui_darwin
  • GitHub Check: release
  • GitHub Check: release_ui
  • GitHub Check: Linux
  • GitHub Check: Client / Unit
  • GitHub Check: Darwin
  • GitHub Check: Windows
  • GitHub Check: Build Cache
  • GitHub Check: Client / Unit
  • GitHub Check: Android / Build
  • GitHub Check: iOS / Build
  • GitHub Check: Client / Unit
  • GitHub Check: JS / Lint
🔇 Additional comments (33)
management/internals/modules/zones/records/manager/manager.go (6)

19-31: LGTM!

The manager implementation follows the established pattern with proper dependency injection of store, account manager, and permissions manager.


33-55: LGTM!

Read operations properly implement permission checks and use appropriate locking strength for concurrent reads.


57-101: LGTM!

The previous concern about missing peer updates has been resolved. The method now correctly triggers UpdateAccountPeers at line 98, ensuring new DNS records are propagated to peers proactively, consistent with the update and delete paths.


103-160: LGTM!

The update logic correctly distinguishes between changes that require conflict revalidation (Name, Type, Content) and those that don't (TTL). The implementation properly updates all fields while only triggering expensive conflict checks when necessary.


162-207: LGTM!

Delete operation correctly implemented with proper locking, transactional safety, event logging, and peer updates.


209-236: LGTM!

The zone membership validation has been corrected to properly check label boundaries (line 211), addressing the previous review concern. The implementation now correctly accepts only example.com or *.example.com for zone example.com, while rejecting notexample.com.

The duplicate and CNAME conflict checks are also correctly implemented.

shared/management/http/api/openapi.yml (3)

1710-1801: LGTM!

The previous issue with DNSRecord requiring an undefined enabled field has been resolved. The schema now correctly requires only id (line 1800) and inherits the necessary fields from DNSRecordRequest. All DNS Zone and Record schemas are well-defined and consistent with the backend implementation.


4592-4741: LGTM!

All DNS Zone CRUD endpoints are properly defined with:

  • Appropriate HTTP methods and paths
  • Correct security requirements
  • Proper request/response schemas
  • Comprehensive error response codes

The API design follows RESTful conventions consistently.


4742-4932: LGTM!

DNS Record endpoints are well-structured as nested resources under zones (/api/dns/zones/{zoneId}/records). All endpoints properly:

  • Define required path parameters (zoneId, recordId)
  • Use correct request/response schemas
  • Include appropriate error codes (notably 404 for resource-not-found scenarios)
  • Follow consistent patterns with the parent zone endpoints
management/server/store/sql_store_test.go (3)

25-26: LGTM! New imports are correctly added for zone and DNS record testing.

The imports properly reference the new internal modules for zones and DNS records that align with the test coverage being added.


3723-3879: Excellent test coverage for zone CRUD operations.

The zone tests comprehensively cover creation, retrieval, update, and deletion with proper error handling and NotFound verification after deletion. The test structure follows existing patterns in the file and uses appropriate assertions.


3881-4126: Comprehensive DNS record test coverage with excellent structure.

The DNS record tests thoroughly cover CRUD operations including:

  • Creation with proper zone dependencies
  • Retrieval by ID and by name
  • Updates to all record fields
  • Individual and bulk deletion
  • Multiple record types (A, AAAA, CNAME)

All tests include proper error handling and NotFound verification.

management/internals/modules/zones/records/record.go (6)

13-29: Well-structured Record model with proper GORM configuration.

The RecordType constants provide type safety for the three supported DNS record types, and the Record struct includes appropriate GORM tags for database indexing and primary key designation.


31-41: LGTM! Constructor properly initializes Record with unique ID.

The use of xid for ID generation ensures uniqueness, and all fields are properly initialized.


43-59: API conversion methods correctly map between internal and external types.

Both ToAPIResponse and FromAPIRequest properly convert between the internal Record model and the API types, with correct field mappings.


61-96: Comprehensive validation with appropriate type-specific checks.

The validation logic properly enforces:

  • Non-empty name with domain format validation
  • Type-specific content validation (IPv4 for A, IPv6 for AAAA, domain for CNAME)
  • Non-negative TTL

Note: The TTL validation allows 0, which is valid in DNS for indicating no caching should occur.


120-129: IPv6 validation fix has been correctly applied.

The validateIPv6 function now properly rejects IPv4 addresses by checking ip.To4() != nil. This correctly addresses the critical bug from the previous review where ip.To16() == nil would incorrectly accept IPv4 addresses (since To16() returns a 16-byte IPv4-mapped representation for IPv4 addresses).

The current implementation ensures only true IPv6 addresses are accepted for AAAA records.


98-118: EventMeta and validateIPv4 are correctly implemented.

The EventMeta method provides comprehensive metadata for event tracking, and validateIPv4 properly validates IPv4 addresses using the correct ip.To4() == nil check.

management/internals/controllers/network_map/controller/controller_test.go (2)

4-17: LGTM!

The new imports are appropriate for the test function and support comprehensive DNS zone testing.


120-630: Excellent test coverage!

The table-driven test comprehensively validates filterPeerAppliedZones across multiple scenarios:

  • Access control via distribution groups
  • Zone enablement filtering
  • Empty record handling
  • Multiple record types (A, AAAA, CNAME)
  • Search domain configuration
  • Multi-zone and multi-group scenarios

The assertions thoroughly verify both zone-level properties and individual record details.

management/internals/controllers/network_map/controller/controller.go (12)

15-15: LGTM!

The new imports support DNS zone filtering and record type mapping in the filterPeerAppliedZones function.

Also applies to: 23-24


174-178: LGTM!

Proper error handling for account zones retrieval with logging and early return.


203-205: LGTM! Bug from previous review has been fixed.

The code correctly uses p.ID (goroutine parameter) instead of the loop variable, avoiding the closure capture issue flagged in the previous review. The customZones construction properly filters zones by peer group membership and appends the peers custom zone.


210-213: LGTM!

Both experimental and non-experimental network map generation paths correctly updated to accept the customZones slice parameter.


320-327: LGTM!

Consistent zone retrieval and filtering pattern applied correctly.


332-335: LGTM!

Function calls correctly updated to pass customZones parameter in both code paths.


459-469: LGTM!

Proper sequence: retrieve account zones, get DNS domain and peers custom zone, then filter zones based on peer groups.


480-483: LGTM!

Function calls consistently updated with customZones parameter.


500-518: LGTM!

The getPeerNetworkMapExp function signature correctly updated to accept customZones slice, and properly retrieves DNS domain from account settings before passing to the network map builder.


790-799: LGTM!

Consistent zone retrieval and filtering pattern properly implemented in GetNetworkMap.


810-813: LGTM!

Network map generation calls correctly updated with customZones parameter.


831-888: Well-implemented helper function!

The filterPeerAppliedZones function correctly:

  • Filters zones based on peer group access control
  • Skips disabled zones and zones without records
  • Maps DNS record types (A, AAAA, CNAME) with proper fallback for unknown types
  • Constructs properly formatted FQDNs using dns.Fqdn()
  • Inverts EnableSearchDomain to SearchDomainDisabled correctly

The early return optimization and access control logic are both sound.

management/internals/modules/zones/manager/manager.go (1)

88-129: Verify if peer updates and serial increment are needed after zone updates.

UpdateZone correctly handles the transaction error (lines 122-124, resolving the previous review concern), but like CreateZone, it does not call IncrementNetworkSerial or UpdateAccountPeers. Changes to zone properties (enabled status, distribution groups, domain, search domain settings) likely affect the network map and should trigger peer updates.

Use the verification script from the CreateZone comment above to check if Update operations typically call these methods.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
shared/management/http/api/openapi.yml (3)

4594-4743: /api/dns/zones collection and item endpoints match existing REST patterns

  • List/create (GET/POST) endpoints for zones reuse the standard security blocks and response code conventions (200 + 400/401/403/500 on write).
  • Single-zone GET/PUT/DELETE correctly use zoneId as a path param with type string and add 404 for missing zones, which is actually stricter than some of the older DNS endpoints.

One minor consistency nit: existing DELETE endpoints often include an explicit empty content: { } block on 200, whereas DELETE /api/dns/zones/{zoneId} omits content. Both are valid OpenAPI, but you may want to align with the existing deletes for uniformity.


4744-4814: /api/dns/zones/{zoneId}/records collection endpoints are well-shaped

  • GET returns DNSRecord[] and includes 404 when the parent zone is missing.
  • POST accepts DNSRecordRequest and returns a full DNSRecord, consistent with other create endpoints returning the created resource.

Again, optional nit: you don’t expose a 400 for the GET despite other list endpoints sometimes defining it even without query params; that’s fine semantically, but if you aim for strict uniformity you might consider adding it later.


4816-4934: Record-level endpoints look correct; consider aligning delete response shape

  • GET/PUT for /api/dns/zones/{zoneId}/records/{recordId} use both zoneId and recordId with clear descriptions and return a single DNSRecord, including 404 for not found.
  • PUT reuses DNSRecordRequest for the body, which matches the Go types and keeps id server-controlled.

For DELETE, you return a bare 200 with description only, whereas most existing deletes specify content: { }. It’s valid as-is, but you might want to add the empty content block for consistency with the rest of the API surface.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1782bbe and 604085d.

📒 Files selected for processing (4)
  • management/internals/modules/zones/zone.go (1 hunks)
  • management/server/store/sql_store.go (3 hunks)
  • shared/management/http/api/openapi.yml (3 hunks)
  • shared/management/http/api/types.gen.go (4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.
📚 Learning: 2025-11-28T12:20:47.254Z
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.

Applied to files:

  • shared/management/http/api/openapi.yml
  • shared/management/http/api/types.gen.go
  • management/server/store/sql_store.go
  • management/internals/modules/zones/zone.go
🧬 Code graph analysis (1)
management/internals/modules/zones/zone.go (3)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
shared/management/http/api/types.gen.go (3)
  • Zone (1910-1931)
  • DNSRecord (418-433)
  • ZoneRequest (1934-1949)
management/server/util/util.go (1)
  • IsValidDomain (58-63)
🔇 Additional comments (12)
management/internals/modules/zones/zone.go (1)

13-89: LGTM! Clean zone model implementation.

The Zone type is well-structured with appropriate GORM tags, proper foreign key relationships, sensible validation rules, and clean API conversion logic. The default handling for the optional Enabled field (defaulting to true when nil) aligns with typical feature flag patterns.

management/server/store/sql_store.go (4)

30-31: LGTM! Proper integration into auto-migration.

The imports and auto-migration additions correctly integrate the new zones and records models into the store initialization, ensuring database schema readiness.

Also applies to: 118-118


4141-4149: LGTM!

The use of Select("*").Save() for full updates is consistent with other entity update patterns in this store (e.g., SavePolicy).


4151-4199: LGTM! Standard CRUD patterns.

The zone retrieval and deletion methods follow established store conventions with proper locking support, association preloading, and error handling.


4235-4295: LGTM! DNS record retrieval methods are properly scoped.

The GetDNSRecordByID, GetZoneDNSRecords, GetZoneDNSRecordsByName, and DeleteZoneDNSRecords methods all correctly scope operations by both accountID and zoneID, ensuring proper data isolation.

shared/management/http/api/types.gen.go (5)

15-20: DNSRecordType constants correctly mirror OpenAPI enum

The A/AAAA/CNAME constants align with the DNSRecordType string enum in the OpenAPI spec; nothing to change here.


417-452: DNSRecord / DNSRecordRequest structs match schema expectations

DNSRecordRequest and DNSRecord fields (name, type, content, ttl, id) line up with the OpenAPI definitions and required sets; using a separate request type without id is consistent with other resources in this API.


450-451: String alias for DNSRecordType is appropriate

Defining DNSRecordType as string keeps the generated client simple while still being constrained by the enum constants above; this matches existing enum patterns in this file.


1909-1949: Zone / ZoneRequest shapes are consistent but rely on backend defaults for enabled

  • ZoneRequest.Enabled as *bool (omittable) vs Zone.Enabled as bool matches the OpenAPI idea of enabled being optional on write with a default of true, and required on read.
  • This puts the onus on handlers to interpret nil as “use default” and not as false.

Please double-check the zone create/update handlers treat nil Enabled correctly (defaulting to true on create and/or leaving it unchanged on update) so behavior matches the OpenAPI default.


2029-2040: Request body aliases for DNS zones/records follow existing patterns

The new PostApiDnsZones*/PutApiDnsZones* and records request-body aliases correctly point to ZoneRequest/DNSRecordRequest, matching the OpenAPI requestBody schemas and the conventions used elsewhere in this generated file.

shared/management/http/api/openapi.yml (2)

28-29: New “DNS Zones” tag integrates cleanly into tag list

Tag naming and description match the style of existing tags and will group the new endpoints coherently in generated docs.


1710-1803: Zone/DNSRecord schemas are structurally sound and consistent**

  • ZoneRequest and Zone follow the existing pattern of *Request vs full resource (allOf with an id-carrying object), similar to networks/resources/routes.
  • enabled being optional on ZoneRequest (with a default) and required via Zone’s required array is a valid allOf usage and matches the generated Go types.
  • DNSRecordType enum and DNSRecordRequest/DNSRecord shapes line up with the Go types (A/AAAA/CNAME; name, type, content, ttl, plus id on the read model).

No inconsistencies spotted between these schemas and the generated api package.

# Conflicts:
#	management/internals/controllers/network_map/controller/controller.go
#	management/internals/controllers/network_map/controller/repository.go
#	management/internals/server/modules.go
#	management/server/activity/codes.go
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
management/server/account_test.go (1)

399-401: Avoid hard‑coded DNS domain in test; use account.Domain

The new GetPeerNetworkMap signature usage looks correct, but you can tighten the test by deriving the domain from the account instead of repeating the literal:

-        customZone := account.GetPeersCustomZone(context.Background(), "netbird.io")
-        networkMap := account.GetPeerNetworkMap(context.Background(), testCase.peerID, "netbird.io", []nbdns.CustomZone{customZone}, validatedPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil)
+        customZone := account.GetPeersCustomZone(context.Background(), account.Domain)
+        networkMap := account.GetPeerNetworkMap(context.Background(), testCase.peerID, account.Domain, []nbdns.CustomZone{customZone}, validatedPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil)
shared/management/http/api/openapi.yml (1)

4604-4787: Add 400 responses to DNS Zones list endpoints for consistency

Most list endpoints in this spec expose a 400 response (e.g. /api/dns/nameservers, /api/peers, etc.), but:

  • GET /api/dns/zones
  • GET /api/dns/zones/{zoneId}/records

currently only define 200, auth, and 500 (plus 404 on the records list).

For consistency with the rest of the API surface and to accommodate potential client or path validation errors, consider adding 400 to both:

   /api/dns/zones:
     get:
@@
       responses:
         '200':
           description: A JSON Array of DNS Zones
           content:
             application/json:
               schema:
                 type: array
                 items:
                   $ref: '#/components/schemas/Zone'
+        '400':
+          "$ref": "#/components/responses/bad_request"
         '401':
           "$ref": "#/components/responses/requires_authentication"
@@
   /api/dns/zones/{zoneId}/records:
     get:
@@
       responses:
         '200':
           description: A JSON Array of DNS Records
           content:
             application/json:
               schema:
                 type: array
                 items:
                   $ref: '#/components/schemas/DNSRecord'
+        '400':
+          "$ref": "#/components/responses/bad_request"
         '401':
           "$ref": "#/components/responses/requires_authentication"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 604085d and 783a120.

📒 Files selected for processing (11)
  • management/internals/controllers/network_map/controller/controller.go (14 hunks)
  • management/internals/controllers/network_map/controller/repository.go (3 hunks)
  • management/internals/server/boot.go (1 hunks)
  • management/internals/server/modules.go (2 hunks)
  • management/server/account_test.go (1 hunks)
  • management/server/activity/codes.go (2 hunks)
  • management/server/http/handler.go (3 hunks)
  • management/server/http/handlers/peers/peers_handler.go (2 hunks)
  • management/server/http/testing/testing_tools/channel/channel.go (2 hunks)
  • shared/management/http/api/openapi.yml (3 hunks)
  • shared/management/http/api/types.gen.go (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • management/server/http/handlers/peers/peers_handler.go
  • management/server/http/testing/testing_tools/channel/channel.go
  • management/server/http/handler.go
  • management/internals/server/boot.go
  • management/internals/controllers/network_map/controller/repository.go
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.
📚 Learning: 2025-11-28T12:20:47.254Z
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.

Applied to files:

  • management/internals/server/modules.go
  • shared/management/http/api/openapi.yml
  • management/internals/controllers/network_map/controller/controller.go
  • shared/management/http/api/types.gen.go
  • management/server/account_test.go
🧬 Code graph analysis (2)
management/internals/server/modules.go (5)
management/internals/modules/zones/records/interface.go (1)
  • Manager (7-13)
management/internals/modules/zones/interface.go (1)
  • Manager (7-13)
management/internals/server/container.go (1)
  • Create (6-10)
management/internals/modules/zones/manager/manager.go (1)
  • NewManager (23-29)
management/internals/modules/zones/records/manager/manager.go (1)
  • NewManager (25-31)
management/server/account_test.go (2)
dns/dns.go (1)
  • CustomZone (43-52)
shared/management/proto/management.pb.go (3)
  • CustomZone (2680-2689)
  • CustomZone (2704-2704)
  • CustomZone (2719-2721)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (27)
  • GitHub Check: Client / Unit (386)
  • GitHub Check: Management / Unit (amd64, sqlite)
  • GitHub Check: Relay / Unit (386)
  • GitHub Check: Management / Unit (amd64, mysql)
  • GitHub Check: Management / Unit (amd64, postgres)
  • GitHub Check: Management / Benchmark (API) (amd64, postgres)
  • GitHub Check: Management / Benchmark (API) (amd64, sqlite)
  • GitHub Check: Relay / Unit (amd64, -race)
  • GitHub Check: Client (Docker) / Unit
  • GitHub Check: Client / Unit (amd64)
  • GitHub Check: Management / Integration (amd64, postgres)
  • GitHub Check: Management / Benchmark (amd64, postgres)
  • GitHub Check: Signal / Unit (amd64)
  • GitHub Check: Signal / Unit (386)
  • GitHub Check: Management / Benchmark (amd64, sqlite)
  • GitHub Check: Android / Build
  • GitHub Check: Linux
  • GitHub Check: Windows
  • GitHub Check: iOS / Build
  • GitHub Check: Darwin
  • GitHub Check: release_ui_darwin
  • GitHub Check: Client / Unit
  • GitHub Check: release_ui
  • GitHub Check: Client / Unit
  • GitHub Check: JS / Lint
  • GitHub Check: Client / Unit
  • GitHub Check: release
🔇 Additional comments (14)
management/internals/server/modules.go (1)

10-13: Zones/records manager wiring is consistent and correct

The new ZonesManager and RecordsManager accessors follow the existing lazy Create pattern, inject the expected dependencies (store, account, permissions), and mirror NetworksManager/ResourcesManager style. No changes needed.

Also applies to: 126-136

shared/management/http/api/openapi.yml (1)

28-29: DNS Zones tagging and schemas look coherent

The new DNS Zones tag plus ZoneRequest/Zone and DNS record schemas are internally consistent and match existing patterns:

  • Zone correctly composes ZoneRequest via allOf, so enabled is defined where it’s required.
  • ZoneRequest vs Zone separation (optional enabled on request, required on response) is a nice touch.
  • DNSRecordType/DNSRecordRequest/DNSRecord cleanly model the limited A/AAAA/CNAME surface.

No changes needed here.

Also applies to: 1710-1803

shared/management/http/api/types.gen.go (4)

15-20: LGTM: DNS record type constants and enum definition.

The DNS record type constants cover the essential record types (A, AAAA, CNAME) for custom DNS zones. The string-based enum follows the existing pattern in this generated file.

Also applies to: 450-451


417-448: LGTM: DNS record models follow REST conventions.

The separation between DNSRecord (with ID) and DNSRecordRequest (without ID) follows standard RESTful API patterns. The field types are appropriate for DNS record data.


1909-1949: LGTM: Zone models are well-structured.

The Zone and ZoneRequest structures provide comprehensive fields for DNS zone management. The use of DistributionGroups for access control aligns with NetBird's group-based architecture, and embedding Records in the zone response is good for API ergonomics.


2035-2045: LGTM: Request body type aliases are consistent.

The request body type aliases follow the established oapi-codegen pattern and cover all necessary CRUD operations for DNS zones and records.

management/internals/controllers/network_map/controller/controller.go (7)

15-15: LGTM: New imports support DNS zones feature.

The addition of miekg/dns (standard Go DNS library) and the internal zones/records modules are necessary for the multi-zone DNS support being added.

Also applies to: 24-25


202-206: LGTM: Comprehensive error handling and clean zone filtering pattern.

The account zones retrieval includes proper error logging and propagation. The pattern of filtering zones by peer groups and then appending the peers custom zone is clear and correctly uses the goroutine parameter p.

Also applies to: 231-233, 238-238, 240-240


348-356: LGTM: Consistent zone handling pattern in UpdateAccountPeer.

The zone retrieval and filtering logic matches the pattern in sendUpdateAccountPeers, maintaining consistency across the codebase.

Also applies to: 360-360, 362-362


455-465: LGTM: Zone handling consistent in GetValidatedPeerWithMap.

The zone filtering follows the established pattern with appropriate error handling.

Also applies to: 476-476, 478-478


501-501: LGTM: Function signature updated for multi-zone support.

The change from a single custom zone to a slice of custom zones enables multiple DNS zones per account. Extracting dnsDomain from account settings before passing to the network map builder is appropriate.

Also applies to: 512-513


834-843: LGTM: Zone handling in GetNetworkMap follows the pattern.

The zone retrieval and filtering logic is consistent with the other updated functions.

Also applies to: 854-854, 856-856


871-928: LGTM: Well-structured zone filtering implementation.

The filterPeerAppliedZones function correctly implements multi-zone DNS filtering:

  • Early return for peers without groups is efficient
  • Filtering logic correctly checks zone enabled status and non-empty records (consistent with the learning that zones without records don't affect network maps)
  • Distribution group access check properly evaluates peer membership
  • DNS record type mapping covers the standard types (A, AAAA, CNAME) with appropriate warning logging for unknown types
  • Use of dns.Fqdn() ensures proper DNS format for names and domains
  • The function is defensive and handles edge cases appropriately

Based on learnings, zones without records are correctly filtered out.

management/server/activity/codes.go (1)

184-190: LGTM: DNS activity codes follow established patterns.

The six new activity constants (DNS zone and record create/update/delete) use sequential values and follow the existing naming conventions. The activity map entries have clear, descriptive messages and use hierarchical code strings (dns.zone.* and dns.zone.record.*) consistent with other activity codes in the system.

Also applies to: 302-308

Signed-off-by: bcmmbaga <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
management/server/store/sql_store.go (1)

4141-4149: Consider omitting associations in UpdateZone.

UpdateZone uses Select("*").Save(zone) without explicitly omitting associations. If a zone is retrieved via GetZoneByID (which preloads Records), then modified and saved, GORM might attempt to update the nested records as well. For consistency with similar operations (e.g., UpdateGroup at line 3163) and to prevent unintended association updates, consider using Omit(clause.Associations):

 func (s *SqlStore) UpdateZone(ctx context.Context, zone *zones.Zone) error {
-	result := s.db.Select("*").Save(zone)
+	result := s.db.Omit(clause.Associations).Save(zone)
 	if result.Error != nil {
 		log.WithContext(ctx).Errorf("failed to update zone to store: %v", result.Error)
 		return status.Errorf(status.Internal, "failed to update zone to store")

This ensures that records are only modified through their dedicated CRUD operations (CreateDNSRecord, UpdateDNSRecord, etc.).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 783a120 and d4416ed.

📒 Files selected for processing (4)
  • management/internals/modules/zones/records/manager/manager.go (1 hunks)
  • management/server/store/sql_store.go (3 hunks)
  • management/server/store/sql_store_test.go (2 hunks)
  • management/server/store/store.go (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.
📚 Learning: 2025-11-28T12:20:47.254Z
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.

Applied to files:

  • management/server/store/sql_store_test.go
  • management/server/store/store.go
  • management/server/store/sql_store.go
  • management/internals/modules/zones/records/manager/manager.go
🧬 Code graph analysis (2)
management/server/store/store.go (3)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
shared/management/http/api/types.gen.go (1)
  • Zone (1910-1931)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
management/server/store/sql_store.go (4)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
shared/management/status/error.go (4)
  • Error (54-57)
  • Errorf (70-75)
  • NewZoneNotFoundError (257-259)
  • NewDNSRecordNotFoundError (262-264)
management/server/store/store.go (2)
  • LockingStrength (42-42)
  • LockingStrengthNone (49-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (26)
  • GitHub Check: Management / Benchmark (API) (amd64, sqlite)
  • GitHub Check: Management / Unit (amd64, sqlite)
  • GitHub Check: Management / Integration (amd64, sqlite)
  • GitHub Check: Management / Unit (amd64, postgres)
  • GitHub Check: Management / Integration (amd64, postgres)
  • GitHub Check: Management / Unit (amd64, mysql)
  • GitHub Check: Management / Benchmark (API) (amd64, postgres)
  • GitHub Check: Management / Benchmark (amd64, sqlite)
  • GitHub Check: Relay / Unit (386)
  • GitHub Check: Management / Benchmark (amd64, postgres)
  • GitHub Check: Client / Unit (amd64)
  • GitHub Check: Relay / Unit (amd64, -race)
  • GitHub Check: Signal / Unit (amd64)
  • GitHub Check: Client / Unit (386)
  • GitHub Check: Client (Docker) / Unit
  • GitHub Check: release
  • GitHub Check: release_ui_darwin
  • GitHub Check: iOS / Build
  • GitHub Check: Client / Unit
  • GitHub Check: Client / Unit
  • GitHub Check: Android / Build
  • GitHub Check: Windows
  • GitHub Check: Darwin
  • GitHub Check: Linux
  • GitHub Check: JS / Lint
  • GitHub Check: Client / Unit
🔇 Additional comments (16)
management/internals/modules/zones/records/manager/manager.go (6)

19-31: LGTM!

Clean constructor and dependency injection pattern for the records manager.


33-55: LGTM!

Read operations correctly validate permissions and use appropriate locking strength for queries.


57-101: LGTM!

The CreateRecord implementation correctly:

  • Validates permissions before mutation
  • Uses transactional boundaries with appropriate locking
  • Validates record conflicts within the transaction
  • Increments network serial to propagate changes
  • Triggers peer updates asynchronously (as addressed from previous review)

103-160: LGTM!

UpdateRecord correctly:

  • Fetches existing record with appropriate locking
  • Only re-validates conflicts when name/type/content changes (TTL changes correctly bypass conflict validation)
  • Increments serial and triggers peer updates for all changes including TTL-only

162-207: LGTM!

DeleteRecord correctly handles transactional deletion with proper locking, event logging, and peer notification.


209-236: LGTM!

The zone membership check correctly enforces label boundary (exact match or proper subdomain with dot prefix), and the CNAME/duplicate conflict detection logic is sound.

management/server/store/sql_store_test.go (3)

25-26: LGTM!

Imports correctly added for the new zones and records test coverage.


3723-3879: LGTM!

Comprehensive test coverage for zone CRUD operations with proper table-driven tests for error cases and field validation.


3881-4126: LGTM!

Thorough test coverage for DNS record operations including:

  • Multiple record types (A, AAAA, CNAME)
  • Name-based filtering
  • Bulk deletion
  • Proper error handling verification
management/server/store/store.go (3)

26-27: LGTM!

Imports correctly added for the new zone and record types used in the interface.


208-213: LGTM!

Zone interface methods follow the established patterns in the Store interface with consistent parameter ordering and locking strength handling for read operations.


215-222: LGTM!

DNS record interface methods are comprehensive and properly scoped to zones:

  • CRUD operations with correct parameter ordering
  • Zone-scoped bulk operations (GetZoneDNSRecords, DeleteZoneDNSRecords)
  • Name-based filtering (GetZoneDNSRecordsByName)
management/server/store/sql_store.go (4)

30-31: LGTM: Imports and migration setup.

The new zones and records imports are correctly added, and the entities are properly registered in AutoMigrate for schema management.

Also applies to: 118-118


4134-4134: Previous review issues resolved.

The typo in the error message (line 4134) has been corrected, and DeleteDNSRecord now properly scopes deletion by all three identifiers (accountID, zoneID, and recordID), fixing the data integrity concern from the earlier review.

Also applies to: 4221-4222


4131-4139: LGTM: Zone and DNS record CRUD operations.

The remaining CRUD implementations follow consistent patterns throughout the file:

  • Proper locking strength support on retrieval operations
  • Appropriate error logging and status error types
  • Correct use of NewZoneNotFoundError and NewDNSRecordNotFoundError
  • Consistent query conditions (e.g., accountAndIDQueryCondition for scoped operations)
  • DeleteZoneDNSRecords correctly omits RowsAffected check since deleting zero records for a zone is valid

The implementations integrate well with the existing store architecture.

Also applies to: 4165-4199, 4201-4219, 4235-4295


4151-4163: The zone deletion is properly handled at the manager layer. Before DeleteZone is called in the store (line 162 in manager.go), DeleteZoneDNSRecords is invoked first (line 157), ensuring all associated DNS records are removed within a transaction. This orchestration prevents both foreign key constraint errors and orphaned records. The simple delete implementation in sql_store.go is appropriate since cascade logic is correctly managed at the manager layer.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
management/internals/modules/zones/manager/manager.go (1)

184-193: Fix closure capture of record when queuing delete events

Inside DeleteZone, the callbacks you append to eventsToStore close over the loop variable record. When the loop finishes, all callbacks will see the last record value, so you’ll emit incorrect IDs/metadata for most deleted records.

Capture the current record in a new variable inside the loop:

-	for _, record := range records {
-		eventsToStore = append(eventsToStore, func() {
-			meta := record.EventMeta(zone.ID, zone.Name)
-			m.accountManager.StoreEvent(ctx, userID, record.ID, accountID, activity.DNSRecordDeleted, meta)
-		})
-	}
+	for _, record := range records {
+		rec := record
+		eventsToStore = append(eventsToStore, func() {
+			meta := rec.EventMeta(zone.ID, zone.Name)
+			m.accountManager.StoreEvent(ctx, userID, rec.ID, accountID, activity.DNSRecordDeleted, meta)
+		})
+	}
🧹 Nitpick comments (3)
management/internals/modules/zones/manager/manager.go (1)

123-145: Consider incrementing network serial on UpdateZone for consistency with DeleteZone

DeleteZone increments the network serial inside the transaction and then calls UpdateAccountPeers, but UpdateZone only calls UpdateAccountPeers without bumping the serial. For zones that already have records, toggling Enabled, EnableSearchDomain, or distribution groups changes peers’ DNS view just like deletion.

To keep the network serial consistent with other config changes (e.g., DNS settings, routes, zone deletion), consider adding:

 		if err = transaction.UpdateZone(ctx, zone); err != nil {
 			return fmt.Errorf("failed to update zone: %w", err)
 		}
+
+		if err = transaction.IncrementNetworkSerial(ctx, accountID); err != nil {
+			return fmt.Errorf("failed to increment network serial: %w", err)
+		}

and leaving CreateZone as-is, since empty zones are filtered out of the network map until records exist (records manager will handle serial bumps). Based on learnings, empty zones are skipped in network map generation.

management/internals/modules/zones/manager/manager_test.go (1)

455-493: Tighten DeleteZone tests to assert per-record event IDs/metadata

Right now TestManagerImpl_DeleteZone/success with records only checks that StoreEvent was called three times and that initiator/account IDs match. This won’t catch issues in how record events are built (e.g., the closure-capture bug in DeleteZone).

Consider extending the test to verify:

  • You receive two DNSRecordDeleted events and one DNSZoneDeleted.
  • The record deletion events reference the expected record IDs and zone metadata.

Capturing the (activityID, targetID) pairs into a slice and asserting them against the created records would harden the test and prevent regressions in the event-building logic.

management/server/store/store.go (1)

209-222: Zone/DNS record Store methods are consistent; consider optional re-grouping

The new zone and DNS record methods follow existing CRUD and locking patterns (ctx first, LockingStrength on read paths, account/zone/record IDs ordered consistently). From a contract perspective this looks solid. If you want slightly better cohesion, you could optionally move these methods nearer to other DNS-related operations (GetAccountDNSSettings, NameServerGroup methods) to keep related concerns clustered.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d4416ed and 5e5312f.

📒 Files selected for processing (9)
  • management/internals/controllers/network_map/controller/controller.go (14 hunks)
  • management/internals/modules/zones/manager/manager.go (1 hunks)
  • management/internals/modules/zones/manager/manager_test.go (1 hunks)
  • management/internals/server/modules.go (2 hunks)
  • management/server/account.go (1 hunks)
  • management/server/account_test.go (3 hunks)
  • management/server/store/sql_store.go (3 hunks)
  • management/server/store/sql_store_test.go (2 hunks)
  • management/server/store/store.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • management/server/account_test.go
  • management/server/store/sql_store.go
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.
📚 Learning: 2025-11-28T12:20:47.254Z
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.

Applied to files:

  • management/server/account.go
  • management/internals/server/modules.go
  • management/internals/modules/zones/manager/manager_test.go
  • management/server/store/sql_store_test.go
  • management/internals/controllers/network_map/controller/controller.go
  • management/internals/modules/zones/manager/manager.go
  • management/server/store/store.go
🧬 Code graph analysis (4)
management/server/account.go (1)
shared/management/status/error.go (2)
  • Errorf (70-75)
  • InvalidArgument (27-27)
management/internals/server/modules.go (5)
management/internals/modules/zones/interface.go (1)
  • Manager (7-13)
management/internals/modules/zones/records/interface.go (1)
  • Manager (7-13)
management/internals/server/container.go (1)
  • Create (6-10)
management/internals/modules/zones/manager/manager.go (1)
  • NewManager (24-31)
management/internals/modules/zones/records/manager/manager.go (1)
  • NewManager (25-31)
management/server/store/sql_store_test.go (3)
management/internals/modules/zones/zone.go (1)
  • NewZone (24-34)
shared/management/status/error.go (3)
  • Error (54-57)
  • FromError (78-87)
  • NotFound (21-21)
management/internals/modules/zones/records/record.go (1)
  • NewRecord (31-41)
management/internals/controllers/network_map/controller/controller.go (4)
shared/management/proto/management.pb.go (6)
  • CustomZone (2680-2689)
  • CustomZone (2704-2704)
  • CustomZone (2719-2721)
  • SimpleRecord (2752-2762)
  • SimpleRecord (2777-2777)
  • SimpleRecord (2792-2794)
management/server/types/account.go (1)
  • LookupMap (57-57)
management/internals/modules/zones/records/record.go (3)
  • RecordTypeA (16-16)
  • RecordTypeAAAA (17-17)
  • RecordTypeCNAME (18-18)
dns/dns.go (1)
  • DefaultClass (21-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (24)
  • GitHub Check: Signal / Unit (386)
  • GitHub Check: Management / Unit (amd64, postgres)
  • GitHub Check: Signal / Unit (amd64)
  • GitHub Check: Relay / Unit (amd64, -race)
  • GitHub Check: Management / Unit (amd64, sqlite)
  • GitHub Check: Client / Unit (386)
  • GitHub Check: Relay / Unit (386)
  • GitHub Check: Management / Unit (amd64, mysql)
  • GitHub Check: Management / Benchmark (amd64, postgres)
  • GitHub Check: Client (Docker) / Unit
  • GitHub Check: Management / Benchmark (amd64, sqlite)
  • GitHub Check: Client / Unit (amd64)
  • GitHub Check: Windows
  • GitHub Check: Darwin
  • GitHub Check: Linux
  • GitHub Check: iOS / Build
  • GitHub Check: Android / Build
  • GitHub Check: Client / Unit
  • GitHub Check: release_ui_darwin
  • GitHub Check: release_ui
  • GitHub Check: release
  • GitHub Check: Client / Unit
  • GitHub Check: JS / Lint
  • GitHub Check: Client / Unit
🔇 Additional comments (4)
management/internals/server/modules.go (1)

10-13: Zones/records manager wiring matches existing pattern

The ZonesManager and RecordsManager accessors follow the established lazy Create pattern and pass the expected dependencies (Store, AccountManager, PermissionsManager, and DNS domain for zones). No issues spotted with init order or signatures.

Also applies to: 126-136

management/server/store/sql_store_test.go (1)

25-27: Zone and DNS record store tests look comprehensive

The new tests cover full CRUD and query flows for zones and DNS records (by ID, by domain/name, and bulk deletion) against the SQL store and assert expected NotFound behavior. This gives solid coverage for the new persistence APIs.

Also applies to: 3722-4194

management/internals/controllers/network_map/controller/controller.go (1)

179-206: Custom DNS zones wiring and filtering logic look correct

The new flow—fetching account zones once, deriving per‑peer peerGroups, then building customZones via filterPeerAppliedZones and appending the per‑account peersCustomZone—is consistent across sendUpdateAccountPeers, UpdateAccountPeer, GetValidatedPeerWithMap, GetNetworkMap, and the experimental builder path.

filterPeerAppliedZones in particular:

  • Skips disabled zones and zones with no records, matching the design that empty zones shouldn’t affect the network map.
  • Applies distribution groups correctly using types.LookupMap.
  • Restricts to supported record types (A/AAAA/CNAME), formats names/RDATA as FQDNs with dns.Fqdn, and uses nbdns.DefaultClass and EnableSearchDomain as expected.

Overall, the multi‑zone DNS integration into network map generation looks solid.

Also applies to: 231-241, 331-356, 455-479, 834-857, 871-929

management/server/store/store.go (1)

25-27: Zones/records imports look correct

Adding zones and zones/records here cleanly wires the new domain types into the store layer; import style is consistent with existing internal modules and doesn’t introduce any obvious coupling or versioning issues.

Signed-off-by: bcmmbaga <[email protected]>
Signed-off-by: bcmmbaga <[email protected]>
Signed-off-by: bcmmbaga <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
management/server/types/account.go (1)

1787-1847: filterPeerAppliedZones helper is duplicated; consider centralizing

This free function is effectively identical to NetworkMapBuilder.filterPeerAppliedZones in networkmapbuilder.go. To avoid future drift in DNS record handling (e.g., when adding new record types or changing search-domain behaviour), it would be better to have a single shared implementation and call it from both the Account and NetworkMapBuilder paths.

🧹 Nitpick comments (3)
management/internals/controllers/network_map/controller/controller_test.go (1)

610-628: Test assertions may be order-dependent.

The test compares zones and records by index (tt.expected[i], result[i]), which assumes filterPeerAppliedZones returns zones in the same order as the input accountZones. If the implementation changes iteration order (e.g., using maps internally), tests could fail unexpectedly.

Consider using ElementsMatch from testify for order-independent comparison, or document that order preservation is a contract of filterPeerAppliedZones.

management/internals/controllers/network_map/controller/controller.go (1)

898-901: Consider adding zone context to the warning log.

The warning at line 899 logs record.ID but doesn't include the zone context, which could make debugging harder when multiple zones have records.

-			log.WithContext(ctx).Warnf("unknown DNS record type %s for record %s", record.Type, record.ID)
+			log.WithContext(ctx).Warnf("unknown DNS record type %s for record %s in zone %s", record.Type, record.ID, zone.ID)
management/server/types/networkmapbuilder.go (1)

2036-2095: Deduplicate NetworkMapBuilder.filterPeerAppliedZones with the Account helper

This method’s logic (enabled check, len(zone.Records) > 0 guard, distribution-group filtering, and record-type mapping) is identical to the package-level filterPeerAppliedZones in account.go. Duplicating this behaviour in two places risks divergence if you add record types or tweak zone semantics later. Consider extracting a shared helper (e.g., in a small DNS/zones utility) and calling it from both paths. The len(zone.Records) == 0 guard also matches the intended “empty zones don’t affect network maps” behaviour. Based on learnings, empty zones are safe to ignore.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e534845 and dbeeb72.

📒 Files selected for processing (8)
  • management/internals/controllers/network_map/controller/controller.go (14 hunks)
  • management/internals/controllers/network_map/controller/controller_test.go (2 hunks)
  • management/server/account_test.go (5 hunks)
  • management/server/http/handlers/peers/peers_handler.go (2 hunks)
  • management/server/types/account.go (7 hunks)
  • management/server/types/networkmap.go (2 hunks)
  • management/server/types/networkmap_golden_test.go (18 hunks)
  • management/server/types/networkmapbuilder.go (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • management/server/http/handlers/peers/peers_handler.go
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.
📚 Learning: 2025-11-28T12:20:47.254Z
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.

Applied to files:

  • management/server/types/networkmap.go
  • management/internals/controllers/network_map/controller/controller.go
  • management/internals/controllers/network_map/controller/controller_test.go
  • management/server/types/account.go
  • management/server/types/networkmapbuilder.go
  • management/server/account_test.go
  • management/server/types/networkmap_golden_test.go
🧬 Code graph analysis (6)
management/server/types/networkmap.go (2)
management/server/telemetry/accountmanager_metrics.go (1)
  • AccountManagerMetrics (11-17)
management/server/types/networkmapbuilder.go (1)
  • NetworkMapCache (35-61)
management/internals/controllers/network_map/controller/controller.go (5)
route/route.go (1)
  • ID (48-48)
shared/management/proto/management.pb.go (6)
  • CustomZone (2680-2689)
  • CustomZone (2704-2704)
  • CustomZone (2719-2721)
  • SimpleRecord (2752-2762)
  • SimpleRecord (2777-2777)
  • SimpleRecord (2792-2794)
management/server/types/account.go (1)
  • LookupMap (59-59)
management/internals/modules/zones/records/record.go (3)
  • RecordTypeA (16-16)
  • RecordTypeAAAA (17-17)
  • RecordTypeCNAME (18-18)
dns/dns.go (1)
  • DefaultClass (21-21)
management/internals/controllers/network_map/controller/controller_test.go (4)
management/server/types/account.go (1)
  • LookupMap (59-59)
route/route.go (1)
  • ID (48-48)
management/internals/modules/zones/records/record.go (3)
  • RecordTypeA (16-16)
  • RecordTypeAAAA (17-17)
  • RecordTypeCNAME (18-18)
dns/dns.go (1)
  • DefaultClass (21-21)
management/server/types/account.go (2)
dns/dns.go (3)
  • CustomZone (43-52)
  • SimpleRecord (55-66)
  • DefaultClass (21-21)
management/internals/modules/zones/records/record.go (3)
  • RecordTypeA (16-16)
  • RecordTypeAAAA (17-17)
  • RecordTypeCNAME (18-18)
management/server/types/networkmapbuilder.go (3)
management/server/types/account.go (1)
  • LookupMap (59-59)
management/internals/modules/zones/records/record.go (3)
  • RecordTypeA (16-16)
  • RecordTypeAAAA (17-17)
  • RecordTypeCNAME (18-18)
dns/dns.go (1)
  • DefaultClass (21-21)
management/server/account_test.go (1)
management/server/types/settings.go (2)
  • Settings (10-55)
  • ExtraSettings (82-99)
🔇 Additional comments (11)
management/internals/controllers/network_map/controller/controller_test.go (1)

119-630: Comprehensive test coverage for zone filtering logic.

The test thoroughly covers:

  • Empty peer groups and zones
  • All DNS record types (A, AAAA, CNAME)
  • Search domain enabled/disabled states
  • Disabled zones filtering
  • Empty record zones filtering
  • Multiple zones with mixed access
  • Peer membership across multiple groups

The CNAME RData correctly includes trailing dot via dns.Fqdn() transformation (line 418).

management/server/types/networkmap.go (1)

25-35: Signature extension looks correct for multi-zone DNS support.

The accountZones parameter is appropriately added and forwarded to NetworkMapCache.GetPeerNetworkMap. This aligns with the network map generation flow where account zones are filtered per-peer based on group membership.

management/internals/controllers/network_map/controller/controller.go (2)

860-920: Well-structured zone filtering implementation.

The filterPeerAppliedZones function correctly:

  • Returns early for empty peer groups (line 864-866)
  • Filters disabled zones and zones without records (line 869) - aligning with the learning that empty zones don't affect network maps
  • Uses set lookup for efficient group membership check (lines 873-879)
  • Properly converts record types to DNS wire types (lines 890-901)
  • Applies dns.Fqdn() to ensure proper DNS name formatting for CNAME content, record names, and zone domains

202-206: Consistent error handling for account zones retrieval.

The error handling pattern is consistently applied across all call sites (sendUpdateAccountPeers, UpdateAccountPeer, GetValidatedPeerWithMap, GetNetworkMap), logging the error and returning it appropriately.

management/server/types/networkmap_golden_test.go (1)

72-72: Golden test signatures correctly updated.

All GetPeerNetworkMap calls are consistently updated to include the new accountZones parameter (passed as nil), maintaining test focus on network map structure while the zone filtering is tested separately in controller_test.go.

management/server/account_test.go (3)

2062-2089: Good test for DNS domain conflict validation.

The test correctly validates that setting DNSDomain to a value that conflicts with an existing custom DNS zone's domain produces an appropriate error. This prevents configuration conflicts that could cause DNS resolution issues.


1680-1680: GetRoutesToSync signature updated to include peer groups.

The updated call now passes account.GetPeerGroups("peer-2") to enable route filtering based on peer group membership, which aligns with the broader multi-zone DNS and access control changes.


401-401: GetPeerNetworkMap call updated consistently.

The signature update passes nil for accountZones, which is appropriate for this test that focuses on peer login expiration behavior rather than DNS zone filtering.

management/server/types/networkmapbuilder.go (1)

941-965: Multi-zone DNS wiring in NetworkMapBuilder looks consistent

Passing accountZones into GetPeerNetworkMap/assembleNetworkMap and combining the per-peer custom zone with filterPeerAppliedZones output under ServiceEnable preserves previous DNSConfig semantics while correctly scoping account zones by peer group membership and ignoring empty/disabled zones. No functional issues from my side.

Also applies to: 980-1051

management/server/types/account.go (2)

149-167: Reusing peerGroups in GetRoutesToSync keeps semantics and saves work

Passing peerGroups into GetRoutesToSync and using it in filterRoutesByGroups preserves the previous behaviour while avoiding recomputing group membership inside the function. This is a clean, minimal change.


270-351: Account-level DNS zones are correctly filtered before inclusion in DNSConfig

Computing peerGroups := a.GetPeerGroups(peerID) once and reusing it for both GetRoutesToSync and filterPeerAppliedZones(ctx, accountZones, peerGroups) ensures:

  • only peers in a zone’s DistributionGroups see that zone, and
  • zones that are disabled or have no records are ignored.

This aligns with the intended behaviour that empty zones don’t affect network maps and so don’t require triggering peer updates. Based on learnings, this is the expected contract.

# Conflicts:
#	management/server/store/sql_store.go
Signed-off-by: bcmmbaga <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
management/server/store/sql_store.go (1)

4070-4153: Zone CRUD implementation matches store patterns; consider small robustness tweaks

The zone create/update/delete/get methods follow existing store conventions (scoping by account_id, preloading associations, and using specific ZoneNotFound errors), which is consistent and correct.

Two optional improvements you may want to consider:

  • Add nil‑argument validation for CreateZone / UpdateZone (mirroring CreateGroup / UpdateGroup) to fail fast on programmer errors instead of relying on GORM behavior with nil pointers.
  • If zone domains are stored normalized (e.g., lower‑cased on write), consider normalizing the domain parameter in GetZoneByDomain for case‑insensitive behavior and consistency with other domain lookups.

Example for the first point:

 func (s *SqlStore) CreateZone(ctx context.Context, zone *zones.Zone) error {
-	result := s.db.Create(zone)
+	if zone == nil {
+		return status.Errorf(status.InvalidArgument, "zone is nil")
+	}
+
+	result := s.db.Create(zone)
@@
 func (s *SqlStore) UpdateZone(ctx context.Context, zone *zones.Zone) error {
-	result := s.db.Select("*").Save(zone)
+	if zone == nil {
+		return status.Errorf(status.InvalidArgument, "zone is nil")
+	}
+
+	result := s.db.Select("*").Save(zone)

Also, please double‑check that zone deletion will not leave orphan DNS records (either via FK cascade or higher‑level use of DeleteZoneDNSRecords), as that invariant isn’t visible from this file alone.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dbeeb72 and a98dfb3.

📒 Files selected for processing (6)
  • management/internals/controllers/network_map/controller/controller.go (12 hunks)
  • management/internals/server/modules.go (2 hunks)
  • management/server/http/handler.go (3 hunks)
  • management/server/store/sql_store.go (3 hunks)
  • management/server/types/account_test.go (2 hunks)
  • management/server/types/networkmapbuilder.go (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • management/internals/server/modules.go
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.
📚 Learning: 2025-11-28T12:20:47.254Z
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.

Applied to files:

  • management/server/http/handler.go
  • management/internals/controllers/network_map/controller/controller.go
  • management/server/types/networkmapbuilder.go
  • management/server/store/sql_store.go
  • management/server/types/account_test.go
🧬 Code graph analysis (3)
management/server/http/handler.go (4)
management/internals/modules/zones/records/interface.go (1)
  • Manager (7-13)
management/internals/modules/zones/interface.go (1)
  • Manager (7-13)
management/internals/modules/zones/manager/api.go (1)
  • RegisterEndpoints (20-30)
management/internals/modules/zones/records/manager/api.go (1)
  • RegisterEndpoints (20-30)
management/internals/controllers/network_map/controller/controller.go (1)
shared/management/proto/management.pb.go (3)
  • CustomZone (2680-2689)
  • CustomZone (2704-2704)
  • CustomZone (2719-2721)
management/server/types/account_test.go (3)
management/server/types/account.go (1)
  • LookupMap (59-59)
management/internals/modules/zones/records/record.go (3)
  • RecordTypeA (16-16)
  • RecordTypeAAAA (17-17)
  • RecordTypeCNAME (18-18)
dns/dns.go (1)
  • DefaultClass (21-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (25)
  • GitHub Check: Management / Benchmark (API) (amd64, sqlite)
  • GitHub Check: Management / Unit (amd64, postgres)
  • GitHub Check: Management / Unit (amd64, sqlite)
  • GitHub Check: Management / Benchmark (API) (amd64, postgres)
  • GitHub Check: Management / Unit (amd64, mysql)
  • GitHub Check: Client / Unit (386)
  • GitHub Check: Management / Integration (amd64, postgres)
  • GitHub Check: Relay / Unit (amd64, -race)
  • GitHub Check: Client (Docker) / Unit
  • GitHub Check: Client / Unit (amd64)
  • GitHub Check: Relay / Unit (386)
  • GitHub Check: Management / Benchmark (amd64, sqlite)
  • GitHub Check: Management / Benchmark (amd64, postgres)
  • GitHub Check: Client / Unit
  • GitHub Check: JS / Lint
  • GitHub Check: Client / Unit
  • GitHub Check: Windows
  • GitHub Check: iOS / Build
  • GitHub Check: release_ui
  • GitHub Check: Darwin
  • GitHub Check: release
  • GitHub Check: Android / Build
  • GitHub Check: Linux
  • GitHub Check: Client / Unit
  • GitHub Check: release_ui_darwin
🔇 Additional comments (6)
management/server/types/account_test.go (1)

1244-1754: Excellent test coverage for zone filtering logic.

This test comprehensively validates the filterPeerAppliedZones function across multiple scenarios including group-based access control, zone state filtering (disabled/empty), record type handling (A/AAAA/CNAME), and DNS formatting conventions. The test structure is clear and each scenario is well-documented.

management/server/types/networkmapbuilder.go (1)

1028-1049: DNS zone assembly logic looks correct.

The implementation properly:

  • Converts peer groups from slice to lookup map for efficient filtering
  • Handles the legacy peersCustomZone (if domain is present)
  • Filters account zones based on peer's group membership
  • Combines both zone types into the final DNS configuration

The peerGroups map is rebuilt on each call, which ensures consistency with the latest cache state and is acceptable given its lightweight nature.

management/internals/controllers/network_map/controller/controller.go (1)

200-204: Consistent integration of account zones across all network map generation paths.

The GetAccountZones calls are properly integrated with consistent error handling across:

  • Bulk peer updates (sendUpdateAccountPeers)
  • Single peer updates (UpdateAccountPeer)
  • Peer validation with map (GetValidatedPeerWithMap)
  • Network map retrieval (GetNetworkMap)

All call sites follow the same pattern: retrieve zones, log errors, and propagate failures appropriately.

Also applies to: 343-347, 448-452, 824-828

management/server/http/handler.go (1)

17-20: Zones and DNS records HTTP wiring looks consistent and correct

The added zones/records imports, new zManager and rManager parameters in NewAPIHandler, and the RegisterEndpoints calls all line up with the managers’ RegisterEndpoints signatures and are placed on the same authenticated, metered router as existing APIs. No issues from a wiring perspective.

Also applies to: 72-73, 142-143

management/server/store/sql_store.go (2)

30-31: Zones and DNS record models are correctly integrated into the store

Importing zones and records and adding &zones.Zone{} / &records.Record{} to AutoMigrate follows the existing pattern for other managed entities and ensures persistence is available for the new APIs.

Also applies to: 117-118


4155-4249: DNS record CRUD looks correct and fixes earlier scoping concern

The DNS record methods are consistent with other store helpers:

  • CreateDNSRecord / UpdateDNSRecord follow the established logging + status.Internal pattern.
  • DeleteDNSRecord now scopes by account_id, zone_id, and id, aligning its behavior with GetDNSRecordByID and resolving the earlier inconsistency about missing zone_id in the delete path.
  • Zone‑scoped list and delete helpers (GetZoneDNSRecords*, DeleteZoneDNSRecords) are straightforward and correctly filter by account_id and zone_id.

Given these are bulk/maintenance operations, returning success from DeleteZoneDNSRecords even when no rows are affected is reasonable and matches how some other batch deletes behave in this store.

# Conflicts:
#	management/server/account.go
#	management/server/account_test.go
#	management/server/store/sql_store_test.go
Signed-off-by: bcmmbaga <[email protected]>
@sonarqubecloud
Copy link

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
management/server/store/sql_store.go (1)

112-121: Add explicit database-level FK constraint with CASCADE delete for records.Record → zones.Zone relationship.

The deletion of records.Record is currently handled only at the application level (in manager.DeleteZone via an explicit transaction), not at the database schema level. While this works in the managed code path, if sql_store.DeleteZone is called directly (bypassing the manager), orphaned records will remain. Add onDelete:cascade to the Zone.Records GORM tag or define explicit database constraints to ensure referential integrity at the schema level.

🧹 Nitpick comments (7)
management/server/account_test.go (2)

400-404: GetPeerNetworkMap arg update: consider passing peer-groups (and rename customZone) to avoid “nil means ignore” drift.

If the new signature is effectively (ctx, peerID, accountZones, peerGroups, ...), hardcoding nil for peer-groups can mask regressions in peer-group filtering logic. Also customZone is confusing if it’s actually a zone list.

- customZone := account.GetPeersCustomZone(context.Background(), "netbird.io")
- networkMap := account.GetPeerNetworkMap(context.Background(), testCase.peerID, customZone, nil, validatedPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil)
+ accountZones := account.GetPeersCustomZone(context.Background(), "netbird.io")
+ peerGroups := account.GetPeerGroups(testCase.peerID)
+ networkMap := account.GetPeerNetworkMap(context.Background(), testCase.peerID, accountZones, peerGroups, validatedPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil)

2099-2126: DNSDomain conflict test is good; make it robust against “effective zones only” and domain normalization.

If conflict validation ever starts considering only zones that “apply” (e.g., enabled + matching groups + possibly having records), this could become a false negative since the zone is created with empty DistributionGroups and no records. Also, if validation normalizes domains (lowercase, trailing dot), asserting against raw strings might drift.

Concrete hardening options:

  • Add a minimal record (if the schema allows) so the zone is unquestionably “active”.
  • Ensure the test uses a normalized domain variant consistent with validation (e.g., lowercased / no trailing dot), or assert using a structured error type if available instead of err.Error().
management/server/store/store.go (2)

25-28: Consider import aliasing to avoid “records” ambiguity at call sites (Line 25-Line 28).
With dns already in this file, importing zones/records as plain records can make call sites read a bit ambiguously (DNS “records” vs package “records”). Aliasing to zonesrecords (or similar) tends to reduce confusion.


209-223: Store interface additions look coherent; consider locking + consistency for domain lookup (Line 209-Line 223).
GetZoneByDomain(ctx, accountID, domain) lacks lockStrength while other “get” methods expose it. If this is expected to be usable in transactional flows (or needs consistent read semantics), consider adding lockStrength for symmetry.

management/server/store/sql_store.go (3)

29-32: Imports OK; consider aliasing zones/records for readability (Line 29-Line 32).
Same note as store.go: records can read ambiguously in DNS-heavy code; an alias may improve clarity.


4237-4251: GetZoneDNSRecordsByName: verify name normalization expectations (Line 4237-Line 4251).
DNS labels are case-insensitive in practice; if the system normalizes record names elsewhere, this is fine—otherwise, consider normalizing name (or documenting exact-match requirement).


4253-4261: DeleteZoneDNSRecords: OK; consider whether callers need a not-found signal (Line 4253-Line 4261).
Bulk delete returning nil when nothing matched is usually fine—just ensure manager/API expectations align.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a98dfb3 and 176fd64.

📒 Files selected for processing (5)
  • management/server/account.go (3 hunks)
  • management/server/account_test.go (5 hunks)
  • management/server/store/sql_store.go (3 hunks)
  • management/server/store/sql_store_test.go (2 hunks)
  • management/server/store/store.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • management/server/account.go
  • management/server/store/sql_store_test.go
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.
📚 Learning: 2025-11-28T12:20:47.254Z
Learnt from: bcmmbaga
Repo: netbirdio/netbird PR: 4849
File: management/internals/modules/zones/manager/manager.go:55-86
Timestamp: 2025-11-28T12:20:47.254Z
Learning: In the NetBird management server, DNS zones without records are automatically filtered out in network map generation (filterPeerAppliedZones in management/internals/controllers/network_map/controller/controller.go checks `len(zone.Records) == 0`). Therefore, CreateZone operations don't need to call UpdateAccountPeers since empty zones don't affect the network map.

Applied to files:

  • management/server/account_test.go
  • management/server/store/store.go
  • management/server/store/sql_store.go
🧬 Code graph analysis (2)
management/server/store/store.go (3)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
shared/management/http/api/types.gen.go (1)
  • Zone (1910-1931)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
management/server/store/sql_store.go (4)
management/internals/modules/zones/zone.go (1)
  • Zone (13-22)
management/internals/modules/zones/records/record.go (1)
  • Record (21-29)
shared/management/status/error.go (4)
  • Error (54-57)
  • Errorf (70-75)
  • NewZoneNotFoundError (257-259)
  • NewDNSRecordNotFoundError (262-264)
management/server/store/store.go (2)
  • LockingStrength (42-42)
  • LockingStrengthNone (49-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (23)
  • GitHub Check: Management / Integration (amd64, sqlite)
  • GitHub Check: Management / Benchmark (API) (amd64, postgres)
  • GitHub Check: Management / Unit (amd64, postgres)
  • GitHub Check: Management / Integration (amd64, postgres)
  • GitHub Check: Management / Benchmark (amd64, sqlite)
  • GitHub Check: Management / Benchmark (API) (amd64, sqlite)
  • GitHub Check: Client / Unit (amd64)
  • GitHub Check: Management / Benchmark (amd64, postgres)
  • GitHub Check: Relay / Unit (386)
  • GitHub Check: Client / Unit (386)
  • GitHub Check: Signal / Unit (386)
  • GitHub Check: Management / Unit (amd64, sqlite)
  • GitHub Check: Relay / Unit (amd64, -race)
  • GitHub Check: Management / Unit (amd64, mysql)
  • GitHub Check: Client (Docker) / Unit
  • GitHub Check: Client / Unit
  • GitHub Check: JS / Lint
  • GitHub Check: Windows
  • GitHub Check: Linux
  • GitHub Check: Client / Unit
  • GitHub Check: Android / Build
  • GitHub Check: Client / Unit
  • GitHub Check: release
🔇 Additional comments (9)
management/server/account_test.go (2)

24-32: Importing zones is fine; keep package naming unambiguous.

zones.Zone reads clearly and avoids alias noise—no issues here.


1680-1692: Good test update to include account.GetPeerGroups(...) in GetRoutesToSync calls.

This better reflects the real call path after peer-group filtering was introduced.

management/server/store/sql_store.go (7)

4116-4134: GetZoneByID: LGTM (locking + Preload) — verify GORM pointer behavior is intentional (Line 4116-Line 4134).
Pattern matches other store methods (var x *T; Take(&x, ...)). Just ensure this is a deliberate GORM usage style across the repo.


4151-4165: GetAccountZones: LGTM (locking + Preload) (Line 4151-Line 4165).


4187-4199: DeleteDNSRecord: good scoping by account_id + zone_id + id (Line 4187-Line 4199).
This matches GetDNSRecordByID scoping and avoids cross-zone deletes by record ID alone.


4201-4219: GetDNSRecordByID: LGTM (scoped query + optional locking) (Line 4201-Line 4219).


4221-4235: GetZoneDNSRecords: LGTM (Line 4221-Line 4235).


4082-4090: No changes needed. The CreateZone function correctly handles zones without records. Records are never populated at zone creation time—they are managed through a separate Records API. The zone parameter is already validated by the caller before reaching this store method, and GORM does not persist nil/empty associations. The learning from the codebase confirms that zones without records are filtered downstream in network map generation, so no defensive checks or association omission is required here.

Likely an incorrect or invalid review comment.


4102-4114: DeleteZone at the store layer can leave orphan DNS records, but this is mitigated by the manager layer.

The SqlStore.DeleteZone method (lines 4102-4114) deletes only the zone without handling related records. However, examining management/internals/modules/zones/manager/manager.go, the DeleteZone manager function wraps the store operation in a transaction that explicitly calls DeleteZoneDNSRecords before deleting the zone. While the public API is safe, the store method itself exposes a data integrity risk if called directly. Consider adding explicit record deletion within DeleteZone for consistency and safety, or document that this method must be called within a transaction that handles records.

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