Skip to content

Commit e980c9e

Browse files
authored
Avoid get_cache_dir errors with read only virtualenvs (#457)
Fixes: ansible/ansible-lint#4501
1 parent 02624c7 commit e980c9e

3 files changed

Lines changed: 106 additions & 20 deletions

File tree

.packit.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ jobs:
4242
- job: tests
4343
trigger: pull_request
4444
branch: main
45+
require:
46+
label:
47+
present:
48+
- bug
49+
- dependencies
50+
- enhancement
51+
- major
52+
- minor
53+
absent:
54+
- skip-changelog
4555
targets:
4656
- fedora-latest
4757
- fedora-rawhide

src/ansible_compat/prerun.py

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,26 @@
33
import hashlib
44
import os
55
import tempfile
6+
import warnings
67
from pathlib import Path
78

89

10+
def is_writable(path: Path) -> bool:
11+
"""Check if path is writable, creating if necessary.
12+
13+
Args:
14+
path: Path to check.
15+
16+
Returns:
17+
True if path is writable, False otherwise.
18+
"""
19+
try:
20+
path.mkdir(parents=True, exist_ok=True)
21+
except OSError:
22+
return False
23+
return path.exists() and os.access(path, os.W_OK)
24+
25+
926
def get_cache_dir(project_dir: Path, *, isolated: bool = True) -> Path:
1027
"""Compute cache directory to be used based on project path.
1128
@@ -14,31 +31,51 @@ def get_cache_dir(project_dir: Path, *, isolated: bool = True) -> Path:
1431
isolated: Whether to use isolated cache directory.
1532
1633
Returns:
17-
Cache directory path.
34+
A writable cache directory.
1835
1936
Raises:
2037
RuntimeError: if cache directory is not writable.
38+
OSError: if cache directory cannot be created.
2139
"""
22-
cache_dir = Path(os.environ.get("ANSIBLE_HOME", "~/.ansible")).expanduser()
23-
40+
cache_dir: Path | None = None
2441
if "VIRTUAL_ENV" in os.environ:
25-
path = Path(os.environ["VIRTUAL_ENV"])
26-
if not path.exists(): # pragma: no cover
27-
msg = f"VIRTUAL_ENV={os.environ['VIRTUAL_ENV']} does not exist."
28-
raise RuntimeError(msg)
29-
cache_dir = path.resolve() / ".ansible"
30-
elif isolated:
31-
if not project_dir.exists() or not os.access(project_dir, os.W_OK):
32-
# As "project_dir" can also be "/" and user might not be able
33-
# to write to it, we use a temporary directory as fallback.
34-
checksum = hashlib.sha256(
35-
project_dir.as_posix().encode("utf-8"),
36-
).hexdigest()[:4]
37-
38-
cache_dir = Path(tempfile.gettempdir()) / f".ansible-{checksum}"
39-
cache_dir.mkdir(parents=True, exist_ok=True)
42+
path = Path(os.environ["VIRTUAL_ENV"]).resolve() / ".ansible"
43+
if is_writable(path):
44+
cache_dir = path
45+
else:
46+
msg = f"Found VIRTUAL_ENV={os.environ['VIRTUAL_ENV']} but we cannot use it for caching as it is not writable."
47+
warnings.warn(
48+
message=msg,
49+
stacklevel=2,
50+
source={"msg": msg},
51+
)
52+
53+
if isolated:
54+
project_dir = project_dir.resolve() / ".ansible"
55+
if is_writable(project_dir):
56+
cache_dir = project_dir
4057
else:
41-
cache_dir = project_dir.resolve() / ".ansible"
58+
msg = f"Project directory {project_dir} cannot be used for caching as it is not writable."
59+
warnings.warn(msg, stacklevel=2)
60+
else:
61+
cache_dir = Path(os.environ.get("ANSIBLE_HOME", "~/.ansible")).expanduser()
62+
# This code should be never be reached because import from ansible-core
63+
# would trigger a fatal error if this location is not writable.
64+
if not is_writable(cache_dir): # pragma: no cover
65+
msg = f"Cache directory {cache_dir} is not writable."
66+
raise OSError(msg)
67+
68+
if not cache_dir:
69+
# As "project_dir" can also be "/" and user might not be able
70+
# to write to it, we use a temporary directory as fallback.
71+
checksum = hashlib.sha256(
72+
project_dir.as_posix().encode("utf-8"),
73+
).hexdigest()[:4]
74+
75+
cache_dir = Path(tempfile.gettempdir()) / f".ansible-{checksum}"
76+
cache_dir.mkdir(parents=True, exist_ok=True)
77+
msg = f"Using unique temporary directory {cache_dir} for caching."
78+
warnings.warn(msg, stacklevel=2)
4279

4380
# Ensure basic folder structure exists so `ansible-galaxy list` does not
4481
# fail with: None of the provided paths were usable. Please specify a valid path with
@@ -49,4 +86,5 @@ def get_cache_dir(project_dir: Path, *, isolated: bool = True) -> Path:
4986
msg = "Failed to create cache directory."
5087
raise RuntimeError(msg) from exc
5188

89+
# We succeed only if the path is writable.
5290
return cache_dir

test/test_prerun.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from pathlib import Path
77
from typing import TYPE_CHECKING
88

9+
import pytest
10+
911
if TYPE_CHECKING:
1012
from _pytest.monkeypatch import MonkeyPatch
1113

@@ -50,5 +52,41 @@ def test_get_cache_dir_isolation_no_venv_root(monkeypatch: MonkeyPatch) -> None:
5052
"""
5153
monkeypatch.delenv("VIRTUAL_ENV", raising=False)
5254
monkeypatch.delenv("ANSIBLE_HOME", raising=False)
53-
cache_dir = get_cache_dir(Path("/"), isolated=True)
55+
with (
56+
pytest.warns(
57+
UserWarning,
58+
match=r"Project directory /.ansible cannot be used for caching as it is not writable.",
59+
),
60+
pytest.warns(
61+
UserWarning,
62+
match=r"Using unique temporary directory .* for caching.",
63+
),
64+
):
65+
cache_dir = get_cache_dir(Path("/"), isolated=True)
66+
assert cache_dir.as_posix().startswith(tempfile.gettempdir())
67+
68+
69+
def test_get_cache_dir_venv_ro_project_ro(monkeypatch: MonkeyPatch) -> None:
70+
"""Test behaviors of get_cache_dir with read-only virtual environment and read only project directory.
71+
72+
Args:
73+
monkeypatch: Pytest fixture for monkeypatching
74+
"""
75+
monkeypatch.setenv("VIRTUAL_ENV", "/")
76+
monkeypatch.delenv("ANSIBLE_HOME", raising=False)
77+
with (
78+
pytest.warns(
79+
UserWarning,
80+
match=r"Using unique temporary directory .* for caching.",
81+
),
82+
pytest.warns(
83+
UserWarning,
84+
match=r"Found VIRTUAL_ENV=/ but we cannot use it for caching as it is not writable.",
85+
),
86+
pytest.warns(
87+
UserWarning,
88+
match=r"Project directory .* cannot be used for caching as it is not writable.",
89+
),
90+
):
91+
cache_dir = get_cache_dir(Path("/etc"), isolated=True)
5492
assert cache_dir.as_posix().startswith(tempfile.gettempdir())

0 commit comments

Comments
 (0)