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:
Ferdinand Thiessen 2022-08-18 19:58:02 +02:00
parent e41be21c47
commit a289664f10
6 changed files with 108 additions and 50 deletions

View File

@ -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:
try: logger.error(
plugin = entry_point.load()(entry_point, config=config.get(entry_point.name, {})) f"Plugin {plugin.name} was enabled, but could not be found.",
if hasattr(plugin, "blueprint") and plugin.blueprint is not None: exc_info=True,
app.register_blueprint(plugin.blueprint) )
except: continue
logger.error( try:
f"Plugin {entry_point.name} was enabled, but could not be loaded due to an error.", loaded = entry_point.load()(entry_point)
exc_info=True, if hasattr(plugin, "blueprint") and plugin.blueprint is not None:
) app.register_blueprint(plugin.blueprint)
continue except:
if isinstance(plugin, AuthPlugin): logger.error(
if entry_point.name != config["FLASCHENGEIST"]["auth"]: f"Plugin {plugin.name} was enabled, but could not be loaded due to an error.",
logger.debug(f"Unload not configured AuthPlugin {entry_point.name}") exc_info=True,
del plugin )
continue continue
else: logger.info(f"Loaded plugin: {plugin.name}")
logger.info(f"Using authentication plugin: {entry_point.name}") app.config["FG_PLUGINS"][plugin.name] = loaded
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):

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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