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  }