From 7b62c10efc368e0bbeaf24acebeea516b6fe8e56 Mon Sep 17 00:00:00 2001 From: Scott Collins Date: Tue, 13 Apr 2021 12:00:22 -0700 Subject: [PATCH] Modified pds_doi_service.core.util.config_parser to better support a containerized context The parser instance created by DOIConfigUtil.get_config() is now cached (memorized) to prevent redundant processing of the INI. The config parser instance itself is now a specialized version of ConfigParser designed to prioritize expected environment variables over the INI values. This allows configuration options to be easily injected into the service when launching via Docker. --- pds_doi_service/core/util/config_parser.py | 57 ++++++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/pds_doi_service/core/util/config_parser.py b/pds_doi_service/core/util/config_parser.py index a87369ca..fd95eef9 100644 --- a/pds_doi_service/core/util/config_parser.py +++ b/pds_doi_service/core/util/config_parser.py @@ -19,10 +19,43 @@ import os import sys +import functools from os.path import abspath, dirname, join from pkg_resources import resource_filename -logging.basicConfig(level=logging.ERROR) + +class DOIConfigParser(configparser.ConfigParser): + """ + Specialized version of ConfigParser which prioritizes environment variables + when searching for the requested configuration section/option. + + """ + + def get(self, section, option, *, raw=False, vars=None, fallback=object()): + """ + Overloaded version of ConfigParser.get() which searches the + current environment for potential configuration values before checking + values from the parsed INI. This allows manipulation of the DOI service + configuration from external contexts such as Docker. + + The key used to search the local environment is determined from the + section and option names passed to this function. The values are + concatenated with an underscore and converted to upper-case, for + example:: + + DOIConfigParser.get('OSTI', 'user') -> os.environ['OSTI_USER'] + DOIConfigParser.get('OTHER', 'db_file') -> os.environ['OTHER_DB_FILE'] + + If the key is not present in os.environ, then the result from the + default ConfigParser.get() is returned. + + """ + env_var_key = '_'.join([section, option]).upper() + + if env_var_key in os.environ: + return os.environ[env_var_key] + + return super().get(section, option, raw=raw, vars=vars, fallback=fallback) class DOIConfigUtil: @@ -37,8 +70,12 @@ def _resolve_relative_path(parser): return parser - def get_config(self): - parser = configparser.ConfigParser() + @staticmethod + @functools.lru_cache() + def get_config(): + logger = logging.getLogger(__name__) + + parser = DOIConfigParser() # default configuration conf_default = 'conf.ini.default' @@ -55,10 +92,20 @@ def get_config(self): candidates_full_path = [conf_default_path, conf_user_prod_path, conf_user_dev_path] - logging.info("Searching for configuration files in %s", candidates_full_path) + logger.info("Searching for configuration files in %s", candidates_full_path) found = parser.read(candidates_full_path) - logging.info("Using the following configuration file: %s", found) + if not found: + raise RuntimeError( + 'Could not find an INI configuration file to ' + f'parse from the following candidates: {candidates_full_path}' + ) + + # When providing multiple configs they are parsed in successive order, + # and any previously parsed values are overwritten. So the config + # we use should correspond to the last file in the list returned + # from ConfigParser.read() + logger.info("Using the following configuration file: %s", found[-1]) parser = DOIConfigUtil._resolve_relative_path(parser) return parser