github.com/osievert/jfrog-cli-core@v1.2.7/artifactory/commands/npm/installorci.go (about)

     1  package npm
     2  
     3  import (
     4  	"encoding/base64"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"os"
    12  	"os/exec"
    13  	"path/filepath"
    14  	"strconv"
    15  	"strings"
    16  
    17  	"github.com/buger/jsonparser"
    18  	gofrogcmd "github.com/jfrog/gofrog/io"
    19  	"github.com/jfrog/gofrog/parallel"
    20  	"github.com/jfrog/jfrog-cli-core/artifactory/utils"
    21  	"github.com/jfrog/jfrog-cli-core/artifactory/utils/npm"
    22  	"github.com/jfrog/jfrog-cli-core/utils/config"
    23  	"github.com/jfrog/jfrog-cli-core/utils/ioutils"
    24  	"github.com/jfrog/jfrog-client-go/artifactory"
    25  	"github.com/jfrog/jfrog-client-go/artifactory/buildinfo"
    26  	"github.com/jfrog/jfrog-client-go/artifactory/services"
    27  	serviceutils "github.com/jfrog/jfrog-client-go/artifactory/services/utils"
    28  	"github.com/jfrog/jfrog-client-go/auth"
    29  	"github.com/jfrog/jfrog-client-go/http/httpclient"
    30  	clientutils "github.com/jfrog/jfrog-client-go/utils"
    31  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    32  	"github.com/jfrog/jfrog-client-go/utils/log"
    33  	"github.com/jfrog/jfrog-client-go/utils/version"
    34  )
    35  
    36  const npmrcFileName = ".npmrc"
    37  const npmrcBackupFileName = "jfrog.npmrc.backup"
    38  const minSupportedArtifactoryVersion = "5.5.2"
    39  const minSupportedNpmVersion = "5.4.0"
    40  
    41  type NpmCommandArgs struct {
    42  	command          string
    43  	threads          int
    44  	jsonOutput       bool
    45  	executablePath   string
    46  	npmrcFileMode    os.FileMode
    47  	workingDirectory string
    48  	registry         string
    49  	npmAuth          string
    50  	collectBuildInfo bool
    51  	dependencies     map[string]*dependency
    52  	typeRestriction  string
    53  	artDetails       auth.ServiceDetails
    54  	packageInfo      *npm.PackageInfo
    55  	NpmCommand
    56  }
    57  
    58  type NpmInstallOrCiCommand struct {
    59  	configFilePath      string
    60  	internalCommandName string
    61  	*NpmCommandArgs
    62  }
    63  
    64  func NewNpmInstallCommand() *NpmInstallOrCiCommand {
    65  	return &NpmInstallOrCiCommand{NpmCommandArgs: NewNpmCommandArgs("install"), internalCommandName: "rt_npm_install"}
    66  }
    67  
    68  func NewNpmCiCommand() *NpmInstallOrCiCommand {
    69  	return &NpmInstallOrCiCommand{NpmCommandArgs: NewNpmCommandArgs("ci"), internalCommandName: "rt_npm_ci"}
    70  }
    71  
    72  func (nic *NpmInstallOrCiCommand) CommandName() string {
    73  	return nic.internalCommandName
    74  }
    75  
    76  func (nic *NpmInstallOrCiCommand) SetConfigFilePath(configFilePath string) *NpmInstallOrCiCommand {
    77  	nic.configFilePath = configFilePath
    78  	return nic
    79  }
    80  
    81  func (nic *NpmInstallOrCiCommand) SetArgs(args []string) *NpmInstallOrCiCommand {
    82  	nic.NpmCommandArgs.npmArgs = args
    83  	return nic
    84  }
    85  
    86  func (nic *NpmInstallOrCiCommand) SetRepoConfig(conf *utils.RepositoryConfig) *NpmInstallOrCiCommand {
    87  	rtDetails, _ := conf.RtDetails()
    88  	nic.NpmCommandArgs.SetRepo(conf.TargetRepo()).SetRtDetails(rtDetails)
    89  	return nic
    90  }
    91  
    92  func (nic *NpmInstallOrCiCommand) Run() error {
    93  	log.Info(fmt.Sprintf("Running npm %s.", nic.command))
    94  	// Read config file.
    95  	log.Debug("Preparing to read the config file", nic.configFilePath)
    96  	vConfig, err := utils.ReadConfigFile(nic.configFilePath, utils.YAML)
    97  	if err != nil {
    98  		return err
    99  	}
   100  	// Extract resolution params.
   101  	resolverParams, err := utils.GetRepoConfigByPrefix(nic.configFilePath, utils.ProjectConfigResolverPrefix, vConfig)
   102  	if err != nil {
   103  		return err
   104  	}
   105  	threads, jsonOutput, filteredNpmArgs, buildConfiguration, err := npm.ExtractNpmOptionsFromArgs(nic.npmArgs)
   106  	nic.SetRepoConfig(resolverParams).SetArgs(filteredNpmArgs).SetThreads(threads).SetJsonOutput(jsonOutput).SetBuildConfiguration(buildConfiguration)
   107  	if err != nil {
   108  		return err
   109  	}
   110  	return nic.run()
   111  }
   112  
   113  func (nca *NpmCommandArgs) SetThreads(threads int) *NpmCommandArgs {
   114  	nca.threads = threads
   115  	return nca
   116  }
   117  
   118  func (nca *NpmCommandArgs) SetJsonOutput(jsonOutput bool) *NpmCommandArgs {
   119  	nca.jsonOutput = jsonOutput
   120  	return nca
   121  }
   122  
   123  func NewNpmCommandArgs(npmCommand string) *NpmCommandArgs {
   124  	return &NpmCommandArgs{command: npmCommand}
   125  }
   126  
   127  func (nca *NpmCommandArgs) RtDetails() (*config.ArtifactoryDetails, error) {
   128  	return nca.rtDetails, nil
   129  }
   130  
   131  func (nca *NpmCommandArgs) run() error {
   132  	if err := nca.preparePrerequisites(nca.repo); err != nil {
   133  		return err
   134  	}
   135  
   136  	if err := nca.createTempNpmrc(); err != nil {
   137  		return nca.restoreNpmrcAndError(err)
   138  	}
   139  
   140  	if err := nca.runInstallOrCi(); err != nil {
   141  		return nca.restoreNpmrcAndError(err)
   142  	}
   143  
   144  	if err := nca.restoreNpmrc(); err != nil {
   145  		return err
   146  	}
   147  
   148  	if !nca.collectBuildInfo {
   149  		log.Info(fmt.Sprintf("npm %s finished successfully.", nca.command))
   150  		return nil
   151  	}
   152  
   153  	if err := nca.setDependenciesList(); err != nil {
   154  		return err
   155  	}
   156  
   157  	if err := nca.collectDependenciesChecksums(); err != nil {
   158  		return err
   159  	}
   160  
   161  	if err := nca.saveDependenciesData(); err != nil {
   162  		return err
   163  	}
   164  
   165  	log.Info(fmt.Sprintf("npm %s finished successfully.", nca.command))
   166  	return nil
   167  }
   168  
   169  func (nca *NpmCommandArgs) preparePrerequisites(repo string) error {
   170  	log.Debug("Preparing prerequisites.")
   171  	if err := nca.setNpmExecutable(); err != nil {
   172  		return err
   173  	}
   174  
   175  	if err := nca.validateNpmVersion(); err != nil {
   176  		return err
   177  	}
   178  
   179  	if err := nca.setWorkingDirectory(); err != nil {
   180  		return err
   181  	}
   182  
   183  	if err := nca.prepareArtifactoryPrerequisites(repo); err != nil {
   184  		return err
   185  	}
   186  
   187  	if err := nca.prepareBuildInfo(); err != nil {
   188  		return err
   189  	}
   190  
   191  	return nca.backupProjectNpmrc()
   192  }
   193  
   194  func (nca *NpmCommandArgs) prepareArtifactoryPrerequisites(repo string) (err error) {
   195  	npmAuth, err := getArtifactoryDetails(nca.artDetails)
   196  	if err != nil {
   197  		return err
   198  	}
   199  	nca.npmAuth = npmAuth
   200  
   201  	if err = utils.CheckIfRepoExists(repo, nca.artDetails); err != nil {
   202  		return err
   203  	}
   204  
   205  	nca.registry = getNpmRepositoryUrl(repo, nca.artDetails.GetUrl())
   206  	return nil
   207  }
   208  
   209  func (nca *NpmCommandArgs) prepareBuildInfo() error {
   210  	var err error
   211  	if len(nca.buildConfiguration.BuildName) > 0 && len(nca.buildConfiguration.BuildNumber) > 0 {
   212  		nca.collectBuildInfo = true
   213  		if err = utils.SaveBuildGeneralDetails(nca.buildConfiguration.BuildName, nca.buildConfiguration.BuildNumber); err != nil {
   214  			return err
   215  		}
   216  
   217  		if nca.packageInfo, err = npm.ReadPackageInfoFromPackageJson(nca.workingDirectory); err != nil {
   218  			return err
   219  		}
   220  	}
   221  	return err
   222  }
   223  
   224  func (nca *NpmCommandArgs) setWorkingDirectory() error {
   225  	currentDir, err := os.Getwd()
   226  	if err != nil {
   227  		return errorutils.CheckError(err)
   228  	}
   229  
   230  	if currentDir, err = filepath.Abs(currentDir); err != nil {
   231  		return errorutils.CheckError(err)
   232  	}
   233  
   234  	nca.workingDirectory = currentDir
   235  	log.Debug("Working directory set to:", nca.workingDirectory)
   236  	if err = nca.setArtifactoryAuth(); err != nil {
   237  		return errorutils.CheckError(err)
   238  	}
   239  	return nil
   240  }
   241  
   242  // In order to make sure the install/ci downloads the dependencies from Artifactory, we are creating a.npmrc file in the project's root directory.
   243  // If such a file already exists, we are copying it aside.
   244  // This method restores the backed up file and deletes the one created by the command.
   245  func (nca *NpmCommandArgs) restoreNpmrc() (err error) {
   246  	log.Debug("Restoring project .npmrc file")
   247  	if err = os.Remove(filepath.Join(nca.workingDirectory, npmrcFileName)); err != nil {
   248  		return errorutils.CheckError(errors.New(createRestoreErrorPrefix(nca.workingDirectory) + err.Error()))
   249  	}
   250  	log.Debug("Deleted the temporary .npmrc file successfully")
   251  
   252  	if _, err = os.Stat(filepath.Join(nca.workingDirectory, npmrcBackupFileName)); err != nil {
   253  		if os.IsNotExist(err) {
   254  			return nil
   255  		}
   256  		return errorutils.CheckError(errors.New(createRestoreErrorPrefix(nca.workingDirectory) + err.Error()))
   257  	}
   258  
   259  	if err = ioutils.CopyFile(
   260  		filepath.Join(nca.workingDirectory, npmrcBackupFileName),
   261  		filepath.Join(nca.workingDirectory, npmrcFileName), nca.npmrcFileMode); err != nil {
   262  		return errorutils.CheckError(err)
   263  	}
   264  	log.Debug("Restored project .npmrc file successfully")
   265  
   266  	if err = os.Remove(filepath.Join(nca.workingDirectory, npmrcBackupFileName)); err != nil {
   267  		return errorutils.CheckError(errors.New(createRestoreErrorPrefix(nca.workingDirectory) + err.Error()))
   268  	}
   269  	log.Debug("Deleted project", npmrcBackupFileName, "file successfully")
   270  	return nil
   271  }
   272  
   273  func createRestoreErrorPrefix(workingDirectory string) string {
   274  	return fmt.Sprintf("Error occurred while restoring project .npmrc file. "+
   275  		"Delete '%s' and move '%s' (if exists) to '%s' in order to restore the project. Failure cause: \n",
   276  		filepath.Join(workingDirectory, npmrcFileName),
   277  		filepath.Join(workingDirectory, npmrcBackupFileName),
   278  		filepath.Join(workingDirectory, npmrcFileName))
   279  }
   280  
   281  // In order to make sure the install/ci downloads the artifacts from Artifactory we create a .npmrc file in the project dir.
   282  // If such a file exists we back it up as npmrcBackupFileName.
   283  func (nca *NpmCommandArgs) createTempNpmrc() error {
   284  	log.Debug("Creating project .npmrc file.")
   285  	data, err := npm.GetConfigList(nca.npmArgs, nca.executablePath)
   286  	configData, err := nca.prepareConfigData(data)
   287  	if err != nil {
   288  		return errorutils.CheckError(err)
   289  	}
   290  
   291  	if err = removeNpmrcIfExists(nca.workingDirectory); err != nil {
   292  		return err
   293  	}
   294  
   295  	return errorutils.CheckError(ioutil.WriteFile(filepath.Join(nca.workingDirectory, npmrcFileName), configData, nca.npmrcFileMode))
   296  }
   297  
   298  func (nca *NpmCommandArgs) runInstallOrCi() error {
   299  	log.Debug(fmt.Sprintf("Running npm %s command.", nca.command))
   300  	filteredArgs := filterFlags(nca.npmArgs)
   301  	npmCmdConfig := &npm.NpmConfig{
   302  		Npm:          nca.executablePath,
   303  		Command:      append([]string{nca.command}, filteredArgs...),
   304  		CommandFlags: nil,
   305  		StrWriter:    nil,
   306  		ErrWriter:    nil,
   307  	}
   308  
   309  	if nca.collectBuildInfo && len(filteredArgs) > 0 {
   310  		log.Warn("Build info dependencies collection with npm arguments is not supported. Build info creation will be skipped.")
   311  		nca.collectBuildInfo = false
   312  	}
   313  
   314  	return errorutils.CheckError(gofrogcmd.RunCmd(npmCmdConfig))
   315  }
   316  
   317  func (nca *NpmCommandArgs) setDependenciesList() (err error) {
   318  	nca.dependencies = make(map[string]*dependency)
   319  	// nca.scope can be empty, "production" or "development" in case of empty both of the functions should run
   320  	if nca.typeRestriction != "production" {
   321  		if err = nca.prepareDependencies("development"); err != nil {
   322  			return
   323  		}
   324  	}
   325  	if nca.typeRestriction != "development" {
   326  		err = nca.prepareDependencies("production")
   327  	}
   328  	return
   329  }
   330  
   331  func (nca *NpmCommandArgs) collectDependenciesChecksums() error {
   332  	log.Info("Collecting dependencies information... This may take a few minutes...")
   333  	servicesManager, err := utils.CreateServiceManager(nca.rtDetails, false)
   334  	if err != nil {
   335  		return err
   336  	}
   337  
   338  	previousBuildDependencies, err := getDependenciesFromLatestBuild(servicesManager, nca.buildConfiguration.BuildName)
   339  	if err != nil {
   340  		return err
   341  	}
   342  	producerConsumer := parallel.NewBounedRunner(nca.threads, false)
   343  	errorsQueue := clientutils.NewErrorsQueue(1)
   344  	handlerFunc := nca.createGetDependencyInfoFunc(servicesManager, previousBuildDependencies)
   345  	go func() {
   346  		defer producerConsumer.Done()
   347  		for i := range nca.dependencies {
   348  			producerConsumer.AddTaskWithError(handlerFunc(i), errorsQueue.AddError)
   349  		}
   350  	}()
   351  	producerConsumer.Run()
   352  	return errorsQueue.GetError()
   353  }
   354  
   355  func getDependenciesFromLatestBuild(servicesManager artifactory.ArtifactoryServicesManager, buildName string) (map[string]*buildinfo.Dependency, error) {
   356  	buildDependencies := make(map[string]*buildinfo.Dependency)
   357  	previousBuild, found, err := servicesManager.GetBuildInfo(services.BuildInfoParams{BuildName: buildName, BuildNumber: "LATEST"})
   358  	if err != nil || !found {
   359  		return buildDependencies, err
   360  	}
   361  	for _, module := range previousBuild.BuildInfo.Modules {
   362  		for _, dependency := range module.Dependencies {
   363  			buildDependencies[dependency.Id] = &buildinfo.Dependency{Id: dependency.Id,
   364  				Checksum: &buildinfo.Checksum{Md5: dependency.Md5, Sha1: dependency.Sha1}}
   365  		}
   366  	}
   367  	return buildDependencies, nil
   368  }
   369  
   370  func (nca *NpmCommandArgs) saveDependenciesData() error {
   371  	log.Debug("Saving data.")
   372  	dependencies, missingDependencies := nca.transformDependencies()
   373  	populateFunc := func(partial *buildinfo.Partial) {
   374  		partial.Dependencies = dependencies
   375  		if nca.buildConfiguration.Module == "" {
   376  			nca.buildConfiguration.Module = nca.packageInfo.BuildInfoModuleId()
   377  		}
   378  		partial.ModuleId = nca.buildConfiguration.Module
   379  		partial.ModuleType = buildinfo.Npm
   380  	}
   381  
   382  	if err := utils.SavePartialBuildInfo(nca.buildConfiguration.BuildName, nca.buildConfiguration.BuildNumber, populateFunc); err != nil {
   383  		return err
   384  	}
   385  
   386  	if len(missingDependencies) > 0 {
   387  		var missingDependenciesText []string
   388  		for _, dependency := range missingDependencies {
   389  			missingDependenciesText = append(missingDependenciesText, dependency.name+":"+dependency.version)
   390  		}
   391  		log.Warn(strings.Join(missingDependenciesText, "\n"))
   392  		log.Warn("The npm dependencies above could not be found in Artifactory and therefore are not included in the build-info.\n" +
   393  			"Make sure the dependencies are available in Artifactory for this build.\n" +
   394  			"Deleting the local cache will force populating Artifactory with these dependencies.")
   395  	}
   396  	return nil
   397  }
   398  
   399  func (nca *NpmCommandArgs) validateNpmVersion() error {
   400  	npmVersion, err := npm.Version(nca.executablePath)
   401  	if err != nil {
   402  		return err
   403  	}
   404  	rtVersion := version.NewVersion(string(npmVersion))
   405  	if rtVersion.Compare(minSupportedNpmVersion) > 0 {
   406  		return errorutils.CheckError(errors.New(fmt.Sprintf(
   407  			"JFrog CLI npm %s command requires npm client version "+minSupportedNpmVersion+" or higher", nca.command)))
   408  	}
   409  	return nil
   410  }
   411  
   412  // To make npm do the resolution from Artifactory we are creating .npmrc file in the project dir.
   413  // If a .npmrc file already exists we will backup it and override while running the command
   414  func (nca *NpmCommandArgs) backupProjectNpmrc() error {
   415  	fileInfo, err := os.Stat(filepath.Join(nca.workingDirectory, npmrcFileName))
   416  	if err != nil {
   417  		if os.IsNotExist(err) {
   418  			nca.npmrcFileMode = 0644
   419  			return nil
   420  		}
   421  		return errorutils.CheckError(err)
   422  	}
   423  
   424  	nca.npmrcFileMode = fileInfo.Mode()
   425  	src := filepath.Join(nca.workingDirectory, npmrcFileName)
   426  	dst := filepath.Join(nca.workingDirectory, npmrcBackupFileName)
   427  	if err = ioutils.CopyFile(src, dst, nca.npmrcFileMode); err != nil {
   428  		return err
   429  	}
   430  	log.Debug("Project .npmrc file backed up successfully to", filepath.Join(nca.workingDirectory, npmrcBackupFileName))
   431  	return nil
   432  }
   433  
   434  // This func transforms "npm config list --json" result to key=val list of values that can be set to .npmrc file.
   435  // it filters any nil values key, changes registry and scope registries to Artifactory url and adds Artifactory authentication to the list
   436  func (nca *NpmCommandArgs) prepareConfigData(data []byte) ([]byte, error) {
   437  	var collectedConfig map[string]interface{}
   438  	var filteredConf []string
   439  	if err := json.Unmarshal(data, &collectedConfig); err != nil {
   440  		return nil, errorutils.CheckError(err)
   441  	}
   442  
   443  	for i := range collectedConfig {
   444  		if isValidKeyVal(i, collectedConfig[i]) {
   445  			filteredConf = append(filteredConf, i, " = ", fmt.Sprint(collectedConfig[i]), "\n")
   446  		} else if strings.HasPrefix(i, "@") {
   447  			// Override scoped registries (@scope = xyz)
   448  			filteredConf = append(filteredConf, i, " = ", nca.registry, "\n")
   449  		}
   450  		nca.setTypeRestriction(i, collectedConfig[i])
   451  	}
   452  	filteredConf = append(filteredConf, "json = ", strconv.FormatBool(nca.jsonOutput), "\n")
   453  	filteredConf = append(filteredConf, "registry = ", nca.registry, "\n")
   454  	filteredConf = append(filteredConf, nca.npmAuth)
   455  	return []byte(strings.Join(filteredConf, "")), nil
   456  }
   457  
   458  // npm install/ci type restriction can be set by "--production" or "-only={prod[uction]|dev[elopment]}" flags
   459  func (nca *NpmCommandArgs) setTypeRestriction(key string, val interface{}) {
   460  	if key == "production" && val != nil && (val == true || val == "true") {
   461  		nca.typeRestriction = "production"
   462  	} else if key == "only" && val != nil {
   463  		if strings.Contains(val.(string), "prod") {
   464  			nca.typeRestriction = "production"
   465  		} else if strings.Contains(val.(string), "dev") {
   466  			nca.typeRestriction = "development"
   467  		}
   468  	}
   469  }
   470  
   471  // Run npm list and parse the returned json
   472  func (nca *NpmCommandArgs) prepareDependencies(typeRestriction string) error {
   473  	// Run npm list
   474  	data, errData, err := npm.RunList(strings.Join(append(nca.npmArgs, " -only="+typeRestriction), " "), nca.executablePath)
   475  	if err != nil {
   476  		log.Warn("npm list command failed with error:", err.Error())
   477  	}
   478  	if len(errData) > 0 {
   479  		log.Warn("Some errors occurred while collecting dependencies info:\n" + string(errData))
   480  	}
   481  
   482  	// Parse the dependencies json object
   483  	return jsonparser.ObjectEach(data, func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error {
   484  		if string(key) == "dependencies" {
   485  			err := nca.parseDependencies(value, typeRestriction)
   486  			if err != nil {
   487  				return err
   488  			}
   489  		}
   490  		return nil
   491  	})
   492  }
   493  
   494  // Parses npm dependencies recursively and adds the collected dependencies to nca.dependencies
   495  func (nca *NpmCommandArgs) parseDependencies(data []byte, scope string) error {
   496  	var transitiveDependencies [][]byte
   497  	err := jsonparser.ObjectEach(data, func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error {
   498  		ver, _, _, err := jsonparser.Get(data, string(key), "version")
   499  		if err != nil && err != jsonparser.KeyPathNotFoundError {
   500  			return errorutils.CheckError(err)
   501  		} else if err == jsonparser.KeyPathNotFoundError {
   502  			log.Warn(fmt.Sprintf("npm dependencies list contains the package '%s' without version information. The dependency will not be added to build-info.", string(key)))
   503  		} else {
   504  			nca.appendDependency(key, ver, scope)
   505  		}
   506  		transitive, _, _, err := jsonparser.Get(data, string(key), "dependencies")
   507  		if err != nil && err.Error() != "Key path not found" {
   508  			return errorutils.CheckError(err)
   509  		}
   510  
   511  		if len(transitive) > 0 {
   512  			transitiveDependencies = append(transitiveDependencies, transitive)
   513  		}
   514  		return nil
   515  	})
   516  
   517  	if err != nil {
   518  		return err
   519  	}
   520  
   521  	for _, element := range transitiveDependencies {
   522  		err := nca.parseDependencies(element, scope)
   523  		if err != nil {
   524  			return err
   525  		}
   526  	}
   527  	return nil
   528  }
   529  
   530  func (nca *NpmCommandArgs) appendDependency(key []byte, ver []byte, scope string) {
   531  	dependencyKey := string(key) + ":" + string(ver)
   532  	if nca.dependencies[dependencyKey] == nil {
   533  		nca.dependencies[dependencyKey] = &dependency{name: string(key), version: string(ver), scopes: []string{scope}}
   534  	} else if !scopeAlreadyExists(scope, nca.dependencies[dependencyKey].scopes) {
   535  		nca.dependencies[dependencyKey].scopes = append(nca.dependencies[dependencyKey].scopes, scope)
   536  	}
   537  }
   538  
   539  // Creates a function that fetches dependency data.
   540  // If a dependency was included in the previous build, take the checksums information from it.
   541  // Otherwise, fetch the checksum from Artifactory.
   542  // Can be applied from a producer-consumer mechanism.
   543  func (nca *NpmCommandArgs) createGetDependencyInfoFunc(servicesManager artifactory.ArtifactoryServicesManager,
   544  	previousBuildDependencies map[string]*buildinfo.Dependency) getDependencyInfoFunc {
   545  	return func(dependencyIndex string) parallel.TaskFunc {
   546  		return func(threadId int) error {
   547  			name := nca.dependencies[dependencyIndex].name
   548  			ver := nca.dependencies[dependencyIndex].version
   549  
   550  			// Get dependency info.
   551  			checksum, fileType, err := getDependencyInfo(name, ver, previousBuildDependencies, servicesManager, threadId)
   552  			if err != nil || checksum == nil {
   553  				return err
   554  			}
   555  
   556  			// Update dependency.
   557  			nca.dependencies[dependencyIndex].fileType = fileType
   558  			nca.dependencies[dependencyIndex].checksum = checksum
   559  			return nil
   560  		}
   561  	}
   562  }
   563  
   564  // Get dependency's checksum and type.
   565  func getDependencyInfo(name, ver string, previousBuildDependencies map[string]*buildinfo.Dependency,
   566  	servicesManager artifactory.ArtifactoryServicesManager, threadId int) (checksum *buildinfo.Checksum, fileType string, err error) {
   567  	id := name + ":" + ver
   568  	if dep, ok := previousBuildDependencies[id]; ok {
   569  		// Get checksum from previous build.
   570  		checksum = dep.Checksum
   571  		fileType = dep.Type
   572  		return
   573  	}
   574  
   575  	// Get info from Artifactory.
   576  	log.Debug(clientutils.GetLogMsgPrefix(threadId, false), "Fetching checksums for", name, ":", ver)
   577  	var stream io.ReadCloser
   578  	stream, err = servicesManager.Aql(serviceutils.CreateAqlQueryForNpm(name, ver))
   579  	if err != nil {
   580  		return
   581  	}
   582  	defer stream.Close()
   583  	var result []byte
   584  	result, err = ioutil.ReadAll(stream)
   585  	if err != nil {
   586  		return
   587  	}
   588  	parsedResult := new(aqlResult)
   589  	if err = json.Unmarshal(result, parsedResult); err != nil {
   590  		return nil, "", errorutils.CheckError(err)
   591  	}
   592  	if len(parsedResult.Results) == 0 {
   593  		log.Debug(clientutils.GetLogMsgPrefix(threadId, false), name, ":", ver, "could not be found in Artifactory.")
   594  		return
   595  	}
   596  	if i := strings.LastIndex(parsedResult.Results[0].Name, "."); i != -1 {
   597  		fileType = parsedResult.Results[0].Name[i+1:]
   598  	}
   599  	log.Debug(clientutils.GetLogMsgPrefix(threadId, false), "Found", parsedResult.Results[0].Name,
   600  		"sha1:", parsedResult.Results[0].Actual_sha1,
   601  		"md5", parsedResult.Results[0].Actual_md5)
   602  
   603  	checksum = &buildinfo.Checksum{Sha1: parsedResult.Results[0].Actual_sha1, Md5: parsedResult.Results[0].Actual_md5}
   604  	return
   605  }
   606  
   607  // Transforms the list of dependencies to buildinfo.Dependencies list and creates a list of dependencies that are missing in Artifactory.
   608  func (nca *NpmCommandArgs) transformDependencies() (dependencies []buildinfo.Dependency, missingDependencies []dependency) {
   609  	for _, dependency := range nca.dependencies {
   610  		if dependency.checksum != nil {
   611  			dependencies = append(dependencies,
   612  				buildinfo.Dependency{Id: dependency.name + ":" + dependency.version, Type: dependency.fileType,
   613  					Scopes: dependency.scopes, Checksum: dependency.checksum})
   614  		} else {
   615  			missingDependencies = append(missingDependencies, *dependency)
   616  		}
   617  	}
   618  	return
   619  }
   620  
   621  func (nca *NpmCommandArgs) restoreNpmrcAndError(err error) error {
   622  	if restoreErr := nca.restoreNpmrc(); restoreErr != nil {
   623  		return errors.New(fmt.Sprintf("Two errors occurred:\n %s\n %s", restoreErr.Error(), err.Error()))
   624  	}
   625  	return err
   626  }
   627  
   628  func (nca *NpmCommandArgs) setArtifactoryAuth() error {
   629  	authArtDetails, err := nca.rtDetails.CreateArtAuthConfig()
   630  	if err != nil {
   631  		return err
   632  	}
   633  	if authArtDetails.GetSshAuthHeaders() != nil {
   634  		return errorutils.CheckError(errors.New("SSH authentication is not supported in this command"))
   635  	}
   636  	nca.artDetails = authArtDetails
   637  	return nil
   638  }
   639  
   640  func removeNpmrcIfExists(workingDirectory string) error {
   641  	if _, err := os.Stat(filepath.Join(workingDirectory, npmrcFileName)); err != nil {
   642  		if os.IsNotExist(err) { // The file dose not exist, nothing to do.
   643  			return nil
   644  		}
   645  		return errorutils.CheckError(err)
   646  	}
   647  
   648  	log.Debug("Removing Existing .npmrc file")
   649  	return errorutils.CheckError(os.Remove(filepath.Join(workingDirectory, npmrcFileName)))
   650  }
   651  
   652  func (nca *NpmCommandArgs) setNpmExecutable() error {
   653  	npmExecPath, err := exec.LookPath("npm")
   654  	if err != nil {
   655  		return errorutils.CheckError(err)
   656  	}
   657  
   658  	if npmExecPath == "" {
   659  		return errorutils.CheckError(errors.New("could not find 'npm' executable"))
   660  	}
   661  	nca.executablePath = npmExecPath
   662  	log.Debug("Found npm executable at:", nca.executablePath)
   663  	return nil
   664  }
   665  
   666  func getArtifactoryDetails(artDetails auth.ServiceDetails) (npmAuth string, err error) {
   667  	// Check Artifactory version.
   668  	err = validateArtifactoryVersion(artDetails)
   669  	if err != nil {
   670  		return "", err
   671  	}
   672  
   673  	// Get npm token from Artifactory.
   674  	if artDetails.GetAccessToken() == "" {
   675  		return getDetailsUsingBasicAuth(artDetails)
   676  	}
   677  	return getDetailsUsingAccessToken(artDetails)
   678  }
   679  
   680  func validateArtifactoryVersion(artDetails auth.ServiceDetails) error {
   681  	// Get Artifactory version.
   682  	versionStr, err := artDetails.GetVersion()
   683  	if err != nil {
   684  		return err
   685  	}
   686  
   687  	// Validate version.
   688  	rtVersion := version.NewVersion(versionStr)
   689  	if !rtVersion.AtLeast(minSupportedArtifactoryVersion) {
   690  		return errorutils.CheckError(errors.New("this operation requires Artifactory version " + minSupportedArtifactoryVersion + " or higher"))
   691  	}
   692  
   693  	return nil
   694  }
   695  
   696  func getDetailsUsingAccessToken(artDetails auth.ServiceDetails) (npmAuth string, err error) {
   697  	npmAuthString := "_auth = %s\nalways-auth = true"
   698  	// Build npm token, consists of <username:password> encoded.
   699  	// Use Artifactory's access-token as username and password to create npm token.
   700  	username, err := auth.ExtractUsernameFromAccessToken(artDetails.GetAccessToken())
   701  	if err != nil {
   702  		return "", err
   703  	}
   704  	encodedNpmToken := base64.StdEncoding.EncodeToString([]byte(username + ":" + artDetails.GetAccessToken()))
   705  	npmAuth = fmt.Sprintf(npmAuthString, encodedNpmToken)
   706  
   707  	return npmAuth, err
   708  }
   709  
   710  func getDetailsUsingBasicAuth(artDetails auth.ServiceDetails) (npmAuth string, err error) {
   711  	authApiUrl := artDetails.GetUrl() + "api/npm/auth"
   712  	log.Debug("Sending npm auth request")
   713  
   714  	// Get npm token from Artifactory.
   715  	client, err := httpclient.ClientBuilder().Build()
   716  	if err != nil {
   717  		return "", err
   718  	}
   719  	resp, body, _, err := client.SendGet(authApiUrl, true, artDetails.CreateHttpClientDetails())
   720  	if err != nil {
   721  		return "", err
   722  	}
   723  	if resp.StatusCode != http.StatusOK {
   724  		return "", errorutils.CheckError(errors.New("Artifactory response: " + resp.Status + "\n" + clientutils.IndentJson(body)))
   725  	}
   726  
   727  	return string(body), nil
   728  }
   729  
   730  func getNpmRepositoryUrl(repo, url string) string {
   731  	if !strings.HasSuffix(url, "/") {
   732  		url += "/"
   733  	}
   734  	url += "api/npm/" + repo
   735  	return url
   736  }
   737  
   738  func scopeAlreadyExists(scope string, existingScopes []string) bool {
   739  	for _, existingScope := range existingScopes {
   740  		if existingScope == scope {
   741  			return true
   742  		}
   743  	}
   744  	return false
   745  }
   746  
   747  // Valid configs keys are not related to registry (registry = xyz) or scoped registry (@scope = xyz)) and have data in their value
   748  // We want to avoid writing "json=true" because we ran the the configuration list command with "--json". We will add it explicitly if necessary.
   749  func isValidKeyVal(key string, val interface{}) bool {
   750  	return !strings.HasPrefix(key, "//") &&
   751  		!strings.HasPrefix(key, "@") &&
   752  		key != "registry" &&
   753  		key != "metrics-registry" &&
   754  		key != "json" &&
   755  		val != nil &&
   756  		val != ""
   757  }
   758  
   759  func filterFlags(splitArgs []string) []string {
   760  	var filteredArgs []string
   761  	for _, arg := range splitArgs {
   762  		if !strings.HasPrefix(arg, "-") {
   763  			filteredArgs = append(filteredArgs, arg)
   764  		}
   765  	}
   766  	return filteredArgs
   767  }
   768  
   769  type getDependencyInfoFunc func(string) parallel.TaskFunc
   770  
   771  type dependency struct {
   772  	name     string
   773  	version  string
   774  	scopes   []string
   775  	fileType string
   776  	checksum *buildinfo.Checksum
   777  }
   778  
   779  type aqlResult struct {
   780  	Results []*results `json:"results,omitempty"`
   781  }
   782  
   783  type results struct {
   784  	Name        string `json:"name,omitempty"`
   785  	Actual_md5  string `json:"actual_md5,omitempty"`
   786  	Actual_sha1 string `json:"actual_sha1,omitempty"`
   787  }