import logging import yaml from jsonschema import validate, ValidationError from util.config.schema import CONFIG_SCHEMA from util.license import LICENSE_FILENAME, LicenseDecodeError, decode_license logger = logging.getLogger(__name__) class CannotWriteConfigException(Exception): """ Exception raised when the config cannot be written. """ pass class SetupIncompleteException(Exception): """ Exception raised when attempting to verify config that has not yet been setup. """ pass def import_yaml(config_obj, config_file): with open(config_file) as f: c = yaml.safe_load(f) if not c: logger.debug('Empty YAML config file') return if isinstance(c, str): raise Exception('Invalid YAML config file: ' + str(c)) for key in c.iterkeys(): if key.isupper(): config_obj[key] = c[key] if config_obj.get('SETUP_COMPLETE', True): try: validate(config_obj, CONFIG_SCHEMA) except ValidationError: # TODO: Change this into a real error logger.exception('Could not validate config schema') else: logger.debug('Skipping config schema validation because setup is not complete') return config_obj def get_yaml(config_obj): return yaml.safe_dump(config_obj, encoding='utf-8', allow_unicode=True) def export_yaml(config_obj, config_file): try: with open(config_file, 'w') as f: f.write(get_yaml(config_obj)) except IOError as ioe: raise CannotWriteConfigException(str(ioe)) class BaseProvider(object): """ A configuration provider helps to load, save, and handle config override in the application. """ def __init__(self): self.license = None @property def provider_id(self): raise NotImplementedError def update_app_config(self, app_config): """ Updates the given application config object with the loaded override config. """ raise NotImplementedError def get_config(self): """ Returns the contents of the config override file, or None if none. """ raise NotImplementedError def save_config(self, config_object): """ Updates the contents of the config override file to those given. """ raise NotImplementedError def config_exists(self): """ Returns true if a config override file exists in the config volume. """ raise NotImplementedError def volume_exists(self): """ Returns whether the config override volume exists. """ raise NotImplementedError def volume_file_exists(self, filename): """ Returns whether the file with the given name exists under the config override volume. """ raise NotImplementedError def get_volume_file(self, filename, mode='r'): """ Returns a Python file referring to the given name under the config override volume. """ raise NotImplementedError def write_volume_file(self, filename, contents): """ Writes the given contents to the config override volumne, with the given filename. """ raise NotImplementedError def remove_volume_file(self, filename): """ Removes the config override volume file with the given filename. """ raise NotImplementedError def list_volume_directory(self, path): """ Returns a list of strings representing the names of the files found in the config override directory under the given path. If the path doesn't exist, returns None. """ raise NotImplementedError def save_volume_file(self, filename, flask_file): """ Saves the given flask file to the config override volume, with the given filename. """ raise NotImplementedError def requires_restart(self, app_config): """ If true, the configuration loaded into memory for the app does not match that on disk, indicating that this container requires a restart. """ raise NotImplementedError def get_volume_path(self, directory, filename): """ Helper for constructing file paths, which may differ between providers. For example, kubernetes can't have subfolders in configmaps """ raise NotImplementedError def _get_license_file(self): """ Returns the contents of the license file. """ if not self.has_license_file(): msg = 'Could not find license file. Please make sure it is in your config volume.' raise LicenseDecodeError(msg) try: return self.get_volume_file(LICENSE_FILENAME) except IOError: msg = 'Could not open license file. Please make sure it is in your config volume.' raise LicenseDecodeError(msg) def get_license(self): """ Returns the decoded license, if any. """ with self._get_license_file() as f: license_file_contents = f.read() return decode_license(license_file_contents) def save_license(self, license_file_contents): """ Saves the given contents as the license file. """ self.write_volume_file(LICENSE_FILENAME, license_file_contents) def has_license_file(self): """ Returns true if a license file was found in the config directory. """ return self.volume_file_exists(LICENSE_FILENAME)