github.com/jfrog/jfrog-cli-core/v2@v2.51.0/artifactory/commands/transferconfig/transferconfig.go (about) 1 package transferconfig 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "net/http" 9 "os" 10 "strings" 11 "time" 12 13 "github.com/jfrog/gofrog/version" 14 15 "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/generic" 16 "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/transferconfig/configxmlutils" 17 commandsUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils" 18 "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils/precheckrunner" 19 "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" 20 "github.com/jfrog/jfrog-cli-core/v2/common/commands" 21 "github.com/jfrog/jfrog-cli-core/v2/utils/config" 22 "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" 23 "github.com/jfrog/jfrog-client-go/artifactory/services" 24 clientutils "github.com/jfrog/jfrog-client-go/utils" 25 "github.com/jfrog/jfrog-client-go/utils/errorutils" 26 "github.com/jfrog/jfrog-client-go/utils/io/fileutils" 27 "github.com/jfrog/jfrog-client-go/utils/io/httputils" 28 "github.com/jfrog/jfrog-client-go/utils/log" 29 ) 30 31 const ( 32 importStartRetries = 3 33 importStartRetriesIntervalMilliSecs = 10000 34 importPollingTimeout = 10 * time.Minute 35 importPollingInterval = 10 * time.Second 36 interruptedByUserErr = "Config transfer was cancelled" 37 minTransferConfigArtifactoryVersion = "6.23.21" 38 ) 39 40 type TransferConfigCommand struct { 41 commandsUtils.TransferConfigBase 42 dryRun bool 43 force bool 44 verbose bool 45 preChecks bool 46 sourceWorkingDir string 47 targetWorkingDir string 48 } 49 50 func NewTransferConfigCommand(sourceServer, targetServer *config.ServerDetails) *TransferConfigCommand { 51 return &TransferConfigCommand{TransferConfigBase: *commandsUtils.NewTransferConfigBase(sourceServer, targetServer)} 52 } 53 54 func (tcc *TransferConfigCommand) CommandName() string { 55 return "rt_transfer_config" 56 } 57 58 func (tcc *TransferConfigCommand) SetDryRun(dryRun bool) *TransferConfigCommand { 59 tcc.dryRun = dryRun 60 return tcc 61 } 62 63 func (tcc *TransferConfigCommand) SetForce(force bool) *TransferConfigCommand { 64 tcc.force = force 65 return tcc 66 } 67 68 func (tcc *TransferConfigCommand) SetVerbose(verbose bool) *TransferConfigCommand { 69 tcc.verbose = verbose 70 return tcc 71 } 72 73 func (tcc *TransferConfigCommand) SetPreChecks(preChecks bool) *TransferConfigCommand { 74 tcc.preChecks = preChecks 75 return tcc 76 } 77 78 func (tcc *TransferConfigCommand) SetSourceWorkingDir(workingDir string) *TransferConfigCommand { 79 tcc.sourceWorkingDir = workingDir 80 return tcc 81 } 82 83 func (tcc *TransferConfigCommand) SetTargetWorkingDir(workingDir string) *TransferConfigCommand { 84 tcc.targetWorkingDir = workingDir 85 return tcc 86 } 87 88 func (tcc *TransferConfigCommand) Run() (err error) { 89 if err = tcc.CreateServiceManagers(tcc.dryRun); err != nil { 90 return err 91 } 92 if tcc.preChecks { 93 return tcc.runPreChecks() 94 } 95 96 tcc.LogTitle("Phase 1/5 - Preparations") 97 err = tcc.printWarnings() 98 if err != nil { 99 return err 100 } 101 err = tcc.validateServerPrerequisites() 102 if err != nil { 103 return err 104 } 105 // Run export on the source Artifactory 106 tcc.LogTitle("Phase 2/5 - Export configuration from the source Artifactory") 107 exportPath, cleanUp, err := tcc.exportSourceArtifactory() 108 defer func() { 109 err = errors.Join(err, cleanUp()) 110 }() 111 if err != nil { 112 return 113 } 114 115 tcc.LogTitle("Phase 3/5 - Download and modify configuration") 116 selectedRepos, err := tcc.GetSelectedRepositories() 117 if err != nil { 118 return 119 } 120 121 // Download and decrypt the config XML from the source Artifactory 122 configXml, remoteRepos, err := tcc.getEncryptedItems(selectedRepos) 123 if err != nil { 124 return 125 } 126 127 // In Artifactory 7.49, the repositories configuration was moved from the artifactory-config.xml to the database. 128 // this method removes the repositories from the artifactory-config.xml file, to be aligned with new Artifactory versions. 129 configXml, err = configxmlutils.RemoveAllRepositories(configXml) 130 if err != nil { 131 return 132 } 133 134 // Create an archive of the source Artifactory export in memory 135 archiveConfig, err := archiveConfig(exportPath, configXml) 136 if err != nil { 137 return 138 } 139 140 // Import the archive to the target Artifactory 141 tcc.LogTitle("Phase 4/5 - Import configuration to the target Artifactory") 142 err = tcc.importToTargetArtifactory(archiveConfig) 143 if err != nil { 144 return 145 } 146 147 // Update the server details of the target Artifactory in the CLI configuration 148 err = tcc.updateServerDetails() 149 if err != nil { 150 return 151 } 152 153 tcc.LogTitle("Phase 5/5 - Import repositories to the target Artifactory") 154 if err = tcc.TransferRepositoriesToTarget(selectedRepos, remoteRepos); err != nil { 155 return 156 } 157 158 // If config transfer passed successfully, add conclusion message 159 log.Output() 160 log.Info("Config transfer completed successfully!") 161 tcc.LogIfFederatedMemberRemoved() 162 log.Info("☝️ Please make sure to disable configuration transfer in MyJFrog before running the 'jf transfer-files' command.") 163 return 164 } 165 166 // Create the directory containing the Artifactory export content 167 // Return values: 168 // exportPath - The export path 169 // unsetTempDir - Clean up function 170 // err - Error if any 171 func (tcc *TransferConfigCommand) createExportPath() (exportPath string, unsetTempDir func(), err error) { 172 if tcc.sourceWorkingDir != "" { 173 // Set the base temp dir according to the value of the --source-working-dir flag 174 oldTempDir := fileutils.GetTempDirBase() 175 fileutils.SetTempDirBase(tcc.sourceWorkingDir) 176 unsetTempDir = func() { 177 fileutils.SetTempDirBase(oldTempDir) 178 } 179 } else { 180 unsetTempDir = func() {} 181 } 182 183 // Create temp directory that will contain the export directory 184 exportPath, err = fileutils.CreateTempDir() 185 if err != nil { 186 return "", unsetTempDir, err 187 } 188 189 return exportPath, unsetTempDir, errorutils.CheckError(os.Chmod(exportPath, 0777)) 190 } 191 192 func (tcc *TransferConfigCommand) runPreChecks() error { 193 // Warn if default admin:password credentials are exist in the source server 194 _, err := tcc.IsDefaultCredentials() 195 if err != nil { 196 return err 197 } 198 199 if err = tcc.validateServerPrerequisites(); err != nil { 200 return err 201 } 202 203 selectedRepos, err := tcc.GetSelectedRepositories() 204 if err != nil { 205 return err 206 } 207 208 // Download and decrypt the remote repositories list from the source Artifactory 209 _, remoteRepositories, err := tcc.getEncryptedItems(selectedRepos) 210 if err != nil { 211 return err 212 } 213 214 return tcc.NewPreChecksRunner(selectedRepos, remoteRepositories).Run(context.Background(), tcc.TargetServerDetails) 215 } 216 217 func (tcc *TransferConfigCommand) printWarnings() (err error) { 218 // Prompt message 219 promptMsg := "This command will transfer Artifactory config data:\n" + 220 fmt.Sprintf("From %s - <%s>\n", coreutils.PrintBold("Source"), tcc.SourceServerDetails.ArtifactoryUrl) + 221 fmt.Sprintf("To %s - <%s>\n", coreutils.PrintBold("Target"), tcc.TargetServerDetails.ArtifactoryUrl) + 222 "This action will wipe out all Artifactory content in the target.\n" + 223 "Make sure that you're using strong credentials in your source platform (for example - having the default admin:password credentials isn't recommended).\n" + 224 "Those credentials will be transferred to your SaaS target platform.\n" + 225 "Are you sure you want to continue?" 226 227 if !coreutils.AskYesNo(promptMsg, false) { 228 return errorutils.CheckErrorf(interruptedByUserErr) 229 } 230 231 // Check if there is a configured user using default credentials in the source platform. 232 isDefaultCredentials, err := tcc.IsDefaultCredentials() 233 if err != nil { 234 return err 235 } 236 if isDefaultCredentials && !coreutils.AskYesNo("Are you sure you want to continue?", false) { 237 return errorutils.CheckErrorf(interruptedByUserErr) 238 } 239 return nil 240 } 241 242 // Make sure the target Artifactory is empty, by counting the number of the users. If it is bigger than 1, return an error. 243 // Also, make sure that the config-import plugin is installed 244 func (tcc *TransferConfigCommand) validateTargetServer() error { 245 // Verify installation of the config-import plugin in the target server and make sure that the user is admin 246 log.Info("Verifying config-import plugin is installed in the target server...") 247 if err := tcc.verifyConfigImportPlugin(); err != nil { 248 return err 249 } 250 251 if tcc.force { 252 return nil 253 } 254 log.Info("Verifying target server is empty...") 255 users, err := tcc.TargetArtifactoryManager.GetAllUsers() 256 if err != nil { 257 return err 258 } 259 // We consider an "empty" Artifactory as an Artifactory server that contains 2 users: the admin user and the anonymous. 260 if len(users) > 2 { 261 return errorutils.CheckErrorf("cowardly refusing to import the config to the target server, because it contains more than 2 users. By default, this command avoids transferring the config to a server which isn't empty. You can bypass this rule by providing the --force flag to the transfer-config command.") 262 } 263 return nil 264 } 265 266 func (tcc *TransferConfigCommand) verifyConfigImportPlugin() error { 267 artifactoryUrl := clientutils.AddTrailingSlashIfNeeded(tcc.TargetServerDetails.GetArtifactoryUrl()) 268 269 // Create rtDetails 270 rtDetails, err := commandsUtils.CreateArtifactoryClientDetails(tcc.TargetArtifactoryManager) 271 if err != nil { 272 return err 273 } 274 275 // Get config-import plugin version 276 configImportVersionUrl := artifactoryUrl + commandsUtils.PluginsExecuteRestApi + "configImportVersion" 277 configImportPluginVersion, err := commandsUtils.GetTransferPluginVersion(tcc.TargetArtifactoryManager.Client(), configImportVersionUrl, "config-import", commandsUtils.Target, rtDetails) 278 if err != nil { 279 return err 280 } 281 log.Info("config-import plugin version: " + configImportPluginVersion) 282 283 // Execute 'GET /api/plugins/execute/checkPermissions' 284 resp, body, _, err := tcc.TargetArtifactoryManager.Client().SendGet(artifactoryUrl+commandsUtils.PluginsExecuteRestApi+"checkPermissions"+tcc.getWorkingDirParam(), false, rtDetails) 285 if err != nil { 286 return err 287 } 288 if resp.StatusCode == http.StatusOK { 289 return nil 290 } 291 292 // Unexpected status received: 403 if the user is not admin, 500+ if there is a server error 293 messageFormat := fmt.Sprintf("Target server response: %s.\n%s", resp.Status, body) 294 return errorutils.CheckErrorf(messageFormat) 295 } 296 297 // Creates the Pre-checks runner for the config import command 298 func (tcc *TransferConfigCommand) NewPreChecksRunner(selectedRepos map[utils.RepoType][]services.RepositoryDetails, remoteRepositories []interface{}) (runner *precheckrunner.PreCheckRunner) { 299 runner = precheckrunner.NewPreChecksRunner() 300 301 // Add pre-checks here 302 runner.AddCheck(precheckrunner.NewRepositoryNamingCheck(selectedRepos)) 303 runner.AddCheck(precheckrunner.NewRemoteRepositoryCheck(&tcc.TargetArtifactoryManager, remoteRepositories)) 304 305 return 306 } 307 308 func (tcc *TransferConfigCommand) getEncryptedItems(selectedSourceRepos map[utils.RepoType][]services.RepositoryDetails) (configXml string, remoteRepositories []interface{}, err error) { 309 reactivateKeyEncryption, err := tcc.DeactivateKeyEncryption() 310 if err != nil { 311 return "", nil, err 312 } 313 defer func() { 314 err = errors.Join(err, reactivateKeyEncryption()) 315 }() 316 317 // Download artifactory.config.xml from the source Artifactory server. 318 // It is safer to not store the decrypted artifactory.config.xml file in the file system, and therefore we only keep it in memory. 319 configXml, err = tcc.SourceArtifactoryManager.GetConfigDescriptor() 320 if err != nil { 321 return 322 } 323 324 // Get all remote repositories from the source Artifactory server. 325 if remoteRepositoriesDetails, ok := selectedSourceRepos[utils.Remote]; ok && len(remoteRepositoriesDetails) > 0 { 326 remoteRepositories = make([]interface{}, len(remoteRepositoriesDetails)) 327 for i, remoteRepositoryDetails := range remoteRepositoriesDetails { 328 if err = tcc.SourceArtifactoryManager.GetRepository(remoteRepositoryDetails.Key, &remoteRepositories[i]); err != nil { 329 return 330 } 331 } 332 } 333 334 return 335 } 336 337 // Export the config from the source Artifactory to a local directory. 338 // Return the path to the export directory, a cleanup function and an error. 339 func (tcc *TransferConfigCommand) exportSourceArtifactory() (string, func() error, error) { 340 // Create temp directory that will contain the export directory 341 exportPath, unsetTempDir, err := tcc.createExportPath() 342 defer unsetTempDir() 343 if err != nil { 344 return "", func() error { return nil }, err 345 } 346 347 // Do export 348 exportParams := services.ExportParams{ 349 ExportPath: exportPath, 350 IncludeMetadata: clientutils.Pointer(false), 351 Verbose: &tcc.verbose, 352 ExcludeContent: clientutils.Pointer(true), 353 } 354 cleanUp := func() error { return fileutils.RemoveTempDir(exportPath) } 355 if err = tcc.SourceArtifactoryManager.Export(exportParams); err != nil { 356 return "", cleanUp, err 357 } 358 359 // Make sure only the export directory contained in the temp directory 360 files, err := fileutils.ListFiles(exportPath, true) 361 if err != nil { 362 return "", cleanUp, err 363 } 364 if len(files) == 0 { 365 return "", cleanUp, errorutils.CheckErrorf("couldn't find the export directory in '%s'. Please make sure to run this command inside the source Artifactory machine", exportPath) 366 } 367 if len(files) > 1 { 368 return "", cleanUp, errorutils.CheckErrorf("only the exported directory is expected to be in the export directory %s, but found %q", exportPath, files) 369 } 370 371 // Return the export directory and the cleanup function 372 return files[0], cleanUp, nil 373 } 374 375 // Import from the input buffer to the target Artifactory 376 func (tcc *TransferConfigCommand) importToTargetArtifactory(buffer *bytes.Buffer) (err error) { 377 artifactoryUrl := clientutils.AddTrailingSlashIfNeeded(tcc.TargetServerDetails.GetArtifactoryUrl()) 378 var timestamp []byte 379 380 // Create rtDetails 381 rtDetails, err := commandsUtils.CreateArtifactoryClientDetails(tcc.TargetArtifactoryManager) 382 if err != nil { 383 return err 384 } 385 386 // Sometimes, POST api/plugins/execute/configImport return unexpectedly 404 errors, although the config-import plugin is installed. 387 // To overcome this issue, we use a custom retryExecutor and not the default retry executor that retries only on HTTP errors >= 500. 388 retryExecutor := clientutils.RetryExecutor{ 389 MaxRetries: importStartRetries, 390 RetriesIntervalMilliSecs: importStartRetriesIntervalMilliSecs, 391 ErrorMessage: fmt.Sprintf("Failed to start the config import process in %s", artifactoryUrl), 392 LogMsgPrefix: "[Config import]", 393 ExecutionHandler: func() (shouldRetry bool, err error) { 394 // Start the config import async process 395 resp, body, err := tcc.TargetArtifactoryManager.Client().SendPost(artifactoryUrl+commandsUtils.PluginsExecuteRestApi+"configImport"+tcc.getWorkingDirParam(), buffer.Bytes(), rtDetails) 396 if err != nil { 397 return false, err 398 } 399 if err = errorutils.CheckResponseStatusWithBody(resp, body, http.StatusOK); err != nil { 400 return true, err 401 } 402 403 log.Debug("Artifactory response:", resp.Status) 404 timestamp = body 405 log.Info("Config import timestamp: " + string(timestamp)) 406 return false, nil 407 }, 408 } 409 410 if err = retryExecutor.Execute(); err != nil { 411 return err 412 } 413 414 // Wait for config import completion 415 return tcc.waitForImportCompletion(rtDetails, timestamp) 416 } 417 418 func (tcc *TransferConfigCommand) waitForImportCompletion(rtDetails *httputils.HttpClientDetails, importTimestamp []byte) error { 419 artifactoryUrl := clientutils.AddTrailingSlashIfNeeded(tcc.TargetServerDetails.GetArtifactoryUrl()) 420 421 pollingExecutor := &httputils.PollingExecutor{ 422 Timeout: importPollingTimeout, 423 PollingInterval: importPollingInterval, 424 MsgPrefix: "Waiting for config import completion in Artifactory server at " + artifactoryUrl, 425 PollingAction: tcc.createImportPollingAction(rtDetails, artifactoryUrl, importTimestamp), 426 } 427 428 body, err := pollingExecutor.Execute() 429 if err != nil { 430 return err 431 } 432 log.Info(fmt.Sprintf("Logs from Artifactory:\n%s", body)) 433 if strings.Contains(string(body), "[ERROR]") { 434 return errorutils.CheckErrorf("Errors detected during config import. Hint: You can skip transferring some Artifactory repositories by using the '--exclude-repos' command option. Run 'jf rt transfer-config -h' for more information.") 435 } 436 return nil 437 } 438 439 func (tcc *TransferConfigCommand) createImportPollingAction(rtDetails *httputils.HttpClientDetails, artifactoryUrl string, importTimestamp []byte) httputils.PollingAction { 440 return func() (shouldStop bool, responseBody []byte, err error) { 441 // Get config import status 442 resp, body, err := tcc.TargetArtifactoryManager.Client().SendPost(artifactoryUrl+commandsUtils.PluginsExecuteRestApi+"configImportStatus"+tcc.getWorkingDirParam(), importTimestamp, rtDetails) 443 if err != nil { 444 return true, nil, err 445 } 446 447 // 200 - Import completed 448 if resp.StatusCode == http.StatusOK { 449 return true, body, nil 450 } 451 452 // 202 - Import in progress 453 if resp.StatusCode == http.StatusAccepted { 454 return false, nil, nil 455 } 456 457 // Unexpected status 458 if err = errorutils.CheckResponseStatusWithBody(resp, body, http.StatusUnauthorized, http.StatusForbidden); err != nil { 459 return false, nil, err 460 } 461 462 // 401 or 403 - The user used for the target Artifactory server does not exist anymore. 463 // This is perfectly normal, because the import caused the user to be deleted. We can now use the credentials of the source Artifactory server. 464 newServerDetails := tcc.TargetServerDetails 465 newServerDetails.SetUser(tcc.SourceServerDetails.GetUser()) 466 newServerDetails.SetPassword(tcc.SourceServerDetails.GetPassword()) 467 newServerDetails.SetAccessToken(tcc.SourceServerDetails.GetAccessToken()) 468 469 tcc.TargetArtifactoryManager, err = utils.CreateServiceManager(newServerDetails, -1, 0, false) 470 if err != nil { 471 return true, nil, err 472 } 473 tcc.TargetAccessManager, err = utils.CreateAccessServiceManager(newServerDetails, false) 474 if err != nil { 475 return true, nil, err 476 } 477 rtDetails, err = commandsUtils.CreateArtifactoryClientDetails(tcc.TargetArtifactoryManager) 478 if err != nil { 479 return true, nil, err 480 } 481 482 // After 401 or 403, the server credentials are fixed, and therefore we can run again 483 return false, nil, nil 484 } 485 } 486 487 func (tcc *TransferConfigCommand) updateServerDetails() error { 488 log.Info("Pinging the target Artifactory...") 489 newTargetServerDetails := tcc.TargetServerDetails 490 491 // Copy credentials from the source server details 492 newTargetServerDetails.User = tcc.SourceServerDetails.User 493 newTargetServerDetails.Password = tcc.SourceServerDetails.Password 494 newTargetServerDetails.SshKeyPath = tcc.SourceServerDetails.SshKeyPath 495 newTargetServerDetails.SshPassphrase = tcc.SourceServerDetails.SshPassphrase 496 newTargetServerDetails.AccessToken = tcc.SourceServerDetails.AccessToken 497 newTargetServerDetails.RefreshToken = tcc.SourceServerDetails.RefreshToken 498 newTargetServerDetails.ArtifactoryRefreshToken = tcc.SourceServerDetails.ArtifactoryRefreshToken 499 newTargetServerDetails.ArtifactoryTokenRefreshInterval = tcc.SourceServerDetails.ArtifactoryTokenRefreshInterval 500 newTargetServerDetails.ClientCertPath = tcc.SourceServerDetails.ClientCertPath 501 newTargetServerDetails.ClientCertKeyPath = tcc.SourceServerDetails.ClientCertKeyPath 502 503 // Ping to validate the transfer ended successfully 504 pingCmd := generic.NewPingCommand().SetServerDetails(newTargetServerDetails) 505 err := pingCmd.Run() 506 if err != nil { 507 return err 508 } 509 log.Info("Ping to the target Artifactory was successful. Updating the server configuration in JFrog CLI.") 510 511 // Update the server details in JFrog CLI configuration 512 configCmd := commands.NewConfigCommand(commands.AddOrEdit, newTargetServerDetails.ServerId).SetInteractive(false).SetDetails(newTargetServerDetails) 513 err = configCmd.Run() 514 if err != nil { 515 return err 516 } 517 tcc.TargetServerDetails = newTargetServerDetails 518 return nil 519 } 520 521 func (tcc *TransferConfigCommand) getWorkingDirParam() string { 522 if tcc.targetWorkingDir != "" { 523 return "?params=workingDir=" + tcc.targetWorkingDir 524 } 525 return "" 526 } 527 528 // Make sure that the source Artifactory version is sufficient. 529 // Returns the source Artifactory version. 530 func (tcc *TransferConfigCommand) validateMinVersion() (sourceArtifactoryVersion string, err error) { 531 log.Info("Verifying minimum version of the source server...") 532 sourceArtifactoryVersion, err = tcc.SourceArtifactoryManager.GetVersion() 533 if err != nil { 534 return 535 } 536 var targetArtifactoryVersion string 537 targetArtifactoryVersion, err = tcc.TargetArtifactoryManager.GetVersion() 538 if err != nil { 539 return 540 } 541 542 // Validate minimal Artifactory version in the source server 543 err = clientutils.ValidateMinimumVersion(clientutils.Artifactory, sourceArtifactoryVersion, minTransferConfigArtifactoryVersion) 544 if err != nil { 545 return 546 } 547 548 // Validate that the target Artifactory server version is >= than the source Artifactory server version 549 if !version.NewVersion(targetArtifactoryVersion).AtLeast(sourceArtifactoryVersion) { 550 err = errorutils.CheckErrorf("The source Artifactory version (%s) can't be higher than the target Artifactory version (%s).", sourceArtifactoryVersion, targetArtifactoryVersion) 551 } 552 553 return 554 } 555 556 func (tcc *TransferConfigCommand) validateServerPrerequisites() (err error) { 557 var sourceArtifactoryVersion string 558 // Make sure that the source Artifactory version is sufficient. 559 if sourceArtifactoryVersion, err = tcc.validateMinVersion(); err != nil { 560 return 561 } 562 563 // Check connectivity to JFrog Access if the source Artifactory version is >= 7.0.0 564 if versionErr := clientutils.ValidateMinimumVersion(clientutils.Projects, sourceArtifactoryVersion, commandsUtils.MinJFrogProjectsArtifactoryVersion); versionErr == nil { 565 if err = tcc.ValidateAccessServerConnection(tcc.SourceServerDetails, tcc.SourceAccessManager); err != nil { 566 return 567 } 568 } 569 570 // Make sure source and target Artifactory URLs are different 571 if err = tcc.ValidateDifferentServers(); err != nil { 572 return 573 } 574 // Make sure that the target Artifactory is empty and the config-import plugin is installed 575 return tcc.validateTargetServer() 576 }