-
Notifications
You must be signed in to change notification settings - Fork 40
feat: Add IDE setup functionality to jfrog-cli-artifactory #99
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
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
114c87b
feat: Add IDE setup functionality to jfrog-cli-artifactory
agrasth 6eb1daf
Make server configuration optional for IDE setup commands
agrasth 32b444c
Add comprehensive documentation for IDE setup commands
agrasth 2d73676
Updated decp and added tests
agrasth 7fc0a96
Fix test failures and improve implementation
agrasth 1644e2e
Updated code on reviews and feedback
agrasth 1042941
Fix IDE CLI commands: clean help output, hide legacy aliases, and ens…
agrasth ea34d7c
Merge branch 'main' into feature/ide-setup-clean-v2
agrasth 4d44b8a
Fix IDE CLI commands: clean help output, hide legacy aliases, and ens…
agrasth 3db2326
Fix IDE CLI commands: clean help output, hide legacy aliases, and ens…
agrasth c7ddf3c
Fix validations fixes
agrasth 93ac10a
Fixed comment
agrasth 1a264de
Fix test failure
agrasth be890a5
Fix jetbrains tests for windows
agrasth e1aab23
Merge branch 'main' into feature/ide-setup-clean-v2
agrasth File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| package ide | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "net/url" | ||
| "strings" | ||
|
|
||
| pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common" | ||
| "github.com/jfrog/jfrog-cli-core/v2/plugins/components" | ||
| ) | ||
|
|
||
| // ValidateSingleNonEmptyArg checks that there is exactly one argument and it is not empty. | ||
| func ValidateSingleNonEmptyArg(c *components.Context, usage string) (string, error) { | ||
| if c.GetNumberOfArgs() != 1 { | ||
| return "", pluginsCommon.WrongNumberOfArgumentsHandler(c) | ||
| } | ||
| arg := c.GetArgumentAt(0) | ||
| if arg == "" { | ||
| return "", fmt.Errorf("argument cannot be empty\n\nUsage: %s", usage) | ||
| } | ||
| return arg, nil | ||
| } | ||
|
|
||
| // HasServerConfigFlags checks if any server configuration flags are provided | ||
| func HasServerConfigFlags(c *components.Context) bool { | ||
| return c.IsFlagSet("url") || | ||
| c.IsFlagSet("user") || | ||
| c.IsFlagSet("access-token") || | ||
| c.IsFlagSet("server-id") || | ||
| // Only consider password if other required fields are also provided | ||
| (c.IsFlagSet("password") && (c.IsFlagSet("url") || c.IsFlagSet("server-id"))) | ||
| } | ||
|
|
||
| // ExtractRepoKeyFromURL extracts the repository key from both JetBrains and VSCode extension URLs. | ||
| // For JetBrains: https://mycompany.jfrog.io/artifactory/api/jetbrainsplugins/jetbrains-plugins | ||
| // For VSCode: https://mycompany.jfrog.io/artifactory/api/vscodeextensions/vscode-extensions/_apis/public/gallery | ||
| // Returns the repo key (e.g., "jetbrains-plugins" or "vscode-extensions") | ||
| func ExtractRepoKeyFromURL(repoURL string) (string, error) { | ||
| if repoURL == "" { | ||
| return "", fmt.Errorf("URL is empty") | ||
| } | ||
|
|
||
| url := strings.TrimSpace(repoURL) | ||
| url = strings.TrimPrefix(url, "https://") | ||
| url = strings.TrimPrefix(url, "http://") | ||
| url = strings.TrimSuffix(url, "/") | ||
|
|
||
| // Check for JetBrains plugins API | ||
| if idx := strings.Index(url, "/api/jetbrainsplugins/"); idx != -1 { | ||
| rest := url[idx+len("/api/jetbrainsplugins/"):] | ||
| parts := strings.SplitN(rest, "/", 2) | ||
| if len(parts) == 0 || parts[0] == "" { | ||
| return "", fmt.Errorf("repository key not found in JetBrains URL") | ||
| } | ||
| return parts[0], nil | ||
| } | ||
|
|
||
| // Check for VSCode extensions API | ||
| if idx := strings.Index(url, "/api/vscodeextensions/"); idx != -1 { | ||
| rest := url[idx+len("/api/vscodeextensions/"):] | ||
| parts := strings.SplitN(rest, "/", 2) | ||
| if len(parts) == 0 || parts[0] == "" { | ||
| return "", fmt.Errorf("repository key not found in VSCode URL") | ||
| } | ||
| return parts[0], nil | ||
| } | ||
|
|
||
| return "", fmt.Errorf("URL does not contain a supported API type (/api/jetbrainsplugins/ or /api/vscodeextensions/)") | ||
| } | ||
|
|
||
| // IsValidUrl checks if a string is a valid URL with scheme and host | ||
| func IsValidUrl(s string) bool { | ||
| u, err := url.Parse(s) | ||
| return err == nil && u.Scheme != "" && u.Host != "" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package ide | ||
|
|
||
| const VscodeConfigDescription = ` | ||
| Configure VSCode to use JFrog Artifactory for extensions. | ||
|
|
||
| The service URL should be in the format: | ||
| https://<artifactory-url>/artifactory/api/vscodeextensions/<repo-key>/_apis/public/gallery | ||
|
|
||
| Examples: | ||
| jf vscode-config https://mycompany.jfrog.io/artifactory/api/vscodeextensions/vscode-extensions/_apis/public/gallery | ||
|
|
||
| This command will: | ||
| - Modify the VSCode product.json file to change the extensions gallery URL | ||
| - Create an automatic backup before making changes | ||
| - Require VSCode to be restarted to apply changes | ||
|
|
||
| Optional: Provide server configuration flags (--url, --user, --password, --access-token, or --server-id) | ||
| to enable repository validation. Without these flags, the command will only modify the local VSCode configuration. | ||
|
|
||
| Note: On macOS/Linux, you may need to run with sudo for system-installed VSCode. | ||
| ` | ||
|
|
||
| const JetbrainsConfigDescription = ` | ||
| Configure JetBrains IDEs to use JFrog Artifactory for plugins. | ||
|
|
||
| The repository URL should be in the format: | ||
| https://<artifactory-url>/artifactory/api/jetbrainsplugins/<repo-key> | ||
|
|
||
| Examples: | ||
| jf jetbrains-config https://mycompany.jfrog.io/artifactory/api/jetbrainsplugins/jetbrains-plugins | ||
|
|
||
| This command will: | ||
| - Detect all installed JetBrains IDEs | ||
| - Modify each IDE's idea.properties file to add the plugins repository URL | ||
| - Create automatic backups before making changes | ||
| - Require IDEs to be restarted to apply changes | ||
|
|
||
| Optional: Provide server configuration flags (--url, --user, --password, --access-token, or --server-id) | ||
| to enable repository validation. Without these flags, the command will only modify the local IDE configuration. | ||
|
|
||
| Supported IDEs: IntelliJ IDEA, PyCharm, WebStorm, PhpStorm, RubyMine, CLion, DataGrip, GoLand, Rider, Android Studio, AppCode, RustRover, Aqua | ||
| ` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| package jetbrains | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "strings" | ||
|
|
||
| "github.com/jfrog/gofrog/log" | ||
| "github.com/jfrog/jfrog-cli-artifactory/artifactory/cli/ide" | ||
| "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/ide/jetbrains" | ||
| pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common" | ||
| "github.com/jfrog/jfrog-cli-core/v2/plugins/components" | ||
| "github.com/jfrog/jfrog-cli-core/v2/utils/config" | ||
| ) | ||
|
|
||
| const ( | ||
| repoKeyFlag = "repo-key" | ||
| urlSuffixFlag = "url-suffix" | ||
| apiType = "jetbrainsplugins" | ||
| ) | ||
|
|
||
| func GetCommands() []components.Command { | ||
| return []components.Command{ | ||
| { | ||
| Name: "jetbrains-config", | ||
| Aliases: []string{"jb"}, | ||
| Hidden: true, | ||
| Flags: getFlags(), | ||
| Arguments: getArguments(), | ||
| Action: jetbrainsConfigCmd, | ||
| Description: ide.JetbrainsConfigDescription, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| func getFlags() []components.Flag { | ||
| return []components.Flag{ | ||
| components.NewStringFlag(repoKeyFlag, "Repository key for the JetBrains plugins repo. [Required if no URL is given]", components.SetMandatoryFalse()), | ||
| components.NewStringFlag(urlSuffixFlag, "Suffix for the JetBrains plugins repository URL. Default: (empty)", components.SetMandatoryFalse()), | ||
| // Server configuration flags | ||
| components.NewStringFlag("url", "JFrog Artifactory URL. (example: https://acme.jfrog.io/artifactory)", components.SetMandatoryFalse()), | ||
| components.NewStringFlag("user", "JFrog username.", components.SetMandatoryFalse()), | ||
| components.NewStringFlag("password", "JFrog password.", components.SetMandatoryFalse()), | ||
| components.NewStringFlag("access-token", "JFrog access token.", components.SetMandatoryFalse()), | ||
| components.NewStringFlag("server-id", "Server ID configured using the 'jf config' command.", components.SetMandatoryFalse()), | ||
| } | ||
| } | ||
|
|
||
| func getArguments() []components.Argument { | ||
| return []components.Argument{ | ||
| { | ||
| Name: "repository-url", | ||
| Description: "The Artifactory JetBrains plugins repository URL (optional when using --repo-key)", | ||
| Optional: true, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // Main command action: orchestrates argument parsing, server config, and command execution | ||
| func jetbrainsConfigCmd(c *components.Context) error { | ||
| repoKey, repositoryURL, err := getJetbrainsRepoKeyAndURL(c) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| rtDetails, err := getJetbrainsServerDetails(c) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| jetbrainsCmd := jetbrains.NewJetbrainsCommand(repositoryURL, repoKey) | ||
|
|
||
| // Determine if this is a direct URL (argument provided) vs constructed URL (server-id + repo-key) | ||
| isDirectURL := c.GetNumberOfArgs() > 0 && ide.IsValidUrl(c.GetArgumentAt(0)) | ||
| jetbrainsCmd.SetDirectURL(isDirectURL) | ||
|
|
||
| if rtDetails != nil { | ||
| jetbrainsCmd.SetServerDetails(rtDetails) | ||
| } | ||
|
|
||
| return jetbrainsCmd.Run() | ||
| } | ||
|
|
||
| // getJetbrainsRepoKeyAndURL determines the repo key and repository URL from args/flags | ||
| func getJetbrainsRepoKeyAndURL(c *components.Context) (repoKey, repositoryURL string, err error) { | ||
| if c.GetNumberOfArgs() > 0 && ide.IsValidUrl(c.GetArgumentAt(0)) { | ||
| repositoryURL = c.GetArgumentAt(0) | ||
| repoKey, err = ide.ExtractRepoKeyFromURL(repositoryURL) | ||
| if err != nil { | ||
| return | ||
| } | ||
| return | ||
| } | ||
|
|
||
| repoKey = c.GetStringFlagValue(repoKeyFlag) | ||
| if repoKey == "" { | ||
| err = fmt.Errorf("You must provide either a repository URL as the first argument or --repo-key flag.") | ||
| return | ||
| } | ||
| // Get Artifactory URL from server details (flags or default) | ||
| var artDetails *config.ServerDetails | ||
| if ide.HasServerConfigFlags(c) { | ||
| artDetails, err = pluginsCommon.CreateArtifactoryDetailsByFlags(c) | ||
| if err != nil { | ||
| err = fmt.Errorf("Failed to get Artifactory server details: %w", err) | ||
| return | ||
| } | ||
| } else { | ||
| artDetails, err = config.GetDefaultServerConf() | ||
| if err != nil { | ||
| err = fmt.Errorf("Failed to get default Artifactory server details: %w", err) | ||
| return | ||
| } | ||
| } | ||
| // Use ArtifactoryUrl if available (when using flags), otherwise use Url (when using config) | ||
| baseUrl := artDetails.ArtifactoryUrl | ||
| if baseUrl == "" { | ||
| baseUrl = artDetails.Url | ||
| } | ||
| baseUrl = strings.TrimRight(baseUrl, "/") | ||
|
|
||
| urlSuffix := c.GetStringFlagValue(urlSuffixFlag) | ||
| if urlSuffix != "" { | ||
| urlSuffix = "/" + strings.TrimLeft(urlSuffix, "/") | ||
| } | ||
| repositoryURL = baseUrl + "/api/jetbrainsplugins/" + repoKey + urlSuffix | ||
| return | ||
| } | ||
|
|
||
| // getJetbrainsServerDetails returns server details for validation, or nil if not available | ||
| func getJetbrainsServerDetails(c *components.Context) (*config.ServerDetails, error) { | ||
| if ide.HasServerConfigFlags(c) { | ||
| // Use explicit server configuration flags | ||
| rtDetails, err := pluginsCommon.CreateArtifactoryDetailsByFlags(c) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to create server configuration: %w", err) | ||
| } | ||
| return rtDetails, nil | ||
| } | ||
| // Use default server configuration for validation when no explicit flags provided | ||
| rtDetails, err := config.GetDefaultServerConf() | ||
| if err != nil { | ||
| // If no default server, that's okay - we'll just skip validation | ||
| log.Debug("No default server configuration found, skipping repository validation") | ||
| return nil, nil //nolint:nilerr // Intentionally ignoring error to skip validation when no default server | ||
| } | ||
| return rtDetails, nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| package jetbrains | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/jfrog/jfrog-cli-artifactory/artifactory/cli/ide" | ||
| "github.com/jfrog/jfrog-cli-core/v2/plugins/components" | ||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| // ... existing code ... | ||
|
|
||
| func TestHasServerConfigFlags(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| flags map[string]string | ||
| expected bool | ||
| }{ | ||
| { | ||
| name: "No flags", | ||
| flags: map[string]string{}, | ||
| expected: false, | ||
| }, | ||
| { | ||
| name: "Only password flag", | ||
| flags: map[string]string{"password": "mypass"}, | ||
| expected: false, | ||
| }, | ||
| { | ||
| name: "Password and URL flags", | ||
| flags: map[string]string{"password": "mypass", "url": "https://example.com"}, | ||
| expected: true, | ||
| }, | ||
| { | ||
| name: "Password and server-id flags", | ||
| flags: map[string]string{"password": "mypass", "server-id": "my-server"}, | ||
| expected: true, | ||
| }, | ||
| { | ||
| name: "URL flag only", | ||
| flags: map[string]string{"url": "https://example.com"}, | ||
| expected: true, | ||
| }, | ||
| { | ||
| name: "User flag only", | ||
| flags: map[string]string{"user": "myuser"}, | ||
| expected: true, | ||
| }, | ||
| { | ||
| name: "Access token flag only", | ||
| flags: map[string]string{"access-token": "mytoken"}, | ||
| expected: true, | ||
| }, | ||
| { | ||
| name: "Server ID flag only", | ||
| flags: map[string]string{"server-id": "my-server"}, | ||
| expected: true, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| ctx := &components.Context{} | ||
| for flag, value := range tt.flags { | ||
| ctx.AddStringFlag(flag, value) | ||
| } | ||
|
|
||
| result := ide.HasServerConfigFlags(ctx) | ||
| assert.Equal(t, tt.expected, result, "Test case: %s", tt.name) | ||
| }) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
instead of having commands here in artifactory/cli/cli.go it is looking good to move them as separate commands for jfrog-cli similar to lifecycle 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.
something similar to
jf vscode configjf jetbrains configso that the command vscode can have its own sub commands likejf vscode installetc