Repository navigation
refactor(containers): one builder for the docker run hardening and resource flags #249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
b76f1bc
test(containers): characterize the docker run argv of both call sites
napetrov 0100bf1
refactor(containers): one builder for the docker run hardening and re…
napetrov 548d8c0
test(containers): pin ContainerPolicy defaults and each field alone
antonwolfy 8f1b688
Merge remote-tracking branch 'intel/main' into refactor/container-policy
napetrov 156ca92
Merge remote-tracking branch 'intel/main' into pr249-fix
napetrov File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # Copyright (C) 2026 Intel Corporation | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """The hardening and resource flags of a ``docker run``, spelled once. | ||
|
|
||
| Two call sites start containers with the docker CLI: :class:`dokimi.repos.container.DockerRunner` | ||
| and the docker backend of :mod:`dokimi.metrics.example_runner`. They use different subsets of | ||
| the same flags, and each has its own argv order. :class:`ContainerPolicy` owns how every flag | ||
| is spelled. A call site sets the fields it wants and splices the groups in the order its argv | ||
| has always had; that order is pinned by ``tests/test_container_argv_characterization.py``. | ||
|
|
||
| The builder chooses nothing and validates nothing: a field left at its default adds no flag, | ||
| so each call site opts in to what it runs with. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from dataclasses import dataclass | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class ContainerPolicy: | ||
| network: str | None = None # --network; None adds no flag | ||
| init: bool = False # --init: a daemonizing build server is reaped, its cache writes are kept | ||
| no_new_privileges: bool = False # --security-opt no-new-privileges: a setuid binary cannot gain root | ||
| cap_drop_all: bool = False # --cap-drop ALL | ||
| pids_limit: int | None = None # --pids-limit: a ceiling against a fork bomb | ||
| cpus: float | None = None # --cpus | ||
| memory_mb: int | None = None # --memory <n>m | ||
| read_only: bool = False # --read-only: the container's root filesystem | ||
|
|
||
| def init_argv(self) -> list[str]: | ||
| return ["--init"] if self.init else [] | ||
|
|
||
| def hardening_argv(self) -> list[str]: | ||
| out: list[str] = [] | ||
| if self.no_new_privileges: | ||
| out += ["--security-opt", "no-new-privileges"] | ||
| if self.cap_drop_all: | ||
| out += ["--cap-drop", "ALL"] | ||
| return out | ||
|
|
||
| def pids_argv(self) -> list[str]: | ||
| return [] if self.pids_limit is None else ["--pids-limit", str(self.pids_limit)] | ||
|
|
||
| def network_argv(self) -> list[str]: | ||
| return [] if self.network is None else ["--network", self.network] | ||
|
|
||
| def resource_argv(self) -> list[str]: | ||
| out: list[str] = [] | ||
| if self.cpus is not None: | ||
| out += ["--cpus", str(self.cpus)] | ||
| if self.memory_mb is not None: | ||
| out += ["--memory", f"{self.memory_mb}m"] | ||
| return out | ||
|
|
||
| def read_only_argv(self) -> list[str]: | ||
| return ["--read-only"] if self.read_only else [] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| # Copyright (C) 2026 Intel Corporation | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Characterization of the ``docker run`` argv each call site builds today. | ||
|
|
||
| These pin the exact argv, flag order included, of the two container call sites that | ||
| the container policy builder serves: :class:`dokimi.repos.container.DockerRunner` and | ||
| the docker backend of :mod:`dokimi.metrics.example_runner`. They were written against | ||
| the code before the builder existed, and must keep passing through the refactor: a | ||
| change to any expected list here is an argv change, which is a separate decision. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| import pytest | ||
|
|
||
| from dokimi.metrics.example_runner import EXECUTORS, ExecutionPolicy, run_example | ||
| from dokimi.process import FakeRunner, reply | ||
| from dokimi.repos import container | ||
| from dokimi.repos.container import ( | ||
| CONTENT_DIR, | ||
| WORKSPACE, | ||
| ContainerError, | ||
| ContainerSpec, | ||
| DockerRunner, | ||
| Mount, | ||
| ) | ||
|
|
||
| # --- repos/container.py: DockerRunner.argv ----------------------------------------- | ||
|
|
||
|
|
||
| def _no_host_user(monkeypatch: pytest.MonkeyPatch, uid_gid: str | None) -> None: | ||
| monkeypatch.setattr(container, "host_user", lambda: uid_gid) | ||
|
|
||
|
|
||
| def test_repo_argv_full_spec_pins_every_flag_in_order(monkeypatch): | ||
| _no_host_user(monkeypatch, None) | ||
| spec = ContainerSpec( | ||
| image="img", | ||
| argv=["python3", "x"], | ||
| mounts=[Mount(Path("/h/run"), "/eval/run"), Mount(Path("/h/content"), CONTENT_DIR, read_only=True)], | ||
| env={"HOME": "/eval/run/home"}, | ||
| pass_env=("AWS_BEARER_TOKEN_BEDROCK",), | ||
| network="none", | ||
| name="dokimi-x", | ||
| user="1000:1000", | ||
| ) | ||
| assert DockerRunner().argv(spec) == [ | ||
| "docker", "run", "--rm", "--init", | ||
| "--security-opt", "no-new-privileges", "--cap-drop", "ALL", | ||
| "--pids-limit", "32768", | ||
| "--network", "none", | ||
| "-w", WORKSPACE, | ||
| "--name", "dokimi-x", | ||
| "--user", "1000:1000", | ||
| "--mount", "type=bind,src=/h/run,dst=/eval/run", | ||
| "--mount", f"type=bind,src=/h/content,dst={CONTENT_DIR},readonly", | ||
| "-e", "HOME=/eval/run/home", | ||
| "-e", "AWS_BEARER_TOKEN_BEDROCK", | ||
| "--entrypoint", "", "img", "python3", "x", | ||
| ] | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("network", ["none", "bridge", "host"]) | ||
| def test_repo_argv_network_mode_is_the_only_network_difference(monkeypatch, network): | ||
| _no_host_user(monkeypatch, None) | ||
| argv = DockerRunner().argv(ContainerSpec(image="img", argv=["make"], network=network)) | ||
| assert argv == [ | ||
| "docker", "run", "--rm", "--init", | ||
| "--security-opt", "no-new-privileges", "--cap-drop", "ALL", | ||
| "--pids-limit", "32768", | ||
| "--network", network, | ||
| "-w", WORKSPACE, | ||
| "--entrypoint", "", "img", "make", | ||
| ] | ||
|
|
||
|
|
||
| def test_repo_argv_takes_the_host_uid_gid_when_the_spec_has_no_user(monkeypatch): | ||
| _no_host_user(monkeypatch, "4242:4343") | ||
| argv = DockerRunner().argv(ContainerSpec(image="img", argv=["true"], network="none")) | ||
| i = argv.index("--user") | ||
| assert argv[i - 2:i + 2] == ["-w", WORKSPACE, "--user", "4242:4343"] | ||
|
|
||
|
|
||
| def test_repo_argv_omits_user_when_there_is_no_posix_uid(monkeypatch): | ||
| _no_host_user(monkeypatch, None) | ||
| assert "--user" not in DockerRunner().argv(ContainerSpec(image="img", argv=["true"])) | ||
|
|
||
|
|
||
| def test_repo_argv_unknown_network_is_refused(): | ||
| with pytest.raises(ContainerError, match="unknown network 'wide-open'"): | ||
| DockerRunner().argv(ContainerSpec(image="i", argv=[], network="wide-open")) | ||
|
|
||
|
|
||
| def test_repo_argv_writable_mount_has_no_readonly_suffix(monkeypatch): | ||
| _no_host_user(monkeypatch, None) | ||
| argv = DockerRunner().argv(ContainerSpec(image="i", argv=[], mounts=[Mount(Path("/h/w"), "/eval/w")])) | ||
| assert "type=bind,src=/h/w,dst=/eval/w" in argv | ||
| assert "readonly" not in " ".join(argv) | ||
|
|
||
|
|
||
| # --- metrics/example_runner.py: docker backend argv ------------------------------ | ||
|
|
||
| _TMP_MOUNT = "<tmp>:/work:ro" | ||
|
|
||
|
|
||
| def _docker_cmd(policy: ExecutionPolicy, lang: str = "python") -> tuple[list[str], float]: | ||
| """The argv and timeout of one docker-backend run, with the per-run temp dir masked.""" | ||
| runner = FakeRunner(lambda call: reply()) | ||
| res = run_example(lang, "print(1)", policy, runner) | ||
| assert res.passed is True | ||
| (call,) = runner.calls | ||
| argv = [_TMP_MOUNT if a.endswith(":/work:ro") else a for a in call.argv] | ||
| return argv, call.timeout | ||
|
|
||
|
|
||
| def test_example_docker_argv_default_policy_pins_every_flag_in_order(): | ||
| argv, timeout = _docker_cmd(ExecutionPolicy(backend="docker", timeout=7)) | ||
| assert argv == [ | ||
| "docker", "run", "--rm", | ||
| "--network", "none", | ||
| "--cpus", "1.0", | ||
| "--memory", "512m", | ||
| "--pids-limit", "64", | ||
| "--read-only", | ||
| "-v", _TMP_MOUNT, | ||
| "python:3.11-slim", | ||
| "timeout", "7", | ||
| *EXECUTORS["python"][1], "/work/snippet.py", | ||
| ] | ||
| assert timeout == 37 # the snippet's timeout plus the docker CLI's grace | ||
|
|
||
|
|
||
| def test_example_docker_argv_resource_values_come_from_the_policy(): | ||
| policy = ExecutionPolicy(backend="docker", timeout=3, docker_image="repo/img:1", mem_mb=1024, cpus=2.5) | ||
| argv, _ = _docker_cmd(policy, lang="bash") | ||
| assert argv == [ | ||
| "docker", "run", "--rm", | ||
| "--network", "none", | ||
| "--cpus", "2.5", | ||
| "--memory", "1024m", | ||
| "--pids-limit", "64", | ||
| "--read-only", | ||
| "-v", _TMP_MOUNT, | ||
| "repo/img:1", | ||
| "timeout", "3", | ||
| *EXECUTORS["bash"][1], "/work/snippet.sh", | ||
| ] | ||
|
|
||
|
|
||
| def test_example_docker_argv_never_sets_network_from_the_policy(): | ||
| # The docker backend has no network mode: it is always "none". | ||
| argv, _ = _docker_cmd(ExecutionPolicy(backend="docker", timeout=5)) | ||
| assert argv[argv.index("--network") + 1] == "none" | ||
|
|
||
|
|
||
| def test_example_docker_argv_has_no_init_no_cap_drop_no_new_privileges(): | ||
| # Recorded gap: this site carries none of the repos/container.py hardening today. | ||
| # Adding it is an argv change and is a separate follow-up, so this test pins the absence. | ||
| argv, _ = _docker_cmd(ExecutionPolicy(backend="docker", timeout=5)) | ||
| for flag in ("--init", "--cap-drop", "no-new-privileges"): | ||
| assert flag not in argv |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| # Copyright (C) 2026 Intel Corporation | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """The defaults and opt-out branches of :class:`dokimi.container_policy.ContainerPolicy`. | ||
|
|
||
| The two call sites exercise one field combination each, so each field is pinned here alone. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import pytest | ||
|
|
||
| from dokimi.container_policy import ContainerPolicy | ||
|
|
||
| _GROUPS = ( | ||
| "init_argv", | ||
| "hardening_argv", | ||
| "pids_argv", | ||
| "network_argv", | ||
| "resource_argv", | ||
| "read_only_argv", | ||
| ) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("group", _GROUPS) | ||
| def test_default_policy_adds_no_flag(group): | ||
| assert getattr(ContainerPolicy(), group)() == [] | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ("policy", "group", "expected"), | ||
| [ | ||
| (ContainerPolicy(init=True), "init_argv", ["--init"]), | ||
| ( | ||
| ContainerPolicy(no_new_privileges=True), | ||
| "hardening_argv", | ||
| ["--security-opt", "no-new-privileges"], | ||
| ), | ||
| (ContainerPolicy(cap_drop_all=True), "hardening_argv", ["--cap-drop", "ALL"]), | ||
| (ContainerPolicy(pids_limit=64), "pids_argv", ["--pids-limit", "64"]), | ||
| (ContainerPolicy(pids_limit=0), "pids_argv", ["--pids-limit", "0"]), | ||
| (ContainerPolicy(network="none"), "network_argv", ["--network", "none"]), | ||
| (ContainerPolicy(cpus=1), "resource_argv", ["--cpus", "1"]), | ||
| (ContainerPolicy(cpus=1.5), "resource_argv", ["--cpus", "1.5"]), | ||
| (ContainerPolicy(cpus=0.0), "resource_argv", ["--cpus", "0.0"]), | ||
| (ContainerPolicy(memory_mb=512), "resource_argv", ["--memory", "512m"]), | ||
| (ContainerPolicy(memory_mb=0), "resource_argv", ["--memory", "0m"]), | ||
| (ContainerPolicy(read_only=True), "read_only_argv", ["--read-only"]), | ||
| ], | ||
| ) | ||
| def test_each_field_alone_yields_exactly_its_flag(policy, group, expected): | ||
| assert getattr(policy, group)() == expected | ||
| assert all(getattr(policy, g)() == [] for g in _GROUPS if g != group) | ||
|
|
||
|
|
||
| def test_hardening_flags_combine_in_order(): | ||
| policy = ContainerPolicy(no_new_privileges=True, cap_drop_all=True) | ||
| assert policy.hardening_argv() == ["--security-opt", "no-new-privileges", "--cap-drop", "ALL"] | ||
|
|
||
|
|
||
| def test_resource_flags_combine_in_order(): | ||
| assert ContainerPolicy(cpus=2, memory_mb=256).resource_argv() == [ | ||
| "--cpus", | ||
| "2", | ||
| "--memory", | ||
| "256m", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.