Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions reposerver/repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -2610,6 +2610,10 @@ func (s *Service) newHelmClientResolveRevision(repo *v1alpha1.Repository, revisi

var tags []string
if enableOCI {
if versions.IsDigest(revision) {
// This checks avoids unnecessary retrieval of the Helm chart tags from the repo if the revision is a digest
return helmClient, revision, nil
}
var err error
tags, err = helmClient.GetTags(chart, noRevisionCache)
if err != nil {
Expand Down
24 changes: 18 additions & 6 deletions util/helm/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
utilio "github.com/argoproj/argo-cd/v3/util/io"
pathutil "github.com/argoproj/argo-cd/v3/util/io/path"
"github.com/argoproj/argo-cd/v3/util/proxy"
"github.com/argoproj/argo-cd/v3/util/versions"
)

// A thin wrapper around the "helm" command, adding logging and error translation.
Expand Down Expand Up @@ -228,9 +229,14 @@ func writeToTmp(data []byte) (string, utilio.Closer, error) {

func (c *Cmd) Fetch(repo, chartName, version, destination string, creds Creds, passCredentials bool) (string, error) {
args := []string{"pull", "--destination", destination}
if version != "" {
if versions.IsDigest(version) {
// For sha256 digest, append it to the chart name and pass as such to Helm CLI
chartName = fmt.Sprintf("%s@%s", chartName, version)
} else {
// use --version flag only if chart is not pointed by image digest
args = append(args, "--version", version)
}

if creds.GetUsername() != "" {
args = append(args, "--username", creds.GetUsername())
}
Expand Down Expand Up @@ -279,12 +285,18 @@ func (c *Cmd) Fetch(repo, chartName, version, destination string, creds Creds, p
}

func (c *Cmd) PullOCI(repo string, chart string, version string, destination string, creds Creds) (string, error) {
args := []string{
"pull", fmt.Sprintf("oci://%s/%s", repo, chart), "--version",
version,
"--destination",
destination,
args := []string{"pull"}
chartRef := fmt.Sprintf("oci://%s/%s", repo, chart)

if versions.IsDigest(version) {
// For sha256 digest, append it to the chart name and pass as such to Helm CLI
args = append(args, fmt.Sprintf("%s@%s", chartRef, version))
} else {
// use --version flag only if chart is not pointed by image digest
args = append(args, chartRef, "--version", version)
}
args = append(args, "--destination", destination)

if creds.GetCAPath() != "" {
args = append(args, "--ca-file", creds.GetCAPath())
}
Expand Down
25 changes: 25 additions & 0 deletions util/versions/digest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package versions

import (
digest "github.com/opencontainers/go-digest"
)

// IsDigest checks if the provided revision string is a valid SHA256 digest.
// It returns true if the revision is a valid digest with SHA256 algorithm,
// and false otherwise.
//
// In OCI (Open Container Initiative) repositories, content is often referenced by
// digest rather than by tag to ensure immutability. A valid digest has the format:
// "sha256:abcdef1234567890..." where the part after the colon is a hexadecimal string.
//
// This function performs two validations:
// 1. Checks if the string can be parsed as a digest (correct format)
// 2. Verifies that the algorithm is specifically SHA256 (not other hash algorithms)
func IsDigest(revision string) bool {
d, err := digest.Parse(revision)
if err != nil {
return false
}

return d.Validate() == nil && d.Algorithm() == digest.SHA256
}
18 changes: 18 additions & 0 deletions util/versions/digest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package versions

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestIsDigest(t *testing.T) {
assert.False(t, IsDigest("*"))
assert.False(t, IsDigest("1.*"))
assert.False(t, IsDigest("1.0.*"))
assert.False(t, IsDigest("1.0"))
assert.False(t, IsDigest("1.0.0"))
assert.False(t, IsDigest("sha256:12345"))
assert.False(t, IsDigest("sha256:zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"))
assert.True(t, IsDigest("sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
}
11 changes: 11 additions & 0 deletions util/versions/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import (
// 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) {
// Check if the revision is a SHA256 digest (used in OCI repositories)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these general purpose functions, is there any other use for digests except for this helm use-case? If not, I would argue the fix does not belong to util/versions/tags.go.

Copy link
Author

@pgodowski pgodowski Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

created versions.IsDigest in util/versions/digest.go

if IsDigest(revision) {
log.Debugf("Revision '%s' is a SHA256 digest, returning as is", revision)
return revision, nil
}

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.
Expand Down Expand Up @@ -65,6 +71,11 @@ func MaxVersion(revision string, tags []string) (string, error) {

// Returns true if the given revision is not an exact semver and can be parsed as a semver constraint
func IsConstraint(revision string) bool {
// SHA256 digests are not constraints
if IsDigest(revision) {
return false
}

if _, err := semver.NewVersion(revision); err == nil {
return false
}
Expand Down
9 changes: 9 additions & 0 deletions util/versions/tags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ func TestTags_MaxVersion(t *testing.T) {
_, err := MaxVersion("0.7.*", []string{})
require.Error(t, err)
})
t.Run("SHA256 digest", func(t *testing.T) {
sha256Digest := "sha256:5dd1328526a5de7577ace7b6734e6b5b4f4d2c52a7013461559d598336b67fe5"
version, err := MaxVersion(sha256Digest, tags)
require.NoError(t, err)
assert.Equal(t, sha256Digest, version)
})
}

func TestTags_IsConstraint(t *testing.T) {
Expand All @@ -88,4 +94,7 @@ func TestTags_IsConstraint(t *testing.T) {
t.Run("Constraint", func(t *testing.T) {
assert.True(t, IsConstraint("*"))
})
t.Run("SHA256 digest", func(t *testing.T) {
assert.False(t, IsConstraint("sha256:5dd1328526a5de7577ace7b6734e6b5b4f4d2c52a7013461559d598336b67fe5"))
})
}
Loading