# and install the dependencies
&& poetry install --no-dev --no-interaction --no-ansi
-# Copy the remaining code
-COPY . /code
+# Here, we would copy the remaining code if we wanted to permanently keep it in the container. We don't do that, we use read-only bind mounts
+# COPY . /code
CMD ["/bin/systemd"]
\ No newline at end of file
--no-interaction --no-ansi \
&& if test "$KNOT_ENV" = "dev"; then yarn install; fi
-# Copy the remaining code
-COPY . /code
+# Here, we would copy the remaining code if we wanted to permanently keep it in the container. We don't do that, we use read-only bind mounts
+# COPY . /code
CMD ["/bin/systemd"]
\ No newline at end of file
print(f"Skipping test {Colors.YELLOW}{test.name}{Colors.RESET}")
continue
- test.run(manager)
+ test.run(manager, inspect_failed)
if __name__ == "__main__":
cmd = ["/test/run"]
[mount]
-"/test" = "integration/tests/basic_startup"
\ No newline at end of file
+"/test" = "integration/tests/basic_startup"
+"/code" = "."
\ No newline at end of file
cmd = ["/test/run"]
[mount]
-"/test" = "integration/tests/worker_count"
\ No newline at end of file
+"/test" = "integration/tests/worker_count"
+"/code" = "."
\ No newline at end of file
+from typing import Optional
+from pathlib import Path
+import sys
+
from aiohttp import web
-from knot_resolver_manager.kresd_manager import KresdManager
+import click
+from .kresd_manager import KresdManager
+from .utils import ignore_exceptions
from . import configuration
+# when changing this, change the help message in main()
_SOCKET_PATH = "/tmp/manager.sock"
return web.Response(text="OK")
-def main():
+@click.command()
+@click.argument("listen", type=str, nargs=1, required=False, default=None)
+def main(listen: Optional[str]):
+ """Knot Resolver Manager
+
+ [listen] ... numeric port or a path for a Unix domain socket, default is \"/tmp/manager.sock\"
+ """
+
app = web.Application()
# initialize KresdManager
# configure routing
app.add_routes([web.get("/", hello), web.post("/config", apply_config)])
- # run forever
- web.run_app(app, path=_SOCKET_PATH)
+ # run forever, listen at the appropriate place
+ maybe_port = ignore_exceptions(None, ValueError, TypeError)(int)(listen)
+ if listen is None:
+ web.run_app(app, path=_SOCKET_PATH)
+ elif maybe_port is not None:
+ web.run_app(app, port=maybe_port)
+ elif Path(listen).parent.exists():
+ web.run_app(app, path=listen)
+ else:
+ print(
+ "Failed to parse LISTEN argument. Not an integer, not a valid path to a file in an existing directory.",
+ file=sys.stderr,
+ )
+ sys.exit(1)
if __name__ == "__main__":
- main()
+ main() # pylint: disable=no-value-for-parameter
from . import asyncio
+from . import dataclasses
-__all__ = ["asyncio"]
+__all__ = ["asyncio", "dataclasses"]
)
-__all__ = ["dataclass_strictyaml_schema", "dataclass_strictyaml", "StrictyamlParser"]
+def ignore_exceptions(default, *exception):
+ def decorator(func):
+ def f(*nargs, **nkwargs):
+ try:
+ return func(*nargs, **nkwargs)
+ except exception:
+ return default
+
+ return f
+
+ return decorator
+
+
+__all__ = [
+ "dataclass_strictyaml_schema",
+ "dataclass_strictyaml",
+ "StrictyamlParser",
+ "ignore_exceptions",
+]
name = "click"
version = "7.1.2"
description = "Composable command line interface toolkit"
-category = "dev"
+category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[metadata]
lock-version = "1.1"
python-versions = "^3.6.12"
-content-hash = "e19083953f5e7f21555443d72701b765528ea26a5a9bbb746bb2093fb551acef"
+content-hash = "84b5fb8bb68a208f7a3b4027815766c8c6c2e82681789eacacf5838199b14a3d"
[metadata.files]
aiohttp = [
pydbus = "^0.6.0"
PyGObject = "^3.38.0"
Jinja2 = "^2.11.3"
+click = "^7.1.2"
[tool.poetry.dev-dependencies]
pytest = "^5.2"
debugpy = "^1.2.1"
[tool.poe.tasks]
-run = { cmd = "python -m knot_resolver_manager", help = "Run the manager" }
+run = { cmd = "scripts/run", help = "Run the manager" }
run-debug = { cmd = "scripts/run-debug", help = "Run the manager under debugger" }
test = { cmd = "pytest --cov=knot_resolver_manager --show-capture=all tests/", help = "Run tests" }
check = { cmd = "scripts/codecheck", help = "Run static code analysis" }
format = { cmd = "poetry run black knot_resolver_manager/ tests/", help = "Run 'Black' code formater" }
fixdeps = { shell = "poetry install; yarn install", help = "Install/update dependencies according to configuration files"}
commit = { shell = "scripts/commit", help = "Invoke every single check before commiting" }
-container-build = { cmd = "scripts/container-buildall", help = "Build all containers" }
+container-build = { cmd = "scripts/container-build", help = "Build containers (no arguments = all, otherwise arguments are tags that should be built)" }
container-run = { cmd = "scripts/container-run.py", help = "Run a container" }
clean = """
rm -rf .coverage
--- /dev/null
+{
+ "include": [
+ "knot_resolver_manager"
+ ],
+ "exclude": [
+ "knot_resolver_manager/compat/asyncio.py"
+ ]
+}
\ No newline at end of file
source $src_dir/_env.sh
-# build the actual containers
-for tag in $(find containers -maxdepth 1 -type d -printf '%f\n' | grep -v containers); do
+if test "$#" -eq 0; then
+ containers="$(find containers -maxdepth 1 -type d -printf '%f\n' | grep -v containers)"
+else
+ containers="$@"
+fi
+
+
+# build all configured containers
+for tag in $containers; do
podman build -t "knot-manager:$tag" -f "containers/$tag/Containerfile" .
done
\ No newline at end of file
#!/usr/bin/env python
import subprocess
-from typing import List, Optional
+from typing import Dict, List, Optional
import click
import time
-import itertools
+from pathlib import Path, PurePath
+import sys
PODMAN_EXECUTABLE = "/usr/bin/podman"
-def start_detached(image: str, publish: List[int] = []) -> str:
+
+def start_detached(
+ image: str, publish: List[int] = [], ro_mounts: Dict[PurePath, PurePath] = {}
+) -> str:
"""Start a detached container"""
- options = [ f"--publish={port}:{port}/tcp" for port in publish ]
+ options = [f"--publish={port}:{port}/tcp" for port in publish] + [
+ f"--mount=type=bind,source={str(src)},destination={str(dst)},ro=true"
+ for src, dst in ro_mounts.items()
+ ]
command = ["podman", "run", "--rm", "-d", *options, image]
- proc = subprocess.run(command, shell=False, executable=PODMAN_EXECUTABLE, stdout=subprocess.PIPE)
+ proc = subprocess.run(
+ command, shell=False, executable=PODMAN_EXECUTABLE, stdout=subprocess.PIPE
+ )
assert proc.returncode == 0
- return str(proc.stdout, 'utf8').strip()
+ return str(proc.stdout, "utf8").strip()
+
def exec(container_id: str, cmd: List[str]) -> int:
command = ["podman", "exec", container_id] + cmd
return subprocess.call(command, shell=False, executable=PODMAN_EXECUTABLE)
+
def exec_interactive(container_id: str, cmd: List[str]) -> int:
command = ["podman", "exec", "-ti", container_id] + cmd
return subprocess.call(command, shell=False, executable=PODMAN_EXECUTABLE)
+
def stop(container_id: str):
command = ["podman", "stop", container_id]
ret = subprocess.call(command, shell=False, executable=PODMAN_EXECUTABLE)
assert ret == 0
+def _get_git_root() -> PurePath:
+ result = subprocess.run(
+ "git rev-parse --show-toplevel", shell=True, stdout=subprocess.PIPE
+ )
+ return PurePath(str(result.stdout, encoding="utf8").strip())
+
+
@click.command()
@click.argument("image", nargs=1)
@click.argument("command", nargs=-1)
-@click.option("-p", "--publish", "publish", type=int, help="Port which should we publish")
-def main(image: str, command: List[str], publish: Optional[int]):
+@click.option(
+ "-p", "--publish", "publish", multiple=True, type=int, help="Port which should be published"
+)
+@click.option(
+ "-m",
+ "--mount",
+ "mount",
+ multiple=True,
+ nargs=1,
+ type=str,
+ help="Read-only bind mounts into the container, value /path/on/host:/path/in/container",
+)
+@click.option(
+ "-c",
+ "--code",
+ "mount_code",
+ default=False,
+ is_flag=True,
+ type=bool,
+ help="Shortcut to mount gitroot into /code",
+)
+def main(
+ image: str,
+ command: List[str],
+ publish: Optional[int],
+ mount: Optional[List[str]],
+ mount_code: bool,
+):
# make sure arguments have the correct type
image = str(image)
command = list(command)
- publish = [] if publish is None else [int(publish)]
+ publish = [] if publish is None else [int(p) for p in publish]
+ mount = [] if mount is None else [x.split(":") for x in mount]
+ mount_path = {Path(x[0]).absolute(): Path(x[1]).absolute() for x in mount}
+ for src_path in mount_path:
+ if not src_path.exists():
+ print(
+ f'The specified path "{str(src_path)}" does not exist on the host system',
+ file=sys.stderr,
+ )
+ exit(1)
+ if mount_code:
+ mount_path[_get_git_root()] = Path("/code")
- cont = start_detached(image, publish=publish)
+ cont = start_detached(image, publish=publish, ro_mounts=mount_path)
# wait for the container to boot properly
time.sleep(0.5)
# run the command
# stop the container
stop(cont)
+
if __name__ == "__main__":
- main()
\ No newline at end of file
+ main()
--- /dev/null
+#!/bin/bash
+
+# fail early
+set -e
+
+# ensure consistent behaviour
+src_dir="$(dirname "$(realpath "$0")")"
+source $src_dir/_env.sh
+
+# build dev container
+poe container-build dev
+
+echo Knot Manager API is accessible on http://localhost:9000
+echo -------------------------------------------------------
+
+poe container-run --code -p 9000 -- knot-manager:dev python -m knot_resolver_manager 9000
\ No newline at end of file
src_dir="$(dirname "$(realpath "$0")")"
source $src_dir/_env.sh
-# build all containers
-poe container-build
+# build dev container
+poe container-build dev
echo The debug server will be listening on port localhost:5678
-echo Use VSCode remote attach feature to connect to the debugger server
+echo Use VSCode remote attach feature to connect to the debug server
echo The manager will start after you connect
echo ----------------------------------------
-poe container-run -p 5678 -- knot-manager:dev python -m debugpy --listen 0.0.0.0:5678 --wait-for-client -m knot_resolver_manager
\ No newline at end of file
+poe container-run -p 5678 --code -- knot-manager:dev python -m debugpy --listen 0.0.0.0:5678 --wait-for-client -m knot_resolver_manager
\ No newline at end of file