|
@@ -16,14 +16,18 @@
|
|
|
|
|
|
import argparse
|
|
|
import errno
|
|
|
+import logging
|
|
|
import os
|
|
|
from collections import OrderedDict
|
|
|
from hashlib import sha256
|
|
|
from textwrap import dedent
|
|
|
from typing import (
|
|
|
Any,
|
|
|
+ ClassVar,
|
|
|
+ Collection,
|
|
|
Dict,
|
|
|
Iterable,
|
|
|
+ Iterator,
|
|
|
List,
|
|
|
MutableMapping,
|
|
|
Optional,
|
|
@@ -40,6 +44,8 @@ import yaml
|
|
|
|
|
|
from synapse.util.templates import _create_mxc_to_http_filter, _format_ts_filter
|
|
|
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
+
|
|
|
|
|
|
class ConfigError(Exception):
|
|
|
"""Represents a problem parsing the configuration
|
|
@@ -55,6 +61,38 @@ class ConfigError(Exception):
|
|
|
self.path = path
|
|
|
|
|
|
|
|
|
+def format_config_error(e: ConfigError) -> Iterator[str]:
|
|
|
+ """
|
|
|
+ Formats a config error neatly
|
|
|
+
|
|
|
+ The idea is to format the immediate error, plus the "causes" of those errors,
|
|
|
+ hopefully in a way that makes sense to the user. For example:
|
|
|
+
|
|
|
+ Error in configuration at 'oidc_config.user_mapping_provider.config.display_name_template':
|
|
|
+ Failed to parse config for module 'JinjaOidcMappingProvider':
|
|
|
+ invalid jinja template:
|
|
|
+ unexpected end of template, expected 'end of print statement'.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ e: the error to be formatted
|
|
|
+
|
|
|
+ Returns: An iterator which yields string fragments to be formatted
|
|
|
+ """
|
|
|
+ yield "Error in configuration"
|
|
|
+
|
|
|
+ if e.path:
|
|
|
+ yield " at '%s'" % (".".join(e.path),)
|
|
|
+
|
|
|
+ yield ":\n %s" % (e.msg,)
|
|
|
+
|
|
|
+ parent_e = e.__cause__
|
|
|
+ indent = 1
|
|
|
+ while parent_e:
|
|
|
+ indent += 1
|
|
|
+ yield ":\n%s%s" % (" " * indent, str(parent_e))
|
|
|
+ parent_e = parent_e.__cause__
|
|
|
+
|
|
|
+
|
|
|
# We split these messages out to allow packages to override with package
|
|
|
# specific instructions.
|
|
|
MISSING_REPORT_STATS_CONFIG_INSTRUCTIONS = """\
|
|
@@ -119,7 +157,7 @@ class Config:
|
|
|
defined in subclasses.
|
|
|
"""
|
|
|
|
|
|
- section: str
|
|
|
+ section: ClassVar[str]
|
|
|
|
|
|
def __init__(self, root_config: "RootConfig" = None):
|
|
|
self.root = root_config
|
|
@@ -309,9 +347,12 @@ class RootConfig:
|
|
|
class, lower-cased and with "Config" removed.
|
|
|
"""
|
|
|
|
|
|
- config_classes = []
|
|
|
+ config_classes: List[Type[Config]] = []
|
|
|
+
|
|
|
+ def __init__(self, config_files: Collection[str] = ()):
|
|
|
+ # Capture absolute paths here, so we can reload config after we daemonize.
|
|
|
+ self.config_files = [os.path.abspath(path) for path in config_files]
|
|
|
|
|
|
- def __init__(self):
|
|
|
for config_class in self.config_classes:
|
|
|
if config_class.section is None:
|
|
|
raise ValueError("%r requires a section name" % (config_class,))
|
|
@@ -512,12 +553,10 @@ class RootConfig:
|
|
|
object from parser.parse_args(..)`
|
|
|
"""
|
|
|
|
|
|
- obj = cls()
|
|
|
-
|
|
|
config_args = parser.parse_args(argv)
|
|
|
|
|
|
config_files = find_config_files(search_paths=config_args.config_path)
|
|
|
-
|
|
|
+ obj = cls(config_files)
|
|
|
if not config_files:
|
|
|
parser.error("Must supply a config file.")
|
|
|
|
|
@@ -627,7 +666,7 @@ class RootConfig:
|
|
|
|
|
|
generate_missing_configs = config_args.generate_missing_configs
|
|
|
|
|
|
- obj = cls()
|
|
|
+ obj = cls(config_files)
|
|
|
|
|
|
if config_args.generate_config:
|
|
|
if config_args.report_stats is None:
|
|
@@ -727,6 +766,34 @@ class RootConfig:
|
|
|
) -> None:
|
|
|
self.invoke_all("generate_files", config_dict, config_dir_path)
|
|
|
|
|
|
+ def reload_config_section(self, section_name: str) -> Config:
|
|
|
+ """Reconstruct the given config section, leaving all others unchanged.
|
|
|
+
|
|
|
+ This works in three steps:
|
|
|
+
|
|
|
+ 1. Create a new instance of the relevant `Config` subclass.
|
|
|
+ 2. Call `read_config` on that instance to parse the new config.
|
|
|
+ 3. Replace the existing config instance with the new one.
|
|
|
+
|
|
|
+ :raises ValueError: if the given `section` does not exist.
|
|
|
+ :raises ConfigError: for any other problems reloading config.
|
|
|
+
|
|
|
+ :returns: the previous config object, which no longer has a reference to this
|
|
|
+ RootConfig.
|
|
|
+ """
|
|
|
+ existing_config: Optional[Config] = getattr(self, section_name, None)
|
|
|
+ if existing_config is None:
|
|
|
+ raise ValueError(f"Unknown config section '{section_name}'")
|
|
|
+ logger.info("Reloading config section '%s'", section_name)
|
|
|
+
|
|
|
+ new_config_data = read_config_files(self.config_files)
|
|
|
+ new_config = type(existing_config)(self)
|
|
|
+ new_config.read_config(new_config_data)
|
|
|
+ setattr(self, section_name, new_config)
|
|
|
+
|
|
|
+ existing_config.root = None
|
|
|
+ return existing_config
|
|
|
+
|
|
|
|
|
|
def read_config_files(config_files: Iterable[str]) -> Dict[str, Any]:
|
|
|
"""Read the config files into a dict
|