diff --git a/changelog.d/20231116_081945_i_rootless_docker.md b/changelog.d/20231116_081945_i_rootless_docker.md new file mode 100644 index 0000000..717e3a2 --- /dev/null +++ b/changelog.d/20231116_081945_i_rootless_docker.md @@ -0,0 +1,2 @@ +- [Improvement] Fix `ulimits` error for elasticsearch in Docker rootless mode (by @OmarIthawi) + diff --git a/tests/test_utils.py b/tests/test_utils.py index 93d4a48..21af475 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,6 +3,7 @@ import os import tempfile import unittest from io import StringIO +import subprocess from typing import List, Tuple from unittest.mock import MagicMock, mock_open, patch @@ -241,6 +242,25 @@ class UtilsTests(unittest.TestCase): self.assertFalse(utils.is_http("home/user/")) self.assertFalse(utils.is_http("http-home/user/")) + @patch("subprocess.run") + def test_is_docker_rootless(self, mock_run: MagicMock) -> None: + # Mock rootless `docker info` output + utils.is_docker_rootless.cache_clear() + mock_run.return_value.stdout = "some prefix\n rootless foo bar".encode("utf-8") + self.assertTrue(utils.is_docker_rootless()) + + # Mock regular `docker info` output + utils.is_docker_rootless.cache_clear() + mock_run.return_value.stdout = "some prefix, regular docker".encode("utf-8") + self.assertFalse(utils.is_docker_rootless()) + + @patch("subprocess.run") + def test_is_docker_rootless_podman(self, mock_run: MagicMock) -> None: + """Test the `is_docker_rootless` when podman is used or any other error with `docker info`""" + utils.is_docker_rootless.cache_clear() + mock_run.side_effect = subprocess.CalledProcessError(1, "docker info") + self.assertFalse(utils.is_docker_rootless()) + def test_format_table(self) -> None: rows: List[Tuple[str, ...]] = [ ("a", "xyz", "value 1"), diff --git a/tutor/env.py b/tutor/env.py index 1baf54b..dd29b0c 100644 --- a/tutor/env.py +++ b/tutor/env.py @@ -57,6 +57,7 @@ def _prepare_environment() -> None: # BuildKit used to be optional. Now, it's always enabled. # This constant is just for temporary backwards compatibility (REMOVE-AFTER-V16). ("is_buildkit_enabled", lambda: True), + ("is_docker_rootless", utils.is_docker_rootless), ], ) diff --git a/tutor/templates/dev/docker-compose.yml b/tutor/templates/dev/docker-compose.yml index 4b8cd38..7c86435 100644 --- a/tutor/templates/dev/docker-compose.yml +++ b/tutor/templates/dev/docker-compose.yml @@ -52,4 +52,13 @@ services: command: openedx-assets watch-themes --env dev restart: unless-stopped + {% if RUN_ELASTICSEARCH and is_docker_rootless() %} + elasticsearch: + ulimits: + memlock: + # Fixes error setting rlimits for ready process in rootless docker + soft: 0 # zero means "unset" in the memlock context + hard: 0 + {% endif %} + {{ patch("local-docker-compose-dev-services")|indent(2) }} diff --git a/tutor/utils.py b/tutor/utils.py index 2d8d3e5..ab479cf 100644 --- a/tutor/utils.py +++ b/tutor/utils.py @@ -173,6 +173,20 @@ def docker(*command: str) -> int: return execute("docker", *command) +@lru_cache(maxsize=None) +def is_docker_rootless() -> bool: + """ + A helper function to determine if Docker is running in rootless mode. + + - https://docs.docker.com/engine/security/rootless/ + """ + try: + results = subprocess.run(["docker", "info"], capture_output=True, check=True) + return "rootless" in results.stdout.decode() + except subprocess.CalledProcessError: + return False + + def docker_compose(*command: str) -> int: return execute("docker", "compose", *command)