Skip to content

Commit eec8174

Browse files
authored
feat: #31 add bind mount ignore patterns and option to disable Overlay2 scan (#44)
1 parent 936efaf commit eec8174

File tree

5 files changed

+102
-4
lines changed

5 files changed

+102
-4
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ Doku can be configured using environment variables. You can set these either dir
9292
| SCAN_INTERVAL | How often to collect basic Docker usage data (in seconds) | 60 |
9393
| SCAN_LOGFILE_INTERVAL | How frequently to check container log sizes (in seconds) | 300 |
9494
| SCAN_BINDMOUNTS_INTERVAL | Time between bind mount scanning operations (in seconds) | 3600 |
95+
| BINDMOUNT_IGNORE_PATTERNS | Paths matching these patterns will be excluded from bind mount scanning (semicolon-separated) (e.g., `/home/*;/tmp/*;*/.git/*`) | "" |
9596
| SCAN_OVERLAY2_INTERVAL | How often to analyze Overlay2 storage (in seconds) | 86400 |
97+
| DISABLE_OVERLAY2_SCAN | Disable Overlay2 storage scanning | false |
9698
| SCAN_INTENSITY | Performance impact level: "aggressive" (highest CPU usage), "normal" (balanced), or "light" (lowest impact) | normal |
9799
| SCAN_USE_DU | Use the faster system `du` command for disk calculations instead of slower built-in methods | true |
98100
| UVICORN_WORKERS | Number of web server worker processes | 1 |

app/scan/du.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@ def main():
2525
schedule.every(settings.SCAN_BINDMOUNTS_INTERVAL).seconds.do(scanner.scan)
2626

2727
### Docker Overlay2 Scanner ###
28-
scanner = Overlay2Scanner(is_stop=signal_.is_stop)
29-
scanner.scan() # run once immediately
30-
schedule.every(settings.SCAN_OVERLAY2_INTERVAL).seconds.do(scanner.scan)
28+
if settings.DISABLE_OVERLAY2_SCAN:
29+
logger.warning('Overlay2 scanner disabled.')
30+
else:
31+
scanner = Overlay2Scanner(is_stop=signal_.is_stop)
32+
scanner.scan() # run once immediately
33+
schedule.every(settings.SCAN_OVERLAY2_INTERVAL).seconds.do(scanner.scan)
3134

3235
# main loop
3336
while not signal_.is_stop():

app/scan/scanner.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import time
2+
import fnmatch
23
from collections.abc import Callable
34
from pathlib import Path
45

@@ -229,6 +230,13 @@ def database_name(self):
229230
def table_name(self):
230231
return settings.TABLE_BINDMOUNTS
231232

233+
def should_ignore_path(self, path: str) -> bool:
234+
"""Check if the path matches any ignore pattern."""
235+
for pattern in settings.BINDMOUNT_IGNORE_PATTERNS:
236+
if fnmatch.fnmatch(path, pattern):
237+
return True
238+
return False
239+
232240
def scan(self):
233241
if not self.doku_mounts:
234242
return
@@ -281,6 +289,11 @@ def scan(self):
281289
if mnt.dst == '/var/run/docker.sock' or mnt.dst.startswith('/run/secrets/'):
282290
continue
283291

292+
# Skip paths matching ignore patterns
293+
if self.should_ignore_path(mnt.src):
294+
self.logger.debug(f'Skipping bind mount {mnt.src} as it matches an ignore pattern')
295+
continue
296+
284297
if mnt.src in already_scanned:
285298
# skip already scanned bind mounts, but update the list of containers
286299
obj = already_scanned[mnt.src]

app/settings.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,22 @@ class Settings(BaseSettings):
6464
default=60 * 60,
6565
description='Time between bind mount scanning operations (in seconds)',
6666
)
67+
bindmount_ignore_patterns: str = Field(
68+
alias='BINDMOUNT_IGNORE_PATTERNS',
69+
default='',
70+
examples=['/home/*;/tmp/*;*/.git/*'],
71+
description='Paths matching these patterns will be excluded from bind mount scanning (semicolon-separated)',
72+
)
6773
scan_overlay2_interval: PositiveInt = Field(
6874
alias='SCAN_OVERLAY2_INTERVAL',
6975
default=60 * 60 * 24,
7076
description='How often to analyze Overlay2 storage (in seconds)',
7177
)
78+
disable_overlay2_scan: bool = Field(
79+
alias='DISABLE_OVERLAY2_SCAN',
80+
default=False,
81+
description='Disable Overlay2 storage scanning',
82+
)
7283
scan_intensity: ScanIntensity = Field(
7384
alias='SCAN_INTENSITY',
7485
default=ScanIntensity.NORMAL,
@@ -136,6 +147,20 @@ def log_level_num(self) -> int:
136147
}
137148
return level_map[self.log_level]
138149

150+
@cached_property
151+
def bindmount_ignore_patterns_list(self) -> list[str]:
152+
patterns = self.bindmount_ignore_patterns
153+
154+
# Remove surrounding quotes from the entire string if present
155+
if (patterns.startswith('"') and patterns.endswith('"')) or (
156+
patterns.startswith("'") and patterns.endswith("'")
157+
):
158+
patterns = patterns[1:-1]
159+
160+
# Split and filter empty values
161+
result = list(filter(None, map(str.strip, patterns.split(';'))))
162+
return result
163+
139164

140165
try:
141166
_settings = Settings()
@@ -168,7 +193,9 @@ def log_level_num(self) -> int:
168193
SCAN_INTERVAL = _settings.scan_interval
169194
SCAN_LOGFILE_INTERVAL = _settings.scan_logfile_interval
170195
SCAN_BINDMOUNTS_INTERVAL = _settings.scan_bindmounts_interval
196+
BINDMOUNT_IGNORE_PATTERNS = _settings.bindmount_ignore_patterns_list
171197
SCAN_OVERLAY2_INTERVAL = _settings.scan_overlay2_interval
198+
DISABLE_OVERLAY2_SCAN = _settings.disable_overlay2_scan
172199
SCAN_INTENSITY = _settings.scan_intensity
173200
SCAN_SLEEP_DURATION = {
174201
ScanIntensity.AGGRESSIVE: 0, # no sleep, but CPU throttling
@@ -242,7 +269,9 @@ def to_string() -> str:
242269
'scan_interval',
243270
'scan_logfile_interval',
244271
'scan_bindmounts_interval',
272+
'bindmount_ignore_patterns',
245273
'scan_overlay2_interval',
274+
'disable_overlay2_scan',
246275
'scan_intensity',
247276
'scan_use_du',
248277
],

app/test_main.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from unittest.mock import patch
22

3-
from settings import to_string as settings_to_string
3+
from settings import Settings, to_string as settings_to_string
44
from main import main
55

66

@@ -76,6 +76,8 @@ def test_settings_to_string():
7676
'ssl_keyfile_password': 'secret',
7777
'ssl_certfile': '/.ssl/cert.pem',
7878
'scan_interval': 60,
79+
'bindmount_ignore_patterns': '/home/*;/tmp/*;*/.git/*',
80+
'disable_overlay2_scan': False,
7981
'workers': 4,
8082
'docker_host': 'unix:///var/run/docker.sock',
8183
'git_tag': 'v1.2.3',
@@ -105,6 +107,11 @@ def test_settings_to_string():
105107
assert 'log_level: info' in result
106108
assert 'root_path: /doku' in result
107109

110+
# Scan settings
111+
assert 'scan_interval: 60' in result
112+
assert 'bindmount_ignore_patterns: /home/*;/tmp/*;*/.git/*' in result
113+
assert 'disable_overlay2_scan: False' in result
114+
108115
# Verify password is masked
109116
assert 'ssl_keyfile_password: ********' in result
110117
assert 'ssl_keyfile_password: secret' not in result
@@ -119,3 +126,47 @@ def test_settings_to_string():
119126
# Verify version info
120127
assert 'git_tag: v1.2.3' in result
121128
assert 'git_sha: abcdef1234567890' in result
129+
130+
131+
def patterns_assert(patterns: list[str]):
132+
assert len(patterns) == 3
133+
assert '/home/*' in patterns
134+
assert '/tmp/*' in patterns
135+
assert '*/.git/*' in patterns
136+
137+
138+
def test_bindmount_ignore_patterns_list():
139+
with patch('os.environ', {'BINDMOUNT_IGNORE_PATTERNS': '/home/*;/tmp/*;*/.git/*'}):
140+
s = Settings()
141+
patterns_assert(s.bindmount_ignore_patterns_list)
142+
143+
# Test with entire string in double quotes
144+
with patch('os.environ', {'BINDMOUNT_IGNORE_PATTERNS': '"/home/*;/tmp/*;*/.git/*"'}):
145+
s = Settings()
146+
patterns_assert(s.bindmount_ignore_patterns_list)
147+
148+
# Test with entire string in single quotes
149+
with patch('os.environ', {'BINDMOUNT_IGNORE_PATTERNS': "';/home/*;/tmp/*;*/.git/*;'"}):
150+
s = Settings()
151+
patterns_assert(s.bindmount_ignore_patterns_list)
152+
153+
# Test with a single pattern
154+
with patch('os.environ', {'BINDMOUNT_IGNORE_PATTERNS': '/var/log/*;'}):
155+
s = Settings()
156+
patterns = s.bindmount_ignore_patterns_list
157+
assert len(patterns) == 1
158+
assert '/var/log/*' in patterns
159+
160+
# Test with empty string
161+
with patch('os.environ', {'BINDMOUNT_IGNORE_PATTERNS': ';;'}):
162+
s = Settings()
163+
patterns = s.bindmount_ignore_patterns_list
164+
assert len(patterns) == 0
165+
166+
# Test with whitespace
167+
with patch('os.environ', {'BINDMOUNT_IGNORE_PATTERNS': ' /path1/*; /path2/* '}):
168+
s = Settings()
169+
patterns = s.bindmount_ignore_patterns_list
170+
assert len(patterns) == 2
171+
assert '/path1/*' in patterns
172+
assert '/path2/*' in patterns

0 commit comments

Comments
 (0)