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

     1  package transferconfigmerge
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"reflect"
     7  	"strings"
     8  	"time"
     9  
    10  	commandsUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils"
    11  	"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils"
    12  	"github.com/jfrog/jfrog-cli-core/v2/utils/config"
    13  	accessServices "github.com/jfrog/jfrog-client-go/access/services"
    14  	"github.com/jfrog/jfrog-client-go/artifactory/services"
    15  	clientutils "github.com/jfrog/jfrog-client-go/utils"
    16  	"github.com/jfrog/jfrog-client-go/utils/log"
    17  	"golang.org/x/exp/slices"
    18  )
    19  
    20  type ConflictType string
    21  
    22  const (
    23  	Repository    ConflictType = "Repository"
    24  	Project       ConflictType = "Project"
    25  	logFilePrefix              = "transfer-config-conflicts"
    26  )
    27  
    28  // Repository key that should be filtered when comparing repositories (all must be lowercase)
    29  var filteredRepoKeys = []string{"url", "password", "suppresspomconsistencychecks", "description", "gitregistryurl", "cargointernalindex"}
    30  
    31  type TransferConfigMergeCommand struct {
    32  	commandsUtils.TransferConfigBase
    33  	includeProjectsPatterns []string
    34  	excludeProjectsPatterns []string
    35  }
    36  
    37  func NewTransferConfigMergeCommand(sourceServer, targetServer *config.ServerDetails) *TransferConfigMergeCommand {
    38  	return &TransferConfigMergeCommand{TransferConfigBase: *commandsUtils.NewTransferConfigBase(sourceServer, targetServer)}
    39  }
    40  
    41  func (tcmc *TransferConfigMergeCommand) CommandName() string {
    42  	return "rt_transfer_config_merge"
    43  }
    44  
    45  func (tcmc *TransferConfigMergeCommand) SetIncludeProjectsPatterns(includeProjectsPatterns []string) *TransferConfigMergeCommand {
    46  	tcmc.includeProjectsPatterns = includeProjectsPatterns
    47  	return tcmc
    48  }
    49  
    50  func (tcmc *TransferConfigMergeCommand) SetExcludeProjectsPatterns(excludeProjectsPatterns []string) *TransferConfigMergeCommand {
    51  	tcmc.excludeProjectsPatterns = excludeProjectsPatterns
    52  	return tcmc
    53  }
    54  
    55  type mergeEntities struct {
    56  	projectsToTransfer []accessServices.Project
    57  	reposToTransfer    map[utils.RepoType][]services.RepositoryDetails
    58  }
    59  
    60  type Conflict struct {
    61  	Type                ConflictType `json:"type,omitempty"`
    62  	SourceName          string       `json:"source_name,omitempty"`
    63  	TargetName          string       `json:"target_name,omitempty"`
    64  	DifferentProperties string       `json:"different_properties,omitempty"`
    65  }
    66  
    67  func (tcmc *TransferConfigMergeCommand) Run() (csvPath string, err error) {
    68  	tcmc.LogTitle("Preparations")
    69  	projectsSupported, err := tcmc.initServiceManagersAndValidateServers()
    70  	if err != nil {
    71  		return
    72  	}
    73  
    74  	var mergeEntities mergeEntities
    75  	mergeEntities, csvPath, err = tcmc.mergeEntities(projectsSupported)
    76  	if err != nil {
    77  		return
    78  	}
    79  
    80  	if err = tcmc.transferEntities(mergeEntities); err != nil {
    81  		return
    82  	}
    83  
    84  	log.Info("Config transfer merge completed successfully!")
    85  	tcmc.LogIfFederatedMemberRemoved()
    86  	return
    87  }
    88  
    89  func (tcmc *TransferConfigMergeCommand) initServiceManagersAndValidateServers() (projectsSupported bool, err error) {
    90  	if err = tcmc.CreateServiceManagers(false); err != nil {
    91  		return
    92  	}
    93  	// Make sure source and target Artifactory URLs are different and the source Artifactory version is sufficient.
    94  	err = tcmc.ValidateDifferentServers()
    95  	if err != nil {
    96  		return
    97  	}
    98  	sourceArtifactoryVersion, err := tcmc.SourceArtifactoryManager.GetVersion()
    99  	if err != nil {
   100  		return
   101  	}
   102  	// Check if JFrog Projects supported by Source Artifactory version
   103  	versionErr := clientutils.ValidateMinimumVersion(clientutils.Projects, sourceArtifactoryVersion, commandsUtils.MinJFrogProjectsArtifactoryVersion)
   104  	if versionErr != nil {
   105  		// Projects not supported by Source Artifactory version
   106  		return
   107  	}
   108  
   109  	projectsSupported = true
   110  
   111  	if err = tcmc.ValidateAccessServerConnection(tcmc.SourceServerDetails, tcmc.SourceAccessManager); err != nil {
   112  		return
   113  	}
   114  	if err = tcmc.ValidateAccessServerConnection(tcmc.TargetServerDetails, tcmc.TargetAccessManager); err != nil {
   115  		return
   116  	}
   117  
   118  	return
   119  }
   120  
   121  func (tcmc *TransferConfigMergeCommand) mergeEntities(projectsSupported bool) (mergeEntities mergeEntities, csvPath string, err error) {
   122  	conflicts := []Conflict{}
   123  	if projectsSupported {
   124  		tcmc.LogTitle("Merging projects config")
   125  		mergeEntities.projectsToTransfer, err = tcmc.mergeProjects(&conflicts)
   126  		if err != nil {
   127  			return
   128  		}
   129  	}
   130  
   131  	tcmc.LogTitle("Merging repositories config")
   132  	mergeEntities.reposToTransfer, err = tcmc.mergeRepositories(&conflicts)
   133  	if err != nil {
   134  		return
   135  	}
   136  
   137  	if len(conflicts) != 0 {
   138  		csvPath, err = commandsUtils.CreateCSVFile(logFilePrefix, conflicts, time.Now())
   139  		if err != nil {
   140  			return
   141  		}
   142  		log.Info(fmt.Sprintf("We found %d conflicts when comparing the projects and repositories configuration between the source and target instances.\n"+
   143  			"Please review the report available at %s", len(conflicts), csvPath) + "\n" +
   144  			"You can either resolve the conflicts by manually modifying the configuration on the source or the target,\n" +
   145  			"or exclude the transfer of the conflicting projects or repositories by adding options to this command.\n" +
   146  			"Run 'jf rt transfer-config-merge -h' for more information.")
   147  		return
   148  	}
   149  
   150  	log.Info("No Merge conflicts were found while comparing the source and target instances.")
   151  	return
   152  }
   153  
   154  func (tcmc *TransferConfigMergeCommand) transferEntities(mergeEntities mergeEntities) (err error) {
   155  	if len(mergeEntities.projectsToTransfer) > 0 {
   156  		tcmc.LogTitle("Transferring projects")
   157  		err = tcmc.transferProjectsToTarget(mergeEntities.projectsToTransfer)
   158  		if err != nil {
   159  			return
   160  		}
   161  	}
   162  
   163  	tcmc.LogTitle("Transferring repositories")
   164  	var remoteRepositories []interface{}
   165  	if len(mergeEntities.reposToTransfer[utils.Remote]) > 0 {
   166  		remoteRepositories, err = tcmc.decryptAndGetAllRemoteRepositories(mergeEntities.reposToTransfer[utils.Remote])
   167  		if err != nil {
   168  			return
   169  		}
   170  	}
   171  
   172  	return tcmc.TransferRepositoriesToTarget(mergeEntities.reposToTransfer, remoteRepositories)
   173  }
   174  
   175  func (tcmc *TransferConfigMergeCommand) mergeProjects(conflicts *[]Conflict) (projectsToTransfer []accessServices.Project, err error) {
   176  	log.Info("Getting all Projects from the source ...")
   177  	sourceProjects, err := tcmc.SourceAccessManager.GetAllProjects()
   178  	if err != nil {
   179  		return
   180  	}
   181  	log.Info("Getting all Projects from the target ...")
   182  	targetProjects, err := tcmc.TargetAccessManager.GetAllProjects()
   183  	if err != nil {
   184  		return
   185  	}
   186  	targetProjectsMapper := newProjectsMapper(targetProjects)
   187  	includeExcludeFilter := &utils.IncludeExcludeFilter{
   188  		IncludePatterns: tcmc.includeProjectsPatterns,
   189  		ExcludePatterns: tcmc.excludeProjectsPatterns,
   190  	}
   191  	for _, sourceProject := range sourceProjects {
   192  		// Check if repository is filtered out.
   193  		var shouldIncludeProject bool
   194  		shouldIncludeProject, err = includeExcludeFilter.ShouldIncludeItem(sourceProject.ProjectKey)
   195  		if err != nil {
   196  			return
   197  		}
   198  		if !shouldIncludeProject {
   199  			continue
   200  		}
   201  		targetProjectWithSameKey := targetProjectsMapper.getProjectByKey(sourceProject.ProjectKey)
   202  		targetProjectWithSameName := targetProjectsMapper.getProjectByName(sourceProject.DisplayName)
   203  
   204  		if targetProjectWithSameKey == nil && targetProjectWithSameName == nil {
   205  			// Project exists on source only, can be created on target
   206  			projectsToTransfer = append(projectsToTransfer, sourceProject)
   207  			continue
   208  		}
   209  		var conflict *Conflict
   210  		if targetProjectWithSameKey != nil {
   211  			// Project with the same projectKey exists on target
   212  			conflict, err = compareProjects(sourceProject, *targetProjectWithSameKey)
   213  			if err != nil {
   214  				return
   215  			}
   216  			if conflict != nil {
   217  				*conflicts = append(*conflicts, *conflict)
   218  			}
   219  		}
   220  		if targetProjectWithSameName != nil && targetProjectWithSameName != targetProjectWithSameKey {
   221  			// Project with the same display name but different projectKey exists on target
   222  			conflict, err = compareProjects(sourceProject, *targetProjectWithSameName)
   223  			if err != nil {
   224  				return
   225  			}
   226  			if conflict != nil {
   227  				*conflicts = append(*conflicts, *conflict)
   228  			}
   229  		}
   230  	}
   231  	return
   232  }
   233  
   234  func compareProjects(sourceProject, targetProject accessServices.Project) (*Conflict, error) {
   235  	diff, err := compareInterfaces(sourceProject, targetProject)
   236  	if err != nil || diff == "" {
   237  		return nil, err
   238  	}
   239  	return &Conflict{
   240  		Type:                Project,
   241  		SourceName:          fmt.Sprintf("%s(%s)", sourceProject.DisplayName, sourceProject.ProjectKey),
   242  		TargetName:          fmt.Sprintf("%s(%s)", targetProject.DisplayName, targetProject.ProjectKey),
   243  		DifferentProperties: diff,
   244  	}, nil
   245  }
   246  
   247  func (tcmc *TransferConfigMergeCommand) mergeRepositories(conflicts *[]Conflict) (reposToTransfer map[utils.RepoType][]services.RepositoryDetails, err error) {
   248  	reposToTransfer = make(map[utils.RepoType][]services.RepositoryDetails)
   249  	sourceRepos, err := tcmc.SourceArtifactoryManager.GetAllRepositories()
   250  	if err != nil {
   251  		return
   252  	}
   253  	targetRepos, err := tcmc.TargetArtifactoryManager.GetAllRepositories()
   254  	if err != nil {
   255  		return
   256  	}
   257  	targetReposMap := make(map[string]services.RepositoryDetails)
   258  	for _, repo := range *targetRepos {
   259  		targetReposMap[repo.Key] = repo
   260  	}
   261  	includeExcludeFilter := tcmc.GetRepoFilter()
   262  	for _, sourceRepo := range *sourceRepos {
   263  		// Check if repository is filtered out.
   264  		var shouldIncludeRepo bool
   265  		shouldIncludeRepo, err = includeExcludeFilter.ShouldIncludeItem(sourceRepo.Key)
   266  		if err != nil {
   267  			return
   268  		}
   269  		if !shouldIncludeRepo {
   270  			continue
   271  		}
   272  		if targetRepo, exists := targetReposMap[sourceRepo.Key]; exists {
   273  			// The repository exists on target. We need to compare the repositories.
   274  			var diff string
   275  			diff, err = tcmc.compareRepositories(sourceRepo, targetRepo)
   276  			if err != nil {
   277  				return
   278  			}
   279  			if diff != "" {
   280  				// Conflicts found, adding to conflicts CSV
   281  				*conflicts = append(*conflicts, Conflict{
   282  					Type:                Repository,
   283  					SourceName:          sourceRepo.Key,
   284  					TargetName:          sourceRepo.Key,
   285  					DifferentProperties: diff,
   286  				})
   287  			}
   288  		} else {
   289  			repoType := utils.RepoTypeFromString(sourceRepo.Type)
   290  			reposToTransfer[repoType] = append(reposToTransfer[repoType], sourceRepo)
   291  		}
   292  	}
   293  	return
   294  }
   295  
   296  func (tcmc *TransferConfigMergeCommand) compareRepositories(sourceRepoBaseDetails, targetRepoBaseDetails services.RepositoryDetails) (diff string, err error) {
   297  	// Compare basic repository details
   298  	diff, err = compareInterfaces(sourceRepoBaseDetails, targetRepoBaseDetails, filteredRepoKeys...)
   299  	if err != nil || diff != "" {
   300  		return
   301  	}
   302  
   303  	// The basic details are equal. Continuing to compare the full repository details.
   304  	// Get full repo info from source and target
   305  	var sourceRepoFullDetails interface{}
   306  	err = tcmc.SourceArtifactoryManager.GetRepository(sourceRepoBaseDetails.Key, &sourceRepoFullDetails)
   307  	if err != nil {
   308  		return
   309  	}
   310  	var targetRepoFullDetails interface{}
   311  	err = tcmc.TargetArtifactoryManager.GetRepository(targetRepoBaseDetails.Key, &targetRepoFullDetails)
   312  	if err != nil {
   313  		return
   314  	}
   315  	diff, err = compareInterfaces(sourceRepoFullDetails, targetRepoFullDetails, filteredRepoKeys...)
   316  	return
   317  }
   318  
   319  func compareInterfaces(first, second interface{}, filteredKeys ...string) (diff string, err error) {
   320  	firstMap, err := commandsUtils.InterfaceToMap(first)
   321  	if err != nil {
   322  		return
   323  	}
   324  	secondMap, err := commandsUtils.InterfaceToMap(second)
   325  	if err != nil {
   326  		return
   327  	}
   328  	diffList := []string{}
   329  	for key, firstValue := range firstMap {
   330  		if slices.Contains(filteredKeys, strings.ToLower(key)) {
   331  			// Key should be filtered out
   332  			continue
   333  		}
   334  		if secondValue, ok := secondMap[key]; ok {
   335  			// Keys are only compared when exiting on both interfaces.
   336  			if !reflect.DeepEqual(firstValue, secondValue) {
   337  				diffList = append(diffList, key)
   338  			}
   339  		}
   340  	}
   341  	diff = strings.Join(diffList, "; ")
   342  	return
   343  }
   344  
   345  func (tcmc *TransferConfigMergeCommand) transferProjectsToTarget(reposToTransfer []accessServices.Project) (err error) {
   346  	for _, project := range reposToTransfer {
   347  		log.Info(fmt.Sprintf("Transferring project '%s' ...", project.DisplayName))
   348  		if err = tcmc.TargetAccessManager.CreateProject(accessServices.ProjectParams{ProjectDetails: project}); err != nil {
   349  			return
   350  		}
   351  	}
   352  	return
   353  }
   354  
   355  func (tcmc *TransferConfigMergeCommand) decryptAndGetAllRemoteRepositories(remoteRepositoriesDetails []services.RepositoryDetails) (remoteRepositories []interface{}, err error) {
   356  	// Decrypt source Artifactory to get remote repositories with raw text passwords
   357  	reactivateKeyEncryption, err := tcmc.DeactivateKeyEncryption()
   358  	if err != nil {
   359  		return
   360  	}
   361  	defer func() {
   362  		err = errors.Join(err, reactivateKeyEncryption())
   363  	}()
   364  	var remoteRepositoryKeys []string
   365  	for _, remoteRepositoryDetails := range remoteRepositoriesDetails {
   366  		remoteRepositoryKeys = append(remoteRepositoryKeys, remoteRepositoryDetails.Key)
   367  	}
   368  	return tcmc.GetAllRemoteRepositories(remoteRepositoryKeys)
   369  }
   370  
   371  type projectsMapper struct {
   372  	byDisplayName map[string]*accessServices.Project
   373  	byProjectKey  map[string]*accessServices.Project
   374  }
   375  
   376  func newProjectsMapper(targetProjects []accessServices.Project) *projectsMapper {
   377  	byDisplayName := make(map[string]*accessServices.Project)
   378  	byProjectKey := make(map[string]*accessServices.Project)
   379  	for i, project := range targetProjects {
   380  		byDisplayName[project.DisplayName] = &targetProjects[i]
   381  		byProjectKey[project.ProjectKey] = &targetProjects[i]
   382  	}
   383  	return &projectsMapper{byDisplayName: byDisplayName, byProjectKey: byProjectKey}
   384  }
   385  
   386  func (p *projectsMapper) getProjectByName(displayName string) *accessServices.Project {
   387  	return p.byDisplayName[displayName]
   388  }
   389  
   390  func (p *projectsMapper) getProjectByKey(projectKey string) *accessServices.Project {
   391  	return p.byProjectKey[projectKey]
   392  }