Skip to content
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

Closed

Conversation

SchrodingersGat
Copy link
Member

@SchrodingersGat SchrodingersGat commented Sep 27, 2023

Working on fixing some issues with plugin installation via the API.

Reference: inventree/inventree-brother-plugin#25

Tasks

  • Improve feedback during plugin install process
  • Display error messages if plugin cannot be installed
  • Display actual location where plugin gets installed to
  • Ensure that all frontend processes are up-to-date when registry is reloaded
  • Implement method for inactive plugins to provide metadata (without being instantiated)
  • Add option to filter plugin list API by "builtin" or "sample" status
  • Add method for uninstalling a plugin via the API (future work)
  • Apply app plugin migrations when plugin is activated (not installed)
  • Reload plugin registry for all frontend worker processes when a new plugin is added or removed
  • Reload plugin registry for all background worker processes when a new plugin is added or removed (Ref: RFC: Custom Hooks django-q2/django-q2#123)

@SchrodingersGat SchrodingersGat added enhancement This is an suggested enhancement or new feature plugin Plugin ecosystem labels Sep 27, 2023
@SchrodingersGat SchrodingersGat marked this pull request as draft September 27, 2023 07:13
@netlify
Copy link

netlify bot commented Sep 27, 2023

Deploy Preview for inventree canceled.

Name Link
🔨 Latest commit 502c4a3
🔍 Latest deploy log https://app.netlify.com/sites/inventree/deploys/651abc68f1b1b80007754a08

@SchrodingersGat SchrodingersGat added the api Relates to the API label Sep 27, 2023
- Once uninstalled, we do not need the configuration options any longer
…abled

- requires a strange hack as the plugin instance is not instantiated
Copy link
Contributor

@matmair matmair left a 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.

InvenTree/plugin/installer.py Outdated Show resolved Hide resolved
InvenTree/plugin/installer.py Outdated Show resolved Hide resolved
InvenTree/plugin/installer.py Outdated Show resolved Hide resolved
InvenTree/plugin/installer.py Outdated Show resolved Hide resolved
Comment on lines -86 to -89
# Ignore if the plugin is not active
if not self.active:
return None

Copy link
Contributor

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?

Copy link
Member Author

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?

Copy link
Contributor

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 /'])

Copy link
Member Author

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

Copy link
Contributor

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.

Copy link
Member Author

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.

InvenTree/plugin/installer.py Outdated Show resolved Hide resolved
InvenTree/plugin/installer.py Outdated Show resolved Hide resolved
InvenTree/InvenTree/settings.py Outdated Show resolved Hide resolved
@SchrodingersGat
Copy link
Member Author

Interesting refactor / extension. Definitely breaking in multiple instances and changing our threat model.

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
- 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
- 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
Copy link
Contributor

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.

Copy link
Member Author

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

Comment on lines -156 to -157
if settings.PLUGIN_TESTING:
print(f'[PLUGIN] Above error occurred during testing - {retry_counter}/{settings.PLUGIN_RETRY} retries left')
Copy link
Contributor

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()
Copy link
Contributor

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?

Copy link
Member Author

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?

Copy link
Contributor

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.

Comment on lines -334 to +378
logger.info('Collected %s plugins', len(collected_plugins))
logger.debug('Collected %s plugins', len(collected_plugins))
Copy link
Contributor

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)
Copy link
Contributor

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?

Comment on lines 78 to 80
# 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)
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

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

Copy link
Member Author

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

Copy link
Contributor

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.

@matmair
Copy link
Contributor

matmair commented Oct 2, 2023

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 matmair added the breaking Indicates a major update or change which breaks compatibility label Oct 2, 2023
@SchrodingersGat
Copy link
Member Author

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

@matmair
Copy link
Contributor

matmair commented Oct 2, 2023

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.

@SchrodingersGat
Copy link
Member Author

Closing this, and re-implementing as a series of smaller PRs - #5645

@SchrodingersGat SchrodingersGat deleted the plugin-installation branch October 5, 2023 11:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api Relates to the API breaking Indicates a major update or change which breaks compatibility enhancement This is an suggested enhancement or new feature plugin Plugin ecosystem
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants