33import argparse
44import fnmatch
55import logging
6- import logging .handlers
76import os
7+ import re
88import shutil
99import signal
1010import subprocess
3232 MAIN_CONFIG_DIR ,
3333 NGINX ,
3434 NGINX_PID_FILE ,
35+ SYNC_IGNORE_FILES ,
3536 UNPRIVILEGED_GID ,
3637 UNPRIVILEGED_UID ,
3738 WATCH_IGNORE_FILES ,
@@ -46,11 +47,12 @@ class NginxConfigReloader(FileSystemEventHandler):
4647 def __init__ (
4748 self ,
4849 logger = None ,
49- no_magento_config = False ,
50- no_custom_config = False ,
51- dir_to_watch = DIR_TO_WATCH ,
52- magento2_flag = None ,
53- use_systemd = False ,
50+ no_magento_config : bool = False ,
51+ no_custom_config : bool = False ,
52+ dir_to_watch : str = DIR_TO_WATCH ,
53+ magento2_flag : str | None = None ,
54+ use_systemd : bool = False ,
55+ error_file : str = ERROR_FILE ,
5456 ):
5557 """Constructor called by ProcessEvent
5658
@@ -59,6 +61,7 @@ def __init__(
5961 :param bool no_custom_config: True if we should not copy custom configuration
6062 :param str dir_to_watch: The directory to watch
6163 :param str magento2_flag: Magento 2 flag location
64+ :param str error_file: File name for error output file
6265 """
6366 if not logger :
6467 self .logger = logging
@@ -76,6 +79,8 @@ def __init__(
7679 self .dirty = False
7780 self .applying = False
7881 self ._on_config_reload = Signal ()
82+ # @TODO(timon): validate the input here
83+ self .error_file = error_file
7984
8085 def on_deleted (self , event ):
8186 """Triggered by inotify on removal of file or removal of dir
@@ -113,7 +118,8 @@ def handle_event(self, event):
113118 return
114119
115120 basename = os .path .basename (event .src_path )
116- if not any (fnmatch .fnmatch (basename , pat ) for pat in WATCH_IGNORE_FILES ):
121+ ignore_files = list (WATCH_IGNORE_FILES ) + [self .error_file ]
122+ if not any (fnmatch .fnmatch (basename , pat ) for pat in ignore_files ):
117123 self .logger .debug (
118124 f"{ event .event_type .upper ()} detected on { event .src_path } "
119125 )
@@ -153,37 +159,38 @@ def check_no_forbidden_config_directives_are_present(self):
153159 True if forbidden config directives are present
154160 False if check couldn't find any forbidden config flags
155161 """
156- if os .path .isdir (self .dir_to_watch ):
157- for rules in FORBIDDEN_CONFIG_REGEX :
158- try :
159- # error file may contain messages that match a forbidden config pattern
160- # then validation could fail while the actual config is correct.
161- # we'll exclude the error file from searching for patterns,
162- # NOTE: exclusion of error_file requires to ensure the
163- # file is removed before moving it to nginx conf dir
164- # @TODO: use Python to search for forbidden configs instead
165- # of spawning external procs. Will have better testing
166- # and even may consume less system resources
167- check_external_resources = (
168- "[ $(grep -r --exclude={} -P '{}' '{}' | wc -l) -lt 1 ]" .format (
169- ERROR_FILE , rules [0 ], self .dir_to_watch
170- )
171- )
172- subprocess .check_output (check_external_resources , shell = True )
173- except subprocess .CalledProcessError :
174- error = f"Unable to load config: { rules [1 ]} "
175- self .logger .error (error )
176- self .write_error_file (error )
177- return True
162+ if not os .path .isdir (self .dir_to_watch ):
178163 return False
179164
165+ for pattern , message in FORBIDDEN_CONFIG_REGEX :
166+ try :
167+ # error file may contain messages that match a forbidden config pattern
168+ # then validation could fail while the actual config is correct.
169+ # we'll exclude the error file from searching for patterns,
170+ # NOTE: exclusion of error_file requires to ensure the
171+ # file is removed before moving it to nginx conf dir
172+ # @TODO: use Python to search for forbidden configs instead
173+ # of spawning external procs. Will have better testing
174+ # and even may consume less system resources
175+ check_external_resources = "[ $(grep -r --exclude={} --exclude={} -P '{}' '{}' | wc -l) -lt 1 ]" .format (
176+ ERROR_FILE , self .error_file , pattern , self .dir_to_watch
177+ )
178+ subprocess .check_output (check_external_resources , shell = True )
179+ except subprocess .CalledProcessError :
180+ error = f"Unable to load config: { message } "
181+ self .logger .error (error )
182+ self .write_error_file (error )
183+ return True
184+
185+ return False
186+
180187 def remove_error_file (self ):
181188 """Try removing the error file. Return True on success or False on errors
182189 :rtype: bool
183190 """
184191 removed = False
185192 try :
186- os .unlink (os .path .join (self .dir_to_watch , ERROR_FILE ))
193+ os .unlink (os .path .join (self .dir_to_watch , self . error_file ))
187194 removed = True
188195 except OSError :
189196 pass
@@ -282,7 +289,8 @@ def install_new_custom_config_dir(self):
282289 if os .path .exists (CUSTOM_CONFIG_DIR ):
283290 shutil .move (CUSTOM_CONFIG_DIR , BACKUP_CONFIG_DIR )
284291 os .mkdir (CUSTOM_CONFIG_DIR )
285- safe_copy_files (self .dir_to_watch , CUSTOM_CONFIG_DIR )
292+ ignore_files = list (SYNC_IGNORE_FILES ) + [self .error_file ]
293+ safe_copy_files (self .dir_to_watch , CUSTOM_CONFIG_DIR , ignore_files )
286294
287295 def restore_old_custom_config_dir (self ):
288296 shutil .rmtree (CUSTOM_CONFIG_DIR )
@@ -308,7 +316,7 @@ def get_nginx_pid(self):
308316 return None
309317
310318 def write_error_file (self , error ):
311- with open (os .path .join (self .dir_to_watch , ERROR_FILE ), "w" ) as f :
319+ with open (os .path .join (self .dir_to_watch , self . error_file ), "w" ) as f :
312320 f .write (error )
313321
314322 @property
@@ -366,6 +374,7 @@ def wait_loop(
366374 recursive_watch = False ,
367375 use_systemd = False ,
368376 no_dbus = False ,
377+ error_file : str = ERROR_FILE ,
369378):
370379 """Main event loop
371380
@@ -382,6 +391,7 @@ def wait_loop(
382391 :param bool recursive_watch: True if we should watch the dir recursively
383392 :param use_systemd: True if we should reload nginx using systemd instead of process signal
384393 :param bool no_dbus: True if we should not use DBus
394+ :param str error_file: Error file to write error output to
385395 :return None:
386396 """
387397 dir_to_watch = os .path .abspath (dir_to_watch )
@@ -392,6 +402,7 @@ def wait_loop(
392402 no_custom_config = no_custom_config ,
393403 dir_to_watch = dir_to_watch ,
394404 use_systemd = use_systemd ,
405+ error_file = error_file ,
395406 )
396407
397408 if not no_dbus :
@@ -474,6 +485,11 @@ def parse_nginx_config_reloader_arguments():
474485 help = "Disable DBus interface" ,
475486 default = False ,
476487 )
488+ parser .add_argument (
489+ "--error-file" ,
490+ help = "File name for error output" ,
491+ default = ERROR_FILE ,
492+ )
477493 return parser .parse_args ()
478494
479495
@@ -491,6 +507,11 @@ def main():
491507 args = parse_nginx_config_reloader_arguments ()
492508 log = get_logger ()
493509
510+ error_file_pattern = re .compile (r"[a-zA-Z0-9_]+" )
511+ if not error_file_pattern .fullmatch (args .error_file ):
512+ log .error (f"Invalid error file name provided: { args .error_file } " )
513+ return 1
514+
494515 if args .monitor :
495516 # Track changed files in the nginx config dir and reload on change
496517 wait_loop (
@@ -501,6 +522,7 @@ def main():
501522 recursive_watch = args .recursivewatch ,
502523 use_systemd = args .use_systemd ,
503524 no_dbus = args .no_dbus ,
525+ error_file = args .error_file ,
504526 )
505527 # should never return
506528 return 1
@@ -512,6 +534,7 @@ def main():
512534 no_custom_config = args .nocustomconfig ,
513535 dir_to_watch = args .watchdir ,
514536 use_systemd = args .use_systemd ,
537+ error_file = args .error_file ,
515538 ).apply_new_config ()
516539 return 0
517540
0 commit comments