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 & records CRUD (A/AAAA/CNAME), validation, REST endpoints, client API and OpenAPI updates.
    • Network maps now accept multiple custom zones and apply zone records based on DNS domain.
    • Server modules and storage persist zones and records; API wiring exposes zone/record management.
  • Tests

    • Extensive unit & integration tests for zones, records, managers, store, client, and network-map zone logic.
  • Activity

    • New audit events for zone/record create/update/delete.
  • Chores

    • Domain validation helper and not-found error helpers added.

✏️ 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 a DNS zones and records subsystem and integrates multi-zone custom DNS handling into network-map generation; extends store, server wiring, REST client, OpenAPI, gRPC conversion, utilities, activity codes, and adds unit/integration tests across managers, store, controllers, and clients.

Changes

Cohort / File(s) Summary
DNS Zones Core
management/internals/modules/zones/zone.go, management/internals/modules/zones/interface.go, management/internals/modules/zones/manager/manager.go, management/internals/modules/zones/manager/api.go
New Zone model, validation, API mappings, Manager interface and implementation with CRUD, permission checks, transactions, events, and HTTP handlers.
DNS Records Core
management/internals/modules/zones/records/record.go, management/internals/modules/zones/records/interface.go, management/internals/modules/zones/records/manager/manager.go, management/internals/modules/zones/records/manager/api.go
New Record model (A/AAAA/CNAME) with validation and conflict rules; records Manager implements transactional CRUD, permission checks, events, and HTTP handlers for records.
Store & Schema
management/server/store/store.go, management/server/store/sql_store.go
Store interface and SqlStore extended with zone and DNS-record CRUD/query methods; AutoMigrate updated; implementations include locking, transactions, and not-found signalling.
Server Wiring / HTTP
management/server/http/handler.go, management/internals/server/boot.go, management/internals/server/modules.go, management/server/http/testing/testing_tools/channel/channel.go
Added ZonesManager and RecordsManager to server/module factory and API handler; new managers wired into API handler and DNS endpoints registered.
Network Map Integration
management/internals/controllers/network_map/controller/controller.go, management/internals/controllers/network_map/controller/repository.go
Repository.GetAccountZones added; controller builds []CustomZone via filterPeerAppliedZones; propagates multi-zone slice through network-map generation and improves error handling.
Account / NetworkMap Types
management/server/types/account.go, management/server/types/networkmap.go, management/server/types/networkmapbuilder.go, management/server/types/networkmap_golden_test.go
GetPeerNetworkMap / GetPeerNetworkMapExp / assembleNetworkMap signatures changed to accept dnsDomain string and []CustomZone; logic updated to apply records for the zone matching dnsDomain.
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; comprehensive unit/integration tests added.
API Spec & Types
shared/management/http/api/openapi.yml, shared/management/http/api/types.gen.go
OpenAPI spec extended with Zone and DNSRecord schemas and endpoints; generated API types and request bodies added.
Activity / Errors / Util / gRPC
management/server/activity/codes.go, shared/management/status/error.go, management/server/util/util.go, management/internals/shared/grpc/conversion.go
New activity codes for zone/record events; NotFound helpers for zone/record; domain validation util IsValidDomain (regex); proto conversion includes SearchDomainDisabled.
Controller & Store Tests
management/internals/controllers/network_map/controller/controller_test.go, management/server/store/sql_store_test.go, management/internals/modules/zones/manager/manager_test.go, management/internals/modules/zones/records/manager/manager_test.go, management/server/account_test.go
Tests added/updated: filterPeerAppliedZones unit tests, extensive SQL store tests for zones/records, manager unit tests, and updated network-map tests to new GetPeerNetworkMap signature.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API as "HTTP API Handler"
    participant Perms as "PermissionsMgr"
    participant ZonesMgr as "Zones Manager"
    participant Store
    participant AccountMgr as "AccountMgr / Events"

    Client->>API: POST /api/dns/zones (ZoneRequest)
    API->>API: auth + decode + validate
    API->>Perms: check permission (dns:create)
    Perms-->>API: allowed
    API->>ZonesMgr: CreateZone(ctx, zone)
    ZonesMgr->>Store: Begin Tx, validate groups
    Store-->>ZonesMgr: ok
    ZonesMgr->>Store: CreateZone, IncNetworkSerial, Commit
    Store-->>ZonesMgr: created zone
    ZonesMgr->>AccountMgr: Store DNSZoneCreated event
    ZonesMgr->>AccountMgr: Trigger background peer updates
    ZonesMgr-->>API: created Zone
    API-->>Client: 200 (Zone)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus areas:
    • filterPeerAppliedZones construction and DNS record type mapping in management/internals/controllers/network_map/controller/controller.go
    • DNS record conflict validation (CNAME vs A/AAAA, duplicates) in management/internals/modules/zones/records/manager/manager.go
    • assembleNetworkMap / GetPeerNetworkMap signature changes and dnsDomain matching in management/server/types/*
    • SqlStore transactional behavior, locking, migrations and NotFound semantics in management/server/store/sql_store.go
    • Permission checks, event emission, and background peer-update triggers in zones/manager and records/manager

Possibly related PRs

Suggested reviewers

  • lixmal
  • crn4

Poem

🐰 I hopped through code with whiskers bright,
I stitched up zones by day and night.
A, AAAA, CNAME seeds in rows,
Peers now fetch DNS where the garden grows.
Hooray — small hops made this forest right! 🌱

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is largely incomplete, containing only template placeholders with no actual content describing the changes, rationale, or affected areas. Fill in 'Describe your changes' section with a summary of the DNS zones feature; link the relevant issue ticket; clarify documentation requirements (needed or explain why not).
Docstring Coverage ⚠️ Warning Docstring coverage is 8.75% 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 PR title '[management] Add custom dns zones' clearly and concisely describes the main feature addition—support for 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]>
@sonarqubecloud
Copy link

sonarqubecloud bot commented Dec 2, 2025

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.

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