[Service]
WorkingDirectory=/code
-ExecStart=/usr/bin/python3 -m knot_resolver_manager
+ExecStart=/usr/bin/python3 -m knot_resolver_manager --config=/etc/knot-resolver/kres-manager.yaml
KillSignal=SIGINT
[Install]
# Create knot-resolver-manager systemd service
COPY ./config/knot-resolver-manager.service /etc/systemd/system
+# Copy knot-resolver-manager YAML configuration file
+COPY ./config/kres-manager.yaml /etc/knot-resolver/
+
# Copy only requirements, to cache them in docker layer
# no poetry.lock, because here we have a different python version
COPY ./pyproject.toml ./yarn.lock ./package.json /code/
--- /dev/null
+{
+ "server": {
+ "instances": 1
+ },
+ "lua": {
+ "script": [
+ "-- SPDX-License-Identifier: CC0-1.0",
+ "-- vim:syntax=lua:set ts=4 sw=4:",
+ "-- Refer to manual: https://knot-resolver.readthedocs.org/en/stable/",
+ "-- Network interface configuration","net.listen('127.0.0.1', 53, { kind = 'dns' })",
+ "net.listen('127.0.0.1', 853, { kind = 'tls' })",
+ "--net.listen('127.0.0.1', 443, { kind = 'doh2' })",
+ "net.listen('::1', 53, { kind = 'dns', freebind = true })",
+ "net.listen('::1', 853, { kind = 'tls', freebind = true })",
+ "--net.listen('::1', 443, { kind = 'doh2' })",
+ "-- Load useful modules","modules = {",
+ "'hints > iterate', -- Load /etc/hosts and allow custom root hints",
+ "'stats', -- Track internal statistics",
+ "'predict', -- Prefetch expiring/frequent records",
+ "}",
+ "-- Cache size",
+ "cache.size = 100 * MB"
+ ]
+ }
+}
\ No newline at end of file
# patch requests library so that it supports unix socket
requests_unixsocket.monkeypatch()
-# prepare the payload
-LUA_CONFIG = """
--- SPDX-License-Identifier: CC0-1.0
--- vim:syntax=lua:set ts=4 sw=4:
--- Refer to manual: https://knot-resolver.readthedocs.org/en/stable/
-
--- Network interface configuration
-net.listen('127.0.0.1', 53, { kind = 'dns' })
-net.listen('127.0.0.1', 853, { kind = 'tls' })
---net.listen('127.0.0.1', 443, { kind = 'doh2' })
-net.listen('::1', 53, { kind = 'dns', freebind = true })
-net.listen('::1', 853, { kind = 'tls', freebind = true })
---net.listen('::1', 443, { kind = 'doh2' })
-
--- Load useful modules
-modules = {
- 'hints > iterate', -- Load /etc/hosts and allow custom root hints
- 'stats', -- Track internal statistics
- 'predict', -- Prefetch expiring/frequent records
-}
-
--- Cache size
-cache.size = 100 * MB
-"""
-PREPROCESSED_CONFIG = "\n ".join(LUA_CONFIG.splitlines(keepends=False))
-PAYLOAD = f"""
-num_workers: 4
-lua_config: |
-{ PREPROCESSED_CONFIG }
-"""
+PAYLOAD_PATH = "./payload.json"
+with open(PAYLOAD_PATH, "r") as file:
+ PAYLOAD = file.read()
# send the config
r = requests.post('http+unix://%2Ftmp%2Fmanager.sock/config', data=PAYLOAD)
--- /dev/null
+{
+ "server": {
+ "instances": %s
+ },
+ "lua": {
+ "script": [
+ "-- SPDX-License-Identifier: CC0-1.0",
+ "-- vim:syntax=lua:set ts=4 sw=4:",
+ "-- Refer to manual: https://knot-resolver.readthedocs.org/en/stable/",
+ "-- Network interface configuration","net.listen('127.0.0.1', 53, { kind = 'dns' })",
+ "net.listen('127.0.0.1', 853, { kind = 'tls' })",
+ "--net.listen('127.0.0.1', 443, { kind = 'doh2' })",
+ "net.listen('::1', 53, { kind = 'dns', freebind = true })",
+ "net.listen('::1', 853, { kind = 'tls', freebind = true })",
+ "--net.listen('::1', 443, { kind = 'doh2' })",
+ "-- Load useful modules","modules = {",
+ "'hints > iterate', -- Load /etc/hosts and allow custom root hints",
+ "'stats', -- Track internal statistics",
+ "'predict', -- Prefetch expiring/frequent records",
+ "}",
+ "-- Cache size",
+ "cache.size = 100 * MB"
+ ]
+ }
+}
\ No newline at end of file
# patch requests library so that it supports unix socket
requests_unixsocket.monkeypatch()
-# prepare the payload
-LUA_CONFIG = """
--- SPDX-License-Identifier: CC0-1.0
--- vim:syntax=lua:set ts=4 sw=4:
--- Refer to manual: https://knot-resolver.readthedocs.org/en/stable/
+PAYLOAD_PATH = "./payload.json"
+with open(PAYLOAD_PATH, "r") as file:
+ PAYLOAD = file.read()
--- Network interface configuration
-net.listen('127.0.0.1', 53, { kind = 'dns' })
-net.listen('127.0.0.1', 853, { kind = 'tls' })
---net.listen('127.0.0.1', 443, { kind = 'doh2' })
-net.listen('::1', 53, { kind = 'dns', freebind = true })
-net.listen('::1', 853, { kind = 'tls', freebind = true })
---net.listen('::1', 443, { kind = 'doh2' })
-
--- Load useful modules
-modules = {
- 'hints > iterate', -- Load /etc/hosts and allow custom root hints
- 'stats', -- Track internal statistics
- 'predict', -- Prefetch expiring/frequent records
-}
-
--- Cache size
-cache.size = 100 * MB
-"""
-PREPROCESSED_CONFIG = "\n ".join(LUA_CONFIG.splitlines(keepends=False))
-PAYLOAD_F = lambda num: f"""
-num_workers: {num}
-lua_config: |
-{ PREPROCESSED_CONFIG }"""
+# f-string is not working with JSON because of {}
+PAYLOAD_F = lambda num: PAYLOAD % num
def set_workers(num: int):
# send the config
import click
from aiohttp import web
-from .kres_manager import KresManager
from . import configuration
+from .datamodel import KresConfig
+from .kres_manager import KresManager
from .utils import ignore_exceptions
# when changing this, change the help message in main()
async def apply_config(request: web.Request) -> web.Response:
- config = await configuration.parse_yaml(await request.text())
+ config: KresConfig = await configuration.parse_json(await request.text())
manager: KresManager = request.app["kres_manager"]
await manager.apply_config(config)
return web.Response(text="OK")
@click.command()
@click.argument("listen", type=str, nargs=1, required=False, default=None)
-def main(listen: Optional[str]):
+@click.option("--config", "-c", type=str, nargs=1, required=False, default=None)
+def main(listen: Optional[str], config: Optional[str]):
"""Knot Resolver Manager
[listen] ... numeric port or a path for a Unix domain socket, default is \"/tmp/manager.sock\"
"""
-from dataclasses import dataclass
+from dataclasses import dataclass, is_dataclass
-__all__ = ["dataclass"]
+__all__ = ["dataclass", "is_dataclass"]
+import json
from typing import Text
+import yaml
from jinja2 import Environment, Template
-from .datamodel import ConfData
+from .datamodel import KresConfig
_LUA_TEMPLATE_STR = """
{% if lua_config -%}
-{{ config.lua_config }}
+{{ cfg.lua.script }}
{% endif -%}
"""
_LUA_TEMPLATE: Template = _ENV.from_string(_LUA_TEMPLATE_STR)
-async def render_lua(config: ConfData) -> Text:
- return await _LUA_TEMPLATE.render_async(config=config)
+async def render_lua(config: KresConfig) -> Text:
+ return await _LUA_TEMPLATE.render_async(cfg=config)
-async def parse_yaml(yaml: str) -> ConfData:
- config = ConfData.from_yaml(yaml)
+async def parse_yaml(yaml_str: str) -> KresConfig:
+ data = yaml.safe_load(yaml_str)
+ config = KresConfig(**data)
await config.validate()
return config
+
+
+async def parse_json(json_str: str) -> KresConfig:
+ data = json.loads(json_str)
+ config: KresConfig = KresConfig(**data)
+ await config.validate()
+ return config
+
+
+async def load_file(path: str) -> KresConfig:
+ try:
+ with open(path, "r") as file:
+ yaml_str = file.read()
+ except FileNotFoundError:
+ # return defaults
+ return KresConfig()
+ return parse_yaml(yaml_str)
-from typing import Optional
+from typing import List, Union
-from .compat.dataclasses import dataclass
-from .utils import StrictyamlParser
+from .utils import dataclass_nested
-class ConfDataValidationException(Exception):
+class DataValidationError(Exception):
pass
-@dataclass
-class ConfData(StrictyamlParser):
- num_workers: int = 1
- lua_config: Optional[str] = None
+@dataclass_nested
+class ServerConfig:
+ instances: int = 1
- async def validate(self) -> bool:
- if self.num_workers < 0:
- raise ConfDataValidationException("Number of workers must be non-negative")
+ async def validate(self):
+ if self.instances < 0:
+ raise DataValidationError("Number of workers must be non-negative")
- return True
+
+@dataclass_nested
+class LuaConfig:
+ script: Union[str, List[str], None] = None
+
+ def __post_init__(self):
+ # Concatenate array to single string
+ if isinstance(self.script, List):
+ self.script = "\n".join(self.script)
+
+
+@dataclass_nested
+class KresConfig:
+ server: ServerConfig = ServerConfig()
+ lua: LuaConfig = LuaConfig()
+
+ async def validate(self):
+ await self.server.validate()
from uuid import uuid4
from . import compat, configuration, systemd
-from .datamodel import ConfData
+from .datamodel import KresConfig
class Kresd:
while len(self._children) < n:
await self._spawn_new_child()
- async def _write_config(self, config: ConfData):
+ async def _write_config(self, config: KresConfig):
# FIXME: this code is blocking!!!
lua_config = await configuration.render_lua(config)
with open("/etc/knot-resolver/kresd.conf", "w") as f:
f.write(lua_config)
- async def apply_config(self, config: ConfData):
+ async def apply_config(self, config: KresConfig):
async with self._children_lock:
await self._write_config(config)
- await self._ensure_number_of_children(config.num_workers)
+ await self._ensure_number_of_children(config.server.instances)
await self._rolling_restart()
from typing import Any, Callable, Optional, Type, TypeVar
-from . import types
+from .dataclasses_nested import dataclass_nested
from .dataclasses_yaml import StrictyamlParser, dataclass_strictyaml, dataclass_strictyaml_schema
T = TypeVar("T")
return decorator
-__all__ = ["dataclass_strictyaml_schema", "dataclass_strictyaml", "StrictyamlParser", "ignore_exceptions", "types"]
+__all__ = [
+ "dataclass_strictyaml_schema",
+ "dataclass_strictyaml",
+ "StrictyamlParser",
+ "ignore_exceptions",
+ "dataclass_nested",
+ "types",
+]
--- /dev/null
+from ..compat.dataclasses import dataclass, is_dataclass
+
+
+# source: https://www.geeksforgeeks.org/creating-nested-dataclass-objects-in-python/
+# decorator to wrap original __init__
+def dataclass_nested(*args, **kwargs):
+ def wrapper(check_class):
+ # passing class to investigate
+ check_class = dataclass(check_class, **kwargs)
+ o_init = check_class.__init__
+
+ def __init__(self, *args, **kwargs):
+ for name, value in kwargs.items():
+ # getting field type
+ ft = check_class.__annotations__.get(name, None)
+ if is_dataclass(ft) and isinstance(value, dict):
+ obj = ft(**value)
+ kwargs[name] = obj
+ o_init(self, *args, **kwargs)
+
+ check_class.__init__ = __init__
+ return check_class
+
+ return wrapper(args[0]) if args else wrapper
name = "pyyaml"
version = "5.4.1"
description = "YAML parser and emitter for Python"
-category = "dev"
+category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[metadata]
lock-version = "1.1"
python-versions = "^3.6.12"
-content-hash = "84b5fb8bb68a208f7a3b4027815766c8c6c2e82681789eacacf5838199b14a3d"
+content-hash = "86ff57487d2a60351c49574b934a11e534d183a148094b08aa456b80fb32efdc"
[metadata.files]
aiohttp = [
PyGObject = "^3.38.0"
Jinja2 = "^2.11.3"
click = "^7.1.2"
+PyYAML = "^5.4.1"
[tool.poetry.dev-dependencies]
pytest = "^5.2"