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

     1  package dotnet
     2  
     3  import (
     4  	"fmt"
     5  	"net/url"
     6  	"os"
     7  	"path"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"github.com/jfrog/gofrog/io"
    12  	"github.com/jfrog/jfrog-cli-core/artifactory/utils"
    13  	"github.com/jfrog/jfrog-cli-core/artifactory/utils/dotnet"
    14  	"github.com/jfrog/jfrog-cli-core/artifactory/utils/dotnet/solution"
    15  	"github.com/jfrog/jfrog-cli-core/utils/config"
    16  	coreutils "github.com/jfrog/jfrog-cli-core/utils/coreutils"
    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  	serverDetails      *config.ServerDetails
    35  	// Indicates the command is run through the legacy syntax, before the 'native' syntax introduction.
    36  	legacy bool
    37  }
    38  
    39  func (dc *DotnetCommand) SetServerDetails(serverDetails *config.ServerDetails) *DotnetCommand {
    40  	dc.serverDetails = serverDetails
    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) ServerDetails() (*config.ServerDetails, error) {
    85  	return dc.serverDetails, 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, dc.buildConfiguration.Project); 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, dc.buildConfiguration.Project, 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 = os.CreateTemp(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  	// We will prefer to write the NuGet configuration using the `nuget add source` command (addSourceToNugetConfig)
   270  	// Currently the NuGet configuration utility doesn't allow setting protocolVersion.
   271  	// Until that is supported, the templated method must be used.
   272  	err = dc.addSourceToNugetTemplate(configFile)
   273  	return
   274  }
   275  
   276  // Set Artifactory repo as source using the toolchain's `add source` command
   277  func (dc *DotnetCommand) AddNugetAuthToConfig(cmdType dotnet.ToolchainType, configFile *os.File, sourceUrl, user, password string) error {
   278  	content := dotnet.ConfigFileTemplate
   279  	_, err := configFile.WriteString(content)
   280  	if err != nil {
   281  		return errorutils.CheckError(err)
   282  	}
   283  	// We need to close the config file to let the toolchain modify it.
   284  	configFile.Close()
   285  	return addSourceToNugetConfig(cmdType, configFile.Name(), sourceUrl, user, password)
   286  }
   287  
   288  // Runs nuget sources add command
   289  func addSourceToNugetConfig(cmdType dotnet.ToolchainType, configFileName, sourceUrl, user, password string) error {
   290  	cmd, err := dotnet.CreateDotnetAddSourceCmd(cmdType, sourceUrl)
   291  	if err != nil {
   292  		return err
   293  	}
   294  
   295  	flagPrefix := cmdType.GetTypeFlagPrefix()
   296  	cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"configfile", configFileName)
   297  	cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"name", SourceName)
   298  	cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"username", user)
   299  	cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"password", password)
   300  	output, err := io.RunCmdOutput(cmd)
   301  	log.Debug("Running command: Add sources. Output:", output)
   302  	return err
   303  }
   304  
   305  // Adds a source to the nuget config template
   306  func (dc *DotnetCommand) addSourceToNugetTemplate(configFile *os.File) error {
   307  	sourceUrl, user, password, err := dc.getSourceDetails()
   308  	if err != nil {
   309  		return err
   310  	}
   311  
   312  	// Specify the protocolVersion
   313  	protoVer := "3"
   314  	if dc.useNugetV2 {
   315  		protoVer = "2"
   316  	}
   317  
   318  	// Format the templates
   319  	_, err = fmt.Fprintf(configFile, dotnet.ConfigFileFormat, sourceUrl, protoVer, user, password)
   320  	return err
   321  }
   322  
   323  func (dc *DotnetCommand) getSourceDetails() (sourceURL, user, password string, err error) {
   324  	var u *url.URL
   325  	u, err = url.Parse(dc.serverDetails.ArtifactoryUrl)
   326  	if errorutils.CheckError(err) != nil {
   327  		return
   328  	}
   329  	nugetApi := "api/nuget/v3"
   330  	if dc.useNugetV2 {
   331  		nugetApi = "api/nuget"
   332  	}
   333  	u.Path = path.Join(u.Path, nugetApi, dc.repoName)
   334  	sourceURL = u.String()
   335  
   336  	user = dc.serverDetails.User
   337  	password = dc.serverDetails.Password
   338  	// If access-token is defined, extract user from it.
   339  	serverDetails, err := dc.ServerDetails()
   340  	if errorutils.CheckError(err) != nil {
   341  		return
   342  	}
   343  	if serverDetails.AccessToken != "" {
   344  		log.Debug("Using access-token details for nuget authentication.")
   345  		user, err = auth.ExtractUsernameFromAccessToken(serverDetails.AccessToken)
   346  		if err != nil {
   347  			return
   348  		}
   349  		password = serverDetails.AccessToken
   350  	}
   351  	return
   352  }
   353  
   354  func (dc *DotnetCommand) createCmd() (*dotnet.Cmd, error) {
   355  	c, err := dotnet.NewToolchainCmd(dc.toolchainType)
   356  	if err != nil {
   357  		return nil, err
   358  	}
   359  	if dc.legacy {
   360  		return dc.createLegacyCmd(c)
   361  	}
   362  	if dc.subCommand != "" {
   363  		c.Command = append(c.Command, strings.Split(dc.subCommand, " ")...)
   364  	}
   365  	c.CommandFlags = dc.argAndFlags
   366  	return c, nil
   367  }
   368  
   369  // In the legacy syntax, parsing args is required since they might include environment variables, or need escaping.
   370  func (dc *DotnetCommand) createLegacyCmd(c *dotnet.Cmd) (*dotnet.Cmd, error) {
   371  	if dc.subCommand != "" {
   372  		subCommand, err := coreutils.ParseArgs(strings.Split(dc.subCommand, " "))
   373  		if err != nil {
   374  			return nil, errorutils.CheckError(err)
   375  		}
   376  		c.Command = append(c.Command, subCommand...)
   377  	}
   378  	var err error
   379  	if len(dc.argAndFlags) > 0 {
   380  		c.CommandFlags, err = coreutils.ParseArgs(dc.argAndFlags)
   381  	}
   382  	return c, errorutils.CheckError(err)
   383  }