diff --git a/artifactory/commands/python/twine.go b/artifactory/commands/python/twine.go new file mode 100644 index 000000000..6cb70469b --- /dev/null +++ b/artifactory/commands/python/twine.go @@ -0,0 +1,195 @@ +package python + +import ( + "errors" + "fmt" + "github.com/jfrog/build-info-go/build" + "github.com/jfrog/build-info-go/utils/pythonutils" + buildUtils "github.com/jfrog/jfrog-cli-core/v2/common/build" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-client-go/auth" + "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/log" + "os" + "os/exec" + "strings" +) + +const ( + _configFileOptionKey = "--config-file" + _repositoryUrlOptionKey = "--repository-url" + _usernameOptionKey = "--username" + _passwordOptionKey = "--password" + _usernamePrefixOptionKey = "-u" + _passwordPrefixOptionKey = "-p" + _repositoryUrlEnvKey = "TWINE_REPOSITORY_URL" + _usernameEnvKey = "TWINE_USERNAME" + _passwordEnvKey = "TWINE_PASSWORD" + // Artifactory endpoint for pypi deployment. + _apiPypi = "api/pypi/" + _twineExecName = "twine" + _uploadCmdName = "upload" +) + +var twineRepoConfigFlags = []string{_configFileOptionKey, _repositoryUrlOptionKey, _usernameOptionKey, _passwordOptionKey, _usernamePrefixOptionKey, _passwordPrefixOptionKey} + +type TwineCommand struct { + serverDetails *config.ServerDetails + commandName string + args []string + targetRepo string + buildConfiguration *buildUtils.BuildConfiguration +} + +func NewTwineCommand(commandName string) *TwineCommand { + return &TwineCommand{ + commandName: commandName, + } +} + +func (tc *TwineCommand) CommandName() string { + return "twine_" + tc.commandName +} + +func (tc *TwineCommand) ServerDetails() (*config.ServerDetails, error) { + return tc.serverDetails, nil +} + +func (tc *TwineCommand) SetServerDetails(serverDetails *config.ServerDetails) *TwineCommand { + tc.serverDetails = serverDetails + return tc +} + +func (tc *TwineCommand) SetTargetRepo(targetRepo string) *TwineCommand { + tc.targetRepo = targetRepo + return tc +} + +func (tc *TwineCommand) SetArgs(args []string) *TwineCommand { + tc.args = args + return tc +} + +func (tc *TwineCommand) Run() (err error) { + // Assert no forbidden flags were provided. + if tc.isRepoConfigFlagProvided() { + return errorutils.CheckErrorf(tc.getRepoConfigFlagProvidedErr()) + } + if err = tc.extractAndFilterArgs(tc.args); err != nil { + return err + } + callbackFunc, err := tc.setAuthEnvVars() + defer func() { + err = errors.Join(err, callbackFunc()) + }() + + collectBuild, err := tc.buildConfiguration.IsCollectBuildInfo() + if err != nil { + return err + } + // If build info is not collected, or this is not an upload command, run the twine command directly. + if !collectBuild || tc.commandName != _uploadCmdName { + return tc.runPlainTwineCommand() + } + return tc.uploadAndCollectBuildInfo() +} + +func (tc *TwineCommand) extractAndFilterArgs(args []string) (err error) { + cleanArgs := append([]string(nil), args...) + cleanArgs, tc.buildConfiguration, err = buildUtils.ExtractBuildDetailsFromArgs(cleanArgs) + if err != nil { + return + } + tc.args = cleanArgs + return +} + +func (tc *TwineCommand) setAuthEnvVars() (callbackFunc func() error, err error) { + oldRepoUrl := os.Getenv(_repositoryUrlEnvKey) + oldUsername := os.Getenv(_usernameEnvKey) + oldPassword := os.Getenv(_passwordEnvKey) + callbackFunc = func() error { + return errors.Join(os.Setenv(_repositoryUrlOptionKey, oldRepoUrl), os.Setenv(_usernameEnvKey, oldUsername), os.Setenv(_passwordEnvKey, oldPassword)) + } + + if err = os.Setenv(_repositoryUrlEnvKey, utils.AddTrailingSlashIfNeeded(tc.serverDetails.ArtifactoryUrl)+_apiPypi+tc.targetRepo); err != nil { + return + } + + username := tc.serverDetails.User + password := tc.serverDetails.Password + // Get credentials from access-token if exists. + if tc.serverDetails.GetAccessToken() != "" { + if username == "" { + username = auth.ExtractUsernameFromAccessToken(tc.serverDetails.GetAccessToken()) + } + password = tc.serverDetails.GetAccessToken() + } + + if err = os.Setenv(_usernameEnvKey, username); err != nil { + return + } + err = os.Setenv(_passwordEnvKey, password) + return +} + +func (tc *TwineCommand) runPlainTwineCommand() error { + log.Debug("Running twine command:", tc.commandName, strings.Join(tc.args, " ")) + args := append([]string{tc.commandName}, tc.args...) + cmd := exec.Command(_twineExecName, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func (tc *TwineCommand) uploadAndCollectBuildInfo() error { + buildInfo, err := buildUtils.PrepareBuildPrerequisites(tc.buildConfiguration) + if err != nil { + return err + } + + defer func() { + if buildInfo != nil && err != nil { + err = errors.Join(err, buildInfo.Clean()) + } + }() + + var pythonModule *build.PythonModule + pythonModule, err = buildInfo.AddPythonModule("", pythonutils.Twine) + if err != nil { + return err + } + if tc.buildConfiguration.GetModule() != "" { + pythonModule.SetName(tc.buildConfiguration.GetModule()) + } + + artifacts, err := pythonModule.TwineUploadWithLogParsing(tc.args) + if err != nil { + return err + } + for i := range artifacts { + artifacts[i].OriginalDeploymentRepo = tc.targetRepo + } + if err = pythonModule.AddArtifacts(artifacts); err != nil { + return err + } + log.Debug(fmt.Sprintf("Command finished successfully. %d artifacs were added to build info.", len(artifacts))) + return nil +} + +func (tc *TwineCommand) isRepoConfigFlagProvided() bool { + for _, arg := range tc.args { + for _, flag := range twineRepoConfigFlags { + if strings.HasPrefix(arg, flag) { + return true + } + } + } + return false +} + +func (tc *TwineCommand) getRepoConfigFlagProvidedErr() string { + return "twine command must not be executed with the following flags: " + coreutils.ListToText(twineRepoConfigFlags) +} diff --git a/artifactory/utils/commandsummary/buildinfosummary.go b/artifactory/utils/commandsummary/buildinfosummary.go index 2a0fe9e08..9861c723c 100644 --- a/artifactory/utils/commandsummary/buildinfosummary.go +++ b/artifactory/utils/commandsummary/buildinfosummary.go @@ -31,6 +31,7 @@ var ( buildInfo.Generic: true, buildInfo.Terraform: true, buildInfo.Docker: true, + buildInfo.Python: true, } ) diff --git a/common/commands/configfile.go b/common/commands/configfile.go index 60e8a35ee..450e71895 100644 --- a/common/commands/configfile.go +++ b/common/commands/configfile.go @@ -153,7 +153,9 @@ func handleInteractiveConfigCreation(configFile *ConfigFile, confType project.Pr switch confType { case project.Go: return configFile.setDeployerResolver() - case project.Pip, project.Pipenv, project.Poetry: + case project.Pip, project.Pipenv: + return configFile.setDeployerResolver() + case project.Poetry: return configFile.setResolver(false) case project.Yarn: return configFile.setResolver(false) diff --git a/go.mod b/go.mod index 246c09df0..0ecd6097d 100644 --- a/go.mod +++ b/go.mod @@ -98,6 +98,6 @@ require ( replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20240918081224-1c584cc334c7 -//replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.8.9-0.20240909072259-13bf8722d051 +replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.8.9-0.20240918150101-ad5b10435a12 -//replace github.com/jfrog/gofrog => github.com/jfrog/gofrog v1.7.6-0.20240909061051-2d36ae4bd05a +// replace github.com/jfrog/gofrog => github.com/jfrog/gofrog v1.3.3-0.20231223133729-ef57bd08cedc diff --git a/go.sum b/go.sum index 5bfdf12d0..5a6415708 100644 --- a/go.sum +++ b/go.sum @@ -92,8 +92,8 @@ github.com/jedib0t/go-pretty/v6 v6.5.9 h1:ACteMBRrrmm1gMsXe9PSTOClQ63IXDUt03H5U+ github.com/jedib0t/go-pretty/v6 v6.5.9/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= github.com/jfrog/archiver/v3 v3.6.1 h1:LOxnkw9pOn45DzCbZNFV6K0+6dCsQ0L8mR3ZcujO5eI= github.com/jfrog/archiver/v3 v3.6.1/go.mod h1:VgR+3WZS4N+i9FaDwLZbq+jeU4B4zctXL+gL4EMzfLw= -github.com/jfrog/build-info-go v1.9.36 h1:bKoYW3o+U70Zbz2kt5NT84N5JWNxdDXHOf+kVdzK+j4= -github.com/jfrog/build-info-go v1.9.36/go.mod h1:JcISnovFXKx3wWf3p1fcMmlPdt6adxScXvoJN4WXqIE= +github.com/jfrog/build-info-go v1.8.9-0.20240918150101-ad5b10435a12 h1:OWxdvdurW6LRRBDEgVl8WFcjJbk9TyBcVGgXX4k5atc= +github.com/jfrog/build-info-go v1.8.9-0.20240918150101-ad5b10435a12/go.mod h1:JcISnovFXKx3wWf3p1fcMmlPdt6adxScXvoJN4WXqIE= github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s= github.com/jfrog/gofrog v1.7.6/go.mod h1:ntr1txqNOZtHplmaNd7rS4f8jpA5Apx8em70oYEe7+4= github.com/jfrog/jfrog-client-go v1.28.1-0.20240918081224-1c584cc334c7 h1:h/bLASJGFaI3QOow1Ix63RZB8kZpAClkA/NpAVWRroc= diff --git a/utils/coreutils/cmdutils.go b/utils/coreutils/cmdutils.go index 4ef9fe34f..d57783c75 100644 --- a/utils/coreutils/cmdutils.go +++ b/utils/coreutils/cmdutils.go @@ -138,7 +138,7 @@ func FindFlagFirstMatch(flags, args []string) (flagIndex, flagValueIndex int, fl } func ExtractServerIdFromCommand(args []string) (cleanArgs []string, serverId string, err error) { - return extractStringOptionFromArgs(args, "server-id") + return ExtractStringOptionFromArgs(args, "server-id") } func ExtractThreadsFromArgs(args []string, defaultValue int) (cleanArgs []string, threads int, err error) { @@ -181,12 +181,12 @@ func ExtractLicensesFromArgs(args []string) (cleanArgs []string, licenses bool, // Used by docker scan (Xray) func ExtractRepoPathFromArgs(args []string) (cleanArgs []string, repoPath string, err error) { - return extractStringOptionFromArgs(args, "repo-path") + return ExtractStringOptionFromArgs(args, "repo-path") } // Used by docker scan (Xray) func ExtractWatchesFromArgs(args []string) (cleanArgs []string, watches string, err error) { - return extractStringOptionFromArgs(args, "watches") + return ExtractStringOptionFromArgs(args, "watches") } func ExtractDetailedSummaryFromArgs(args []string) (cleanArgs []string, detailedSummary bool, err error) { @@ -198,14 +198,14 @@ func ExtractXrayScanFromArgs(args []string) (cleanArgs []string, xrayScan bool, } func ExtractXrayOutputFormatFromArgs(args []string) (cleanArgs []string, format string, err error) { - return extractStringOptionFromArgs(args, "format") + return ExtractStringOptionFromArgs(args, "format") } func ExtractTagFromArgs(args []string) (cleanArgs []string, tag string, err error) { - return extractStringOptionFromArgs(args, "tag") + return ExtractStringOptionFromArgs(args, "tag") } -func extractStringOptionFromArgs(args []string, optionName string) (cleanArgs []string, value string, err error) { +func ExtractStringOptionFromArgs(args []string, optionName string) (cleanArgs []string, value string, err error) { cleanArgs = append([]string(nil), args...) flagIndex, valIndex, value, err := FindFlag("--"+optionName, cleanArgs)