diff --git a/artifactory/commands/container/containermanagercommand.go b/artifactory/commands/container/containermanagercommand.go index 6190a5693..eb4e74efe 100644 --- a/artifactory/commands/container/containermanagercommand.go +++ b/artifactory/commands/container/containermanagercommand.go @@ -41,7 +41,11 @@ func (cm *ContainerCommand) PerformLogin(serverDetails *config.ServerDetails, co } } loginConfig := &container.ContainerManagerLoginConfig{ServerDetails: serverDetails} - return container.ContainerManagerLogin(cm.image, loginConfig, containerManagerType) + imageRegistry, err := cm.image.GetRegistry() + if err != nil { + return err + } + return container.ContainerManagerLogin(imageRegistry, loginConfig, containerManagerType) } return nil } diff --git a/artifactory/commands/packagemanagerlogin/packagemanagerlogin.go b/artifactory/commands/packagemanagerlogin/packagemanagerlogin.go index 2a3958669..e462724a2 100644 --- a/artifactory/commands/packagemanagerlogin/packagemanagerlogin.go +++ b/artifactory/commands/packagemanagerlogin/packagemanagerlogin.go @@ -10,21 +10,50 @@ import ( "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/container" "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-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-client-go/artifactory/services" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/log" + "golang.org/x/exp/maps" + "net/url" + "slices" ) +// packageManagerToRepositoryPackageType maps project types to corresponding Artifactory repository package types. +var packageManagerToRepositoryPackageType = map[project.ProjectType]string{ + // Npm package managers + project.Npm: repository.Npm, + project.Yarn: repository.Npm, + + // Python (pypi) package managers + project.Pip: repository.Pypi, + project.Pipenv: repository.Pypi, + project.Poetry: repository.Pypi, + + // Nuget package managers + project.Nuget: repository.Nuget, + project.Dotnet: repository.Nuget, + + // Docker package managers + project.Docker: repository.Docker, + project.Podman: repository.Docker, + + project.Go: repository.Go, +} + // 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 + // projectKey is the JFrog Project key in JFrog Platform. + projectKey string // serverDetails contains Artifactory server configuration. serverDetails *config.ServerDetails // commandName specifies the command for this instance. @@ -40,20 +69,19 @@ func NewPackageManagerLoginCommand(packageManager project.ProjectType) *PackageM } } -// 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) - } +// GetSupportedPackageManagersList returns a sorted list of supported package managers. +func GetSupportedPackageManagersList() []project.ProjectType { + 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 +} + +func IsSupportedPackageManager(packageManager project.ProjectType) bool { + _, exists := packageManagerToRepositoryPackageType[packageManager] + return exists } // CommandName returns the name of the login command. @@ -72,8 +100,24 @@ func (pmlc *PackageManagerLoginCommand) ServerDetails() (*config.ServerDetails, return pmlc.serverDetails, nil } +// SetRepoName assigns the repository name to the command. +func (pmlc *PackageManagerLoginCommand) SetRepoName(repoName string) *PackageManagerLoginCommand { + pmlc.repoName = repoName + return pmlc +} + +// SetProjectKey assigns the project key to the command. +func (pmlc *PackageManagerLoginCommand) SetProjectKey(projectKey string) *PackageManagerLoginCommand { + pmlc.projectKey = projectKey + return pmlc +} + // 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) + } + if pmlc.repoName == "" { // Prompt the user to select a virtual repository that matches the package manager. if err = pmlc.promptUserToSelectRepository(); err != nil { @@ -95,6 +139,8 @@ func (pmlc *PackageManagerLoginCommand) Run() (err error) { err = pmlc.configureGo() case project.Nuget, project.Dotnet: err = pmlc.configureDotnetNuget() + case project.Docker, project.Podman: + err = pmlc.configureContainer() default: err = errorutils.CheckErrorf("unsupported package manager: %s", pmlc.packageManager) } @@ -102,20 +148,16 @@ func (pmlc *PackageManagerLoginCommand) Run() (err error) { 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)) + log.Output(fmt.Sprintf("Successfully configured %s to use JFrog Artifactory repository '%s'.", coreutils.PrintBoldTitle(pmlc.packageManager.String()), coreutils.PrintBoldTitle(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 - } +func (pmlc *PackageManagerLoginCommand) promptUserToSelectRepository() (err error) { repoFilterParams := services.RepositoriesFilterParams{ RepoType: utils.Virtual.String(), - PackageType: packageType, + PackageType: packageManagerToRepositoryPackageType[pmlc.packageManager], + ProjectKey: pmlc.projectKey, } // Prompt for repository selection based on filter parameters. @@ -239,3 +281,36 @@ func (pmlc *PackageManagerLoginCommand) configureDotnetNuget() error { // Add the repository as a source in the NuGet configuration with credentials for authentication. return dotnet.AddSourceToNugetConfig(toolchainType, sourceUrl, user, password) } + +// configureContainer configures container managers like Docker or Podman to authenticate with JFrog Artifactory. +// It performs a login using the container manager's CLI command. +// +// For Docker: +// +// echo | docker login -u --password-stdin +// +// For Podman: +// +// echo | podman login -u --password-stdin +func (pmlc *PackageManagerLoginCommand) configureContainer() error { + var containerManagerType container.ContainerManagerType + switch pmlc.packageManager { + case project.Docker: + containerManagerType = container.DockerClient + case project.Podman: + containerManagerType = container.Podman + default: + return errorutils.CheckErrorf("unsupported container manager: %s", pmlc.packageManager) + } + // Parse the URL to remove the scheme (https:// or http://) + parsedPlatformURL, err := url.Parse(pmlc.serverDetails.GetUrl()) + if err != nil { + return err + } + urlWithoutScheme := parsedPlatformURL.Host + parsedPlatformURL.Path + return container.ContainerManagerLogin( + urlWithoutScheme, + &container.ContainerManagerLoginConfig{ServerDetails: pmlc.serverDetails}, + containerManagerType, + ) +} diff --git a/artifactory/commands/packagemanagerlogin/packagemanagerlogin_test.go b/artifactory/commands/packagemanagerlogin/packagemanagerlogin_test.go index 8bb95abde..9783e8133 100644 --- a/artifactory/commands/packagemanagerlogin/packagemanagerlogin_test.go +++ b/artifactory/commands/packagemanagerlogin/packagemanagerlogin_test.go @@ -13,6 +13,7 @@ import ( clientTestUtils "github.com/jfrog/jfrog-client-go/utils/tests" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/exp/slices" "os" "os/exec" "path/filepath" @@ -52,6 +53,13 @@ func createTestPackageManagerLoginCommand(packageManager project.ProjectType) *P return cmd } +func TestPackageManagerLoginCommand_NotSupported(t *testing.T) { + notSupportedLoginCmd := createTestPackageManagerLoginCommand(project.Cocoapods) + err := notSupportedLoginCmd.Run() + assert.Error(t, err) + assert.ErrorContains(t, err, "unsupported package manager") +} + func TestPackageManagerLoginCommand_Npm(t *testing.T) { // Create a temporary directory to act as the environment's npmrc file location. tempDir := t.TempDir() @@ -381,3 +389,20 @@ 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") +} + +func TestIsSupportedPackageManager(t *testing.T) { + // Test valid package managers + for pm := range packageManagerToRepositoryPackageType { + assert.True(t, IsSupportedPackageManager(pm), "Package manager %s should be supported", pm) + } + + // Test unsupported package manager + assert.False(t, IsSupportedPackageManager(project.Cocoapods), "Package manager Cocoapods should not be supported") +} diff --git a/artifactory/utils/container/containermanager.go b/artifactory/utils/container/containermanager.go index 8c16c6659..9867352bc 100644 --- a/artifactory/utils/container/containermanager.go +++ b/artifactory/utils/container/containermanager.go @@ -189,11 +189,7 @@ func (loginCmd *LoginCmd) RunCmd() error { // First we'll try to log in assuming a proxy-less tag (e.g. "registry-address/docker-repo/image:ver"). // If fails, we will try assuming a reverse proxy tag (e.g. "registry-address-docker-repo/image:ver"). -func ContainerManagerLogin(image *Image, config *ContainerManagerLoginConfig, containerManager ContainerManagerType) error { - imageRegistry, err := image.GetRegistry() - if err != nil { - return err - } +func ContainerManagerLogin(imageRegistry string, config *ContainerManagerLoginConfig, containerManager ContainerManagerType) error { username := config.ServerDetails.User password := config.ServerDetails.Password // If access-token exists, perform login with it. @@ -206,7 +202,7 @@ func ContainerManagerLogin(image *Image, config *ContainerManagerLoginConfig, co } // Perform login. cmd := &LoginCmd{DockerRegistry: imageRegistry, Username: username, Password: password, containerManager: containerManager} - err = cmd.RunCmd() + err := cmd.RunCmd() if exitCode := coreutils.GetExitCode(err, 0, 0, false); exitCode == coreutils.ExitCodeNoError { // Login succeeded return nil diff --git a/artifactory/utils/repositoryutils.go b/artifactory/utils/repositoryutils.go index 40e11b30f..1e3aa39a6 100644 --- a/artifactory/utils/repositoryutils.go +++ b/artifactory/utils/repositoryutils.go @@ -1,6 +1,7 @@ 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" @@ -101,7 +102,7 @@ func SelectRepositoryInteractively(serverDetails *config.ServerDetails, repoFilt return filteredRepos[0], nil } // Prompt the user to select a repository. - return ioutils.AskFromListWithMismatchConfirmation("Please select a repository to login to:", "Repository not found.", ioutils.ConvertToSuggests(filteredRepos)), nil + return ioutils.AskFromListWithMismatchConfirmation(fmt.Sprintf("Please select a %s %s repository to configure:", repoFilterParams.RepoType, repoFilterParams.PackageType), "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/common/project/projectconfig.go b/common/project/projectconfig.go index 92134ae75..646f014eb 100644 --- a/common/project/projectconfig.go +++ b/common/project/projectconfig.go @@ -26,6 +26,7 @@ const ( type ProjectType int const ( + // When adding new ProjectType here, Must also add it as a string to the ProjectTypes slice Go ProjectType = iota Pip Pipenv @@ -41,6 +42,8 @@ const ( Terraform Cocoapods Swift + Docker + Podman ) type ConfigType string @@ -66,12 +69,24 @@ var ProjectTypes = []string{ "terraform", "cocoapods", "swift", + "docker", + "podman", } func (projectType ProjectType) String() string { return ProjectTypes[projectType] } +// FromString converts a string to its corresponding ProjectType +func FromString(value string) ProjectType { + for i, projectType := range ProjectTypes { + if projectType == value { + return ProjectType(i) + } + } + return -1 +} + type MissingResolverErr struct { message string } diff --git a/common/project/projectconfig_test.go b/common/project/projectconfig_test.go new file mode 100644 index 000000000..9a7edafc5 --- /dev/null +++ b/common/project/projectconfig_test.go @@ -0,0 +1,30 @@ +package project + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestFromString(t *testing.T) { + // Test valid conversions + testCases := []struct { + input string + expected ProjectType + }{ + {"go", Go}, + {"pip", Pip}, + {"npm", Npm}, + {"pnpm", Pnpm}, + } + + for _, testCase := range testCases { + t.Run(testCase.input, func(t *testing.T) { + result := FromString(testCase.input) + assert.Equal(t, testCase.expected, result) + }) + } + + // Test invalid conversion + result := FromString("InvalidProject") + assert.Equal(t, ProjectType(-1), result) +} diff --git a/utils/ioutils/questionnaire.go b/utils/ioutils/questionnaire.go index 74459ac43..40ba1b965 100644 --- a/utils/ioutils/questionnaire.go +++ b/utils/ioutils/questionnaire.go @@ -82,7 +82,7 @@ func interruptKeyBind() prompt.Option { interrupt := prompt.KeyBind{ Key: prompt.ControlC, Fn: func(buf *prompt.Buffer) { - panic("Interrupted") + panic("Operation interrupted. Exiting...") }, } return prompt.OptionAddKeyBind(interrupt) @@ -177,7 +177,7 @@ func validateAnswer(answer string, options []prompt.Suggest, allowVars bool) boo // If the provided answer does not appear in list, confirm the choice. func AskFromListWithMismatchConfirmation(promptPrefix, misMatchMsg string, options []prompt.Suggest) string { for { - answer := prompt.Input(promptPrefix+" ", prefixCompleter(options), interruptKeyBind()) + answer := prompt.Input(promptPrefix+" ", prefixCompleter(options), interruptKeyBind(), prompt.OptionShowCompletionAtStart(), prompt.OptionCompletionOnDown()) if answer == "" { log.Output(EmptyValueMsg) }