diff --git a/Makefile b/Makefile index 25cb7b8..62ceb49 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ -BUILD_TAG := v1.2.0 +BUILD_TAG := v1.2.1 IMAGE_NAME ?= mattermost/pgbouncer-config-reload all: build-image scan push -build-image: ## Build the docker image for mattermost-cloud - @echo Building Mattermost-cloud Docker Image +build-image: ## Build the docker image + @echo Building Docker Image @if [ -z "$(DOCKER_USERNAME)" ] || [ -z "$(DOCKER_PASSWORD)" ]; then \ echo "DOCKER_USERNAME and/or DOCKER_PASSWORD not set. Skipping Docker login."; \ else \ @@ -10,8 +10,17 @@ build-image: ## Build the docker image for mattermost-cloud fi docker buildx build \ --platform linux/amd64,linux/arm64 \ - . -f build/Dockerfile -t $(IMAGE_NAME):$(BUILD_TAG) \ + . -f Dockerfile -t $(IMAGE_NAME):$(BUILD_TAG) \ --no-cache \ --push + +build-image-locally: ## Build the docker image locally + @echo Building Docker Image Locally + docker buildx build \ + --platform linux/arm64 \ + . -f Dockerfile -t $(IMAGE_NAME):$(BUILD_TAG) \ + --no-cache \ + --load + scan: docker scan --accept-license ${IMAGE_NAME}:${BUILD_TAG} diff --git a/pgbouncer_config_reload/cli.py b/pgbouncer_config_reload/cli.py index eb1e7e5..e7e472c 100644 --- a/pgbouncer_config_reload/cli.py +++ b/pgbouncer_config_reload/cli.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 """ -Tools for monitoring changes configuration and reload pgbouncer +Tools for monitoring changes to configuration and reloading pgbouncer. """ - import configargparse import logging -import pyinotify import psycopg2 import os import sys import signal import time +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler __author__ = "kvelichko" __email__ = "kvelichko@wallarm.com" @@ -21,25 +21,20 @@ log = logging.getLogger('configmap-reload') -class ConfigmapHandler(pyinotify.ProcessEvent): - - def __init__( - self, - host, - port, - user, - password, - database='pgbouncer', - timeout=10 - ): +class ConfigmapHandler(FileSystemEventHandler): + """ + A watchdog event handler to reload pgbouncer when relevant files change. + """ + def __init__(self, host, port, user, password, database='pgbouncer', timeout=10): """ - :param host - pgbouncer hostname - :param port - pgbouncer port - :param user - pgbouncer admin user - :param password - pgbouncer admin password - :param database - pgbouncer admin database - :param timeout - timeout before send reload to pgbouncer + :param host: pgbouncer hostname + :param port: pgbouncer port + :param user: pgbouncer admin user + :param password: pgbouncer admin password + :param database: pgbouncer admin database + :param timeout: wait time (seconds) before sending reload to pgbouncer """ + super().__init__() self.host = host self.port = port self.user = user @@ -47,88 +42,102 @@ def __init__( self.database = database self.timeout = timeout - def pgbouncer_reload(self): + def on_created(self, event): """ - Function for reload pgbouncer + Triggered when a file/directory is created. + If the created file's name starts with "..data", + wait and then reload pgbouncer. """ + if not event.is_directory: + log.info(f"CREATE event: '{event.src_path}'") + if os.path.basename(event.src_path).startswith('..data'): + time.sleep(self.timeout) + self.pgbouncer_reload() + def pgbouncer_reload(self): + """ + Execute pgbouncer RELOAD. + """ + log.debug("Pgbouncer graceful reload starting...") connection = None - log.debug("Pgbouncer gracefull reload starting...") + cursor = None try: connection = psycopg2.connect( - user=self.user, - password=self.password, - host=self.host, - port=self.port, - database=self.database - ) + user=self.user, + password=self.password, + host=self.host, + port=self.port, + database=self.database + ) connection.set_isolation_level(0) cursor = connection.cursor() cursor.execute("RELOAD;") connection.commit() except (Exception, psycopg2.Error) as error: - log.error("Failed to RELOAD pgbouncer: %s" % (error)) + log.error(f"Failed to RELOAD pgbouncer: {error}") finally: - if (connection): + if cursor: cursor.close() + if connection: connection.close() log.debug("Pgbouncer connection is closed") - log.info("Pgbouncer gracefull reloaded.") - - def process_IN_CREATE(self, event): - log.info("CREATE event: '%s'" % (event.pathname)) - if os.path.basename(event.pathname).startswith('..data'): - time.sleep(self.timeout) - self.pgbouncer_reload() + log.info("Pgbouncer gracefully reloaded.") def exit_signal_handler(signum, frame): """ - Function for logging signals and interrupt program + Handle termination signals to ensure clean shutdown. """ - log.info("Signal '%s' received. Shutdown..." - % (signal.Signals(signum).name)) + log.info(f"Signal '{signal.Signals(signum).name}' received. Shutting down...") sys.exit() -def run(args={}): +def run(args): """ - main function - :param args - dict of arguments + Main function that sets up the file observers and starts the watchdog loop. """ - # watch manager - wm = pyinotify.WatchManager() + observer = Observer() + + event_handler = ConfigmapHandler( + host=args.pgbouncer_host, + port=args.pgbouncer_port, + user=args.pgbouncer_user, + password=args.pgbouncer_password, + database=args.pgbouncer_database, + timeout=int(args.pgbouncer_reload_timeout) + ) + for path in args.config_path.split(";"): - wm.add_watch(path, pyinotify.IN_CREATE, rec=True) - - # event handler - eh = ConfigmapHandler( - args.pgbouncer_host, - args.pgbouncer_port, - args.pgbouncer_user, - args.pgbouncer_password, - args.pgbouncer_database, - int(args.pgbouncer_reload_timeout) - ) + if os.path.isdir(path): + log.info(f"Watching path: {path}") + observer.schedule(event_handler, path, recursive=True) + else: + log.warning(f"Path '{path}' is not a directory or does not exist. Skipping...") - # notifier + observer.start() log.info("Entering event loop...") - notifier = pyinotify.Notifier(wm, eh) - notifier.loop() + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + pass + finally: + observer.stop() + observer.join() def main(): p = configargparse.ArgParser( - description='Tools for monitoring pgbouncer configurations' - ' files and gracefull reload it.' - ) + description='Tool for monitoring pgbouncer configuration files and gracefully reloading them.' + ) p.add("-v", "--verbose", help='Verbosity (-v -vv -vvv)', action='count', env_var='VERBOSE', default=0) p.add("-c", "--config-path", - help="Semicolons separated configuration path for watching. (Ex: /etc/pgbouncer;/etc/userlist)", + help="Semicolon-separated paths to watch. (e.g. /etc/pgbouncer;/etc/userlist)", required=True, env_var='CONFIG_PATH') p.add("-H", "--pgbouncer-host", @@ -152,43 +161,45 @@ def main(): default='pgbouncer', env_var='PGBOUNCER_DATABASE') p.add("-t", "--pgbouncer-reload-timeout", - help="Timeout before reload configuration of pgbouncer " - "(default: 10)", + help="Timeout before reloading pgbouncer (default: 10)", default=10, env_var='PGBOUNCER_RELOAD_TIMEOUT') p.add("-j", "--json-log", action='store_true', - help="Print logs as JSON", + help="Enable JSON-formatted logs", default=False, env_var='LOG_JSON') + args = p.parse_args() # Configure log handler = logging.StreamHandler() if args.json_log: - from pythonjsonlogger import jsonlogger - formatter = jsonlogger.JsonFormatter( - fmt="%(asctime)s %(levelname)s %(message)s", - datefmt="%Y-%m-%d %H:%M:%S") + # Adjusted import for python-json-logger change + from pythonjsonlogger.json import JsonFormatter + formatter = JsonFormatter( + fmt="%(asctime)s %(levelname)s %(message)s", + datefmt="%Y-%m-%d %H:%M:%S" + ) handler.setFormatter(formatter) else: formatter = logging.Formatter( - fmt='%(asctime)s\t%(levelname)s: %(message)s', - datefmt="%Y-%m-%d %H:%M:%S") + fmt='%(asctime)s\t%(levelname)s: %(message)s', + datefmt="%Y-%m-%d %H:%M:%S" + ) handler.setFormatter(formatter) - log.addHandler(handler) + log.addHandler(handler) loglvl = logging.ERROR if args.verbose > 0: - loglvl = 40 - 10*args.verbose + loglvl = max(logging.ERROR - 10 * args.verbose, logging.DEBUG) log.setLevel(loglvl) - # Configure interrupt signal handler + # Configure interrupt signal handlers signal.signal(signal.SIGINT, exit_signal_handler) signal.signal(signal.SIGTERM, exit_signal_handler) log.info("Initialization complete...") - run(args) diff --git a/requirements.txt b/requirements.txt index b499b4b..117e403 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -psycopg2 >= 2.9.1 +psycopg2 >= 2.9.10 python-json-logger >= 2.0.2 configargparse >= 1.5.2 -pyinotify >= 0.9.6 +watchdog >= 3.0.0