Files
dns-sync/dns_sync/registry.py
T

268 lines
6.0 KiB
Python

import importlib
import importlib.util
import os
import sys
from pathlib import Path
from types import ModuleType
_EXTERNAL_NAMESPACE = "dns_sync_external_plugins"
def _validate_plugin_name(kind: str, name: str) -> str:
normalized = name.strip().lower()
if (
not normalized
or not normalized.replace("_", "").replace("-", "").isalnum()
):
raise RuntimeError(
f"Invalid {kind} plugin name: {name!r}"
)
return normalized
def enabled_plugins() -> frozenset[str]:
raw = os.getenv("ENABLED_PLUGINS", "npm")
names = [
item.strip().lower()
for item in raw.split(",")
]
if not names or any(not name for name in names):
raise RuntimeError(
"ENABLED_PLUGINS contains an empty plugin name"
)
validated = tuple(
_validate_plugin_name("adapter", name)
for name in names
)
if len(set(validated)) != len(validated):
raise RuntimeError(
"ENABLED_PLUGINS contains duplicate plugin names"
)
return frozenset(validated)
def _external_plugin_root() -> Path:
return Path(
os.getenv(
"DNS_SYNC_PLUGIN_PATH",
"/plugins",
)
)
def _ensure_external_namespace() -> None:
if _EXTERNAL_NAMESPACE in sys.modules:
return
namespace = ModuleType(_EXTERNAL_NAMESPACE)
namespace.__package__ = _EXTERNAL_NAMESPACE
namespace.__path__ = [
str(_external_plugin_root())
]
sys.modules[_EXTERNAL_NAMESPACE] = namespace
def _load_external_adapter_module(name: str):
module_name = name.replace("-", "_")
package_path = (
_external_plugin_root()
/ module_name
/ "__init__.py"
)
if not package_path.is_file():
return None
_ensure_external_namespace()
qualified_name = (
f"{_EXTERNAL_NAMESPACE}.{module_name}"
)
existing = sys.modules.get(qualified_name)
if existing is not None:
existing_file = getattr(
existing,
"__file__",
None,
)
if existing_file is not None:
try:
same_file = (
Path(existing_file).resolve()
== package_path.resolve()
)
except OSError:
same_file = False
else:
same_file = False
if same_file:
return existing
# The same plugin name is being resolved from a different
# external plugin root. Do not reuse stale module state.
sys.modules.pop(qualified_name, None)
spec = importlib.util.spec_from_file_location(
qualified_name,
package_path,
submodule_search_locations=[
str(package_path.parent)
],
)
if spec is None or spec.loader is None:
raise RuntimeError(
f"Could not load external adapter plugin "
f"{name!r} from {package_path}"
)
module = importlib.util.module_from_spec(spec)
sys.modules[qualified_name] = module
try:
spec.loader.exec_module(module)
except Exception:
sys.modules.pop(qualified_name, None)
raise
return module
def _load_builtin(kind: str, name: str):
module_name = name.replace("-", "_")
path = f"dns_sync.plugins.{kind}.{module_name}"
try:
return importlib.import_module(path)
except ModuleNotFoundError as exc:
if exc.name == path:
return None
raise
def _adapter_module(name: str):
normalized = _validate_plugin_name("adapter", name)
enabled = enabled_plugins()
if normalized not in enabled:
raise RuntimeError(
f"Adapter plugin {normalized!r} is not enabled"
)
module = _load_builtin("adapters", normalized)
if module is not None:
return (
normalized,
module,
f"dns_sync.plugins.adapters.{normalized}",
)
module = _load_external_adapter_module(normalized)
if module is not None:
return (
normalized,
module,
str(
_external_plugin_root()
/ normalized
/ "__init__.py"
),
)
raise RuntimeError(
f"Unknown adapter plugin: {normalized}"
)
def configured_adapter_endpoints(
name: str,
config,
):
normalized, module, display_path = _adapter_module(name)
loader = getattr(
module,
"configured_endpoints",
None,
)
if loader is None:
raise RuntimeError(
f"Adapter plugin {display_path} does not export "
"configured_endpoints"
)
endpoints = tuple(loader(config))
for endpoint in endpoints:
if endpoint.plugin != normalized:
raise RuntimeError(
f"Adapter plugin {normalized!r} returned endpoint "
f"for plugin {endpoint.plugin!r}"
)
return endpoints
def _plugin_class(module, display_path: str):
plugin_class = getattr(module, "Plugin", None)
if plugin_class is None:
raise RuntimeError(
f"Plugin module {display_path} does not export Plugin"
)
return plugin_class
def _load(kind: str, name: str, config, *args):
normalized = _validate_plugin_name(kind, name)
if kind == "adapters":
(
normalized,
module,
display_path,
) = _adapter_module(normalized)
plugin_class = _plugin_class(
module,
display_path,
)
return plugin_class(config, *args)
module = _load_builtin(kind, normalized)
if module is not None:
plugin_class = _plugin_class(
module,
f"dns_sync.plugins.{kind}.{normalized}",
)
return plugin_class(config, *args)
raise RuntimeError(
f"Unknown {kind[:-1]} plugin: {normalized}"
)
def load_adapter(name: str, config, endpoint):
return _load(
"adapters",
name,
config,
endpoint,
)