きり丸の技術日記

技術検証したり、資格等をここに残していきます。

ReadOnlyなDocker環境のPytestを実行するとNo such file or directoryが発生した

pytestの起動時に失敗するので、原因を把握するのに時間がかかってしまいました。

環境

  • Docker
    • Docker version 20.10.13, build a224086
  • Docker Compose
    • docker-compose version 1.29.2, build 5becea4c
  • Windows
    • Macだと発生しない事象のようです

前提

Docker Composeのボリュームにread_only属性が設定されている。

原因

pytestが/tmpディレクトリに何かしらの書き込みをしようとしています。

read_only属性がついている場合、書き込み先ディレクトリをvolumesに設定する必要があります。

volumes/tmpが設定されていないため、書き込みができずにエラーとなっているようです。

対策

volumes/tmpを追加します。

services:
  api:
    read_only: true # 前提
    volumes:
      - /tmp # 追加

ソースコード

終わりに

read_only属性はホスト側に余計な情報を書き込ませないようにするためのもので、コンテナ内のディレクトリについて制限するものではないと思っていました。もし、コンテナ内のディレクトリを制限するものだとしても、/tmpディレクトリに関しては常に全権限与えられているものだという思い込みがあったので、調査に時間がかかってしまいました。

しかもなぜかpytestではエラーとなるが、pytest -sでは正常に動くという謎挙動をしておりました。-sは詳細ログを出すようにするためのオプションだと思っていたのですが、標準出力先が違うのかもしれませんね(未検証)

事象自体はpytestに限らないと思うので、ぜひ同事象に引っかかったら教えていただけると、記事に載せていただこうと考えています。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考情報

発生ログ

$ docker-compose run --entrypoint "poetry run pytest --asyncio-mode=strict" api
Creating fastapi-practice_api_run ... done
============================================================ test session starts ============================================================
platform linux -- Python 3.9.12, pytest-7.1.1, pluggy-1.0.0
rootdir: /src
plugins: anyio-3.5.0, asyncio-0.18.3
asyncio: mode=strict
collected 0 items

=========================================================== no tests ran in 0.17s ===========================================================
Traceback (most recent call last):
  File "/src/.venv/lib/python3.9/site-packages/_pytest/main.py", line 268, in wrap_session
    session.exitstatus = doit(config, session) or 0
  File "/src/.venv/lib/python3.9/site-packages/_pytest/main.py", line 321, in _main
    config.hook.pytest_collection(session=session)
  File "/src/.venv/lib/python3.9/site-packages/pluggy/_hooks.py", line 265, in __call__
    return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
  File "/src/.venv/lib/python3.9/site-packages/pluggy/_manager.py", line 80, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
  File "/src/.venv/lib/python3.9/site-packages/pluggy/_callers.py", line 60, in _multicall
    return outcome.get_result()
  File "/src/.venv/lib/python3.9/site-packages/pluggy/_result.py", line 60, in get_result
    raise ex[1].with_traceback(ex[2])
  File "/src/.venv/lib/python3.9/site-packages/pluggy/_callers.py", line 39, in _multicall
    res = hook_impl.function(*args)
  File "/src/.venv/lib/python3.9/site-packages/_pytest/main.py", line 332, in pytest_collection
    session.perform_collect()
  File "/src/.venv/lib/python3.9/site-packages/_pytest/main.py", line 657, in perform_collect
    self.items.extend(self.genitems(node))
  File "/src/.venv/lib/python3.9/site-packages/_pytest/main.py", line 824, in genitems
    rep = collect_one_node(node)
  File "/src/.venv/lib/python3.9/site-packages/_pytest/runner.py", line 537, in collect_one_node
    rep: CollectReport = ihook.pytest_make_collect_report(collector=collector)
  File "/src/.venv/lib/python3.9/site-packages/pluggy/_hooks.py", line 265, in __call__
    return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
  File "/src/.venv/lib/python3.9/site-packages/pluggy/_manager.py", line 80, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
  File "/src/.venv/lib/python3.9/site-packages/pluggy/_callers.py", line 55, in _multicall
    gen.send(outcome)
  File "/src/.venv/lib/python3.9/site-packages/_pytest/capture.py", line 769, in pytest_make_collect_report
    out, err = self.read_global_capture()
  File "/src/.venv/lib/python3.9/site-packages/_pytest/capture.py", line 691, in read_global_capture
    return self._global_capturing.readouterr()
  File "/src/.venv/lib/python3.9/site-packages/_pytest/capture.py", line 596, in readouterr
    out = self.out.snap() if self.out else ""
  File "/src/.venv/lib/python3.9/site-packages/_pytest/capture.py", line 457, in snap
    self.tmpfile.truncate()
FileNotFoundError: [Errno 2] No such file or directory

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/src/.venv/lib/python3.9/site-packages/_pytest/main.py", line 288, in wrap_session
    config.notify_exception(excinfo, config.option)
  File "/src/.venv/lib/python3.9/site-packages/_pytest/config/__init__.py", line 1050, in notify_exception
    res = self.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo)
  File "/src/.venv/lib/python3.9/site-packages/pluggy/_hooks.py", line 265, in __call__
    return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
  File "/src/.venv/lib/python3.9/site-packages/pluggy/_manager.py", line 80, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
  File "/src/.venv/lib/python3.9/site-packages/pluggy/_callers.py", line 60, in _multicall
    return outcome.get_result()
  File "/src/.venv/lib/python3.9/site-packages/pluggy/_result.py", line 60, in get_result
    raise ex[1].with_traceback(ex[2])
  File "/src/.venv/lib/python3.9/site-packages/pluggy/_callers.py", line 39, in _multicall
    res = hook_impl.function(*args)
  File "/src/.venv/lib/python3.9/site-packages/_pytest/capture.py", line 799, in pytest_internalerror
    self.stop_global_capturing()
  File "/src/.venv/lib/python3.9/site-packages/_pytest/capture.py", line 666, in stop_global_capturing
    self._global_capturing.pop_outerr_to_orig()
  File "/src/.venv/lib/python3.9/site-packages/_pytest/capture.py", line 552, in pop_outerr_to_orig
    out, err = self.readouterr()
  File "/src/.venv/lib/python3.9/site-packages/_pytest/capture.py", line 596, in readouterr
    out = self.out.snap() if self.out else ""
  File "/src/.venv/lib/python3.9/site-packages/_pytest/capture.py", line 457, in snap
    self.tmpfile.truncate()
FileNotFoundError: [Errno 2] No such file or directory

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/src/.venv/bin/pytest", line 8, in <module>
    sys.exit(console_main())
  File "/src/.venv/lib/python3.9/site-packages/_pytest/config/__init__.py", line 187, in console_main
    code = main()
  File "/src/.venv/lib/python3.9/site-packages/_pytest/config/__init__.py", line 164, in main
    ret: Union[ExitCode, int] = config.hook.pytest_cmdline_main(
  File "/src/.venv/lib/python3.9/site-packages/pluggy/_hooks.py", line 265, in __call__
    return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
  File "/src/.venv/lib/python3.9/site-packages/pluggy/_manager.py", line 80, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
  File "/src/.venv/lib/python3.9/site-packages/pluggy/_callers.py", line 60, in _multicall
    return outcome.get_result()
  File "/src/.venv/lib/python3.9/site-packages/pluggy/_result.py", line 60, in get_result
    raise ex[1].with_traceback(ex[2])
  File "/src/.venv/lib/python3.9/site-packages/pluggy/_callers.py", line 39, in _multicall
    res = hook_impl.function(*args)
  File "/src/.venv/lib/python3.9/site-packages/_pytest/main.py", line 315, in pytest_cmdline_main
    return wrap_session(config, _main)
  File "/src/.venv/lib/python3.9/site-packages/_pytest/main.py", line 310, in wrap_session
    config._ensure_unconfigure()
  File "/src/.venv/lib/python3.9/site-packages/_pytest/config/__init__.py", line 1004, in _ensure_unconfigure
    fin()
  File "/src/.venv/lib/python3.9/site-packages/_pytest/capture.py", line 666, in stop_global_capturing
    self._global_capturing.pop_outerr_to_orig()
  File "/src/.venv/lib/python3.9/site-packages/_pytest/capture.py", line 552, in pop_outerr_to_orig
    out, err = self.readouterr()
  File "/src/.venv/lib/python3.9/site-packages/_pytest/capture.py", line 596, in readouterr
    out = self.out.snap() if self.out else ""
  File "/src/.venv/lib/python3.9/site-packages/_pytest/capture.py", line 457, in snap
    self.tmpfile.truncate()
FileNotFoundError: [Errno 2] No such file or directory
ERROR: 1