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 }