Skip to content

Commit f97a2e8

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix-auditor-middleware-transport
2 parents d79ab63 + 21408ee commit f97a2e8

29 files changed

+2616
-173
lines changed

cmd/thv-operator/api/v1alpha1/mcpregistry_types.go

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import (
1010
const (
1111
// RegistrySourceTypeConfigMap is the type for registry data stored in ConfigMaps
1212
RegistrySourceTypeConfigMap = "configmap"
13+
14+
// RegistrySourceTypeGit is the type for registry data stored in Git repositories
15+
RegistrySourceTypeGit = "git"
1316
)
1417

1518
// Registry formats
@@ -45,8 +48,8 @@ type MCPRegistrySpec struct {
4548

4649
// MCPRegistrySource defines the source configuration for registry data
4750
type MCPRegistrySource struct {
48-
// Type is the type of source (configmap)
49-
// +kubebuilder:validation:Enum=configmap
51+
// Type is the type of source (configmap, git)
52+
// +kubebuilder:validation:Enum=configmap;git
5053
// +kubebuilder:default=configmap
5154
Type string `json:"type"`
5255

@@ -59,6 +62,11 @@ type MCPRegistrySource struct {
5962
// Only used when Type is "configmap"
6063
// +optional
6164
ConfigMap *ConfigMapSource `json:"configmap,omitempty"`
65+
66+
// Git defines the Git repository source configuration
67+
// Only used when Type is "git"
68+
// +optional
69+
Git *GitSource `json:"git,omitempty"`
6270
}
6371

6472
// ConfigMapSource defines ConfigMap source configuration
@@ -75,6 +83,36 @@ type ConfigMapSource struct {
7583
Key string `json:"key,omitempty"`
7684
}
7785

86+
// GitSource defines Git repository source configuration
87+
type GitSource struct {
88+
// Repository is the Git repository URL (HTTP/HTTPS/SSH)
89+
// +kubebuilder:validation:Required
90+
// +kubebuilder:validation:MinLength=1
91+
// +kubebuilder:validation:Pattern="^(https?://|git@|ssh://|git://).*"
92+
Repository string `json:"repository"`
93+
94+
// Branch is the Git branch to use (mutually exclusive with Tag and Commit)
95+
// +kubebuilder:validation:MinLength=1
96+
// +optional
97+
Branch string `json:"branch,omitempty"`
98+
99+
// Tag is the Git tag to use (mutually exclusive with Branch and Commit)
100+
// +kubebuilder:validation:MinLength=1
101+
// +optional
102+
Tag string `json:"tag,omitempty"`
103+
104+
// Commit is the Git commit SHA to use (mutually exclusive with Branch and Tag)
105+
// +kubebuilder:validation:MinLength=1
106+
// +optional
107+
Commit string `json:"commit,omitempty"`
108+
109+
// Path is the path to the registry file within the repository
110+
// +kubebuilder:validation:Pattern=^.*\.json$
111+
// +kubebuilder:default=registry.json
112+
// +optional
113+
Path string `json:"path,omitempty"`
114+
}
115+
78116
// SyncPolicy defines automatic synchronization behavior.
79117
// When specified, enables automatic synchronization at the given interval.
80118
// Manual synchronization via annotation-based triggers is always available
@@ -231,7 +269,7 @@ const (
231269
//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
232270
//+kubebuilder:resource:scope=Namespaced,categories=toolhive
233271
//nolint:lll
234-
//+kubebuilder:validation:XValidation:rule="self.spec.source.type == 'configmap' ? has(self.spec.source.configmap) : true",message="configMap field is required when source type is 'configmap'"
272+
//+kubebuilder:validation:XValidation:rule="self.spec.source.type == 'configmap' ? has(self.spec.source.configmap) : (self.spec.source.type == 'git' ? has(self.spec.source.git) : true)",message="configMap field is required when source type is 'configmap', git field is required when source type is 'git'"
235273

236274
// MCPRegistry is the Schema for the mcpregistries API
237275
// ⚠️ Experimental API (v1alpha1) — subject to change.

cmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package git
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
"github.com/go-git/go-git/v5"
9+
"github.com/go-git/go-git/v5/plumbing"
10+
)
11+
12+
// Client defines the interface for Git operations
13+
type Client interface {
14+
// Clone clones a repository with the given configuration
15+
Clone(ctx context.Context, config *CloneConfig) (*RepositoryInfo, error)
16+
17+
// GetFileContent retrieves the content of a file from the repository
18+
GetFileContent(repoInfo *RepositoryInfo, path string) ([]byte, error)
19+
20+
// Cleanup removes local repository directory
21+
Cleanup(repoInfo *RepositoryInfo) error
22+
}
23+
24+
// DefaultGitClient implements GitClient using go-git
25+
type DefaultGitClient struct{}
26+
27+
// NewDefaultGitClient creates a new DefaultGitClient
28+
func NewDefaultGitClient() *DefaultGitClient {
29+
return &DefaultGitClient{}
30+
}
31+
32+
// Clone clones a repository with the given configuration
33+
func (c *DefaultGitClient) Clone(ctx context.Context, config *CloneConfig) (*RepositoryInfo, error) {
34+
// Prepare clone options (no authentication for initial version)
35+
cloneOptions := &git.CloneOptions{
36+
URL: config.URL,
37+
}
38+
39+
// Set reference if specified (but not for commit-based clones)
40+
if config.Commit == "" {
41+
cloneOptions.Depth = 1
42+
if config.Branch != "" {
43+
cloneOptions.ReferenceName = plumbing.NewBranchReferenceName(config.Branch)
44+
cloneOptions.SingleBranch = true
45+
} else if config.Tag != "" {
46+
cloneOptions.ReferenceName = plumbing.NewTagReferenceName(config.Tag)
47+
cloneOptions.SingleBranch = true
48+
}
49+
}
50+
// For commit-based clones, we need the full repository to ensure the commit is available
51+
52+
// Clone the repository
53+
repo, err := git.PlainCloneContext(ctx, config.Directory, false, cloneOptions)
54+
if err != nil {
55+
return nil, fmt.Errorf("failed to clone repository: %w", err)
56+
}
57+
58+
// Get repository information
59+
repoInfo := &RepositoryInfo{
60+
Repository: repo,
61+
RemoteURL: config.URL,
62+
}
63+
64+
// If specific commit is requested, checkout that commit
65+
if config.Commit != "" {
66+
workTree, err := repo.Worktree()
67+
if err != nil {
68+
return nil, fmt.Errorf("failed to get worktree: %w", err)
69+
}
70+
71+
hash := plumbing.NewHash(config.Commit)
72+
err = workTree.Checkout(&git.CheckoutOptions{
73+
Hash: hash,
74+
})
75+
if err != nil {
76+
return nil, fmt.Errorf("failed to checkout commit %s: %w", config.Commit, err)
77+
}
78+
}
79+
80+
// Update repository info with current state
81+
if err := c.updateRepositoryInfo(repoInfo); err != nil {
82+
return nil, fmt.Errorf("failed to update repository info: %w", err)
83+
}
84+
85+
return repoInfo, nil
86+
}
87+
88+
// GetFileContent retrieves the content of a file from the repository
89+
func (*DefaultGitClient) GetFileContent(repoInfo *RepositoryInfo, path string) ([]byte, error) {
90+
if repoInfo == nil || repoInfo.Repository == nil {
91+
return nil, fmt.Errorf("repository is nil")
92+
}
93+
94+
// Get the HEAD reference
95+
ref, err := repoInfo.Repository.Head()
96+
if err != nil {
97+
return nil, fmt.Errorf("failed to get HEAD reference: %w", err)
98+
}
99+
100+
// Get the commit object
101+
commit, err := repoInfo.Repository.CommitObject(ref.Hash())
102+
if err != nil {
103+
return nil, fmt.Errorf("failed to get commit object: %w", err)
104+
}
105+
106+
// Get the tree
107+
tree, err := commit.Tree()
108+
if err != nil {
109+
return nil, fmt.Errorf("failed to get tree: %w", err)
110+
}
111+
112+
// Get the file
113+
file, err := tree.File(path)
114+
if err != nil {
115+
return nil, fmt.Errorf("failed to get file %s: %w", path, err)
116+
}
117+
118+
// Read file contents
119+
content, err := file.Contents()
120+
if err != nil {
121+
return nil, fmt.Errorf("failed to read file contents: %w", err)
122+
}
123+
124+
return []byte(content), nil
125+
}
126+
127+
// Cleanup removes local repository directory
128+
func (*DefaultGitClient) Cleanup(repoInfo *RepositoryInfo) error {
129+
if repoInfo == nil || repoInfo.Repository == nil {
130+
return nil
131+
}
132+
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)
137+
}
138+
139+
// Remove the directory
140+
return os.RemoveAll(workTree.Filesystem.Root())
141+
}
142+
143+
// updateRepositoryInfo updates the repository info with current state
144+
func (*DefaultGitClient) updateRepositoryInfo(repoInfo *RepositoryInfo) error {
145+
if repoInfo == nil || repoInfo.Repository == nil {
146+
return fmt.Errorf("repository is nil")
147+
}
148+
149+
// Get current branch name
150+
ref, err := repoInfo.Repository.Head()
151+
if err != nil {
152+
return fmt.Errorf("failed to get HEAD reference: %w", err)
153+
}
154+
155+
if ref.Name().IsBranch() {
156+
repoInfo.Branch = ref.Name().Short()
157+
}
158+
159+
return nil
160+
}

0 commit comments

Comments
 (0)