From e17b275473eb5ea077c52b308db22435a5348c13 Mon Sep 17 00:00:00 2001 From: Angel Misevski Date: Thu, 1 Jun 2023 14:48:52 -0400 Subject: [PATCH 1/2] Rework project-clone checkout refernce to not rely on go-git To avoid issues with self-signed certificates on git repositories, avoid use of go-git (which would require configuring additional CA bundles) and instead delegate to the git binary, which respects additional bundles automatically. The main cause for failure is go-git attempting to list references in a remote, which can fail due to untrusted certificates. Instead, we rely on the git binary to determine if a checkoutFrom.revision is a local/remote branch, tag, or hash. Signed-off-by: Angel Misevski --- project-clone/internal/git/operations.go | 69 +++++++----------------- project-clone/internal/shell/execute.go | 47 ++++++++++++++++ 2 files changed, 67 insertions(+), 49 deletions(-) diff --git a/project-clone/internal/git/operations.go b/project-clone/internal/git/operations.go index 2a8e0c58d..59949e021 100644 --- a/project-clone/internal/git/operations.go +++ b/project-clone/internal/git/operations.go @@ -22,9 +22,7 @@ import ( dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/go-git/go-git/v5" gitConfig "github.com/go-git/go-git/v5/config" - "github.com/go-git/go-git/v5/plumbing" - "github.com/devfile/devworkspace-operator/project-clone/internal" "github.com/devfile/devworkspace-operator/project-clone/internal/shell" ) @@ -109,44 +107,25 @@ func CheckoutReference(repo *git.Repository, project *dw.Project, projectPath st } else { defaultRemoteName = checkoutFrom.Remote } - remote, err := repo.Remote(defaultRemoteName) - if err != nil { - return fmt.Errorf("could not find remote %s: %s", defaultRemoteName, err) - } - auth, err := internal.GetAuthForHost(remote.Config().URLs[0]) - if err != nil { - log.Printf("Error reading credentials file: %s", err) - } - refs, err := remote.List(&git.ListOptions{ - Auth: auth, - }) + revision := checkoutFrom.Revision + refType, err := shell.GitResolveReference(projectPath, defaultRemoteName, revision) if err != nil { - return fmt.Errorf("failed to read remote %s: %s", defaultRemoteName, err) - } - - branch, err := repo.Branch(checkoutFrom.Revision) - if err == nil { - return checkoutLocalBranch(projectPath, branch.Name, defaultRemoteName) - } - - for _, ref := range refs { - if ref.Name().Short() != checkoutFrom.Revision { - continue - } - if ref.Name().IsBranch() { - return checkoutRemoteBranch(projectPath, defaultRemoteName, ref) - } else if ref.Name().IsTag() { - return checkoutTag(projectPath, defaultRemoteName, ref) - } - } - - log.Printf("No tag or branch named %s found on remote %s; attempting to resolve commit", checkoutFrom.Revision, defaultRemoteName) - if _, err := repo.ResolveRevision(plumbing.Revision(checkoutFrom.Revision)); err == nil { - return checkoutCommit(projectPath, checkoutFrom.Revision) + return fmt.Errorf("failed to resolve git revision %s: %w", revision, err) + } + switch refType { + case shell.GitRefLocalBranch: + return checkoutLocalBranch(projectPath, revision, defaultRemoteName) + case shell.GitRefRemoteBranch: + return checkoutRemoteBranch(projectPath, revision, defaultRemoteName) + case shell.GitRefTag: + return checkoutTag(projectPath, revision) + case shell.GitRefHash: + return checkoutCommit(projectPath, revision) + default: + log.Printf("Could not find revision %s in repository, using default branch", checkoutFrom.Revision) + return nil } - log.Printf("Could not find revision %s in repository, using default branch", checkoutFrom.Revision) - return nil } func checkoutLocalBranch(projectPath, branchName, remote string) error { @@ -163,29 +142,21 @@ func checkoutLocalBranch(projectPath, branchName, remote string) error { return nil } -func checkoutRemoteBranch(projectPath string, remote string, branchRef *plumbing.Reference) error { - // Implement logic of `git checkout `: - // 1. Create tracking info in .git/config to properly track remote branch - // 2. Create local branch to match name of remote branch with hash matching remote branch - // More info: see https://git-scm.com/docs/git-checkout section `git checkout []` - branchName := branchRef.Name().Short() - log.Printf("Creating branch %s to track remote branch %s from %s", branchName, branchName, remote) +func checkoutRemoteBranch(projectPath, branchName, remote string) error { + log.Printf("Checking out remote branch %s", branchName) if err := shell.GitCheckoutBranch(projectPath, branchName, remote); err != nil { return fmt.Errorf("failed to checkout branch %s: %s", branchName, err) } - return nil } -func checkoutTag(projectPath, remote string, tagRef *plumbing.Reference) error { - tagName := tagRef.Name().Short() - log.Printf("Checking out tag %s from remote %s", tagName, remote) +func checkoutTag(projectPath, tagName string) error { + log.Printf("Checking out tag %s", tagName) if err := shell.GitCheckoutRef(projectPath, tagName); err != nil { return fmt.Errorf("failed to checkout tag %s: %s", tagName, err) } - return nil } diff --git a/project-clone/internal/shell/execute.go b/project-clone/internal/shell/execute.go index 579f5989e..ce2d3feb0 100644 --- a/project-clone/internal/shell/execute.go +++ b/project-clone/internal/shell/execute.go @@ -22,6 +22,16 @@ import ( "os/exec" ) +type GitRefType int64 + +const ( + GitRefUnknown GitRefType = iota + GitRefLocalBranch + GitRefRemoteBranch + GitRefTag + GitRefHash +) + // GitCloneProject constructs a command-line string for cloning a git project, and delegates execution // to the os/exec package. func GitCloneProject(repoUrl, defaultRemoteName, destPath string) error { @@ -139,9 +149,46 @@ func GitSetTrackingRemoteBranch(projectPath, branchName, remote string) error { return executeCommand("git", "branch", "--set-upstream-to", fmt.Sprintf("%s/%s", remote, branchName), branchName) } +// GitResolveReference determines if the provided revision is a (local) branch, tag, or hash for use when preparing a +// cloned repository. This is done by using `git show-ref` for branches/tags and `git rev-parse` for checking whether +// a commit hash exists. If the reference type cannot be determined, GitRefUnknown is returned. +func GitResolveReference(projectPath, remote, revision string) (GitRefType, error) { + currDir, err := os.Getwd() + if err != nil { + return GitRefUnknown, fmt.Errorf("failed to get current working directory: %s", err) + } + defer func() { + if err := os.Chdir(currDir); err != nil { + log.Printf("failed to return to original working directory: %s", err) + } + }() + err = os.Chdir(projectPath) + if err != nil { + return GitRefUnknown, fmt.Errorf("failed to move to project directory %s: %s", projectPath, err) + } + if err := executeCommandSilent("git", "show-ref", "-q", "--verify", fmt.Sprintf("refs/heads/%s", revision)); err == nil { + return GitRefLocalBranch, nil + } + if err := executeCommandSilent("git", "show-ref", "-q", "--verify", fmt.Sprintf("refs/remotes/%s/%s", remote, revision)); err == nil { + return GitRefRemoteBranch, nil + } + if err := executeCommandSilent("git", "show-ref", "-q", "--verify", fmt.Sprintf("refs/tags/%s", revision)); err == nil { + return GitRefTag, nil + } + if err := executeCommandSilent("git", "rev-parse", "-q", "--verify", fmt.Sprintf("%s^{commit}", revision)); err == nil { + return GitRefHash, nil + } + return GitRefUnknown, nil +} + func executeCommand(name string, args ...string) error { cmd := exec.Command(name, args...) cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout return cmd.Run() } + +func executeCommandSilent(name string, args ...string) error { + cmd := exec.Command(name, args...) + return cmd.Run() +} From 25123f6af688c88236dedb80230398cc07b8c7d1 Mon Sep 17 00:00:00 2001 From: Angel Misevski Date: Thu, 1 Jun 2023 15:12:21 -0400 Subject: [PATCH 2/2] Fixup git clone sample to use existing branch for checkout Fix the git-clone-sample.yaml sample to reference an existing branch for checkout, as the previous branch has since been deleted. This uses a release branch (0.21.x) which should not be deleted in the future. Signed-off-by: Angel Misevski --- samples/git-clone-sample.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/git-clone-sample.yaml b/samples/git-clone-sample.yaml index 46af77b17..0e6241ef4 100644 --- a/samples/git-clone-sample.yaml +++ b/samples/git-clone-sample.yaml @@ -13,8 +13,8 @@ spec: - name: devworkspace-operator git: checkoutFrom: - remote: amisevsk - revision: clone-projects-on-start + remote: origin + revision: 0.21.x remotes: origin: "https://github.com/devfile/devworkspace-operator.git" amisevsk: "https://github.com/amisevsk/devworkspace-operator.git"