github.com/jfrog/jfrog-cli-core/v2@v2.51.0/artifactory/commands/npm/npmcommand.go (about)

     1  package npm
     2  
     3  import (
     4  	"bufio"
     5  	"errors"
     6  	"fmt"
     7  	"github.com/jfrog/build-info-go/build"
     8  	biutils "github.com/jfrog/build-info-go/build/utils"
     9  	"github.com/jfrog/gofrog/version"
    10  	"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/npm"
    11  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
    12  	"github.com/jfrog/jfrog-client-go/auth"
    13  	"os"
    14  	"path/filepath"
    15  	"strconv"
    16  	"strings"
    17  
    18  	commandUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils"
    19  	buildUtils "github.com/jfrog/jfrog-cli-core/v2/common/build"
    20  	"github.com/jfrog/jfrog-cli-core/v2/common/project"
    21  	"github.com/jfrog/jfrog-cli-core/v2/utils/config"
    22  	"github.com/jfrog/jfrog-cli-core/v2/utils/ioutils"
    23  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    24  	"github.com/jfrog/jfrog-client-go/utils/log"
    25  )
    26  
    27  const (
    28  	npmrcFileName          = ".npmrc"
    29  	npmrcBackupFileName    = "jfrog.npmrc.backup"
    30  	minSupportedNpmVersion = "5.4.0"
    31  )
    32  
    33  type NpmCommand struct {
    34  	CommonArgs
    35  	cmdName        string
    36  	jsonOutput     bool
    37  	executablePath string
    38  	// Function to be called to restore the user's old npmrc and delete the one we created.
    39  	restoreNpmrcFunc func() error
    40  	workingDirectory string
    41  	// Npm registry as exposed by Artifactory.
    42  	registry string
    43  	// Npm token generated by Artifactory using the user's provided credentials.
    44  	npmAuth             string
    45  	authArtDetails      auth.ServiceDetails
    46  	npmVersion          *version.Version
    47  	internalCommandName string
    48  	configFilePath      string
    49  	collectBuildInfo    bool
    50  	buildInfoModule     *build.NpmModule
    51  }
    52  
    53  func NewNpmCommand(cmdName string, collectBuildInfo bool) *NpmCommand {
    54  	return &NpmCommand{
    55  		cmdName:          cmdName,
    56  		collectBuildInfo: collectBuildInfo,
    57  	}
    58  }
    59  
    60  func NewNpmInstallCommand() *NpmCommand {
    61  	return &NpmCommand{cmdName: "install", internalCommandName: "rt_npm_install"}
    62  }
    63  
    64  func NewNpmCiCommand() *NpmCommand {
    65  	return &NpmCommand{cmdName: "ci", internalCommandName: "rt_npm_ci"}
    66  }
    67  
    68  func (nc *NpmCommand) CommandName() string {
    69  	return nc.internalCommandName
    70  }
    71  
    72  func (nc *NpmCommand) SetConfigFilePath(configFilePath string) *NpmCommand {
    73  	nc.configFilePath = configFilePath
    74  	return nc
    75  }
    76  
    77  func (nc *NpmCommand) SetArgs(args []string) *NpmCommand {
    78  	nc.npmArgs = args
    79  	return nc
    80  }
    81  
    82  func (nc *NpmCommand) SetRepoConfig(conf *project.RepositoryConfig) *NpmCommand {
    83  	serverDetails, _ := conf.ServerDetails()
    84  	nc.SetRepo(conf.TargetRepo()).SetServerDetails(serverDetails)
    85  	return nc
    86  }
    87  
    88  func (nc *NpmCommand) SetServerDetails(serverDetails *config.ServerDetails) *NpmCommand {
    89  	nc.serverDetails = serverDetails
    90  	return nc
    91  }
    92  
    93  func (nc *NpmCommand) SetRepo(repo string) *NpmCommand {
    94  	nc.repo = repo
    95  	return nc
    96  }
    97  
    98  func (nc *NpmCommand) Init() error {
    99  	// Read config file.
   100  	log.Debug("Preparing to read the config file", nc.configFilePath)
   101  	vConfig, err := project.ReadConfigFile(nc.configFilePath, project.YAML)
   102  	if err != nil {
   103  		return err
   104  	}
   105  	// Extract resolution params.
   106  	resolverParams, err := project.GetRepoConfigByPrefix(nc.configFilePath, project.ProjectConfigResolverPrefix, vConfig)
   107  	if err != nil {
   108  		return err
   109  	}
   110  	_, _, _, filteredNpmArgs, buildConfiguration, err := commandUtils.ExtractNpmOptionsFromArgs(nc.npmArgs)
   111  	if err != nil {
   112  		return err
   113  	}
   114  	nc.SetRepoConfig(resolverParams).SetArgs(filteredNpmArgs).SetBuildConfiguration(buildConfiguration)
   115  	return nil
   116  }
   117  
   118  func (nc *NpmCommand) SetBuildConfiguration(buildConfiguration *buildUtils.BuildConfiguration) *NpmCommand {
   119  	nc.buildConfiguration = buildConfiguration
   120  	return nc
   121  }
   122  
   123  func (nc *NpmCommand) ServerDetails() (*config.ServerDetails, error) {
   124  	return nc.serverDetails, nil
   125  }
   126  
   127  func (nc *NpmCommand) RestoreNpmrcFunc() func() error {
   128  	return nc.restoreNpmrcFunc
   129  }
   130  
   131  func (nc *NpmCommand) PreparePrerequisites(repo string) error {
   132  	log.Debug("Preparing prerequisites...")
   133  	var err error
   134  	nc.npmVersion, nc.executablePath, err = biutils.GetNpmVersionAndExecPath(log.Logger)
   135  	if err != nil {
   136  		return err
   137  	}
   138  	if nc.npmVersion.Compare(minSupportedNpmVersion) > 0 {
   139  		return errorutils.CheckErrorf(
   140  			"JFrog CLI npm %s command requires npm client version %s or higher. The Current version is: %s", nc.cmdName, minSupportedNpmVersion, nc.npmVersion.GetVersion())
   141  	}
   142  
   143  	if err = nc.setJsonOutput(); err != nil {
   144  		return err
   145  	}
   146  
   147  	nc.workingDirectory, err = coreutils.GetWorkingDirectory()
   148  	if err != nil {
   149  		return err
   150  	}
   151  	log.Debug("Working directory set to:", nc.workingDirectory)
   152  	if err = nc.setArtifactoryAuth(); err != nil {
   153  		return err
   154  	}
   155  
   156  	nc.npmAuth, nc.registry, err = commandUtils.GetArtifactoryNpmRepoDetails(repo, &nc.authArtDetails)
   157  	if err != nil {
   158  		return err
   159  	}
   160  
   161  	return nc.setRestoreNpmrcFunc()
   162  }
   163  
   164  func (nc *NpmCommand) setRestoreNpmrcFunc() error {
   165  	restoreNpmrcFunc, err := ioutils.BackupFile(filepath.Join(nc.workingDirectory, npmrcFileName), npmrcBackupFileName)
   166  	if err != nil {
   167  		return err
   168  	}
   169  	nc.restoreNpmrcFunc = func() error {
   170  		if unsetEnvErr := os.Unsetenv(npmConfigAuthEnv); unsetEnvErr != nil {
   171  			return unsetEnvErr
   172  		}
   173  		return restoreNpmrcFunc()
   174  	}
   175  	return nil
   176  }
   177  
   178  func (nc *NpmCommand) setArtifactoryAuth() error {
   179  	authArtDetails, err := nc.serverDetails.CreateArtAuthConfig()
   180  	if err != nil {
   181  		return err
   182  	}
   183  	if authArtDetails.GetSshAuthHeaders() != nil {
   184  		return errorutils.CheckErrorf("SSH authentication is not supported in this command")
   185  	}
   186  	nc.authArtDetails = authArtDetails
   187  	return nil
   188  }
   189  
   190  func (nc *NpmCommand) setJsonOutput() error {
   191  	jsonOutput, err := npm.ConfigGet(nc.npmArgs, "json", nc.executablePath)
   192  	if err != nil {
   193  		return err
   194  	}
   195  
   196  	// In case of --json=<not boolean>, the value of json is set to 'true', but the result from the command is not 'true'
   197  	nc.jsonOutput = jsonOutput != "false"
   198  	return nil
   199  }
   200  
   201  func (nc *NpmCommand) processConfigLine(configLine string) (filteredLine string, err error) {
   202  	splitOption := strings.SplitN(configLine, "=", 2)
   203  	key := strings.TrimSpace(splitOption[0])
   204  	validLine := len(splitOption) == 2 && isValidKey(key)
   205  	if !validLine {
   206  		if strings.HasPrefix(splitOption[0], "@") {
   207  			// Override scoped registries (@scope = xyz)
   208  			return fmt.Sprintf("%s = %s\n", splitOption[0], nc.registry), nil
   209  		}
   210  		return
   211  	}
   212  	value := strings.TrimSpace(splitOption[1])
   213  	if key == "_auth" {
   214  		return "", nc.setNpmConfigAuthEnv(value)
   215  	}
   216  	if strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]") {
   217  		return addArrayConfigs(key, value), nil
   218  	}
   219  
   220  	return fmt.Sprintf("%s\n", configLine), err
   221  }
   222  
   223  func (nc *NpmCommand) setNpmConfigAuthEnv(value string) error {
   224  	// Check if the npm version is bigger or equal to 9.3.1
   225  	if nc.npmVersion.Compare(npmVersionForLegacyEnv) <= 0 {
   226  		// Get registry name without the protocol name but including the '//'
   227  		registryWithoutProtocolName := nc.registry[strings.Index(nc.registry, "://")+1:]
   228  		// Set "npm_config_//<registry-url>:_auth" environment variable to allow authentication with Artifactory
   229  		scopedRegistryEnv := fmt.Sprintf(npmConfigAuthEnv, registryWithoutProtocolName)
   230  		return os.Setenv(scopedRegistryEnv, value)
   231  	}
   232  	// Set "npm_config__auth" environment variable to allow authentication with Artifactory when running postinstall scripts on subdirectories.
   233  	// For Legacy NPM version < 9.3.1
   234  	return os.Setenv(npmLegacyConfigAuthEnv, value)
   235  }
   236  
   237  func (nc *NpmCommand) prepareConfigData(data []byte) ([]byte, error) {
   238  	var filteredConf []string
   239  	configString := string(data) + "\n" + nc.npmAuth
   240  	scanner := bufio.NewScanner(strings.NewReader(configString))
   241  	for scanner.Scan() {
   242  		currOption := scanner.Text()
   243  		if currOption == "" {
   244  			continue
   245  		}
   246  		filteredLine, err := nc.processConfigLine(currOption)
   247  		if err != nil {
   248  			return nil, errorutils.CheckError(err)
   249  		}
   250  		if filteredLine != "" {
   251  			filteredConf = append(filteredConf, filteredLine)
   252  		}
   253  	}
   254  	if err := scanner.Err(); err != nil {
   255  		return nil, errorutils.CheckError(err)
   256  	}
   257  
   258  	filteredConf = append(filteredConf, "json = ", strconv.FormatBool(nc.jsonOutput), "\n")
   259  	filteredConf = append(filteredConf, "registry = ", nc.registry, "\n")
   260  	return []byte(strings.Join(filteredConf, "")), nil
   261  }
   262  
   263  func (nc *NpmCommand) CreateTempNpmrc() error {
   264  	data, err := npm.GetConfigList(nc.npmArgs, nc.executablePath)
   265  	if err != nil {
   266  		return err
   267  	}
   268  	configData, err := nc.prepareConfigData(data)
   269  	if err != nil {
   270  		return errorutils.CheckError(err)
   271  	}
   272  
   273  	if err = removeNpmrcIfExists(nc.workingDirectory); err != nil {
   274  		return err
   275  	}
   276  	log.Debug("Creating temporary .npmrc file.")
   277  	return errorutils.CheckError(os.WriteFile(filepath.Join(nc.workingDirectory, npmrcFileName), configData, 0755))
   278  }
   279  
   280  func (nc *NpmCommand) Run() (err error) {
   281  	if err = nc.PreparePrerequisites(nc.repo); err != nil {
   282  		return
   283  	}
   284  	defer func() {
   285  		err = errors.Join(err, nc.restoreNpmrcFunc())
   286  	}()
   287  	if err = nc.CreateTempNpmrc(); err != nil {
   288  		return
   289  	}
   290  
   291  	if err = nc.prepareBuildInfoModule(); err != nil {
   292  		return
   293  	}
   294  
   295  	err = nc.collectDependencies()
   296  	return
   297  }
   298  
   299  func (nc *NpmCommand) prepareBuildInfoModule() error {
   300  	var err error
   301  	if nc.collectBuildInfo {
   302  		nc.collectBuildInfo, err = nc.buildConfiguration.IsCollectBuildInfo()
   303  		if err != nil {
   304  			return err
   305  		}
   306  	}
   307  	// Build-info should not be created when installing a single package (npm install <package name>).
   308  	if nc.collectBuildInfo && len(filterFlags(nc.npmArgs)) > 0 {
   309  		log.Info("Build-info dependencies collection is not supported for installations of single packages. Build-info creation is skipped.")
   310  		nc.collectBuildInfo = false
   311  	}
   312  	buildName, err := nc.buildConfiguration.GetBuildName()
   313  	if err != nil {
   314  		return err
   315  	}
   316  	buildNumber, err := nc.buildConfiguration.GetBuildNumber()
   317  	if err != nil {
   318  		return err
   319  	}
   320  	buildInfoService := buildUtils.CreateBuildInfoService()
   321  	npmBuild, err := buildInfoService.GetOrCreateBuildWithProject(buildName, buildNumber, nc.buildConfiguration.GetProject())
   322  	if err != nil {
   323  		return errorutils.CheckError(err)
   324  	}
   325  	nc.buildInfoModule, err = npmBuild.AddNpmModule(nc.workingDirectory)
   326  	if err != nil {
   327  		return errorutils.CheckError(err)
   328  	}
   329  	nc.buildInfoModule.SetCollectBuildInfo(nc.collectBuildInfo)
   330  	if nc.buildConfiguration.GetModule() != "" {
   331  		nc.buildInfoModule.SetName(nc.buildConfiguration.GetModule())
   332  	}
   333  	return nil
   334  }
   335  
   336  func (nc *NpmCommand) collectDependencies() error {
   337  	nc.buildInfoModule.SetNpmArgs(append([]string{nc.cmdName}, nc.npmArgs...))
   338  	return errorutils.CheckError(nc.buildInfoModule.Build())
   339  }
   340  
   341  // Gets a config with value which is an array
   342  func addArrayConfigs(key, arrayValue string) string {
   343  	if arrayValue == "[]" {
   344  		return ""
   345  	}
   346  
   347  	values := strings.TrimPrefix(strings.TrimSuffix(arrayValue, "]"), "[")
   348  	valuesSlice := strings.Split(values, ",")
   349  	var configArrayValues strings.Builder
   350  	for _, val := range valuesSlice {
   351  		configArrayValues.WriteString(fmt.Sprintf("%s[] = %s\n", key, val))
   352  	}
   353  
   354  	return configArrayValues.String()
   355  }
   356  
   357  func removeNpmrcIfExists(workingDirectory string) error {
   358  	if _, err := os.Stat(filepath.Join(workingDirectory, npmrcFileName)); err != nil {
   359  		// The file does not exist, nothing to do.
   360  		if os.IsNotExist(err) {
   361  			return nil
   362  		}
   363  		return errorutils.CheckError(err)
   364  	}
   365  
   366  	log.Debug("Removing existing .npmrc file")
   367  	return errorutils.CheckError(os.Remove(filepath.Join(workingDirectory, npmrcFileName)))
   368  }
   369  
   370  // To avoid writing configurations that are used by us
   371  func isValidKey(key string) bool {
   372  	return !strings.HasPrefix(key, "//") &&
   373  		!strings.HasPrefix(key, ";") && // Comments
   374  		!strings.HasPrefix(key, "@") && // Scoped configurations
   375  		key != "registry" &&
   376  		key != "metrics-registry" &&
   377  		key != "json" // Handled separately because 'npm c ls' should run with json=false
   378  }
   379  
   380  func filterFlags(splitArgs []string) []string {
   381  	var filteredArgs []string
   382  	for _, arg := range splitArgs {
   383  		if !strings.HasPrefix(arg, "-") {
   384  			filteredArgs = append(filteredArgs, arg)
   385  		}
   386  	}
   387  	return filteredArgs
   388  }
   389  
   390  func (nc *NpmCommand) GetRepo() string {
   391  	return nc.repo
   392  }
   393  
   394  // Creates an .npmrc file in the project's directory in order to configure the provided Artifactory server as a resolution server
   395  func SetArtifactoryAsResolutionServer(serverDetails *config.ServerDetails, depsRepo string) (clearResolutionServerFunc func() error, err error) {
   396  	npmCmd := NewNpmInstallCommand().SetServerDetails(serverDetails)
   397  	if err = npmCmd.PreparePrerequisites(depsRepo); err != nil {
   398  		return
   399  	}
   400  	if err = npmCmd.CreateTempNpmrc(); err != nil {
   401  		return
   402  	}
   403  	clearResolutionServerFunc = npmCmd.RestoreNpmrcFunc()
   404  	log.Info(fmt.Sprintf("Resolving dependencies from '%s' from repo '%s'", serverDetails.Url, depsRepo))
   405  	return
   406  }