Skip to content

Commit 9ce5dc5

Browse files
SergioAAISergio AnguloIbarra
andauthored
Fix ownership for mounted volume for dag processing logs (#431)
*Description of changes:* Change in entrypoint.py Adding the mount point to the Airflow container caused an error when deploying, which is needed to expose the log files to the FluentBit container. This changes resolves this, changing the ownership of the directory to the 'airflow' user , when the director is mounted. Added unit tests to match this change. Edit 03/18: Moved the fix to execute earlier in entrypoint.py. This is due to AF 2.x interacting with the mounted folder earlier than in 3.x , causing the permission error earlier in the deployment Also added this change to AF 2.X since NCL may be enabled , which means this fix is required --------- Co-authored-by: Sergio AnguloIbarra <aglser@amazon.com>
1 parent b4936f4 commit 9ce5dc5

File tree

12 files changed

+342
-13
lines changed

12 files changed

+342
-13
lines changed

images/airflow/2.10.1/python/mwaa/entrypoint.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,36 @@
66
after setting up the necessary configurations.
77
"""
88

9+
# Fix shared log volume permissions before any Airflow imports, since Airflow's
10+
# logging config tries to create subdirectories under /usr/local/airflow/logs
11+
# at import time.
12+
# ruff: noqa: E402
13+
import os
14+
import subprocess
15+
16+
def _fix_shared_log_volume_permissions():
17+
"""
18+
When /usr/local/airflow/logs is backed by a shared volume (e.g. for Fluent Bit
19+
to tail log files), Docker creates it owned by root. Fix ownership so the
20+
airflow user can write logs.
21+
"""
22+
log_dir = "/usr/local/airflow/logs"
23+
if os.path.ismount(log_dir):
24+
try:
25+
subprocess.run(
26+
["sudo", "chown", "-R", "airflow:", log_dir],
27+
check=True,
28+
)
29+
print(f"Fixed ownership of shared log volume {log_dir}")
30+
except Exception as e:
31+
print(f"WARNING: Could not fix ownership of {log_dir}: {e}")
32+
33+
_fix_shared_log_volume_permissions()
34+
935
# Setup logging first thing to make sure all logs happen under the right setup. The
1036
# reason for needing this is that typically a `logger` object is defined at the top
1137
# of the module and is used through out it. So, if we import a module before logging
1238
# is setup, its `logger` object will not have the right setup.
13-
# ruff: noqa: E402
1439
# fmt: off
1540
from airflow.config_templates.airflow_local_settings import DEFAULT_LOGGING_CONFIG
1641
import logging.config
@@ -20,7 +45,6 @@
2045
# Python imports
2146
import asyncio
2247
import logging
23-
import os
2448
import sys
2549
import time
2650

images/airflow/2.10.3/python/mwaa/entrypoint.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,36 @@
66
after setting up the necessary configurations.
77
"""
88

9+
# Fix shared log volume permissions before any Airflow imports, since Airflow's
10+
# logging config tries to create subdirectories under /usr/local/airflow/logs
11+
# at import time.
12+
# ruff: noqa: E402
13+
import os
14+
import subprocess
15+
16+
def _fix_shared_log_volume_permissions():
17+
"""
18+
When /usr/local/airflow/logs is backed by a shared volume (e.g. for Fluent Bit
19+
to tail log files), Docker creates it owned by root. Fix ownership so the
20+
airflow user can write logs.
21+
"""
22+
log_dir = "/usr/local/airflow/logs"
23+
if os.path.ismount(log_dir):
24+
try:
25+
subprocess.run(
26+
["sudo", "chown", "-R", "airflow:", log_dir],
27+
check=True,
28+
)
29+
print(f"Fixed ownership of shared log volume {log_dir}")
30+
except Exception as e:
31+
print(f"WARNING: Could not fix ownership of {log_dir}: {e}")
32+
33+
_fix_shared_log_volume_permissions()
34+
935
# Setup logging first thing to make sure all logs happen under the right setup. The
1036
# reason for needing this is that typically a `logger` object is defined at the top
1137
# of the module and is used through out it. So, if we import a module before logging
1238
# is setup, its `logger` object will not have the right setup.
13-
# ruff: noqa: E402
1439
# fmt: off
1540
from airflow.config_templates.airflow_local_settings import DEFAULT_LOGGING_CONFIG
1641
import logging.config
@@ -20,7 +45,6 @@
2045
# Python imports
2146
import asyncio
2247
import logging
23-
import os
2448
import sys
2549
import time
2650

images/airflow/2.11.0/python/mwaa/entrypoint.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,36 @@
66
after setting up the necessary configurations.
77
"""
88

9+
# Fix shared log volume permissions before any Airflow imports, since Airflow's
10+
# logging config tries to create subdirectories under /usr/local/airflow/logs
11+
# at import time.
12+
# ruff: noqa: E402
13+
import os
14+
import subprocess
15+
16+
def _fix_shared_log_volume_permissions():
17+
"""
18+
When /usr/local/airflow/logs is backed by a shared volume (e.g. for Fluent Bit
19+
to tail log files), Docker creates it owned by root. Fix ownership so the
20+
airflow user can write logs.
21+
"""
22+
log_dir = "/usr/local/airflow/logs"
23+
if os.path.ismount(log_dir):
24+
try:
25+
subprocess.run(
26+
["sudo", "chown", "-R", "airflow:", log_dir],
27+
check=True,
28+
)
29+
print(f"Fixed ownership of shared log volume {log_dir}")
30+
except Exception as e:
31+
print(f"WARNING: Could not fix ownership of {log_dir}: {e}")
32+
33+
_fix_shared_log_volume_permissions()
34+
935
# Setup logging first thing to make sure all logs happen under the right setup. The
1036
# reason for needing this is that typically a `logger` object is defined at the top
1137
# of the module and is used through out it. So, if we import a module before logging
1238
# is setup, its `logger` object will not have the right setup.
13-
# ruff: noqa: E402
1439
# fmt: off
1540
from airflow.config_templates.airflow_local_settings import DEFAULT_LOGGING_CONFIG
1641
import logging.config
@@ -20,7 +45,6 @@
2045
# Python imports
2146
import asyncio
2247
import logging
23-
import os
2448
import sys
2549
import time
2650

@@ -244,7 +268,6 @@ async def main() -> None:
244268

245269
execute_command(command, environ, CONTAINER_START_TIME)
246270

247-
248271
if __name__ == "__main__":
249272
try:
250273
asyncio.run(main())

images/airflow/2.9.2/python/mwaa/entrypoint.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,36 @@
66
after setting up the necessary configurations.
77
"""
88

9+
# Fix shared log volume permissions before any Airflow imports, since Airflow's
10+
# logging config tries to create subdirectories under /usr/local/airflow/logs
11+
# at import time.
12+
# ruff: noqa: E402
13+
import os
14+
import subprocess
15+
16+
def _fix_shared_log_volume_permissions():
17+
"""
18+
When /usr/local/airflow/logs is backed by a shared volume (e.g. for Fluent Bit
19+
to tail log files), Docker creates it owned by root. Fix ownership so the
20+
airflow user can write logs.
21+
"""
22+
log_dir = "/usr/local/airflow/logs"
23+
if os.path.ismount(log_dir):
24+
try:
25+
subprocess.run(
26+
["sudo", "chown", "-R", "airflow:", log_dir],
27+
check=True,
28+
)
29+
print(f"Fixed ownership of shared log volume {log_dir}")
30+
except Exception as e:
31+
print(f"WARNING: Could not fix ownership of {log_dir}: {e}")
32+
33+
_fix_shared_log_volume_permissions()
34+
935
# Setup logging first thing to make sure all logs happen under the right setup. The
1036
# reason for needing this is that typically a `logger` object is defined at the top
1137
# of the module and is used through out it. So, if we import a module before logging
1238
# is setup, its `logger` object will not have the right setup.
13-
# ruff: noqa: E402
1439
# fmt: off
1540
from airflow.config_templates.airflow_local_settings import DEFAULT_LOGGING_CONFIG
1641
import logging.config
@@ -21,7 +46,6 @@
2146
from datetime import datetime
2247
import asyncio
2348
import logging
24-
import os
2549
import sys
2650
import time
2751

images/airflow/3.0.6/python/mwaa/entrypoint.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,36 @@
66
after setting up the necessary configurations.
77
"""
88

9+
# Fix shared log volume permissions before any Airflow imports, since Airflow's
10+
# logging config tries to create subdirectories under /usr/local/airflow/logs
11+
# at import time.
12+
# ruff: noqa: E402
13+
import os
14+
import subprocess
15+
16+
def _fix_shared_log_volume_permissions():
17+
"""
18+
When /usr/local/airflow/logs is backed by a shared volume (e.g. for Fluent Bit
19+
to tail log files), Docker creates it owned by root. Fix ownership so the
20+
airflow user can write logs.
21+
"""
22+
log_dir = "/usr/local/airflow/logs"
23+
if os.path.ismount(log_dir):
24+
try:
25+
subprocess.run(
26+
["sudo", "chown", "-R", "airflow:", log_dir],
27+
check=True,
28+
)
29+
print(f"Fixed ownership of shared log volume {log_dir}")
30+
except Exception as e:
31+
print(f"WARNING: Could not fix ownership of {log_dir}: {e}")
32+
33+
_fix_shared_log_volume_permissions()
34+
935
# Setup logging first thing to make sure all logs happen under the right setup. The
1036
# reason for needing this is that typically a `logger` object is defined at the top
1137
# of the module and is used through out it. So, if we import a module before logging
1238
# is setup, its `logger` object will not have the right setup.
13-
# ruff: noqa: E402
1439
# fmt: off
1540
from airflow.config_templates.airflow_local_settings import DEFAULT_LOGGING_CONFIG
1641
import logging.config
@@ -20,7 +45,6 @@
2045
# Python imports
2146
import asyncio
2247
import logging
23-
import os
2448
import sys
2549
import time
2650

images/airflow/3.1.6/python/mwaa/entrypoint.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,36 @@
66
after setting up the necessary configurations.
77
"""
88

9+
# Fix shared log volume permissions before any Airflow imports, since Airflow's
10+
# logging config tries to create subdirectories under /usr/local/airflow/logs
11+
# at import time.
12+
# ruff: noqa: E402
13+
import os
14+
import subprocess
15+
16+
def _fix_shared_log_volume_permissions():
17+
"""
18+
When /usr/local/airflow/logs is backed by a shared volume (e.g. for Fluent Bit
19+
to tail log files), Docker creates it owned by root. Fix ownership so the
20+
airflow user can write logs.
21+
"""
22+
log_dir = "/usr/local/airflow/logs"
23+
if os.path.ismount(log_dir):
24+
try:
25+
subprocess.run(
26+
["sudo", "chown", "-R", "airflow:", log_dir],
27+
check=True,
28+
)
29+
print(f"Fixed ownership of shared log volume {log_dir}")
30+
except Exception as e:
31+
print(f"WARNING: Could not fix ownership of {log_dir}: {e}")
32+
33+
_fix_shared_log_volume_permissions()
34+
935
# Setup logging first thing to make sure all logs happen under the right setup. The
1036
# reason for needing this is that typically a `logger` object is defined at the top
1137
# of the module and is used through out it. So, if we import a module before logging
1238
# is setup, its `logger` object will not have the right setup.
13-
# ruff: noqa: E402
1439
# fmt: off
1540
from airflow.config_templates.airflow_local_settings import DEFAULT_LOGGING_CONFIG
1641
import logging.config
@@ -20,7 +45,6 @@
2045
# Python imports
2146
import asyncio
2247
import logging
23-
import os
2448
import sys
2549
import time
2650

tests/images/airflow/2.10.1/python/mwaa/test_entrypoint_2_10_1.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,3 +261,34 @@ def test_mark_as_unhealthy_error(mock_environ):
261261

262262
# Verify sleep was still called
263263
mock_sleep.assert_called_once_with(1100)
264+
265+
266+
def test_fix_shared_log_volume_permissions_on_mount(mock_environ):
267+
"""Test _fix_shared_log_volume_permissions runs chown when path is a mount point."""
268+
with patch('os.path.ismount', return_value=True) as mock_ismount, \
269+
patch('subprocess.run') as mock_run:
270+
entrypoint._fix_shared_log_volume_permissions()
271+
272+
mock_ismount.assert_called_once_with('/usr/local/airflow/logs')
273+
mock_run.assert_called_once_with(
274+
['sudo', 'chown', '-R', 'airflow:', '/usr/local/airflow/logs'],
275+
check=True,
276+
)
277+
278+
279+
def test_fix_shared_log_volume_permissions_not_mount(mock_environ):
280+
"""Test _fix_shared_log_volume_permissions skips chown when path is not a mount."""
281+
with patch('os.path.ismount', return_value=False) as mock_ismount, \
282+
patch('subprocess.run') as mock_run:
283+
entrypoint._fix_shared_log_volume_permissions()
284+
285+
mock_ismount.assert_called_once_with('/usr/local/airflow/logs')
286+
mock_run.assert_not_called()
287+
288+
289+
def test_fix_shared_log_volume_permissions_chown_failure(mock_environ):
290+
"""Test _fix_shared_log_volume_permissions handles chown failure gracefully."""
291+
with patch('os.path.ismount', return_value=True), \
292+
patch('subprocess.run', side_effect=Exception('Permission denied')):
293+
# Should not raise — just prints a warning
294+
entrypoint._fix_shared_log_volume_permissions()

tests/images/airflow/2.10.3/python/mwaa/test_entrypoint_2_10_3.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,3 +261,34 @@ def test_mark_as_unhealthy_error(mock_environ):
261261

262262
# Verify sleep was still called
263263
mock_sleep.assert_called_once_with(1100)
264+
265+
266+
def test_fix_shared_log_volume_permissions_on_mount(mock_environ):
267+
"""Test _fix_shared_log_volume_permissions runs chown when path is a mount point."""
268+
with patch('os.path.ismount', return_value=True) as mock_ismount, \
269+
patch('subprocess.run') as mock_run:
270+
entrypoint._fix_shared_log_volume_permissions()
271+
272+
mock_ismount.assert_called_once_with('/usr/local/airflow/logs')
273+
mock_run.assert_called_once_with(
274+
['sudo', 'chown', '-R', 'airflow:', '/usr/local/airflow/logs'],
275+
check=True,
276+
)
277+
278+
279+
def test_fix_shared_log_volume_permissions_not_mount(mock_environ):
280+
"""Test _fix_shared_log_volume_permissions skips chown when path is not a mount."""
281+
with patch('os.path.ismount', return_value=False) as mock_ismount, \
282+
patch('subprocess.run') as mock_run:
283+
entrypoint._fix_shared_log_volume_permissions()
284+
285+
mock_ismount.assert_called_once_with('/usr/local/airflow/logs')
286+
mock_run.assert_not_called()
287+
288+
289+
def test_fix_shared_log_volume_permissions_chown_failure(mock_environ):
290+
"""Test _fix_shared_log_volume_permissions handles chown failure gracefully."""
291+
with patch('os.path.ismount', return_value=True), \
292+
patch('subprocess.run', side_effect=Exception('Permission denied')):
293+
# Should not raise — just prints a warning
294+
entrypoint._fix_shared_log_volume_permissions()

tests/images/airflow/2.11.0/python/mwaa/test_entrypoint_2_11_0.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,3 +261,34 @@ def test_mark_as_unhealthy_error(mock_environ):
261261

262262
# Verify sleep was still called
263263
mock_sleep.assert_called_once_with(1100)
264+
265+
266+
def test_fix_shared_log_volume_permissions_on_mount(mock_environ):
267+
"""Test _fix_shared_log_volume_permissions runs chown when path is a mount point."""
268+
with patch('os.path.ismount', return_value=True) as mock_ismount, \
269+
patch('subprocess.run') as mock_run:
270+
entrypoint._fix_shared_log_volume_permissions()
271+
272+
mock_ismount.assert_called_once_with('/usr/local/airflow/logs')
273+
mock_run.assert_called_once_with(
274+
['sudo', 'chown', '-R', 'airflow:', '/usr/local/airflow/logs'],
275+
check=True,
276+
)
277+
278+
279+
def test_fix_shared_log_volume_permissions_not_mount(mock_environ):
280+
"""Test _fix_shared_log_volume_permissions skips chown when path is not a mount."""
281+
with patch('os.path.ismount', return_value=False) as mock_ismount, \
282+
patch('subprocess.run') as mock_run:
283+
entrypoint._fix_shared_log_volume_permissions()
284+
285+
mock_ismount.assert_called_once_with('/usr/local/airflow/logs')
286+
mock_run.assert_not_called()
287+
288+
289+
def test_fix_shared_log_volume_permissions_chown_failure(mock_environ):
290+
"""Test _fix_shared_log_volume_permissions handles chown failure gracefully."""
291+
with patch('os.path.ismount', return_value=True), \
292+
patch('subprocess.run', side_effect=Exception('Permission denied')):
293+
# Should not raise — just prints a warning
294+
entrypoint._fix_shared_log_volume_permissions()

0 commit comments

Comments
 (0)