github.com/osievert/jfrog-cli-core@v1.2.7/artifactory/commands/dotnet/dotnetcommand.go (about)

     1  package dotnet
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"net/url"
     7  	"os"
     8  	"path"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/jfrog/gofrog/io"
    13  	"github.com/jfrog/jfrog-cli-core/artifactory/utils"
    14  	"github.com/jfrog/jfrog-cli-core/artifactory/utils/dotnet"
    15  	"github.com/jfrog/jfrog-cli-core/artifactory/utils/dotnet/solution"
    16  	"github.com/jfrog/jfrog-cli-core/utils/config"
    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/io/fileutils"
    20  	"github.com/jfrog/jfrog-client-go/utils/log"
    21  )
    22  
    23  const SourceName = "JFrogCli"
    24  
    25  type DotnetCommand struct {
    26  	toolchainType      dotnet.ToolchainType
    27  	subCommand         string
    28  	argAndFlags        []string
    29  	repoName           string
    30  	solutionPath       string
    31  	useNugetAddSource  bool
    32  	useNugetV2         bool
    33  	buildConfiguration *utils.BuildConfiguration
    34  	rtDetails          *config.ArtifactoryDetails
    35  	// Indicates the command is run through the legacy syntax, before the 'native' syntax introduction.
    36  	legacy bool
    37  }
    38  
    39  func (dc *DotnetCommand) SetRtDetails(rtDetails *config.ArtifactoryDetails) *DotnetCommand {
    40  	dc.rtDetails = rtDetails
    41  	return dc
    42  }
    43  
    44  func (dc *DotnetCommand) SetBuildConfiguration(buildConfiguration *utils.BuildConfiguration) *DotnetCommand {
    45  	dc.buildConfiguration = buildConfiguration
    46  	return dc
    47  }
    48  
    49  func (dc *DotnetCommand) SetToolchainType(toolchainType dotnet.ToolchainType) *DotnetCommand {
    50  	dc.toolchainType = toolchainType
    51  	return dc
    52  }
    53  
    54  func (dc *DotnetCommand) SetSolutionPath(solutionPath string) *DotnetCommand {
    55  	dc.solutionPath = solutionPath
    56  	return dc
    57  }
    58  
    59  func (dc *DotnetCommand) SetRepoName(repoName string) *DotnetCommand {
    60  	dc.repoName = repoName
    61  	return dc
    62  }
    63  
    64  func (dc *DotnetCommand) SetUseNugetV2(useNugetV2 bool) *DotnetCommand {
    65  	dc.useNugetV2 = useNugetV2
    66  	return dc
    67  }
    68  
    69  func (dc *DotnetCommand) SetArgAndFlags(argAndFlags []string) *DotnetCommand {
    70  	dc.argAndFlags = argAndFlags
    71  	return dc
    72  }
    73  
    74  func (dc *DotnetCommand) SetBasicCommand(subCommand string) *DotnetCommand {
    75  	dc.subCommand = subCommand
    76  	return dc
    77  }
    78  
    79  func (dc *DotnetCommand) SetUseNugetAddSource(useNugetAddSource bool) *DotnetCommand {
    80  	dc.useNugetAddSource = useNugetAddSource
    81  	return dc
    82  }
    83  
    84  func (dc *DotnetCommand) RtDetails() (*config.ArtifactoryDetails, error) {
    85  	return dc.rtDetails, nil
    86  }
    87  
    88  func (dc *DotnetCommand) CommandName() string {
    89  	return "rt_" + dc.toolchainType.String()
    90  }
    91  
    92  // Exec all consume type nuget commands, install, update, add, restore.
    93  func (dc *DotnetCommand) Exec() error {
    94  	log.Info("Running " + dc.toolchainType.String() + "...")
    95  	// Use temp dir to save config file, so that config will be removed at the end.
    96  	tempDirPath, err := fileutils.CreateTempDir()
    97  	if err != nil {
    98  		return err
    99  	}
   100  	defer fileutils.RemoveTempDir(tempDirPath)
   101  
   102  	dc.solutionPath, err = changeWorkingDir(dc.solutionPath)
   103  	if err != nil {
   104  		return err
   105  	}
   106  
   107  	err = dc.prepareAndRunCmd(tempDirPath)
   108  	if err != nil {
   109  		return err
   110  	}
   111  
   112  	isCollectBuildInfo := len(dc.buildConfiguration.BuildName) > 0 && len(dc.buildConfiguration.BuildNumber) > 0
   113  	if !isCollectBuildInfo {
   114  		return nil
   115  	}
   116  
   117  	slnFile, err := dc.updateSolutionPathAndGetFileName()
   118  	if err != nil {
   119  		return err
   120  	}
   121  	sol, err := solution.Load(dc.solutionPath, slnFile)
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	if err = utils.SaveBuildGeneralDetails(dc.buildConfiguration.BuildName, dc.buildConfiguration.BuildNumber); err != nil {
   127  		return err
   128  	}
   129  	buildInfo, err := sol.BuildInfo(dc.buildConfiguration.Module)
   130  	if err != nil {
   131  		return err
   132  	}
   133  	return utils.SaveBuildInfo(dc.buildConfiguration.BuildName, dc.buildConfiguration.BuildNumber, buildInfo)
   134  }
   135  
   136  func (dc *DotnetCommand) updateSolutionPathAndGetFileName() (string, error) {
   137  	// The path argument wasn't provided, sln file will be searched under working directory.
   138  	if len(dc.argAndFlags) == 0 || strings.HasPrefix(dc.argAndFlags[0], "-") {
   139  		return "", nil
   140  	}
   141  	cmdFirstArg := dc.argAndFlags[0]
   142  	exist, err := fileutils.IsDirExists(cmdFirstArg, false)
   143  	if err != nil {
   144  		return "", err
   145  	}
   146  	// The path argument is a directory. sln/project file will be searched under this directory.
   147  	if exist {
   148  		dc.updateSolutionPath(cmdFirstArg)
   149  		return "", err
   150  	}
   151  	exist, err = fileutils.IsFileExists(cmdFirstArg, false)
   152  	if err != nil {
   153  		return "", err
   154  	}
   155  	if exist {
   156  		// The path argument is a .sln file.
   157  		if strings.HasSuffix(cmdFirstArg, ".sln") {
   158  			dc.updateSolutionPath(filepath.Dir(cmdFirstArg))
   159  			return filepath.Base(cmdFirstArg), nil
   160  		}
   161  		// The path argument is a .*proj/packages.config file.
   162  		if strings.HasSuffix(filepath.Ext(cmdFirstArg), "proj") || strings.HasSuffix(cmdFirstArg, "packages.config") {
   163  			dc.updateSolutionPath(filepath.Dir(cmdFirstArg))
   164  		}
   165  	}
   166  	return "", nil
   167  }
   168  
   169  func (dc *DotnetCommand) updateSolutionPath(slnRootPath string) {
   170  	if filepath.IsAbs(slnRootPath) {
   171  		dc.solutionPath = slnRootPath
   172  	} else {
   173  		dc.solutionPath = filepath.Join(dc.solutionPath, slnRootPath)
   174  	}
   175  }
   176  
   177  // Changes the working directory if provided.
   178  // Returns the path to the solution
   179  func changeWorkingDir(newWorkingDir string) (string, error) {
   180  	var err error
   181  	if newWorkingDir != "" {
   182  		err = os.Chdir(newWorkingDir)
   183  	} else {
   184  		newWorkingDir, err = os.Getwd()
   185  	}
   186  
   187  	return newWorkingDir, errorutils.CheckError(err)
   188  }
   189  
   190  // Prepares the nuget configuration file within the temp directory
   191  // Runs NuGet itself with the arguments and flags provided.
   192  func (dc *DotnetCommand) prepareAndRunCmd(configDirPath string) error {
   193  	cmd, err := dc.createCmd()
   194  	if err != nil {
   195  		return err
   196  	}
   197  	// To prevent NuGet prompting for credentials
   198  	err = os.Setenv("NUGET_EXE_NO_PROMPT", "true")
   199  	if err != nil {
   200  		return errorutils.CheckError(err)
   201  	}
   202  
   203  	err = dc.prepareConfigFile(cmd, configDirPath)
   204  	if err != nil {
   205  		return err
   206  	}
   207  	err = io.RunCmd(cmd)
   208  	if err != nil {
   209  		return err
   210  	}
   211  
   212  	return nil
   213  }
   214  
   215  // Checks if the user provided input such as -configfile flag or -Source flag.
   216  // If those flags were provided, NuGet will use the provided configs (default config file or the one with -configfile)
   217  // If neither provided, we are initializing our own config.
   218  func (dc *DotnetCommand) prepareConfigFile(cmd *dotnet.Cmd, configDirPath string) error {
   219  	cmdFlag := cmd.GetToolchain().GetTypeFlagPrefix() + "configfile"
   220  	currentConfigPath, err := getFlagValueIfExists(cmdFlag, cmd)
   221  	if err != nil {
   222  		return err
   223  	}
   224  	if currentConfigPath != "" {
   225  		return nil
   226  	}
   227  
   228  	cmdFlag = cmd.GetToolchain().GetTypeFlagPrefix() + "source"
   229  	sourceCommandValue, err := getFlagValueIfExists(cmdFlag, cmd)
   230  	if err != nil {
   231  		return err
   232  	}
   233  	if sourceCommandValue != "" {
   234  		return nil
   235  	}
   236  
   237  	configFile, err := dc.InitNewConfig(configDirPath)
   238  	if err == nil {
   239  		cmd.CommandFlags = append(cmd.CommandFlags, cmd.GetToolchain().GetTypeFlagPrefix()+"configfile", configFile.Name())
   240  	}
   241  	return err
   242  }
   243  
   244  // Returns the value of the flag if exists
   245  func getFlagValueIfExists(cmdFlag string, cmd *dotnet.Cmd) (string, error) {
   246  	for i := 0; i < len(cmd.CommandFlags); i++ {
   247  		if !strings.EqualFold(cmd.CommandFlags[i], cmdFlag) {
   248  			continue
   249  		}
   250  		if i+1 == len(cmd.CommandFlags) {
   251  			return "", errorutils.CheckError(errorutils.CheckError(fmt.Errorf(cmdFlag, " flag was provided without value")))
   252  		}
   253  		return cmd.CommandFlags[i+1], nil
   254  	}
   255  
   256  	return "", nil
   257  }
   258  
   259  // Got to here, means that neither of the flags provided and we need to init our own config.
   260  func (dc *DotnetCommand) InitNewConfig(configDirPath string) (configFile *os.File, err error) {
   261  	// Initializing a new NuGet config file that NuGet will use into a temp file
   262  	configFile, err = ioutil.TempFile(configDirPath, "jfrog.cli.nuget.")
   263  	if errorutils.CheckError(err) != nil {
   264  		return
   265  	}
   266  	log.Debug("Nuget config file created at:", configFile.Name())
   267  	defer configFile.Close()
   268  
   269  	sourceUrl, user, password, err := dc.getSourceDetails()
   270  	if err != nil {
   271  		return
   272  	}
   273  	// We will prefer to write the NuGet configuration using the `nuget add source` command if we can.
   274  	// The command isn't available in all toolchain's versions.
   275  	// Therefore if the useNugetAddSource flag is set we'll use the command, otherwise we will write the configuration using a formatted string.
   276  	if dc.useNugetAddSource {
   277  		err = dc.AddNugetAuthToConfig(dc.toolchainType, configFile, sourceUrl, user, password)
   278  	} else {
   279  		_, err = fmt.Fprintf(configFile, dotnet.ConfigFileFormat, sourceUrl, user, password)
   280  	}
   281  	return
   282  }
   283  
   284  // Set Artifactory repo as source using the toolchain's `add source` command
   285  func (dc *DotnetCommand) AddNugetAuthToConfig(cmdType dotnet.ToolchainType, configFile *os.File, sourceUrl, user, password string) error {
   286  	content := dotnet.ConfigFileTemplate
   287  	_, err := configFile.WriteString(content)
   288  	if err != nil {
   289  		return errorutils.CheckError(err)
   290  	}
   291  	// We need to close the config file to let the toolchain modify it.
   292  	configFile.Close()
   293  	return addSourceToNugetConfig(cmdType, configFile.Name(), sourceUrl, user, password)
   294  }
   295  
   296  // Runs nuget sources add command
   297  func addSourceToNugetConfig(cmdType dotnet.ToolchainType, configFileName, sourceUrl, user, password string) error {
   298  	cmd, err := dotnet.CreateDotnetAddSourceCmd(cmdType, sourceUrl)
   299  	if err != nil {
   300  		return err
   301  	}
   302  
   303  	flagPrefix := cmdType.GetTypeFlagPrefix()
   304  	cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"configfile", configFileName)
   305  	cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"name", SourceName)
   306  	cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"username", user)
   307  	cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"password", password)
   308  	output, err := io.RunCmdOutput(cmd)
   309  	log.Debug("Running command: Add sources. Output:", output)
   310  	return err
   311  }
   312  
   313  func (dc *DotnetCommand) getSourceDetails() (sourceURL, user, password string, err error) {
   314  	var u *url.URL
   315  	u, err = url.Parse(dc.rtDetails.Url)
   316  	if errorutils.CheckError(err) != nil {
   317  		return
   318  	}
   319  	nugetApi := "api/nuget/v3"
   320  	if dc.useNugetV2 {
   321  		nugetApi = "api/nuget"
   322  	}
   323  	u.Path = path.Join(u.Path, nugetApi, dc.repoName)
   324  	sourceURL = u.String()
   325  
   326  	user = dc.rtDetails.User
   327  	password = dc.rtDetails.Password
   328  	// If access-token is defined, extract user from it.
   329  	rtDetails, err := dc.RtDetails()
   330  	if errorutils.CheckError(err) != nil {
   331  		return
   332  	}
   333  	if rtDetails.AccessToken != "" {
   334  		log.Debug("Using access-token details for nuget authentication.")
   335  		user, err = auth.ExtractUsernameFromAccessToken(rtDetails.AccessToken)
   336  		if err != nil {
   337  			return
   338  		}
   339  		password = rtDetails.AccessToken
   340  	}
   341  	return
   342  }
   343  
   344  func (dc *DotnetCommand) createCmd() (*dotnet.Cmd, error) {
   345  	c, err := dotnet.NewToolchainCmd(dc.toolchainType)
   346  	if err != nil {
   347  		return nil, err
   348  	}
   349  	if dc.legacy {
   350  		return dc.createLegacyCmd(c)
   351  	}
   352  	if dc.subCommand != "" {
   353  		c.Command = append(c.Command, strings.Split(dc.subCommand, " ")...)
   354  	}
   355  	c.CommandFlags = dc.argAndFlags
   356  	return c, nil
   357  }
   358  
   359  // In the legacy syntax, parsing args is required since they might include environment variables, or need escaping.
   360  func (dc *DotnetCommand) createLegacyCmd(c *dotnet.Cmd) (*dotnet.Cmd, error) {
   361  	if dc.subCommand != "" {
   362  		subCommand, err := utils.ParseArgs(strings.Split(dc.subCommand, " "))
   363  		if err != nil {
   364  			return nil, errorutils.CheckError(err)
   365  		}
   366  		c.Command = append(c.Command, subCommand...)
   367  	}
   368  	var err error
   369  	if len(dc.argAndFlags) > 0 {
   370  		c.CommandFlags, err = utils.ParseArgs(dc.argAndFlags)
   371  	}
   372  	return c, errorutils.CheckError(err)
   373  }