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 }