github.com/jfrog/jfrog-cli-core/v2@v2.51.0/artifactory/commands/yarn/yarn.go (about) 1 package yarn 2 3 import ( 4 "bufio" 5 "encoding/json" 6 "errors" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "strings" 11 12 "github.com/jfrog/build-info-go/build" 13 buildUtils "github.com/jfrog/jfrog-cli-core/v2/common/build" 14 15 commandUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils" 16 "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" 17 "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/yarn" 18 "github.com/jfrog/jfrog-cli-core/v2/common/project" 19 "github.com/jfrog/jfrog-cli-core/v2/utils/config" 20 "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" 21 "github.com/jfrog/jfrog-cli-core/v2/utils/ioutils" 22 "github.com/jfrog/jfrog-client-go/auth" 23 "github.com/jfrog/jfrog-client-go/utils/errorutils" 24 "github.com/jfrog/jfrog-client-go/utils/log" 25 ) 26 27 const ( 28 YarnrcFileName = ".yarnrc.yml" 29 YarnrcBackupFileName = "jfrog.yarnrc.backup" 30 NpmScopesConfigName = "npmScopes" 31 YarnLockFileName = "yarn.lock" 32 //#nosec G101 33 yarnNpmRegistryServerEnv = "YARN_NPM_REGISTRY_SERVER" 34 yarnNpmAuthIndent = "YARN_NPM_AUTH_IDENT" 35 yarnNpmAlwaysAuth = "YARN_NPM_ALWAYS_AUTH" 36 ) 37 38 type YarnCommand struct { 39 executablePath string 40 workingDirectory string 41 registry string 42 npmAuthIdent string 43 repo string 44 collectBuildInfo bool 45 configFilePath string 46 yarnArgs []string 47 threads int 48 serverDetails *config.ServerDetails 49 buildConfiguration *buildUtils.BuildConfiguration 50 buildInfoModule *build.YarnModule 51 } 52 53 func NewYarnCommand() *YarnCommand { 54 return &YarnCommand{} 55 } 56 57 func (yc *YarnCommand) SetConfigFilePath(configFilePath string) *YarnCommand { 58 yc.configFilePath = configFilePath 59 return yc 60 } 61 62 func (yc *YarnCommand) SetArgs(args []string) *YarnCommand { 63 yc.yarnArgs = args 64 return yc 65 } 66 67 func (yc *YarnCommand) Run() (err error) { 68 log.Info("Running Yarn...") 69 if err = yc.validateSupportedCommand(); err != nil { 70 return 71 } 72 73 if err = yc.readConfigFile(); err != nil { 74 return 75 } 76 77 var filteredYarnArgs []string 78 yc.threads, _, _, _, filteredYarnArgs, yc.buildConfiguration, err = commandUtils.ExtractYarnOptionsFromArgs(yc.yarnArgs) 79 if err != nil { 80 return 81 } 82 83 if err = yc.preparePrerequisites(); err != nil { 84 return 85 } 86 87 var missingDepsChan chan string 88 var missingDependencies []string 89 if yc.collectBuildInfo { 90 missingDepsChan, err = yc.prepareBuildInfo() 91 if err != nil { 92 return 93 } 94 go func() { 95 for depId := range missingDepsChan { 96 missingDependencies = append(missingDependencies, depId) 97 } 98 }() 99 } 100 101 restoreYarnrcFunc, err := ioutils.BackupFile(filepath.Join(yc.workingDirectory, YarnrcFileName), YarnrcBackupFileName) 102 if err != nil { 103 return errors.Join(err, restoreYarnrcFunc()) 104 } 105 backupEnvMap, err := ModifyYarnConfigurations(yc.executablePath, yc.registry, yc.npmAuthIdent) 106 if err != nil { 107 return errors.Join(err, restoreYarnrcFunc()) 108 } 109 110 yc.buildInfoModule.SetArgs(filteredYarnArgs) 111 if err = yc.buildInfoModule.Build(); err != nil { 112 return errors.Join(err, restoreYarnrcFunc()) 113 } 114 115 if yc.collectBuildInfo { 116 close(missingDepsChan) 117 commandUtils.PrintMissingDependencies(missingDependencies) 118 } 119 120 if err = RestoreConfigurationsFromBackup(backupEnvMap, restoreYarnrcFunc); err != nil { 121 return 122 } 123 124 log.Info("Yarn finished successfully.") 125 return 126 } 127 128 func (yc *YarnCommand) ServerDetails() (*config.ServerDetails, error) { 129 return yc.serverDetails, nil 130 } 131 132 func (yc *YarnCommand) CommandName() string { 133 return "rt_yarn" 134 } 135 136 func (yc *YarnCommand) validateSupportedCommand() error { 137 for index, arg := range yc.yarnArgs { 138 if arg == "npm" && len(yc.yarnArgs) > index { 139 npmCommand := yc.yarnArgs[index+1] 140 // The command 'yarn npm publish' is not supported 141 if npmCommand == "publish" { 142 return errorutils.CheckErrorf("The command 'jfrog rt yarn npm publish' is not supported. Use 'jfrog rt upload' instead.") 143 } 144 // 'yarn npm *' commands other than 'info' and 'whoami' are not supported 145 if npmCommand != "info" && npmCommand != "whoami" { 146 return errorutils.CheckErrorf("The command 'jfrog rt yarn npm %s' is not supported.", npmCommand) 147 } 148 } 149 } 150 return nil 151 } 152 153 func (yc *YarnCommand) readConfigFile() error { 154 log.Debug("Preparing to read the config file", yc.configFilePath) 155 vConfig, err := project.ReadConfigFile(yc.configFilePath, project.YAML) 156 if err != nil { 157 return err 158 } 159 160 // Extract resolution params 161 resolverParams, err := project.GetRepoConfigByPrefix(yc.configFilePath, project.ProjectConfigResolverPrefix, vConfig) 162 if err != nil { 163 return err 164 } 165 yc.repo = resolverParams.TargetRepo() 166 yc.serverDetails, err = resolverParams.ServerDetails() 167 return err 168 } 169 170 func (yc *YarnCommand) preparePrerequisites() error { 171 log.Debug("Preparing prerequisites.") 172 var err error 173 if err = yc.setYarnExecutable(); err != nil { 174 return err 175 } 176 177 yc.workingDirectory, err = coreutils.GetWorkingDirectory() 178 if err != nil { 179 return err 180 } 181 log.Debug("Working directory set to:", yc.workingDirectory) 182 183 yc.collectBuildInfo, err = yc.buildConfiguration.IsCollectBuildInfo() 184 if err != nil { 185 return err 186 } 187 188 buildName, err := yc.buildConfiguration.GetBuildName() 189 if err != nil { 190 return err 191 } 192 buildNumber, err := yc.buildConfiguration.GetBuildNumber() 193 if err != nil { 194 return err 195 } 196 197 buildInfoService := buildUtils.CreateBuildInfoService() 198 npmBuild, err := buildInfoService.GetOrCreateBuildWithProject(buildName, buildNumber, yc.buildConfiguration.GetProject()) 199 if err != nil { 200 return errorutils.CheckError(err) 201 } 202 yc.buildInfoModule, err = npmBuild.AddYarnModule(yc.workingDirectory) 203 if err != nil { 204 return errorutils.CheckError(err) 205 } 206 if yc.buildConfiguration.GetModule() != "" { 207 yc.buildInfoModule.SetName(yc.buildConfiguration.GetModule()) 208 } 209 210 yc.registry, yc.npmAuthIdent, err = GetYarnAuthDetails(yc.serverDetails, yc.repo) 211 return err 212 } 213 214 func (yc *YarnCommand) prepareBuildInfo() (missingDepsChan chan string, err error) { 215 log.Info("Preparing for dependencies information collection... For the first run of the build, the dependencies collection may take a few minutes. Subsequent runs should be faster.") 216 servicesManager, err := utils.CreateServiceManager(yc.serverDetails, -1, 0, false) 217 if err != nil { 218 return 219 } 220 221 // Collect checksums from last build to decrease requests to Artifactory 222 buildName, err := yc.buildConfiguration.GetBuildName() 223 if err != nil { 224 return 225 } 226 previousBuildDependencies, err := commandUtils.GetDependenciesFromLatestBuild(servicesManager, buildName) 227 if err != nil { 228 return 229 } 230 missingDepsChan = make(chan string) 231 collectChecksumsFunc := commandUtils.CreateCollectChecksumsFunc(previousBuildDependencies, servicesManager, missingDepsChan) 232 yc.buildInfoModule.SetTraverseDependenciesFunc(collectChecksumsFunc) 233 yc.buildInfoModule.SetThreads(yc.threads) 234 return 235 } 236 237 func (yc *YarnCommand) setYarnExecutable() error { 238 yarnExecPath, err := exec.LookPath("yarn") 239 if err != nil { 240 return errorutils.CheckError(err) 241 } 242 243 yc.executablePath = yarnExecPath 244 log.Debug("Found Yarn executable at:", yc.executablePath) 245 return nil 246 } 247 248 func GetYarnAuthDetails(server *config.ServerDetails, repo string) (string, string, error) { 249 authRtDetails, err := setArtifactoryAuth(server) 250 if err != nil { 251 return "", "", err 252 } 253 var npmAuthOutput string 254 npmAuthOutput, registry, err := commandUtils.GetArtifactoryNpmRepoDetails(repo, &authRtDetails) 255 if err != nil { 256 return "", "", err 257 } 258 npmAuthIdent, err := extractAuthIdentFromNpmAuth(npmAuthOutput) 259 if err != nil { 260 return "", "", err 261 } 262 return registry, npmAuthIdent, nil 263 } 264 265 func setArtifactoryAuth(server *config.ServerDetails) (auth.ServiceDetails, error) { 266 authArtDetails, err := server.CreateArtAuthConfig() 267 if err != nil { 268 return nil, err 269 } 270 if authArtDetails.GetSshAuthHeaders() != nil { 271 return nil, errorutils.CheckErrorf("SSH authentication is not supported in this command") 272 } 273 return authArtDetails, nil 274 } 275 276 func RestoreConfigurationsFromBackup(envVarsBackup map[string]*string, restoreYarnrcFunc func() error) error { 277 if err := restoreEnvironmentVariables(envVarsBackup); err != nil { 278 return err 279 } 280 return restoreYarnrcFunc() 281 } 282 283 func restoreEnvironmentVariables(envVarsBackup map[string]*string) error { 284 for key, value := range envVarsBackup { 285 if value == nil || *value == "" { 286 if err := os.Unsetenv(key); err != nil { 287 return err 288 } 289 continue 290 } 291 292 if err := os.Setenv(key, *value); err != nil { 293 return err 294 } 295 } 296 return nil 297 } 298 299 func ModifyYarnConfigurations(execPath, registry, npmAuthIdent string) (map[string]*string, error) { 300 envVarsUpdated := map[string]string{ 301 yarnNpmRegistryServerEnv: registry, 302 yarnNpmAuthIndent: npmAuthIdent, 303 yarnNpmAlwaysAuth: "true", 304 } 305 envVarsBackup := make(map[string]*string) 306 for key, value := range envVarsUpdated { 307 oldVal, err := backupAndSetEnvironmentVariable(key, value) 308 if err != nil { 309 return nil, err 310 } 311 envVarsBackup[key] = &oldVal 312 } 313 // Update scoped registries (these cannot be set in environment variables) 314 return envVarsBackup, errorutils.CheckError(updateScopeRegistries(execPath, registry, npmAuthIdent)) 315 } 316 317 func updateScopeRegistries(execPath, registry, npmAuthIdent string) error { 318 npmScopesStr, err := yarn.ConfigGet(NpmScopesConfigName, execPath, true) 319 if err != nil { 320 return err 321 } 322 npmScopesMap := make(map[string]yarnNpmScope) 323 err = json.Unmarshal([]byte(npmScopesStr), &npmScopesMap) 324 if err != nil { 325 return errorutils.CheckError(err) 326 } 327 artifactoryScope := yarnNpmScope{NpmAlwaysAuth: true, NpmAuthIdent: npmAuthIdent, NpmRegistryServer: registry} 328 for scopeName := range npmScopesMap { 329 npmScopesMap[scopeName] = artifactoryScope 330 } 331 updatedNpmScopesStr, err := json.Marshal(npmScopesMap) 332 if err != nil { 333 return errorutils.CheckError(err) 334 } 335 return yarn.ConfigSet(NpmScopesConfigName, string(updatedNpmScopesStr), execPath, true) 336 } 337 338 type yarnNpmScope struct { 339 NpmAlwaysAuth bool `json:"npmAlwaysAuth,omitempty"` 340 NpmAuthIdent string `json:"npmAuthIdent,omitempty"` 341 NpmRegistryServer string `json:"npmRegistryServer,omitempty"` 342 } 343 344 func backupAndSetEnvironmentVariable(key, value string) (string, error) { 345 oldVal, _ := os.LookupEnv(key) 346 return oldVal, errorutils.CheckError(os.Setenv(key, value)) 347 } 348 349 // npmAuth we get back from Artifactory includes several fields, but we need only the field '_auth' 350 func extractAuthIdentFromNpmAuth(npmAuth string) (string, error) { 351 authIdentFieldName := "_auth" 352 scanner := bufio.NewScanner(strings.NewReader(npmAuth)) 353 354 for scanner.Scan() { 355 currLine := scanner.Text() 356 if !strings.HasPrefix(currLine, authIdentFieldName) { 357 continue 358 } 359 360 lineParts := strings.SplitN(currLine, "=", 2) 361 if len(lineParts) < 2 { 362 return "", errorutils.CheckErrorf("failed while retrieving npm auth details from Artifactory") 363 } 364 return strings.TrimSpace(lineParts[1]), nil 365 } 366 367 return "", errorutils.CheckErrorf("failed while retrieving npm auth details from Artifactory") 368 }