-
Notifications
You must be signed in to change notification settings - Fork 5.4k
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
feat(cli): Add Plugin Support to the Argo CD CLI #20074
base: master
Are you sure you want to change the base?
Conversation
🔴 Preview Environment stopped on BunnyshellSee: Environment Details | Pipeline Logs Available commands (reply to this comment):
|
✅ Preview Environment created on Bunnyshell but will not be auto-deployedSee: Environment Details Available commands (reply to this comment):
|
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #20074 +/- ##
==========================================
- Coverage 55.87% 55.80% -0.08%
==========================================
Files 321 322 +1
Lines 44490 44573 +83
==========================================
+ Hits 24860 24872 +12
- Misses 17067 17129 +62
- Partials 2563 2572 +9 ☔ View full report in Codecov by Sentry. |
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.
Please consider adding a new documentation page explaining how to write plugins and how to install and consume them.
92967cd
to
4a260dd
Compare
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.
plugin_test.go
is kept in separate package because keeping it in the util will create circular dependency.
Update: Resolved
0e8f338
to
c314e43
Compare
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.
@nitishfy Great progress. I think this PR needs to be reviewed by someone from the security sig as I see some potential risks in the code. I added comments to the security issues that I identified at first but a more security driven review would be important in this case.
cc @jannfis @crenshaw-dev
} | ||
if filepath.Base(name) == name { | ||
lp, err := exec.LookPath(name) | ||
if lp != "" && !shouldSkipOnLookPathErr(err) { |
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 may lead to security implications. For example: if someone has their PATH
configured as PATH=.:/usr/bin
, the first .
makes the current directory available in the PATH which enables different types of exploits. Go 1.19+ will return ErrDot
to notify us that the return value from LookPath()
is a relative path. We shouldn't ignore this error. Instead, we should fail and return an error stating that the executable wasn't found. Also, we must state in the docs that the plugin executable must be in the path and should not be provided as relative path.
{ | ||
name: "test that a plugin does not execute over Cobra's __completeNoDesc command", | ||
args: []string{"argocd", cobra.ShellCompNoDescRequestCmd, "de"}, | ||
}, |
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.
Please add a test case to validate that relative paths are not allowed.
// If it finds a command, it continues without invoking the plugin. | ||
// If it doesn't find the command (err is non-nil), it means foo isn't a built-in Argo CD command, | ||
// so it might be a plugin like argocd-foo. | ||
if _, _, err := cmd.Find(cmdPathPieces); err != nil { |
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 will introduce an overhead in all argocd commands. To avoid this problem, I would suggest a modification in the proposal to only execute plugin commands in a dedicated subcommand.
For example, instead of running plugins as:
argocd some-plugin arg1 arg2
declare a dedicated subcommand to invoke plugins as:
argocd plugin some-plugin arg1 arg2
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.
Curious to know is it like a really big deal if all the argocd commands have to go through this? IMO, It doesn't look like a big burden but I'm definitely open to opinions.
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.
Two main reasons why I am suggesting this change are:
- Will greatly simplify the code. This is a big deal as it reduces the possible edge cases and increases maintainability.
- Avoid unnecessary loop that all
argocd
commands would have to execute. Some companies are very sensitive by the durationargocd
commands take to execute. While saving a few milliseconds in a single execution looks irrelevant, it can become a significant issue when executed millions of times.
cmd/main.go
Outdated
switch binaryName { | ||
case "argocd", "argocd-linux-amd64", "argocd-darwin-amd64", "argocd-windows-amd64.exe": | ||
command = cli.NewCommand() | ||
command = cli.NewDefaultArgoCDCommandWithArgs(o) |
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 is a bit too invasive. I think plugins should be implemented as a simple cobra subcommand. See my other comment about defining a dedicated subcommand for running plugins.
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 wondering why do we have to go through a separate subcommand when we can simple invoke those plugins by putting them in the path?
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.
Sorry if I didn't explain it clearly. My suggestion is not about having every single plugin to be implemented as subcommands. It is just to create a single subcommand called plugin
so that can be dedicated to finding binaries in the PATH and executing (example: argocd plugin <some-plugin>
). This is mainly to avoid the loop to decide if the invoked command is a plugin or not that all argocd
commands would have to execute with the current implementation.
@@ -53,7 +58,7 @@ func main() { | |||
case "argocd-k8s-auth": | |||
command = k8sauth.NewCommand() | |||
default: | |||
command = cli.NewCommand() | |||
command = cli.NewDefaultArgoCDCommandWithArgs(o) | |||
} | |||
|
|||
if err := command.Execute(); err != nil { |
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.
As discussed offline between @leoluz, @nitishfy and @christianh814 we can use the error path here to fallback to the plugin lookup, which would alleviate the performance concerns stated above.
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.
@blakepettersson @nitishfy In this case we just need to make sure that the error returned by Cobra is in fact a "command not found". Unfortunately Cobra doesn't return an specific error type for that case. This PR implements it but is still open by the time of this writing: spf13/cobra#2167
We shouldn't trigger the plugin lookup if the command execution returns anything different than "unknown command..". Pattern matching on the error message isn't the direction I would prefer to 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.
@leoluz damn, I thought something so obvious would have already been implemented 😞
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.
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.
@nitishfy I think this is much better. Unfortunately it still doesn't address the issue that I am referring here in this thread. I explained the error scenario a bit better in the comment below.
Signed-off-by: nitishfy <justnitish06@gmail.com>
Signed-off-by: nitishfy <justnitish06@gmail.com>
Signed-off-by: nitishfy <justnitish06@gmail.com>
Signed-off-by: nitishfy <justnitish06@gmail.com>
Signed-off-by: nitishfy <justnitish06@gmail.com>
Signed-off-by: nitishfy <justnitish06@gmail.com>
Signed-off-by: nitishfy <justnitish06@gmail.com>
Signed-off-by: nitishfy <justnitish06@gmail.com>
Signed-off-by: nitishfy <justnitish06@gmail.com>
Signed-off-by: nitishfy <justnitish06@gmail.com>
Signed-off-by: nitishfy <justnitish06@gmail.com>
Signed-off-by: nitishfy <justnitish06@gmail.com>
Signed-off-by: nitishfy <justnitish06@gmail.com>
Signed-off-by: nitishfy <justnitish06@gmail.com>
652887c
to
fea85cd
Compare
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 are not using the cmd.Find()
anymore.
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.
Please check my comments
PluginHandler: cli.NewDefaultPluginHandler([]string{"argocd"}), | ||
Arguments: os.Args, | ||
} | ||
|
||
binaryName := filepath.Base(os.Args[0]) | ||
if val := os.Getenv(binaryNameEnv); val != "" { | ||
binaryName = val | ||
} | ||
|
||
isCLI := false |
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.
For clarity I would suggest to rename this var to isArgocdCLI
as plugins are also CLIs
@@ -63,8 +71,19 @@ func main() { | |||
isCLI = true | |||
} | |||
util.SetAutoMaxProcs(isCLI) | |||
if isCLI { |
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 guess in this case you want the opposite right (if !isCli
)?
|
||
if err := command.Execute(); err != nil { | ||
if isCLI { |
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.
not sure I understand what is the real intention with this isCLI
variable but what we want is to execute the plugin handler only when this is not an argocd
CLI subcommand.
if isCLI { | ||
if pluginErr := cli.HandlePluginCommand(o.PluginHandler, o.Arguments[1:], 1); pluginErr != nil { | ||
fmt.Fprintf(os.Stderr, "Error: %v\n", pluginErr) | ||
os.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.
I am afraid that this is going to swallow the error returned by the command.Execute()
execution above.
Consider the scenario:
- run
argocd app list
argocd app list
returns an errorcli.HandlePluginCommand
is invoked and will always return errorcommand not found
- exit(1)
- the main
argocd app list
error is never presented to the user
@@ -53,7 +58,7 @@ func main() { | |||
case "argocd-k8s-auth": | |||
command = k8sauth.NewCommand() | |||
default: | |||
command = cli.NewCommand() | |||
command = cli.NewDefaultArgoCDCommandWithArgs(o) | |||
} | |||
|
|||
if err := command.Execute(); err != nil { |
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.
@nitishfy I think this is much better. Unfortunately it still doesn't address the issue that I am referring here in this thread. I explained the error scenario a bit better in the comment below.
Ref: #19624
Checklist: