Skip to content

Commit c45f9c4

Browse files
committed
refactor: Extract common git repository preparation logic (#229)
Consolidate duplicated git repository setup code across chain handlers into a reusable PrepareGitRepository function and GitRepositoryContext struct. Changes: - Add GitRepositoryContext struct to hold git operation context - Add PrepareGitRepository function for common git setup workflow - Refactor PutDeployConfigs to use PrepareGitRepository - Refactor PutGitLabCIConfig to use PrepareGitRepository - Add comprehensive unit tests for PrepareGitRepository This reduces code duplication and improves maintainability by centralizing GitServer retrieval, SSH credential extraction, repository cloning, and branch checkout logic. Signed-off-by: Sergiy Kulanov <[email protected]>
1 parent a172e1f commit c45f9c4

File tree

4 files changed

+370
-96
lines changed

4 files changed

+370
-96
lines changed

controllers/codebase/service/chain/common.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,27 @@ import (
55
"fmt"
66
"strconv"
77

8+
corev1 "k8s.io/api/core/v1"
89
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
ctrl "sigs.k8s.io/controller-runtime"
911
"sigs.k8s.io/controller-runtime/pkg/client"
1012

1113
codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1"
14+
"github.com/epam/edp-codebase-operator/v2/pkg/git"
1215
"github.com/epam/edp-codebase-operator/v2/pkg/util"
1316
)
1417

18+
// GitRepositoryContext holds all context needed for git operations.
19+
// It contains the GitServer configuration, credentials, and paths
20+
// required for SSH-based git operations.
21+
type GitRepositoryContext struct {
22+
GitServer *codebaseApi.GitServer
23+
Secret *corev1.Secret
24+
PrivateSSHKey string
25+
RepoSSHUrl string
26+
WorkDir string
27+
}
28+
1529
func setIntermediateSuccessFields(ctx context.Context, c client.Client, cb *codebaseApi.Codebase, action codebaseApi.ActionType) error {
1630
// Set WebHookRef from WebHookID for backward compatibility.
1731
webHookRef := cb.Status.WebHookRef
@@ -71,3 +85,92 @@ func updateGitStatusWithPatch(
7185

7286
return nil
7387
}
88+
89+
// PrepareGitRepository performs complete git repository preparation workflow:
90+
// 1. Retrieves GitServer resource and its Secret
91+
// 2. Extracts SSH credentials and builds repository paths
92+
// 3. Clones repository via SSH if not already present locally
93+
// 4. Checks out the codebase's default branch
94+
//
95+
// This function handles the common git setup operations shared across
96+
// multiple chain handlers. Provider-specific operations (e.g., Gerrit hooks)
97+
// should be handled by the caller after this function returns.
98+
//
99+
// Returns GitRepositoryContext containing all necessary context for
100+
// subsequent git operations (commit, push, etc.).
101+
func PrepareGitRepository(
102+
ctx context.Context,
103+
c client.Client,
104+
g git.Git,
105+
codebase *codebaseApi.Codebase,
106+
) (*GitRepositoryContext, error) {
107+
log := ctrl.LoggerFrom(ctx)
108+
109+
// Step 1: Retrieve GitServer resource
110+
gitServer := &codebaseApi.GitServer{}
111+
if err := c.Get(
112+
ctx,
113+
client.ObjectKey{Name: codebase.Spec.GitServer, Namespace: codebase.Namespace},
114+
gitServer,
115+
); err != nil {
116+
return nil, fmt.Errorf("failed to get GitServer: %w", err)
117+
}
118+
119+
// Step 2: Retrieve GitServer Secret
120+
gitServerSecret := &corev1.Secret{}
121+
if err := c.Get(
122+
ctx,
123+
client.ObjectKey{Name: gitServer.Spec.NameSshKeySecret, Namespace: codebase.Namespace},
124+
gitServerSecret,
125+
); err != nil {
126+
return nil, fmt.Errorf("failed to get GitServer secret: %w", err)
127+
}
128+
129+
// Step 3: Extract SSH key and build paths
130+
privateSSHKey := string(gitServerSecret.Data[util.PrivateSShKeyName])
131+
repoSshUrl := util.GetSSHUrl(gitServer, codebase.Spec.GetProjectID())
132+
wd := util.GetWorkDir(codebase.Name, codebase.Namespace)
133+
134+
// Step 4: Clone repository if needed
135+
if !util.DoesDirectoryExist(wd) || util.IsDirectoryEmpty(wd) {
136+
log.Info("Start cloning repository", "url", repoSshUrl)
137+
138+
if err := g.CloneRepositoryBySsh(
139+
ctx,
140+
privateSSHKey,
141+
gitServer.Spec.GitUser,
142+
repoSshUrl,
143+
wd,
144+
gitServer.Spec.SshPort,
145+
); err != nil {
146+
return nil, fmt.Errorf("failed to clone git repository: %w", err)
147+
}
148+
149+
log.Info("Repository has been cloned", "url", repoSshUrl)
150+
}
151+
152+
// Step 5: Get repo URL for checkout
153+
repoUrl, err := util.GetRepoUrl(codebase)
154+
if err != nil {
155+
return nil, fmt.Errorf("failed to build repo url: %w", err)
156+
}
157+
158+
// Step 6: Checkout default branch
159+
log.Info("Start checkout default branch", "branch", codebase.Spec.DefaultBranch, "repo", repoUrl)
160+
161+
err = CheckoutBranch(repoUrl, wd, codebase.Spec.DefaultBranch, g, codebase, c)
162+
if err != nil {
163+
return nil, fmt.Errorf("failed to checkout default branch %v: %w", codebase.Spec.DefaultBranch, err)
164+
}
165+
166+
log.Info("Default branch has been checked out", "branch", codebase.Spec.DefaultBranch, "repo", repoUrl)
167+
168+
// Return context for subsequent operations
169+
return &GitRepositoryContext{
170+
GitServer: gitServer,
171+
Secret: gitServerSecret,
172+
PrivateSSHKey: privateSSHKey,
173+
RepoSSHUrl: repoSshUrl,
174+
WorkDir: wd,
175+
}, nil
176+
}

controllers/codebase/service/chain/common_test.go

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ import (
55
"testing"
66

77
"github.com/stretchr/testify/assert"
8+
testify "github.com/stretchr/testify/mock"
89
"github.com/stretchr/testify/require"
10+
corev1 "k8s.io/api/core/v1"
911
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1012
"k8s.io/apimachinery/pkg/runtime"
1113
"k8s.io/apimachinery/pkg/types"
14+
"sigs.k8s.io/controller-runtime/pkg/client"
1215
"sigs.k8s.io/controller-runtime/pkg/client/fake"
1316

1417
codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1"
18+
gitMocks "github.com/epam/edp-codebase-operator/v2/pkg/git/mocks"
1519
"github.com/epam/edp-codebase-operator/v2/pkg/util"
1620
)
1721

@@ -248,3 +252,230 @@ func TestUpdateGitStatusWithPatch_SequentialUpdates(t *testing.T) {
248252
assert.NoError(t, err)
249253
assert.Equal(t, util.ProjectTemplatesPushedStatus, updated.Status.Git)
250254
}
255+
256+
func TestPrepareGitRepository(t *testing.T) {
257+
scheme := runtime.NewScheme()
258+
require.NoError(t, codebaseApi.AddToScheme(scheme))
259+
require.NoError(t, corev1.AddToScheme(scheme))
260+
261+
gitServer := &codebaseApi.GitServer{
262+
ObjectMeta: metav1.ObjectMeta{
263+
Name: "test-gitserver",
264+
Namespace: fakeNamespace,
265+
},
266+
Spec: codebaseApi.GitServerSpec{
267+
GitHost: "github.com",
268+
GitUser: "git",
269+
NameSshKeySecret: "git-secret",
270+
SshPort: 22,
271+
GitProvider: codebaseApi.GitProviderGithub,
272+
},
273+
}
274+
275+
secret := &corev1.Secret{
276+
ObjectMeta: metav1.ObjectMeta{
277+
Name: "git-secret",
278+
Namespace: fakeNamespace,
279+
},
280+
Data: map[string][]byte{
281+
util.PrivateSShKeyName: []byte("test-ssh-key"),
282+
},
283+
}
284+
285+
tests := []struct {
286+
name string
287+
codebase *codebaseApi.Codebase
288+
objects []client.Object
289+
gitClient func(t *testing.T) *gitMocks.MockGit
290+
setup func(t *testing.T)
291+
wantErr require.ErrorAssertionFunc
292+
want func(t *testing.T, gitCtx *GitRepositoryContext)
293+
}{
294+
{
295+
name: "successfully prepare repository - clone required",
296+
codebase: &codebaseApi.Codebase{
297+
ObjectMeta: metav1.ObjectMeta{
298+
Name: fakeName,
299+
Namespace: fakeNamespace,
300+
},
301+
Spec: codebaseApi.CodebaseSpec{
302+
GitServer: gitServer.Name,
303+
DefaultBranch: "main",
304+
Strategy: codebaseApi.Create,
305+
GitUrlPath: fakeName,
306+
},
307+
},
308+
objects: []client.Object{gitServer, secret},
309+
gitClient: func(t *testing.T) *gitMocks.MockGit {
310+
m := gitMocks.NewMockGit(t)
311+
m.On("CloneRepositoryBySsh",
312+
testify.Anything, "test-ssh-key", "git",
313+
testify.Anything, testify.Anything, int32(22),
314+
).Return(nil)
315+
m.On("GetCurrentBranchName", testify.Anything).Return("main", nil)
316+
m.On("CheckPermissions", testify.Anything, testify.Anything, testify.Anything, testify.Anything).Return(true)
317+
return m
318+
},
319+
wantErr: require.NoError,
320+
want: func(t *testing.T, gitCtx *GitRepositoryContext) {
321+
require.NotNil(t, gitCtx)
322+
assert.Equal(t, "test-ssh-key", gitCtx.PrivateSSHKey)
323+
assert.Equal(t, gitServer.Name, gitCtx.GitServer.Name)
324+
assert.Equal(t, secret.Name, gitCtx.Secret.Name)
325+
assert.NotEmpty(t, gitCtx.RepoSSHUrl)
326+
assert.NotEmpty(t, gitCtx.WorkDir)
327+
},
328+
},
329+
{
330+
name: "failed to get GitServer",
331+
codebase: &codebaseApi.Codebase{
332+
ObjectMeta: metav1.ObjectMeta{
333+
Name: fakeName,
334+
Namespace: fakeNamespace,
335+
},
336+
Spec: codebaseApi.CodebaseSpec{
337+
GitServer: "missing-gitserver",
338+
DefaultBranch: "main",
339+
},
340+
},
341+
gitClient: func(t *testing.T) *gitMocks.MockGit {
342+
return gitMocks.NewMockGit(t)
343+
},
344+
wantErr: func(t require.TestingT, err error, i ...interface{}) {
345+
require.Error(t, err)
346+
require.Contains(t, err.Error(), "failed to get GitServer")
347+
},
348+
want: func(t *testing.T, gitCtx *GitRepositoryContext) {
349+
assert.Nil(t, gitCtx)
350+
},
351+
},
352+
{
353+
name: "failed to get GitServer secret",
354+
codebase: &codebaseApi.Codebase{
355+
ObjectMeta: metav1.ObjectMeta{
356+
Name: fakeName,
357+
Namespace: fakeNamespace,
358+
},
359+
Spec: codebaseApi.CodebaseSpec{
360+
GitServer: gitServer.Name,
361+
DefaultBranch: "main",
362+
},
363+
},
364+
objects: []client.Object{gitServer},
365+
gitClient: func(t *testing.T) *gitMocks.MockGit {
366+
return gitMocks.NewMockGit(t)
367+
},
368+
wantErr: func(t require.TestingT, err error, i ...interface{}) {
369+
require.Error(t, err)
370+
require.Contains(t, err.Error(), "failed to get GitServer secret")
371+
},
372+
want: func(t *testing.T, gitCtx *GitRepositoryContext) {
373+
assert.Nil(t, gitCtx)
374+
},
375+
},
376+
{
377+
name: "failed to clone repository",
378+
codebase: &codebaseApi.Codebase{
379+
ObjectMeta: metav1.ObjectMeta{
380+
Name: fakeName,
381+
Namespace: fakeNamespace,
382+
},
383+
Spec: codebaseApi.CodebaseSpec{
384+
GitServer: gitServer.Name,
385+
DefaultBranch: "main",
386+
Strategy: codebaseApi.Create,
387+
},
388+
},
389+
objects: []client.Object{gitServer, secret},
390+
gitClient: func(t *testing.T) *gitMocks.MockGit {
391+
m := gitMocks.NewMockGit(t)
392+
m.On("CloneRepositoryBySsh", testify.Anything, testify.Anything, testify.Anything, testify.Anything, testify.Anything, testify.Anything).
393+
Return(assert.AnError)
394+
return m
395+
},
396+
wantErr: func(t require.TestingT, err error, i ...interface{}) {
397+
require.Error(t, err)
398+
require.Contains(t, err.Error(), "failed to clone git repository")
399+
},
400+
want: func(t *testing.T, gitCtx *GitRepositoryContext) {
401+
assert.Nil(t, gitCtx)
402+
},
403+
},
404+
{
405+
name: "failed to checkout default branch",
406+
codebase: &codebaseApi.Codebase{
407+
ObjectMeta: metav1.ObjectMeta{
408+
Name: fakeName,
409+
Namespace: fakeNamespace,
410+
},
411+
Spec: codebaseApi.CodebaseSpec{
412+
GitServer: gitServer.Name,
413+
DefaultBranch: "main",
414+
Strategy: codebaseApi.Create,
415+
GitUrlPath: fakeName,
416+
Repository: &codebaseApi.Repository{Url: "https://github.com/test/repo.git"},
417+
},
418+
},
419+
objects: []client.Object{
420+
gitServer,
421+
secret,
422+
&corev1.Secret{
423+
ObjectMeta: metav1.ObjectMeta{
424+
Name: "repository-codebase-fake-name-temp",
425+
Namespace: fakeNamespace,
426+
},
427+
Data: map[string][]byte{
428+
"username": []byte("user"),
429+
"password": []byte("pass"),
430+
},
431+
},
432+
},
433+
gitClient: func(t *testing.T) *gitMocks.MockGit {
434+
m := gitMocks.NewMockGit(t)
435+
m.On("CloneRepositoryBySsh", testify.Anything, testify.Anything, testify.Anything, testify.Anything, testify.Anything, testify.Anything).
436+
Return(nil)
437+
m.On("CheckPermissions", testify.Anything, testify.Anything, testify.Anything, testify.Anything).
438+
Return(false)
439+
return m
440+
},
441+
wantErr: func(t require.TestingT, err error, i ...interface{}) {
442+
require.Error(t, err)
443+
require.Contains(t, err.Error(), "cannot get access to the repository")
444+
},
445+
want: func(t *testing.T, gitCtx *GitRepositoryContext) {
446+
assert.Nil(t, gitCtx)
447+
},
448+
},
449+
}
450+
451+
for _, tt := range tests {
452+
t.Run(tt.name, func(t *testing.T) {
453+
tmpDir := t.TempDir()
454+
t.Setenv(util.WorkDirEnv, tmpDir)
455+
456+
if tt.setup != nil {
457+
tt.setup(t)
458+
}
459+
460+
allObjects := append(tt.objects, tt.codebase)
461+
462+
k8sClient := fake.NewClientBuilder().
463+
WithScheme(scheme).
464+
WithObjects(allObjects...).
465+
Build()
466+
467+
gitCtx, err := PrepareGitRepository(
468+
context.Background(),
469+
k8sClient,
470+
tt.gitClient(t),
471+
tt.codebase,
472+
)
473+
474+
tt.wantErr(t, err)
475+
476+
if tt.want != nil {
477+
tt.want(t, gitCtx)
478+
}
479+
})
480+
}
481+
}

0 commit comments

Comments
 (0)