Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions internal/mirroring/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,12 +259,12 @@ func TestCheckLicense(t *testing.T) {
}{
{
name: "Ultimate tier license",
license: "ultimate",
license: ULTIMATE_PLAN,
expectedError: false,
},
{
name: "Premium tier license",
license: "premium",
license: PREMIUM_PLAN,
expectedError: false,
},
{
Expand Down
18 changes: 9 additions & 9 deletions internal/mirroring/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func (destinationGitlab *GitlabInstance) createGroups(sourceGitlab *GitlabInstan
// Reverse the mirror mapping to get the source group path for each destination group
reversedMirrorMap, destinationGroupPaths := sourceGitlab.reverseGroupMirrorMap(mirrorMapping)

errorChan := make(chan error, len(destinationGroupPaths))
errorChan := make(chan []error, len(destinationGroupPaths))
// Iterate over the groups in alphabetical order (little hack to ensure parent groups are created before children)
for _, destinationGroupPath := range destinationGroupPaths {
_, err := destinationGitlab.createGroup(destinationGroupPath, sourceGitlab, mirrorMapping, &reversedMirrorMap)
Expand All @@ -39,20 +39,20 @@ func (destinationGitlab *GitlabInstance) createGroups(sourceGitlab *GitlabInstan
// createGroup creates a GitLab group in the destination GitLab instance based on the source group and mirror mapping.
// It checks if the group already exists in the destination instance and creates it if not.
// The function also handles the copying of group avatars from the source to the destination instance.
func (destinationGitlab *GitlabInstance) createGroup(destinationGroupPath string, sourceGitlab *GitlabInstance, mirrorMapping *utils.MirrorMapping, reversedMirrorMap *map[string]string) (*gitlab.Group, error) {
func (destinationGitlab *GitlabInstance) createGroup(destinationGroupPath string, sourceGitlab *GitlabInstance, mirrorMapping *utils.MirrorMapping, reversedMirrorMap *map[string]string) (*gitlab.Group, []error) {
// Retrieve the corresponding source group path
sourceGroupPath := (*reversedMirrorMap)[destinationGroupPath]
zap.L().Debug("Mirroring group", zap.String(ROLE_SOURCE, sourceGroupPath), zap.String(ROLE_DESTINATION, destinationGroupPath))

sourceGroup := sourceGitlab.Groups[sourceGroupPath]
if sourceGroup == nil {
return nil, fmt.Errorf("group %s not found in destination GitLab instance (internal error, please review script)", sourceGroupPath)
return nil, []error{fmt.Errorf("group %s not found in destination GitLab instance (internal error, please review script)", sourceGroupPath)}
}

// Retrieve the corresponding group creation options from the mirror mapping
groupCreationOptions, ok := mirrorMapping.GetGroup(sourceGroupPath)
if !ok {
return nil, fmt.Errorf("source group %s not found in mirror mapping (internal error, please review script)", sourceGroupPath)
return nil, []error{fmt.Errorf("source group %s not found in mirror mapping (internal error, please review script)", sourceGroupPath)}
}

// Check if the group already exists in the destination GitLab instance
Expand All @@ -62,12 +62,12 @@ func (destinationGitlab *GitlabInstance) createGroup(destinationGroupPath string
zap.L().Debug("Group not found, creating new group in GitLab Instance", zap.String("group", destinationGroupPath), zap.String(ROLE, ROLE_DESTINATION))
destinationGroup, err = destinationGitlab.createGroupFromSource(sourceGroup, groupCreationOptions)
if err != nil {
return nil, fmt.Errorf("failed to create group %s in destination GitLab instance: %s", destinationGroupPath, err)
return nil, []error{fmt.Errorf("failed to create group %s in destination GitLab instance: %s", destinationGroupPath, err)}
} else {
// Copy the group avatar from the source to the destination instance
err = sourceGitlab.copyGroupAvatar(destinationGitlab, destinationGroup, sourceGroup)
if err != nil {
return destinationGroup, fmt.Errorf("failed to copy group avatar for %s: %s", destinationGroupPath, err)
errArray := sourceGitlab.updateGroupFromSource(destinationGitlab, destinationGroup, sourceGroup, groupCreationOptions)
if errArray != nil {
return destinationGroup, errArray
}
}
}
Expand Down Expand Up @@ -226,7 +226,7 @@ func (g *GitlabInstance) createProjectFromSource(sourceProject *gitlab.Project,
// The function handles the API calls concurrently using goroutines and a wait group.
// It returns an error if any of the API calls fail.
func (destinationGitlab *GitlabInstance) mirrorReleases(sourceGitlab *GitlabInstance, sourceProject *gitlab.Project, destinationProject *gitlab.Project) []error {
zap.L().Debug("Starting releases mirroring", zap.String(ROLE_SOURCE, sourceProject.HTTPURLToRepo), zap.String(ROLE_DESTINATION, destinationProject.HTTPURLToRepo))
zap.L().Info("Starting releases mirroring", zap.String(ROLE_SOURCE, sourceProject.HTTPURLToRepo), zap.String(ROLE_DESTINATION, destinationProject.HTTPURLToRepo))
// Fetch existing releases from the destination project
existingReleases, _, err := destinationGitlab.Gitlab.Releases.ListReleases(destinationProject.ID, &gitlab.ListReleasesOptions{})
if err != nil {
Expand Down
216 changes: 164 additions & 52 deletions internal/mirroring/put.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,118 @@ import (
"go.uber.org/zap"
)

// ===========================================================================
// PROJECTS PUT FUNCTIONS //
// ===========================================================================

// updateProjectFromSource updates the destination project with settings from the source project.
// It enables the project mirror pull, copies the project avatar, and optionally adds the project to the CI/CD catalog.
// It also mirrors releases if the option is set.
// The function uses goroutines to perform these tasks concurrently and waits for all of them to finish.
func (destinationGitlabInstance *GitlabInstance) updateProjectFromSource(sourceGitlabInstance *GitlabInstance, sourceProject *gitlab.Project, destinationProject *gitlab.Project, copyOptions *utils.MirroringOptions) []error {
wg := sync.WaitGroup{}
maxErrors := 3
if copyOptions.CI_CD_Catalog {
maxErrors++
}
if copyOptions.MirrorReleases {
maxErrors++
}
wg.Add(maxErrors)
errorChan := make(chan error, maxErrors)

go func() {
defer wg.Done()
errorChan <- destinationGitlabInstance.syncProjectAttributes(sourceProject, destinationProject, copyOptions)
}()

go func() {
defer wg.Done()
errorChan <- destinationGitlabInstance.enableProjectMirrorPull(sourceProject, destinationProject, copyOptions)
}()

go func() {
defer wg.Done()
errorChan <- sourceGitlabInstance.copyProjectAvatar(destinationGitlabInstance, destinationProject, sourceProject)
}()

if copyOptions.CI_CD_Catalog {
go func() {
defer wg.Done()
errorChan <- destinationGitlabInstance.addProjectToCICDCatalog(destinationProject)
}()
}

allErrors := []error{}
if copyOptions.MirrorReleases {
go func() {
defer wg.Done()
allErrors = destinationGitlabInstance.mirrorReleases(sourceGitlabInstance, sourceProject, destinationProject)
}()
}

wg.Wait()
close(errorChan)
for err := range errorChan {
if err != nil {
allErrors = append(allErrors, err)
}
}
return allErrors
}

// syncProjectAttributes updates the destination project with settings from the source project.
// It checks if any diverged project data exists and if so, it overwrites it.
func (destinationGitlabInstance *GitlabInstance) syncProjectAttributes(sourceProject *gitlab.Project, destinationProject *gitlab.Project, copyOptions *utils.MirroringOptions) error {
zap.L().Debug("Checking if project requires attributes resync", zap.String(ROLE_SOURCE, sourceProject.HTTPURLToRepo), zap.String(ROLE_DESTINATION, destinationProject.HTTPURLToRepo))
gitlabEditOptions := &gitlab.EditProjectOptions{}
missmatched := false
if sourceProject.Name != destinationProject.Name {
gitlabEditOptions.Name = &sourceProject.Name
missmatched = true
}
if sourceProject.Description != destinationProject.Description {
gitlabEditOptions.Description = &sourceProject.Description
missmatched = true
}
if sourceProject.DefaultBranch != destinationProject.DefaultBranch {
gitlabEditOptions.DefaultBranch = &sourceProject.DefaultBranch
missmatched = true
}
if !utils.StringArraysMatchValues(sourceProject.Topics, destinationProject.Topics) {
gitlabEditOptions.Topics = &sourceProject.Topics
missmatched = true
}
if copyOptions.MirrorTriggerBuilds != destinationProject.MirrorTriggerBuilds {
gitlabEditOptions.MirrorTriggerBuilds = &copyOptions.MirrorTriggerBuilds
missmatched = true
}
if !destinationProject.MirrorOverwritesDivergedBranches {
gitlabEditOptions.MirrorOverwritesDivergedBranches = gitlab.Ptr(true)
missmatched = true
}
if !destinationProject.Mirror {
gitlabEditOptions.Mirror = gitlab.Ptr(true)
missmatched = true
}
if copyOptions.Visibility != string(destinationProject.Visibility) {
visibilityValue := utils.ConvertVisibility(copyOptions.Visibility)
gitlabEditOptions.Visibility = &visibilityValue
missmatched = true
}

if missmatched {
destinationProject, _, err := destinationGitlabInstance.Gitlab.Projects.EditProject(destinationProject.ID, gitlabEditOptions)
if err != nil {
return fmt.Errorf("failed to edit project %s: %s", destinationProject.HTTPURLToRepo, err)
}
zap.L().Debug("Project attributes resync completed", zap.String(ROLE_SOURCE, sourceProject.HTTPURLToRepo), zap.String(ROLE_DESTINATION, destinationProject.HTTPURLToRepo))
} else {
zap.L().Debug("Project attributes are already in sync, skipping", zap.String(ROLE_SOURCE, sourceProject.HTTPURLToRepo), zap.String(ROLE_DESTINATION, destinationProject.HTTPURLToRepo))
}
return nil
}

// enableProjectMirrorPull enables the pull mirror for a project in the destination GitLab instance.
// It sets the source project URL, enables mirroring, and configures other options like triggering builds and overwriting diverged branches.
func (g *GitlabInstance) enableProjectMirrorPull(sourceProject *gitlab.Project, destinationProject *gitlab.Project, mirrorOptions *utils.MirroringOptions) error {
Expand Down Expand Up @@ -56,6 +168,34 @@ func (sourceGitlabInstance *GitlabInstance) copyProjectAvatar(destinationGitlabI
return nil
}

// ===========================================================================
// GROUPS PUT FUNCTIONS //
// ===========================================================================

// updateGroupFromSource updates the destination group with settings from the source group.
// It copies the group avatar and updates the group attributes.
func (destinationGitlabInstance *GitlabInstance) updateGroupFromSource(sourceGitlabInstance *GitlabInstance, sourceGroup *gitlab.Group, destinationGroup *gitlab.Group, copyOptions *utils.MirroringOptions) []error {
wg := sync.WaitGroup{}
maxErrors := 2
wg.Add(maxErrors)
errorChan := make(chan error, maxErrors)

go func() {
defer wg.Done()
errorChan <- destinationGitlabInstance.syncGroupAttributes(sourceGroup, destinationGroup, copyOptions)
}()

go func() {
defer wg.Done()
errorChan <- sourceGitlabInstance.copyGroupAvatar(destinationGitlabInstance, destinationGroup, sourceGroup)
}()

wg.Wait()
close(errorChan)

return utils.MergeErrors(errorChan)
}

// copyGroupAvatar copies the avatar from the source group to the destination group.
// It first checks if the destination group already has an avatar set. If not, it downloads the avatar from the source group
// and uploads it to the destination group.
Expand Down Expand Up @@ -88,62 +228,34 @@ func (sourceGitlabInstance *GitlabInstance) copyGroupAvatar(destinationGitlabIns
return nil
}

// updateProjectFromSource updates the destination project with settings from the source project.
// It enables the project mirror pull, copies the project avatar, and optionally adds the project to the CI/CD catalog.
// It also mirrors releases if the option is set.
// The function uses goroutines to perform these tasks concurrently and waits for all of them to finish.
func (destinationGitlabInstance *GitlabInstance) updateProjectFromSource(sourceGitlabInstance *GitlabInstance, sourceProject *gitlab.Project, destinationProject *gitlab.Project, copyOptions *utils.MirroringOptions) []error {
wg := sync.WaitGroup{}
maxErrors := 2
if copyOptions.CI_CD_Catalog {
maxErrors++
// syncGroupAttributes updates the destination group with settings from the source group.
// It checks if any diverged group data exists and if so, it overwrites it.
func (destinationGitlabInstance *GitlabInstance) syncGroupAttributes(sourceGroup *gitlab.Group, destinationGroup *gitlab.Group, copyOptions *utils.MirroringOptions) error {
zap.L().Debug("Checking if group requires attributes resync", zap.String(ROLE_SOURCE, sourceGroup.FullPath), zap.String(ROLE_DESTINATION, destinationGroup.FullPath))
gitlabEditOptions := &gitlab.UpdateGroupOptions{}
missmatched := false
if sourceGroup.Name != destinationGroup.Name {
gitlabEditOptions.Name = &sourceGroup.Name
missmatched = true
}
if copyOptions.MirrorReleases {
maxErrors++
if sourceGroup.Description != destinationGroup.Description {
gitlabEditOptions.Description = &sourceGroup.Description
missmatched = true
}
if copyOptions.Visibility != string(destinationGroup.Visibility) {
visibilityValue := utils.ConvertVisibility(copyOptions.Visibility)
gitlabEditOptions.Visibility = &visibilityValue
missmatched = true
}
wg.Add(maxErrors)
errorChan := make(chan error, maxErrors)

go func() {
defer wg.Done()

zap.L().Debug("Enabling project mirror pull", zap.String(ROLE_SOURCE, sourceProject.HTTPURLToRepo), zap.String(ROLE_DESTINATION, destinationProject.HTTPURLToRepo))
err := destinationGitlabInstance.enableProjectMirrorPull(sourceProject, destinationProject, copyOptions)
if err != nil {
errorChan <- fmt.Errorf("failed to enable project mirror pull for %s: %s", destinationProject.HTTPURLToRepo, err)
}
}()

go func() {
defer wg.Done()
zap.L().Debug("Copying project avatar", zap.String(ROLE_SOURCE, sourceProject.HTTPURLToRepo), zap.String(ROLE_DESTINATION, destinationProject.HTTPURLToRepo))
err := sourceGitlabInstance.copyProjectAvatar(destinationGitlabInstance, destinationProject, sourceProject)
if missmatched {
destinationGroup, _, err := destinationGitlabInstance.Gitlab.Groups.UpdateGroup(destinationGroup.ID, gitlabEditOptions)
if err != nil {
errorChan <- fmt.Errorf("failed to copy project avatar for %s: %s", destinationProject.HTTPURLToRepo, err)
return fmt.Errorf("failed to edit group %s: %s", destinationGroup.FullPath, err)
}
}()

if copyOptions.CI_CD_Catalog {
go func() {
defer wg.Done()
err := destinationGitlabInstance.addProjectToCICDCatalog(destinationProject)
if err != nil {
errorChan <- fmt.Errorf("failed to add project %s to CI/CD catalog: %s", destinationProject.HTTPURLToRepo, err)
}
}()
zap.L().Debug("Group attributes resync completed", zap.String(ROLE_SOURCE, sourceGroup.FullPath), zap.String(ROLE_DESTINATION, destinationGroup.FullPath))
} else {
zap.L().Debug("Group attributes are already in sync, skipping", zap.String(ROLE_SOURCE, sourceGroup.FullPath), zap.String(ROLE_DESTINATION, destinationGroup.FullPath))
}

if copyOptions.MirrorReleases {
go func() {
defer wg.Done()
err := destinationGitlabInstance.mirrorReleases(sourceGitlabInstance, sourceProject, destinationProject)
if err != nil {
errorChan <- fmt.Errorf("failed to copy project %s releases: %s", destinationProject.HTTPURLToRepo, err)
}
}()
}

wg.Wait()
close(errorChan)
return utils.MergeErrors(errorChan)
return nil
}
31 changes: 30 additions & 1 deletion internal/utils/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func (m *MirrorMapping) checkProjects(errChan chan error) {
// Check the visibility
visibilityString := strings.TrimSpace(string(options.Visibility))
if visibilityString != "" && !checkVisibility(visibilityString) {
errChan <- fmt.Errorf("invalid project visibility: %s", string(options.Visibility))
errChan <- fmt.Errorf("invalid project visibility: %s", options.Visibility)
options.Visibility = string(gitlab.PublicVisibility)
}
}
Expand Down Expand Up @@ -225,3 +225,32 @@ func checkVisibility(visibility string) bool {
}
return valid
}

func ConvertVisibility(visibility string) gitlab.VisibilityValue {
switch visibility {
case string(gitlab.PublicVisibility):
return gitlab.PublicVisibility
case string(gitlab.InternalVisibility):
return gitlab.InternalVisibility
case string(gitlab.PrivateVisibility):
return gitlab.PrivateVisibility
default:
return gitlab.PublicVisibility
}
}

func StringArraysMatchValues(array1 []string, array2 []string) bool {
if len(array1) != len(array2) {
return false
}
matchMap := make(map[string]struct{}, len(array1))
for _, value := range array1 {
matchMap[value] = struct{}{}
}
for _, value := range array2 {
if _, ok := matchMap[value]; !ok {
return false
}
}
return true
}
Loading
Loading