Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions cmd/thv-operator/api/v1alpha1/mcpregistry_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import (
const (
// RegistrySourceTypeConfigMap is the type for registry data stored in ConfigMaps
RegistrySourceTypeConfigMap = "configmap"

// RegistrySourceTypeGit is the type for registry data stored in Git repositories
RegistrySourceTypeGit = "git"
)

// Registry formats
Expand Down Expand Up @@ -45,8 +48,8 @@ type MCPRegistrySpec struct {

// MCPRegistrySource defines the source configuration for registry data
type MCPRegistrySource struct {
// Type is the type of source (configmap)
// +kubebuilder:validation:Enum=configmap
// Type is the type of source (configmap, git)
// +kubebuilder:validation:Enum=configmap;git
// +kubebuilder:default=configmap
Type string `json:"type"`

Expand All @@ -59,6 +62,11 @@ type MCPRegistrySource struct {
// Only used when Type is "configmap"
// +optional
ConfigMap *ConfigMapSource `json:"configmap,omitempty"`

// Git defines the Git repository source configuration
// Only used when Type is "git"
// +optional
Git *GitSource `json:"git,omitempty"`
}

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

// GitSource defines Git repository source configuration
type GitSource struct {
// Repository is the Git repository URL (HTTP/HTTPS/SSH)
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:Pattern="^(https?://|git@|ssh://|git://).*"
Repository string `json:"repository"`

// Branch is the Git branch to use (mutually exclusive with Tag and Commit)
// +kubebuilder:validation:MinLength=1
// +optional
Branch string `json:"branch,omitempty"`

// Tag is the Git tag to use (mutually exclusive with Branch and Commit)
// +kubebuilder:validation:MinLength=1
// +optional
Tag string `json:"tag,omitempty"`

// Commit is the Git commit SHA to use (mutually exclusive with Branch and Tag)
// +kubebuilder:validation:MinLength=1
// +optional
Commit string `json:"commit,omitempty"`

// Path is the path to the registry file within the repository
// +kubebuilder:validation:Pattern=^.*\.json$
// +kubebuilder:default=registry.json
// +optional
Path string `json:"path,omitempty"`
}

// SyncPolicy defines automatic synchronization behavior.
// When specified, enables automatic synchronization at the given interval.
// Manual synchronization via annotation-based triggers is always available
Expand Down Expand Up @@ -231,7 +269,7 @@ const (
//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
//+kubebuilder:resource:scope=Namespaced,categories=toolhive
//nolint:lll
//+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'"
//+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'"

// MCPRegistry is the Schema for the mcpregistries API
// ⚠️ Experimental API (v1alpha1) — subject to change.
Expand Down
20 changes: 20 additions & 0 deletions cmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

160 changes: 160 additions & 0 deletions cmd/thv-operator/pkg/git/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package git

import (
"context"
"fmt"
"os"

"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
)

// Client defines the interface for Git operations
type Client interface {
// Clone clones a repository with the given configuration
Clone(ctx context.Context, config *CloneConfig) (*RepositoryInfo, error)

// GetFileContent retrieves the content of a file from the repository
GetFileContent(repoInfo *RepositoryInfo, path string) ([]byte, error)

// Cleanup removes local repository directory
Cleanup(repoInfo *RepositoryInfo) error
}

// DefaultGitClient implements GitClient using go-git
type DefaultGitClient struct{}

// NewDefaultGitClient creates a new DefaultGitClient
func NewDefaultGitClient() *DefaultGitClient {
return &DefaultGitClient{}
}

// Clone clones a repository with the given configuration
func (c *DefaultGitClient) Clone(ctx context.Context, config *CloneConfig) (*RepositoryInfo, error) {
// Prepare clone options (no authentication for initial version)
cloneOptions := &git.CloneOptions{
URL: config.URL,
}

// Set reference if specified (but not for commit-based clones)
if config.Commit == "" {
cloneOptions.Depth = 1
if config.Branch != "" {
cloneOptions.ReferenceName = plumbing.NewBranchReferenceName(config.Branch)
cloneOptions.SingleBranch = true
} else if config.Tag != "" {
cloneOptions.ReferenceName = plumbing.NewTagReferenceName(config.Tag)
cloneOptions.SingleBranch = true
}
}
// For commit-based clones, we need the full repository to ensure the commit is available

// Clone the repository
repo, err := git.PlainCloneContext(ctx, config.Directory, false, cloneOptions)
if err != nil {
return nil, fmt.Errorf("failed to clone repository: %w", err)
}

// Get repository information
repoInfo := &RepositoryInfo{
Repository: repo,
RemoteURL: config.URL,
}

// If specific commit is requested, checkout that commit
if config.Commit != "" {
workTree, err := repo.Worktree()
if err != nil {
return nil, fmt.Errorf("failed to get worktree: %w", err)
}

hash := plumbing.NewHash(config.Commit)
err = workTree.Checkout(&git.CheckoutOptions{
Hash: hash,
})
if err != nil {
return nil, fmt.Errorf("failed to checkout commit %s: %w", config.Commit, err)
}
}

// Update repository info with current state
if err := c.updateRepositoryInfo(repoInfo); err != nil {
return nil, fmt.Errorf("failed to update repository info: %w", err)
}

return repoInfo, nil
}

// GetFileContent retrieves the content of a file from the repository
func (*DefaultGitClient) GetFileContent(repoInfo *RepositoryInfo, path string) ([]byte, error) {
if repoInfo == nil || repoInfo.Repository == nil {
return nil, fmt.Errorf("repository is nil")
}

// Get the HEAD reference
ref, err := repoInfo.Repository.Head()
if err != nil {
return nil, fmt.Errorf("failed to get HEAD reference: %w", err)
}

// Get the commit object
commit, err := repoInfo.Repository.CommitObject(ref.Hash())
if err != nil {
return nil, fmt.Errorf("failed to get commit object: %w", err)
}

// Get the tree
tree, err := commit.Tree()
if err != nil {
return nil, fmt.Errorf("failed to get tree: %w", err)
}

// Get the file
file, err := tree.File(path)
if err != nil {
return nil, fmt.Errorf("failed to get file %s: %w", path, err)
}

// Read file contents
content, err := file.Contents()
if err != nil {
return nil, fmt.Errorf("failed to read file contents: %w", err)
}

return []byte(content), nil
}

// Cleanup removes local repository directory
func (*DefaultGitClient) Cleanup(repoInfo *RepositoryInfo) error {
if repoInfo == nil || repoInfo.Repository == nil {
return nil
}

// Get the repository directory from the worktree
workTree, err := repoInfo.Repository.Worktree()
if err != nil {
return fmt.Errorf("failed to get worktree: %w", err)
}

// Remove the directory
return os.RemoveAll(workTree.Filesystem.Root())
}

// updateRepositoryInfo updates the repository info with current state
func (*DefaultGitClient) updateRepositoryInfo(repoInfo *RepositoryInfo) error {
if repoInfo == nil || repoInfo.Repository == nil {
return fmt.Errorf("repository is nil")
}

// Get current branch name
ref, err := repoInfo.Repository.Head()
if err != nil {
return fmt.Errorf("failed to get HEAD reference: %w", err)
}

if ref.Name().IsBranch() {
repoInfo.Branch = ref.Name().Short()
}

return nil
}
Loading
Loading