Skip to content

Commit 4ae650c

Browse files
committed
reloader: chmod target dir idempotently
In d57b7bc we introduced a solution to stale watchers, but this introduced a new observer restart loop caused by the chmod always being executed. By using an idempotent chmod, changes are only made when necessary.
1 parent 35e1827 commit 4ae650c

5 files changed

Lines changed: 92 additions & 13 deletions

File tree

nginx_config_reloader/__init__.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
UNPRIVILEGED_UID,
3838
WATCH_IGNORE_FILES,
3939
)
40-
from nginx_config_reloader.utils import directory_is_unmounted
40+
from nginx_config_reloader.utils import apply_chmod, directory_is_unmounted
4141

4242
logger = logging.getLogger(__name__)
4343
dbus_loop: EventLoop | None = None
@@ -269,19 +269,13 @@ def _apply(self):
269269

270270
def fix_custom_config_dir_permissions(self):
271271
try:
272-
subprocess.check_output(
273-
["chmod", "755", self.dir_to_watch],
274-
preexec_fn=as_unprivileged_user,
275-
)
272+
apply_chmod(self.dir_to_watch, "755", preexec_fn=as_unprivileged_user)
276273
for root, dirs, _ in os.walk(self.dir_to_watch):
277274
for name in dirs:
278275
path = os.path.join(root, name)
279276
if os.path.islink(path):
280277
continue
281-
subprocess.check_output(
282-
["chmod", "755", path],
283-
preexec_fn=as_unprivileged_user,
284-
)
278+
apply_chmod(path, "755", preexec_fn=as_unprivileged_user)
285279
except subprocess.CalledProcessError:
286280
self.logger.info("Failed fixing permissions on watched directory")
287281

nginx_config_reloader/utils.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,28 @@
11
import json
2+
import os
23
import subprocess
34

45

6+
def apply_chmod(path, mode, preexec_fn=None):
7+
if isinstance(mode, int):
8+
chmod_mode = oct(mode)[2:]
9+
stat_mode = mode
10+
else:
11+
chmod_mode = str(mode)
12+
stat_mode = int(chmod_mode, 8)
13+
14+
try:
15+
if os.stat(path).st_mode & 0o777 == stat_mode:
16+
return
17+
except OSError:
18+
pass
19+
20+
subprocess.check_call(
21+
["chmod", chmod_mode, path],
22+
preexec_fn=preexec_fn,
23+
)
24+
25+
526
def directory_is_unmounted(path):
627
output = subprocess.check_output(
728
["systemctl", "list-units", "-t", "mount", "--all", "-o", "json"],

tests/test_apply_chmod.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import os
2+
import tempfile
3+
4+
from nginx_config_reloader import as_unprivileged_user
5+
from nginx_config_reloader.utils import apply_chmod
6+
from tests.testcase import TestCase
7+
8+
9+
class TestApplyChmod(TestCase):
10+
def setUp(self):
11+
self.check_call = self.set_up_patch("subprocess.check_call")
12+
_, self.path = tempfile.mkstemp()
13+
14+
def tearDown(self):
15+
try:
16+
os.unlink(self.path)
17+
except OSError:
18+
pass
19+
20+
def test_apply_chmod_changes_mode(self):
21+
os.chmod(self.path, 0o600)
22+
23+
apply_chmod(self.path, "644", preexec_fn=as_unprivileged_user)
24+
25+
self.check_call.assert_called_once_with(
26+
["chmod", "644", self.path], preexec_fn=as_unprivileged_user
27+
)
28+
29+
def test_apply_chmod_skips_existing_mode(self):
30+
os.chmod(self.path, 0o644)
31+
32+
apply_chmod(self.path, "644", preexec_fn=as_unprivileged_user)
33+
34+
self.check_call.assert_not_called()
35+
36+
def test_apply_chmod_accepts_int_mode(self):
37+
os.chmod(self.path, 0o600)
38+
39+
apply_chmod(self.path, 0o644, preexec_fn=as_unprivileged_user)
40+
41+
self.check_call.assert_called_once_with(
42+
["chmod", "644", self.path], preexec_fn=as_unprivileged_user
43+
)

tests/test_fix_custom_config_dir_permissions.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
class TestFixCustomConfigDirPermissions(TestCase):
1010
def setUp(self):
11-
self.check_output = self.set_up_patch("subprocess.check_output")
11+
self.check_call = self.set_up_patch("subprocess.check_call")
1212
self.temp_dir = tempfile.mkdtemp()
1313
self.tm = NginxConfigReloader(
1414
no_magento_config=False,
@@ -17,12 +17,14 @@ def setUp(self):
1717
magento2_flag=None,
1818
)
1919

20-
def test_fix_custom_config_dir_permissions_chmods_all_dirs_to_755(self):
20+
def test_fix_custom_config_dir_permissions_chmods_dirs_to_755(self):
21+
os.chmod(self.temp_dir, 0o700)
2122
os.mkdir(self.temp_dir + "/some_dir")
23+
os.chmod(self.temp_dir + "/some_dir", 0o700)
2224

2325
self.tm.fix_custom_config_dir_permissions()
2426

25-
self.check_output.assert_has_calls(
27+
self.check_call.assert_has_calls(
2628
[
2729
call(["chmod", "755", self.temp_dir], preexec_fn=as_unprivileged_user),
2830
call(
@@ -32,12 +34,22 @@ def test_fix_custom_config_dir_permissions_chmods_all_dirs_to_755(self):
3234
]
3335
)
3436

37+
def test_fix_custom_config_dir_permissions_skips_dirs_that_are_already_755(self):
38+
os.chmod(self.temp_dir, 0o755)
39+
os.mkdir(self.temp_dir + "/some_dir")
40+
os.chmod(self.temp_dir + "/some_dir", 0o755)
41+
42+
self.tm.fix_custom_config_dir_permissions()
43+
44+
self.check_call.assert_not_called()
45+
3546
def test_fix_custom_config_dir_permissions_ignores_symlinks(self):
3647
other_temp_dir = tempfile.mkdtemp()
3748
os.symlink(other_temp_dir, self.temp_dir + "/some_pointing_dir")
49+
os.chmod(self.temp_dir, 0o700)
3850

3951
self.tm.fix_custom_config_dir_permissions()
4052

41-
self.check_output.assert_called_once_with(
53+
self.check_call.assert_called_once_with(
4254
["chmod", "755", self.temp_dir], preexec_fn=as_unprivileged_user
4355
)

tests/test_symlink_targets.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,15 @@ def test_changed_is_true_when_target_inode_is_replaced(self):
100100

101101
self.assertTrue(handler.symlink_targets_changed())
102102

103+
def test_changed_is_true_when_target_metadata_changes(self):
104+
self._symlink("example.com", self.target_a)
105+
handler = self._handler()
106+
handler.watched_symlink_targets = handler.get_symlink_targets()
107+
108+
os.chmod(self.target_a, 0o700)
109+
110+
self.assertTrue(handler.symlink_targets_changed())
111+
103112
def test_changed_is_true_when_a_new_symlink_appears(self):
104113
handler = self._handler()
105114
handler.watched_symlink_targets = handler.get_symlink_targets()

0 commit comments

Comments
 (0)