The enabled state of plugins is now loaded from database rather than config file
Signed-off-by: Ferdinand Thiessen <rpm@fthiessen.de>
This commit is contained in:
parent
e41be21c47
commit
a289664f10
|
@ -11,8 +11,7 @@ from werkzeug.exceptions import HTTPException
|
||||||
from flaschengeist import logger
|
from flaschengeist import logger
|
||||||
from flaschengeist.models import Plugin
|
from flaschengeist.models import Plugin
|
||||||
from flaschengeist.utils.hook import Hook
|
from flaschengeist.utils.hook import Hook
|
||||||
from flaschengeist.plugins import AuthPlugin
|
from flaschengeist.config import configure_app
|
||||||
from flaschengeist.config import config, configure_app
|
|
||||||
|
|
||||||
|
|
||||||
class CustomJSONEncoder(JSONEncoder):
|
class CustomJSONEncoder(JSONEncoder):
|
||||||
|
@ -40,39 +39,30 @@ class CustomJSONEncoder(JSONEncoder):
|
||||||
def load_plugins(app: Flask):
|
def load_plugins(app: Flask):
|
||||||
app.config["FG_PLUGINS"] = {}
|
app.config["FG_PLUGINS"] = {}
|
||||||
|
|
||||||
for entry_point in entry_points(group="flaschengeist.plugins"):
|
enabled_plugins = Plugin.query.filter(Plugin.enabled == True).all()
|
||||||
logger.debug(f"Found plugin: {entry_point.name} ({entry_point.dist.version})")
|
all_plugins = entry_points(group="flaschengeist.plugins")
|
||||||
|
|
||||||
if entry_point.name == config["FLASCHENGEIST"]["auth"] or (
|
for plugin in enabled_plugins:
|
||||||
entry_point.name in config and config[entry_point.name].get("enabled", False)
|
logger.debug(f"Searching for enabled plugin {plugin.name}")
|
||||||
):
|
entry_point = all_plugins.select(name=plugin.name)
|
||||||
logger.debug(f"Load plugin {entry_point.name}")
|
if not entry_point:
|
||||||
|
logger.error(
|
||||||
|
f"Plugin {plugin.name} was enabled, but could not be found.",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
plugin = entry_point.load()(entry_point, config=config.get(entry_point.name, {}))
|
loaded = entry_point.load()(entry_point)
|
||||||
if hasattr(plugin, "blueprint") and plugin.blueprint is not None:
|
if hasattr(plugin, "blueprint") and plugin.blueprint is not None:
|
||||||
app.register_blueprint(plugin.blueprint)
|
app.register_blueprint(plugin.blueprint)
|
||||||
except:
|
except:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Plugin {entry_point.name} was enabled, but could not be loaded due to an error.",
|
f"Plugin {plugin.name} was enabled, but could not be loaded due to an error.",
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
if isinstance(plugin, AuthPlugin):
|
logger.info(f"Loaded plugin: {plugin.name}")
|
||||||
if entry_point.name != config["FLASCHENGEIST"]["auth"]:
|
app.config["FG_PLUGINS"][plugin.name] = loaded
|
||||||
logger.debug(f"Unload not configured AuthPlugin {entry_point.name}")
|
|
||||||
del plugin
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
logger.info(f"Using authentication plugin: {entry_point.name}")
|
|
||||||
app.config["FG_AUTH_BACKEND"] = plugin
|
|
||||||
else:
|
|
||||||
logger.info(f"Using plugin: {entry_point.name}")
|
|
||||||
app.config["FG_PLUGINS"][entry_point.name] = plugin
|
|
||||||
else:
|
|
||||||
logger.debug(f"Skip disabled plugin {entry_point.name}")
|
|
||||||
if "FG_AUTH_BACKEND" not in app.config:
|
|
||||||
logger.fatal("No authentication plugin configured or authentication plugin not found")
|
|
||||||
raise RuntimeError("No authentication plugin configured or authentication plugin not found")
|
|
||||||
|
|
||||||
|
|
||||||
def create_app(test_config=None, cli=False):
|
def create_app(test_config=None, cli=False):
|
||||||
|
|
|
@ -77,17 +77,6 @@ def configure_app(app, test_config=None):
|
||||||
|
|
||||||
configure_logger()
|
configure_logger()
|
||||||
|
|
||||||
# Always enable this builtin plugins!
|
|
||||||
update_dict(
|
|
||||||
config,
|
|
||||||
{
|
|
||||||
"auth": {"enabled": True},
|
|
||||||
"roles": {"enabled": True},
|
|
||||||
"users": {"enabled": True},
|
|
||||||
"scheduler": {"enabled": True},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if "secret_key" not in config["FLASCHENGEIST"]:
|
if "secret_key" not in config["FLASCHENGEIST"]:
|
||||||
logger.critical("No secret key was configured, please configure one for production systems!")
|
logger.critical("No secret key was configured, please configure one for production systems!")
|
||||||
raise RuntimeError("No secret key was configured")
|
raise RuntimeError("No secret key was configured")
|
||||||
|
|
|
@ -81,3 +81,64 @@ def notify(plugin_id: str, user, text: str, data=None):
|
||||||
db.session.add(n)
|
db.session.add(n)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return n.id
|
return n.id
|
||||||
|
|
||||||
|
|
||||||
|
@Hook("plugins.installed")
|
||||||
|
def install_plugin(plugin_name: str):
|
||||||
|
logger.debug(f"Installing plugin {plugin_name}")
|
||||||
|
entry_point = entry_points(group="flaschengeist.plugins", name=plugin_name)
|
||||||
|
if not entry_point:
|
||||||
|
raise NotFound
|
||||||
|
|
||||||
|
plugin = entry_point[0].load()(entry_point[0])
|
||||||
|
entity = Plugin(name=plugin.name, version=plugin.version)
|
||||||
|
db.session.add(entity)
|
||||||
|
db.session.commit()
|
||||||
|
return entity
|
||||||
|
|
||||||
|
|
||||||
|
@Hook("plugin.uninstalled")
|
||||||
|
def uninstall_plugin(plugin_id: Union[str, int]):
|
||||||
|
plugin = disable_plugin(plugin_id)
|
||||||
|
logger.debug(f"Uninstall plugin {plugin.name}")
|
||||||
|
|
||||||
|
entity = current_app.config["FG_PLUGINS"][plugin.name]
|
||||||
|
entity.uninstall()
|
||||||
|
del current_app.config["FG_PLUGINS"][plugin.name]
|
||||||
|
db.session.delete(plugin)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@Hook("plugins.enabled")
|
||||||
|
def enable_plugin(plugin_id: Union[str, int]):
|
||||||
|
logger.debug(f"Enabling plugin {plugin_id}")
|
||||||
|
plugin: Plugin = Plugin.query
|
||||||
|
if isinstance(plugin_id, str):
|
||||||
|
plugin = plugin.filter(Plugin.name == plugin_id).one_or_none()
|
||||||
|
if plugin is None:
|
||||||
|
logger.debug("Plugin not installed, trying to install")
|
||||||
|
plugin = install_plugin(plugin_id)
|
||||||
|
else:
|
||||||
|
plugin = plugin.get(plugin_id)
|
||||||
|
if plugin is None:
|
||||||
|
raise NotFound
|
||||||
|
plugin.enabled = True
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return plugin
|
||||||
|
|
||||||
|
|
||||||
|
@Hook("plugins.disabled")
|
||||||
|
def disable_plugin(plugin_id: Union[str, int]):
|
||||||
|
logger.debug(f"Disabling plugin {plugin_id}")
|
||||||
|
plugin: Plugin = Plugin.query
|
||||||
|
if isinstance(plugin_id, str):
|
||||||
|
plugin = plugin.filter(Plugin.name == plugin_id).one_or_none()
|
||||||
|
else:
|
||||||
|
plugin = plugin.get(plugin_id)
|
||||||
|
if plugin is None:
|
||||||
|
raise NotFound
|
||||||
|
plugin.enabled = False
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return plugin
|
||||||
|
|
|
@ -8,10 +8,17 @@ from ..database.types import Serial, UtcDateTime, ModelSerializeMixin
|
||||||
class Notification(db.Model, ModelSerializeMixin):
|
class Notification(db.Model, ModelSerializeMixin):
|
||||||
__tablename__ = "notification"
|
__tablename__ = "notification"
|
||||||
id: int = db.Column("id", Serial, primary_key=True)
|
id: int = db.Column("id", Serial, primary_key=True)
|
||||||
plugin: str = db.Column(db.String(127), nullable=False)
|
|
||||||
text: str = db.Column(db.Text)
|
text: str = db.Column(db.Text)
|
||||||
data: Any = db.Column(db.PickleType(protocol=4))
|
data: Any = db.Column(db.PickleType(protocol=4))
|
||||||
time: datetime = db.Column(UtcDateTime, nullable=False, default=UtcDateTime.current_utc)
|
time: datetime = db.Column(UtcDateTime, nullable=False, default=UtcDateTime.current_utc)
|
||||||
|
|
||||||
user_id_: int = db.Column("user_id", Serial, db.ForeignKey("user.id"), nullable=False)
|
user_id_: int = db.Column("user", Serial, db.ForeignKey("user.id"), nullable=False)
|
||||||
user_: User = db.relationship("User")
|
plugin_id_: int = db.Column("plugin", Serial, db.ForeignKey("plugin.id"), nullable=False)
|
||||||
|
user_: "User" = db.relationship("User")
|
||||||
|
plugin_: "Plugin" = db.relationship(
|
||||||
|
"Plugin", backref=db.backref("notifications_", cascade="all, delete, delete-orphan")
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugin(self):
|
||||||
|
return self.plugin_.name
|
||||||
|
|
|
@ -22,6 +22,7 @@ class Permission(db.Model, ModelSerializeMixin):
|
||||||
name: str = db.Column(db.String(30), unique=True)
|
name: str = db.Column(db.String(30), unique=True)
|
||||||
|
|
||||||
_id = db.Column("id", Serial, primary_key=True)
|
_id = db.Column("id", Serial, primary_key=True)
|
||||||
|
_plugin_id: int = db.Column("plugin", Serial, db.ForeignKey("plugin.id"))
|
||||||
|
|
||||||
|
|
||||||
class Role(db.Model, ModelSerializeMixin):
|
class Role(db.Model, ModelSerializeMixin):
|
||||||
|
|
|
@ -115,10 +115,10 @@ class BasePlugin:
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, entry_point: EntryPoint, config=None):
|
def __init__(self, entry_point: EntryPoint):
|
||||||
"""Constructor called by create_app
|
"""Constructor called by create_app
|
||||||
Args:
|
Args:
|
||||||
config: Dict configuration containing the plugin section
|
entry_point: EntryPoint from which this plugin was loaded
|
||||||
"""
|
"""
|
||||||
self.version = entry_point.dist.version
|
self.version = entry_point.dist.version
|
||||||
self.name = entry_point.name
|
self.name = entry_point.name
|
||||||
|
@ -127,6 +127,8 @@ class BasePlugin:
|
||||||
def install(self):
|
def install(self):
|
||||||
"""Installation routine
|
"""Installation routine
|
||||||
|
|
||||||
|
Also called when updating the plugin, compare `version` and `installed_version`.
|
||||||
|
|
||||||
Is always called with Flask application context,
|
Is always called with Flask application context,
|
||||||
it is called after the plugin permissions are installed.
|
it is called after the plugin permissions are installed.
|
||||||
"""
|
"""
|
||||||
|
@ -143,6 +145,14 @@ class BasePlugin:
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def installed_version(self):
|
||||||
|
"""Installed version of the plugin"""
|
||||||
|
from ..controller import pluginController
|
||||||
|
|
||||||
|
self.__installed_version = pluginController.get_installed_version(self.name)
|
||||||
|
return self.__installed_version
|
||||||
|
|
||||||
def get_setting(self, name: str, **kwargs):
|
def get_setting(self, name: str, **kwargs):
|
||||||
"""Get plugin setting from database
|
"""Get plugin setting from database
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue