diff --git a/reposerver/repository/repository.go b/reposerver/repository/repository.go index cf12165a01178..cb484c7fc3f60 100644 --- a/reposerver/repository/repository.go +++ b/reposerver/repository/repository.go @@ -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 { diff --git a/util/helm/cmd.go b/util/helm/cmd.go index f54cf96198344..b5a16d556f6d3 100644 --- a/util/helm/cmd.go +++ b/util/helm/cmd.go @@ -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. @@ -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()) } @@ -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()) } diff --git a/util/versions/digest.go b/util/versions/digest.go new file mode 100644 index 0000000000000..85acef0d4091f --- /dev/null +++ b/util/versions/digest.go @@ -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 +} diff --git a/util/versions/digest_test.go b/util/versions/digest_test.go new file mode 100644 index 0000000000000..46c5cada1851f --- /dev/null +++ b/util/versions/digest_test.go @@ -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")) +} diff --git a/util/versions/tags.go b/util/versions/tags.go index 0223b910b78d1..444040baf30b7 100644 --- a/util/versions/tags.go +++ b/util/versions/tags.go @@ -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) + 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. @@ -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 } diff --git a/util/versions/tags_test.go b/util/versions/tags_test.go index 3cf2713a0be98..c3f869df6bdba 100644 --- a/util/versions/tags_test.go +++ b/util/versions/tags_test.go @@ -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) { @@ -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")) + }) }