Files
managed-containerd/daemon/registry.py
T

202 lines
5.3 KiB
Python

#!/usr/bin/env python3
import importlib.util
import inspect
import json
import os
from pathlib import Path
class RegistryError(Exception):
pass
class Plugin:
def __init__(self, plugin_root, name):
self.name = name
self.path = Path(plugin_root) / name
self.manifest_path = self.path / "manifest.json"
if not self.manifest_path.is_file():
raise RegistryError(
f"plugin manifest not found: {self.manifest_path}"
)
with self.manifest_path.open("r", encoding="utf-8") as f:
self.manifest = json.load(f)
module_name = self.manifest.get("module", "plugin.py")
module_path = self.path / module_name
if not module_path.is_file():
raise RegistryError(
f"plugin module not found: {module_path}"
)
spec = importlib.util.spec_from_file_location(
f"managed_containerd_plugin_{name}",
module_path,
)
if spec is None or spec.loader is None:
raise RegistryError(
f"unable to load plugin: {name}"
)
self.module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(self.module)
@property
def actions(self):
return list(self.manifest.get("actions", []))
@property
def required_config(self):
return list(
self.manifest.get("required_config", [])
)
@property
def optional_config(self):
return list(
self.manifest.get("optional_config", [])
)
@property
def config_keys(self):
return list(dict.fromkeys(
self.required_config + self.optional_config
))
def validate_config(self, config):
missing = [
key
for key in self.required_config
if not config.get(key)
]
if missing:
raise RegistryError(
"missing required configuration: "
+ ", ".join(missing)
)
def execute(self, action, config, runtime_state):
if action not in self.actions:
raise RegistryError(
f"plugin {self.name} does not support action {action}"
)
handler = getattr(self.module, action, None)
if handler is None:
raise RegistryError(
f"plugin {self.name} advertises {action} "
"but does not implement it"
)
# Backward-compatible generic state injection. Existing one-argument
# plugins continue to receive only config; state-aware plugins can
# declare a second positional parameter named runtime_state.
parameters = list(inspect.signature(handler).parameters.values())
if len(parameters) >= 2:
return handler(config, runtime_state)
return handler(config)
class Resource:
def __init__(self, resource_id, plugin, config):
self.resource_id = resource_id
self.plugin = plugin
self.config = config
# Generic daemon-lifetime state storage. The daemon does not interpret
# plugin keys or values; applications may use this for transition
# history, grace windows, caches, or other resource-local state.
self.runtime_state = {}
self.plugin.validate_config(config)
def describe(self):
return {
"resource": self.resource_id,
"plugin": self.plugin.name,
"plugin_version": self.plugin.manifest.get(
"version"
),
"actions": self.plugin.actions,
"configured": True,
}
class Registry:
def __init__(self):
self.plugin_root = os.environ.get(
"MANAGED_CONTAINERD_PLUGIN_ROOT",
"/opt/managed-containerd/plugins",
)
self.resources = {}
self._load_environment_resource()
def _load_environment_resource(self):
resource_id = os.environ.get("RESOURCE_ID")
plugin_name = os.environ.get("PLUGIN")
if not resource_id:
raise RegistryError(
"RESOURCE_ID is not configured"
)
if not plugin_name:
raise RegistryError(
"PLUGIN is not configured"
)
plugin = Plugin(
self.plugin_root,
plugin_name,
)
config = {
key: os.environ.get(key)
for key in plugin.config_keys
}
self.resources[resource_id] = Resource(
resource_id,
plugin,
config,
)
def list_resources(self):
return {
resource_id: resource.describe()
for resource_id, resource
in self.resources.items()
}
def execute(self, resource_id, action):
resource = self.resources.get(resource_id)
if resource is None:
raise RegistryError(
f"unknown resource: {resource_id}"
)
result = resource.plugin.execute(
action,
resource.config,
resource.runtime_state,
)
return {
"ok": True,
"resource": resource_id,
"plugin": resource.plugin.name,
"action": action,
"result": result,
}