github.com/jfrog/jfrog-cli-go@v1.22.1-0.20200318093948-4826ef344ffd/artifactory/utils/pip/installer.go (about)

     1  package pip
     2  
     3  import (
     4  	"fmt"
     5  	gofrogcmd "github.com/jfrog/gofrog/io"
     6  	"github.com/jfrog/jfrog-cli-go/utils/config"
     7  	"github.com/jfrog/jfrog-client-go/auth"
     8  	clientutils "github.com/jfrog/jfrog-client-go/utils"
     9  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    10  	"github.com/jfrog/jfrog-client-go/utils/log"
    11  	"net/url"
    12  	"strings"
    13  )
    14  
    15  type PipInstaller struct {
    16  	RtDetails           *config.ArtifactoryDetails
    17  	Args                []string
    18  	Repository          string
    19  	ShouldParseLogs     bool
    20  	DependencyToFileMap map[string]string
    21  }
    22  
    23  func (pi *PipInstaller) Install() error {
    24  	// Prepare for running.
    25  	pipExecutablePath, pipIndexUrl, err := pi.prepare()
    26  	if err != nil {
    27  		return err
    28  	}
    29  
    30  	// Run pip install.
    31  	err = pi.runPipInstall(pipExecutablePath, pipIndexUrl)
    32  	if err != nil {
    33  		return err
    34  	}
    35  
    36  	return nil
    37  }
    38  
    39  func (pi *PipInstaller) prepare() (pipExecutablePath, pipIndexUrl string, err error) {
    40  	log.Debug("Preparing prerequisites.")
    41  
    42  	pipExecutablePath, err = GetExecutablePath("pip")
    43  	if err != nil {
    44  		return
    45  	}
    46  
    47  	pipIndexUrl, err = getArtifactoryUrlWithCredentials(pi.RtDetails, pi.Repository)
    48  	if err != nil {
    49  		return
    50  	}
    51  
    52  	return
    53  }
    54  
    55  func getArtifactoryUrlWithCredentials(rtDetails *config.ArtifactoryDetails, repository string) (string, error) {
    56  	rtUrl, err := url.Parse(rtDetails.GetUrl())
    57  	if err != nil {
    58  		return "", errorutils.CheckError(err)
    59  	}
    60  
    61  	username := rtDetails.GetUser()
    62  	password := rtDetails.GetPassword()
    63  
    64  	// Get credentials from access-token if exists.
    65  	if rtDetails.GetAccessToken() != "" {
    66  		username, err = auth.ExtractUsernameFromAccessToken(rtDetails.GetAccessToken())
    67  		if err != nil {
    68  			return "", err
    69  		}
    70  		password = rtDetails.GetAccessToken()
    71  	}
    72  
    73  	if username != "" && password != "" {
    74  		rtUrl.User = url.UserPassword(username, password)
    75  	}
    76  	rtUrl.Path += "api/pypi/" + repository + "/simple"
    77  
    78  	return rtUrl.String(), nil
    79  }
    80  
    81  func (pi *PipInstaller) runPipInstall(pipExecutablePath, pipIndexUrl string) error {
    82  	pipInstallCmd := &PipCmd{
    83  		Executable:  pipExecutablePath,
    84  		Command:     "install",
    85  		CommandArgs: append(pi.Args, "-i", pipIndexUrl),
    86  	}
    87  
    88  	// Check if need to run with log parsing.
    89  	if pi.ShouldParseLogs {
    90  		return pi.runPipInstallWithLogParsing(pipInstallCmd)
    91  	}
    92  
    93  	// Run without log parsing.
    94  	return gofrogcmd.RunCmd(pipInstallCmd)
    95  }
    96  
    97  // Run pip-install command while parsing the logs for downloaded packages.
    98  // Supports running pip either in non-verbose and verbose mode.
    99  // Populates 'dependencyToFileMap' with downloaded package-name and its actual downloaded file (wheel/egg/zip...).
   100  func (pi *PipInstaller) runPipInstallWithLogParsing(pipInstallCmd *PipCmd) error {
   101  	// Create regular expressions for log parsing.
   102  	collectingPackageRegexp, err := clientutils.GetRegExp(`^Collecting\s(\w[\w-\.]+)`)
   103  	if err != nil {
   104  		return err
   105  	}
   106  	downloadFileRegexp, err := clientutils.GetRegExp(`^\s\sDownloading\s[^\s]*\/packages\/[^\s]*\/([^\s]*)`)
   107  	if err != nil {
   108  		return err
   109  	}
   110  	installedPackagesRegexp, err := clientutils.GetRegExp(`^Requirement\salready\ssatisfied\:\s(\w[\w-\.]+)`)
   111  	if err != nil {
   112  		return err
   113  	}
   114  
   115  	downloadedDependencies := make(map[string]string)
   116  	var packageName string
   117  	expectingPackageFilePath := false
   118  
   119  	// Extract downloaded package name.
   120  	dependencyNameParser := gofrogcmd.CmdOutputPattern{
   121  		RegExp: collectingPackageRegexp,
   122  		ExecFunc: func(pattern *gofrogcmd.CmdOutputPattern) (string, error) {
   123  			// If this pattern matched a second time before downloaded-file-name was found, prompt a message.
   124  			if expectingPackageFilePath {
   125  				// This may occur when a package-installation file is saved in pip-cache-dir, thus not being downloaded during the installation.
   126  				// Re-running pip-install with 'no-cache-dir' fixes this issue.
   127  				log.Debug(fmt.Sprintf("Could not resolve download path for package: %s, continuing...", packageName))
   128  
   129  				// Save package with empty file path.
   130  				downloadedDependencies[strings.ToLower(packageName)] = ""
   131  			}
   132  
   133  			// Check for out of bound results.
   134  			if len(pattern.MatchedResults)-1 < 0 {
   135  				log.Debug(fmt.Sprintf("Failed extracting package name from line: %s", pattern.Line))
   136  				return pattern.Line, nil
   137  			}
   138  
   139  			// Save dependency information.
   140  			expectingPackageFilePath = true
   141  			packageName = pattern.MatchedResults[1]
   142  
   143  			return pattern.Line, nil
   144  		},
   145  	}
   146  
   147  	// Extract downloaded file, stored in Artifactory.
   148  	dependencyFileParser := gofrogcmd.CmdOutputPattern{
   149  		RegExp: downloadFileRegexp,
   150  		ExecFunc: func(pattern *gofrogcmd.CmdOutputPattern) (string, error) {
   151  			// Check for out of bound results.
   152  			if len(pattern.MatchedResults)-1 < 0 {
   153  				log.Debug(fmt.Sprintf("Failed extracting download path from line: %s", pattern.Line))
   154  				return pattern.Line, nil
   155  			}
   156  
   157  			// If this pattern matched before package-name was found, do not collect this path.
   158  			if !expectingPackageFilePath {
   159  				log.Debug(fmt.Sprintf("Could not resolve package name for download path: %s , continuing...", packageName))
   160  				return pattern.Line, nil
   161  			}
   162  
   163  			// Save dependency information.
   164  			filePath := pattern.MatchedResults[1]
   165  			downloadedDependencies[strings.ToLower(packageName)] = filePath
   166  			expectingPackageFilePath = false
   167  
   168  			log.Debug(fmt.Sprintf("Found package: %s installed with: %s", packageName, filePath))
   169  			return pattern.Line, nil
   170  		},
   171  	}
   172  
   173  	// Extract already installed packages names.
   174  	installedPackagesParser := gofrogcmd.CmdOutputPattern{
   175  		RegExp: installedPackagesRegexp,
   176  		ExecFunc: func(pattern *gofrogcmd.CmdOutputPattern) (string, error) {
   177  			// Check for out of bound results.
   178  			if len(pattern.MatchedResults)-1 < 0 {
   179  				log.Debug(fmt.Sprintf("Failed extracting package name from line: %s", pattern.Line))
   180  				return pattern.Line, nil
   181  			}
   182  
   183  			// Save dependency with empty file name.
   184  			downloadedDependencies[strings.ToLower(pattern.MatchedResults[1])] = ""
   185  
   186  			log.Debug(fmt.Sprintf("Found package: %s already installed", pattern.MatchedResults[1]))
   187  			return pattern.Line, nil
   188  		},
   189  	}
   190  
   191  	// Execute command.
   192  	_, _, _, err = gofrogcmd.RunCmdWithOutputParser(pipInstallCmd, true, &dependencyNameParser, &dependencyFileParser, &installedPackagesParser)
   193  	if errorutils.CheckError(err) != nil {
   194  		return err
   195  	}
   196  
   197  	// Update dependencyToFileMap.
   198  	pi.DependencyToFileMap = downloadedDependencies
   199  
   200  	return nil
   201  }