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

[WIP] Runtime plugins #1861

Merged
merged 34 commits into from
Aug 5, 2021
Merged

[WIP] Runtime plugins #1861

merged 34 commits into from
Aug 5, 2021

Conversation

jimil749
Copy link
Contributor

@jimil749 jimil749 commented Jul 2, 2021

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:

[shared]
plugin = true

[grpc.services.userprovider]
driver = '/path/to/binary'

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 the New method to initialize the plugin with the configurations.

Progress

  • Global Plugins flag: Option to load plugins from either registry or plugin package
  • Complementing the existing userprovider JSON driver with runtime paradigm.
  • Plugin Package
    • Support for loading plugin binaries
    • Support for compiling and loading the plugin from source
    • Support for plugins hosted as a version controlled repository
      - [ ] Watcher module (Hot Reload)
      - [ ] Separate dev and prod environment

Since, 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! :)

@update-docs
Copy link

update-docs bot commented Jul 2, 2021

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.

examples/plugin/plugin.toml Outdated Show resolved Hide resolved
pkg/user/user.go Outdated Show resolved Hide resolved
@ishank011
Copy link
Contributor

@jimil749 can you also push the changes in the json pkg? It'll help me review the code

@ishank011
Copy link
Contributor

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

@jimil749
Copy link
Contributor Author

jimil749 commented Jul 5, 2021

@ishank011 sure. The plugin code is slightly different than the existing json pkg. I'll add the source code in the example directory.

@ishank011
Copy link
Contributor

Really good progress!

One thought about managing the configuration part. Can we have an interface in the plugin pkg with one method Configure(m map[string]interface{}) error and the manager interfaces in the drivers will embed this? Their New methods will be sort of wrappers around this method. And we can get rid of the duplicate interfaces and assume that all managers will have the Configure method. If this works, we can have the same driver code for both paradigms.

Also, a question. The ManagerRPC would work for all drivers, right? If you have two plugins, json and demo, just changing the path in the configuration should work?

@jimil749
Copy link
Contributor Author

jimil749 commented Jul 5, 2021

Also, a question. The ManagerRPC would work for all drivers, right? If you have two plugins, json and demo, just changing the path in the configuration should work?

Just to quickly answer this: Yes! This method works for all the "userprovider" plugins. I tested it out with demo plugin and it works!

@jimil749
Copy link
Contributor Author

jimil749 commented Jul 5, 2021

One thought about managing the configuration part. Can we have an interface in the plugin pkg with one method Configure(m map[string]interface{}) error and the manager interfaces in the drivers will embed this? Their New methods will be sort of wrappers around this method. And we can get rid of the duplicate interfaces and assume that all managers will have the Configure method. If this works, we can have the same driver code for both paradigms.

That's a great suggestion! Let me try to implement this locally and see how it goes. This would really improve the implementation.

@ishank011 ishank011 requested a review from refs July 5, 2021 10:39
@jimil749
Copy link
Contributor Author

jimil749 commented Jul 5, 2021

One thought about managing the configuration part. Can we have an interface in the plugin pkg with one method Configure(m map[string]interface{}) error and the manager interfaces in the drivers will embed this? Their New methods will be sort of wrappers around this method. And we can get rid of the duplicate interfaces and assume that all managers will have the Configure method. If this works, we can have the same driver code for both paradigms.

@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 New methods as wrappers, which would use the Configure method to configure the plugins. Does that seem fine?

@ishank011
Copy link
Contributor

ishank011 commented Jul 5, 2021

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 pluginMap? The reason why I want to have this interface in the plugin package is to maintain consistency across packages. If we want to add new things sometime in the future, that would be a lot easier as well

@jimil749
Copy link
Contributor Author

jimil749 commented Jul 5, 2021

@ishank011, thanks for the clarification! Makes sense to me. 👍 Also +1 for the registry way of populating the pluginMap. Implementing the same.

@jimil749 jimil749 linked an issue Jul 5, 2021 that may be closed by this pull request
@jimil749 jimil749 requested a review from ishank011 July 6, 2021 08:47
@ishank011
Copy link
Contributor

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

@jimil749
Copy link
Contributor Author

jimil749 commented Jul 7, 2021

Sure!

// 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" {
Copy link
Contributor Author

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!

Copy link
Contributor

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.

Copy link
Contributor Author

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.

return "", fmt.Errorf("could not find current directory: %v", err)
}
name := fmt.Sprintf("%d", rand.Int())
pluginsDir := filepath.Join(wd, "bin", pluginType, name)
Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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

@jimil749
Copy link
Contributor Author

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

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!

Copy link
Contributor

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

@ishank011
Copy link
Contributor

The GRPC services have a Close method where we can call Kill if the plugin flag is set

pkg/plugin/plugin.go Outdated Show resolved Hide resolved
pkg/plugin/plugin.go Outdated Show resolved Hide resolved
@jimil749 jimil749 requested a review from ishank011 July 19, 2021 08:31
@jimil749
Copy link
Contributor Author

jimil749 commented Jul 22, 2021

@ishank011, currently this approach works just fine:

  1. The plugin package is able to load plugin binary via the hashicorp go-plugin.
  2. The plugin package is also able to first compile the source code, store the binary at ./bin/userprovider/<driver> and then load the binary. (Only constraint is that the user needs to provide relative path to the source code!). Package differentiates the binary file and source code using os.Stat. If the user points to a package/directory, we compile else we directly load the driver.
  3. The older in-memory registry way loading is also supported, can be toggled by the plugin flag in the shared config.

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

func init() {
gob.Register(&userpb.User{})
Copy link
Contributor Author

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!

Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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!

Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor

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>
@jimil749
Copy link
Contributor Author

jimil749 commented Aug 4, 2021

/cc @ishank011.

@ishank011 ishank011 merged commit 324f703 into cs3org:master Aug 5, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Runtime Plugins: Design & integration
3 participants