-
-
Notifications
You must be signed in to change notification settings - Fork 821
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Plugin installation improvements #5624
Plugin installation improvements #5624
Conversation
✅ Deploy Preview for inventree canceled.
|
- Once uninstalled, we do not need the configuration options any longer
…abled - requires a strange hack as the plugin instance is not instantiated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting refactor / extension. Definitely breaking in multiple instances and changing our threat model.
# Ignore if the plugin is not active | ||
if not self.active: | ||
return None | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment: This means any discovered plugin gets initiated - also deactivated once. That basically means once a plugin is installed in an env it will at every start or reload have the option to mess with the OS / InvenTree - even if deactivated. Is that intended?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm that's a fair point.
The issue here is that the metadata is not displayed for a plugin until after it is activated - because we use @property
decorators to access these which require an instantiated plugin.
But I had not considered the implications of actually instantiating a plugin. I'll look into whether these is a different way around this. Perhaps implement @classmethod
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anything that imports the class will have the same problem, to load the class the module file is opened. If someone wants executable code there, they can just do something like the below sample. This is the reason I structured the code like it is currently. Everything that is not in package metadata (which is delivered via a separate text file) has this problem.
import os
from plugin import InvenTreePlugin
class MyPlugins(InvenTreePlugin):
"""Some sample code"""
print('I can put everything I want here')
os.popen(['sudo rm -R /'])
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@matmair all of the metadata gathering has been refactored into plugin/meta.py
- I think it should be a lot safer against this sort of thing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just tested this in my prod-clone. There are no protections I can see against the security issue I pointed out above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes after your previous comments I went back and looked at that, and did some more research into whether there is any way to introspect a python file without running it. Doesn't look like there is.
AFAIK the only "breaking" change here would be the ability to uninstall plugins. Everything else is either a bug fix or usability improvement. I think I will leave off the "uninstall" feature for a new / separate PR - I would like this one to focus on fixes and improvements. At the core of this is trying to work out why the "install via web" feature claims to work but does not actually install the plugin in some instances (see the linked issue for the brother plugin) |
… installed (based on required features) - Does not require the plugin class to be instantiated
- Run completely outside plugin class - Does not allow plugins to run any code
- Allows the plugin to be visible right away, even without restarting server
- Name clash
- Store a hash of the plugin registry in the database - Check if the hash matches, if not, reload the plugins - Addresses problem of background and foreground workers being out of sync - Any "external" code that calls plugin functionality will trigger a check
- Only if app mixin is enabled
…nstallation # Conflicts: # InvenTree/plugin/registry.py
- Handle version spec better - Don't use os.path
- Re-throw a ValidationError when saving a setting
- Reduce log output - Fix some typos - Loading plugins updates plugin paths correctly - Use class name as default value for plugin NAME
# Conflicts: # InvenTree/plugin/test_plugin.py
- Use a mutex lock in the plugin registry for cleaner loading / prevent recursion - Cleanup / refactor plugin reload mechanism - Update various unit tests - Assorted cleanups
- More cleanup of typos and logging output - Address more issues with unit tests - Fix some bugs which have existed for a while
- class name attribute lookup is better
blocked_plugin = None | ||
retry_counter = settings.PLUGIN_RETRY | ||
blocked_plugins = [] | ||
retry_counter = settings.PLUGIN_RETRY if not settings.TESTING else 2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be too low - we have already 2 broken samples. There is a reason the default is 5.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was trying to track down a different issue here, I'll change it back :)
if settings.PLUGIN_TESTING: | ||
print(f'[PLUGIN] Above error occurred during testing - {retry_counter}/{settings.PLUGIN_RETRY} retries left') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this removed? Debugging plugin testing will become increasingly hard if reloads are not obvious
# ensure plugins_loaded is True | ||
self.plugins_loaded = True | ||
# Cleanup old plugin configs | ||
self.cleanup_old_plugin_configs() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This means a temporary dependency clash that throws an error deletes a plugin config - i.e. all linked settings. Is that intended?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair point - what if we only run cleanup if plugins loaded without any errors?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At least that - this is a breaking change in how plugins/database settings behave and can break a lot of auto-scaling deployments as any container might delete a bunch of settings before the environment setup is completed.
logger.info('Collected %s plugins', len(collected_plugins)) | ||
logger.debug('Collected %s plugins', len(collected_plugins)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this changed? This means a sysadmin is needed to debug plugins.
if not name: | ||
name = self.human_name | ||
name = plugin.meta.get_plugin_classname(self) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: Why is this changed?
# Get all label plugins | ||
plugins = registry.with_mixin('labels') | ||
self.assertEqual(len(plugins), 2) | ||
|
||
# But, it is not 'active' | ||
self.do_activate_plugin() | ||
plugins = registry.with_mixin('labels') | ||
self.assertEqual(len(plugins), 1) | ||
self.assertEqual(len(plugins), 2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So activation is not tested anymore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are plenty of other tests for plugin activation, either via the API or directly called via the registry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we had problems with this not working for label plugins in the past but fine for me if you are confident the other test catch that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This refactor has uncovered a few areas where the plugins were not behaving as "expected" - I will definitely ensure that the unit tests are up to scratch :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this point this is a full refactor, I will have to retest and rewrite everything in my repos anyway.
This diff is getting so large and spread around different areas of the plugin system, I am not confident simple reviews will be enough to get this merged safely. We will have to test this thoughtfully in the real world, nearly all important surfaces/mechanisms and tests have been changed at this point. |
@matmair I fear you are correct here. I started this to fix a bug, and have got deeper and deeper into the weeds. I think what I will have to do is re-submit as smaller piecemeal changes - otherwise it is unreasonable to expect it can be adequately reviewed or tested. What I have learned in my journey here is that there a few areas where the plugin system needs some work :) I'll make some smaller target PRs and tag you for review |
A lot of decisions in the system are learned in weeks of testing, so I would be careful with large changes. While installations fail in the current implementation in specific cases it is stable and proven - after building an image and deploying it, I am not sure if this PR will be any time soon. |
Closing this, and re-implementing as a series of smaller PRs - #5645 |
Working on fixing some issues with plugin installation via the API.
Reference: inventree/inventree-brother-plugin#25
Tasks
Add method for uninstalling a plugin via the API(future work)