Skip to content

Commit d98a0ca

Browse files
rouke-broersmaPaulSonOfLarsblakepetterssonagaudreault
authored
chore(repo-server): unify semver resolution in new versions subpackage (#20216) (#23310)
Signed-off-by: Paul Larsen <[email protected]> Signed-off-by: Blake Pettersson <[email protected]> Signed-off-by: Alexandre Gaudreault <[email protected]> Co-authored-by: Paul Larsen <[email protected]> Co-authored-by: Blake Pettersson <[email protected]> Co-authored-by: Alexandre Gaudreault <[email protected]>
1 parent 59d4519 commit d98a0ca

File tree

15 files changed

+226
-290
lines changed

15 files changed

+226
-290
lines changed

reposerver/repository/repository.go

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
"strings"
1717
"time"
1818

19-
"github.com/Masterminds/semver/v3"
2019
"github.com/TomOnTime/utfutil"
2120
"github.com/argoproj/gitops-engine/pkg/utils/kube"
2221
textutils "github.com/argoproj/gitops-engine/pkg/utils/text"
@@ -60,6 +59,7 @@ import (
6059
"github.com/argoproj/argo-cd/v3/util/kustomize"
6160
"github.com/argoproj/argo-cd/v3/util/manifeststream"
6261
"github.com/argoproj/argo-cd/v3/util/text"
62+
"github.com/argoproj/argo-cd/v3/util/versions"
6363
)
6464

6565
const (
@@ -2403,40 +2403,37 @@ func (s *Service) newClientResolveRevision(repo *v1alpha1.Repository, revision s
24032403
func (s *Service) newHelmClientResolveRevision(repo *v1alpha1.Repository, revision string, chart string, noRevisionCache bool) (helm.Client, string, error) {
24042404
enableOCI := repo.EnableOCI || helm.IsHelmOciRepo(repo.Repo)
24052405
helmClient := s.newHelmClient(repo.Repo, repo.GetHelmCreds(), enableOCI, repo.Proxy, repo.NoProxy, helm.WithIndexCache(s.cache), helm.WithChartPaths(s.chartPaths))
2406-
if helm.IsVersion(revision) {
2406+
2407+
// Note: This check runs the risk of returning a version which is not found in the helm registry.
2408+
if versions.IsVersion(revision) {
24072409
return helmClient, revision, nil
24082410
}
2409-
constraints, err := semver.NewConstraint(revision)
2410-
if err != nil {
2411-
return nil, "", fmt.Errorf("invalid revision '%s': %w", revision, err)
2412-
}
24132411

2412+
var tags []string
24142413
if enableOCI {
2415-
tags, err := helmClient.GetTags(chart, noRevisionCache)
2414+
var err error
2415+
tags, err = helmClient.GetTags(chart, noRevisionCache)
24162416
if err != nil {
24172417
return nil, "", fmt.Errorf("unable to get tags: %w", err)
24182418
}
2419-
2420-
version, err := tags.MaxVersion(constraints)
2419+
} else {
2420+
index, err := helmClient.GetIndex(noRevisionCache, s.initConstants.HelmRegistryMaxIndexSize)
24212421
if err != nil {
2422-
return nil, "", fmt.Errorf("no version for constraints: %w", err)
2422+
return nil, "", err
24232423
}
2424-
return helmClient, version.String(), nil
2424+
entries, err := index.GetEntries(chart)
2425+
if err != nil {
2426+
return nil, "", err
2427+
}
2428+
tags = entries.Tags()
24252429
}
24262430

2427-
index, err := helmClient.GetIndex(noRevisionCache, s.initConstants.HelmRegistryMaxIndexSize)
2431+
maxV, err := versions.MaxVersion(revision, tags)
24282432
if err != nil {
2429-
return nil, "", err
2433+
return nil, "", fmt.Errorf("invalid revision: %w", err)
24302434
}
2431-
entries, err := index.GetEntries(chart)
2432-
if err != nil {
2433-
return nil, "", err
2434-
}
2435-
version, err := entries.MaxVersion(constraints)
2436-
if err != nil {
2437-
return nil, "", err
2438-
}
2439-
return helmClient, version.String(), nil
2435+
2436+
return helmClient, maxV, nil
24402437
}
24412438

24422439
// directoryPermissionInitializer ensures the directory has read/write/execute permissions and returns
@@ -2564,13 +2561,10 @@ func (s *Service) GetHelmCharts(_ context.Context, q *apiclient.HelmChartsReques
25642561
}
25652562
res := apiclient.HelmChartsResponse{}
25662563
for chartName, entries := range index.Entries {
2567-
chart := apiclient.HelmChart{
2568-
Name: chartName,
2569-
}
2570-
for _, entry := range entries {
2571-
chart.Versions = append(chart.Versions, entry.Version)
2572-
}
2573-
res.Items = append(res.Items, &chart)
2564+
res.Items = append(res.Items, &apiclient.HelmChart{
2565+
Name: chartName,
2566+
Versions: entries.Tags(),
2567+
})
25742568
}
25752569
return &res, nil
25762570
}

reposerver/repository/repository_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1828,11 +1828,11 @@ func TestService_newHelmClientResolveRevision(t *testing.T) {
18281828

18291829
t.Run("EmptyRevision", func(t *testing.T) {
18301830
_, _, err := service.newHelmClientResolveRevision(&v1alpha1.Repository{}, "", "my-chart", true)
1831-
assert.EqualError(t, err, "invalid revision '': improper constraint: ")
1831+
assert.EqualError(t, err, "invalid revision: failed to determine semver constraint: improper constraint: ")
18321832
})
18331833
t.Run("InvalidRevision", func(t *testing.T) {
18341834
_, _, err := service.newHelmClientResolveRevision(&v1alpha1.Repository{}, "???", "my-chart", true)
1835-
assert.EqualError(t, err, "invalid revision '???': improper constraint: ???")
1835+
assert.EqualError(t, err, "invalid revision: failed to determine semver constraint: improper constraint: ???")
18361836
})
18371837
}
18381838

util/git/client.go

Lines changed: 14 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ import (
1717
"syscall"
1818
"time"
1919

20-
"github.com/Masterminds/semver/v3"
21-
2220
argoexec "github.com/argoproj/pkg/exec"
2321
"github.com/bmatcuk/doublestar/v4"
2422
"github.com/go-git/go-git/v5"
@@ -39,6 +37,7 @@ import (
3937
"github.com/argoproj/argo-cd/v3/util/env"
4038
executil "github.com/argoproj/argo-cd/v3/util/exec"
4139
"github.com/argoproj/argo-cd/v3/util/proxy"
40+
"github.com/argoproj/argo-cd/v3/util/versions"
4241
)
4342

4443
var ErrInvalidRepoURL = errors.New("repo URL is invalid")
@@ -645,6 +644,16 @@ func (m *nativeGitClient) LsRemote(revision string) (res string, err error) {
645644
return
646645
}
647646

647+
func getGitTags(refs []*plumbing.Reference) []string {
648+
var tags []string
649+
for _, ref := range refs {
650+
if ref.Name().IsTag() {
651+
tags = append(tags, ref.Name().Short())
652+
}
653+
}
654+
return tags
655+
}
656+
648657
func (m *nativeGitClient) lsRemote(revision string) (string, error) {
649658
if IsCommitSHA(revision) {
650659
return revision, nil
@@ -659,9 +668,9 @@ func (m *nativeGitClient) lsRemote(revision string) (string, error) {
659668
revision = "HEAD"
660669
}
661670

662-
semverSha := m.resolveSemverRevision(revision, refs)
663-
if semverSha != "" {
664-
return semverSha, nil
671+
maxV, err := versions.MaxVersion(revision, getGitTags(refs))
672+
if err == nil {
673+
revision = maxV
665674
}
666675

667676
// refToHash keeps a maps of remote refs to their hash
@@ -710,59 +719,6 @@ func (m *nativeGitClient) lsRemote(revision string) (string, error) {
710719
return "", fmt.Errorf("unable to resolve '%s' to a commit SHA", revision)
711720
}
712721

713-
// resolveSemverRevision is a part of the lsRemote method workflow.
714-
// When the user correctly configures the Git repository revision, and that revision is a valid semver constraint, we
715-
// use this logic path rather than the standard lsRemote revision resolution loop.
716-
// Some examples to illustrate the actual behavior - if the revision is:
717-
// * "v0.1.2"/"0.1.2" or "v0.1"/"0.1", then this is not a constraint, it's a pinned version - so we fall back to the standard tag matching in the lsRemote loop.
718-
// * "v0.1.*"/"0.1.*", and there's a tag matching that constraint, then we find the latest matching version and return its commit hash.
719-
// * "v0.1.*"/"0.1.*", and there is *no* tag matching that constraint, then we fall back to the standard tag matching in the lsRemote loop.
720-
// * "custom-tag", only the lsRemote loop will run - because that revision is an invalid semver;
721-
// * "master-branch", only the lsRemote loop will run because that revision is an invalid semver;
722-
func (m *nativeGitClient) resolveSemverRevision(revision string, refs []*plumbing.Reference) string {
723-
if _, err := semver.NewVersion(revision); err == nil {
724-
// If the revision is a valid version, then we know it isn't a constraint; it's just a pin.
725-
// In which case, we should use standard tag resolution mechanisms.
726-
return ""
727-
}
728-
729-
constraint, err := semver.NewConstraint(revision)
730-
if err != nil {
731-
log.Debugf("Revision '%s' is not a valid semver constraint, skipping semver resolution.", revision)
732-
return ""
733-
}
734-
735-
maxVersion := semver.New(0, 0, 0, "", "")
736-
maxVersionHash := plumbing.ZeroHash
737-
for _, ref := range refs {
738-
if !ref.Name().IsTag() {
739-
continue
740-
}
741-
742-
tag := ref.Name().Short()
743-
version, err := semver.NewVersion(tag)
744-
if err != nil {
745-
log.Debugf("Error parsing version for tag: '%s': %v", tag, err)
746-
// Skip this tag and continue to the next one
747-
continue
748-
}
749-
750-
if constraint.Check(version) {
751-
if version.GreaterThan(maxVersion) {
752-
maxVersion = version
753-
maxVersionHash = ref.Hash()
754-
}
755-
}
756-
}
757-
758-
if maxVersionHash.IsZero() {
759-
return ""
760-
}
761-
762-
log.Debugf("Semver constraint '%s' resolved to tag '%s', at reference '%s'", revision, maxVersion.Original(), maxVersionHash.String())
763-
return maxVersionHash.String()
764-
}
765-
766722
// CommitSHA returns current commit sha from `git rev-parse HEAD`
767723
func (m *nativeGitClient) CommitSHA() (string, error) {
768724
out, err := m.runCmd("rev-parse", "HEAD")

util/git/client_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ func Test_SemverTags(t *testing.T) {
315315
// However, if one specifies the minor/patch versions, semver constraints can be used to match non-semver tags.
316316
// 2024-banana is considered as "2024.0.0-banana" in semver-ish, and banana > apple, so it's a match.
317317
// Note: this is more for documentation and future reference than real testing, as it seems like quite odd behaviour.
318-
name: "semver constraints on non-semver tags",
318+
name: "semver constraints on semver tags",
319319
ref: "> 2024.0.0-apple",
320320
expected: mapTagRefs["2024-banana"],
321321
}} {

util/helm/client.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ type Client interface {
4949
CleanChartCache(chart string, version string) error
5050
ExtractChart(chart string, version string, passCredentials bool, manifestMaxExtractedSize int64, disableManifestMaxExtractedSize bool) (string, argoio.Closer, error)
5151
GetIndex(noCache bool, maxIndexSize int64) (*Index, error)
52-
GetTags(chart string, noCache bool) (*TagsList, error)
52+
GetTags(chart string, noCache bool) ([]string, error)
5353
TestHelmOCI() (bool, error)
5454
}
5555

@@ -416,7 +416,7 @@ func getIndexURL(rawURL string) (string, error) {
416416
return repoURL.String(), nil
417417
}
418418

419-
func (c *nativeHelmChart) GetTags(chart string, noCache bool) (*TagsList, error) {
419+
func (c *nativeHelmChart) GetTags(chart string, noCache bool) ([]string, error) {
420420
if !c.enableOci {
421421
return nil, OCINotEnabledErr
422422
}
@@ -432,7 +432,11 @@ func (c *nativeHelmChart) GetTags(chart string, noCache bool) (*TagsList, error)
432432
}
433433
}
434434

435-
tags := &TagsList{}
435+
type entriesStruct struct {
436+
Tags []string
437+
}
438+
439+
entries := &entriesStruct{}
436440
if len(data) == 0 {
437441
start := time.Now()
438442
repo, err := remote.NewRepository(tagsURL)
@@ -479,7 +483,7 @@ func (c *nativeHelmChart) GetTags(chart string, noCache bool) (*TagsList, error)
479483
for _, tag := range tagsResult {
480484
// By convention: Change underscore (_) back to plus (+) to get valid SemVer
481485
convertedTag := strings.ReplaceAll(tag, "_", "+")
482-
tags.Tags = append(tags.Tags, convertedTag)
486+
entries.Tags = append(entries.Tags, convertedTag)
483487
}
484488

485489
return nil
@@ -497,11 +501,11 @@ func (c *nativeHelmChart) GetTags(chart string, noCache bool) (*TagsList, error)
497501
}
498502
}
499503
} else {
500-
err := json.Unmarshal(data, tags)
504+
err := json.Unmarshal(data, entries)
501505
if err != nil {
502506
return nil, fmt.Errorf("failed to decode tags: %w", err)
503507
}
504508
}
505509

506-
return tags, nil
510+
return entries.Tags, nil
507511
}

util/helm/client_test.go

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ type fakeIndexCache struct {
2626
data []byte
2727
}
2828

29+
type fakeTagsList struct {
30+
Tags []string `json:"tags"`
31+
}
32+
2933
func (f *fakeIndexCache) SetHelmIndex(_ string, indexData []byte) error {
3034
f.data = indexData
3135
return nil
@@ -171,19 +175,23 @@ func TestGetTagsFromUrl(t *testing.T) {
171175
t.Run("should return tags correctly while following the link header", func(t *testing.T) {
172176
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
173177
t.Logf("called %s", r.URL.Path)
174-
responseTags := TagsList{}
178+
var responseTags fakeTagsList
175179
w.Header().Set("Content-Type", "application/json")
176180
if !strings.Contains(r.URL.String(), "token") {
177181
w.Header().Set("Link", fmt.Sprintf("<https://%s%s?token=next-token>; rel=next", r.Host, r.URL.Path))
178-
responseTags.Tags = []string{"first"}
182+
responseTags = fakeTagsList{
183+
Tags: []string{"first"},
184+
}
179185
} else {
180-
responseTags.Tags = []string{
181-
"second",
182-
"2.8.0",
183-
"2.8.0-prerelease",
184-
"2.8.0_build",
185-
"2.8.0-prerelease_build",
186-
"2.8.0-prerelease.1_build.1234",
186+
responseTags = fakeTagsList{
187+
Tags: []string{
188+
"second",
189+
"2.8.0",
190+
"2.8.0-prerelease",
191+
"2.8.0_build",
192+
"2.8.0-prerelease_build",
193+
"2.8.0-prerelease.1_build.1234",
194+
},
187195
}
188196
}
189197
w.WriteHeader(http.StatusOK)
@@ -194,7 +202,7 @@ func TestGetTagsFromUrl(t *testing.T) {
194202

195203
tags, err := client.GetTags("mychart", true)
196204
require.NoError(t, err)
197-
assert.ElementsMatch(t, tags.Tags, []string{
205+
assert.ElementsMatch(t, tags, []string{
198206
"first",
199207
"second",
200208
"2.8.0",
@@ -230,7 +238,7 @@ func TestGetTagsFromURLPrivateRepoAuthentication(t *testing.T) {
230238

231239
assert.Equal(t, expectedAuthorization, authorization)
232240

233-
responseTags := TagsList{
241+
responseTags := fakeTagsList{
234242
Tags: []string{
235243
"2.8.0",
236244
"2.8.0-prerelease",
@@ -282,7 +290,7 @@ func TestGetTagsFromURLPrivateRepoAuthentication(t *testing.T) {
282290
tags, err := client.GetTags("mychart", true)
283291

284292
require.NoError(t, err)
285-
assert.ElementsMatch(t, tags.Tags, []string{
293+
assert.ElementsMatch(t, tags, []string{
286294
"2.8.0",
287295
"2.8.0-prerelease",
288296
"2.8.0+build",
@@ -327,7 +335,7 @@ func TestGetTagsFromURLPrivateRepoWithAzureWorkloadIdentityAuthentication(t *tes
327335

328336
assert.Equal(t, expectedAuthorization, authorization)
329337

330-
responseTags := TagsList{
338+
responseTags := fakeTagsList{
331339
Tags: []string{
332340
"2.8.0",
333341
"2.8.0-prerelease",
@@ -380,7 +388,7 @@ func TestGetTagsFromURLPrivateRepoWithAzureWorkloadIdentityAuthentication(t *tes
380388
tags, err := client.GetTags("mychart", true)
381389

382390
require.NoError(t, err)
383-
assert.ElementsMatch(t, tags.Tags, []string{
391+
assert.ElementsMatch(t, tags, []string{
384392
"2.8.0",
385393
"2.8.0-prerelease",
386394
"2.8.0+build",
@@ -406,7 +414,7 @@ func TestGetTagsFromURLEnvironmentAuthentication(t *testing.T) {
406414

407415
assert.Equal(t, expectedAuthorization, authorization)
408416

409-
responseTags := TagsList{
417+
responseTags := fakeTagsList{
410418
Tags: []string{
411419
"2.8.0",
412420
"2.8.0-prerelease",
@@ -463,7 +471,7 @@ func TestGetTagsFromURLEnvironmentAuthentication(t *testing.T) {
463471
tags, err := client.GetTags("mychart", true)
464472

465473
require.NoError(t, err)
466-
assert.ElementsMatch(t, tags.Tags, []string{
474+
assert.ElementsMatch(t, tags, []string{
467475
"2.8.0",
468476
"2.8.0-prerelease",
469477
"2.8.0+build",

0 commit comments

Comments
 (0)