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

     1  package utils
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"strings"
     8  
     9  	"github.com/jfrog/gofrog/datastructures"
    10  	"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils"
    11  	"github.com/jfrog/jfrog-cli-core/v2/utils/config"
    12  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
    13  	"github.com/jfrog/jfrog-client-go/access"
    14  	"github.com/jfrog/jfrog-client-go/artifactory"
    15  	"github.com/jfrog/jfrog-client-go/artifactory/services"
    16  	clientUtils "github.com/jfrog/jfrog-client-go/utils"
    17  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    18  	"github.com/jfrog/jfrog-client-go/utils/io/httputils"
    19  	"github.com/jfrog/jfrog-client-go/utils/log"
    20  	"golang.org/x/exp/slices"
    21  )
    22  
    23  const (
    24  	MinJFrogProjectsArtifactoryVersion = "7.0.0"
    25  	defaultAdminUsername               = "admin"
    26  	defaultAdminPassword               = "password"
    27  )
    28  
    29  type TransferConfigBase struct {
    30  	SourceServerDetails      *config.ServerDetails
    31  	TargetServerDetails      *config.ServerDetails
    32  	SourceArtifactoryManager artifactory.ArtifactoryServicesManager
    33  	TargetArtifactoryManager artifactory.ArtifactoryServicesManager
    34  	SourceAccessManager      *access.AccessServicesManager
    35  	TargetAccessManager      *access.AccessServicesManager
    36  	IncludeReposPatterns     []string
    37  	ExcludeReposPatterns     []string
    38  	FederatedMembersRemoved  bool
    39  }
    40  
    41  func NewTransferConfigBase(sourceServer, targetServer *config.ServerDetails) *TransferConfigBase {
    42  	return &TransferConfigBase{
    43  		SourceServerDetails: sourceServer,
    44  		TargetServerDetails: targetServer,
    45  	}
    46  }
    47  
    48  func (tcb *TransferConfigBase) SetIncludeReposPatterns(includeReposPatterns []string) *TransferConfigBase {
    49  	tcb.IncludeReposPatterns = includeReposPatterns
    50  	return tcb
    51  }
    52  
    53  func (tcb *TransferConfigBase) SetExcludeReposPatterns(excludeReposPatterns []string) *TransferConfigBase {
    54  	tcb.ExcludeReposPatterns = excludeReposPatterns
    55  	return tcb
    56  }
    57  
    58  func (tcb *TransferConfigBase) GetRepoFilter() *utils.IncludeExcludeFilter {
    59  	return &utils.IncludeExcludeFilter{
    60  		IncludePatterns: tcb.IncludeReposPatterns,
    61  		ExcludePatterns: tcb.ExcludeReposPatterns,
    62  	}
    63  }
    64  
    65  func (tcb *TransferConfigBase) CreateServiceManagers(dryRun bool) (err error) {
    66  	if tcb.SourceArtifactoryManager, err = utils.CreateServiceManager(tcb.SourceServerDetails, -1, 0, dryRun); err != nil {
    67  		return
    68  	}
    69  	if tcb.TargetArtifactoryManager, err = utils.CreateServiceManager(tcb.TargetServerDetails, -1, 0, dryRun); err != nil {
    70  		return
    71  	}
    72  	if tcb.SourceAccessManager, err = utils.CreateAccessServiceManager(tcb.SourceServerDetails, false); err != nil {
    73  		return
    74  	}
    75  	if tcb.TargetAccessManager, err = utils.CreateAccessServiceManager(tcb.TargetServerDetails, false); err != nil {
    76  		return
    77  	}
    78  	return
    79  }
    80  
    81  // Make sure that the server is configured with a valid admin Access Token.
    82  // serverDetails - The server to check
    83  // accessManager - Access Manager to run ping
    84  func (tcb *TransferConfigBase) ValidateAccessServerConnection(serverDetails *config.ServerDetails, accessManager *access.AccessServicesManager) error {
    85  	if serverDetails.Password != "" {
    86  		return errorutils.CheckErrorf("it looks like you configured the '%[1]s' instance with username and password.\n"+
    87  			"This command can be used with admin Access Token only.\n"+
    88  			"Please use the 'jf c edit %[1]s' command to configure the Access Token, and then re-run the command", serverDetails.ServerId)
    89  	}
    90  
    91  	if _, err := accessManager.Ping(); err != nil {
    92  		return errors.Join(err, fmt.Errorf("the '%[1]s' instance Access Token is not valid. Please provide a valid access token by running the 'jf c edit %[1]s'", serverDetails.ServerId))
    93  	}
    94  	return nil
    95  }
    96  
    97  // Make sure source and target Artifactory URLs are different.
    98  func (tcb *TransferConfigBase) ValidateDifferentServers() error {
    99  	// Avoid exporting and importing to the same server
   100  	log.Info("Verifying source and target servers are different...")
   101  	if tcb.SourceServerDetails.GetArtifactoryUrl() == tcb.TargetServerDetails.GetArtifactoryUrl() {
   102  		return errorutils.CheckErrorf("The source and target Artifactory servers are identical, but should be different.")
   103  	}
   104  
   105  	return nil
   106  }
   107  
   108  // Create a map between the repository types to the list of repositories to transfer.
   109  func (tcb *TransferConfigBase) GetSelectedRepositories() (map[utils.RepoType][]services.RepositoryDetails, error) {
   110  	allTargetRepos, err := tcb.getAllTargetRepositories()
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  
   115  	result := make(map[utils.RepoType][]services.RepositoryDetails, len(utils.RepoTypes)+1)
   116  	sourceRepos, err := tcb.SourceArtifactoryManager.GetAllRepositories()
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	includeExcludeFilter := tcb.GetRepoFilter()
   122  	for _, sourceRepo := range *sourceRepos {
   123  		if shouldIncludeRepo, err := includeExcludeFilter.ShouldIncludeRepository(sourceRepo.Key); err != nil {
   124  			return nil, err
   125  		} else if shouldIncludeRepo {
   126  			if allTargetRepos.Exists(sourceRepo.Key) {
   127  				log.Info("Repository '" + sourceRepo.Key + "' already exists in the target Artifactory server. Skipping.")
   128  				continue
   129  			}
   130  			repoType := utils.RepoTypeFromString(sourceRepo.Type)
   131  			result[repoType] = append(result[repoType], sourceRepo)
   132  		}
   133  	}
   134  	return result, nil
   135  }
   136  
   137  // Deactivate key encryption in Artifactory, to allow retrieving raw text values in the artifactory-config.xml or in a remote repository.
   138  func (tcb *TransferConfigBase) DeactivateKeyEncryption() (reactivateKeyEncryption func() error, err error) {
   139  	var wasEncrypted bool
   140  	if wasEncrypted, err = tcb.SourceArtifactoryManager.DeactivateKeyEncryption(); err != nil {
   141  		return func() error { return nil }, err
   142  	}
   143  	if !wasEncrypted {
   144  		return func() error { return nil }, nil
   145  	}
   146  	return tcb.SourceArtifactoryManager.ActivateKeyEncryption, nil
   147  }
   148  
   149  // Transfer all repositories to the target Artifactory server
   150  // reposToTransfer - Map between a repository type to the list of repository names
   151  // remoteRepositories - Remote repositories params, we get the remote repository params in an earlier stage after decryption
   152  func (tcb *TransferConfigBase) TransferRepositoriesToTarget(reposToTransfer map[utils.RepoType][]services.RepositoryDetails, remoteRepositories []interface{}) (err error) {
   153  	// Transfer remote repositories
   154  	for i, remoteRepositoryName := range reposToTransfer[utils.Remote] {
   155  		if err = tcb.createRepositoryAndAssignToProject(remoteRepositories[i], remoteRepositoryName); err != nil {
   156  			return
   157  		}
   158  	}
   159  	// Transfer local, federated and unknown repositories.
   160  	for _, repoType := range []utils.RepoType{utils.Local, utils.Federated, utils.Unknown} {
   161  		if len(reposToTransfer[repoType]) == 0 {
   162  			continue
   163  		}
   164  
   165  		if err = tcb.transferSpecificRepositoriesToTarget(reposToTransfer[repoType], repoType); err != nil {
   166  			return
   167  		}
   168  	}
   169  	if len(reposToTransfer[utils.Virtual]) == 0 {
   170  		return
   171  	}
   172  	return tcb.transferVirtualRepositoriesToTarget(reposToTransfer[utils.Virtual])
   173  }
   174  
   175  // Get a set of all repositories in the target Artifactory server.
   176  func (tcb *TransferConfigBase) getAllTargetRepositories() (*datastructures.Set[string], error) {
   177  	targetRepos, err := tcb.TargetArtifactoryManager.GetAllRepositories()
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  	allTargetRepos := datastructures.MakeSet[string]()
   182  	for _, targetRepo := range *targetRepos {
   183  		allTargetRepos.Add(targetRepo.Key)
   184  	}
   185  	return allTargetRepos, nil
   186  }
   187  
   188  // Transfer local, federated, unknown, or virtual repositories
   189  // reposToTransfer - Repositories names to transfer
   190  // repoType - Repository type
   191  func (tcb *TransferConfigBase) transferSpecificRepositoriesToTarget(reposToTransfer []services.RepositoryDetails, repoType utils.RepoType) (err error) {
   192  	for _, repo := range reposToTransfer {
   193  		var params interface{}
   194  		if err = tcb.SourceArtifactoryManager.GetRepository(repo.Key, &params); err != nil {
   195  			return
   196  		}
   197  		if repoType == utils.Federated {
   198  			if params, err = tcb.removeFederatedMembers(params); err != nil {
   199  				return
   200  			}
   201  		}
   202  		if err = tcb.createRepositoryAndAssignToProject(params, repo); err != nil {
   203  			return
   204  		}
   205  	}
   206  	return
   207  }
   208  
   209  // Transfer virtual repositories
   210  // reposToTransfer - Repositories names to transfer
   211  func (tcb *TransferConfigBase) transferVirtualRepositoriesToTarget(reposToTransfer []services.RepositoryDetails) (err error) {
   212  	allReposParams := make(map[string]interface{})
   213  	var singleRepoParamsMap map[string]interface{}
   214  	var singleRepoParams interface{}
   215  	// Step 1 - Get and create all virtual repositories with the included repositories removed
   216  	for _, repoToTransfer := range reposToTransfer {
   217  		// Get repository params
   218  		if err = tcb.SourceArtifactoryManager.GetRepository(repoToTransfer.Key, &singleRepoParams); err != nil {
   219  			return
   220  		}
   221  		allReposParams[repoToTransfer.Key] = singleRepoParams
   222  		singleRepoParamsMap, err = InterfaceToMap(singleRepoParams)
   223  		if err != nil {
   224  			return
   225  		}
   226  
   227  		// Create virtual repository without included repositories
   228  		repositories := singleRepoParamsMap["repositories"]
   229  		delete(singleRepoParamsMap, "repositories")
   230  		if err = tcb.createRepositoryAndAssignToProject(singleRepoParamsMap, repoToTransfer); err != nil {
   231  			return
   232  		}
   233  
   234  		// Restore included repositories to set them later on
   235  		if repositories != nil {
   236  			singleRepoParamsMap["repositories"] = repositories
   237  		}
   238  	}
   239  
   240  	// Step 2 - Update all virtual repositories with the included repositories
   241  	for repoKey, repoParams := range allReposParams {
   242  		if err = tcb.TargetArtifactoryManager.UpdateRepositoryWithParams(repoParams, repoKey); err != nil {
   243  			return
   244  		}
   245  	}
   246  	return
   247  }
   248  
   249  // Get all remote repositories. This method must run after disabling Artifactory key encryption.
   250  // remoteRepositoryNames - Remote repository names to transfer
   251  func (tcb *TransferConfigBase) GetAllRemoteRepositories(remoteRepositoryNames []string) ([]interface{}, error) {
   252  	remoteRepositories := make([]interface{}, 0, len(remoteRepositoryNames))
   253  	for _, repoKey := range remoteRepositoryNames {
   254  		var repoDetails interface{}
   255  		if err := tcb.SourceArtifactoryManager.GetRepository(repoKey, &repoDetails); err != nil {
   256  			return nil, err
   257  		}
   258  		remoteRepositories = append(remoteRepositories, repoDetails)
   259  	}
   260  	return remoteRepositories, nil
   261  }
   262  
   263  func CreateArtifactoryClientDetails(serviceManager artifactory.ArtifactoryServicesManager) (*httputils.HttpClientDetails, error) {
   264  	config := serviceManager.GetConfig()
   265  	if config == nil {
   266  		return nil, errorutils.CheckErrorf("expected full config, but no configuration exists")
   267  	}
   268  	rtDetails := config.GetServiceDetails()
   269  	if rtDetails == nil {
   270  		return nil, errorutils.CheckErrorf("artifactory details not configured")
   271  	}
   272  	clientDetails := rtDetails.CreateHttpClientDetails()
   273  	return &clientDetails, nil
   274  }
   275  
   276  // Check if there is a configured user using default credentials 'admin:password' by pinging Artifactory.
   277  func (tcb *TransferConfigBase) IsDefaultCredentials() (bool, error) {
   278  	// Check if admin is locked
   279  	lockedUsers, err := tcb.SourceArtifactoryManager.GetLockedUsers()
   280  	if err != nil {
   281  		return false, err
   282  	}
   283  	if slices.Contains(lockedUsers, defaultAdminUsername) {
   284  		return false, nil
   285  	}
   286  
   287  	// Ping Artifactory with the default admin:password credentials
   288  	artDetails := config.ServerDetails{ArtifactoryUrl: clientUtils.AddTrailingSlashIfNeeded(tcb.SourceServerDetails.ArtifactoryUrl), User: defaultAdminUsername, Password: defaultAdminPassword}
   289  	servicesManager, err := utils.CreateServiceManager(&artDetails, -1, 0, false)
   290  	if err != nil {
   291  		return false, err
   292  	}
   293  
   294  	// This cannot be executed with commands.Exec()! Doing so will cause usage report being sent with admin:password as well.
   295  	if _, err = servicesManager.Ping(); err == nil {
   296  		log.Output()
   297  		log.Warn("The default 'admin:password' credentials are used by a configured user in your source platform.\n" +
   298  			"Those credentials will be transferred to your SaaS target platform.")
   299  		return true, nil
   300  	}
   301  
   302  	// If the password of the admin user is not the default one, reset the failed login attempts counter in Artifactory
   303  	// by unlocking the user. We do that to avoid login suspension to the admin user.
   304  	return false, tcb.SourceArtifactoryManager.UnlockUser(defaultAdminUsername)
   305  }
   306  
   307  func (tcb *TransferConfigBase) LogTitle(title string) {
   308  	log.Info(coreutils.PrintBoldTitle(fmt.Sprintf("========== %s ==========", title)))
   309  }
   310  
   311  // Remove federated members from the input federated repository.
   312  // federatedRepoParams - Federated repository parameters
   313  func (tcb *TransferConfigBase) removeFederatedMembers(federatedRepoParams interface{}) (interface{}, error) {
   314  	repoMap, err := InterfaceToMap(federatedRepoParams)
   315  	if err != nil {
   316  		return nil, err
   317  	}
   318  	if _, exist := repoMap["members"]; exist {
   319  		delete(repoMap, "members")
   320  		tcb.FederatedMembersRemoved = true
   321  	} else {
   322  		return federatedRepoParams, nil
   323  	}
   324  	return MapToInterface(repoMap)
   325  }
   326  
   327  // Create a repository in the target server and assign the repository to the required project, if any.
   328  // repoParams - Repository parameters
   329  // repoKey    - Repository key
   330  func (tcb *TransferConfigBase) createRepositoryAndAssignToProject(repoParams interface{}, repoDetails services.RepositoryDetails) (err error) {
   331  	var projectKey string
   332  	if repoParams, projectKey, err = removeProjectKeyIfNeeded(repoParams, repoDetails.Key); err != nil {
   333  		return
   334  	}
   335  	if projectKey != "" {
   336  		// Workaround - It's possible that the repository could be assigned to a project in the access.bootstrap.json.
   337  		// This is why we make sure to detach it before actually creating the repository.
   338  		// If the project isn't linked to the repository, an error might come up, but we ignore it because we can't
   339  		// be certain whether the repository was actually assigned to the project or not.
   340  		_ = tcb.TargetAccessManager.UnassignRepoFromProject(repoDetails.Key)
   341  	}
   342  	if err = tcb.TargetArtifactoryManager.CreateRepositoryWithParams(repoParams, repoDetails.Key); err != nil {
   343  		return
   344  	}
   345  	if projectKey != "" {
   346  		return tcb.TargetAccessManager.AssignRepoToProject(repoDetails.Key, projectKey, true)
   347  	}
   348  	return
   349  }
   350  
   351  // Remove non-default project key from the repository parameters if existed and the repository key does not start with it.
   352  // This is needed to allow creating repository assigned to projects so that the repository name is not starting with the project key prefix.
   353  // Returns the updated repository params, the project key if removed and an error if any.
   354  func removeProjectKeyIfNeeded(repoParams interface{}, repoKey string) (interface{}, string, error) {
   355  	var projectKey string
   356  	repoMap, err := InterfaceToMap(repoParams)
   357  	if err != nil {
   358  		return nil, "", err
   359  	}
   360  	if value, exist := repoMap["projectKey"]; exist {
   361  		var ok bool
   362  		if projectKey, ok = value.(string); !ok {
   363  			return nil, "", errorutils.CheckErrorf("couldn't parse the 'projectKey' value '%v' of repository '%s'", value, repoKey)
   364  		}
   365  		if projectKey == "default" || strings.HasPrefix(repoKey, projectKey+"-") {
   366  			// The repository key is starting with the project key prefix:
   367  			// <project-key>-<repo-name>
   368  			return repoParams, "", nil
   369  		}
   370  		delete(repoMap, "projectKey")
   371  	} else {
   372  		return repoParams, "", nil
   373  	}
   374  	response, err := MapToInterface(repoMap)
   375  	if err != nil {
   376  		return nil, "", err
   377  	}
   378  	return response, projectKey, errorutils.CheckError(err)
   379  }
   380  
   381  // During the transfer-config commands we remove federated members, if existed.
   382  // This method log an info that the federated members should be reconfigured in the target server.
   383  func (tcb *TransferConfigBase) LogIfFederatedMemberRemoved() {
   384  	if tcb.FederatedMembersRemoved {
   385  		log.Info("☝️  Your Federated repositories have been transferred to your target instance, but their members have been removed on the target.\n",
   386  			"You should add members to your Federated repositories on your target instance as described here:",
   387  			coreutils.JFrogHelpUrl+"jfrog-artifactory-documentation/federated-repositories")
   388  	}
   389  }
   390  
   391  // Convert the input JSON interface to a map.
   392  // jsonInterface - JSON interface, such as repository params
   393  func InterfaceToMap(jsonInterface interface{}) (map[string]interface{}, error) {
   394  	b, err := json.Marshal(jsonInterface)
   395  	if err != nil {
   396  		return nil, errorutils.CheckError(err)
   397  	}
   398  	newMap := make(map[string]interface{})
   399  	err = errorutils.CheckError(json.Unmarshal(b, &newMap))
   400  	return newMap, err
   401  }
   402  
   403  // Convert the input map to JSON interface.
   404  // mapToTransfer - Map of string to interface, such as repository name
   405  func MapToInterface(mapToTransfer map[string]interface{}) (interface{}, error) {
   406  	repoBytes, err := json.Marshal(mapToTransfer)
   407  	if err != nil {
   408  		return nil, errorutils.CheckError(err)
   409  	}
   410  	var response interface{}
   411  	err = json.Unmarshal(repoBytes, &response)
   412  	return response, errorutils.CheckError(err)
   413  }