-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Add subcommand extension support #5399
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
base: master
Are you sure you want to change the base?
Conversation
ankur22
left a comment
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.
Generally looks good to me, but i'm not an expert in this particular area of k6.
One thing that I think might be worth doing is adding tests for the failure cases, e.g. when duplicate sub command extensions are added.
@ankur22, Thank you, you are absolutely right. I added a few tests to the |
oleiade
left a comment
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.
Left a couple of non-blocking comments behind. This looks pretty good, and I'm excited to try it out 👏🏻
ext/ext.go
Outdated
| mx.RLock() | ||
| defer mx.RUnlock() | ||
|
|
||
| js, out := extensions[JSExtension], extensions[OutputExtension] |
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.
Would it make sense to also add the Subcommand extensions here? That way users using a subcommand extension can see it listed by the version command when extensions are included?
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.
@oleiade you are absolutely right. I will 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.
@oleiade, done, could you approve again please
mstoykov
left a comment
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.
Most of my critique will be on the ... idea of this feature and less on the actual implementation. I have comments on the implementation taht IMO should be implemented before it is merged, but that is not my bigger problem. Specifically having prefix for the subcommands.
AFAIK v2 will break all extensions unless we do something, and even then it likely will do so. As such I am okay with us having extensions endpoints, with the understanding that v2 might drop some of them.
For me this seems to be a feature we want for ... some use cases that I will categorize mostly as:
- other programs that someone wants to run parallel to k6 or to do somethign with k6 - IMO those will be better off as separate programs
- things that interact with k6 directly. For some of those this makes some sense, but even for xk6-dashboard - it likely should be split in the output and a dashboard management system. Which makes it a simple output and a separate command.
The use case for making k6 cloud not part of core IMO:
- will baloon the public API to proportions that will completely make any upside totally irrelevant
- not being part of core ... isn't really a thing that IMO will make it easier to develop, more stable and w/e
As a whole it seems to just be "a nice to have".
A nice to have that IMO will again expose us to a lot of maintaince burden and IMO won't change who maintaince it and what the work on it needs to be.
On this same topic, I feel like this will benefit from a vision on how it will work with the catalog? I do not see how this will wokr in the cloud at all.
But it will be nice to have an idea of will k6 x-httpbin do something with a k6 without it ? Will it go fetch a new binary with a command ? Will it not, would it tell you about it ?
| // extensionSubcommands returns an iterator over all registered subcommand extensions | ||
| // that are not already defined in the given slice of commands. | ||
| func extensionSubcommands(gs *state.GlobalState, defined []*cobra.Command) iter.Seq[*cobra.Command] { | ||
| already := make(map[string]struct{}, len(defined)) | ||
| for _, cmd := range defined { | ||
| already[cmd.Name()] = struct{}{} | ||
| } | ||
|
|
||
| return func(yield func(*cobra.Command) bool) { | ||
| for _, extension := range ext.Get(ext.SubcommandExtension) { | ||
| if _, exists := already[extension.Name]; exists { | ||
| gs.Logger.WithFields(logrus.Fields{"name": extension.Name, "path": extension.Path}). | ||
| Warnf("subcommand already exists") | ||
| continue | ||
| } | ||
|
|
||
| already[extension.Name] = struct{}{} | ||
|
|
||
| if !yield(getCmdForExtension(extension, gs)) { | ||
| break | ||
| } | ||
| } | ||
| } | ||
| } |
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 seems to be used only in tests and should then be put in the tests
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.
Here it is used to add registered subcommands to the root command: root.go:90-92
| func RegisterExtension(name string, c Constructor) { | ||
| ext.Register(name, ext.SubcommandExtension, c) | ||
| } |
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 do think that it is a requirement that we do not let people just have any names, but similar to js extension have a prefix that they need to have. As otherwise any new command in core will break an extension that uses that same command 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.
You are absolutely right. I missed the namespace for output extensions before. I also thought about the namespace for subcommand extensions, but I haven't found a good solution until now.
The cobra.Command#CommandPath() function gave me an idea of what a good namespace solution would be for subcommand extensions: instead of registering the subcommands of the extensions under the root command, we should create a subcommand called x and register the subcommands of the extensions as its subcommands. The usage for an httpbin subcommand would look like this:
k6 x httpbin
That is, the x subcommand itself would be the namespace that would ensure that the subcommands of the extensions don't conflict with the future k6 subcommands.
What do you think?
| // Constructor is a function type that creates a new cobra.Command for a subcommand extension. | ||
| // It receives a GlobalState instance that provides access to configuration, logging, | ||
| // file system, and other shared k6 runtime state. The returned Command will be | ||
| // integrated into k6's CLI as a subcommand. | ||
| type Constructor func(*state.GlobalState) *cobra.Command | ||
|
|
||
| // RegisterExtension registers a subcommand extension with the given name and constructor function. | ||
| // | ||
| // The name parameter specifies the subcommand name that users will invoke (e.g., "k6 <name>"). | ||
| // The constructor function will be called when k6 initializes to create the cobra.Command | ||
| // instance for this subcommand. | ||
| // | ||
| // This function must be called during package initialization (typically in an init() function) | ||
| // and will panic if a subcommand with the same name is already registered. | ||
| // |
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 would prefer that we have a warning here that modifying the GlobalState is not intended and likely will lead to not working k6.
I do not really like that we provide basically full access to extensions, and this doesn't make this any better.
As usual most k6 APIs are meant for internal usage, and their usage for extensions was experimental and we are now continuing without actually making this different.
This is slightly a mute poitn as I do expect k6 v2 will break eveyr extension whether on purpose or by accident, so 🤷
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.
You are right, a warning would be useful that it is not the intention to modify the GlobalState. I will add a sentence about 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 added a warning: extension.go:19-21
| // Validate that the command's name matches the extension name. | ||
| if cmd.Name() != extension.Name { | ||
| gs.Logger.WithFields(logrus.Fields{"name": extension.Name, "path": extension.Path}). | ||
| Fatalf("subcommand's command name (%s) does not match the extension name (%s)", cmd.Name(), extension.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 am not a fan of using Fatal as a way to return errors and ending execution. It would be a lot nicer to just use 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.
I don't really like the use of Fatal log either. Upon closer inspection, this is not a runtime error, but a faulty extension implementation. In this case, I think the use of panic is justified, I changed it to panic.
(there is an example of panic being used in JavaScript extension registration in case of duplicate registration)
|
@mstoykov thank you for the valuable and inspiring review, I appreciate it. To summarize my answers:
In short, what I replied to the comment: k6 x httpbinThis solution ensures that the subcommands created by the extensions will not conflict with the future subcommands of k6. The
A very good question. The answer is the same as for output extensions. Support for subcommand extensions can then be easily implemented in the above-mentioned namespace ( In short: by parsing the command line, support for subcommand extensions in AER can be implemented. In the cloud, I think support for subcommand extensions in AER is not relevant. |
|
@mstoykov, The |
I don't think we are supporting extensions ONLY for the cloud. My understanding is that we still want to have a thriving OSS extensions ecosystem.
My experience as an extension developer is that having to distribute another binary to supplement an extension is not the best user experience. In my case, it was to help people check the configuration for the disruptor. something like |
Agree here. I started looking at this issue for output extensions. As I understand it, we have the output in the consolidated config, including those defined in the CLI flags. We also have the outputs for extension in the catalog, so we could map the output name to the extension in the catalog. Maybe we could do something similar with subcommands: parse the CLI and collect the name of the subcommand and make it available in the consolidated config. |
Description
This PR adds support for subcommand extensions, allowing external modules to register custom subcommands under the
k6 xnamespace.Changes
Core Extension Framework
SubcommandExtensiontype toext/ext.goSubcommand Package (
subcommand/extension.go)subcommandpackage for registering subcommand extensionsConstructorfunction type that creates cobra.Command instancesRegisterExtensionfunction for registering subcommands during initGlobalStatefor access to k6 runtime stateIntegration with k6 CLI (
internal/cmd/)New
k6 xCommand NamespacegetXfunction ininternal/cmd/subcommand.goxparent command for all extension subcommandsExtension Loading
Added
extensionSubcommandsiterator ininternal/cmd/subcommand.goAdded
getCmdForExtensionhelper with strict validationsubcommand.ConstructorIntegrated into root command (
internal/cmd/root.go)k6 xnamespaceTesting
Comprehensive Test Coverage (
internal/cmd/subcommand_test.go)TestExtensionSubcommands- 5 test cases covering:TestXCommandHelpDisplayCommands- Verifies help outputk6 x helpTest infrastructure (
internal/cmd/root_test.go)registerTestSubcommandExtensionshelper with sync.Oncexcommand presenceImportant Notes
k6 xUse Case
Extensions register subcommands during init:
Users invoke:
Closes #5398