diff --git a/pkg/getter/gitgetter.go b/pkg/getter/gitgetter.go index 559045547..dddf054f4 100644 --- a/pkg/getter/gitgetter.go +++ b/pkg/getter/gitgetter.go @@ -18,6 +18,7 @@ package getter import ( "bytes" + "context" "crypto/sha256" "encoding/hex" "fmt" @@ -27,6 +28,7 @@ import ( "os/exec" "path/filepath" "strings" + "time" ) // GitGetter handles fetching charts from Git repositories @@ -68,8 +70,12 @@ func (g *GitGetter) get(href string) (*bytes.Buffer, error) { slog.Debug("cloning git repository", "url", repoURL, "ref", ref, "path", chartPath) + // Create context with timeout + ctx, cancel := g.createContext() + defer cancel() + // Clone the repository - if err := g.cloneRepo(repoURL, ref, tmpDir); err != nil { + if err := g.cloneRepo(ctx, repoURL, ref, tmpDir); err != nil { return nil, fmt.Errorf("failed to clone repository: %w", err) } @@ -85,7 +91,7 @@ func (g *GitGetter) get(href string) (*bytes.Buffer, error) { } // Package the chart into a tarball - tarData, err := g.packageChart(chartDir) + tarData, err := g.packageChart(ctx, chartDir) if err != nil { return nil, fmt.Errorf("failed to package chart: %w", err) } @@ -93,8 +99,17 @@ func (g *GitGetter) get(href string) (*bytes.Buffer, error) { return tarData, nil } +// createContext creates a context with timeout from getter options +func (g *GitGetter) createContext() (context.Context, context.CancelFunc) { + if g.opts.timeout > 0 { + return context.WithTimeout(context.Background(), g.opts.timeout) + } + // Default timeout of 5 minutes for Git operations + return context.WithTimeout(context.Background(), 5*time.Minute) +} + // cloneRepo clones a Git repository to the specified directory -func (g *GitGetter) cloneRepo(repoURL, ref, destDir string) error { +func (g *GitGetter) cloneRepo(ctx context.Context, repoURL, ref, destDir string) error { // Use shallow clone for better performance args := []string{"clone", "--depth", "1"} @@ -105,24 +120,32 @@ func (g *GitGetter) cloneRepo(repoURL, ref, destDir string) error { args = append(args, repoURL, destDir) - cmd := exec.Command("git", args...) + cmd := exec.CommandContext(ctx, "git", args...) var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil { + // Check if it was a timeout + if ctx.Err() == context.DeadlineExceeded { + return fmt.Errorf("git clone timed out") + } + // If shallow clone with branch failed, try full clone and checkout if ref != "" && ref != "HEAD" && ref != "master" && ref != "main" { slog.Debug("shallow clone failed, trying full clone", "error", stderr.String()) - return g.fullCloneAndCheckout(repoURL, ref, destDir) + return g.fullCloneAndCheckout(ctx, repoURL, ref, destDir) } return fmt.Errorf("git clone failed: %s", stderr.String()) } // If ref is specified but wasn't used in clone (HEAD, master, main), checkout now if ref != "" && (ref == "HEAD" || ref == "master" || ref == "main") { - cmd := exec.Command("git", "-C", destDir, "checkout", ref) + cmd := exec.CommandContext(ctx, "git", "-C", destDir, "checkout", ref) cmd.Stderr = &stderr if err := cmd.Run(); err != nil { + if ctx.Err() == context.DeadlineExceeded { + return fmt.Errorf("git checkout timed out") + } return fmt.Errorf("git checkout failed: %s", stderr.String()) } } @@ -131,24 +154,30 @@ func (g *GitGetter) cloneRepo(repoURL, ref, destDir string) error { } // fullCloneAndCheckout performs a full clone and checks out a specific ref (commit SHA, tag, or branch) -func (g *GitGetter) fullCloneAndCheckout(repoURL, ref, destDir string) error { +func (g *GitGetter) fullCloneAndCheckout(ctx context.Context, repoURL, ref, destDir string) error { var stderr bytes.Buffer // Remove the directory if it exists from failed shallow clone os.RemoveAll(destDir) // Full clone - cmd := exec.Command("git", "clone", repoURL, destDir) + cmd := exec.CommandContext(ctx, "git", "clone", repoURL, destDir) cmd.Stderr = &stderr if err := cmd.Run(); err != nil { + if ctx.Err() == context.DeadlineExceeded { + return fmt.Errorf("git clone timed out") + } return fmt.Errorf("git clone failed: %s", stderr.String()) } // Checkout the specific ref - cmd = exec.Command("git", "-C", destDir, "checkout", ref) + cmd = exec.CommandContext(ctx, "git", "-C", destDir, "checkout", ref) stderr.Reset() cmd.Stderr = &stderr if err := cmd.Run(); err != nil { + if ctx.Err() == context.DeadlineExceeded { + return fmt.Errorf("git checkout timed out") + } return fmt.Errorf("git checkout failed for ref %q: %s", ref, stderr.String()) } @@ -156,7 +185,7 @@ func (g *GitGetter) fullCloneAndCheckout(repoURL, ref, destDir string) error { } // packageChart packages a chart directory into a tarball -func (g *GitGetter) packageChart(chartDir string) (*bytes.Buffer, error) { +func (g *GitGetter) packageChart(ctx context.Context, chartDir string) (*bytes.Buffer, error) { // Use helm package command to create the tarball tmpDir, err := os.MkdirTemp("", "helm-git-package-") if err != nil { @@ -166,11 +195,14 @@ func (g *GitGetter) packageChart(chartDir string) (*bytes.Buffer, error) { // Use --dependency-update to automatically fetch any dependencies the chart needs // This handles charts that have their own dependencies - cmd := exec.Command("helm", "package", chartDir, "-d", tmpDir, "--dependency-update") + cmd := exec.CommandContext(ctx, "helm", "package", chartDir, "-d", tmpDir, "--dependency-update") var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil { + if ctx.Err() == context.DeadlineExceeded { + return nil, fmt.Errorf("helm package timed out") + } return nil, fmt.Errorf("helm package failed: %s", stderr.String()) }