diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5765ee044..4977965ef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,6 +38,11 @@ jobs: with: dotnet-version: '6.x' + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + # Install Mono on Ubuntu to run nuget.exe (due to this issue on Ubuntu 24 that hasn't been fixed yet - https://github.com/NuGet/setup-nuget/issues/168) - name: Install Mono on Ubuntu if: matrix.os == 'ubuntu' diff --git a/artifactory/commands/dotnet/dotnetcommand.go b/artifactory/commands/dotnet/dotnetcommand.go index 57cd73d4f..6c72ff6a3 100644 --- a/artifactory/commands/dotnet/dotnetcommand.go +++ b/artifactory/commands/dotnet/dotnetcommand.go @@ -15,6 +15,7 @@ import ( "net/url" "os" "path" + "path/filepath" "strings" ) @@ -168,7 +169,7 @@ func changeWorkingDir(newWorkingDir string) (string, error) { } // Runs nuget/dotnet source add command -func AddSourceToNugetConfig(cmdType dotnet.ToolchainType, sourceUrl, user, password string) error { +func AddSourceToNugetConfig(cmdType dotnet.ToolchainType, sourceUrl, user, password, customConfigPath string) error { cmd, err := dotnet.CreateDotnetAddSourceCmd(cmdType, sourceUrl) if err != nil { return err @@ -178,15 +179,21 @@ func AddSourceToNugetConfig(cmdType dotnet.ToolchainType, sourceUrl, user, passw cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"name", SourceName) cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"username", user) cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"password", password) - stdOut, errorOut, _, err := frogio.RunCmdWithOutputParser(cmd, false) + + if customConfigPath != "" { + addConfigFileFlag(cmd, customConfigPath) + } + + _, _, _, err = frogio.RunCmdWithOutputParser(cmd, false) if err != nil { - return fmt.Errorf("failed to add source: %w\n%s", err, strings.TrimSpace(stdOut+errorOut)) + return fmt.Errorf("failed to add source: %w", err) } return nil } -// Runs nuget/dotnet source remove command -func RemoveSourceFromNugetConfigIfExists(cmdType dotnet.ToolchainType) error { +// RemoveSourceFromNugetConfigIfExists runs the nuget/dotnet source remove command. +// Removes the source if it exists in the configuration. +func RemoveSourceFromNugetConfigIfExists(cmdType dotnet.ToolchainType, customConfigPath string) error { cmd, err := dotnet.NewToolchainCmd(cmdType) if err != nil { return err @@ -197,16 +204,55 @@ func RemoveSourceFromNugetConfigIfExists(cmdType dotnet.ToolchainType) error { cmd.Command = append(cmd.Command, "sources", "remove") cmd.CommandFlags = append(cmd.CommandFlags, "-name", SourceName) } + + if customConfigPath != "" { + addConfigFileFlag(cmd, customConfigPath) + } + stdOut, stdErr, _, err := frogio.RunCmdWithOutputParser(cmd, false) if err != nil { - if strings.Contains(stdOut+stdErr, "Unable to find") { + if strings.Contains(stdOut+stdErr, "Unable to find") || strings.Contains(stdOut+stdErr, "does not exist") { return nil } - return fmt.Errorf("failed to remove source: %w\n%s", err, strings.TrimSpace(stdOut+stdErr)) + return errorutils.CheckErrorf("failed to remove source: %s", err.Error()) } return nil } +// GetConfigPathFromEnvIfProvided returns the path to the custom NuGet.Config file if it was provided by the user. +func GetConfigPathFromEnvIfProvided(cmdType dotnet.ToolchainType) string { + if cmdType == dotnet.DotnetCore { + if customDotnetDir := os.Getenv("DOTNET_CLI_HOME"); customDotnetDir != "" { + return filepath.Join(customDotnetDir, "NuGet.Config") + } + } + return os.Getenv("NUGET_CONFIG_FILE") +} + +// CreateConfigFileIfNeeded creates a new config file if it does not exist. +func CreateConfigFileIfNeeded(customConfigPath string) error { + // Ensure the file exists + exists, err := fileutils.IsFileExists(customConfigPath, false) + if err != nil || exists { + return err + } + // If the file does not exist, create it + if err = os.MkdirAll(filepath.Dir(customConfigPath), 0755); err != nil { + return err + } + // Write the default config content to the file + return os.WriteFile(customConfigPath, []byte(""), 0644) +} + +func addConfigFileFlag(cmd *dotnet.Cmd, configFilePath string) { + // Add the config file flag if needed. + if cmd.GetToolchain() == dotnet.DotnetCore { + cmd.CommandFlags = append(cmd.CommandFlags, "--configfile", configFilePath) + } else { + cmd.CommandFlags = append(cmd.CommandFlags, "-ConfigFile", configFilePath) + } +} + // Checks if the user provided input such as -configfile flag or -Source flag. // If those flags were provided, NuGet will use the provided configs (default config file or the one with -configfile) // If neither provided, we are initializing our own config. diff --git a/artifactory/commands/dotnet/dotnetcommand_test.go b/artifactory/commands/dotnet/dotnetcommand_test.go index f64af2bbd..223486e4a 100644 --- a/artifactory/commands/dotnet/dotnetcommand_test.go +++ b/artifactory/commands/dotnet/dotnetcommand_test.go @@ -1,6 +1,7 @@ package dotnet import ( + "github.com/jfrog/jfrog-cli-core/v2/utils/ioutils" "os" "path/filepath" "reflect" @@ -223,3 +224,124 @@ func createNewDotnetModule(t *testing.T, tmpDir string) *build.DotnetModule { assert.NoError(t, err) return module } + +func TestGetConfigPathFromEnvIfProvided(t *testing.T) { + testCases := []struct { + name string + mockEnv map[string]string + cmdType dotnet.ToolchainType + expectedPath string + }{ + { + name: "DotnetCore with DOTNET_CLI_HOME", + mockEnv: map[string]string{ + "DOTNET_CLI_HOME": "/custom/dotnet", + }, + cmdType: dotnet.DotnetCore, + expectedPath: "/custom/dotnet/NuGet.Config", + }, + { + name: "NuGet with NUGET_CONFIG_FILE", + mockEnv: map[string]string{ + "NUGET_CONFIG_FILE": "/custom/nuget.config", + }, + cmdType: dotnet.Nuget, + expectedPath: "/custom/nuget.config", + }, + { + name: "No env variable", + mockEnv: map[string]string{}, + cmdType: dotnet.Nuget, + expectedPath: "", + }, + } + + // Test the function with different environment variable settings + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + t.Setenv("DOTNET_CLI_HOME", testCase.mockEnv["DOTNET_CLI_HOME"]) + + // Set other environment variables if needed + if testCase.mockEnv["NUGET_CONFIG_FILE"] != "" { + t.Setenv("NUGET_CONFIG_FILE", testCase.mockEnv["NUGET_CONFIG_FILE"]) + } + result := GetConfigPathFromEnvIfProvided(testCase.cmdType) + assert.Equal(t, testCase.expectedPath, ioutils.WinToUnixPathSeparator(result)) + }) + } +} + +func TestCreateConfigFileIfNeeded(t *testing.T) { + testCases := []struct { + name string + configPath string + fileExists bool + expectedError error + }{ + { + name: "File does not exist, create file with default content", + configPath: "/custom/path/NuGet.Config", + fileExists: false, + }, + { + name: "File exists, no changes", + configPath: "/custom/path/NuGet.Config", + fileExists: true, + }, + } + + // Setup for testing file existence and creation + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + configPath := filepath.Join(t.TempDir(), testCase.configPath) + if testCase.fileExists { + assert.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0777)) + assert.NoError(t, os.WriteFile(configPath, []byte{}, 0644)) + } + err := CreateConfigFileIfNeeded(configPath) + assert.NoError(t, err) + + if !testCase.fileExists { + // Read the content of the file + content, err := os.ReadFile(configPath) + assert.NoError(t, err) + + // Assert the content is the default config content + assert.Equal(t, "", string(content)) + } + }) + } +} + +func TestAddConfigFileFlag(t *testing.T) { + testCases := []struct { + name string + toolchainType dotnet.ToolchainType + expectedFlags []string + }{ + { + name: "DotnetCore toolchain", + toolchainType: dotnet.DotnetCore, + expectedFlags: []string{"--configfile", "/path/to/NuGet.Config"}, + }, + { + name: "NuGet toolchain", + toolchainType: dotnet.Nuget, + expectedFlags: []string{"-ConfigFile", "/path/to/NuGet.Config"}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + // Create a mock command object + cmd, err := dotnet.NewToolchainCmd(testCase.toolchainType) + assert.NoError(t, err) + + // Call the function + addConfigFileFlag(cmd, "/path/to/NuGet.Config") + + // Assert that the flags are as expected + assert.Equal(t, testCase.expectedFlags, cmd.CommandFlags) + }) + } +} diff --git a/artifactory/commands/golang/go_test.go b/artifactory/commands/golang/go_test.go index 7b334a3fe..8fc86ad9e 100644 --- a/artifactory/commands/golang/go_test.go +++ b/artifactory/commands/golang/go_test.go @@ -128,7 +128,7 @@ func TestGetArtifactoryApiUrl(t *testing.T) { } func TestGoProxyUrlParams_BuildUrl(t *testing.T) { - tests := []struct { + testCases := []struct { name string RepoName string Direct bool @@ -153,15 +153,15 @@ func TestGoProxyUrlParams_BuildUrl(t *testing.T) { ExpectedUrl: "https://test/prefix/api/go/go", }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { remoteUrl, err := url.Parse("https://test") require.NoError(t, err) gdu := &GoProxyUrlParams{ - Direct: tt.Direct, - EndpointPrefix: tt.EndpointPrefix, + Direct: testCase.Direct, + EndpointPrefix: testCase.EndpointPrefix, } - assert.Equalf(t, tt.ExpectedUrl, gdu.BuildUrl(remoteUrl, tt.RepoName), "BuildUrl(%v, %v)", remoteUrl, tt.RepoName) + assert.Equalf(t, testCase.ExpectedUrl, gdu.BuildUrl(remoteUrl, testCase.RepoName), "BuildUrl(%v, %v)", remoteUrl, testCase.RepoName) }) } } diff --git a/artifactory/commands/python/pip.go b/artifactory/commands/python/pip.go index 8b7b2267a..ca6ac8dee 100644 --- a/artifactory/commands/python/pip.go +++ b/artifactory/commands/python/pip.go @@ -1,8 +1,11 @@ package python import ( + "fmt" "io" + "os" "os/exec" + "path/filepath" "github.com/jfrog/build-info-go/entities" "github.com/jfrog/build-info-go/utils/pythonutils" @@ -46,6 +49,15 @@ func (pc *PipCommand) SetCommandName(commandName string) *PipCommand { return pc } +func CreatePipConfigManually(customPipConfigPath, repoWithCredsUrl string) error { + if err := os.MkdirAll(filepath.Dir(customPipConfigPath), os.ModePerm); err != nil { + return err + } + // Write the configuration to pip.conf. + configContent := fmt.Sprintf("[global]\nindex-url = %s\n", repoWithCredsUrl) + return os.WriteFile(customPipConfigPath, []byte(configContent), 0644) +} + func (pc *PipCommand) CommandName() string { return "rt_python_pip" } diff --git a/artifactory/commands/python/pip_test.go b/artifactory/commands/python/pip_test.go new file mode 100644 index 000000000..92079ada6 --- /dev/null +++ b/artifactory/commands/python/pip_test.go @@ -0,0 +1,27 @@ +package python + +import ( + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "testing" +) + +func TestCreatePipConfigManually(t *testing.T) { + // Define the test parameters + customConfigPath := filepath.Join(t.TempDir(), "/tmp/test/pip.conf") + // #nosec G101 -- False positive - no hardcoded credentials. + repoWithCredsUrl := "https://example.com/simple/" + expectedContent := "[global]\nindex-url = https://example.com/simple/\n" + + // Call the function under test + err := CreatePipConfigManually(customConfigPath, repoWithCredsUrl) + + // Assert no error occurred + assert.NoError(t, err) + + // Verify the file exists and has the correct content + fileContent, err := os.ReadFile(customConfigPath) + assert.NoError(t, err) + assert.Equal(t, expectedContent, string(fileContent)) +} diff --git a/artifactory/commands/python/python_test.go b/artifactory/commands/python/python_test.go index 2b25468c9..18666ce45 100644 --- a/artifactory/commands/python/python_test.go +++ b/artifactory/commands/python/python_test.go @@ -10,7 +10,7 @@ import ( ) func TestGetPypiRepoUrlWithCredentials(t *testing.T) { - tests := []struct { + testCases := []struct { name string curationCmd bool }{ @@ -24,11 +24,11 @@ func TestGetPypiRepoUrlWithCredentials(t *testing.T) { }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - url, _, _, err := GetPypiRepoUrlWithCredentials(&config.ServerDetails{}, "test", tt.curationCmd) + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + url, _, _, err := GetPypiRepoUrlWithCredentials(&config.ServerDetails{}, "test", testCase.curationCmd) require.NoError(t, err) - assert.Equal(t, tt.curationCmd, strings.Contains(url.Path, coreutils.CurationPassThroughApi)) + assert.Equal(t, testCase.curationCmd, strings.Contains(url.Path, coreutils.CurationPassThroughApi)) }) } } diff --git a/artifactory/commands/packagemanagerlogin/packagemanagerlogin.go b/artifactory/commands/setup/setup.go similarity index 58% rename from artifactory/commands/packagemanagerlogin/packagemanagerlogin.go rename to artifactory/commands/setup/setup.go index e462724a2..213d8f62f 100644 --- a/artifactory/commands/packagemanagerlogin/packagemanagerlogin.go +++ b/artifactory/commands/setup/setup.go @@ -1,4 +1,4 @@ -package packagemanagerlogin +package setup import ( "fmt" @@ -21,6 +21,7 @@ import ( "github.com/jfrog/jfrog-client-go/utils/log" "golang.org/x/exp/maps" "net/url" + "os" "slices" ) @@ -28,6 +29,7 @@ import ( var packageManagerToRepositoryPackageType = map[project.ProjectType]string{ // Npm package managers project.Npm: repository.Npm, + project.Pnpm: repository.Npm, project.Yarn: repository.Npm, // Python (pypi) package managers @@ -46,8 +48,8 @@ var packageManagerToRepositoryPackageType = map[project.ProjectType]string{ project.Go: repository.Go, } -// PackageManagerLoginCommand configures registries and authentication for various package manager (npm, Yarn, Pip, Pipenv, Poetry, Go) -type PackageManagerLoginCommand struct { +// SetupCommand configures registries and authentication for various package manager (npm, Yarn, Pip, Pipenv, Poetry, Go) +type SetupCommand 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. @@ -60,23 +62,27 @@ type PackageManagerLoginCommand struct { 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{ +// NewSetupCommand initializes a new SetupCommand for the specified package manager +func NewSetupCommand(packageManager project.ProjectType) *SetupCommand { + return &SetupCommand{ packageManager: packageManager, - commandName: packageManager.String() + "_login", + commandName: "setup_" + packageManager.String(), } } -// GetSupportedPackageManagersList returns a sorted list of supported package managers. -func GetSupportedPackageManagersList() []project.ProjectType { +// GetSupportedPackageManagersList returns a sorted list of supported package manager names as strings. +func GetSupportedPackageManagersList() []string { allSupportedPackageManagers := maps.Keys(packageManagerToRepositoryPackageType) // Sort keys based on their natural enum order slices.SortFunc(allSupportedPackageManagers, func(a, b project.ProjectType) int { return int(a) - int(b) }) - return allSupportedPackageManagers + // Convert enums to their string representation + result := make([]string, len(allSupportedPackageManagers)) + for i, manager := range allSupportedPackageManagers { + result[i] = manager.String() + } + return result } func IsSupportedPackageManager(packageManager project.ProjectType) bool { @@ -85,83 +91,87 @@ func IsSupportedPackageManager(packageManager project.ProjectType) bool { } // CommandName returns the name of the login command. -func (pmlc *PackageManagerLoginCommand) CommandName() string { - return pmlc.commandName +func (sc *SetupCommand) CommandName() string { + return sc.commandName } // SetServerDetails assigns the server configuration details to the command. -func (pmlc *PackageManagerLoginCommand) SetServerDetails(serverDetails *config.ServerDetails) *PackageManagerLoginCommand { - pmlc.serverDetails = serverDetails - return pmlc +func (sc *SetupCommand) SetServerDetails(serverDetails *config.ServerDetails) *SetupCommand { + sc.serverDetails = serverDetails + return sc } // ServerDetails returns the stored server configuration details. -func (pmlc *PackageManagerLoginCommand) ServerDetails() (*config.ServerDetails, error) { - return pmlc.serverDetails, nil +func (sc *SetupCommand) ServerDetails() (*config.ServerDetails, error) { + return sc.serverDetails, nil } // SetRepoName assigns the repository name to the command. -func (pmlc *PackageManagerLoginCommand) SetRepoName(repoName string) *PackageManagerLoginCommand { - pmlc.repoName = repoName - return pmlc +func (sc *SetupCommand) SetRepoName(repoName string) *SetupCommand { + sc.repoName = repoName + return sc } // SetProjectKey assigns the project key to the command. -func (pmlc *PackageManagerLoginCommand) SetProjectKey(projectKey string) *PackageManagerLoginCommand { - pmlc.projectKey = projectKey - return pmlc +func (sc *SetupCommand) SetProjectKey(projectKey string) *SetupCommand { + sc.projectKey = projectKey + return sc } // Run executes the configuration method corresponding to the package manager specified for the command. -func (pmlc *PackageManagerLoginCommand) Run() (err error) { - if !IsSupportedPackageManager(pmlc.packageManager) { - return errorutils.CheckErrorf("unsupported package manager: %s", pmlc.packageManager) +func (sc *SetupCommand) Run() (err error) { + if !IsSupportedPackageManager(sc.packageManager) { + return errorutils.CheckErrorf("unsupported package manager: %s", sc.packageManager) } - if pmlc.repoName == "" { + if sc.repoName == "" { // Prompt the user to select a virtual repository that matches the package manager. - if err = pmlc.promptUserToSelectRepository(); err != nil { + if err = sc.promptUserToSelectRepository(); err != nil { return err } } // Configure the appropriate package manager based on the package manager. - switch pmlc.packageManager { - case project.Npm: - err = pmlc.configureNpm() + switch sc.packageManager { + case project.Npm, project.Pnpm: + err = sc.configureNpmPnpm() case project.Yarn: - err = pmlc.configureYarn() + err = sc.configureYarn() case project.Pip, project.Pipenv: - err = pmlc.configurePip() + err = sc.configurePip() case project.Poetry: - err = pmlc.configurePoetry() + err = sc.configurePoetry() case project.Go: - err = pmlc.configureGo() + err = sc.configureGo() case project.Nuget, project.Dotnet: - err = pmlc.configureDotnetNuget() + err = sc.configureDotnetNuget() case project.Docker, project.Podman: - err = pmlc.configureContainer() + err = sc.configureContainer() default: - err = errorutils.CheckErrorf("unsupported package manager: %s", pmlc.packageManager) + err = errorutils.CheckErrorf("unsupported package manager: %s", sc.packageManager) } if err != nil { - return fmt.Errorf("failed to configure %s: %w", pmlc.packageManager.String(), err) + return fmt.Errorf("failed to configure %s: %w", sc.packageManager.String(), err) } - log.Output(fmt.Sprintf("Successfully configured %s to use JFrog Artifactory repository '%s'.", coreutils.PrintBoldTitle(pmlc.packageManager.String()), coreutils.PrintBoldTitle(pmlc.repoName))) + log.Output(fmt.Sprintf("Successfully configured %s to use JFrog Artifactory repository '%s'.", coreutils.PrintBoldTitle(sc.packageManager.String()), coreutils.PrintBoldTitle(sc.repoName))) return nil } // promptUserToSelectRepository prompts the user to select a compatible virtual repository. -func (pmlc *PackageManagerLoginCommand) promptUserToSelectRepository() (err error) { +func (sc *SetupCommand) promptUserToSelectRepository() (err error) { repoFilterParams := services.RepositoriesFilterParams{ RepoType: utils.Virtual.String(), - PackageType: packageManagerToRepositoryPackageType[pmlc.packageManager], - ProjectKey: pmlc.projectKey, + PackageType: packageManagerToRepositoryPackageType[sc.packageManager], + ProjectKey: sc.projectKey, } // Prompt for repository selection based on filter parameters. - pmlc.repoName, err = utils.SelectRepositoryInteractively(pmlc.serverDetails, repoFilterParams) + sc.repoName, err = utils.SelectRepositoryInteractively( + sc.serverDetails, + repoFilterParams, + fmt.Sprintf("To configure %s, we need you to select a %s repository in Artifactory", repoFilterParams.PackageType, repoFilterParams.RepoType)) + return err } @@ -169,11 +179,18 @@ func (pmlc *PackageManagerLoginCommand) promptUserToSelectRepository() (err erro // Runs the following command: // // pip config set global.index-url https://:@/artifactory/api/pypi//simple -func (pmlc *PackageManagerLoginCommand) configurePip() error { - repoWithCredsUrl, err := pythoncommands.GetPypiRepoUrl(pmlc.serverDetails, pmlc.repoName, false) +// +// Note: Custom configuration file can be set by setting the PIP_CONFIG_FILE environment variable. +func (sc *SetupCommand) configurePip() error { + repoWithCredsUrl, err := pythoncommands.GetPypiRepoUrl(sc.serverDetails, sc.repoName, false) if err != nil { return err } + // If PIP_CONFIG_FILE is set, write the configuration to the custom config file manually. + // Using 'pip config set' native command is not supported together with PIP_CONFIG_FILE. + if customPipConfigPath := os.Getenv("PIP_CONFIG_FILE"); customPipConfigPath != "" { + return pythoncommands.CreatePipConfigManually(customPipConfigPath, repoWithCredsUrl) + } return pythoncommands.RunConfigCommand(project.Pip, []string{"set", "global.index-url", repoWithCredsUrl}) } @@ -182,36 +199,39 @@ func (pmlc *PackageManagerLoginCommand) configurePip() error { // // poetry config repositories. https:///artifactory/api/pypi//simple // poetry config http-basic. -func (pmlc *PackageManagerLoginCommand) configurePoetry() error { - repoUrl, username, password, err := pythoncommands.GetPypiRepoUrlWithCredentials(pmlc.serverDetails, pmlc.repoName, false) +// +// Note: Custom configuration file can be set by setting the POETRY_CONFIG_DIR environment variable. +func (sc *SetupCommand) configurePoetry() error { + repoUrl, username, password, err := pythoncommands.GetPypiRepoUrlWithCredentials(sc.serverDetails, sc.repoName, false) if err != nil { return err } - return pythoncommands.RunPoetryConfig(repoUrl.String(), username, password, pmlc.repoName) + return pythoncommands.RunPoetryConfig(repoUrl.String(), username, password, sc.repoName) } -// configureNpm configures npm to use the Artifactory repository URL and sets authentication. +// configureNpmPnpm configures npm to use the Artifactory repository URL and sets authentication. Pnpm supports the same commands. // Runs the following commands: // -// npm config set registry https:///artifactory/api/npm/ +// npm/pnpm config set registry https:///artifactory/api/npm/ // // For token-based auth: // -// npm config set //your-artifactory-url/artifactory/api/npm//:_authToken "" +// npm/pnpm config set //your-artifactory-url/artifactory/api/npm//:_authToken "" // // For basic auth: // -// npm config set //your-artifactory-url/artifactory/api/npm//:_auth "" -func (pmlc *PackageManagerLoginCommand) configureNpm() error { - repoUrl := commandsutils.GetNpmRepositoryUrl(pmlc.repoName, pmlc.serverDetails.ArtifactoryUrl) - - if err := npm.ConfigSet(commandsutils.NpmConfigRegistryKey, repoUrl, "npm"); err != nil { +// npm/pnpm config set //your-artifactory-url/artifactory/api/npm//:_auth "" +// +// Note: Custom configuration file can be set by setting the NPM_CONFIG_USERCONFIG environment variable. +func (sc *SetupCommand) configureNpmPnpm() error { + repoUrl := commandsutils.GetNpmRepositoryUrl(sc.repoName, sc.serverDetails.ArtifactoryUrl) + if err := npm.ConfigSet(commandsutils.NpmConfigRegistryKey, repoUrl, sc.packageManager.String()); err != nil { return err } - authKey, authValue := commandsutils.GetNpmAuthKeyValue(pmlc.serverDetails, repoUrl) + authKey, authValue := commandsutils.GetNpmAuthKeyValue(sc.serverDetails, repoUrl) if authKey != "" && authValue != "" { - return npm.ConfigSet(authKey, authValue, "npm") + return npm.ConfigSet(authKey, authValue, sc.packageManager.String()) } return nil } @@ -228,14 +248,15 @@ func (pmlc *PackageManagerLoginCommand) configureNpm() error { // For basic auth: // // yarn config set //your-artifactory-url/artifactory/api/npm//:_auth "" -func (pmlc *PackageManagerLoginCommand) configureYarn() error { - repoUrl := commandsutils.GetNpmRepositoryUrl(pmlc.repoName, pmlc.serverDetails.ArtifactoryUrl) - - if err := yarn.ConfigSet(commandsutils.NpmConfigRegistryKey, repoUrl, "yarn", false); err != nil { +// +// Note: Custom configuration file can be set by setting the YARN_RC_FILENAME environment variable. +func (sc *SetupCommand) configureYarn() (err error) { + repoUrl := commandsutils.GetNpmRepositoryUrl(sc.repoName, sc.serverDetails.ArtifactoryUrl) + if err = yarn.ConfigSet(commandsutils.NpmConfigRegistryKey, repoUrl, "yarn", false); err != nil { return err } - authKey, authValue := commandsutils.GetNpmAuthKeyValue(pmlc.serverDetails, repoUrl) + authKey, authValue := commandsutils.GetNpmAuthKeyValue(sc.serverDetails, repoUrl) if authKey != "" && authValue != "" { return yarn.ConfigSet(authKey, authValue, "yarn", false) } @@ -246,8 +267,8 @@ func (pmlc *PackageManagerLoginCommand) configureYarn() error { // Runs the following command: // // go env -w GOPROXY=https://:@/artifactory/go/,direct -func (pmlc *PackageManagerLoginCommand) configureGo() error { - repoWithCredsUrl, err := gocommands.GetArtifactoryRemoteRepoUrl(pmlc.serverDetails, pmlc.repoName, gocommands.GoProxyUrlParams{Direct: true}) +func (sc *SetupCommand) configureGo() error { + repoWithCredsUrl, err := gocommands.GetArtifactoryRemoteRepoUrl(sc.serverDetails, sc.repoName, gocommands.GoProxyUrlParams{Direct: true}) if err != nil { return err } @@ -263,23 +284,37 @@ func (pmlc *PackageManagerLoginCommand) configureGo() error { // For NuGet: // // nuget sources add -Name -Source "https://acme.jfrog.io/artifactory/api/nuget/{repository-name}" -Username -Password -func (pmlc *PackageManagerLoginCommand) configureDotnetNuget() error { +// +// Note: Custom dotnet/nuget configuration file can be set by setting the DOTNET_CLI_HOME/NUGET_CONFIG_FILE environment variable. +func (sc *SetupCommand) configureDotnetNuget() error { // Retrieve repository URL and credentials for NuGet or .NET Core. - sourceUrl, user, password, err := dotnet.GetSourceDetails(pmlc.serverDetails, pmlc.repoName, false) + sourceUrl, user, password, err := dotnet.GetSourceDetails(sc.serverDetails, sc.repoName, false) if err != nil { return err } - // Determine the appropriate toolchain type (NuGet or .NET Core). + // Determine toolchain type based on the package manager toolchainType := bidotnet.DotnetCore - if pmlc.packageManager == project.Nuget { + if sc.packageManager == project.Nuget { toolchainType = bidotnet.Nuget } - if err = dotnet.RemoveSourceFromNugetConfigIfExists(toolchainType); err != nil { + + // Get config path from the environment if provided + customConfigPath := dotnet.GetConfigPathFromEnvIfProvided(toolchainType) + if customConfigPath != "" { + // Ensure the config file exists + if err = dotnet.CreateConfigFileIfNeeded(customConfigPath); err != nil { + return err + } + } + + // Remove existing source if it exists + if err = dotnet.RemoveSourceFromNugetConfigIfExists(toolchainType, customConfigPath); 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) + + // Add the repository as a source in the NuGet configuration with credentials for authentication + return dotnet.AddSourceToNugetConfig(toolchainType, sourceUrl, user, password, customConfigPath) } // configureContainer configures container managers like Docker or Podman to authenticate with JFrog Artifactory. @@ -292,25 +327,25 @@ func (pmlc *PackageManagerLoginCommand) configureDotnetNuget() error { // For Podman: // // echo | podman login -u --password-stdin -func (pmlc *PackageManagerLoginCommand) configureContainer() error { +func (sc *SetupCommand) configureContainer() error { var containerManagerType container.ContainerManagerType - switch pmlc.packageManager { + switch sc.packageManager { case project.Docker: containerManagerType = container.DockerClient case project.Podman: containerManagerType = container.Podman default: - return errorutils.CheckErrorf("unsupported container manager: %s", pmlc.packageManager) + return errorutils.CheckErrorf("unsupported container manager: %s", sc.packageManager) } // Parse the URL to remove the scheme (https:// or http://) - parsedPlatformURL, err := url.Parse(pmlc.serverDetails.GetUrl()) + parsedPlatformURL, err := url.Parse(sc.serverDetails.GetUrl()) if err != nil { return err } urlWithoutScheme := parsedPlatformURL.Host + parsedPlatformURL.Path return container.ContainerManagerLogin( urlWithoutScheme, - &container.ContainerManagerLoginConfig{ServerDetails: pmlc.serverDetails}, + &container.ContainerManagerLoginConfig{ServerDetails: sc.serverDetails}, containerManagerType, ) } diff --git a/artifactory/commands/packagemanagerlogin/packagemanagerlogin_test.go b/artifactory/commands/setup/setup_test.go similarity index 76% rename from artifactory/commands/packagemanagerlogin/packagemanagerlogin_test.go rename to artifactory/commands/setup/setup_test.go index 9783e8133..7fffd7724 100644 --- a/artifactory/commands/packagemanagerlogin/packagemanagerlogin_test.go +++ b/artifactory/commands/setup/setup_test.go @@ -1,4 +1,4 @@ -package packagemanagerlogin +package setup import ( "fmt" @@ -9,8 +9,6 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/utils/ioutils" "github.com/jfrog/jfrog-client-go/auth" - "github.com/jfrog/jfrog-client-go/utils/io" - clientTestUtils "github.com/jfrog/jfrog-client-go/utils/tests" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/exp/slices" @@ -44,8 +42,8 @@ var testCases = []struct { }, } -func createTestPackageManagerLoginCommand(packageManager project.ProjectType) *PackageManagerLoginCommand { - cmd := NewPackageManagerLoginCommand(packageManager) +func createTestSetupCommand(packageManager project.ProjectType) *SetupCommand { + cmd := NewSetupCommand(packageManager) cmd.repoName = "test-repo" dummyUrl := "https://acme.jfrog.io" cmd.serverDetails = &config.ServerDetails{Url: dummyUrl, ArtifactoryUrl: dummyUrl + "/artifactory"} @@ -53,14 +51,22 @@ func createTestPackageManagerLoginCommand(packageManager project.ProjectType) *P return cmd } -func TestPackageManagerLoginCommand_NotSupported(t *testing.T) { - notSupportedLoginCmd := createTestPackageManagerLoginCommand(project.Cocoapods) +func TestSetupCommand_NotSupported(t *testing.T) { + notSupportedLoginCmd := createTestSetupCommand(project.Cocoapods) err := notSupportedLoginCmd.Run() assert.Error(t, err) assert.ErrorContains(t, err, "unsupported package manager") } -func TestPackageManagerLoginCommand_Npm(t *testing.T) { +func TestSetupCommand_Npm(t *testing.T) { + testSetupCommandNpmPnpm(t, project.Npm) +} + +func TestSetupCommand_Pnpm(t *testing.T) { + testSetupCommandNpmPnpm(t, project.Pnpm) +} + +func testSetupCommandNpmPnpm(t *testing.T, packageManager project.ProjectType) { // Create a temporary directory to act as the environment's npmrc file location. tempDir := t.TempDir() npmrcFilePath := filepath.Join(tempDir, ".npmrc") @@ -68,17 +74,17 @@ func TestPackageManagerLoginCommand_Npm(t *testing.T) { // Set NPM_CONFIG_USERCONFIG to point to the temporary npmrc file path. t.Setenv("NPM_CONFIG_USERCONFIG", npmrcFilePath) - npmLoginCmd := createTestPackageManagerLoginCommand(project.Npm) + loginCmd := createTestSetupCommand(packageManager) for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { // Set up server details for the current test case's authentication type. - npmLoginCmd.serverDetails.SetUser(testCase.user) - npmLoginCmd.serverDetails.SetPassword(testCase.password) - npmLoginCmd.serverDetails.SetAccessToken(testCase.accessToken) + loginCmd.serverDetails.SetUser(testCase.user) + loginCmd.serverDetails.SetPassword(testCase.password) + loginCmd.serverDetails.SetAccessToken(testCase.accessToken) // Run the login command and ensure no errors occur. - require.NoError(t, npmLoginCmd.Run()) + require.NoError(t, loginCmd.Run()) // Read the contents of the temporary npmrc file. npmrcContentBytes, err := os.ReadFile(npmrcFilePath) @@ -104,7 +110,7 @@ func TestPackageManagerLoginCommand_Npm(t *testing.T) { } } -func TestPackageManagerLoginCommand_Yarn(t *testing.T) { +func TestSetupCommand_Yarn(t *testing.T) { // Retrieve the home directory and construct the .yarnrc file path. homeDir, err := os.UserHomeDir() assert.NoError(t, err) @@ -117,7 +123,7 @@ func TestPackageManagerLoginCommand_Yarn(t *testing.T) { assert.NoError(t, restoreYarnrcFunc()) }() - yarnLoginCmd := createTestPackageManagerLoginCommand(project.Yarn) + yarnLoginCmd := createTestSetupCommand(project.Yarn) for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { @@ -153,34 +159,27 @@ func TestPackageManagerLoginCommand_Yarn(t *testing.T) { } } -func TestPackageManagerLoginCommand_Pip(t *testing.T) { - // pip and pipenv share the same configuration file. - testPackageManagerLoginCommandPip(t, project.Pip) -} - -func TestPackageManagerLoginCommand_Pipenv(t *testing.T) { - // pip and pipenv share the same configuration file. - testPackageManagerLoginCommandPip(t, project.Pipenv) +func TestSetupCommand_Pip(t *testing.T) { + // Test with global configuration file. + testSetupCommandPip(t, project.Pip, false) + // Test with custom configuration file. + testSetupCommandPip(t, project.Pip, true) } -func testPackageManagerLoginCommandPip(t *testing.T, packageManager project.ProjectType) { +func testSetupCommandPip(t *testing.T, packageManager project.ProjectType, customConfig bool) { var pipConfFilePath string - if coreutils.IsWindows() { - pipConfFilePath = filepath.Join(os.Getenv("APPDATA"), "pip", "pip.ini") + if customConfig { + // For custom configuration file, set the PIP_CONFIG_FILE environment variable to point to the temporary pip.conf file. + pipConfFilePath = filepath.Join(t.TempDir(), "pip.conf") + t.Setenv("PIP_CONFIG_FILE", pipConfFilePath) } else { - // Retrieve the home directory and construct the pip.conf file path. - homeDir, err := os.UserHomeDir() - assert.NoError(t, err) - pipConfFilePath = filepath.Join(homeDir, ".config", "pip", "pip.conf") + // For global configuration file, back up the existing pip.conf file and ensure restoration after the test. + var restoreFunc func() + pipConfFilePath, restoreFunc = globalGlobalPipConfigPath(t) + defer restoreFunc() } - // Back up the existing .pip.conf file and ensure restoration after the test. - restorePipConfFunc, err := ioutils.BackupFile(pipConfFilePath, ".pipconf.backup") - assert.NoError(t, err) - defer func() { - assert.NoError(t, restorePipConfFunc()) - }() - pipLoginCmd := createTestPackageManagerLoginCommand(packageManager) + pipLoginCmd := createTestSetupCommand(packageManager) for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { @@ -215,35 +214,31 @@ func testPackageManagerLoginCommandPip(t *testing.T, packageManager project.Proj } } -func TestPackageManagerLoginCommand_configurePoetry(t *testing.T) { - // Retrieve the home directory and construct the .yarnrc file path. - homeDir, err := os.UserHomeDir() - assert.NoError(t, err) - var poetryConfigDir string - switch { - case io.IsWindows(): - poetryConfigDir = filepath.Join(homeDir, "AppData", "Roaming") - case io.IsMacOS(): - poetryConfigDir = filepath.Join(homeDir, "Library", "Application Support") - default: - poetryConfigDir = filepath.Join(homeDir, ".config") +// globalGlobalPipConfigPath returns the path to the global pip.conf file and a backup function to restore the original file. +func globalGlobalPipConfigPath(t *testing.T) (string, func()) { + var pipConfFilePath string + if coreutils.IsWindows() { + pipConfFilePath = filepath.Join(os.Getenv("APPDATA"), "pip", "pip.ini") + } else { + // Retrieve the home directory and construct the pip.conf file path. + homeDir, err := os.UserHomeDir() + assert.NoError(t, err) + pipConfFilePath = filepath.Join(homeDir, ".config", "pip", "pip.conf") } - - poetryConfigFilePath := filepath.Join(poetryConfigDir, "pypoetry", "config.toml") - // Poetry stores the auth in a separate file - poetryAuthFilePath := filepath.Join(poetryConfigDir, "pypoetry", "auth.toml") - - // Back up the existing config.toml and auth.toml files and ensure restoration after the test. - restorePoetryConfigFunc, err := ioutils.BackupFile(poetryConfigFilePath, ".poetry.config.backup") - assert.NoError(t, err) - restorePoetryAuthFunc, err := ioutils.BackupFile(poetryAuthFilePath, ".poetry-auth.backup") + // Back up the existing .pip.conf file and ensure restoration after the test. + restorePipConfFunc, err := ioutils.BackupFile(pipConfFilePath, ".pipconf.backup") assert.NoError(t, err) - defer func() { - assert.NoError(t, restorePoetryConfigFunc()) - assert.NoError(t, restorePoetryAuthFunc()) - }() + return pipConfFilePath, func() { + assert.NoError(t, restorePipConfFunc()) + } +} - poetryLoginCmd := createTestPackageManagerLoginCommand(project.Poetry) +func TestSetupCommand_configurePoetry(t *testing.T) { + configDir := t.TempDir() + poetryConfigFilePath := filepath.Join(configDir, "config.toml") + poetryAuthFilePath := filepath.Join(configDir, "auth.toml") + t.Setenv("POETRY_CONFIG_DIR", configDir) + poetryLoginCmd := createTestSetupCommand(project.Poetry) for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { @@ -288,14 +283,13 @@ func TestPackageManagerLoginCommand_configurePoetry(t *testing.T) { } } -func TestPackageManagerLoginCommand_Go(t *testing.T) { +func TestSetupCommand_Go(t *testing.T) { goProxyEnv := "GOPROXY" // Restore the original value of the GOPROXY environment variable after the test. - restoreGoProxy := clientTestUtils.SetEnvWithCallbackAndAssert(t, goProxyEnv, "") - defer restoreGoProxy() + t.Setenv(goProxyEnv, "") - // Assuming createTestPackageManagerLoginCommand initializes your Go login command - goLoginCmd := createTestPackageManagerLoginCommand(project.Go) + // Assuming createTestSetupCommand initializes your Go login command + goLoginCmd := createTestSetupCommand(project.Go) for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { @@ -308,7 +302,7 @@ func TestPackageManagerLoginCommand_Go(t *testing.T) { require.NoError(t, goLoginCmd.Run()) // Get the value of the GOPROXY environment variable. - outputBytes, err := exec.Command("go", "env", "GOPROXY").Output() + outputBytes, err := exec.Command("go", "env", goProxyEnv).Output() assert.NoError(t, err) goProxy := string(outputBytes) @@ -336,29 +330,18 @@ func TestBuildToolLoginCommand_configureDotnet(t *testing.T) { } func testBuildToolLoginCommandConfigureDotnetNuget(t *testing.T, packageManager project.ProjectType) { - // Retrieve the home directory and construct the NuGet.config file path. - homeDir, err := os.UserHomeDir() - assert.NoError(t, err) - var nugetConfigDir string - switch { - case io.IsWindows(): - nugetConfigDir = filepath.Join("AppData", "Roaming") - case packageManager == project.Nuget: - nugetConfigDir = ".config" - default: - nugetConfigDir = ".nuget" - } + var nugetConfigFilePath string - nugetConfigFilePath := filepath.Join(homeDir, nugetConfigDir, "NuGet", "NuGet.Config") - - // Back up the existing NuGet.config and ensure restoration after the test. - restoreNugetConfigFunc, err := ioutils.BackupFile(nugetConfigFilePath, packageManager.String()+".config.backup") - assert.NoError(t, err) - defer func() { - assert.NoError(t, restoreNugetConfigFunc()) - }() + // Set the NuGet.config file path to a custom location. + if packageManager == project.Dotnet { + nugetConfigFilePath = filepath.Join(t.TempDir(), "NuGet.Config") + t.Setenv("DOTNET_CLI_HOME", filepath.Dir(nugetConfigFilePath)) + } else { + nugetConfigFilePath = filepath.Join(t.TempDir(), "nuget.config") + t.Setenv("NUGET_CONFIG_FILE", nugetConfigFilePath) + } - nugetLoginCmd := createTestPackageManagerLoginCommand(packageManager) + nugetLoginCmd := createTestSetupCommand(packageManager) for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { @@ -391,10 +374,10 @@ func testBuildToolLoginCommandConfigureDotnetNuget(t *testing.T, packageManager } func TestGetSupportedPackageManagersList(t *testing.T) { - result := GetSupportedPackageManagersList() - // Check that Go is before Pip, and Pip is before Npm using GreaterOrEqual - assert.GreaterOrEqual(t, slices.Index(result, project.Pip), slices.Index(result, project.Go), "Go should come before Pip") - assert.GreaterOrEqual(t, slices.Index(result, project.Npm), slices.Index(result, project.Pip), "Pip should come before Npm") + packageManagersList := GetSupportedPackageManagersList() + // Check that "Go" is before "Pip", and "Pip" is before "Npm" + assert.Less(t, slices.Index(packageManagersList, project.Go.String()), slices.Index(packageManagersList, project.Pip.String()), "Go should come before Pip") + assert.Less(t, slices.Index(packageManagersList, project.Pip.String()), slices.Index(packageManagersList, project.Npm.String()), "Pip should come before Npm") } func TestIsSupportedPackageManager(t *testing.T) { diff --git a/artifactory/utils/repositoryutils.go b/artifactory/utils/repositoryutils.go index 1e3aa39a6..a11b91c6c 100644 --- a/artifactory/utils/repositoryutils.go +++ b/artifactory/utils/repositoryutils.go @@ -1,7 +1,6 @@ package utils import ( - "fmt" "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/ioutils" @@ -81,7 +80,7 @@ func IsRemoteRepo(repoName string, serviceManager artifactory.ArtifactoryService } // SelectRepositoryInteractively prompts the user to select a repository from a list of repositories that match the given filter parameters. -func SelectRepositoryInteractively(serverDetails *config.ServerDetails, repoFilterParams services.RepositoriesFilterParams) (string, error) { +func SelectRepositoryInteractively(serverDetails *config.ServerDetails, repoFilterParams services.RepositoriesFilterParams, promptMessage string) (string, error) { sm, err := CreateServiceManager(serverDetails, 3, 0, false) if err != nil { return "", err @@ -102,7 +101,7 @@ func SelectRepositoryInteractively(serverDetails *config.ServerDetails, repoFilt return filteredRepos[0], nil } // Prompt the user to select a repository. - return ioutils.AskFromListWithMismatchConfirmation(fmt.Sprintf("Please select a %s %s repository to configure:", repoFilterParams.RepoType, repoFilterParams.PackageType), "Repository not found.", ioutils.ConvertToSuggests(filteredRepos)), nil + return ioutils.AskFromListWithMismatchConfirmation(promptMessage, "Repository not found.", ioutils.ConvertToSuggests(filteredRepos)), nil } // GetFilteredRepositoriesWithFilterParams returns the names of local, remote, virtual, and federated repositories filtered by their names and type. diff --git a/artifactory/utils/utils.go b/artifactory/utils/utils.go index 0ebe42265..8cef7b4ff 100644 --- a/artifactory/utils/utils.go +++ b/artifactory/utils/utils.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "io" "net/http" "net/url" @@ -314,7 +315,7 @@ func ValidateRepoExists(repoKey string, serviceDetails auth.ServiceDetails) erro } exists, err := servicesManager.IsRepoExists(repoKey) if err != nil { - return err + return fmt.Errorf("failed while attempting to check if repository %q exists in Artifactory: %w", repoKey, err) } if !exists { diff --git a/go.mod b/go.mod index 702fa6e99..c240a7309 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/jfrog/jfrog-cli-core/v2 -go 1.23.3 +go 1.23.4 require github.com/c-bata/go-prompt v0.2.5 // Should not be updated to 0.2.6 due to a bug (https://github.com/jfrog/jfrog-cli-core/pull/372) @@ -21,7 +21,7 @@ require ( github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 github.com/urfave/cli v1.22.16 - github.com/vbauerster/mpb/v8 v8.8.3 + github.com/vbauerster/mpb/v8 v8.9.1 golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 golang.org/x/mod v0.22.0 golang.org/x/sync v0.10.0 @@ -35,20 +35,20 @@ require ( github.com/BurntSushi/toml v1.4.0 // indirect github.com/CycloneDX/cyclonedx-go v0.9.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v1.1.2 // indirect + github.com/ProtonMail/go-crypto v1.1.3 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect - github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/cyphar/filepath-securejoin v0.2.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dsnet/compress v0.0.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.5.0 // indirect - github.com/go-git/go-git/v5 v5.12.0 // indirect + github.com/go-git/go-billy/v5 v5.6.0 // indirect + github.com/go-git/go-git/v5 v5.13.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/snappy v0.0.4 // indirect @@ -76,7 +76,7 @@ require ( github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/skeema/knownhosts v1.2.2 // indirect + github.com/skeema/knownhosts v1.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect diff --git a/go.sum b/go.sum index ffce8e369..856eb31e4 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,8 @@ github.com/CycloneDX/cyclonedx-go v0.9.0/go.mod h1:NE/EWvzELOFlG6+ljX/QeMlVt9VKc github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/ProtonMail/go-crypto v1.1.2 h1:A7JbD57ThNqh7XjmHE+PXpQ3Dqt3BrSAC0AL0Go3KS0= -github.com/ProtonMail/go-crypto v1.1.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= +github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= @@ -38,8 +38,8 @@ github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vc github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= -github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= +github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -47,8 +47,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy v1.2.1 h1:njjgvO6cRG9rIqN2ebkqy6cQz2Njkx7Fsfv/zIZqgug= +github.com/elazarl/goproxy v1.2.1/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/forPelevin/gomoji v1.2.0 h1:9k4WVSSkE1ARO/BWywxgEUBvR/jMnao6EZzrql5nxJ8= @@ -57,16 +57,16 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= -github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= -github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= +github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= -github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/go-git/go-git/v5 v5.13.0 h1:vLn5wlGIh/X78El6r3Jr+30W16Blk0CTcxTYcYPWi5E= +github.com/go-git/go-git/v5 v5.13.0/go.mod h1:Wjo7/JyVKtQgUNdXYXIepzWfJQkUEIGvkvVkiXRR/zw= github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ= github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= @@ -138,8 +138,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc= github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= -github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= @@ -169,8 +169,8 @@ github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWR github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= -github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= +github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -203,8 +203,8 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= -github.com/vbauerster/mpb/v8 v8.8.3 h1:dTOByGoqwaTJYPubhVz3lO5O6MK553XVgUo33LdnNsQ= -github.com/vbauerster/mpb/v8 v8.8.3/go.mod h1:JfCCrtcMsJwP6ZwMn9e5LMnNyp3TVNpUWWkN+nd4EWk= +github.com/vbauerster/mpb/v8 v8.9.1 h1:LH5R3lXPfE2e3lIGxN7WNWv3Hl5nWO6LRi2B0L0ERHw= +github.com/vbauerster/mpb/v8 v8.9.1/go.mod h1:4XMvznPh8nfe2NpnDo1QTPvW9MVkUhbG90mPWvmOzcQ= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=