github.com/jfrog/jfrog-cli-core/v2@v2.52.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 commandUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils" 11 "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/npm" 12 buildUtils "github.com/jfrog/jfrog-cli-core/v2/common/build" 13 "github.com/jfrog/jfrog-cli-core/v2/common/project" 14 "github.com/jfrog/jfrog-cli-core/v2/utils/config" 15 "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" 16 "github.com/jfrog/jfrog-cli-core/v2/utils/ioutils" 17 "github.com/jfrog/jfrog-client-go/auth" 18 "github.com/jfrog/jfrog-client-go/utils/errorutils" 19 "github.com/jfrog/jfrog-client-go/utils/log" 20 "github.com/spf13/viper" 21 "os" 22 "path/filepath" 23 "strconv" 24 "strings" 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 106 repoConfig, err := nc.getRepoConfig(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(repoConfig).SetArgs(filteredNpmArgs).SetBuildConfiguration(buildConfiguration) 115 return nil 116 } 117 118 // Get the repository configuration from the config file. 119 // Use the resolver prefix for all commands except for 'dist-tag' which use the deployer prefix. 120 func (nc *NpmCommand) getRepoConfig(vConfig *viper.Viper) (repoConfig *project.RepositoryConfig, err error) { 121 prefix := project.ProjectConfigResolverPrefix 122 // Aliases accepted by npm. 123 if nc.cmdName == "dist-tag" || nc.cmdName == "dist-tags" { 124 prefix = project.ProjectConfigDeployerPrefix 125 } 126 return project.GetRepoConfigByPrefix(nc.configFilePath, prefix, vConfig) 127 } 128 129 func (nc *NpmCommand) SetBuildConfiguration(buildConfiguration *buildUtils.BuildConfiguration) *NpmCommand { 130 nc.buildConfiguration = buildConfiguration 131 return nc 132 } 133 134 func (nc *NpmCommand) ServerDetails() (*config.ServerDetails, error) { 135 return nc.serverDetails, nil 136 } 137 138 func (nc *NpmCommand) RestoreNpmrcFunc() func() error { 139 return nc.restoreNpmrcFunc 140 } 141 142 func (nc *NpmCommand) PreparePrerequisites(repo string) error { 143 log.Debug("Preparing prerequisites...") 144 var err error 145 nc.npmVersion, nc.executablePath, err = biUtils.GetNpmVersionAndExecPath(log.Logger) 146 if err != nil { 147 return err 148 } 149 if nc.npmVersion.Compare(minSupportedNpmVersion) > 0 { 150 return errorutils.CheckErrorf( 151 "JFrog CLI npm %s command requires npm client version %s or higher. The Current version is: %s", nc.cmdName, minSupportedNpmVersion, nc.npmVersion.GetVersion()) 152 } 153 154 if err = nc.setJsonOutput(); err != nil { 155 return err 156 } 157 158 nc.workingDirectory, err = coreutils.GetWorkingDirectory() 159 if err != nil { 160 return err 161 } 162 log.Debug("Working directory set to:", nc.workingDirectory) 163 if err = nc.setArtifactoryAuth(); err != nil { 164 return err 165 } 166 167 nc.npmAuth, nc.registry, err = commandUtils.GetArtifactoryNpmRepoDetails(repo, &nc.authArtDetails) 168 if err != nil { 169 return err 170 } 171 172 return nc.setRestoreNpmrcFunc() 173 } 174 175 func (nc *NpmCommand) setRestoreNpmrcFunc() error { 176 restoreNpmrcFunc, err := ioutils.BackupFile(filepath.Join(nc.workingDirectory, npmrcFileName), npmrcBackupFileName) 177 if err != nil { 178 return err 179 } 180 nc.restoreNpmrcFunc = func() error { 181 if unsetEnvErr := os.Unsetenv(npmConfigAuthEnv); unsetEnvErr != nil { 182 return unsetEnvErr 183 } 184 return restoreNpmrcFunc() 185 } 186 return nil 187 } 188 189 func (nc *NpmCommand) setArtifactoryAuth() error { 190 authArtDetails, err := nc.serverDetails.CreateArtAuthConfig() 191 if err != nil { 192 return err 193 } 194 if authArtDetails.GetSshAuthHeaders() != nil { 195 return errorutils.CheckErrorf("SSH authentication is not supported in this command") 196 } 197 nc.authArtDetails = authArtDetails 198 return nil 199 } 200 201 func (nc *NpmCommand) setJsonOutput() error { 202 jsonOutput, err := npm.ConfigGet(nc.npmArgs, "json", nc.executablePath) 203 if err != nil { 204 return err 205 } 206 207 // In case of --json=<not boolean>, the value of json is set to 'true', but the result from the command is not 'true' 208 nc.jsonOutput = jsonOutput != "false" 209 return nil 210 } 211 212 func (nc *NpmCommand) processConfigLine(configLine string) (filteredLine string, err error) { 213 splitOption := strings.SplitN(configLine, "=", 2) 214 key := strings.TrimSpace(splitOption[0]) 215 validLine := len(splitOption) == 2 && isValidKey(key) 216 if !validLine { 217 if strings.HasPrefix(splitOption[0], "@") { 218 // Override scoped registries (@scope = xyz) 219 return fmt.Sprintf("%s = %s\n", splitOption[0], nc.registry), nil 220 } 221 return 222 } 223 value := strings.TrimSpace(splitOption[1]) 224 if key == "_auth" { 225 return "", nc.setNpmConfigAuthEnv(value) 226 } 227 if strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]") { 228 return addArrayConfigs(key, value), nil 229 } 230 231 return fmt.Sprintf("%s\n", configLine), err 232 } 233 234 func (nc *NpmCommand) setNpmConfigAuthEnv(value string) error { 235 // Check if the npm version is bigger or equal to 9.3.1 236 if nc.npmVersion.Compare(npmVersionForLegacyEnv) <= 0 { 237 // Get registry name without the protocol name but including the '//' 238 registryWithoutProtocolName := nc.registry[strings.Index(nc.registry, "://")+1:] 239 // Set "npm_config_//<registry-url>:_auth" environment variable to allow authentication with Artifactory 240 scopedRegistryEnv := fmt.Sprintf(npmConfigAuthEnv, registryWithoutProtocolName) 241 return os.Setenv(scopedRegistryEnv, value) 242 } 243 // Set "npm_config__auth" environment variable to allow authentication with Artifactory when running post-install scripts on subdirectories. 244 // For Legacy NPM version < 9.3.1 245 return os.Setenv(npmLegacyConfigAuthEnv, value) 246 } 247 248 func (nc *NpmCommand) prepareConfigData(data []byte) ([]byte, error) { 249 var filteredConf []string 250 configString := string(data) + "\n" + nc.npmAuth 251 scanner := bufio.NewScanner(strings.NewReader(configString)) 252 for scanner.Scan() { 253 currOption := scanner.Text() 254 if currOption == "" { 255 continue 256 } 257 filteredLine, err := nc.processConfigLine(currOption) 258 if err != nil { 259 return nil, errorutils.CheckError(err) 260 } 261 if filteredLine != "" { 262 filteredConf = append(filteredConf, filteredLine) 263 } 264 } 265 if err := scanner.Err(); err != nil { 266 return nil, errorutils.CheckError(err) 267 } 268 269 filteredConf = append(filteredConf, "json = ", strconv.FormatBool(nc.jsonOutput), "\n") 270 filteredConf = append(filteredConf, "registry = ", nc.registry, "\n") 271 return []byte(strings.Join(filteredConf, "")), nil 272 } 273 274 func (nc *NpmCommand) CreateTempNpmrc() error { 275 data, err := npm.GetConfigList(nc.npmArgs, nc.executablePath) 276 if err != nil { 277 return err 278 } 279 configData, err := nc.prepareConfigData(data) 280 if err != nil { 281 return errorutils.CheckError(err) 282 } 283 284 if err = removeNpmrcIfExists(nc.workingDirectory); err != nil { 285 return err 286 } 287 log.Debug("Creating temporary .npmrc file.") 288 return errorutils.CheckError(os.WriteFile(filepath.Join(nc.workingDirectory, npmrcFileName), configData, 0755)) 289 } 290 291 func (nc *NpmCommand) Run() (err error) { 292 if err = nc.PreparePrerequisites(nc.repo); err != nil { 293 return 294 } 295 defer func() { 296 err = errors.Join(err, nc.restoreNpmrcFunc()) 297 }() 298 if err = nc.CreateTempNpmrc(); err != nil { 299 return 300 } 301 302 if err = nc.prepareBuildInfoModule(); err != nil { 303 return 304 } 305 306 err = nc.collectDependencies() 307 return 308 } 309 310 func (nc *NpmCommand) prepareBuildInfoModule() error { 311 var err error 312 if nc.collectBuildInfo { 313 nc.collectBuildInfo, err = nc.buildConfiguration.IsCollectBuildInfo() 314 if err != nil { 315 return err 316 } 317 } 318 // Build-info should not be created when installing a single package (npm install <package name>). 319 if nc.collectBuildInfo && len(filterFlags(nc.npmArgs)) > 0 { 320 log.Info("Build-info dependencies collection is not supported for installations of single packages. Build-info creation is skipped.") 321 nc.collectBuildInfo = false 322 } 323 buildName, err := nc.buildConfiguration.GetBuildName() 324 if err != nil { 325 return err 326 } 327 buildNumber, err := nc.buildConfiguration.GetBuildNumber() 328 if err != nil { 329 return err 330 } 331 buildInfoService := buildUtils.CreateBuildInfoService() 332 npmBuild, err := buildInfoService.GetOrCreateBuildWithProject(buildName, buildNumber, nc.buildConfiguration.GetProject()) 333 if err != nil { 334 return errorutils.CheckError(err) 335 } 336 nc.buildInfoModule, err = npmBuild.AddNpmModule(nc.workingDirectory) 337 if err != nil { 338 return errorutils.CheckError(err) 339 } 340 nc.buildInfoModule.SetCollectBuildInfo(nc.collectBuildInfo) 341 if nc.buildConfiguration.GetModule() != "" { 342 nc.buildInfoModule.SetName(nc.buildConfiguration.GetModule()) 343 } 344 return nil 345 } 346 347 func (nc *NpmCommand) collectDependencies() error { 348 nc.buildInfoModule.SetNpmArgs(append([]string{nc.cmdName}, nc.npmArgs...)) 349 return errorutils.CheckError(nc.buildInfoModule.Build()) 350 } 351 352 // Gets a config with value which is an array 353 func addArrayConfigs(key, arrayValue string) string { 354 if arrayValue == "[]" { 355 return "" 356 } 357 358 values := strings.TrimPrefix(strings.TrimSuffix(arrayValue, "]"), "[") 359 valuesSlice := strings.Split(values, ",") 360 var configArrayValues strings.Builder 361 for _, val := range valuesSlice { 362 configArrayValues.WriteString(fmt.Sprintf("%s[] = %s\n", key, val)) 363 } 364 365 return configArrayValues.String() 366 } 367 368 func removeNpmrcIfExists(workingDirectory string) error { 369 if _, err := os.Stat(filepath.Join(workingDirectory, npmrcFileName)); err != nil { 370 // The file does not exist, nothing to do. 371 if os.IsNotExist(err) { 372 return nil 373 } 374 return errorutils.CheckError(err) 375 } 376 377 log.Debug("Removing existing .npmrc file") 378 return errorutils.CheckError(os.Remove(filepath.Join(workingDirectory, npmrcFileName))) 379 } 380 381 // To avoid writing configurations that are used by us 382 func isValidKey(key string) bool { 383 return !strings.HasPrefix(key, "//") && 384 !strings.HasPrefix(key, ";") && // Comments 385 !strings.HasPrefix(key, "@") && // Scoped configurations 386 key != "registry" && 387 key != "metrics-registry" && 388 key != "json" // Handled separately because 'npm c ls' should run with json=false 389 } 390 391 func filterFlags(splitArgs []string) []string { 392 var filteredArgs []string 393 for _, arg := range splitArgs { 394 if !strings.HasPrefix(arg, "-") { 395 filteredArgs = append(filteredArgs, arg) 396 } 397 } 398 return filteredArgs 399 } 400 401 func (nc *NpmCommand) GetRepo() string { 402 return nc.repo 403 } 404 405 // Creates an .npmrc file in the project's directory in order to configure the provided Artifactory server as a resolution server 406 func SetArtifactoryAsResolutionServer(serverDetails *config.ServerDetails, depsRepo string) (clearResolutionServerFunc func() error, err error) { 407 npmCmd := NewNpmInstallCommand().SetServerDetails(serverDetails) 408 if err = npmCmd.PreparePrerequisites(depsRepo); err != nil { 409 return 410 } 411 if err = npmCmd.CreateTempNpmrc(); err != nil { 412 return 413 } 414 clearResolutionServerFunc = npmCmd.RestoreNpmrcFunc() 415 log.Info(fmt.Sprintf("Resolving dependencies from '%s' from repo '%s'", serverDetails.Url, depsRepo)) 416 return 417 }