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 }