Skip to content

Commit b6efb65

Browse files
authored
Package manager login command - Npm, Yarn, Pip, Pipenv, Poetry,Go, Nuget, Dotnet (#1285)
1 parent 24197a7 commit b6efb65

File tree

19 files changed

+918
-126
lines changed

19 files changed

+918
-126
lines changed

artifactory/commands/dotnet/dotnetcommand.go

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"fmt"
66
"github.com/jfrog/build-info-go/build"
77
"github.com/jfrog/build-info-go/build/utils/dotnet"
8-
"github.com/jfrog/gofrog/io"
8+
frogio "github.com/jfrog/gofrog/io"
99
commonBuild "github.com/jfrog/jfrog-cli-core/v2/common/build"
1010
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
1111
"github.com/jfrog/jfrog-client-go/auth"
@@ -19,7 +19,7 @@ import (
1919
)
2020

2121
const (
22-
SourceName = "JFrogCli"
22+
SourceName = "JFrogArtifactory"
2323
configFilePattern = "jfrog.cli.nuget."
2424

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

162-
// Runs nuget sources add command
163-
func AddSourceToNugetConfig(cmdType dotnet.ToolchainType, configFileName, sourceUrl, user, password string) error {
162+
// Runs nuget/dotnet source add command
163+
func AddSourceToNugetConfig(cmdType dotnet.ToolchainType, sourceUrl, user, password string) error {
164164
cmd, err := dotnet.CreateDotnetAddSourceCmd(cmdType, sourceUrl)
165165
if err != nil {
166166
return err
167167
}
168168

169169
flagPrefix := cmdType.GetTypeFlagPrefix()
170-
cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"configfile", configFileName)
171170
cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"name", SourceName)
172171
cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"username", user)
173172
cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"password", password)
174-
output, err := io.RunCmdOutput(cmd)
175-
log.Debug("'Add sources' command executed. Output:", output)
176-
return err
173+
stdOut, errorOut, _, err := frogio.RunCmdWithOutputParser(cmd, false)
174+
if err != nil {
175+
return fmt.Errorf("failed to add source: %w\n%s", err, strings.TrimSpace(stdOut+errorOut))
176+
}
177+
return nil
178+
}
179+
180+
// Runs nuget/dotnet source remove command
181+
func RemoveSourceFromNugetConfigIfExists(cmdType dotnet.ToolchainType) error {
182+
cmd, err := dotnet.NewToolchainCmd(cmdType)
183+
if err != nil {
184+
return err
185+
}
186+
if cmdType == dotnet.DotnetCore {
187+
cmd.Command = append(cmd.Command, "nuget", "remove", "source", SourceName)
188+
} else {
189+
cmd.Command = append(cmd.Command, "sources", "remove")
190+
cmd.CommandFlags = append(cmd.CommandFlags, "-name", SourceName)
191+
}
192+
stdOut, stdErr, _, err := frogio.RunCmdWithOutputParser(cmd, false)
193+
if err != nil {
194+
if strings.Contains(stdOut+stdErr, "Unable to find") {
195+
return nil
196+
}
197+
return fmt.Errorf("failed to remove source: %w\n%s", err, strings.TrimSpace(stdOut+stdErr))
198+
}
199+
return nil
177200
}
178201

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

267290
// Adds a source to the nuget config template
268291
func addSourceToNugetTemplate(configFile *os.File, server *config.ServerDetails, useNugetV2 bool, repoName string) error {
269-
sourceUrl, user, password, err := getSourceDetails(server, repoName, useNugetV2)
292+
sourceUrl, user, password, err := GetSourceDetails(server, repoName, useNugetV2)
270293
if err != nil {
271294
return err
272295
}
@@ -282,7 +305,7 @@ func addSourceToNugetTemplate(configFile *os.File, server *config.ServerDetails,
282305
return err
283306
}
284307

285-
func getSourceDetails(details *config.ServerDetails, repoName string, useNugetV2 bool) (sourceURL, user, password string, err error) {
308+
func GetSourceDetails(details *config.ServerDetails, repoName string, useNugetV2 bool) (sourceURL, user, password string, err error) {
286309
var u *url.URL
287310
u, err = url.Parse(details.ArtifactoryUrl)
288311
if errorutils.CheckError(err) != nil {

artifactory/commands/dotnet/dotnetcommand_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,14 @@ func TestGetSourceDetails(t *testing.T) {
129129
Password: "pass",
130130
}
131131
repoName := "repo-name"
132-
url, user, pass, err := getSourceDetails(server, repoName, false)
132+
url, user, pass, err := GetSourceDetails(server, repoName, false)
133133
assert.NoError(t, err)
134134
assert.Equal(t, "user", user)
135135
assert.Equal(t, "pass", pass)
136136
assert.Equal(t, "https://server.com/artifactory/api/nuget/v3/repo-name", url)
137137
server.Password = ""
138138
server.AccessToken = "abc123"
139-
url, user, pass, err = getSourceDetails(server, repoName, true)
139+
url, user, pass, err = GetSourceDetails(server, repoName, true)
140140
assert.Equal(t, "user", user)
141141
assert.Equal(t, "abc123", pass)
142142
assert.NoError(t, err)

artifactory/commands/golang/go.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ func (gc *GoCommand) run() (err error) {
154154
return
155155
}
156156
// If noFallback=false, missing packages will be fetched directly from VCS
157-
repoUrl, err := getArtifactoryRemoteRepoUrl(resolverDetails, gc.resolverParams.TargetRepo(), GoProxyUrlParams{Direct: !gc.noFallback})
157+
repoUrl, err := GetArtifactoryRemoteRepoUrl(resolverDetails, gc.resolverParams.TargetRepo(), GoProxyUrlParams{Direct: !gc.noFallback})
158158
if err != nil {
159159
return
160160
}
@@ -336,7 +336,7 @@ func SetArtifactoryAsResolutionServer(serverDetails *config.ServerDetails, depsR
336336
}
337337

338338
func setGoProxy(server *config.ServerDetails, remoteGoRepo string, goProxyParams GoProxyUrlParams) error {
339-
repoUrl, err := getArtifactoryRemoteRepoUrl(server, remoteGoRepo, goProxyParams)
339+
repoUrl, err := GetArtifactoryRemoteRepoUrl(server, remoteGoRepo, goProxyParams)
340340
if err != nil {
341341
return err
342342
}
@@ -380,7 +380,7 @@ func (gdu *GoProxyUrlParams) addDirect(url string) string {
380380
return url
381381
}
382382

383-
func getArtifactoryRemoteRepoUrl(serverDetails *config.ServerDetails, repo string, goProxyParams GoProxyUrlParams) (string, error) {
383+
func GetArtifactoryRemoteRepoUrl(serverDetails *config.ServerDetails, repo string, goProxyParams GoProxyUrlParams) (string, error) {
384384
authServerDetails, err := serverDetails.CreateArtAuthConfig()
385385
if err != nil {
386386
return "", err

artifactory/commands/golang/go_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func TestGetArtifactoryRemoteRepoUrl(t *testing.T) {
8686
AccessToken: "eyJ0eXAiOiJKV1QifQ.eyJzdWIiOiJmYWtlXC91c2Vyc1wvdGVzdCJ9.MTIzNDU2Nzg5MA",
8787
}
8888
repoName := "test-repo"
89-
repoUrl, err := getArtifactoryRemoteRepoUrl(server, repoName, GoProxyUrlParams{})
89+
repoUrl, err := GetArtifactoryRemoteRepoUrl(server, repoName, GoProxyUrlParams{})
9090
assert.NoError(t, err)
9191
assert.Equal(t, "https://test:eyJ0eXAiOiJKV1QifQ.eyJzdWIiOiJmYWtlXC91c2Vyc1wvdGVzdCJ9.MTIzNDU2Nzg5MA@server.com/artifactory/api/go/test-repo", repoUrl)
9292
}
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
package packagemanagerlogin
2+
3+
import (
4+
"fmt"
5+
bidotnet "github.com/jfrog/build-info-go/build/utils/dotnet"
6+
biutils "github.com/jfrog/build-info-go/utils"
7+
"github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/dotnet"
8+
gocommands "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/golang"
9+
pythoncommands "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/python"
10+
"github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/repository"
11+
commandsutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils"
12+
"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils"
13+
"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/npm"
14+
"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/yarn"
15+
"github.com/jfrog/jfrog-cli-core/v2/common/project"
16+
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
17+
"github.com/jfrog/jfrog-client-go/artifactory/services"
18+
"github.com/jfrog/jfrog-client-go/utils/errorutils"
19+
"github.com/jfrog/jfrog-client-go/utils/log"
20+
)
21+
22+
// PackageManagerLoginCommand configures registries and authentication for various package manager (npm, Yarn, Pip, Pipenv, Poetry, Go)
23+
type PackageManagerLoginCommand struct {
24+
// packageManager represents the type of package manager (e.g., NPM, Yarn).
25+
packageManager project.ProjectType
26+
// repoName is the name of the repository used for configuration.
27+
repoName string
28+
// serverDetails contains Artifactory server configuration.
29+
serverDetails *config.ServerDetails
30+
// commandName specifies the command for this instance.
31+
commandName string
32+
}
33+
34+
// NewPackageManagerLoginCommand initializes a new PackageManagerLoginCommand for the specified package manager
35+
// and automatically sets a command name for the login operation.
36+
func NewPackageManagerLoginCommand(packageManager project.ProjectType) *PackageManagerLoginCommand {
37+
return &PackageManagerLoginCommand{
38+
packageManager: packageManager,
39+
commandName: packageManager.String() + "_login",
40+
}
41+
}
42+
43+
// packageManagerToPackageType maps project types to corresponding Artifactory package types (e.g., npm, pypi).
44+
func packageManagerToPackageType(packageManager project.ProjectType) (string, error) {
45+
switch packageManager {
46+
case project.Npm, project.Yarn:
47+
return repository.Npm, nil
48+
case project.Pip, project.Pipenv, project.Poetry:
49+
return repository.Pypi, nil
50+
case project.Go:
51+
return repository.Go, nil
52+
case project.Nuget, project.Dotnet:
53+
return repository.Nuget, nil
54+
default:
55+
return "", errorutils.CheckErrorf("unsupported package manager: %s", packageManager)
56+
}
57+
}
58+
59+
// CommandName returns the name of the login command.
60+
func (pmlc *PackageManagerLoginCommand) CommandName() string {
61+
return pmlc.commandName
62+
}
63+
64+
// SetServerDetails assigns the server configuration details to the command.
65+
func (pmlc *PackageManagerLoginCommand) SetServerDetails(serverDetails *config.ServerDetails) *PackageManagerLoginCommand {
66+
pmlc.serverDetails = serverDetails
67+
return pmlc
68+
}
69+
70+
// ServerDetails returns the stored server configuration details.
71+
func (pmlc *PackageManagerLoginCommand) ServerDetails() (*config.ServerDetails, error) {
72+
return pmlc.serverDetails, nil
73+
}
74+
75+
// Run executes the configuration method corresponding to the package manager specified for the command.
76+
func (pmlc *PackageManagerLoginCommand) Run() (err error) {
77+
if pmlc.repoName == "" {
78+
// Prompt the user to select a virtual repository that matches the package manager.
79+
if err = pmlc.promptUserToSelectRepository(); err != nil {
80+
return err
81+
}
82+
}
83+
84+
// Configure the appropriate package manager based on the package manager.
85+
switch pmlc.packageManager {
86+
case project.Npm:
87+
err = pmlc.configureNpm()
88+
case project.Yarn:
89+
err = pmlc.configureYarn()
90+
case project.Pip, project.Pipenv:
91+
err = pmlc.configurePip()
92+
case project.Poetry:
93+
err = pmlc.configurePoetry()
94+
case project.Go:
95+
err = pmlc.configureGo()
96+
case project.Nuget, project.Dotnet:
97+
err = pmlc.configureDotnetNuget()
98+
default:
99+
err = errorutils.CheckErrorf("unsupported package manager: %s", pmlc.packageManager)
100+
}
101+
if err != nil {
102+
return fmt.Errorf("failed to configure %s: %w", pmlc.packageManager.String(), err)
103+
}
104+
105+
log.Info(fmt.Sprintf("Successfully configured %s to use JFrog Artifactory repository '%s'.", pmlc.packageManager.String(), pmlc.repoName))
106+
return nil
107+
}
108+
109+
// promptUserToSelectRepository prompts the user to select a compatible virtual repository.
110+
func (pmlc *PackageManagerLoginCommand) promptUserToSelectRepository() error {
111+
// Map the package manager to its corresponding package type.
112+
packageType, err := packageManagerToPackageType(pmlc.packageManager)
113+
if err != nil {
114+
return err
115+
}
116+
repoFilterParams := services.RepositoriesFilterParams{
117+
RepoType: utils.Virtual.String(),
118+
PackageType: packageType,
119+
}
120+
121+
// Prompt for repository selection based on filter parameters.
122+
pmlc.repoName, err = utils.SelectRepositoryInteractively(pmlc.serverDetails, repoFilterParams)
123+
return err
124+
}
125+
126+
// configurePip sets the global index-url for pip and pipenv to use the Artifactory PyPI repository.
127+
// Runs the following command:
128+
//
129+
// pip config set global.index-url https://<user>:<token>@<your-artifactory-url>/artifactory/api/pypi/<repo-name>/simple
130+
func (pmlc *PackageManagerLoginCommand) configurePip() error {
131+
repoWithCredsUrl, err := pythoncommands.GetPypiRepoUrl(pmlc.serverDetails, pmlc.repoName, false)
132+
if err != nil {
133+
return err
134+
}
135+
return pythoncommands.RunConfigCommand(project.Pip, []string{"set", "global.index-url", repoWithCredsUrl})
136+
}
137+
138+
// configurePoetry configures Poetry to use the specified repository and authentication credentials.
139+
// Runs the following commands:
140+
//
141+
// poetry config repositories.<repo-name> https://<your-artifactory-url>/artifactory/api/pypi/<repo-name>/simple
142+
// poetry config http-basic.<repo-name> <user> <password/token>
143+
func (pmlc *PackageManagerLoginCommand) configurePoetry() error {
144+
repoUrl, username, password, err := pythoncommands.GetPypiRepoUrlWithCredentials(pmlc.serverDetails, pmlc.repoName, false)
145+
if err != nil {
146+
return err
147+
}
148+
return pythoncommands.RunPoetryConfig(repoUrl.String(), username, password, pmlc.repoName)
149+
}
150+
151+
// configureNpm configures npm to use the Artifactory repository URL and sets authentication.
152+
// Runs the following commands:
153+
//
154+
// npm config set registry https://<your-artifactory-url>/artifactory/api/npm/<repo-name>
155+
//
156+
// For token-based auth:
157+
//
158+
// npm config set //your-artifactory-url/artifactory/api/npm/<repo-name>/:_authToken "<token>"
159+
//
160+
// For basic auth:
161+
//
162+
// npm config set //your-artifactory-url/artifactory/api/npm/<repo-name>/:_auth "<base64-encoded-username:password>"
163+
func (pmlc *PackageManagerLoginCommand) configureNpm() error {
164+
repoUrl := commandsutils.GetNpmRepositoryUrl(pmlc.repoName, pmlc.serverDetails.ArtifactoryUrl)
165+
166+
if err := npm.ConfigSet(commandsutils.NpmConfigRegistryKey, repoUrl, "npm"); err != nil {
167+
return err
168+
}
169+
170+
authKey, authValue := commandsutils.GetNpmAuthKeyValue(pmlc.serverDetails, repoUrl)
171+
if authKey != "" && authValue != "" {
172+
return npm.ConfigSet(authKey, authValue, "npm")
173+
}
174+
return nil
175+
}
176+
177+
// configureYarn configures Yarn to use the specified Artifactory repository and sets authentication.
178+
// Runs the following commands:
179+
//
180+
// yarn config set registry https://<your-artifactory-url>/artifactory/api/npm/<repo-name>
181+
//
182+
// For token-based auth:
183+
//
184+
// yarn config set //your-artifactory-url/artifactory/api/npm/<repo-name>/:_authToken "<token>"
185+
//
186+
// For basic auth:
187+
//
188+
// yarn config set //your-artifactory-url/artifactory/api/npm/<repo-name>/:_auth "<base64-encoded-username:password>"
189+
func (pmlc *PackageManagerLoginCommand) configureYarn() error {
190+
repoUrl := commandsutils.GetNpmRepositoryUrl(pmlc.repoName, pmlc.serverDetails.ArtifactoryUrl)
191+
192+
if err := yarn.ConfigSet(commandsutils.NpmConfigRegistryKey, repoUrl, "yarn", false); err != nil {
193+
return err
194+
}
195+
196+
authKey, authValue := commandsutils.GetNpmAuthKeyValue(pmlc.serverDetails, repoUrl)
197+
if authKey != "" && authValue != "" {
198+
return yarn.ConfigSet(authKey, authValue, "yarn", false)
199+
}
200+
return nil
201+
}
202+
203+
// configureGo configures Go to use the Artifactory repository for GOPROXY.
204+
// Runs the following command:
205+
//
206+
// go env -w GOPROXY=https://<user>:<token>@<your-artifactory-url>/artifactory/go/<repo-name>,direct
207+
func (pmlc *PackageManagerLoginCommand) configureGo() error {
208+
repoWithCredsUrl, err := gocommands.GetArtifactoryRemoteRepoUrl(pmlc.serverDetails, pmlc.repoName, gocommands.GoProxyUrlParams{Direct: true})
209+
if err != nil {
210+
return err
211+
}
212+
return biutils.RunGo([]string{"env", "-w", "GOPROXY=" + repoWithCredsUrl}, "")
213+
}
214+
215+
// configureDotnetNuget configures NuGet or .NET Core to use the specified Artifactory repository with credentials.
216+
// Adds the repository source to the NuGet configuration file, using appropriate credentials for authentication.
217+
// The following command is run for dotnet:
218+
//
219+
// dotnet nuget add source --name <JFrog-Artifactory> "https://acme.jfrog.io/artifactory/api/nuget/{repository-name}" --username <your-username> --password <your-password>
220+
//
221+
// For NuGet:
222+
//
223+
// nuget sources add -Name <JFrog-Artifactory> -Source "https://acme.jfrog.io/artifactory/api/nuget/{repository-name}" -Username <your-username> -Password <your-password>
224+
func (pmlc *PackageManagerLoginCommand) configureDotnetNuget() error {
225+
// Retrieve repository URL and credentials for NuGet or .NET Core.
226+
sourceUrl, user, password, err := dotnet.GetSourceDetails(pmlc.serverDetails, pmlc.repoName, false)
227+
if err != nil {
228+
return err
229+
}
230+
231+
// Determine the appropriate toolchain type (NuGet or .NET Core).
232+
toolchainType := bidotnet.DotnetCore
233+
if pmlc.packageManager == project.Nuget {
234+
toolchainType = bidotnet.Nuget
235+
}
236+
if err = dotnet.RemoveSourceFromNugetConfigIfExists(toolchainType); err != nil {
237+
return err
238+
}
239+
// Add the repository as a source in the NuGet configuration with credentials for authentication.
240+
return dotnet.AddSourceToNugetConfig(toolchainType, sourceUrl, user, password)
241+
}

0 commit comments

Comments
 (0)