-
Notifications
You must be signed in to change notification settings - Fork 73
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 framework #51
Plugin framework #51
Conversation
closing #50 as deprecated |
before I go on reviewing I have a doubt: is there support for config files? Having to specify paths on command invocation sounds very unergonomic, so having a setting for that would be nice, plus when adding subcommands they could be installed in the right place |
Next step will be implementation of "remotes" feature. Like git remote, CLI will allow adding, listing, updating and deleting remotes. Something like: 3scale remote add origin https://my_provider_key@3scale-admin.eguzki-amp-cli-01.master.staging.3sca.net
3scale remote add destination https://my_provider_key@3scale-admin.eguzki-amp-cli-02.master.staging.3sca.net Then 3scale copy service origin destination -t new_service service_id CLI framework should provide necessary API for commands to use it easily |
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 it is great. Idiomatic ruby, minimal meta programming. Pretty obvious where everything points, so it is IDE friendly.
end | ||
end | ||
|
||
def self.run(opts, args) |
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.
Could (sub)commands be classes too? I guess so, right?
It does not affect the plugin system at all, but I think it would be nice as an example to actually instantiate the class with parameters and then run it.
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.
Sure. Just change module FooCommand
for class FooCommand
and example should work. But instantiate the class is not going to work as it is know. I will think about it. Initially modules are used because it did not make sense to create "objects" of commands.
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.
The "command" can still rely on class level .run
, but that one would do .new(opts, args).call
to actually instantiate the class and call it.
That will allow "commands" to have state per execution without messing global/class variables.
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.
Something like this?
$ cat lib/3scale_toolbox_plugin.rb
require '3scale_toolbox/cli'
class FooCommand
extend ThreeScaleToolbox::Command
def self.command
Cri::Command.define do
name 'foo'
usage 'foo [options]'
summary '3scale copy foo'
description '3scale copy foo subcommand'
flag :h, :help, 'show help for this command' do |_, cmd|
puts cmd.help
exit 0
end
run do |opts, args, _|
FooCommand.new(opts, args).call
end
end
end
def initialize(opts, args)
@opts = opts
@args = args
end
def call
puts "foo done"
end
end
ThreeScaleToolbox::Commands::CopyCommand.add_subcommand(FooCommand)
Then running
$ RUBYOPT=-Ilib 3scale copy foo
foo done
This example works perfectly
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.
Yep. Something like that. 👍 🥇
I think we should "promote" this kind of interface instead of class level one, because it is harder to make mistakes (like instance level variables in class methods).
But that does not need to be concern of this PR.
|
||
def self.exit_with_message(message) | ||
puts message | ||
exit 1 |
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 will need to find some abstraction for this later, so it does not exit running test suite.
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 looks ok, with some doubts about the plugin system that I think will be cleared up. However I think having a way to specify settings in a config file will be necessary.
-v --version Prints the version of this command | ||
``` | ||
|
||
Now, package your plugin as a [gem](https://guides.rubygems.org/make-your-own-gem/) and let us know about it. |
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 part is the most fragile. Gems have a flat namespace where you basically would need to name your plugin as 3scale_toolbox-myplugin
to avoid polluting this global shared namespace. I'd advocate having the code installed in something such as /usr/share/3scale_toolbox/plugins
(or .local/share
...) or having a config file setting to specify it.
At the very least, here we should suggest using a specific prefix for the gem names, and possibly enforce it when looking up plugin code.
Update: I see we are using a specific path to look up plugins, but I guess that is still not enforcing things like the above, and it is not clear how to install such plugins from this doc.
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.
That is a common practice in Ruby ecosystem. https://guides.rubygems.org/name-your-gem/
We just require gems to have 3scale_toolbox_plugin.rb
to be automatically loaded by the 3scale
executable.
I don't really see any need to enforce any rules. People are free to do whatever they want to do.
It is common practice to name extensions prefixed with the gem name. Also it is common practice to namespace all your code with the correct namespace according to your gem name.
}.reject { |key, value| !value } | ||
end | ||
|
||
def self.copy_service(service_id, source, destination, system_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.
this method is very long and calls for refactoring into smaller parts
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.
Agreed, but it is an improvement over existing one file solution :) This PR is supposed to show off the plugin system, not to reimplement those commands in a nice, easy to test way.
require 'uri' | ||
require 'csv' | ||
require '3scale/api' | ||
require '3scale_toolbox/base_command' |
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.
shouldn't both 3scale/api
and 3scale_toolbox/base_command
be assumed to have been required by the command loader?
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'm definitely for not assuming anything. At some point you are going to test this in isolation and it will fire back.
I guess all doubts have been resolved. Further issues can be discussed in coming PR's. |
3scale Toolbox CLI is based on cri library for building command line tools.
Plugin system also uses cri to leverage easy to develop, flexible and extensible plugin system.
3scale Toolbox will load plugins installed in gems or $LOAD_PATH. Plugins are discovered via Gem::find_files, then loaded.
Plugins must be named ‘3scale_toolbox_plugin’ (.rb, .so, etc) and placed at the root of your gem’s #require_path.
Plugins may add commands to 3scale CLI or may add subcommands to any existing command.
Subcommands may be added to main commands or other subcommands as children.
Nothing better than few examples to illustrate .
Let's create a plugin to add a main
simple hello world
command.Few things worth to highlight.
command
module function and return instance ofCri::Command
from cri3scale
CLI command tree by callingThreeScaleToolbox::CLI.add_command
Your plugin help is also available using builtin help command
Let's create a plugin to add a
simple hello world
subcommand for the builtin copy command.Few things worth to highlight.
command
module function and return instance ofCri::Command
from cri3scale
CLI command tree by calling parent command's module'sadd_subcommand
method.Checking
copy
command help, it can be verified the new subcommandfoo
is added.