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

Begin prototyping plugin invocation #9

Closed
wants to merge 1 commit into from

Conversation

juanvallejo
Copy link
Owner

@juanvallejo juanvallejo commented May 29, 2018

Opening here first to generate some discussion about implementation details.

With the current design, a user is able to place an executable file anywhere on their PATH.
A search is performed based on the command line arguments given by the user.
For a path kubectl plugin foo, for example, we:

  1. search to see if the subcommand "plugin" exists.
  • if "plugin" exists, we lookup its annotations to see if it is marked as "can-be-extended"
  • if "plugin" does not exist, kubectl cannot be "extended", therefore, we treat that as a "command-not-found" error always.
  1. search to see if subcommand "foo" exists.
  • if "foo" exists, we lookup its annotations to see if it is marked as "can-be-extended"
  • if "foo" does not exist, we check to see if its parent was marked as "can-be-extended"
    • if foo's parent was marked as "can-be-extended", we attempt to look for the executable kubectl-plugin-foo [3] (kubectl-plugin-foo.exe on windows) [2] in the user's PATH.
      • if the executable (kubectl-plugin-foo) is not found in the user's PATH, we exit with error [1].
    • if foo's parent was not marked as "can-be-extended", we exit with error [1].
error: unknown command "foo"
See 'kubectl plugin -h' for help and examples.
  1. Note that the binary's name is not bound to kubectl-plugin-..., if we were writing a plugin for "create", for example, we would look for a binary kubectl-create-foo
  2. Also note that the current implementation looks for the longest possible binary in the user's path that it can find. For example, if a user specifies kubectl plugin foo bar baz, and the user's PATH contains two binaries (kubectl-plugin-foo, and kubectl-plugin-foo-bar-baz), then kubectl-plugin-foo-bar-baz would always be executed.

cc @deads2k @soltysh @mfojtik

@juanvallejo juanvallejo force-pushed the jvallejo/prototype-plugins branch 2 times, most recently from 0ce1e8d to 68259c2 Compare May 29, 2018 18:18
@juanvallejo
Copy link
Owner Author

Example:

$ cat ./plugins/kubectl-plugin-foo
#!/bin/bash

echo "Hello world"
$ kubectl plugin foo
Hello world
$ kubectl plugin nonexistent
error: unknown command "nonexistent"
See 'kubectl plugin -h' for help and examples.

@juanvallejo
Copy link
Owner Author

juanvallejo commented May 29, 2018

TODO:

  • Handle passing current working directory to plugin binary
    • we could pass this information via an environment variable to the plugin binary
    • This information is preserved via the execve call
  • Handle environment and how it is passed to plugin binary (document)
    • execve syscall receives an argument containing environment variables to make available to the subprocess
  • Handle commands with dashes in their name
    • if a plugin binary is extending a command whose name contains dashes (e.g. kubectl api-resources foo), those must be represented as underscores in the binary name: kubectl-api_resources-foo
  • Describe where we load plugins from (user's PATH)
    • plugin binaries are looked up on the user's PATH.
    • a plugin binary must start with "kubectl-" in its filename
    • a plugin binary must include the full "command path" that it is extending, with each argument separated by dashes (kubectl-plugin-foo-bar)
    • if there are two "competing" binaries in the user's path, (e.g. kubectl-plugin-foo, and kubectl-plugin-foo-bar), then the binary with the "longest matching" name will be executed. See [1].
  • commands opt-in to be extensible
    • commands do this by setting a cobra command annotation in their constructor
  • Describe how args are passed to a plugin binary
    • all args are passed as-is to the plugin binary via an execve syscall
  • Handle verification of commands that opt-in to be extensible (extensions-verifier that allows us to approve new commands that opt-in to be extended)
  • Figure out how to best handle sub-command aliases when mapping command args to a plugin binary's name (e.g. kubectl cp foo -> plugins/kubectl-copy-foo)
$ ls plugins
kubectl-plugin-foo  kubectl-plugin-foo-bar
$ kubectl plugin foo bar baz
# executing binary `kubectl-plugin-foo-bar`
$ kubectl plugin foo baz buz
# executing binary `kubectl-plugin-foo`

@juanvallejo juanvallejo force-pushed the jvallejo/prototype-plugins branch 6 times, most recently from a6baaa6 to 95199b2 Compare May 29, 2018 21:20
@juanvallejo
Copy link
Owner Author

cc @liggitt


platformSuffix := ""
if runtime.GOOS == "windows" {
platformSuffix = ".exe"
Copy link

@liggitt liggitt May 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LookPath on windows appears to append this on its own - https://golang.org/src/os/exec/lp_windows.go#L59

}

// invoke cmd binary relaying the current environment and args given
if err := syscall.Exec(foundBinaryPath, os.Args[1:], os.Environ()); err != nil {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • does this preserve the current working dir of the kubectl invocation?
  • should we pass os.Args[len(remainingArgs):], so that kubectl plugin foo bar baz passes baz to kubectl-plugin-foo-bar?
  • should we pass cmdArgs[...] rather than os.Args[...]?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this preserve the current working dir of the kubectl invocation?

At least on a local env, I saw this to be true - os.Getwd() in kubectl returned the same value as pwd in the plugin executable when run via the execve call.

should we pass os.Args[len(remainingArgs):], so that kubectl plugin foo bar baz passes baz to kubectl-plugin-foo-bar?

I was not sure about this since there was some discussion about passing args "as-is", but I'm not against this.

func handleEndpointExtensions(cmdArgs []string) {
remainingArgs := cmdArgs
for idx := range remainingArgs {
remainingArgs[idx] = strings.Replace(remainingArgs[idx], "-", "_", -1)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • should we stop as soon as we encounter an option (an arg starting with -)?
  • append the normalized form to a new slice, overwriting here modifies cmdArgs and cmdPathPieces and os.Args

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we stop as soon as we encounter an option (an arg starting with -)?

Would this be an invalid operation? Could we instead "move" those from remainingArgs to a separate slice, then append them to the list of args that we ultimately pass to the subprocess? I would be in support of allowing flags to be passed as arguments to plugins

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

append the normalized form to a new slice, overwriting here modifies cmdArgs and cmdPathPieces and os.Args

ack

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we instead "move" those from remainingArgs to a separate slice, then append them to the list of args that we ultimately pass to the subprocess?

we don't have enough info to move flags here. you would also have to move accompanying value args (e.g. move --foo bar as a unit, without knowing whether --foo is an arg that expects a value).

I would be in support of allowing flags to be passed as arguments to plugins

absolutely, I'm saying we should not include them in the list of args considered when searching for a named plugin.

If I call kubectl plugins foo bar --baz qux, we should start our search with kubectl-plugins-foo-bar. If we find kubectl-plugins-foo, I'd expect to pass the args ["bar","--baz","qux"] to it

Copy link

@liggitt liggitt May 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this implies all flags come after the command name, which is sometimes-required-and-sometimes-not for native kubectl commands, depending on whether they are global flags like --namespace or command-specific flags like --dry-run

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@liggitt

If I call kubectl plugins foo bar --baz qux, we should start our search with kubectl-plugins-foo-bar. If we find kubectl-plugins-foo, I'd expect to pass the args ["bar","--baz","qux"] to it

This was my original approach when searching for a plugin binary, however @deads2k pointed out that if a user has two binaries kubectl-plugins-foo and kubectl-plugins-foo-bar in his PATH, it is more reasonable for our use-case that we match kubectl-plugins-foo-bar over the other one, hence why I switched to reverse matching the binary name.

Also, thinking about flag handling a bit more, another scenario to consider would be when a user specifies a flag [and a potential value for it] in the middle of a command invocation: kubectl plugin foo --flagname maybeflagvalue bar baz (binary to match here could be kubectl-plugin-foo-bar)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was my original approach when searching for a plugin binary, however @deads2k pointed out that if a user has two binaries kubectl-plugins-foo and kubectl-plugins-foo-bar in his PATH, it is more reasonable for our use-case that we match kubectl-plugins-foo-bar over the other one, hence why I switched to reverse matching the binary name.

I agree with the reverse search. In my example, the reverse search would consist of:

  • kubectl-plugins-foo-bar
  • kubectl-plugins-foo

It would not include kubectl-plugins-foo-bar-__bar-qux

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, thinking about flag handling a bit more, another scenario to consider would be when a user specifies a flag [and a potential value for it] in the middle of a command invocation: kubectl plugin foo --flagname maybeflagvalue bar baz (binary to match here could be kubectl-plugin-foo-bar)

We don't have sufficient information to support that case. That's what I was trying to describe in this comment:

we don't have enough info to move flags here. you would also have to move accompanying value args (e.g. move --foo bar as a unit, without knowing whether --foo is an arg that expects a value).

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have sufficient information to support that case. That's what I was trying to describe in this comment:

Makes sense, will update reverse search to take "flag-value" pairs into account. Thanks

@liggitt
Copy link

liggitt commented May 30, 2018

Handle passing current working directory to plugin binary

is that not inherited by an exec call?

@mfojtik
Copy link

mfojtik commented May 30, 2018

@liggitt it is, we use same way to execute in oc adm for example...

@juanvallejo juanvallejo force-pushed the jvallejo/prototype-plugins branch 6 times, most recently from e8b71b0 to 91f8077 Compare May 30, 2018 16:10
@juanvallejo
Copy link
Owner Author

juanvallejo commented May 31, 2018

Example with --flags

$ ls plugins
kubectl-plugin-foo  kubectl-plugin-foo-bar
$ kubectl plugin foo bar --flag buz baz
exec plugin "foo bar"
with options: baz --flag buz
$ kubectl plugin foo bar --flag=buz baz
exec plugin "foo bar"
with options: baz --flag=buz
$ touch plugins/kubectl-plugin-foo-bar-baz
# populate new plugin file...

$ ls plugins
kubectl-plugin-foo  kubectl-plugin-foo-bar  kubectl-plugin-foo-bar-baz
$ kubectl plugin foo bar --flag buz baz
exec plugin "foo bar baz"
with options: --flag buz

@liggitt
Copy link

liggitt commented May 31, 2018

don't reorder args at all... you don't know if the arg following a flag is associated with the flag or not

$ kubectl plugin foo bar --flag buz baz
exec plugin "foo bar"
with options: --flag buz baz
$ kubectl plugin foo bar --flag=buz baz
exec plugin "foo bar"
with options: --flag=buz baz

notably, kubectl plugin foo bar --flag buz baz would not be able to locate kubectl-plugin-foo-bar-baz:

$ kubectl plugin foo bar --flag buz baz
exec plugin "foo bar"
with options: --flag buz baz

@juanvallejo
Copy link
Owner Author

juanvallejo commented May 31, 2018

@liggitt

don't reorder args at all... you don't know if the arg following a flag is associated with the flag or not

Done in latest commit:

$ kubectl plugin foo bar --flag buz baz
exec plugin "foo bar"
with options: --flag buz baz
$ kubectl plugin foo bar baz --flag buz
exec plugin "foo bar"
with options: baz --flag buz
$ kubectl plugin foo --flag buz bar baz
exec plugin "foo bar"
with options: --flag buz baz

@liggitt
Copy link

liggitt commented May 31, 2018

close, but the last one didn't look right:

$ kubectl plugin foo --flag buz bar baz
exec plugin "foo"
with options: --flag buz bar baz

as soon as you hit a flag arg, stop searching for plugins

@juanvallejo
Copy link
Owner Author

juanvallejo commented May 31, 2018

@liggitt

as soon as you hit a flag arg, stop searching for plugins

ack, updated last commit

// if dealing with a binary extension.
currentCmd := cmd
canBeExtended := false
for _, target := range cmdPathPieces {
Copy link

@liggitt liggitt Jun 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be simplified to:

args := os.Args[1:]
leafCmd, _, _ := cmd.Find(args)
if leafCmd != nil && leafCmd.Annotations[EndpointCanBeExtendedAnnotation] == "true" {
  handleEndpointExtensions(args)
}

Copy link

@soltysh soltysh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@juanvallejo
Copy link
Owner Author

This has been opened against upstream.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants