Skip to content

Commit 5381de4

Browse files
authored
chore: improve missing branch error (#587) (#588)
* chore: improve missing branch error (#587) Signed-off-by: Michael Crenshaw <[email protected]> * fix lint Signed-off-by: Michael Crenshaw <[email protected]> * make test actually test things Signed-off-by: Michael Crenshaw <[email protected]> --------- Signed-off-by: Michael Crenshaw <[email protected]>
1 parent f1ca27f commit 5381de4

File tree

3 files changed

+150
-36
lines changed

3 files changed

+150
-36
lines changed

internal/controller/changetransferpolicy_controller.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,11 @@ func (r *ChangeTransferPolicyReconciler) Reconcile(ctx context.Context, req ctrl
109109
if err != nil {
110110
return ctrl.Result{}, fmt.Errorf("failed to get git auth provider for ScmProvider %q: %w", scmProvider.GetName(), err)
111111
}
112-
gitOperations, err := git.NewEnvironmentOperations(ctx, r.Client, gitAuthProvider, ctp.Spec.RepositoryReference, &ctp, ctp.Spec.ActiveBranch)
112+
gitRepo, err := utils.GetGitRepositoryFromObjectKey(ctx, r.Client, client.ObjectKey{Namespace: ctp.GetNamespace(), Name: ctp.Spec.RepositoryReference.Name})
113113
if err != nil {
114-
return ctrl.Result{}, fmt.Errorf("failed to initialize git client: %w", err)
114+
return ctrl.Result{}, fmt.Errorf("failed to get GitRepository: %w", err)
115115
}
116+
gitOperations := git.NewEnvironmentOperations(gitRepo, gitAuthProvider, ctp.Spec.ActiveBranch)
116117

117118
// TODO: could probably short circuit the clone and use an ls-remote to compare the sha's of the current ctp status,
118119
// this would help with slamming the git provider with clone requests on controller restarts.

internal/git/git.go

Lines changed: 20 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"context"
1414
_ "embed"
1515
"encoding/json"
16-
"errors"
1716
"fmt"
1817
"os"
1918
"os/exec"
@@ -24,13 +23,11 @@ import (
2423
"github.com/argoproj-labs/gitops-promoter/internal/types/constants"
2524
"github.com/relvacode/iso8601"
2625
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27-
"sigs.k8s.io/controller-runtime/pkg/client"
2826
"sigs.k8s.io/controller-runtime/pkg/log"
2927

3028
"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
3129
"github.com/argoproj-labs/gitops-promoter/internal/metrics"
3230
"github.com/argoproj-labs/gitops-promoter/internal/scms"
33-
"github.com/argoproj-labs/gitops-promoter/internal/utils"
3431
"github.com/argoproj-labs/gitops-promoter/internal/utils/gitpaths"
3532
)
3633

@@ -64,19 +61,12 @@ type HydratorMetadata struct {
6461
// NewEnvironmentOperations creates a new EnvironmentOperations instance. The activeBranch parameter is used to differentiate
6562
// between different environments that might use the same GitRepository and avoid conflicts between concurrent
6663
// operations.
67-
func NewEnvironmentOperations(ctx context.Context, k8sClient client.Client, gap scms.GitOperationsProvider, repoRef v1alpha1.ObjectReference, obj v1.Object, activeBranch string) (*EnvironmentOperations, error) {
68-
gitRepo, err := utils.GetGitRepositoryFromObjectKey(ctx, k8sClient, client.ObjectKey{Namespace: obj.GetNamespace(), Name: repoRef.Name})
69-
if err != nil {
70-
return nil, fmt.Errorf("failed to get GitRepository: %w", err)
71-
}
72-
73-
gitOperations := EnvironmentOperations{
64+
func NewEnvironmentOperations(gitRepo *v1alpha1.GitRepository, gap scms.GitOperationsProvider, activeBranch string) *EnvironmentOperations {
65+
return &EnvironmentOperations{
7466
gap: gap,
7567
gitRepo: gitRepo,
7668
activeBranch: activeBranch,
7769
}
78-
79-
return &gitOperations, nil
8070
}
8171

8272
// CloneRepo clones the gitRepo to a temporary directory if needed. Does nothing if the repo is already cloned.
@@ -143,46 +133,42 @@ func (g *EnvironmentOperations) GetBranchShas(ctx context.Context, branch string
143133
}
144134

145135
logger.V(4).Info("git path", "path", gitPath)
146-
_, stderr, err := g.runCmd(ctx, gitPath, "checkout", "--progress", "-B", branch, "origin/"+branch)
147-
if err != nil {
148-
logger.Error(err, "could not git checkout", "gitError", stderr)
149-
return BranchShas{}, err
150-
}
151-
logger.V(4).Info("Checked out branch", "branch", branch)
152136

137+
// Fetch the branch to ensure we have the latest remote ref
153138
start := time.Now()
154-
_, stderr, err = g.runCmd(ctx, gitPath, "pull", "--progress")
155-
metrics.RecordGitOperation(g.gitRepo, metrics.GitOperationPull, metrics.GitOperationResultFromError(err), time.Since(start))
139+
_, stderr, err := g.runCmd(ctx, gitPath, "fetch", "origin", branch)
140+
metrics.RecordGitOperation(g.gitRepo, metrics.GitOperationFetch, metrics.GitOperationResultFromError(err), time.Since(start))
156141
if err != nil {
157-
logger.Error(err, "could not git pull", "gitError", stderr)
158-
return BranchShas{}, err
142+
logger.Error(err, "could not fetch branch", "gitError", stderr)
143+
return BranchShas{}, fmt.Errorf("failed to fetch branch %q: %w", branch, err)
159144
}
160-
logger.V(4).Info("Pulled branch", "branch", branch)
145+
logger.V(4).Info("Fetched branch", "branch", branch)
161146

162-
stdout, stderr, err := g.runCmd(ctx, gitPath, "rev-parse", branch)
147+
// Get the SHA of the remote branch
148+
stdout, stderr, err := g.runCmd(ctx, gitPath, "rev-parse", "origin/"+branch)
163149
if err != nil {
164-
logger.Error(err, "could not get branch shas", "gitError", stderr)
165-
return BranchShas{}, err
150+
logger.Error(err, "could not get branch sha", "gitError", stderr)
151+
return BranchShas{}, fmt.Errorf("failed to get SHA for branch %q: %w", branch, err)
166152
}
167153

168154
shas := BranchShas{}
169155
shas.Hydrated = strings.TrimSpace(stdout)
170156
logger.V(4).Info("Got hydrated branch sha", "branch", branch, "sha", shas.Hydrated)
171157

172-
// TODO: safe path join
173-
metadataFile := gitPath + "/hydrator.metadata"
174-
jsonFile, err := os.Open(metadataFile)
158+
// Get the metadata file contents directly from the remote branch
159+
metadataFileStdout, stderr, err := g.runCmd(ctx, gitPath, "show", "origin/"+branch+":hydrator.metadata")
175160
if err != nil {
176-
if errors.Is(err, os.ErrNotExist) {
177-
logger.Info("hydrator.metadata file not found", "file", metadataFile)
161+
if strings.Contains(stderr, "does not exist") || strings.Contains(stderr, "Path not in") {
162+
logger.Info("hydrator.metadata file not found", "branch", branch)
178163
return shas, nil
179164
}
180-
return BranchShas{}, fmt.Errorf("could not open metadata file: %w", err)
165+
logger.Error(err, "could not get metadata file", "gitError", stderr)
166+
return BranchShas{}, fmt.Errorf("failed to read hydrator.metadata from branch %q: %w", branch, err)
181167
}
168+
logger.V(4).Info("Got metadata file", "branch", branch)
182169

183170
var hydratorFile HydratorMetadata
184-
decoder := json.NewDecoder(jsonFile)
185-
err = decoder.Decode(&hydratorFile)
171+
err = json.Unmarshal([]byte(metadataFileStdout), &hydratorFile)
186172
if err != nil {
187173
return BranchShas{}, fmt.Errorf("could not unmarshal metadata file: %w", err)
188174
}

internal/git/git_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package git_test
2+
3+
import (
4+
"context"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"strings"
9+
"testing"
10+
11+
. "github.com/onsi/ginkgo/v2"
12+
. "github.com/onsi/gomega"
13+
14+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15+
16+
"github.com/argoproj-labs/gitops-promoter/api/v1alpha1"
17+
"github.com/argoproj-labs/gitops-promoter/internal/git"
18+
)
19+
20+
func TestGit(t *testing.T) {
21+
t.Parallel()
22+
RegisterFailHandler(Fail)
23+
RunSpecs(t, "Git Suite")
24+
}
25+
26+
// Helper function to run git commands
27+
func runGitCmd(dir string, args ...string) (string, error) {
28+
cmd := exec.Command("git", args...)
29+
cmd.Dir = dir
30+
cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
31+
output, err := cmd.CombinedOutput()
32+
return string(output), err
33+
}
34+
35+
var _ = Describe("GetBranchShas", func() {
36+
var tempRepoDir string
37+
38+
BeforeEach(func() {
39+
// Create a temporary directory for the test repository
40+
var err error
41+
tempRepoDir, err = os.MkdirTemp("", "git-test-*")
42+
Expect(err).NotTo(HaveOccurred())
43+
})
44+
45+
AfterEach(func() {
46+
if tempRepoDir != "" {
47+
Expect(os.RemoveAll(tempRepoDir)).To(Succeed())
48+
}
49+
})
50+
51+
Context("When the branch does not exist on the remote", func() {
52+
It("should provide a clear error message from GetBranchShas", func() {
53+
By("Setting up a bare git repository")
54+
_, err := runGitCmd(tempRepoDir, "init", "--bare")
55+
Expect(err).NotTo(HaveOccurred())
56+
57+
By("Creating an initial commit")
58+
workDir, err := os.MkdirTemp("", "git-work-*")
59+
Expect(err).NotTo(HaveOccurred())
60+
defer func() {
61+
Expect(os.RemoveAll(workDir)).To(Succeed())
62+
}()
63+
64+
_, err = runGitCmd(workDir, "clone", tempRepoDir, ".")
65+
Expect(err).NotTo(HaveOccurred())
66+
67+
_, err = runGitCmd(workDir, "config", "user.name", "Test User")
68+
Expect(err).NotTo(HaveOccurred())
69+
_, err = runGitCmd(workDir, "config", "user.email", "[email protected]")
70+
Expect(err).NotTo(HaveOccurred())
71+
72+
err = os.WriteFile(filepath.Join(workDir, "hydrator.metadata"), []byte(`{"drySha": "abc123"}`), 0o644)
73+
Expect(err).NotTo(HaveOccurred())
74+
_, err = runGitCmd(workDir, "add", "hydrator.metadata")
75+
Expect(err).NotTo(HaveOccurred())
76+
_, err = runGitCmd(workDir, "commit", "-m", "Initial commit")
77+
Expect(err).NotTo(HaveOccurred())
78+
79+
defaultBranch, err := runGitCmd(workDir, "rev-parse", "--abbrev-ref", "HEAD")
80+
Expect(err).NotTo(HaveOccurred())
81+
defaultBranch = strings.TrimSpace(defaultBranch)
82+
83+
_, err = runGitCmd(workDir, "push", "origin", defaultBranch)
84+
Expect(err).NotTo(HaveOccurred())
85+
86+
// Prepare EnvironmentOperations
87+
repo := &v1alpha1.GitRepository{
88+
Spec: v1alpha1.GitRepositorySpec{
89+
GitHub: &v1alpha1.GitHubRepo{
90+
Owner: "test-owner",
91+
Name: "testrepo",
92+
},
93+
ScmProviderRef: v1alpha1.ScmProviderObjectReference{
94+
Kind: "ScmProvider",
95+
Name: "testprovider",
96+
},
97+
},
98+
ObjectMeta: metav1.ObjectMeta{
99+
Name: "testrepo",
100+
Namespace: "default",
101+
},
102+
}
103+
gap := &fakeGitProvider{tempDirPath: tempRepoDir}
104+
g := git.NewEnvironmentOperations(repo, gap, defaultBranch)
105+
Expect(g.CloneRepo(GinkgoT().Context())).To(Succeed())
106+
107+
// Call GetBranchShas with a non-existent branch
108+
_, err = g.GetBranchShas(GinkgoT().Context(), "environments/qal-usw2-eks-next")
109+
Expect(err).To(HaveOccurred())
110+
Expect(err.Error()).To(ContainSubstring("failed to fetch branch"))
111+
112+
// Having a missing branch is a common error, so we're ensuring the error message is clear.
113+
Expect(err.Error()).To(ContainSubstring("couldn't find remote ref"))
114+
})
115+
})
116+
})
117+
118+
type fakeGitProvider struct {
119+
tempDirPath string
120+
}
121+
122+
func (f *fakeGitProvider) GetGitHttpsRepoUrl(repo v1alpha1.GitRepository) string {
123+
// Return the local bare repo path for testing
124+
return f.tempDirPath
125+
}
126+
func (f *fakeGitProvider) GetUser(ctx context.Context) (string, error) { return "user", nil }
127+
func (f *fakeGitProvider) GetToken(ctx context.Context) (string, error) { return "token", nil }

0 commit comments

Comments
 (0)