mirror of
https://github.com/argoproj/argo-cd
synced 2026-04-21 17:07:16 +00:00
chore(repo-server): unify semver resolution in new versions subpackage (#20216)
Signed-off-by: Paul Larsen <pnvlarsen@gmail.com> Signed-off-by: Blake Pettersson <blake.pettersson@gmail.com> Signed-off-by: Alexandre Gaudreault <alexandre_gaudreault@intuit.com> Co-authored-by: Blake Pettersson <blake.pettersson@gmail.com> Co-authored-by: Alexandre Gaudreault <alexandre_gaudreault@intuit.com>
This commit is contained in:
parent
3f3ac06fd1
commit
6625d07859
15 changed files with 230 additions and 292 deletions
|
|
@ -16,7 +16,6 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/TomOnTime/utfutil"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
textutils "github.com/argoproj/gitops-engine/pkg/utils/text"
|
||||
|
|
@ -60,6 +59,7 @@ import (
|
|||
"github.com/argoproj/argo-cd/v3/util/kustomize"
|
||||
"github.com/argoproj/argo-cd/v3/util/manifeststream"
|
||||
"github.com/argoproj/argo-cd/v3/util/text"
|
||||
"github.com/argoproj/argo-cd/v3/util/versions"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -2404,40 +2404,37 @@ func (s *Service) newClientResolveRevision(repo *v1alpha1.Repository, revision s
|
|||
func (s *Service) newHelmClientResolveRevision(repo *v1alpha1.Repository, revision string, chart string, noRevisionCache bool) (helm.Client, string, error) {
|
||||
enableOCI := repo.EnableOCI || helm.IsHelmOciRepo(repo.Repo)
|
||||
helmClient := s.newHelmClient(repo.Repo, repo.GetHelmCreds(), enableOCI, repo.Proxy, repo.NoProxy, helm.WithIndexCache(s.cache), helm.WithChartPaths(s.chartPaths))
|
||||
if helm.IsVersion(revision) {
|
||||
|
||||
// Note: This check runs the risk of returning a version which is not found in the helm registry.
|
||||
if versions.IsVersion(revision) {
|
||||
return helmClient, revision, nil
|
||||
}
|
||||
constraints, err := semver.NewConstraint(revision)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("invalid revision '%s': %w", revision, err)
|
||||
}
|
||||
|
||||
var tags []string
|
||||
if enableOCI {
|
||||
tags, err := helmClient.GetTags(chart, noRevisionCache)
|
||||
var err error
|
||||
tags, err = helmClient.GetTags(chart, noRevisionCache)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("unable to get tags: %w", err)
|
||||
}
|
||||
|
||||
version, err := tags.MaxVersion(constraints)
|
||||
} else {
|
||||
index, err := helmClient.GetIndex(noRevisionCache, s.initConstants.HelmRegistryMaxIndexSize)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("no version for constraints: %w", err)
|
||||
return nil, "", err
|
||||
}
|
||||
return helmClient, version.String(), nil
|
||||
entries, err := index.GetEntries(chart)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
tags = entries.Tags()
|
||||
}
|
||||
|
||||
index, err := helmClient.GetIndex(noRevisionCache, s.initConstants.HelmRegistryMaxIndexSize)
|
||||
maxV, err := versions.MaxVersion(revision, tags)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, "", fmt.Errorf("invalid revision: %w", err)
|
||||
}
|
||||
entries, err := index.GetEntries(chart)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
version, err := entries.MaxVersion(constraints)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return helmClient, version.String(), nil
|
||||
|
||||
return helmClient, maxV, nil
|
||||
}
|
||||
|
||||
// directoryPermissionInitializer ensures the directory has read/write/execute permissions and returns
|
||||
|
|
@ -2565,13 +2562,10 @@ func (s *Service) GetHelmCharts(_ context.Context, q *apiclient.HelmChartsReques
|
|||
}
|
||||
res := apiclient.HelmChartsResponse{}
|
||||
for chartName, entries := range index.Entries {
|
||||
chart := apiclient.HelmChart{
|
||||
Name: chartName,
|
||||
}
|
||||
for _, entry := range entries {
|
||||
chart.Versions = append(chart.Versions, entry.Version)
|
||||
}
|
||||
res.Items = append(res.Items, &chart)
|
||||
res.Items = append(res.Items, &apiclient.HelmChart{
|
||||
Name: chartName,
|
||||
Versions: entries.Tags(),
|
||||
})
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ func newServiceWithMocks(t *testing.T, root string, signed bool) (*Service, *git
|
|||
chart: {{Version: "1.0.0"}, {Version: version}},
|
||||
oobChart: {{Version: "1.0.0"}, {Version: version}},
|
||||
}}, nil)
|
||||
helmClient.On("GetTags", mock.Anything, mock.Anything).Return(nil, nil)
|
||||
helmClient.On("ExtractChart", chart, version, false, int64(0), false).Return("./testdata/my-chart", io.NopCloser, nil)
|
||||
helmClient.On("ExtractChart", oobChart, version, false, int64(0), false).Return("./testdata2/out-of-bounds-chart", io.NopCloser, nil)
|
||||
helmClient.On("CleanChartCache", chart, version).Return(nil)
|
||||
|
|
@ -1826,12 +1827,12 @@ func TestService_newHelmClientResolveRevision(t *testing.T) {
|
|||
service := newService(t, ".")
|
||||
|
||||
t.Run("EmptyRevision", func(t *testing.T) {
|
||||
_, _, err := service.newHelmClientResolveRevision(&v1alpha1.Repository{}, "", "", true)
|
||||
assert.EqualError(t, err, "invalid revision '': improper constraint: ")
|
||||
_, _, err := service.newHelmClientResolveRevision(&v1alpha1.Repository{}, "", "my-chart", true)
|
||||
assert.EqualError(t, err, "invalid revision: failed to determine semver constraint: improper constraint: ")
|
||||
})
|
||||
t.Run("InvalidRevision", func(t *testing.T) {
|
||||
_, _, err := service.newHelmClientResolveRevision(&v1alpha1.Repository{}, "???", "", true)
|
||||
assert.EqualError(t, err, "invalid revision '???': improper constraint: ???", true)
|
||||
_, _, err := service.newHelmClientResolveRevision(&v1alpha1.Repository{}, "???", "my-chart", true)
|
||||
assert.EqualError(t, err, "invalid revision: failed to determine semver constraint: improper constraint: ???")
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -4101,7 +4102,7 @@ func TestGetRefs_CacheLockTryLockGitRefCacheError(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetRevisionChartDetails(t *testing.T) {
|
||||
t.Run("Test revision semvar", func(t *testing.T) {
|
||||
t.Run("Test revision semver", func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
service := newService(t, root)
|
||||
_, err := service.GetRevisionChartDetails(t.Context(), &apiclient.RepoServerRevisionChartDetailsRequest{
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/config"
|
||||
|
|
@ -37,6 +36,7 @@ import (
|
|||
"github.com/argoproj/argo-cd/v3/util/env"
|
||||
executil "github.com/argoproj/argo-cd/v3/util/exec"
|
||||
"github.com/argoproj/argo-cd/v3/util/proxy"
|
||||
"github.com/argoproj/argo-cd/v3/util/versions"
|
||||
)
|
||||
|
||||
var ErrInvalidRepoURL = errors.New("repo URL is invalid")
|
||||
|
|
@ -634,6 +634,16 @@ func (m *nativeGitClient) LsRemote(revision string) (res string, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func getGitTags(refs []*plumbing.Reference) []string {
|
||||
var tags []string
|
||||
for _, ref := range refs {
|
||||
if ref.Name().IsTag() {
|
||||
tags = append(tags, ref.Name().Short())
|
||||
}
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
func (m *nativeGitClient) lsRemote(revision string) (string, error) {
|
||||
if IsCommitSHA(revision) {
|
||||
return revision, nil
|
||||
|
|
@ -648,9 +658,9 @@ func (m *nativeGitClient) lsRemote(revision string) (string, error) {
|
|||
revision = "HEAD"
|
||||
}
|
||||
|
||||
semverSha := m.resolveSemverRevision(revision, refs)
|
||||
if semverSha != "" {
|
||||
return semverSha, nil
|
||||
maxV, err := versions.MaxVersion(revision, getGitTags(refs))
|
||||
if err == nil {
|
||||
revision = maxV
|
||||
}
|
||||
|
||||
// refToHash keeps a maps of remote refs to their hash
|
||||
|
|
@ -699,59 +709,6 @@ func (m *nativeGitClient) lsRemote(revision string) (string, error) {
|
|||
return "", fmt.Errorf("unable to resolve '%s' to a commit SHA", revision)
|
||||
}
|
||||
|
||||
// resolveSemverRevision is a part of the lsRemote method workflow.
|
||||
// When the user correctly configures the Git repository revision, and that revision is a valid semver constraint, we
|
||||
// use this logic path rather than the standard lsRemote revision resolution loop.
|
||||
// Some examples to illustrate the actual behavior - if the revision is:
|
||||
// * "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.
|
||||
// * "v0.1.*"/"0.1.*", and there's a tag matching that constraint, then we find the latest matching version and return its commit hash.
|
||||
// * "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.
|
||||
// * "custom-tag", only the lsRemote loop will run - because that revision is an invalid semver;
|
||||
// * "master-branch", only the lsRemote loop will run because that revision is an invalid semver;
|
||||
func (m *nativeGitClient) resolveSemverRevision(revision string, refs []*plumbing.Reference) string {
|
||||
if _, err := semver.NewVersion(revision); err == nil {
|
||||
// If the revision is a valid version, then we know it isn't a constraint; it's just a pin.
|
||||
// In which case, we should use standard tag resolution mechanisms.
|
||||
return ""
|
||||
}
|
||||
|
||||
constraint, err := semver.NewConstraint(revision)
|
||||
if err != nil {
|
||||
log.Debugf("Revision '%s' is not a valid semver constraint, skipping semver resolution.", revision)
|
||||
return ""
|
||||
}
|
||||
|
||||
maxVersion := semver.New(0, 0, 0, "", "")
|
||||
maxVersionHash := plumbing.ZeroHash
|
||||
for _, ref := range refs {
|
||||
if !ref.Name().IsTag() {
|
||||
continue
|
||||
}
|
||||
|
||||
tag := ref.Name().Short()
|
||||
version, err := semver.NewVersion(tag)
|
||||
if err != nil {
|
||||
log.Debugf("Error parsing version for tag: '%s': %v", tag, err)
|
||||
// Skip this tag and continue to the next one
|
||||
continue
|
||||
}
|
||||
|
||||
if constraint.Check(version) {
|
||||
if version.GreaterThan(maxVersion) {
|
||||
maxVersion = version
|
||||
maxVersionHash = ref.Hash()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if maxVersionHash.IsZero() {
|
||||
return ""
|
||||
}
|
||||
|
||||
log.Debugf("Semver constraint '%s' resolved to tag '%s', at reference '%s'", revision, maxVersion.Original(), maxVersionHash.String())
|
||||
return maxVersionHash.String()
|
||||
}
|
||||
|
||||
// CommitSHA returns current commit sha from `git rev-parse HEAD`
|
||||
func (m *nativeGitClient) CommitSHA() (string, error) {
|
||||
out, err := m.runCmd("rev-parse", "HEAD")
|
||||
|
|
|
|||
|
|
@ -327,7 +327,7 @@ func Test_SemverTags(t *testing.T) {
|
|||
// However, if one specifies the minor/patch versions, semver constraints can be used to match non-semver tags.
|
||||
// 2024-banana is considered as "2024.0.0-banana" in semver-ish, and banana > apple, so it's a match.
|
||||
// Note: this is more for documentation and future reference than real testing, as it seems like quite odd behaviour.
|
||||
name: "semver constraints on non-semver tags",
|
||||
name: "semver constraints on semver tags",
|
||||
ref: "> 2024.0.0-apple",
|
||||
expected: mapTagRefs["2024-banana"],
|
||||
}} {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ type Client interface {
|
|||
CleanChartCache(chart string, version string) error
|
||||
ExtractChart(chart string, version string, passCredentials bool, manifestMaxExtractedSize int64, disableManifestMaxExtractedSize bool) (string, argoio.Closer, error)
|
||||
GetIndex(noCache bool, maxIndexSize int64) (*Index, error)
|
||||
GetTags(chart string, noCache bool) (*TagsList, error)
|
||||
GetTags(chart string, noCache bool) ([]string, error)
|
||||
TestHelmOCI() (bool, error)
|
||||
}
|
||||
|
||||
|
|
@ -416,7 +416,7 @@ func getIndexURL(rawURL string) (string, error) {
|
|||
return repoURL.String(), nil
|
||||
}
|
||||
|
||||
func (c *nativeHelmChart) GetTags(chart string, noCache bool) (*TagsList, error) {
|
||||
func (c *nativeHelmChart) GetTags(chart string, noCache bool) ([]string, error) {
|
||||
if !c.enableOci {
|
||||
return nil, ErrOCINotEnabled
|
||||
}
|
||||
|
|
@ -432,7 +432,11 @@ func (c *nativeHelmChart) GetTags(chart string, noCache bool) (*TagsList, error)
|
|||
}
|
||||
}
|
||||
|
||||
tags := &TagsList{}
|
||||
type entriesStruct struct {
|
||||
Tags []string
|
||||
}
|
||||
|
||||
entries := &entriesStruct{}
|
||||
if len(data) == 0 {
|
||||
start := time.Now()
|
||||
repo, err := remote.NewRepository(tagsURL)
|
||||
|
|
@ -479,7 +483,7 @@ func (c *nativeHelmChart) GetTags(chart string, noCache bool) (*TagsList, error)
|
|||
for _, tag := range tagsResult {
|
||||
// By convention: Change underscore (_) back to plus (+) to get valid SemVer
|
||||
convertedTag := strings.ReplaceAll(tag, "_", "+")
|
||||
tags.Tags = append(tags.Tags, convertedTag)
|
||||
entries.Tags = append(entries.Tags, convertedTag)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -497,11 +501,11 @@ func (c *nativeHelmChart) GetTags(chart string, noCache bool) (*TagsList, error)
|
|||
}
|
||||
}
|
||||
} else {
|
||||
err := json.Unmarshal(data, tags)
|
||||
err := json.Unmarshal(data, entries)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode tags: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
return entries.Tags, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ type fakeIndexCache struct {
|
|||
data []byte
|
||||
}
|
||||
|
||||
type fakeTagsList struct {
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
func (f *fakeIndexCache) SetHelmIndex(_ string, indexData []byte) error {
|
||||
f.data = indexData
|
||||
return nil
|
||||
|
|
@ -170,19 +174,23 @@ func TestGetTagsFromUrl(t *testing.T) {
|
|||
t.Run("should return tags correctly while following the link header", func(t *testing.T) {
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("called %s", r.URL.Path)
|
||||
responseTags := TagsList{}
|
||||
var responseTags fakeTagsList
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if !strings.Contains(r.URL.String(), "token") {
|
||||
w.Header().Set("Link", fmt.Sprintf("<https://%s%s?token=next-token>; rel=next", r.Host, r.URL.Path))
|
||||
responseTags.Tags = []string{"first"}
|
||||
responseTags = fakeTagsList{
|
||||
Tags: []string{"first"},
|
||||
}
|
||||
} else {
|
||||
responseTags.Tags = []string{
|
||||
"second",
|
||||
"2.8.0",
|
||||
"2.8.0-prerelease",
|
||||
"2.8.0_build",
|
||||
"2.8.0-prerelease_build",
|
||||
"2.8.0-prerelease.1_build.1234",
|
||||
responseTags = fakeTagsList{
|
||||
Tags: []string{
|
||||
"second",
|
||||
"2.8.0",
|
||||
"2.8.0-prerelease",
|
||||
"2.8.0_build",
|
||||
"2.8.0-prerelease_build",
|
||||
"2.8.0-prerelease.1_build.1234",
|
||||
},
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
|
@ -193,7 +201,7 @@ func TestGetTagsFromUrl(t *testing.T) {
|
|||
|
||||
tags, err := client.GetTags("mychart", true)
|
||||
require.NoError(t, err)
|
||||
assert.ElementsMatch(t, tags.Tags, []string{
|
||||
assert.ElementsMatch(t, tags, []string{
|
||||
"first",
|
||||
"second",
|
||||
"2.8.0",
|
||||
|
|
@ -229,7 +237,7 @@ func TestGetTagsFromURLPrivateRepoAuthentication(t *testing.T) {
|
|||
|
||||
assert.Equal(t, expectedAuthorization, authorization)
|
||||
|
||||
responseTags := TagsList{
|
||||
responseTags := fakeTagsList{
|
||||
Tags: []string{
|
||||
"2.8.0",
|
||||
"2.8.0-prerelease",
|
||||
|
|
@ -281,7 +289,7 @@ func TestGetTagsFromURLPrivateRepoAuthentication(t *testing.T) {
|
|||
tags, err := client.GetTags("mychart", true)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.ElementsMatch(t, tags.Tags, []string{
|
||||
assert.ElementsMatch(t, tags, []string{
|
||||
"2.8.0",
|
||||
"2.8.0-prerelease",
|
||||
"2.8.0+build",
|
||||
|
|
@ -326,7 +334,7 @@ func TestGetTagsFromURLPrivateRepoWithAzureWorkloadIdentityAuthentication(t *tes
|
|||
|
||||
assert.Equal(t, expectedAuthorization, authorization)
|
||||
|
||||
responseTags := TagsList{
|
||||
responseTags := fakeTagsList{
|
||||
Tags: []string{
|
||||
"2.8.0",
|
||||
"2.8.0-prerelease",
|
||||
|
|
@ -379,7 +387,7 @@ func TestGetTagsFromURLPrivateRepoWithAzureWorkloadIdentityAuthentication(t *tes
|
|||
tags, err := client.GetTags("mychart", true)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.ElementsMatch(t, tags.Tags, []string{
|
||||
assert.ElementsMatch(t, tags, []string{
|
||||
"2.8.0",
|
||||
"2.8.0-prerelease",
|
||||
"2.8.0+build",
|
||||
|
|
@ -405,7 +413,7 @@ func TestGetTagsFromURLEnvironmentAuthentication(t *testing.T) {
|
|||
|
||||
assert.Equal(t, expectedAuthorization, authorization)
|
||||
|
||||
responseTags := TagsList{
|
||||
responseTags := fakeTagsList{
|
||||
Tags: []string{
|
||||
"2.8.0",
|
||||
"2.8.0-prerelease",
|
||||
|
|
@ -462,7 +470,7 @@ func TestGetTagsFromURLEnvironmentAuthentication(t *testing.T) {
|
|||
tags, err := client.GetTags("mychart", true)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.ElementsMatch(t, tags.Tags, []string{
|
||||
assert.ElementsMatch(t, tags, []string{
|
||||
"2.8.0",
|
||||
"2.8.0-prerelease",
|
||||
"2.8.0+build",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
package helm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
)
|
||||
|
||||
type Entry struct {
|
||||
|
|
@ -15,6 +10,16 @@ type Entry struct {
|
|||
Created time.Time
|
||||
}
|
||||
|
||||
type Entries []Entry
|
||||
|
||||
func (es Entries) Tags() []string {
|
||||
tags := make([]string, len(es))
|
||||
for i, e := range es {
|
||||
tags[i] = e.Version
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
type Index struct {
|
||||
Entries map[string]Entries
|
||||
}
|
||||
|
|
@ -26,34 +31,3 @@ func (i *Index) GetEntries(chart string) (Entries, error) {
|
|||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
type Entries []Entry
|
||||
|
||||
func (e Entries) MaxVersion(constraints *semver.Constraints) (*semver.Version, error) {
|
||||
versions := semver.Collection{}
|
||||
for _, entry := range e {
|
||||
v, err := semver.NewVersion(entry.Version)
|
||||
|
||||
// Invalid semantic version ignored
|
||||
if errors.Is(err, semver.ErrInvalidSemVer) {
|
||||
log.Debugf("Invalid sementic version: %s", entry.Version)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid constraint in index: %w", err)
|
||||
}
|
||||
if constraints.Check(v) {
|
||||
versions = append(versions, v)
|
||||
}
|
||||
}
|
||||
if len(versions) == 0 {
|
||||
return nil, errors.New("constraint not found in index")
|
||||
}
|
||||
maxVersion := versions[0]
|
||||
for _, v := range versions {
|
||||
if v.GreaterThan(maxVersion) {
|
||||
maxVersion = v
|
||||
}
|
||||
}
|
||||
return maxVersion, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package helm
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -12,10 +11,10 @@ var index = Index{
|
|||
Entries: map[string]Entries{
|
||||
"argo-cd": {
|
||||
{Version: "~0.7.3"},
|
||||
{Version: "0.7.2"},
|
||||
{Version: "0.7.1"},
|
||||
{Version: "0.5.4"},
|
||||
{Version: "0.5.3"},
|
||||
{Version: "0.7.2"},
|
||||
{Version: "0.5.2"},
|
||||
{Version: "~0.5.2"},
|
||||
{Version: "0.5.1"},
|
||||
|
|
@ -30,53 +29,8 @@ func TestIndex_GetEntries(t *testing.T) {
|
|||
require.EqualError(t, err, "chart 'foo' not found in index")
|
||||
})
|
||||
t.Run("Found", func(t *testing.T) {
|
||||
entries, err := index.GetEntries("argo-cd")
|
||||
ts, err := index.GetEntries("argo-cd")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, entries, 9)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEntries_MaxVersion(t *testing.T) {
|
||||
entries, _ := index.GetEntries("argo-cd")
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
constraints, _ := semver.NewConstraint("0.8.1")
|
||||
_, err := entries.MaxVersion(constraints)
|
||||
require.EqualError(t, err, "constraint not found in index")
|
||||
})
|
||||
t.Run("Exact", func(t *testing.T) {
|
||||
constraints, _ := semver.NewConstraint("0.5.3")
|
||||
version, err := entries.MaxVersion(constraints)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, semver.MustParse("0.5.3"), version)
|
||||
})
|
||||
t.Run("Constraint", func(t *testing.T) {
|
||||
constraints, _ := semver.NewConstraint("> 0.5.3")
|
||||
version, err := entries.MaxVersion(constraints)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, semver.MustParse("0.7.2"), version)
|
||||
})
|
||||
t.Run("Constraint", func(t *testing.T) {
|
||||
constraints, _ := semver.NewConstraint("> 0.0.0")
|
||||
version, err := entries.MaxVersion(constraints)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, semver.MustParse("0.7.2"), version)
|
||||
})
|
||||
t.Run("Constraint", func(t *testing.T) {
|
||||
constraints, _ := semver.NewConstraint(">0.5.0,<0.7.0")
|
||||
version, err := entries.MaxVersion(constraints)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, semver.MustParse("0.5.4"), version)
|
||||
})
|
||||
t.Run("Constraint", func(t *testing.T) {
|
||||
constraints, _ := semver.NewConstraint("0.7.*")
|
||||
version, err := entries.MaxVersion(constraints)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, semver.MustParse("0.7.2"), version)
|
||||
})
|
||||
t.Run("Constraint", func(t *testing.T) {
|
||||
constraints, _ := semver.NewConstraint("*")
|
||||
version, err := entries.MaxVersion(constraints)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, semver.MustParse("0.7.2"), version)
|
||||
assert.Len(t, ts, len(index.Entries["argo-cd"]))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
10
util/helm/mocks/Client.go
generated
10
util/helm/mocks/Client.go
generated
|
|
@ -100,23 +100,23 @@ func (_m *Client) GetIndex(noCache bool, maxIndexSize int64) (*helm.Index, error
|
|||
}
|
||||
|
||||
// GetTags provides a mock function with given fields: chart, noCache
|
||||
func (_m *Client) GetTags(chart string, noCache bool) (*helm.TagsList, error) {
|
||||
func (_m *Client) GetTags(chart string, noCache bool) ([]string, error) {
|
||||
ret := _m.Called(chart, noCache)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetTags")
|
||||
}
|
||||
|
||||
var r0 *helm.TagsList
|
||||
var r0 []string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, bool) (*helm.TagsList, error)); ok {
|
||||
if rf, ok := ret.Get(0).(func(string, bool) ([]string, error)); ok {
|
||||
return rf(chart, noCache)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, bool) *helm.TagsList); ok {
|
||||
if rf, ok := ret.Get(0).(func(string, bool) []string); ok {
|
||||
r0 = rf(chart, noCache)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*helm.TagsList)
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
package helm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
)
|
||||
|
||||
type TagsList struct {
|
||||
Tags []string
|
||||
}
|
||||
|
||||
func (t TagsList) MaxVersion(constraints *semver.Constraints) (*semver.Version, error) {
|
||||
versions := semver.Collection{}
|
||||
for _, tag := range t.Tags {
|
||||
v, err := semver.NewVersion(tag)
|
||||
|
||||
// Invalid semantic version ignored
|
||||
if errors.Is(err, semver.ErrInvalidSemVer) {
|
||||
log.Debugf("Invalid semantic version: %s", tag)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid constraint in tags: %w", err)
|
||||
}
|
||||
if constraints.Check(v) {
|
||||
versions = append(versions, v)
|
||||
}
|
||||
}
|
||||
if len(versions) == 0 {
|
||||
return nil, fmt.Errorf("constraint not found in %v tags", len(t.Tags))
|
||||
}
|
||||
maxVersion := versions[0]
|
||||
for _, v := range versions {
|
||||
if v.GreaterThan(maxVersion) {
|
||||
maxVersion = v
|
||||
}
|
||||
}
|
||||
return maxVersion, nil
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
package helm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var tags = TagsList{
|
||||
Tags: []string{
|
||||
"~0.7.3",
|
||||
"0.7.1",
|
||||
"0.5.4",
|
||||
"0.5.3",
|
||||
"0.7.2",
|
||||
"0.5.2",
|
||||
"~0.5.2",
|
||||
"0.5.1",
|
||||
"0.5.0",
|
||||
},
|
||||
}
|
||||
|
||||
func TestTagsList_MaxVersion(t *testing.T) {
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
constraints, _ := semver.NewConstraint("0.8.1")
|
||||
_, err := tags.MaxVersion(constraints)
|
||||
assert.EqualError(t, err, "constraint not found in 9 tags")
|
||||
})
|
||||
t.Run("Exact", func(t *testing.T) {
|
||||
constraints, _ := semver.NewConstraint("0.5.3")
|
||||
version, err := tags.MaxVersion(constraints)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, semver.MustParse("0.5.3"), version)
|
||||
})
|
||||
t.Run("Constraint", func(t *testing.T) {
|
||||
constraints, _ := semver.NewConstraint("> 0.5.3")
|
||||
version, err := tags.MaxVersion(constraints)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, semver.MustParse("0.7.2"), version)
|
||||
})
|
||||
}
|
||||
64
util/versions/tags.go
Normal file
64
util/versions/tags.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
package versions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
)
|
||||
|
||||
// MaxVersion takes a revision and a list of tags.
|
||||
// If the revision is a version, it returns that version, even if it is not in the list of tags.
|
||||
// If the revision is not a version, but is also not a constraint, it returns that revision, even if it is not in the list of tags.
|
||||
// If the revision is a constraint, it iterates over the list of tags to find the "maximum" tag which satisfies that
|
||||
// constraint.
|
||||
// If the revision is a constraint, but no tag satisfies that constraint, then it returns an error.
|
||||
func MaxVersion(revision string, tags []string) (string, error) {
|
||||
if v, err := semver.NewVersion(revision); err == nil {
|
||||
// If the revision is a valid version, then we know it isn't a constraint; it's just a pin.
|
||||
// In which case, we should use standard tag resolution mechanisms and return the original value.
|
||||
// For example, the following are considered valid versions, and therefore should match an exact tag:
|
||||
// - "v1.0.0"/"1.0.0"
|
||||
// - "v1.0"/"1.0"
|
||||
return v.Original(), nil
|
||||
}
|
||||
|
||||
constraints, err := semver.NewConstraint(revision)
|
||||
if err != nil {
|
||||
log.Debugf("Revision '%s' is not a valid semver constraint, resolving via basic string equality.", revision)
|
||||
// If this is also an invalid constraint, we just iterate over available tags to determine if it is valid/invalid.
|
||||
for _, tag := range tags {
|
||||
if tag == revision {
|
||||
return revision, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("failed to determine semver constraint: %w", err)
|
||||
}
|
||||
|
||||
var maxVersion *semver.Version
|
||||
for _, tag := range tags {
|
||||
v, err := semver.NewVersion(tag)
|
||||
|
||||
// Invalid semantic version ignored
|
||||
if errors.Is(err, semver.ErrInvalidSemVer) {
|
||||
log.Debugf("Invalid semantic version: %s", tag)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid semver version in tags: %w", err)
|
||||
}
|
||||
if constraints.Check(v) {
|
||||
if maxVersion == nil || v.GreaterThan(maxVersion) {
|
||||
maxVersion = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if maxVersion == nil {
|
||||
return "", fmt.Errorf("version matching constraint not found in %d tags", len(tags))
|
||||
}
|
||||
|
||||
log.Debugf("Semver constraint '%s' resolved to version '%s'", constraints.String(), maxVersion.Original())
|
||||
return maxVersion.Original(), nil
|
||||
}
|
||||
67
util/versions/tags_test.go
Normal file
67
util/versions/tags_test.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
package versions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var tags = []string{
|
||||
"0.7.1",
|
||||
"0.5.4",
|
||||
"0.5.3",
|
||||
"0.7.2",
|
||||
"0.5.2",
|
||||
"0.5.1",
|
||||
"0.5.0",
|
||||
"2024.03-LTS-RC19",
|
||||
}
|
||||
|
||||
func TestTags_MaxVersion(t *testing.T) {
|
||||
t.Run("Exact", func(t *testing.T) {
|
||||
version, err := MaxVersion("0.5.3", tags)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "0.5.3", version)
|
||||
})
|
||||
t.Run("Exact nonsemver", func(t *testing.T) {
|
||||
version, err := MaxVersion("2024.03-LTS-RC19", tags)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "2024.03-LTS-RC19", version)
|
||||
})
|
||||
t.Run("Exact missing", func(t *testing.T) {
|
||||
// Passing an exact version which is not in the list of tags still returns that version
|
||||
version, err := MaxVersion("99.99", []string{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "99.99", version)
|
||||
})
|
||||
t.Run("Constraint", func(t *testing.T) {
|
||||
version, err := MaxVersion("> 0.5.3", tags)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "0.7.2", version)
|
||||
})
|
||||
t.Run("Constraint", func(t *testing.T) {
|
||||
version, err := MaxVersion("> 0.0.0", tags)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "0.7.2", version)
|
||||
})
|
||||
t.Run("Constraint", func(t *testing.T) {
|
||||
version, err := MaxVersion(">0.5.0,<0.7.0", tags)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "0.5.4", version)
|
||||
})
|
||||
t.Run("Constraint", func(t *testing.T) {
|
||||
version, err := MaxVersion("0.7.*", tags)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "0.7.2", version)
|
||||
})
|
||||
t.Run("Constraint", func(t *testing.T) {
|
||||
version, err := MaxVersion("*", tags)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "0.7.2", version)
|
||||
})
|
||||
t.Run("Constraint missing", func(t *testing.T) {
|
||||
_, err := MaxVersion("0.7.*", []string{})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package helm
|
||||
package versions
|
||||
|
||||
import "github.com/Masterminds/semver/v3"
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package helm
|
||||
package versions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
|
@ -10,5 +10,6 @@ func TestIsVersion(t *testing.T) {
|
|||
assert.False(t, IsVersion("*"))
|
||||
assert.False(t, IsVersion("1.*"))
|
||||
assert.False(t, IsVersion("1.0.*"))
|
||||
assert.True(t, IsVersion("1.0"))
|
||||
assert.True(t, IsVersion("1.0.0"))
|
||||
}
|
||||
Loading…
Reference in a new issue