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

feat(cli): Add Plugin Support to the Argo CD CLI #20074

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from

Conversation

nitishfy
Copy link
Contributor

@nitishfy nitishfy commented Sep 24, 2024

Ref: #19624
Checklist:

  • Either (a) I've created an enhancement proposal and discussed it with the community, (b) this is a bug fix, or (c) this does not need to be in the release notes.
  • The title of the PR states what changed and the related issues number (used for the release note).
  • The title of the PR conforms to the Toolchain Guide
  • I've included "Closes [ISSUE #]" or "Fixes [ISSUE #]" in the description to automatically close the associated issue.
  • I've updated both the CLI and UI to expose my feature, or I plan to submit a second PR with them.
  • Does this PR require documentation updates?
  • I've updated documentation as required by this PR.
  • I have signed off all my commits as required by DCO
  • I have written unit and/or e2e tests for my change. PRs without these are unlikely to be merged.
  • My build is green (troubleshooting builds).
  • My new feature complies with the feature status guidelines.
  • I have added a brief description of why this PR is necessary and/or what this PR solves.
  • Optional. My organization is added to USERS.md.
  • Optional. For bug fixes, I've indicated what older releases this fix should be cherry-picked into (this may or may not happen depending on risk/complexity).

Copy link

bunnyshell bot commented Sep 24, 2024

🔴 Preview Environment stopped on Bunnyshell

See: Environment Details | Pipeline Logs

Available commands (reply to this comment):

  • 🔵 /bns:start to start the environment
  • 🚀 /bns:deploy to redeploy the environment
  • /bns:delete to remove the environment

Copy link

bunnyshell bot commented Sep 24, 2024

✅ Preview Environment created on Bunnyshell but will not be auto-deployed

See: Environment Details

Available commands (reply to this comment):

  • 🚀 /bns:deploy to deploy the environment

Copy link

codecov bot commented Sep 24, 2024

Codecov Report

Attention: Patch coverage is 7.05882% with 79 lines in your changes missing coverage. Please review.

Project coverage is 55.80%. Comparing base (e28a05f) to head (92967cd).

Files with missing lines Patch % Lines
cmd/argocd/commands/root.go 0.00% 47 Missing ⚠️
cmd/util/plugin.go 9.09% 30 Missing ⚠️
cmd/main.go 60.00% 2 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

Copy link
Collaborator

@leoluz leoluz left a 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.

cmd/argocd/commands/root.go Outdated Show resolved Hide resolved
cmd/argocd/commands/root.go Outdated Show resolved Hide resolved
cmd/argocd/commands/root.go Outdated Show resolved Hide resolved
@leoluz leoluz self-assigned this Sep 24, 2024
@nitishfy nitishfy force-pushed the nitish/argocd-plugin-support branch 2 times, most recently from 92967cd to 4a260dd Compare October 1, 2024 11:40
Copy link
Contributor Author

@nitishfy nitishfy left a 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

@nitishfy nitishfy force-pushed the nitish/argocd-plugin-support branch 2 times, most recently from 0e8f338 to c314e43 Compare October 8, 2024 11:35
@nitishfy nitishfy changed the title WIP: Add Plugin Support to the Argo CD CLI feat: Add Plugin Support to the Argo CD CLI Oct 8, 2024
@nitishfy nitishfy requested a review from leoluz October 8, 2024 11:57
Copy link
Collaborator

@leoluz leoluz left a 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) {
Copy link
Collaborator

@leoluz leoluz Oct 8, 2024

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"},
},
Copy link
Collaborator

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 {
Copy link
Collaborator

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

cc @christianh814

Copy link
Contributor Author

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.

Copy link
Collaborator

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 duration argocd 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)
Copy link
Collaborator

@leoluz leoluz Oct 8, 2024

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.

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

Copy link
Collaborator

@leoluz leoluz Oct 9, 2024

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.

cmd/argocd/commands/root.go Outdated Show resolved Hide resolved
@@ -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 {
Copy link
Member

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.

Copy link
Collaborator

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 :/

Copy link
Member

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 😞

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@leoluz need your review now. I am yet to remove the dead code here, it is only getting used for the test, but I can fix that too. Let me know what do you think about the current approach.

Copy link
Collaborator

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.

@blakepettersson blakepettersson changed the title feat: Add Plugin Support to the Argo CD CLI feat(cli): Add Plugin Support to the Argo CD CLI Oct 10, 2024
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>
Copy link
Contributor Author

@nitishfy nitishfy left a 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.

@nitishfy nitishfy requested a review from leoluz October 11, 2024 11:58
Copy link
Collaborator

@leoluz leoluz left a 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
Copy link
Collaborator

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 {
Copy link
Collaborator

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 {
Copy link
Collaborator

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)
Copy link
Collaborator

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:

  1. run argocd app list
  2. argocd app list returns an error
  3. cli.HandlePluginCommand is invoked and will always return error command not found
  4. exit(1)
  5. 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 {
Copy link
Collaborator

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.

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.

3 participants