Skip to content

Commit cbb3a8b

Browse files
committed
feat: add version check for destination gitlab
The execution now fails gracefully if the version of the destination GitLab instance is not at least 17.6 This is required for pull mirroring via API call NOTE: update go version to current latest (1.24.3)
1 parent 721c1a8 commit cbb3a8b

File tree

6 files changed

+130
-16
lines changed

6 files changed

+130
-16
lines changed

Containerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM docker.io/golang:1.24.2-alpine AS build
1+
FROM docker.io/golang:1.24.3-alpine AS build
22

33
ARG VERSION="dev"
44

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
module gitlab-sync
22

3-
go 1.24.2
3+
go 1.24.3
44

55
require (
6+
github.com/Masterminds/semver/v3 v3.3.1
67
github.com/hashicorp/go-retryablehttp v0.7.7
78
github.com/spf13/cobra v1.9.1
89
gitlab.com/gitlab-org/api/client-go v0.128.0

internal/mirroring/get.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ import (
77
"strings"
88
"sync"
99

10+
"github.com/Masterminds/semver/v3"
1011
"go.uber.org/zap"
1112
)
1213

14+
const (
15+
INSTANCE_SEMVER_THRESHOLD = "17.6"
16+
)
17+
1318
// fetchAll retrieves all projects and groups from the GitLab instance
1419
// that match the filters and stores them in the instance cache.
1520
func (g *GitlabInstance) fetchAll(projectFilters map[string]struct{}, groupFilters map[string]struct{}, mirrorMapping *utils.MirrorMapping) []error {
@@ -77,3 +82,25 @@ func checkPathMatchesFilters(resourcePath string, projectFilters *map[string]str
7782
}
7883
return "", false
7984
}
85+
86+
func (g *GitlabInstance) CheckVersion() error {
87+
metadata, _, err := g.Gitlab.Metadata.GetMetadata()
88+
if err != nil {
89+
return fmt.Errorf("failed to get GitLab version: %w", err)
90+
}
91+
zap.L().Debug("GitLab Instance version", zap.String(ROLE, g.Role), zap.String("version", metadata.Version))
92+
93+
currentVer, err := semver.NewVersion(metadata.Version)
94+
if err != nil {
95+
return fmt.Errorf("failed to parse GitLab version: %w", err)
96+
}
97+
thresholdVer, err := semver.NewVersion(INSTANCE_SEMVER_THRESHOLD)
98+
if err != nil {
99+
return fmt.Errorf("failed to parse version threshold: %w", err)
100+
}
101+
102+
if currentVer.LessThan(thresholdVer) {
103+
return fmt.Errorf("GitLab version %s is below required threshold %s", currentVer, thresholdVer)
104+
}
105+
return nil
106+
}

internal/mirroring/get_test.go

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package mirroring
22

33
import (
44
"gitlab-sync/internal/utils"
5+
"net/http"
56
"testing"
67
)
78

@@ -64,11 +65,10 @@ func TestCheckPathMatchesFilters(t *testing.T) {
6465
}
6566
})
6667
}
67-
6868
}
6969

7070
func TestGetParentNamespaceID(t *testing.T) {
71-
gitlabInstance := setupTestGitlabInstance(t, ROLE_DESTINATION, INSTANCE_SIZE_SMALL)
71+
_, gitlabInstance := setupEmptyTestServer(t, ROLE_DESTINATION, INSTANCE_SIZE_SMALL)
7272
gitlabInstance.addGroup(TEST_GROUP)
7373
gitlabInstance.addProject(TEST_PROJECT)
7474

@@ -194,3 +194,59 @@ func TestFetchAll(t *testing.T) {
194194
}
195195

196196
}
197+
198+
func TestCheckVersion(t *testing.T) {
199+
tests := []struct {
200+
name string
201+
version string
202+
expectedError bool
203+
}{
204+
{
205+
name: "Valid version under threshold",
206+
version: "15.0.0",
207+
expectedError: true,
208+
},
209+
{
210+
name: "Valid version above threshold",
211+
version: "17.9.3-ce.0",
212+
expectedError: false,
213+
},
214+
{
215+
name: "Invalid version format with 1 dot",
216+
version: "invalid.version",
217+
expectedError: true,
218+
},
219+
{
220+
name: "Invalid version format with 2 dots",
221+
version: "invalid.version.1",
222+
expectedError: true,
223+
},
224+
{
225+
name: "Invalid empty version",
226+
version: "",
227+
expectedError: true,
228+
},
229+
}
230+
231+
// Iterate over the test cases
232+
for _, test := range tests {
233+
t.Run(test.name, func(t *testing.T) {
234+
t.Parallel()
235+
236+
mux, gitlabInstance := setupEmptyTestServer(t, ROLE_DESTINATION, INSTANCE_SIZE_SMALL)
237+
mux.HandleFunc("/api/v4/metadata", func(w http.ResponseWriter, r *http.Request) {
238+
w.Header().Set("Content-Type", "application/json")
239+
w.WriteHeader(http.StatusOK)
240+
_, err := w.Write([]byte(`{"version": "` + test.version + `"}`))
241+
if err != nil {
242+
t.Errorf("failed to write response: %v", err)
243+
}
244+
})
245+
246+
err := gitlabInstance.CheckVersion()
247+
if (err != nil) != test.expectedError {
248+
t.Errorf("expected error: %v, got: %v", test.expectedError, err)
249+
}
250+
})
251+
}
252+
}

internal/mirroring/helper_test.go

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,29 @@ var (
265265
}`}
266266
)
267267

268+
func setupEmptyTestServer(t *testing.T, role string, instanceSize string) (*http.ServeMux, *GitlabInstance) {
269+
// mux is the HTTP request multiplexer used with the test server.
270+
mux := http.NewServeMux()
271+
272+
// server is a test HTTP server used to provide mock API responses.
273+
server := httptest.NewServer(mux)
274+
t.Cleanup(server.Close)
275+
276+
gitlabInstance, err := newGitlabInstance(&GitlabInstanceOpts{
277+
GitlabURL: server.URL,
278+
GitlabToken: "test-token",
279+
Role: role,
280+
InstanceSize: instanceSize,
281+
MaxRetries: 0,
282+
})
283+
284+
if err != nil {
285+
t.Fatalf("Failed to create client: %v", err)
286+
}
287+
288+
return mux, gitlabInstance
289+
}
290+
268291
// setup sets up a test HTTP server along with a gitlab.Client that is
269292
// configured to talk to that test server. Tests should register handlers on
270293
// mux which provide mock responses for the API method being tested.
@@ -531,19 +554,22 @@ func setupTestProject(mux *http.ServeMux, project *gitlab.Project, stringRespons
531554
})
532555
}
533556

534-
// setupTestGitlabInstance sets up a test Gitlab instance with the given role and instance size.
535-
func setupTestGitlabInstance(t *testing.T, role string, instanceSize string) *GitlabInstance {
536-
gitlabInstance, err := newGitlabInstance(&GitlabInstanceOpts{
537-
GitlabURL: "https://gitlab.example.com",
538-
GitlabToken: "test-token",
539-
Role: role,
540-
InstanceSize: instanceSize,
541-
MaxRetries: 0,
557+
// setupMetadata sets up the test HTTP server with handlers for metadata-related actions.
558+
// This includes the version endpoint.
559+
func setupMetadata(mux *http.ServeMux) {
560+
// Setup the version endpoint to return a mock response.
561+
mux.HandleFunc("/api/v4/metadata", func(w http.ResponseWriter, r *http.Request) {
562+
switch r.Method {
563+
case http.MethodGet:
564+
w.Header().Set(HEADER_CONTENT_TYPE, HEADER_ACCEPT)
565+
// Set response status to 200 OK
566+
w.WriteHeader(http.StatusOK)
567+
fmt.Fprint(w, `{"version": "18.0.0"}`)
568+
default:
569+
// Set response status to 405 Method Not Allowed
570+
w.WriteHeader(http.StatusMethodNotAllowed)
571+
}
542572
})
543-
if err != nil {
544-
t.Fatalf("Failed to create Gitlab instance: %v", err)
545-
}
546-
return gitlabInstance
547573
}
548574

549575
func TestReverseGroupMirrorMap(t *testing.T) {

internal/mirroring/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ func MirrorGitlabs(gitlabMirrorArgs *utils.ParserArgs) []error {
4646
if err != nil {
4747
return []error{err}
4848
}
49+
err = destinationGitlabInstance.CheckVersion()
50+
if err != nil {
51+
return []error{err}
52+
}
4953

5054
sourceProjectFilters, sourceGroupFilters, destinationProjectFilters, destinationGroupFilters := processFilters(gitlabMirrorArgs.MirrorMapping)
5155

0 commit comments

Comments
 (0)