Skip to content

Commit e77786e

Browse files
authored
fix: Cannot deploy MCPRegistry from git in OpenShift (#2263)
* Implement FetchFileSparse method for optimized file retrieval in Git client Using configurable workspacedir to checkout the repo Signed-off-by: Daniele Martinoli <[email protected]> * review comments Signed-off-by: Daniele Martinoli <[email protected]> * - forced GC invocation - more logging - increased memory size Signed-off-by: Daniele Martinoli <[email protected]> * - reverted to use the previous approach (git clone) but with in-memory FS - added env variables to expedite the GC and avoid out of memory errors Signed-off-by: Daniele Martinoli <[email protected]> * review comments Signed-off-by: Daniele Martinoli <[email protected]> * restored GC() chart version bump run task operator-manifests Signed-off-by: Daniele Martinoli <[email protected]> * chart version bump Signed-off-by: Daniele Martinoli <[email protected]> * added missing space Signed-off-by: Daniele Martinoli <[email protected]> * updated go mod * version bump --------- Signed-off-by: Daniele Martinoli <[email protected]>
1 parent 6c2a83e commit e77786e

File tree

19 files changed

+777
-366
lines changed

19 files changed

+777
-366
lines changed

cmd/thv-operator/pkg/git/client.go

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@ package git
33
import (
44
"context"
55
"fmt"
6-
"os"
6+
"runtime"
77

8+
"github.com/go-git/go-billy/v5/memfs"
9+
"github.com/go-git/go-billy/v5/util"
810
"github.com/go-git/go-git/v5"
911
"github.com/go-git/go-git/v5/plumbing"
12+
"github.com/go-git/go-git/v5/plumbing/cache"
13+
"github.com/go-git/go-git/v5/storage/filesystem"
14+
"sigs.k8s.io/controller-runtime/pkg/log"
1015
)
1116

1217
// Client defines the interface for Git operations
@@ -18,7 +23,7 @@ type Client interface {
1823
GetFileContent(repoInfo *RepositoryInfo, path string) ([]byte, error)
1924

2025
// Cleanup removes local repository directory
21-
Cleanup(repoInfo *RepositoryInfo) error
26+
Cleanup(ctx context.Context, repoInfo *RepositoryInfo) error
2227
}
2328

2429
// DefaultGitClient implements GitClient using go-git
@@ -49,16 +54,37 @@ func (c *DefaultGitClient) Clone(ctx context.Context, config *CloneConfig) (*Rep
4954
}
5055
// For commit-based clones, we need the full repository to ensure the commit is available
5156

57+
// Use in-memory filesystems for the repository and the storer
58+
// See https://github.com/mindersec/minder/blob/main/internal/providers/git/git.go
59+
// for more details
5260
// Clone the repository
53-
repo, err := git.PlainCloneContext(ctx, config.Directory, false, cloneOptions)
61+
memFS := memfs.New()
62+
memFS = &LimitedFs{
63+
Fs: memFS,
64+
MaxFiles: 10 * 1000,
65+
TotalFileSize: 100 * 1024 * 1024,
66+
}
67+
// go-git seems to want separate filesystems for the storer and the checked out files
68+
storerFs := memfs.New()
69+
storerFs = &LimitedFs{
70+
Fs: storerFs,
71+
MaxFiles: 10 * 1000,
72+
TotalFileSize: 100 * 1024 * 1024,
73+
}
74+
storerCache := cache.NewObjectLRUDefault()
75+
storer := filesystem.NewStorage(storerFs, storerCache)
76+
77+
repo, err := git.CloneContext(ctx, storer, memFS, cloneOptions)
5478
if err != nil {
5579
return nil, fmt.Errorf("failed to clone repository: %w", err)
5680
}
5781

5882
// Get repository information
5983
repoInfo := &RepositoryInfo{
60-
Repository: repo,
61-
RemoteURL: config.URL,
84+
Repository: repo,
85+
RemoteURL: config.URL,
86+
storerFilesystem: storerFs,
87+
objectCache: storerCache,
6288
}
6389

6490
// If specific commit is requested, checkout that commit
@@ -125,19 +151,39 @@ func (*DefaultGitClient) GetFileContent(repoInfo *RepositoryInfo, path string) (
125151
}
126152

127153
// Cleanup removes local repository directory
128-
func (*DefaultGitClient) Cleanup(repoInfo *RepositoryInfo) error {
154+
func (*DefaultGitClient) Cleanup(ctx context.Context, repoInfo *RepositoryInfo) error {
155+
logger := log.FromContext(ctx)
129156
if repoInfo == nil || repoInfo.Repository == nil {
130-
return nil
157+
return fmt.Errorf("repository is nil")
131158
}
132159

133-
// Get the repository directory from the worktree
134-
workTree, err := repoInfo.Repository.Worktree()
135-
if err != nil {
136-
return fmt.Errorf("failed to get worktree: %w", err)
160+
// 1. Clear object cache explicitly
161+
if repoInfo.objectCache != nil {
162+
logger.V(1).Info("Clearing object cache")
163+
repoInfo.objectCache.Clear()
164+
}
165+
166+
// 2. Clear worktree filesystem
167+
worktree, err := repoInfo.Repository.Worktree()
168+
if err == nil && worktree.Filesystem != nil {
169+
logger.V(1).Info("Clearing worktree filesystem")
170+
_ = util.RemoveAll(worktree.Filesystem, "/")
137171
}
138172

139-
// Remove the directory
140-
return os.RemoveAll(workTree.Filesystem.Root())
173+
// 3. Clear storer filesystem (memfs)
174+
if repoInfo.storerFilesystem != nil {
175+
logger.V(1).Info("Clearing storer filesystem")
176+
_ = util.RemoveAll(repoInfo.storerFilesystem, "/")
177+
}
178+
179+
// 4. Nil out all references
180+
repoInfo.objectCache = nil
181+
repoInfo.storerFilesystem = nil
182+
repoInfo.Repository = nil
183+
184+
// // 5. Force GC to reclaim memory
185+
runtime.GC()
186+
return nil
141187
}
142188

143189
// updateRepositoryInfo updates the repository info with current state

cmd/thv-operator/pkg/git/client_test.go

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

33
import (
44
"context"
5-
"os"
65
"testing"
76
)
87

@@ -24,15 +23,8 @@ func TestDefaultGitClient_Clone_InvalidURL(t *testing.T) {
2423
client := NewDefaultGitClient()
2524
ctx := context.Background()
2625

27-
tempDir, err := os.MkdirTemp("", "git-test-*")
28-
if err != nil {
29-
t.Fatalf("Failed to create temp dir: %v", err)
30-
}
31-
defer os.RemoveAll(tempDir)
32-
3326
config := &CloneConfig{
34-
URL: "invalid-url",
35-
Directory: tempDir,
27+
URL: "invalid-url",
3628
}
3729

3830
repoInfo, err := client.Clone(ctx, config)
@@ -49,15 +41,8 @@ func TestDefaultGitClient_Clone_NonExistentRepo(t *testing.T) {
4941
client := NewDefaultGitClient()
5042
ctx := context.Background()
5143

52-
tempDir, err := os.MkdirTemp("", "git-test-*")
53-
if err != nil {
54-
t.Fatalf("Failed to create temp dir: %v", err)
55-
}
56-
defer os.RemoveAll(tempDir)
57-
5844
config := &CloneConfig{
59-
URL: "https://github.com/nonexistent/nonexistent.git",
60-
Directory: tempDir,
45+
URL: "https://github.com/nonexistent/nonexistent.git",
6146
}
6247

6348
repoInfo, err := client.Clone(ctx, config)
@@ -73,9 +58,9 @@ func TestDefaultGitClient_Cleanup_NilRepoInfo(t *testing.T) {
7358
t.Parallel()
7459
client := NewDefaultGitClient()
7560

76-
err := client.Cleanup(nil)
77-
if err != nil {
78-
t.Errorf("Expected no error for nil repoInfo, got: %v", err)
61+
err := client.Cleanup(context.Background(), nil)
62+
if err == nil {
63+
t.Errorf("Expected error for nil repoInfo, got nil")
7964
}
8065
}
8166

@@ -86,9 +71,9 @@ func TestDefaultGitClient_Cleanup_NilRepository(t *testing.T) {
8671
Repository: nil,
8772
}
8873

89-
err := client.Cleanup(repoInfo)
90-
if err != nil {
91-
t.Errorf("Expected no error for nil repository, got: %v", err)
74+
err := client.Cleanup(context.Background(), repoInfo)
75+
if err == nil {
76+
t.Errorf("Expected error for nil repository, got nil")
9277
}
9378
}
9479

@@ -111,11 +96,10 @@ func TestDefaultGitClient_GetFileContent_NoRepo(t *testing.T) {
11196
func TestCloneConfig_Structure(t *testing.T) {
11297
t.Parallel()
11398
config := CloneConfig{
114-
URL: "https://github.com/example/repo.git",
115-
Branch: "main",
116-
Tag: "v1.0.0",
117-
Commit: "abc123",
118-
Directory: "/tmp/repo",
99+
URL: "https://github.com/example/repo.git",
100+
Branch: "main",
101+
Tag: "v1.0.0",
102+
Commit: "abc123",
119103
}
120104

121105
if config.URL != "https://github.com/example/repo.git" {
@@ -130,9 +114,6 @@ func TestCloneConfig_Structure(t *testing.T) {
130114
if config.Commit != "abc123" {
131115
t.Errorf("Expected Commit to be set correctly")
132116
}
133-
if config.Directory != "/tmp/repo" {
134-
t.Errorf("Expected Directory to be set correctly")
135-
}
136117
}
137118

138119
func TestRepositoryInfo_Structure(t *testing.T) {
@@ -169,9 +150,6 @@ func TestCloneConfig_EmptyFields(t *testing.T) {
169150
if config.Commit != "" {
170151
t.Error("Expected empty Commit by default")
171152
}
172-
if config.Directory != "" {
173-
t.Error("Expected empty Directory by default")
174-
}
175153
}
176154

177155
func TestRepositoryInfo_EmptyFields(t *testing.T) {

cmd/thv-operator/pkg/git/commit_test.go

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

33
import (
44
"context"
5-
"os"
65
"testing"
76
)
87

@@ -16,18 +15,10 @@ func TestDefaultGitClient_CloneSpecificCommit_RealRepo(t *testing.T) {
1615
client := NewDefaultGitClient()
1716
ctx := context.Background()
1817

19-
// Create temporary directory for cloning
20-
tempDir, err := os.MkdirTemp("", "git-commit-test-*")
21-
if err != nil {
22-
t.Fatalf("Failed to create temp dir: %v", err)
23-
}
24-
defer os.RemoveAll(tempDir)
25-
2618
// Test with a known commit from a public repository
2719
config := &CloneConfig{
28-
URL: "https://github.com/stacklok/toolhive",
29-
Commit: "395b6ba11bdd60b615a9630a66dcede2abcfbb48", // Known valid commit
30-
Directory: tempDir,
20+
URL: "https://github.com/stacklok/toolhive",
21+
Commit: "395b6ba11bdd60b615a9630a66dcede2abcfbb48", // Known valid commit
3122
}
3223

3324
repoInfo, err := client.Clone(ctx, config)
@@ -46,16 +37,10 @@ func TestDefaultGitClient_CloneSpecificCommit_RealRepo(t *testing.T) {
4637
}
4738

4839
// Clean up
49-
err = client.Cleanup(repoInfo)
40+
err = client.Cleanup(context.Background(), repoInfo)
5041
if err != nil {
5142
t.Fatalf("Failed to cleanup: %v", err)
5243
}
53-
54-
// Verify directory was removed
55-
_, err = os.Stat(tempDir)
56-
if !os.IsNotExist(err) {
57-
t.Error("Expected directory to be removed after cleanup")
58-
}
5944
}
6045

6146
// TestDefaultGitClient_CloneInvalidCommit tests error handling for invalid commit hash
@@ -68,25 +53,17 @@ func TestDefaultGitClient_CloneInvalidCommit(t *testing.T) {
6853
client := NewDefaultGitClient()
6954
ctx := context.Background()
7055

71-
// Create temporary directory for cloning
72-
tempDir, err := os.MkdirTemp("", "git-invalid-commit-test-*")
73-
if err != nil {
74-
t.Fatalf("Failed to create temp dir: %v", err)
75-
}
76-
defer os.RemoveAll(tempDir)
77-
7856
// Test with an invalid commit hash
7957
config := &CloneConfig{
80-
URL: "https://github.com/stacklok/toolhive",
81-
Commit: "f4da6f2", // This is the original invalid commit that was causing the error
82-
Directory: tempDir,
58+
URL: "https://github.com/stacklok/toolhive",
59+
Commit: "f4da6f2", // This is the original invalid commit that was causing the error
8360
}
8461

8562
repoInfo, err := client.Clone(ctx, config)
8663
if err == nil {
8764
t.Error("Expected error for invalid commit hash, got nil")
8865
if repoInfo != nil {
89-
client.Cleanup(repoInfo)
66+
client.Cleanup(context.Background(), repoInfo)
9067
}
9168
}
9269

0 commit comments

Comments
 (0)