]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
conf: dataclasses_nested
authorAleš <ales.mrazek@nic.cz>
Mon, 29 Mar 2021 17:42:27 +0000 (19:42 +0200)
committerAleš Mrázek <ales.mrazek@nic.cz>
Fri, 8 Apr 2022 14:17:52 +0000 (16:17 +0200)
- new datamodel
- JSON data on API
- added configuration file argument
- deps: added pyYAML
- integration: JSON payload in tests

15 files changed:
manager/config/knot-resolver-manager.service
manager/containers/debian/Containerfile
manager/integration/tests/basic_startup/payload.json [new file with mode: 0644]
manager/integration/tests/basic_startup/send_request.py
manager/integration/tests/worker_count/payload.json [new file with mode: 0644]
manager/integration/tests/worker_count/run_test.py
manager/knot_resolver_manager/__main__.py
manager/knot_resolver_manager/compat/dataclasses.py
manager/knot_resolver_manager/configuration.py
manager/knot_resolver_manager/datamodel.py
manager/knot_resolver_manager/kres_manager.py
manager/knot_resolver_manager/utils/__init__.py
manager/knot_resolver_manager/utils/dataclasses_nested.py [new file with mode: 0644]
manager/poetry.lock
manager/pyproject.toml

index a1e6775a1cd98c3d384c1a73758dcf839d286216..c673d1462eab6c5676914280f173b015f02685c8 100644 (file)
@@ -5,7 +5,7 @@ After=dbus
 
 [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]
index 5fd349b94533de6d2c18a919466551ddd09d41cc..57c3fad0f332f57c34f62e5d3347f0b1ebd55a50 100644 (file)
@@ -57,6 +57,9 @@ RUN apt-get update \
 # 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/
diff --git a/manager/integration/tests/basic_startup/payload.json b/manager/integration/tests/basic_startup/payload.json
new file mode 100644 (file)
index 0000000..8439f86
--- /dev/null
@@ -0,0 +1,25 @@
+{
+  "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
index 5c9d8142f4fe94c36e02d5ede84e46b2edad9b6e..75dc3b36915bcb73cdd0c4d9d03fac63b40682b6 100644 (file)
@@ -7,36 +7,9 @@ import requests_unixsocket
 # 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)
diff --git a/manager/integration/tests/worker_count/payload.json b/manager/integration/tests/worker_count/payload.json
new file mode 100644 (file)
index 0000000..6def291
--- /dev/null
@@ -0,0 +1,25 @@
+{
+  "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
index 12eac1d8e00b45bcad2a378fd5919e48a0d523e0..3c862819a2f456d066335b15f8f8cf168d2ad204 100644 (file)
@@ -10,35 +10,12 @@ import requests_unixsocket
 # 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
index 8e5691f886d8180b8a41fc8e5793f78b17319374..329180319d373a32d051b4b9055beb8a2fac83c0 100644 (file)
@@ -5,8 +5,9 @@ from typing import Optional
 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()
@@ -18,7 +19,7 @@ async def hello(_request: web.Request) -> web.Response:
 
 
 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")
@@ -26,7 +27,8 @@ async def apply_config(request: web.Request) -> web.Response:
 
 @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\"
index 09c41110b4b026c216f2f80c6fa14cc6c0a4e6d8..2bd9dee3339ac3763ceaa3048151427b4f7c3b75 100644 (file)
@@ -7,6 +7,6 @@ the option to do it transparently, without changing anything else.
 """
 
 
-from dataclasses import dataclass
+from dataclasses import dataclass, is_dataclass
 
-__all__ = ["dataclass"]
+__all__ = ["dataclass", "is_dataclass"]
index 3e325d5e3fdfac7b4f0620f04db0ebf3b268f017..c69be3c798d5b751d949c27796f054a33bd2c114 100644 (file)
@@ -1,12 +1,14 @@
+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 -%}
 """
 
@@ -14,11 +16,29 @@ _ENV = Environment(enable_async=True)
 _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)
index 728dbfd68c46540e733a6d1a15e61b33c6720164..764f594db8a56964710ba8e0729888039d69bacd 100644 (file)
@@ -1,20 +1,35 @@
-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()
index a107ab43377e241e88500c35c1ce9fa92ae6abd4..ab8910ee4bbaa6f6a97451a51366f4c68b485ba1 100644 (file)
@@ -3,7 +3,7 @@ from typing import List, Optional
 from uuid import uuid4
 
 from . import compat, configuration, systemd
-from .datamodel import ConfData
+from .datamodel import KresConfig
 
 
 class Kresd:
@@ -73,14 +73,14 @@ class KresManager:
         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()
index 9557c9a6d6afd107d87d4e0213ee876221c1010c..0fffb7d99168bf8f70faae2545f42c6086e50cf7 100644 (file)
@@ -1,6 +1,6 @@
 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")
@@ -24,4 +24,11 @@ def ignore_exceptions(
     return decorator
 
 
-__all__ = ["dataclass_strictyaml_schema", "dataclass_strictyaml", "StrictyamlParser", "ignore_exceptions", "types"]
+__all__ = [
+    "dataclass_strictyaml_schema",
+    "dataclass_strictyaml",
+    "StrictyamlParser",
+    "ignore_exceptions",
+    "dataclass_nested",
+    "types",
+]
diff --git a/manager/knot_resolver_manager/utils/dataclasses_nested.py b/manager/knot_resolver_manager/utils/dataclasses_nested.py
new file mode 100644 (file)
index 0000000..8698d99
--- /dev/null
@@ -0,0 +1,24 @@
+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
index fba5323ed4bc96265ac839d191083c1b9e28500a..e60e9e107734da98411042c24a5d1d1f78b3c724 100644 (file)
@@ -676,7 +676,7 @@ six = ">=1.5"
 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.*"
 
@@ -954,7 +954,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.6.12"
-content-hash = "84b5fb8bb68a208f7a3b4027815766c8c6c2e82681789eacacf5838199b14a3d"
+content-hash = "86ff57487d2a60351c49574b934a11e534d183a148094b08aa456b80fb32efdc"
 
 [metadata.files]
 aiohttp = [
index d5c5368d9a92069813fa4cd6edaa99a566df79b7..4a97c198858f84f516f0da32286db67a71ae2965 100644 (file)
@@ -15,6 +15,7 @@ pydbus = "^0.6.0"
 PyGObject = "^3.38.0"
 Jinja2 = "^2.11.3"
 click = "^7.1.2"
+PyYAML = "^5.4.1"
 
 [tool.poetry.dev-dependencies]
 pytest = "^5.2"