Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors the DNS backend from Bind9 to PowerDNS, implementing a two-component architecture with PowerDNS Authoritative Server and Recursor. The refactoring involves significant changes to the DNS management layer, API endpoints, and configuration.
Key changes:
- Migration from Bind9 to PowerDNS with custom Dockerfile for LMDB backend support
- Removal of self-hosted Bind9 manager and custom DNS API
- Introduction of new DTOs and enums aligned with PowerDNS data structures
- API restructuring with zone_id as path parameters and separate endpoints for forward zones
- Removal of DNS server-level configuration endpoints (DNSSEC now zone-specific)
Reviewed changes
Copilot reviewed 33 out of 33 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| pdns.conf | PowerDNS Authoritative Server configuration with LMDB backend |
| recursor.conf | PowerDNS Recursor configuration with API access |
| .docker/pdns_auth.Dockerfile | Alpine-based build for PowerDNS with LMDB support |
| docker-compose.yml | Replaced bind_dns service with pdns_auth and pdns_recursor services |
| app/ldap_protocol/dns/powerdns.py | New PowerDNS manager implementing AbstractDNSManager interface |
| app/ldap_protocol/dns/dto.py | Extended DTOs for PowerDNS zones, records, and RRSets |
| app/ldap_protocol/dns/enums.py | New enums for DNS record types, zone types, and change types |
| app/api/main/dns_router.py | Restructured routes with zone_id path params and forward zone endpoints |
| app/api/main/adapters/dns.py | Adapter updated to construct DTOs from request schemas |
| app/ldap_protocol/dns/use_cases.py | Simplified use cases with removed server options functionality |
| app/config.py | New config for PDNS hosts and API key |
| app/ioc.py | Updated DI container for PowerDNS client provisioning |
| tests/test_api/test_main/test_dns.py | Comprehensive test updates for new API structure |
Comments suppressed due to low confidence (1)
app/ldap_protocol/dns/stub.py:77
- This method requires 2 positional arguments, whereas overridden AbstractDNSManager.check_forward_dns_server requires 3.
@logger_wraps(is_stub=True)
async def check_forward_dns_server(
self,
dns_server_ip: str,
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
app/ldap_protocol/dns/use_cases.py
Outdated
| async def delete_zones(self, zone_ids: list[str]) -> None: | ||
| """Delete DNS zones.""" | ||
| for zone_id in zone_ids: | ||
| await self._dns_manager.delete_zone(zone_id) |
There was a problem hiding this comment.
The use_cases.py delete_zones method iterates through zone_ids and calls delete_zone for each one. If any of these calls fail, the remaining zones won't be deleted. Consider implementing error handling to either collect errors and raise them at the end, or use a transaction-like mechanism to ensure all-or-nothing deletion.
| @@ -0,0 +1,10 @@ | |||
| local-address=0.0.0.0 | |||
| webserver-allow-from=0.0.0.0/0 | |||
There was a problem hiding this comment.
The webserver is configured to allow connections from any IP address (0.0.0.0/0). This is a security risk in production environments. Consider restricting access to specific IP ranges or implementing additional authentication mechanisms.
| webserver-allow-from=0.0.0.0/0 | |
| webserver-allow-from=127.0.0.1/32 |
There was a problem hiding this comment.
Пока сделано так, в дальнейшем, при добавлении PowerDNS DNSdist будет переделано.
| local-port=53 | ||
| api=yes | ||
| api-key=supersecretapikey | ||
| webserver-allow-from=0.0.0.0/0 |
There was a problem hiding this comment.
The webserver is configured to allow connections from any IP address (0.0.0.0/0). This is a security risk in production environments. Consider restricting access to specific IP ranges or implementing additional authentication mechanisms.
| webserver-allow-from=0.0.0.0/0 | |
| webserver-allow-from=127.0.0.1,::1 |
There was a problem hiding this comment.
Здесь то же самое
recursor.conf
Outdated
| webserver=yes | ||
| webserver-address=0.0.0.0 | ||
| webserver-port=8083 | ||
| api-key=supersecretapikey |
There was a problem hiding this comment.
The API key "supersecretapikey" is hardcoded in the configuration file. This is a security risk as it exposes the API key in version control. The API key should be passed as an environment variable or mounted from a secrets management system.
| api-key=supersecretapikey | |
| api-key=changeme # real value must be injected from environment/secrets at deploy time |
There was a problem hiding this comment.
То же самое, что и выше.
app/ldap_protocol/dns/use_cases.py
Outdated
| async def delete_forward_zones(self, zone_ids: list[str]) -> None: | ||
| """Delete DNS forward zones.""" | ||
| for zone_id in zone_ids: | ||
| await self._dns_manager.delete_forward_zone(zone_id) |
There was a problem hiding this comment.
The same issue exists in delete_forward_zones - if any deletion fails, the remaining zones won't be deleted. Consider implementing error handling to either collect errors and raise them at the end, or use a transaction-like mechanism to ensure all-or-nothing deletion.
| async def create(self, data: CatalogueSetting) -> None: | ||
| """Create DNS.""" | ||
| self._session.add(data) | ||
| await self._session.commit() |
There was a problem hiding this comment.
на уровне gateway мы можем делать commit? по моему на таком низком уровне мы договаривались без коммита, а только flush
т.к. если тут сделать коммит, то транзакция закроется и мы не сможем собрать пайплайн из двух небольших методов гейтвея (если в каждом из них будет по коммиту), тк первый метод закроет транзакцию
There was a problem hiding this comment.
Нужна помощь второго ревьювера, не берусь утверждать
There was a problem hiding this comment.
в manager/use_case/service есть коммит
гейтвей как и dao как и репозиторий примерно одно и то же с нюансами, но все инкапсулирует доступ к данным, поэтому там коммита нет, чтобы можно было легко собрать любой пайплайн из этих методов, как из кирпичиков и транзакция не закрылась по середине
There was a problem hiding this comment.
Дима прав. Егор говорил, что мы управляем сессией на уровне use_case не gateway. Т.к. задача gateway работать с данными, а когда их отнести до БД, решается выше уровнем
| code = ErrorCodes.DNS_NOT_IMPLEMENTED_ERROR | ||
|
|
||
|
|
||
| class DNSUnavailableError(DNSError): |
There was a problem hiding this comment.
если какая-то из ошибок идет на фронт, нужно добавить код в enum выше. так же добавить в error_map ее, чтобы она обрабатывалась в роуте
There was a problem hiding this comment.
Эта ошибка не идет на фронт, но добавил другие
| base_retort = Retort() | ||
|
|
||
|
|
||
| class PowerDNSManager(AbstractDNSManager): |
There was a problem hiding this comment.
как по мне, получился уберкомбайнер, который шлет\валидирует запросы(работа с http), логика самого днс, вычисление и агрегация данных. ИМХО слишком много всего, я бы декомпозировал
There was a problem hiding this comment.
Согласен, но предлагаю отложить пока
|
а мы от адаптикса отказались, почему все дто вручную? |
Потому что пытался по минимуму менять схемы фронтовые, чтобы меньше фронтам переделывать |
так адаптикс просто код перевода объектов убирает из логики, схему фронтовые можно не менять |
49664af to
8bd9a0e
Compare
milov-dmitriy
left a comment
There was a problem hiding this comment.
ничего существенного только нейминг
я бы в будущем посмотрел в сторону рефакторинга app/ldap_protocol/dns, хотелось бы сгруппировать логические блоки кода между собой, например всех менеджеров в одну кучу, все Enum положить в одно место, нужно уменьшить кол-во разных сущностей в base, по итогу получится в лучших традициях high cohesion/low coupling
| - ./recursor.conf:/etc/powerdns/recursor.conf | ||
| - forward_zones:/etc/powerdns/recursor.d/ | ||
|
|
||
| pdnsdist: |
There was a problem hiding this comment.
pdns_dist ? стоит в едином стиле делать?
There was a problem hiding this comment.
Официально оно просто dnsdist называется
| base_retort = Retort() | ||
|
|
||
|
|
||
| class PowerDNSDistClient: |
There was a problem hiding this comment.
-
предлагаю согласовать названия файла и класса:
power_dns_manager.py< - >PowerDNSDistClient -
а что значит Dist?
There was a problem hiding this comment.
- думаю, что было бы яснее, если мы бы смогли более явно указать, что значит remote_dns_manager. указать его отличие от других dns менеджеров если они есть (судя по названию вроде как должны быть)
т.е. предполагается что
- есть не только remote? но и local?
или - remote client и local client?
There was a problem hiding this comment.
Remote DNS manager - унифицированный менеджер для DNS'a клиента, который может только записями управлять
Хз, как его еще обозвать
| PDNS_AUTH_SERVER_HOST: str = "pdns_auth" | ||
| PDNS_AUTH_SERVER_IP: str = "172.20.0.4" | ||
| PDNS_AUTH_SERVER_PORT: int = 8082 | ||
| PDNS_RECURSOR_SERVER_HOST: str = "pdns_recursor" | ||
| PDNS_RECURSOR_SERVER_IP: str = "172.20.0.2" | ||
| PDNS_RECURSOR_SERVER_PORT: int = 8083 | ||
| PDNS_DIST_HOST: str = "172.20.0.3" | ||
| PDNS_DIST_PORT: int = 8084 | ||
| PDNS_DIST_CONFIG_PATH: str = "/dnsdist/delta.conf" | ||
| PDNS_DIST_KEY: str | ||
| PDNS_API_KEY: str | ||
| DEFAULT_NAMESERVER: str |
There was a problem hiding this comment.
добавить описание в вики
…agement and error handling
…te resolv.conf file
7198477 to
2c2afb5
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 60 out of 63 changed files in this pull request and generated 14 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| rules = self._get_all_rules() | ||
| if not rules.count: | ||
| DNSdistError( | ||
| "Failed to delete existing rule in dnsdist: Not Found", | ||
| ) | ||
|
|
There was a problem hiding this comment.
In remove_zone_rule, the DNSdistError(...) is instantiated but never raised when no rules are found, so the failure is silently ignored. This can leave stale routing rules in place without surfacing an error. Raise the exception (or otherwise handle the not-found case explicitly).
| async def _validate_response(self, response: httpx.Response) -> None: | ||
| """Validate the API response.""" | ||
| match response.status_code: | ||
| case status.HTTP_400_BAD_REQUEST: | ||
| raise DNSNotSupportedError(response.text or "Bad Request") | ||
| case status.HTTP_404_NOT_FOUND: | ||
| raise DNSEntryNotFoundError(response.text or "Not Found") | ||
| case status.HTTP_422_UNPROCESSABLE_ENTITY: | ||
| raise DNSValidationError( | ||
| response.text or "Unprocessable Entity", | ||
| ) | ||
| case status.HTTP_500_INTERNAL_SERVER_ERROR: | ||
| raise DNSUnavailableError( | ||
| response.text or "Internal Server Error", | ||
| ) | ||
|
|
There was a problem hiding this comment.
AbstractDNSHTTPClient._validate_response() only raises for a small set of status codes (400/404/422/500) and silently treats other failures (e.g., 401/403/409/429/502/503) as success. This can cause callers to proceed on error responses. Consider raising on any response.is_error (or add a default case _ that raises a suitable DNSError for unhandled 4xx/5xx).
| async def get_status(self) -> dict[str, str | None]: | ||
| """Get DNS status.""" | ||
| return { | ||
| "dns_status": await self._dns_gateway.get_dns_state(), | ||
| "zone_name": self._dns_settings.zone_name, | ||
| "dns_server_ip": self._dns_settings.dns_server_ip, | ||
| "dns_status": await self._dns_gateway.get_state(), | ||
| "zone_name": self._dns_settings.domain, | ||
| "dns_server_ip": str(self._dns_settings.dns_server_ip), | ||
| } |
There was a problem hiding this comment.
get_status() always stringifies self._dns_settings.dns_server_ip, so when it is None the API returns the literal string "None" even though the return type is str | None. Return None when the value is missing to keep the response contract consistent.
| --with-unixodbc-lib=/usr/lib/$(dpkg-architecture -q DEB_BUILD_GNU_TYPE) && \ | ||
| make clean && \ | ||
| make $MAKEFLAGS -C ext &&\ | ||
| make $MAKEFLAGS -C modules &&\ | ||
| make $MAKEFLAGS -C pdns && \ | ||
| make -C pdns install DESTDIR=/build &&\ | ||
| make -C modules install DESTDIR=/build &&\ |
There was a problem hiding this comment.
The build stage uses $(dpkg-architecture -q DEB_BUILD_GNU_TYPE) inside an Alpine image; dpkg-architecture is not available on Alpine by default, so this Dockerfile will fail to build. Also, the lines &&\ (backslash followed by a space) will break shell line-continuation and can cause a syntax error. Remove the dpkg-architecture usage (or install the needed tool) and fix the trailing whitespace after \ in the RUN command.
| --with-unixodbc-lib=/usr/lib/$(dpkg-architecture -q DEB_BUILD_GNU_TYPE) && \ | |
| make clean && \ | |
| make $MAKEFLAGS -C ext &&\ | |
| make $MAKEFLAGS -C modules &&\ | |
| make $MAKEFLAGS -C pdns && \ | |
| make -C pdns install DESTDIR=/build &&\ | |
| make -C modules install DESTDIR=/build &&\ | |
| --with-unixodbc-lib=/usr/lib && \ | |
| make clean && \ | |
| make $MAKEFLAGS -C ext &&\ | |
| make $MAKEFLAGS -C modules &&\ | |
| make $MAKEFLAGS -C pdns && \ | |
| make -C pdns install DESTDIR=/build &&\ | |
| make -C modules install DESTDIR=/build &&\ |
| log = logger.bind(name="DNSManager") | ||
|
|
||
| log.add( | ||
| "logs/dnsmanager_{time:DD-MM-YYYY}.log", | ||
| filter=lambda rec: rec["extra"].get("name") == "dnsmanager", | ||
| retention="10 days", | ||
| rotation="1d", | ||
| colorize=False, | ||
| ) |
There was a problem hiding this comment.
The log sink filter checks rec["extra"].get("name") == "dnsmanager", but the logger is bound with name="DNSManager". With the current mismatch, this sink will never receive records. Align the bound name and filter value (and consider ensuring the logs/ directory exists before adding the sink, otherwise Loguru will fail to open the file).
| @logger_wraps(is_stub=True) | ||
| async def check_forward_dns_server( | ||
| self, | ||
| dns_server_ip: str, | ||
| ) -> None: ... |
There was a problem hiding this comment.
StubDNSManager.check_forward_dns_server signature does not match the abstract interface / use-case call site (it should accept (dns_server_ip: IPv4Address|IPv6Address, host_dns_servers: list[str]) and return DNSForwardServerStatus). As-is, DNSUseCase.check_forward_server() will raise TypeError when the Stub manager is used. Update the stub method signature/return type to match AbstractDNSManager.
| DEFAULT_NAMESERVER=172.20.0.4 | ||
| PDNS_API_KEY=supersecretapikey | ||
| PDNS_DIST_KEY=PSAag0AEziPZuBB7kdcfIEkVJOyQInRcBRAhadWDpU0= |
There was a problem hiding this comment.
local.env now contains PDNS_API_KEY and PDNS_DIST_KEY values committed to the repo. Even if this is “dev only”, it trains users to reuse secrets and risks accidental exposure. Prefer placeholders (e.g., supersecretapikey) and generate real keys via the setup scripts / environment injection, or move these to .env (already gitignored).
| # | ||
| # macOS Notice | ||
| # | ||
| # This file is not consulted for DNS hostname resolution, address | ||
| # resolution, or the DNS query routing mechanism used by most | ||
| # processes on this system. | ||
| # | ||
| # To view the DNS configuration used by this system, use: | ||
| # scutil --dns | ||
| # | ||
| # SEE ALSO | ||
| # dns-sd(1), scutil(8) | ||
| # | ||
| # This file is automatically generated. | ||
| # | ||
| nameserver 192.168.68.1 |
There was a problem hiding this comment.
.package/resolv.conf looks like a machine-generated copy of a developer host’s resolver config (it even contains the macOS autogenerated header and a local nameserver). This should be generated at install time (as setup.sh already does) and gitignored, not committed to the repo.
| @logger_wraps() | ||
| async def get_records(self) -> list[DNSRRSetDTO]: | ||
| """Get all DNS records.""" |
There was a problem hiding this comment.
RemoteDNSManager.get_records is missing the required zone_id parameter (the abstract interface and DNSUseCase.get_records(zone_id) call it with an argument). This will raise TypeError: ... takes 1 positional argument but 2 were given in HOSTED mode. Adjust the method signature to accept zone_id: str (even if unused) to match AbstractDNSManager.
| _dns_settings: DNSSettingsDTO | ||
| _dns_master_client: AbstractDNSMasterHTTPClient | None = None | ||
| _dns_forward_cient: AbstractDNSForwardHTTPClient | None = None | ||
|
|
There was a problem hiding this comment.
Typo in attribute name: _dns_forward_cient should be _dns_forward_client. Even if unused now, keeping the correct spelling avoids confusion and prevents future bugs when this attribute is referenced.
milov-dmitriy
left a comment
There was a problem hiding this comment.
[опционально]
по-хорошему проверить у всех затронутых файлов шапку: докстринг + год копирайта.
| if "to pool" in line: | ||
| matches = pattern.match(line) | ||
| if matches: | ||
| rules.append( | ||
| RuleEntry( | ||
| id=int(matches.group(1)), | ||
| match=matches.group(2).strip(), | ||
| action=matches.group(3).strip(), | ||
| ), | ||
| ) |
There was a problem hiding this comment.
[опционально]
if "to pool" in line and (matches := pattern.match(line)):
можно так и чуть поменьше отступов будет, поменьше "вложенность"
| command = f""" | ||
| newServer({{ | ||
| address = "{recursor_ip}:53", | ||
| pool = "recursor" | ||
| }}) | ||
| """ |
There was a problem hiding this comment.
смотрю и хочется эти команды унести в отдельный класс, например: DNSdistCommands
сейчас команды выглядят как магические переменные
| """ | ||
| self._send_command( | ||
| command, | ||
| expected=DNSdistCommandType.GENERIC, |
There was a problem hiding this comment.
DNSdistCommandType -> DNSdistCommandTypes ?
| .installed.cfg | ||
| *.egg | ||
| MANIFEST | ||
| resolve.conf |
| [ | ||
| DNS_MANAGER_ZONE_NAME, | ||
| DNS_MANAGER_IP_ADDRESS_NAME, | ||
| DNS_MANAGER_TSIG_KEY_NAME, | ||
| ], |
There was a problem hiding this comment.
[nitpick] лучше вместо списка - кортеж
и лучше in в одну строку с атрибутом, так:
settings = await self._session.scalars(
select(CatalogueSetting)
.filter(
qa(CatalogueSetting.name).in_([
DNS_MANAGER_ZONE_NAME,
DNS_MANAGER_IP_ADDRESS_NAME,
DNS_MANAGER_TSIG_KEY_NAME,
],
),
),
) # fmt: skip
Задачи: 1057, 1058
Произведена замена DNS сервера с Bind9 на PowerDNS, состоящий из двух компонентов: Authoritative Server и Recursor. Произведен рефакторинг компонентов DNS сервера. Вся логика, относящаяся к Bind9, была удалена, за исключением некоторым датаклассов по просьбе Егора. Вся логика была воспроизведена в соответствии с новым DNS сервером за исключением пары фич: включение/отключение DNSSEC опции DNS сервера - теперь это опция относится к зоне, соответственно, временно удален функционал получения и изменения параметров DNS сервера, удалена возможность перезагружать DNS сервер и отдельную зону, так как в этом больше нет необходимости и на фронт этот функционал так и не был выведен.
Сделано:
UPD: Добавлен dnsdist