diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..f194e40 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,54 @@ +name: lint-test + +on: + workflow_dispatch: + push: + pull_request: + +permissions: + contents: read + +jobs: + golangci-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: stable + - run: go mod tidy + - name: golangci-lint + uses: golangci/golangci-lint-action@v7 + with: + version: v2.0 + continue-on-error: true + + + gosec: + runs-on: ubuntu-latest + env: + GO111MODULE: on + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.23.7' + - run: go mod tidy + - name: Run Gosec Security Scanner + uses: securego/gosec@master + with: + args: ./... + continue-on-error: true + + + gotestsum: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.23.7' + - run: go mod tidy + - name: go-test + run: | + go test -v ./... diff --git a/mirroring/get.go b/mirroring/get.go index bbaa909..f782341 100644 --- a/mirroring/get.go +++ b/mirroring/get.go @@ -67,10 +67,12 @@ func (g *GitlabInstance) fetchProjects(projectFilters *map[string]bool, groupFil } // Add the project to the mirror mapping - mirrorMapping.AddProject(project.PathWithNamespace, &utils.ProjectMirroringOptions{ - CI_CD_Catalog: groupCreationOptions.CI_CD_Catalog, - Issues: groupCreationOptions.Issues, - DestinationPath: filepath.Join(groupCreationOptions.DestinationPath, relativePath), + mirrorMapping.AddProject(project.PathWithNamespace, &utils.MirroringOptions{ + DestinationPath: filepath.Join(groupCreationOptions.DestinationPath, relativePath), + CI_CD_Catalog: groupCreationOptions.CI_CD_Catalog, + Issues: groupCreationOptions.Issues, + MirrorTriggerBuilds: groupCreationOptions.MirrorTriggerBuilds, + Visibility: groupCreationOptions.Visibility, }) } break @@ -143,10 +145,12 @@ func (g *GitlabInstance) fetchGroups(groupFilters *map[string]bool, mirrorMappin } // Add the group to the mirror mapping - mirrorMapping.AddGroup(group.FullPath, &utils.GroupMirroringOptions{ - CI_CD_Catalog: groupCreationOptions.CI_CD_Catalog, - Issues: groupCreationOptions.Issues, - DestinationPath: filepath.Join(groupCreationOptions.DestinationPath, relativePath), + mirrorMapping.AddGroup(group.FullPath, &utils.MirroringOptions{ + DestinationPath: filepath.Join(groupCreationOptions.DestinationPath, relativePath), + CI_CD_Catalog: groupCreationOptions.CI_CD_Catalog, + Issues: groupCreationOptions.Issues, + MirrorTriggerBuilds: groupCreationOptions.MirrorTriggerBuilds, + Visibility: groupCreationOptions.Visibility, }) } break diff --git a/mirroring/main_test.go b/mirroring/main_test.go index 34cca01..28c77b2 100644 --- a/mirroring/main_test.go +++ b/mirroring/main_test.go @@ -19,8 +19,8 @@ func TestProcessFilters(t *testing.T) { { name: "EmptyMirrorMapping", mirrorMapping: &utils.MirrorMapping{ - Projects: make(map[string]*utils.ProjectMirroringOptions), - Groups: make(map[string]*utils.GroupMirroringOptions), + Projects: make(map[string]*utils.MirroringOptions), + Groups: make(map[string]*utils.MirroringOptions), }, expectedSourceProjectFilters: map[string]bool{}, expectedSourceGroupFilters: map[string]bool{}, @@ -30,14 +30,14 @@ func TestProcessFilters(t *testing.T) { { name: "SingleProjectAndGroup", mirrorMapping: &utils.MirrorMapping{ - Projects: map[string]*utils.ProjectMirroringOptions{ + Projects: map[string]*utils.MirroringOptions{ "sourceProject": { DestinationPath: "destinationGroupPath/destinationProjectPath", CI_CD_Catalog: true, Issues: true, }, }, - Groups: map[string]*utils.GroupMirroringOptions{ + Groups: map[string]*utils.MirroringOptions{ "sourceGroup": { DestinationPath: "destinationGroupPath", CI_CD_Catalog: true, @@ -61,7 +61,7 @@ func TestProcessFilters(t *testing.T) { { name: "MultipleProjectsAndGroups", mirrorMapping: &utils.MirrorMapping{ - Projects: map[string]*utils.ProjectMirroringOptions{ + Projects: map[string]*utils.MirroringOptions{ "sourceProject1": { DestinationPath: "destinationGroupPath1/destinationProjectPath1", CI_CD_Catalog: true, @@ -73,7 +73,7 @@ func TestProcessFilters(t *testing.T) { Issues: false, }, }, - Groups: map[string]*utils.GroupMirroringOptions{ + Groups: map[string]*utils.MirroringOptions{ "sourceGroup1": { DestinationPath: "destinationGroupPath3", CI_CD_Catalog: true, diff --git a/mirroring/post.go b/mirroring/post.go index 26f8f68..6cb847a 100644 --- a/mirroring/post.go +++ b/mirroring/post.go @@ -143,15 +143,16 @@ func createProjects(sourceGitlab *GitlabInstance, destinationGitlab *GitlabInsta return utils.MergeErrors(errorChan, 2) } -func (g *GitlabInstance) createProjectFromSource(sourceProject *gitlab.Project, copyOptions *utils.ProjectMirroringOptions) (*gitlab.Project, error) { +func (g *GitlabInstance) createProjectFromSource(sourceProject *gitlab.Project, copyOptions *utils.MirroringOptions) (*gitlab.Project, error) { projectCreationArgs := &gitlab.CreateProjectOptions{ Name: &sourceProject.Name, Path: &sourceProject.Path, DefaultBranch: &sourceProject.DefaultBranch, Description: &sourceProject.Description, - MirrorTriggerBuilds: gitlab.Ptr(true), + MirrorTriggerBuilds: gitlab.Ptr(copyOptions.MirrorTriggerBuilds), Mirror: gitlab.Ptr(true), Topics: &sourceProject.Topics, + Visibility: gitlab.Ptr(gitlab.VisibilityValue(copyOptions.Visibility)), } utils.LogVerbosef("Retrieving project namespace ID for %s", copyOptions.DestinationPath) @@ -173,7 +174,7 @@ func (g *GitlabInstance) createProjectFromSource(sourceProject *gitlab.Project, return destinationProject, nil } -func (g *GitlabInstance) createGroupFromSource(sourceGroup *gitlab.Group, copyOptions *utils.GroupMirroringOptions) (*gitlab.Group, error) { +func (g *GitlabInstance) createGroupFromSource(sourceGroup *gitlab.Group, copyOptions *utils.MirroringOptions) (*gitlab.Group, error) { groupCreationArgs := &gitlab.CreateGroupOptions{ Name: &sourceGroup.Name, Path: &sourceGroup.Path, diff --git a/mirroring/put.go b/mirroring/put.go index 1a7f160..36b6d08 100644 --- a/mirroring/put.go +++ b/mirroring/put.go @@ -69,7 +69,7 @@ func (g *GitlabInstance) copyGroupAvatar(destinationGitlabInstance *GitlabInstan return nil } -func (g *GitlabInstance) updateProjectFromSource(sourceGitlab *GitlabInstance, sourceProject *gitlab.Project, destinationProject *gitlab.Project, copyOptions *utils.ProjectMirroringOptions) error { +func (g *GitlabInstance) updateProjectFromSource(sourceGitlab *GitlabInstance, sourceProject *gitlab.Project, destinationProject *gitlab.Project, copyOptions *utils.MirroringOptions) error { wg := sync.WaitGroup{} maxErrors := 2 if copyOptions.CI_CD_Catalog { diff --git a/utils/types.go b/utils/types.go index c0f0793..4ba15a8 100644 --- a/utils/types.go +++ b/utils/types.go @@ -13,6 +13,8 @@ import ( "os" "strings" "sync" + + gitlab "gitlab.com/gitlab-org/api/client-go" ) // ParserArgs defines the command line arguments @@ -42,21 +44,12 @@ type ParserArgs struct { // - destination_url: the URL of the destination GitLab instance // - ci_cd_catalog: whether to add the project to the CI/CD catalog // - issues: whether to mirror the issues -type ProjectMirroringOptions struct { - DestinationPath string `json:"destination_path"` - CI_CD_Catalog bool `json:"ci_cd_catalog"` - Issues bool `json:"issues"` -} - -// GroupMirrorOptions defines how the group should be mirrored -// to the destination GitLab instance -// - destination_url: the URL of the destination GitLab instance -// - ci_cd_catalog: whether to add the group to the CI/CD catalog -// - issues: whether to mirror the issues -type GroupMirroringOptions struct { - DestinationPath string `json:"destination_path"` - CI_CD_Catalog bool `json:"ci_cd_catalog"` - Issues bool `json:"issues"` +type MirroringOptions struct { + DestinationPath string `json:"destination_path"` + CI_CD_Catalog bool `json:"ci_cd_catalog"` + Issues bool `json:"issues"` + MirrorTriggerBuilds bool `json:"mirror_trigger_builds"` + Visibility string `json:"visibility"` } // MirrorMapping defines the mapping of projects and groups @@ -65,19 +58,19 @@ type GroupMirroringOptions struct { // - projects: a map of project names to their mirroring options // - groups: a map of group names to their mirroring options type MirrorMapping struct { - Projects map[string]*ProjectMirroringOptions `json:"projects"` - Groups map[string]*GroupMirroringOptions `json:"groups"` + Projects map[string]*MirroringOptions `json:"projects"` + Groups map[string]*MirroringOptions `json:"groups"` muProjects sync.Mutex muGroups sync.Mutex } -func (m *MirrorMapping) AddProject(project string, options *ProjectMirroringOptions) { +func (m *MirrorMapping) AddProject(project string, options *MirroringOptions) { m.muProjects.Lock() defer m.muProjects.Unlock() m.Projects[project] = options } -func (m *MirrorMapping) AddGroup(group string, options *GroupMirroringOptions) { +func (m *MirrorMapping) AddGroup(group string, options *MirroringOptions) { m.muGroups.Lock() defer m.muGroups.Unlock() m.Groups[group] = options @@ -88,8 +81,8 @@ func (m *MirrorMapping) AddGroup(group string, options *GroupMirroringOptions) { // It returns the mapping and an error if any func OpenMirrorMapping(path string) (*MirrorMapping, error) { mapping := &MirrorMapping{ - Projects: make(map[string]*ProjectMirroringOptions), - Groups: make(map[string]*GroupMirroringOptions), + Projects: make(map[string]*MirroringOptions), + Groups: make(map[string]*MirroringOptions), } // Read the file @@ -115,7 +108,7 @@ func OpenMirrorMapping(path string) (*MirrorMapping, error) { } func (m *MirrorMapping) check() error { - errChan := make(chan error, 3*(len(m.Projects)+len(m.Groups)+1)) + errChan := make(chan error, 4*(len(m.Projects)+len(m.Groups))+1) // Check if the mapping is valid if len(m.Projects) == 0 && len(m.Groups) == 0 { errChan <- errors.New("no projects or groups defined in the mapping") @@ -134,6 +127,11 @@ func (m *MirrorMapping) check() error { } else if strings.Count(options.DestinationPath, "/") < 1 { errChan <- fmt.Errorf("invalid project destination path (must be in a namespace): %s", options.DestinationPath) } + visibilityString := strings.TrimSpace(string(options.Visibility)) + if visibilityString != "" && !checkVisibility(visibilityString) { + errChan <- fmt.Errorf("invalid project visibility: %s", string(options.Visibility)) + options.Visibility = string(gitlab.PublicVisibility) + } } // Check if the groups are valid @@ -147,11 +145,31 @@ func (m *MirrorMapping) check() error { if strings.HasPrefix(options.DestinationPath, "/") || strings.HasSuffix(options.DestinationPath, "/") { errChan <- fmt.Errorf("invalid destination path (must not start or end with /): %s", options.DestinationPath) } + visibilityString := strings.TrimSpace(string(options.Visibility)) + if visibilityString != "" && !checkVisibility(visibilityString) { + errChan <- fmt.Errorf("invalid group visibility: %s", string(options.Visibility)) + options.Visibility = string(gitlab.PublicVisibility) + } } close(errChan) return MergeErrors(errChan, 2) } +func checkVisibility(visibility string) bool { + var valid bool + switch visibility { + case string(gitlab.PublicVisibility): + valid = true + case string(gitlab.InternalVisibility): + valid = true + case string(gitlab.PrivateVisibility): + valid = true + default: + valid = false + } + return valid +} + // GraphQLClient is a client for sending GraphQL requests to GitLab type GraphQLClient struct { token string diff --git a/utils/types_test.go b/utils/types_test.go index 58fb646..f6ca03b 100644 --- a/utils/types_test.go +++ b/utils/types_test.go @@ -8,32 +8,23 @@ import ( "testing" ) -// testProjectMirroringOptions is a helper function to create a ProjectMirroringOptions instance for testing -func testProjectMirroringOptions() *ProjectMirroringOptions { - return &ProjectMirroringOptions{ +// testMirroringOptions is a helper function to create a MirroringOptions instance for testing +func testMirroringOptions() *MirroringOptions { + return &MirroringOptions{ DestinationPath: "project", CI_CD_Catalog: true, Issues: true, } } -// testGroupMirroringOptions is a helper function to create a GroupMirroringOptions instance for testing -func testGroupMirroringOptions() *GroupMirroringOptions { - return &GroupMirroringOptions{ - DestinationPath: "group", - CI_CD_Catalog: true, - Issues: true, - } -} - // TestAddProject tests adding a project to the MirrorMapping func TestAddProject(t *testing.T) { m := &MirrorMapping{ - Projects: make(map[string]*ProjectMirroringOptions), + Projects: make(map[string]*MirroringOptions), } project := "test-project" - options := testProjectMirroringOptions() + options := testMirroringOptions() m.AddProject(project, options) @@ -47,11 +38,11 @@ func TestAddProject(t *testing.T) { // TestAddGroup tests adding a group to the MirrorMapping func TestAddGroup(t *testing.T) { m := &MirrorMapping{ - Groups: make(map[string]*GroupMirroringOptions), + Groups: make(map[string]*MirroringOptions), } group := "test-group" - options := testGroupMirroringOptions() + options := testMirroringOptions() m.AddGroup(group, options) @@ -65,18 +56,22 @@ func TestAddGroup(t *testing.T) { // TestOpenMirrorMapping tests opening and parsing a JSON file into a MirrorMapping func TestOpenMirrorMapping(t *testing.T) { expectedMapping := &MirrorMapping{ - Projects: map[string]*ProjectMirroringOptions{ + Projects: map[string]*MirroringOptions{ "project1": { - DestinationPath: "http://example.com/project", - CI_CD_Catalog: true, - Issues: true, + DestinationPath: "http://example.com/project", + CI_CD_Catalog: true, + Issues: true, + MirrorTriggerBuilds: false, + Visibility: "private", }, }, - Groups: map[string]*GroupMirroringOptions{ + Groups: map[string]*MirroringOptions{ "group1": { - DestinationPath: "http://example.com/group", - CI_CD_Catalog: true, - Issues: true, + DestinationPath: "http://example.com/group", + CI_CD_Catalog: true, + Issues: true, + MirrorTriggerBuilds: false, + Visibility: "private", }, }, } @@ -86,14 +81,18 @@ func TestOpenMirrorMapping(t *testing.T) { "project1": { "destination_path": "http://example.com/project", "ci_cd_catalog": true, - "issues": true + "issues": true, + "mirror_trigger_builds": false, + "visibility": "private" } }, "groups": { "group1": { "destination_path": "http://example.com/group", "ci_cd_catalog": true, - "issues": true + "issues": true, + "mirror_trigger_builds": false, + "visibility": "private" } } }` @@ -115,8 +114,27 @@ func TestOpenMirrorMapping(t *testing.T) { t.Fatalf("OpenMirrorMapping() error = %v", err) } - if !reflect.DeepEqual(mapping, expectedMapping) { - t.Errorf("expected mapping %v, got %v", expectedMapping, mapping) + // Check if projects and groups are equal + if len(mapping.Projects) != len(expectedMapping.Projects) || len(mapping.Groups) != len(expectedMapping.Groups) { + t.Fatalf("expected mapping to have %d projects and %d groups, got %d projects and %d groups", len(expectedMapping.Projects), len(expectedMapping.Groups), len(mapping.Projects), len(mapping.Groups)) + } + for k, v := range mapping.Projects { + if expected, ok := expectedMapping.Projects[k]; ok { + if !reflect.DeepEqual(v, expected) { + t.Errorf("expected project %s options %v, got %v", k, expected, v) + } + } else { + t.Errorf("unexpected project %s in mapping", k) + } + } + for k, v := range mapping.Groups { + if expected, ok := expectedMapping.Groups[k]; ok { + if !reflect.DeepEqual(v, expected) { + t.Errorf("expected group %s options %v, got %v", k, expected, v) + } + } else { + t.Errorf("unexpected group %s in mapping", k) + } } } @@ -130,14 +148,14 @@ func TestCheck(t *testing.T) { { name: "ValidMapping", mapping: &MirrorMapping{ - Projects: map[string]*ProjectMirroringOptions{ + Projects: map[string]*MirroringOptions{ "project1": { DestinationPath: "http://example.com/project", CI_CD_Catalog: true, Issues: true, }, }, - Groups: map[string]*GroupMirroringOptions{ + Groups: map[string]*MirroringOptions{ "group1": { DestinationPath: "http://example.com/group", CI_CD_Catalog: true, @@ -150,28 +168,28 @@ func TestCheck(t *testing.T) { { name: "InvalidMappingNoProjectsOrGroups", mapping: &MirrorMapping{ - Projects: map[string]*ProjectMirroringOptions{}, - Groups: map[string]*GroupMirroringOptions{}, + Projects: map[string]*MirroringOptions{}, + Groups: map[string]*MirroringOptions{}, }, expectedErr: "\n - no projects or groups defined in the mapping\n", }, { name: "InvalidProjectMapping", mapping: &MirrorMapping{ - Projects: map[string]*ProjectMirroringOptions{ + Projects: map[string]*MirroringOptions{ "": { DestinationPath: "", }, }, - Groups: map[string]*GroupMirroringOptions{}, + Groups: map[string]*MirroringOptions{}, }, expectedErr: "\n - invalid (empty) string in project mapping: \n - invalid project destination path (must be in a namespace): \n", }, { name: "InvalidGroupMapping", mapping: &MirrorMapping{ - Projects: map[string]*ProjectMirroringOptions{}, - Groups: map[string]*GroupMirroringOptions{ + Projects: map[string]*MirroringOptions{}, + Groups: map[string]*MirroringOptions{ "": { DestinationPath: "", },