Skip to content

Commit

Permalink
Package manager login command - Npm, Yarn, Pip, Pipenv, `Poet…
Browse files Browse the repository at this point in the history
…ry`,`Go`, `Nuget`, `Dotnet` (#1285)
  • Loading branch information
sverdlov93 authored Nov 18, 2024
1 parent 24197a7 commit b6efb65
Show file tree
Hide file tree
Showing 19 changed files with 918 additions and 126 deletions.
43 changes: 33 additions & 10 deletions artifactory/commands/dotnet/dotnetcommand.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"fmt"
"github.com/jfrog/build-info-go/build"
"github.com/jfrog/build-info-go/build/utils/dotnet"
"github.com/jfrog/gofrog/io"
frogio "github.com/jfrog/gofrog/io"
commonBuild "github.com/jfrog/jfrog-cli-core/v2/common/build"
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
"github.com/jfrog/jfrog-client-go/auth"
Expand All @@ -19,7 +19,7 @@ import (
)

const (
SourceName = "JFrogCli"
SourceName = "JFrogArtifactory"
configFilePattern = "jfrog.cli.nuget."

dotnetTestError = `the command failed with an error.
Expand Down Expand Up @@ -159,21 +159,44 @@ func changeWorkingDir(newWorkingDir string) (string, error) {
return newWorkingDir, errorutils.CheckError(err)
}

// Runs nuget sources add command
func AddSourceToNugetConfig(cmdType dotnet.ToolchainType, configFileName, sourceUrl, user, password string) error {
// Runs nuget/dotnet source add command
func AddSourceToNugetConfig(cmdType dotnet.ToolchainType, sourceUrl, user, password string) error {
cmd, err := dotnet.CreateDotnetAddSourceCmd(cmdType, sourceUrl)
if err != nil {
return err
}

flagPrefix := cmdType.GetTypeFlagPrefix()
cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"configfile", configFileName)
cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"name", SourceName)
cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"username", user)
cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"password", password)
output, err := io.RunCmdOutput(cmd)
log.Debug("'Add sources' command executed. Output:", output)
return err
stdOut, errorOut, _, err := frogio.RunCmdWithOutputParser(cmd, false)
if err != nil {
return fmt.Errorf("failed to add source: %w\n%s", err, strings.TrimSpace(stdOut+errorOut))
}
return nil
}

// Runs nuget/dotnet source remove command
func RemoveSourceFromNugetConfigIfExists(cmdType dotnet.ToolchainType) error {
cmd, err := dotnet.NewToolchainCmd(cmdType)
if err != nil {
return err
}
if cmdType == dotnet.DotnetCore {
cmd.Command = append(cmd.Command, "nuget", "remove", "source", SourceName)
} else {
cmd.Command = append(cmd.Command, "sources", "remove")
cmd.CommandFlags = append(cmd.CommandFlags, "-name", SourceName)
}
stdOut, stdErr, _, err := frogio.RunCmdWithOutputParser(cmd, false)
if err != nil {
if strings.Contains(stdOut+stdErr, "Unable to find") {
return nil
}
return fmt.Errorf("failed to remove source: %w\n%s", err, strings.TrimSpace(stdOut+stdErr))
}
return nil
}

// Checks if the user provided input such as -configfile flag or -Source flag.
Expand Down Expand Up @@ -266,7 +289,7 @@ func InitNewConfig(configDirPath, repoName string, server *config.ServerDetails,

// Adds a source to the nuget config template
func addSourceToNugetTemplate(configFile *os.File, server *config.ServerDetails, useNugetV2 bool, repoName string) error {
sourceUrl, user, password, err := getSourceDetails(server, repoName, useNugetV2)
sourceUrl, user, password, err := GetSourceDetails(server, repoName, useNugetV2)
if err != nil {
return err
}
Expand All @@ -282,7 +305,7 @@ func addSourceToNugetTemplate(configFile *os.File, server *config.ServerDetails,
return err
}

func getSourceDetails(details *config.ServerDetails, repoName string, useNugetV2 bool) (sourceURL, user, password string, err error) {
func GetSourceDetails(details *config.ServerDetails, repoName string, useNugetV2 bool) (sourceURL, user, password string, err error) {
var u *url.URL
u, err = url.Parse(details.ArtifactoryUrl)
if errorutils.CheckError(err) != nil {
Expand Down
4 changes: 2 additions & 2 deletions artifactory/commands/dotnet/dotnetcommand_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,14 @@ func TestGetSourceDetails(t *testing.T) {
Password: "pass",
}
repoName := "repo-name"
url, user, pass, err := getSourceDetails(server, repoName, false)
url, user, pass, err := GetSourceDetails(server, repoName, false)
assert.NoError(t, err)
assert.Equal(t, "user", user)
assert.Equal(t, "pass", pass)
assert.Equal(t, "https://server.com/artifactory/api/nuget/v3/repo-name", url)
server.Password = ""
server.AccessToken = "abc123"
url, user, pass, err = getSourceDetails(server, repoName, true)
url, user, pass, err = GetSourceDetails(server, repoName, true)
assert.Equal(t, "user", user)
assert.Equal(t, "abc123", pass)
assert.NoError(t, err)
Expand Down
6 changes: 3 additions & 3 deletions artifactory/commands/golang/go.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ func (gc *GoCommand) run() (err error) {
return
}
// If noFallback=false, missing packages will be fetched directly from VCS
repoUrl, err := getArtifactoryRemoteRepoUrl(resolverDetails, gc.resolverParams.TargetRepo(), GoProxyUrlParams{Direct: !gc.noFallback})
repoUrl, err := GetArtifactoryRemoteRepoUrl(resolverDetails, gc.resolverParams.TargetRepo(), GoProxyUrlParams{Direct: !gc.noFallback})
if err != nil {
return
}
Expand Down Expand Up @@ -336,7 +336,7 @@ func SetArtifactoryAsResolutionServer(serverDetails *config.ServerDetails, depsR
}

func setGoProxy(server *config.ServerDetails, remoteGoRepo string, goProxyParams GoProxyUrlParams) error {
repoUrl, err := getArtifactoryRemoteRepoUrl(server, remoteGoRepo, goProxyParams)
repoUrl, err := GetArtifactoryRemoteRepoUrl(server, remoteGoRepo, goProxyParams)
if err != nil {
return err
}
Expand Down Expand Up @@ -380,7 +380,7 @@ func (gdu *GoProxyUrlParams) addDirect(url string) string {
return url
}

func getArtifactoryRemoteRepoUrl(serverDetails *config.ServerDetails, repo string, goProxyParams GoProxyUrlParams) (string, error) {
func GetArtifactoryRemoteRepoUrl(serverDetails *config.ServerDetails, repo string, goProxyParams GoProxyUrlParams) (string, error) {
authServerDetails, err := serverDetails.CreateArtAuthConfig()
if err != nil {
return "", err
Expand Down
2 changes: 1 addition & 1 deletion artifactory/commands/golang/go_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func TestGetArtifactoryRemoteRepoUrl(t *testing.T) {
AccessToken: "eyJ0eXAiOiJKV1QifQ.eyJzdWIiOiJmYWtlXC91c2Vyc1wvdGVzdCJ9.MTIzNDU2Nzg5MA",
}
repoName := "test-repo"
repoUrl, err := getArtifactoryRemoteRepoUrl(server, repoName, GoProxyUrlParams{})
repoUrl, err := GetArtifactoryRemoteRepoUrl(server, repoName, GoProxyUrlParams{})
assert.NoError(t, err)
assert.Equal(t, "https://test:eyJ0eXAiOiJKV1QifQ.eyJzdWIiOiJmYWtlXC91c2Vyc1wvdGVzdCJ9.MTIzNDU2Nzg5MA@server.com/artifactory/api/go/test-repo", repoUrl)
}
Expand Down
241 changes: 241 additions & 0 deletions artifactory/commands/packagemanagerlogin/packagemanagerlogin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
package packagemanagerlogin

import (
"fmt"
bidotnet "github.com/jfrog/build-info-go/build/utils/dotnet"
biutils "github.com/jfrog/build-info-go/utils"
"github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/dotnet"
gocommands "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/golang"
pythoncommands "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/python"
"github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/repository"
commandsutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils"
"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils"
"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/npm"
"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/yarn"
"github.com/jfrog/jfrog-cli-core/v2/common/project"
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
"github.com/jfrog/jfrog-client-go/artifactory/services"
"github.com/jfrog/jfrog-client-go/utils/errorutils"
"github.com/jfrog/jfrog-client-go/utils/log"
)

// PackageManagerLoginCommand configures registries and authentication for various package manager (npm, Yarn, Pip, Pipenv, Poetry, Go)
type PackageManagerLoginCommand struct {
// packageManager represents the type of package manager (e.g., NPM, Yarn).
packageManager project.ProjectType
// repoName is the name of the repository used for configuration.
repoName string
// serverDetails contains Artifactory server configuration.
serverDetails *config.ServerDetails
// commandName specifies the command for this instance.
commandName string
}

// NewPackageManagerLoginCommand initializes a new PackageManagerLoginCommand for the specified package manager
// and automatically sets a command name for the login operation.
func NewPackageManagerLoginCommand(packageManager project.ProjectType) *PackageManagerLoginCommand {
return &PackageManagerLoginCommand{
packageManager: packageManager,
commandName: packageManager.String() + "_login",
}
}

// packageManagerToPackageType maps project types to corresponding Artifactory package types (e.g., npm, pypi).
func packageManagerToPackageType(packageManager project.ProjectType) (string, error) {
switch packageManager {
case project.Npm, project.Yarn:
return repository.Npm, nil
case project.Pip, project.Pipenv, project.Poetry:
return repository.Pypi, nil
case project.Go:
return repository.Go, nil
case project.Nuget, project.Dotnet:
return repository.Nuget, nil
default:
return "", errorutils.CheckErrorf("unsupported package manager: %s", packageManager)
}
}

// CommandName returns the name of the login command.
func (pmlc *PackageManagerLoginCommand) CommandName() string {
return pmlc.commandName
}

// SetServerDetails assigns the server configuration details to the command.
func (pmlc *PackageManagerLoginCommand) SetServerDetails(serverDetails *config.ServerDetails) *PackageManagerLoginCommand {
pmlc.serverDetails = serverDetails
return pmlc
}

// ServerDetails returns the stored server configuration details.
func (pmlc *PackageManagerLoginCommand) ServerDetails() (*config.ServerDetails, error) {
return pmlc.serverDetails, nil
}

// Run executes the configuration method corresponding to the package manager specified for the command.
func (pmlc *PackageManagerLoginCommand) Run() (err error) {
if pmlc.repoName == "" {
// Prompt the user to select a virtual repository that matches the package manager.
if err = pmlc.promptUserToSelectRepository(); err != nil {
return err
}
}

// Configure the appropriate package manager based on the package manager.
switch pmlc.packageManager {
case project.Npm:
err = pmlc.configureNpm()
case project.Yarn:
err = pmlc.configureYarn()
case project.Pip, project.Pipenv:
err = pmlc.configurePip()
case project.Poetry:
err = pmlc.configurePoetry()
case project.Go:
err = pmlc.configureGo()
case project.Nuget, project.Dotnet:
err = pmlc.configureDotnetNuget()
default:
err = errorutils.CheckErrorf("unsupported package manager: %s", pmlc.packageManager)
}
if err != nil {
return fmt.Errorf("failed to configure %s: %w", pmlc.packageManager.String(), err)
}

log.Info(fmt.Sprintf("Successfully configured %s to use JFrog Artifactory repository '%s'.", pmlc.packageManager.String(), pmlc.repoName))
return nil
}

// promptUserToSelectRepository prompts the user to select a compatible virtual repository.
func (pmlc *PackageManagerLoginCommand) promptUserToSelectRepository() error {
// Map the package manager to its corresponding package type.
packageType, err := packageManagerToPackageType(pmlc.packageManager)
if err != nil {
return err
}
repoFilterParams := services.RepositoriesFilterParams{
RepoType: utils.Virtual.String(),
PackageType: packageType,
}

// Prompt for repository selection based on filter parameters.
pmlc.repoName, err = utils.SelectRepositoryInteractively(pmlc.serverDetails, repoFilterParams)
return err
}

// configurePip sets the global index-url for pip and pipenv to use the Artifactory PyPI repository.
// Runs the following command:
//
// pip config set global.index-url https://<user>:<token>@<your-artifactory-url>/artifactory/api/pypi/<repo-name>/simple
func (pmlc *PackageManagerLoginCommand) configurePip() error {
repoWithCredsUrl, err := pythoncommands.GetPypiRepoUrl(pmlc.serverDetails, pmlc.repoName, false)
if err != nil {
return err
}
return pythoncommands.RunConfigCommand(project.Pip, []string{"set", "global.index-url", repoWithCredsUrl})
}

// configurePoetry configures Poetry to use the specified repository and authentication credentials.
// Runs the following commands:
//
// poetry config repositories.<repo-name> https://<your-artifactory-url>/artifactory/api/pypi/<repo-name>/simple
// poetry config http-basic.<repo-name> <user> <password/token>
func (pmlc *PackageManagerLoginCommand) configurePoetry() error {
repoUrl, username, password, err := pythoncommands.GetPypiRepoUrlWithCredentials(pmlc.serverDetails, pmlc.repoName, false)
if err != nil {
return err
}
return pythoncommands.RunPoetryConfig(repoUrl.String(), username, password, pmlc.repoName)
}

// configureNpm configures npm to use the Artifactory repository URL and sets authentication.
// Runs the following commands:
//
// npm config set registry https://<your-artifactory-url>/artifactory/api/npm/<repo-name>
//
// For token-based auth:
//
// npm config set //your-artifactory-url/artifactory/api/npm/<repo-name>/:_authToken "<token>"
//
// For basic auth:
//
// npm config set //your-artifactory-url/artifactory/api/npm/<repo-name>/:_auth "<base64-encoded-username:password>"
func (pmlc *PackageManagerLoginCommand) configureNpm() error {
repoUrl := commandsutils.GetNpmRepositoryUrl(pmlc.repoName, pmlc.serverDetails.ArtifactoryUrl)

if err := npm.ConfigSet(commandsutils.NpmConfigRegistryKey, repoUrl, "npm"); err != nil {
return err
}

authKey, authValue := commandsutils.GetNpmAuthKeyValue(pmlc.serverDetails, repoUrl)
if authKey != "" && authValue != "" {
return npm.ConfigSet(authKey, authValue, "npm")
}
return nil
}

// configureYarn configures Yarn to use the specified Artifactory repository and sets authentication.
// Runs the following commands:
//
// yarn config set registry https://<your-artifactory-url>/artifactory/api/npm/<repo-name>
//
// For token-based auth:
//
// yarn config set //your-artifactory-url/artifactory/api/npm/<repo-name>/:_authToken "<token>"
//
// For basic auth:
//
// yarn config set //your-artifactory-url/artifactory/api/npm/<repo-name>/:_auth "<base64-encoded-username:password>"
func (pmlc *PackageManagerLoginCommand) configureYarn() error {
repoUrl := commandsutils.GetNpmRepositoryUrl(pmlc.repoName, pmlc.serverDetails.ArtifactoryUrl)

if err := yarn.ConfigSet(commandsutils.NpmConfigRegistryKey, repoUrl, "yarn", false); err != nil {
return err
}

authKey, authValue := commandsutils.GetNpmAuthKeyValue(pmlc.serverDetails, repoUrl)
if authKey != "" && authValue != "" {
return yarn.ConfigSet(authKey, authValue, "yarn", false)
}
return nil
}

// configureGo configures Go to use the Artifactory repository for GOPROXY.
// Runs the following command:
//
// go env -w GOPROXY=https://<user>:<token>@<your-artifactory-url>/artifactory/go/<repo-name>,direct
func (pmlc *PackageManagerLoginCommand) configureGo() error {
repoWithCredsUrl, err := gocommands.GetArtifactoryRemoteRepoUrl(pmlc.serverDetails, pmlc.repoName, gocommands.GoProxyUrlParams{Direct: true})
if err != nil {
return err
}
return biutils.RunGo([]string{"env", "-w", "GOPROXY=" + repoWithCredsUrl}, "")
}

// configureDotnetNuget configures NuGet or .NET Core to use the specified Artifactory repository with credentials.
// Adds the repository source to the NuGet configuration file, using appropriate credentials for authentication.
// The following command is run for dotnet:
//
// dotnet nuget add source --name <JFrog-Artifactory> "https://acme.jfrog.io/artifactory/api/nuget/{repository-name}" --username <your-username> --password <your-password>
//
// For NuGet:
//
// nuget sources add -Name <JFrog-Artifactory> -Source "https://acme.jfrog.io/artifactory/api/nuget/{repository-name}" -Username <your-username> -Password <your-password>
func (pmlc *PackageManagerLoginCommand) configureDotnetNuget() error {
// Retrieve repository URL and credentials for NuGet or .NET Core.
sourceUrl, user, password, err := dotnet.GetSourceDetails(pmlc.serverDetails, pmlc.repoName, false)
if err != nil {
return err
}

// Determine the appropriate toolchain type (NuGet or .NET Core).
toolchainType := bidotnet.DotnetCore
if pmlc.packageManager == project.Nuget {
toolchainType = bidotnet.Nuget
}
if err = dotnet.RemoveSourceFromNugetConfigIfExists(toolchainType); err != nil {
return err
}
// Add the repository as a source in the NuGet configuration with credentials for authentication.
return dotnet.AddSourceToNugetConfig(toolchainType, sourceUrl, user, password)
}
Loading

0 comments on commit b6efb65

Please sign in to comment.