Skip to content

Commit

Permalink
fix: disable ulimits in rootless docker mode
Browse files Browse the repository at this point in the history
  • Loading branch information
OmarIthawi committed Oct 12, 2023
1 parent 509cddc commit 8c1312b
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 4 deletions.
17 changes: 17 additions & 0 deletions tests/commands/test_dev.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import unittest
from unittest.mock import patch

from .base import TestCommandMixin

from tutor.commands import dev


class DevTests(unittest.TestCase, TestCommandMixin):
def test_dev_help(self) -> None:
result = self.invoke(["dev", "--help"])
self.assertEqual(0, result.exit_code)
self.assertIsNone(result.exception)

@patch("tutor.utils.is_docker_rootless")
def test_rootless_no_ulimits(self, mock_is_docker_rootless) -> None:
configs = {
'DEV_PROJECT_NAME': 'maple',
}

mock_is_docker_rootless.return_value = False
task_runner = dev.DevTaskRunner('/app', configs)
self.assertIn('/app/env/local/docker-compose.ulimits.yml', task_runner.docker_compose_files)

mock_is_docker_rootless.return_value = True
task_runner = dev.DevTaskRunner('/app', configs)
self.assertNotIn('/app/env/local/docker-compose.ulimits.yml', task_runner.docker_compose_files)
20 changes: 20 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import tempfile
import unittest
from io import StringIO
import subprocess
from typing import List, Tuple
from unittest.mock import MagicMock, mock_open, patch

Expand Down Expand Up @@ -241,6 +242,25 @@ def test_is_http(self) -> None:
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) -> 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) -> 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"),
Expand Down
9 changes: 9 additions & 0 deletions tutor/commands/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from tutor import hooks
from tutor.commands import compose
from tutor.types import Config, get_typed
from tutor import utils


class DevTaskRunner(compose.ComposeTaskRunner):
Expand All @@ -23,6 +24,14 @@ def __init__(self, root: str, config: Config):
tutor_env.pathjoin(self.root, "local", "docker-compose.override.yml"),
tutor_env.pathjoin(self.root, "dev", "docker-compose.override.yml"),
]


if not utils.is_docker_rootless():
# Setting `ulimits` throws an error when running in rootless mode.
self.docker_compose_files += [
tutor_env.pathjoin(self.root, "local", "docker-compose.ulimits.yml"),
]

self.docker_compose_job_files += [
tutor_env.pathjoin(self.root, "local", "docker-compose.jobs.yml"),
tutor_env.pathjoin(self.root, "dev", "docker-compose.jobs.yml"),
Expand Down
1 change: 1 addition & 0 deletions tutor/commands/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def __init__(self, root: str, config: Config):
self.project_name = get_typed(self.config, "LOCAL_PROJECT_NAME", str)
self.docker_compose_files += [
tutor_env.pathjoin(self.root, "local", "docker-compose.yml"),
tutor_env.pathjoin(self.root, "local", "docker-compose.ulimits.yml"),
tutor_env.pathjoin(self.root, "local", "docker-compose.prod.yml"),
tutor_env.pathjoin(self.root, "local", "docker-compose.override.yml"),
]
Expand Down
11 changes: 11 additions & 0 deletions tutor/templates/local/docker-compose.ulimits.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# This file contains the `ulimits` configuration which is skipped on rootless Docker because
# it's not supported.
version: "{{ DOCKER_COMPOSE_VERSION }}"
services:
{% if RUN_ELASTICSEARCH -%}
elasticsearch:
ulimits:
memlock:
soft: -1
hard: -1
{%- endif %}
4 changes: 0 additions & 4 deletions tutor/templates/local/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,6 @@ services:
- bootstrap.memory_lock=true
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms{{ ELASTICSEARCH_HEAP_SIZE }} -Xmx{{ ELASTICSEARCH_HEAP_SIZE }}"
ulimits:
memlock:
soft: -1
hard: -1
restart: unless-stopped
user: "1000:1000"
volumes:
Expand Down
14 changes: 14 additions & 0 deletions tutor/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,20 @@ def is_buildkit_enabled() -> bool:
return False


@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)

Expand Down

0 comments on commit 8c1312b

Please sign in to comment.