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  }