Skip to content

Commit 0538b2b

Browse files
committed
fix: handle annotated git tags correctly in repo server cache (argoproj#21771)
Signed-off-by: Atif Ali <[email protected]>
1 parent 44c36b8 commit 0538b2b

File tree

6 files changed

+225
-1
lines changed

6 files changed

+225
-1
lines changed

test/e2e/fixture/app/actions.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ func (a *Actions) AddTag(name string) *Actions {
7878
return a
7979
}
8080

81+
func (a *Actions) AddAnnotatedTag(name string, message string) *Actions {
82+
a.context.t.Helper()
83+
fixture.AddAnnotatedTag(a.context.t, name, message)
84+
return a
85+
}
86+
87+
func (a *Actions) AddTagWithForce(name string) *Actions {
88+
a.context.t.Helper()
89+
fixture.AddTagWithForce(a.context.t, name)
90+
return a
91+
}
92+
8193
func (a *Actions) RemoveSubmodule() *Actions {
8294
a.context.t.Helper()
8395
fixture.RemoveSubmodule()

test/e2e/fixture/fixture.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,25 @@ func AddTag(name string) {
861861
}
862862
}
863863

864+
func AddTagWithForce(t *testing.T, name string) {
865+
t.Helper()
866+
prevGnuPGHome := os.Getenv("GNUPGHOME")
867+
t.Setenv("GNUPGHOME", TmpDir+"/gpg")
868+
defer t.Setenv("GNUPGHOME", prevGnuPGHome)
869+
errors.NewHandler(t).FailOnErr(Run(repoDirectory(), "git", "tag", "-f", name))
870+
if IsRemote() {
871+
errors.NewHandler(t).FailOnErr(Run(repoDirectory(), "git", "push", "--tags", "-f", "origin", "master"))
872+
}
873+
}
874+
875+
func AddAnnotatedTag(t *testing.T, name string, message string) {
876+
t.Helper()
877+
errors.NewHandler(t).FailOnErr(Run(repoDirectory(), "git", "tag", "-f", "-a", name, "-m", message))
878+
if IsRemote() {
879+
errors.NewHandler(t).FailOnErr(Run(repoDirectory(), "git", "push", "--tags", "-f", "origin", "master"))
880+
}
881+
}
882+
864883
// create the resource by creating using "kubectl apply", with bonus templating
865884
func Declarative(filename string, values interface{}) (string, error) {
866885
bytes, err := os.ReadFile(path.Join("testdata", filename))

test/e2e/git_test.go

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
package e2e
22

33
import (
4+
"context"
45
"strings"
56
"testing"
7+
"time"
68

79
v1 "k8s.io/api/core/v1"
10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
"k8s.io/apimachinery/pkg/types"
12+
"k8s.io/apimachinery/pkg/util/wait"
813

9-
"github.com/argoproj/argo-cd/v2/test/e2e/fixture"
14+
"github.com/stretchr/testify/require"
1015

1116
. "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
17+
"github.com/argoproj/argo-cd/v2/test/e2e/fixture"
1218
. "github.com/argoproj/argo-cd/v2/test/e2e/fixture/app"
19+
"github.com/argoproj/argo-cd/v2/util/errors"
1320
)
1421

1522
func TestGitSemverResolutionNotUsingConstraint(t *testing.T) {
@@ -51,3 +58,145 @@ func TestGitSemverResolutionUsingConstraint(t *testing.T) {
5158
Expect(SyncStatusIs(SyncStatusCodeSynced)).
5259
Expect(Pod(func(p v1.Pod) bool { return strings.HasPrefix(p.Name, "new-app") }))
5360
}
61+
62+
func TestAnnotatedTagInStatusSyncRevision(t *testing.T) {
63+
Given(t).
64+
Path(guestbookPath).
65+
When().
66+
// Create annotated tag name 'annotated-tag'
67+
AddAnnotatedTag("annotated-tag", "my-generic-tag-message").
68+
// Create Application targeting annotated-tag, with automatedSync: true
69+
CreateFromFile(func(app *Application) {
70+
app.Spec.Source.TargetRevision = "annotated-tag"
71+
app.Spec.SyncPolicy = &SyncPolicy{Automated: &SyncPolicyAutomated{Prune: true, SelfHeal: false}}
72+
}).
73+
Then().
74+
Expect(SyncStatusIs(SyncStatusCodeSynced)).
75+
And(func(app *Application) {
76+
annotatedTagIDOutput, err := fixture.Run(fixture.TmpDir+"/testdata.git", "git", "show-ref", "annotated-tag")
77+
require.NoError(t, err)
78+
require.NotEmpty(t, annotatedTagIDOutput)
79+
// example command output:
80+
// "569798c430515ffe170bdb23e3aafaf8ae24b9ff refs/tags/annotated-tag"
81+
annotatedTagIDFields := strings.Fields(string(annotatedTagIDOutput))
82+
require.Len(t, annotatedTagIDFields, 2)
83+
84+
targetCommitID, err := fixture.Run(fixture.TmpDir+"/testdata.git", "git", "rev-parse", "--verify", "annotated-tag^{commit}")
85+
// example command output:
86+
// "bcd35965e494273355265b9f0bf85075b6bc5163"
87+
require.NoError(t, err)
88+
require.NotEmpty(t, targetCommitID)
89+
90+
require.NotEmpty(t, app.Status.Sync.Revision, "revision in sync status should be set by sync operation")
91+
92+
require.NotEqual(t, app.Status.Sync.Revision, annotatedTagIDFields[0], "revision should not match the annotated tag id")
93+
require.Equal(t, app.Status.Sync.Revision, strings.TrimSpace(string(targetCommitID)), "revision SHOULD match the target commit SHA")
94+
})
95+
}
96+
97+
// Test updates to K8s resources should not trigger a self-heal when self-heal is false.
98+
func TestAutomatedSelfHealingAgainstAnnotatedTag(t *testing.T) {
99+
Given(t).
100+
Path(guestbookPath).
101+
When().
102+
AddAnnotatedTag("annotated-tag", "my-generic-tag-message").
103+
// App should be auto-synced once created
104+
CreateFromFile(func(app *Application) {
105+
app.Spec.Source.TargetRevision = "annotated-tag"
106+
app.Spec.SyncPolicy = &SyncPolicy{Automated: &SyncPolicyAutomated{Prune: true, SelfHeal: false}}
107+
}).
108+
Then().
109+
Expect(SyncStatusIs(SyncStatusCodeSynced)).
110+
ExpectConsistently(SyncStatusIs(SyncStatusCodeSynced), WaitDuration, time.Second*10).
111+
When().
112+
// Update the annotated tag to a new git commit, that has a new revisionHistoryLimit.
113+
PatchFile("guestbook-ui-deployment.yaml", `[{"op": "replace", "path": "/spec/revisionHistoryLimit", "value": 10}]`).
114+
AddAnnotatedTag("annotated-tag", "my-generic-tag-message").
115+
Refresh(RefreshTypeHard).
116+
// The Application should update to the new annotated tag value within 10 seconds.
117+
And(func() {
118+
// Deployment revisionHistoryLimit should switch to 10
119+
timeoutErr := wait.PollUntilContextTimeout(t.Context(), 1*time.Second, 10*time.Second, true, func(context.Context) (done bool, err error) {
120+
deployment, err := fixture.KubeClientset.AppsV1().Deployments(fixture.DeploymentNamespace()).Get(t.Context(), "guestbook-ui", metav1.GetOptions{})
121+
if err != nil {
122+
return false, nil
123+
}
124+
125+
revisionHistoryLimit := deployment.Spec.RevisionHistoryLimit
126+
return revisionHistoryLimit != nil && *revisionHistoryLimit == 10, nil
127+
})
128+
require.NoError(t, timeoutErr)
129+
}).
130+
// Update the Deployment to a different revisionHistoryLimit
131+
And(func() {
132+
errors.NewHandler(t).FailOnErr(fixture.KubeClientset.AppsV1().Deployments(fixture.DeploymentNamespace()).Patch(t.Context(),
133+
"guestbook-ui", types.MergePatchType, []byte(`{"spec": {"revisionHistoryLimit": 9}}`), metav1.PatchOptions{}))
134+
}).
135+
// The revisionHistoryLimit should NOT be self-healed, because selfHealing: false. It should remain at 9.
136+
And(func() {
137+
// Wait up to 10 seconds to ensure that deployment revisionHistoryLimit does NOT should switch to 10, it should remain at 9.
138+
waitErr := wait.PollUntilContextTimeout(t.Context(), 1*time.Second, 10*time.Second, true, func(context.Context) (done bool, err error) {
139+
deployment, err := fixture.KubeClientset.AppsV1().Deployments(fixture.DeploymentNamespace()).Get(t.Context(), "guestbook-ui", metav1.GetOptions{})
140+
if err != nil {
141+
return false, nil
142+
}
143+
144+
revisionHistoryLimit := deployment.Spec.RevisionHistoryLimit
145+
return revisionHistoryLimit != nil && *revisionHistoryLimit != 9, nil
146+
})
147+
require.Error(t, waitErr, "A timeout error should occur, indicating that revisionHistoryLimit never changed from 9")
148+
})
149+
}
150+
151+
func TestAutomatedSelfHealingAgainstLightweightTag(t *testing.T) {
152+
Given(t).
153+
Path(guestbookPath).
154+
When().
155+
AddTag("annotated-tag").
156+
// App should be auto-synced once created
157+
CreateFromFile(func(app *Application) {
158+
app.Spec.Source.TargetRevision = "annotated-tag"
159+
app.Spec.SyncPolicy = &SyncPolicy{Automated: &SyncPolicyAutomated{Prune: true, SelfHeal: false}}
160+
}).
161+
Then().
162+
Expect(SyncStatusIs(SyncStatusCodeSynced)).
163+
ExpectConsistently(SyncStatusIs(SyncStatusCodeSynced), WaitDuration, time.Second*10).
164+
When().
165+
// Update the annotated tag to a new git commit, that has a new revisionHistoryLimit.
166+
PatchFile("guestbook-ui-deployment.yaml", `[{"op": "replace", "path": "/spec/revisionHistoryLimit", "value": 10}]`).
167+
AddTagWithForce("annotated-tag").
168+
Refresh(RefreshTypeHard).
169+
// The Application should update to the new annotated tag value within 10 seconds.
170+
And(func() {
171+
// Deployment revisionHistoryLimit should switch to 10
172+
timeoutErr := wait.PollUntilContextTimeout(t.Context(), 1*time.Second, 10*time.Second, true, func(context.Context) (done bool, err error) {
173+
deployment, err := fixture.KubeClientset.AppsV1().Deployments(fixture.DeploymentNamespace()).Get(t.Context(), "guestbook-ui", metav1.GetOptions{})
174+
if err != nil {
175+
return false, nil
176+
}
177+
178+
revisionHistoryLimit := deployment.Spec.RevisionHistoryLimit
179+
return revisionHistoryLimit != nil && *revisionHistoryLimit == 10, nil
180+
})
181+
require.NoError(t, timeoutErr)
182+
}).
183+
// Update the Deployment to a different revisionHistoryLimit
184+
And(func() {
185+
errors.NewHandler(t).FailOnErr(fixture.KubeClientset.AppsV1().Deployments(fixture.DeploymentNamespace()).Patch(t.Context(),
186+
"guestbook-ui", types.MergePatchType, []byte(`{"spec": {"revisionHistoryLimit": 9}}`), metav1.PatchOptions{}))
187+
}).
188+
// The revisionHistoryLimit should NOT be self-healed, because selfHealing: false
189+
And(func() {
190+
// Wait up to 10 seconds to ensure that deployment revisionHistoryLimit does NOT should switch to 10, it should remain at 9.
191+
waitErr := wait.PollUntilContextTimeout(t.Context(), 1*time.Second, 10*time.Second, true, func(context.Context) (done bool, err error) {
192+
deployment, err := fixture.KubeClientset.AppsV1().Deployments(fixture.DeploymentNamespace()).Get(t.Context(), "guestbook-ui", metav1.GetOptions{})
193+
if err != nil {
194+
return false, nil
195+
}
196+
197+
revisionHistoryLimit := deployment.Spec.RevisionHistoryLimit
198+
return revisionHistoryLimit != nil && *revisionHistoryLimit != 9, nil
199+
})
200+
require.Error(t, waitErr, "A timeout error should occur, indicating that revisionHistoryLimit never changed from 9")
201+
})
202+
}

util/git/client_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"path/filepath"
99
"testing"
1010

11+
"github.com/go-git/go-git/v5/plumbing"
1112
"github.com/stretchr/testify/assert"
1213
"github.com/stretchr/testify/require"
1314
)
@@ -118,6 +119,19 @@ func Test_IsAnnotatedTag(t *testing.T) {
118119
assert.False(t, atag)
119120
}
120121

122+
func Test_resolveTagReference(t *testing.T) {
123+
// Setup
124+
commitHash := plumbing.NewHash("0123456789abcdef0123456789abcdef01234567")
125+
tagRef := plumbing.NewReferenceFromStrings("refs/tags/v1.0.0", "sometaghash")
126+
127+
// Test single function
128+
resolvedRef := plumbing.NewHashReference(tagRef.Name(), commitHash)
129+
130+
// Verify
131+
assert.Equal(t, commitHash, resolvedRef.Hash())
132+
assert.Equal(t, tagRef.Name(), resolvedRef.Name())
133+
}
134+
121135
func Test_ChangedFiles(t *testing.T) {
122136
tempDir := t.TempDir()
123137

util/git/git_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,3 +501,27 @@ func TestLsFiles(t *testing.T) {
501501
require.NoError(t, err)
502502
assert.Equal(t, nilResult, lsResult)
503503
}
504+
505+
func TestAnnotatedTagHandling(t *testing.T) {
506+
dir := t.TempDir()
507+
508+
client, err := NewClientExt("https://github.com/argoproj/argo-cd.git", dir, NopCreds{}, false, false, "", "")
509+
require.NoError(t, err)
510+
511+
err = client.Init()
512+
require.NoError(t, err)
513+
514+
// Test annotated tag resolution
515+
commitSHA, err := client.LsRemote("v1.0.0") // Known annotated tag
516+
require.NoError(t, err)
517+
518+
// Verify we get commit SHA, not tag SHA
519+
assert.True(t, IsCommitSHA(commitSHA))
520+
521+
// Test tag reference handling
522+
refs, err := client.LsRefs()
523+
require.NoError(t, err)
524+
525+
// Verify tag exists in the list and points to a valid commit SHA
526+
assert.Contains(t, refs.Tags, "v1.0.0", "Tag v1.0.0 should exist in refs")
527+
}

util/git/workaround.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ func listRemote(r *git.Remote, o *git.ListOptions, insecure bool, creds Creds, p
8585

8686
var resultRefs []*plumbing.Reference
8787
_ = refs.ForEach(func(ref *plumbing.Reference) error {
88+
if ref.Name().IsTag() {
89+
if peeled, ok := ar.Peeled[ref.Name().String()]; ok {
90+
resultRefs = append(resultRefs, plumbing.NewHashReference(ref.Name(), peeled))
91+
return nil
92+
}
93+
}
8894
resultRefs = append(resultRefs, ref)
8995
return nil
9096
})

0 commit comments

Comments
 (0)