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

     1  package dotnet
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"github.com/jfrog/build-info-go/build"
     7  	"github.com/jfrog/build-info-go/build/utils/dotnet"
     8  	"github.com/jfrog/gofrog/io"
     9  	commonBuild "github.com/jfrog/jfrog-cli-core/v2/common/build"
    10  	"github.com/jfrog/jfrog-cli-core/v2/utils/config"
    11  	"github.com/jfrog/jfrog-client-go/auth"
    12  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    13  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
    14  	"github.com/jfrog/jfrog-client-go/utils/log"
    15  	"net/url"
    16  	"os"
    17  	"path"
    18  	"strings"
    19  )
    20  
    21  const (
    22  	SourceName        = "JFrogCli"
    23  	configFilePattern = "jfrog.cli.nuget."
    24  
    25  	dotnetTestError = `the command failed with an error.
    26  Note that JFrog CLI does not restore dependencies during a 'dotnet test' command, so if needed, run a preceding 'dotnet restore'.
    27  The initial error is:
    28  `
    29  	noRestoreFlag = "--no-restore"
    30  )
    31  
    32  type DotnetCommand struct {
    33  	toolchainType      dotnet.ToolchainType
    34  	subCommand         string
    35  	argAndFlags        []string
    36  	repoName           string
    37  	solutionPath       string
    38  	useNugetV2         bool
    39  	buildConfiguration *commonBuild.BuildConfiguration
    40  	serverDetails      *config.ServerDetails
    41  }
    42  
    43  func (dc *DotnetCommand) SetServerDetails(serverDetails *config.ServerDetails) *DotnetCommand {
    44  	dc.serverDetails = serverDetails
    45  	return dc
    46  }
    47  
    48  func (dc *DotnetCommand) SetBuildConfiguration(buildConfiguration *commonBuild.BuildConfiguration) *DotnetCommand {
    49  	dc.buildConfiguration = buildConfiguration
    50  	return dc
    51  }
    52  
    53  func (dc *DotnetCommand) SetToolchainType(toolchainType dotnet.ToolchainType) *DotnetCommand {
    54  	dc.toolchainType = toolchainType
    55  	return dc
    56  }
    57  
    58  func (dc *DotnetCommand) SetSolutionPath(solutionPath string) *DotnetCommand {
    59  	dc.solutionPath = solutionPath
    60  	return dc
    61  }
    62  
    63  func (dc *DotnetCommand) SetRepoName(repoName string) *DotnetCommand {
    64  	dc.repoName = repoName
    65  	return dc
    66  }
    67  
    68  func (dc *DotnetCommand) SetUseNugetV2(useNugetV2 bool) *DotnetCommand {
    69  	dc.useNugetV2 = useNugetV2
    70  	return dc
    71  }
    72  
    73  func (dc *DotnetCommand) SetArgAndFlags(argAndFlags []string) *DotnetCommand {
    74  	dc.argAndFlags = argAndFlags
    75  	return dc
    76  }
    77  
    78  func (dc *DotnetCommand) SetBasicCommand(subCommand string) *DotnetCommand {
    79  	dc.subCommand = subCommand
    80  	return dc
    81  }
    82  
    83  func (dc *DotnetCommand) ServerDetails() (*config.ServerDetails, error) {
    84  	return dc.serverDetails, nil
    85  }
    86  
    87  func (dc *DotnetCommand) GetToolchain() dotnet.ToolchainType {
    88  	return dc.toolchainType
    89  }
    90  
    91  func (dc *DotnetCommand) CommandName() string {
    92  	return "rt_" + dc.toolchainType.String()
    93  }
    94  
    95  // Exec all consume type nuget commands, install, update, add, restore.
    96  func (dc *DotnetCommand) Exec() (err error) {
    97  	log.Info("Running " + dc.toolchainType.String() + "...")
    98  	buildName, err := dc.buildConfiguration.GetBuildName()
    99  	if err != nil {
   100  		return err
   101  	}
   102  	buildNumber, err := dc.buildConfiguration.GetBuildNumber()
   103  	if err != nil {
   104  		return err
   105  	}
   106  
   107  	buildInfoService := commonBuild.CreateBuildInfoService()
   108  	dotnetBuild, err := buildInfoService.GetOrCreateBuildWithProject(buildName, buildNumber, dc.buildConfiguration.GetProject())
   109  	if err != nil {
   110  		return errorutils.CheckError(err)
   111  	}
   112  	buildInfoModule, err := dotnetBuild.AddDotnetModules(dc.solutionPath)
   113  	if err != nil {
   114  		return errorutils.CheckError(err)
   115  	}
   116  	callbackFunc, err := dc.prepareDotnetBuildInfoModule(buildInfoModule)
   117  	if err != nil {
   118  		return err
   119  	}
   120  	defer func() {
   121  		err = errors.Join(err, callbackFunc())
   122  	}()
   123  	if err = buildInfoModule.CalcDependencies(); err != nil {
   124  		if dc.isDotnetTestCommand() {
   125  			return errors.New(dotnetTestError + err.Error())
   126  		}
   127  		return err
   128  	}
   129  	log.Info(fmt.Sprintf("%s finished successfully.", dc.toolchainType))
   130  	return nil
   131  }
   132  
   133  // prepareDotnetBuildInfoModule prepare dotnet modules with the provided cli parameters.
   134  // In case no config file was provided - creates a temporary one.
   135  func (dc *DotnetCommand) prepareDotnetBuildInfoModule(buildInfoModule *build.DotnetModule) (func() error, error) {
   136  	callbackFunc, err := dc.prepareConfigFileIfNeeded()
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  	buildInfoModule.SetName(dc.buildConfiguration.GetModule())
   141  	buildInfoModule.SetSubcommand(dc.subCommand)
   142  	buildInfoModule.SetArgAndFlags(dc.argAndFlags)
   143  	buildInfoModule.SetToolchainType(dc.toolchainType)
   144  	return callbackFunc, nil
   145  }
   146  
   147  // Changes the working directory if provided.
   148  // Returns the path to the solution
   149  func changeWorkingDir(newWorkingDir string) (string, error) {
   150  	var err error
   151  	if newWorkingDir != "" {
   152  		err = os.Chdir(newWorkingDir)
   153  	} else {
   154  		newWorkingDir, err = os.Getwd()
   155  	}
   156  
   157  	return newWorkingDir, errorutils.CheckError(err)
   158  }
   159  
   160  // Set Artifactory repo as source using the toolchain's `add source` command
   161  func (dc *DotnetCommand) AddNugetAuthToConfig(cmdType dotnet.ToolchainType, configFile *os.File, sourceUrl, user, password string) error {
   162  	content := dotnet.ConfigFileTemplate
   163  	_, err := configFile.WriteString(content)
   164  	if err != nil {
   165  		return errorutils.CheckError(err)
   166  	}
   167  	// We need to close the config file to let the toolchain modify it.
   168  	err = configFile.Close()
   169  	if err != nil {
   170  		return errorutils.CheckError(err)
   171  	}
   172  	return addSourceToNugetConfig(cmdType, configFile.Name(), sourceUrl, user, password)
   173  }
   174  
   175  // Runs nuget sources add command
   176  func addSourceToNugetConfig(cmdType dotnet.ToolchainType, configFileName, sourceUrl, user, password string) error {
   177  	cmd, err := dotnet.CreateDotnetAddSourceCmd(cmdType, sourceUrl)
   178  	if err != nil {
   179  		return err
   180  	}
   181  
   182  	flagPrefix := cmdType.GetTypeFlagPrefix()
   183  	cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"configfile", configFileName)
   184  	cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"name", SourceName)
   185  	cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"username", user)
   186  	cmd.CommandFlags = append(cmd.CommandFlags, flagPrefix+"password", password)
   187  	output, err := io.RunCmdOutput(cmd)
   188  	log.Debug("'Add sources' command executed. Output:", output)
   189  	return err
   190  }
   191  
   192  // Checks if the user provided input such as -configfile flag or -Source flag.
   193  // If those flags were provided, NuGet will use the provided configs (default config file or the one with -configfile)
   194  // If neither provided, we are initializing our own config.
   195  func (dc *DotnetCommand) prepareConfigFileIfNeeded() (cleanup func() error, err error) {
   196  	dc.solutionPath, err = changeWorkingDir(dc.solutionPath)
   197  	if err != nil {
   198  		return
   199  	}
   200  
   201  	if dc.isDotnetTestCommand() {
   202  		// The dotnet test command does not support the configfile flag.
   203  		// To avoid resolving from a registry that is not Artifactory, we add the no-restore flag and require the user to run a restore before the test command.
   204  		dc.argAndFlags = append(dc.argAndFlags, noRestoreFlag)
   205  		return
   206  	}
   207  
   208  	cmdFlag := dc.GetToolchain().GetTypeFlagPrefix() + "configfile"
   209  	currentConfigPath, err := getFlagValueIfExists(cmdFlag, dc.argAndFlags)
   210  	if err != nil {
   211  		return
   212  	}
   213  	if currentConfigPath != "" {
   214  		return
   215  	}
   216  
   217  	cmdFlag = dc.GetToolchain().GetTypeFlagPrefix() + "source"
   218  	sourceCommandValue, err := getFlagValueIfExists(cmdFlag, dc.argAndFlags)
   219  	if err != nil {
   220  		return
   221  	}
   222  	if sourceCommandValue != "" {
   223  		return
   224  	}
   225  
   226  	// Use temp dir to save config file, so that config will be removed at the end.
   227  	tempDirPath, err := fileutils.CreateTempDir()
   228  	if err != nil {
   229  		return
   230  	}
   231  	cleanup = func() error {
   232  		return fileutils.RemoveTempDir(tempDirPath)
   233  	}
   234  
   235  	configFile, err := InitNewConfig(tempDirPath, dc.repoName, dc.serverDetails, dc.useNugetV2)
   236  	if err == nil {
   237  		dc.argAndFlags = append(dc.argAndFlags, dc.GetToolchain().GetTypeFlagPrefix()+"configfile", configFile.Name())
   238  	}
   239  	return
   240  }
   241  
   242  func (dc *DotnetCommand) isDotnetTestCommand() bool {
   243  	return dc.GetToolchain() == dotnet.DotnetCore && dc.subCommand == "test"
   244  }
   245  
   246  // Returns the value of the flag if exists
   247  func getFlagValueIfExists(cmdFlag string, argAndFlags []string) (string, error) {
   248  	for i := 0; i < len(argAndFlags); i++ {
   249  		if !strings.EqualFold(argAndFlags[i], cmdFlag) {
   250  			continue
   251  		}
   252  		if i+1 == len(argAndFlags) {
   253  			return "", errorutils.CheckErrorf(cmdFlag, " flag was provided without value")
   254  		}
   255  		return argAndFlags[i+1], nil
   256  	}
   257  
   258  	return "", nil
   259  }
   260  
   261  // InitNewConfig is used when neither of the flags were provided, and we need to init our own config.
   262  func InitNewConfig(configDirPath, repoName string, server *config.ServerDetails, useNugetV2 bool) (configFile *os.File, err error) {
   263  	// Initializing a new NuGet config file that NuGet will use into a temp file
   264  	configFile, err = os.CreateTemp(configDirPath, configFilePattern)
   265  	if errorutils.CheckError(err) != nil {
   266  		return
   267  	}
   268  	log.Debug("Nuget config file created at:", configFile.Name())
   269  	defer func() {
   270  		err = errors.Join(err, errorutils.CheckError(configFile.Close()))
   271  	}()
   272  
   273  	// We would prefer to write the NuGet configuration using the `nuget add source` command,
   274  	// but the NuGet configuration utility doesn't currently allow setting protocolVersion.
   275  	// Until that is supported, the templated method must be used.
   276  	err = addSourceToNugetTemplate(configFile, server, useNugetV2, repoName)
   277  	return
   278  }
   279  
   280  // Adds a source to the nuget config template
   281  func addSourceToNugetTemplate(configFile *os.File, server *config.ServerDetails, useNugetV2 bool, repoName string) error {
   282  	sourceUrl, user, password, err := getSourceDetails(server, repoName, useNugetV2)
   283  	if err != nil {
   284  		return err
   285  	}
   286  
   287  	// Specify the protocolVersion
   288  	protoVer := "3"
   289  	if useNugetV2 {
   290  		protoVer = "2"
   291  	}
   292  
   293  	// Format the templates
   294  	_, err = fmt.Fprintf(configFile, dotnet.ConfigFileFormat, sourceUrl, protoVer, user, password)
   295  	return err
   296  }
   297  
   298  func getSourceDetails(details *config.ServerDetails, repoName string, useNugetV2 bool) (sourceURL, user, password string, err error) {
   299  	var u *url.URL
   300  	u, err = url.Parse(details.ArtifactoryUrl)
   301  	if errorutils.CheckError(err) != nil {
   302  		return
   303  	}
   304  	nugetApi := "api/nuget/v3"
   305  	if useNugetV2 {
   306  		nugetApi = "api/nuget"
   307  	}
   308  	u.Path = path.Join(u.Path, nugetApi, repoName)
   309  	sourceURL = u.String()
   310  
   311  	user = details.User
   312  	password = details.Password
   313  	// If access-token is defined, extract user from it.
   314  	if details.AccessToken != "" {
   315  		log.Debug("Using access-token details for nuget authentication.")
   316  		if user == "" {
   317  			user = auth.ExtractUsernameFromAccessToken(details.AccessToken)
   318  		}
   319  		password = details.AccessToken
   320  	}
   321  	return
   322  }