-
Notifications
You must be signed in to change notification settings - Fork 113
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
[WIP] Runtime plugins #1861
[WIP] Runtime plugins #1861
Conversation
Thanks for opening this pull request! The maintainers of this repository would appreciate it if you would create a changelog item based on your changes. |
@jimil749 can you also push the changes in the json pkg? It'll help me review the code |
Or do we have different code for the plugin and the registry paths? |
@ishank011 sure. The plugin code is slightly different than the existing json pkg. I'll add the source code in the example directory. |
Really good progress! One thought about managing the configuration part. Can we have an interface in the plugin pkg with one method Also, a question. The |
Just to quickly answer this: Yes! This method works for all the "userprovider" plugins. I tested it out with demo plugin and it works! |
That's a great suggestion! Let me try to implement this locally and see how it goes. This would really improve the implementation. |
@ishank011, why can't we just add the method to the manager interface, instead of embedding? I think that should also work? Because, embedding will add dependency on the plugins package. (which would cause cyclic imports) We could simply add Configure method to all the managers and have the |
The dependency on the individual packages isn't ideal. Can we have a registry similar to what we have for the individual packages to populate |
@ishank011, thanks for the clarification! Makes sense to me. 👍 Also +1 for the registry way of populating the |
Majorly looks good. I have a few nitpicks but we can get to those later on. We can proceed to the next step I guess? Let's discuss the compilation part in the issue |
Sure! |
pkg/plugin/plugin.go
Outdated
// Load loads the plugin using the hashicorp go-plugin system | ||
func Load(driver string, pluginType string) (interface{}, error) { | ||
bin := driver | ||
if filepath.Ext(driver) == ".go" { |
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.
Is this okay? I am currently assuming that if the user wants to compile, he/he can provide path to the source code ending with .go
. So whenever the config ends with .go
, we compile!
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 path should point to a package as it can have multiple go files.
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.
Ah okay. So then we can stat the path, and check if that's a directory, we compile else we directly load.
pkg/plugin/plugin.go
Outdated
return "", fmt.Errorf("could not find current directory: %v", err) | ||
} | ||
name := fmt.Sprintf("%d", rand.Int()) | ||
pluginsDir := filepath.Join(wd, "bin", pluginType, name) |
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 store the binaries in ./bin/userprovider/<rand_int>
. Is this fine?
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 a random integer? Let's use ./bin/userprovider/driver_name
, we can replace the old binaries and won't have to worry about cleaning those up.
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.
Yeah makes sense. I'll make the changes
@ishank011, I'm a bit worried about one thing: with the current approach, loading a plugin binary works just fine. The hashicorp go-plugin spawns a new plugin process for us, which can be used over rpc. But we're not killing the process once we're done using it. So when I quit revad, the plugin process is not killed. We need to clean that up too. |
} | ||
|
||
basename := filepath.Base(c.Driver) | ||
pluginConfig := strings.TrimSuffix(basename, filepath.Ext(basename)) |
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.
Again, I need to do this because, whenever a user provides configure like:
[grpc.services.userprovider]
driver = /home/path/to/json.go
[grpc.services.userprovider.json]
users = users.demo.json
so I need to strip off, .go
from the driver field to access to users field!
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.
Pointing to the package should take care of that
The GRPC services have a |
@ishank011, currently this approach works just fine:
|
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
1. Global registry to populate the plugin map (plugin registry) 2. Plugin Config interface with `Configure(m map[string]interface{})` to configure plugins. Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
dd53745
to
ab793c3
Compare
) | ||
|
||
func init() { | ||
gob.Register(&userpb.User{}) |
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 can register interfaces (which we would like to pass as context) here. But I am not sure what all structs/interfaces that we'd like to add!
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.
Are we able to log from the plugins now?
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 tried passing the user object as a part of the context. I was able to access the user object (via the context.Value) on the plugin side. But here's the problem with the logger: the appctx's WithLogger
attaches zerolog's ctxKey
struct to the context, which is not exported, hence we cannot register that at init time.
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.
Can you look into ways of getting around 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.
In the auth pkg, we're passing auth.Scope
without registering it. How does that work?
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 think this might be a roadblock as there is not way to gob.Register(ctxkey{})
at init stage because ctxKey{} is private! Hence the RPC is always going to fail when attaching logger to context.
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.
Can you look into ways of getting around that?
Yes. I was just investigating ways around this!
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'll merge this for now but this needs to be addressed ASAP.
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! So the reason we're getting the gob error is because we're passing map[interface{}]interface{}
, If we are passing interface{}
through gob, that NEEDS to be registered, passing structs is fine. That's why it does not complain when using auth.Scope
. https://pkg.go.dev/encoding/gob#Register I'll further dig into this
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.
Then instead of the map, let's store all the keys that we know of in a struct. The ones we currently use are the logger, user object and the token.
Signed-off-by: Jimil Desai <jimildesai42@gmail.com>
/cc @ishank011. |
Description
This is my first try to enable runtime plugin support in the Reva ecosystem. This PR introduces a new
plugin
package, which is responsible for loading the plugins using the hashicorp go-plugin library. The current implementation uses Remote Procedure Call (RPC) to communicate between the host(reva core) and the plugin. I tried to move the userprovider drivers to the runtime paradigm, which implies that a user load userprovider drivers (like json, ldap etc) at runtime by just pointing to the binary in the configuration.Working
In order to facilitate backward compatibility, there aren't any major changes to the configuration. Also, loading plugins from the in-memory is also supported. If the user wishes to load external plugins, he/she needs to add an extra shared config:
This will configure the services to load plugins from the plugins package rather than the registry. The plugins package creates an rpc client, loads the required plugin and returns an
interface{}
value, which is then asserted into required type by the respective services. It is then responsibility of the services to call theNew
method to initialize the plugin with the configurations.Progress
- [ ] Watcher module (Hot Reload)- [ ] Separate dev and prod environmentSince, there maybe a lot of changes involved with the approach I've taken, I am creating a draft PR. Any comments for improvement would be highly appreciated! :)