fix: support OCI chart digest syntax (@sha256:...) (#2398)

fix: support OCI chart digest syntax in chart URLs and version fields

Helm supports pinning OCI chart images by digest (@sha256:...), version
tag (:version), or both (:version@sha256:digest) since helm/helm#12690.
Helmfile failed to parse these formats, incorrectly constructing helm
commands and losing version/digest information embedded in chart URLs.

Root causes:
- resolveOciChart() used last ":" to find version tag, but sha256:abc
  contains ":", so digest URLs were split incorrectly
- getOCIQualifiedChartName() included :version and @digest in chartName
  with no parsing of either source
- appendChartVersionFlags() passed release.Version verbatim to --version
  flag, including any digest suffix
- ChartPull() discarded the tag from resolveOciChart but did not
  preserve digest in the URL

This commit adds parseOCIChartRef() and parseVersionDigest() utilities,
then updates the OCI chart handling pipeline so that:
- Digests are preserved in the chart URL passed to helm pull
- Version tags are extracted cleanly for the --version flag
- Both chart URL and version field are parsed for version/digest info

Fixes #2097

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>
This commit is contained in:
Aditya Menon 2026-02-12 17:50:43 +05:30 committed by GitHub
parent 2f8b9cbdfb
commit 5c43fa6465
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 910 additions and 10 deletions

View File

@ -962,8 +962,16 @@ func (helm *execer) IsVersionAtLeast(versionStr string) bool {
}
func resolveOciChart(ociChart string) (ociChartURL, ociChartTag string) {
// Split off digest (e.g., @sha256:abc) first so the colon in sha256:
// does not confuse the version tag search below.
var digest string
if atIdx := strings.Index(ociChart, "@"); atIdx >= 0 {
digest = ociChart[atIdx:] // includes the "@"
ociChart = ociChart[:atIdx]
}
var urlTagIndex int
// Get the last : index
// Get the last : index in the pre-digest part
// e.g.,
// 1. registry:443/helm-charts
// 2. registry/helm-charts:latest
@ -975,7 +983,7 @@ func resolveOciChart(ociChart string) (ociChartURL, ociChartTag string) {
urlTagIndex = strings.LastIndex(ociChart, ":")
ociChartTag = ociChart[urlTagIndex+1:]
}
ociChartURL = fmt.Sprintf("oci://%s", ociChart[:urlTagIndex])
ociChartURL = fmt.Sprintf("oci://%s%s", ociChart[:urlTagIndex], digest)
return ociChartURL, ociChartTag
}

View File

@ -1381,6 +1381,30 @@ func Test_resolveOciChart(t *testing.T) {
ociChartURL: "oci://chart:5000/nginx",
ociChartTag: "",
},
{
name: "digest only",
chartPath: "ghcr.io/nginxinc/charts/nginx-ingress@sha256:87ad282a8e7cc31913ce0543de2933ddb3f3eba80d6e5285f33b62ed720fc085",
ociChartURL: "oci://ghcr.io/nginxinc/charts/nginx-ingress@sha256:87ad282a8e7cc31913ce0543de2933ddb3f3eba80d6e5285f33b62ed720fc085",
ociChartTag: "",
},
{
name: "version and digest",
chartPath: "ghcr.io/nginxinc/charts/nginx-ingress:2.0.0@sha256:87ad282a8e7cc31913ce0543de2933ddb3f3eba80d6e5285f33b62ed720fc085",
ociChartURL: "oci://ghcr.io/nginxinc/charts/nginx-ingress@sha256:87ad282a8e7cc31913ce0543de2933ddb3f3eba80d6e5285f33b62ed720fc085",
ociChartTag: "2.0.0",
},
{
name: "port with digest",
chartPath: "registry:5000/chart@sha256:abc123",
ociChartURL: "oci://registry:5000/chart@sha256:abc123",
ociChartTag: "",
},
{
name: "port with version and digest",
chartPath: "registry:5000/chart:1.0.0@sha256:abc123",
ociChartURL: "oci://registry:5000/chart@sha256:abc123",
ociChartTag: "1.0.0",
},
}
for i := range tests {
tt := tests[i]

View File

@ -83,6 +83,51 @@ func TestOCIChartVersionHandling(t *testing.T) {
expectedError: false,
expectedQualifiedChart: "",
},
{
name: "OCI chart with digest in version field",
chart: "oci://registry.example.com/my-chart",
version: "1.2.3@sha256:abc123def456",
helmVersion: "3.18.0",
expectedVersion: "1.2.3",
expectedError: false,
expectedQualifiedChart: "registry.example.com/my-chart@sha256:abc123def456",
},
{
name: "OCI chart with digest-only in version field",
chart: "oci://registry.example.com/my-chart",
version: "@sha256:abc123def456",
helmVersion: "3.18.0",
expectedVersion: "",
expectedError: false,
expectedQualifiedChart: "registry.example.com/my-chart@sha256:abc123def456",
},
{
name: "OCI chart with version tag in URL",
chart: "oci://registry.example.com/my-chart:1.2.3",
version: "",
helmVersion: "3.18.0",
expectedVersion: "1.2.3",
expectedError: false,
expectedQualifiedChart: "registry.example.com/my-chart",
},
{
name: "OCI chart with digest in URL",
chart: "oci://registry.example.com/my-chart@sha256:abc123def456",
version: "",
helmVersion: "3.18.0",
expectedVersion: "",
expectedError: false,
expectedQualifiedChart: "registry.example.com/my-chart@sha256:abc123def456",
},
{
name: "OCI chart with version and digest in URL",
chart: "oci://registry.example.com/my-chart:1.2.3@sha256:abc123def456",
version: "",
helmVersion: "3.18.0",
expectedVersion: "1.2.3",
expectedError: false,
expectedQualifiedChart: "registry.example.com/my-chart@sha256:abc123def456",
},
}
for _, tt := range tests {

118
pkg/state/oci_parse_test.go Normal file
View File

@ -0,0 +1,118 @@
package state
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseOCIChartRef(t *testing.T) {
tests := []struct {
name string
chartURL string
expectedBase string
expectedVer string
expectedDigest string
}{
{
name: "plain OCI URL",
chartURL: "oci://registry/chart",
expectedBase: "oci://registry/chart",
expectedVer: "",
expectedDigest: "",
},
{
name: "OCI URL with version",
chartURL: "oci://registry/chart:2.0.0",
expectedBase: "oci://registry/chart",
expectedVer: "2.0.0",
expectedDigest: "",
},
{
name: "OCI URL with digest",
chartURL: "oci://registry/chart@sha256:abc",
expectedBase: "oci://registry/chart",
expectedVer: "",
expectedDigest: "sha256:abc",
},
{
name: "OCI URL with version and digest",
chartURL: "oci://reg/chart:2.0@sha256:abc",
expectedBase: "oci://reg/chart",
expectedVer: "2.0",
expectedDigest: "sha256:abc",
},
{
name: "OCI URL with port, version, and digest",
chartURL: "oci://reg:5000/chart:1.0@sha256:a",
expectedBase: "oci://reg:5000/chart",
expectedVer: "1.0",
expectedDigest: "sha256:a",
},
{
name: "OCI URL with port only",
chartURL: "oci://reg:5000/chart",
expectedBase: "oci://reg:5000/chart",
expectedVer: "",
expectedDigest: "",
},
{
name: "OCI URL with port and digest, no version",
chartURL: "oci://reg:5000/chart@sha256:abc",
expectedBase: "oci://reg:5000/chart",
expectedVer: "",
expectedDigest: "sha256:abc",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
base, ver, digest := parseOCIChartRef(tt.chartURL)
assert.Equal(t, tt.expectedBase, base, "baseURL mismatch")
assert.Equal(t, tt.expectedVer, ver, "version mismatch")
assert.Equal(t, tt.expectedDigest, digest, "digest mismatch")
})
}
}
func TestParseVersionDigest(t *testing.T) {
tests := []struct {
name string
version string
expectedVer string
expectedDigest string
}{
{
name: "version only",
version: "2.0.0",
expectedVer: "2.0.0",
expectedDigest: "",
},
{
name: "version with digest",
version: "2.0.0@sha256:abc",
expectedVer: "2.0.0",
expectedDigest: "sha256:abc",
},
{
name: "digest only",
version: "@sha256:abc",
expectedVer: "",
expectedDigest: "sha256:abc",
},
{
name: "empty string",
version: "",
expectedVer: "",
expectedDigest: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ver, digest := parseVersionDigest(tt.version)
assert.Equal(t, tt.expectedVer, ver, "version mismatch")
assert.Equal(t, tt.expectedDigest, digest, "digest mismatch")
})
}
}

View File

@ -3513,8 +3513,13 @@ func (st *HelmState) appendTakeOwnershipFlagsForDiff(flags []string, release *Re
}
func (st *HelmState) appendChartVersionFlags(flags []string, release *ReleaseSpec) []string {
if release.Version != "" {
flags = append(flags, "--version", release.Version)
version := release.Version
// Strip OCI digest from version (digest is handled in chart URL, not --version flag)
if idx := strings.Index(version, "@"); idx >= 0 {
version = version[:idx]
}
if version != "" {
flags = append(flags, "--version", version)
}
if st.isDevelopment(release) {
@ -4807,7 +4812,17 @@ func (st *HelmState) getOCIChart(release *ReleaseSpec, tempDir string, helm helm
flags = st.appendVerifyFlags(flags, release)
flags = st.appendKeyringFlags(flags, release)
flags = st.appendChartDownloadFlags(flags, release)
flags = st.appendChartVersionFlags(flags, release)
// Use the clean chartVersion (without digest) from getOCIQualifiedChartName
// rather than appendChartVersionFlags which uses release.Version verbatim.
// The digest is already embedded in qualifiedChartName.
// When a digest is present, omit --version: the digest is the authoritative
// content identifier, and passing both causes errors in some Helm versions.
if chartVersion != "" && !strings.Contains(qualifiedChartName, "@") {
flags = append(flags, "--version", chartVersion)
}
if st.isDevelopment(release) {
flags = append(flags, "--devel")
}
if err := helm.ChartPull(qualifiedChartName, chartPath, flags...); err != nil {
lockResult.Release(st.logger)
@ -4850,6 +4865,48 @@ func (st *HelmState) IsOCIChart(chart string) bool {
return repo.OCI
}
// parseOCIChartRef parses an OCI chart URL into base URL, version tag, and digest.
// Examples:
//
// oci://registry/chart → (oci://registry/chart, "", "")
// oci://registry/chart:2.0.0 → (oci://registry/chart, "2.0.0", "")
// oci://registry/chart@sha256:a → (oci://registry/chart, "", "sha256:a")
// oci://registry/chart:2.0@sha256:a → (oci://registry/chart, "2.0", "sha256:a")
// oci://reg:5000/chart:1.0@sha256:a → (oci://reg:5000/chart, "1.0", "sha256:a")
func parseOCIChartRef(chartURL string) (baseURL, version, digest string) {
// Split off digest first (everything after @)
if atIdx := strings.Index(chartURL, "@"); atIdx >= 0 {
digest = chartURL[atIdx+1:]
chartURL = chartURL[:atIdx]
}
// Find version tag: last ":" that comes after the last "/"
lastSlash := strings.LastIndex(chartURL, "/")
lastColon := strings.LastIndex(chartURL, ":")
if lastColon > lastSlash {
version = chartURL[lastColon+1:]
baseURL = chartURL[:lastColon]
} else {
baseURL = chartURL
}
return baseURL, version, digest
}
// parseVersionDigest splits a version string that may contain an OCI digest.
// Examples:
//
// "2.0.0" → ("2.0.0", "")
// "2.0.0@sha256:abc" → ("2.0.0", "sha256:abc")
// "@sha256:abc" → ("", "sha256:abc")
// "" → ("", "")
func parseVersionDigest(version string) (ver, digest string) {
if atIdx := strings.Index(version, "@"); atIdx >= 0 {
return version[:atIdx], version[atIdx+1:]
}
return version, ""
}
func (st *HelmState) getOCIQualifiedChartName(release *ReleaseSpec) (string, string, string, error) {
// For issue #2247: Don't default to "latest" - use empty string to let Helm pull the latest version
// Only use the version explicitly provided by the user
@ -4867,21 +4924,69 @@ func (st *HelmState) getOCIQualifiedChartName(release *ReleaseSpec) (string, str
// Reject explicit "latest" for OCI charts (issue #1047, #2247)
// This only applies if user explicitly specified "latest", not when version is omitted
// We reject for all Helm versions to ensure consistent behavior
if release.Version == "latest" {
// Strip any digest suffix before checking (e.g. "latest@sha256:..." is still invalid)
versionForCheck, _ := parseVersionDigest(release.Version)
if versionForCheck == "latest" {
return "", "", "", fmt.Errorf("the version for OCI charts should be semver compliant, the latest tag is not supported")
}
var qualifiedChartName, chartName string
if strings.HasPrefix(release.Chart, "oci://") {
parts := strings.Split(release.Chart, "/")
// Parse version and digest from the chart URL
baseURL, versionInURL, digestInURL := parseOCIChartRef(release.Chart)
// Parse version and digest from the version field
versionInField, digestInField := parseVersionDigest(chartVersion)
// Merge: version field takes precedence; fall back to URL-embedded version
finalVersion := versionInField
if finalVersion == "" && versionInURL != "" {
finalVersion = versionInURL
}
// Merge: URL-embedded digest takes precedence; fall back to version field digest
finalDigest := digestInURL
if finalDigest == "" {
finalDigest = digestInField
}
// Extract chart name from base URL (last path segment)
parts := strings.Split(baseURL, "/")
chartName = parts[len(parts)-1]
qualifiedChartName = strings.Replace(fmt.Sprintf("%s:%s", release.Chart, chartVersion), "oci://", "", 1)
// Build qualifiedChartName: base (without oci:// prefix) + digest or version
base := strings.TrimPrefix(baseURL, "oci://")
if finalDigest != "" {
// Digest present — put it in the URL; version goes through --version flag only
qualifiedChartName = fmt.Sprintf("%s@%s", base, finalDigest)
} else if versionInURL == "" && finalVersion != "" {
// Version from field only (backward compatible format)
qualifiedChartName = fmt.Sprintf("%s:%s", base, finalVersion)
} else {
// Version came from URL (handled via --version flag) or no version at all
qualifiedChartName = base
}
chartVersion = finalVersion
} else {
var repo *RepositorySpec
repo, chartName = st.GetRepositoryAndNameFromChartName(release.Chart)
qualifiedChartName = fmt.Sprintf("%s/%s:%s", repo.URL, chartName, chartVersion)
// Handle digest in version field for repo-aliased OCI charts too
base := fmt.Sprintf("%s/%s", repo.URL, chartName)
finalVersion, digest := parseVersionDigest(chartVersion)
switch {
case digest != "":
qualifiedChartName = fmt.Sprintf("%s@%s", base, digest)
case finalVersion != "":
qualifiedChartName = fmt.Sprintf("%s:%s", base, finalVersion)
default:
qualifiedChartName = base
}
chartVersion = finalVersion
}
qualifiedChartName = strings.TrimSuffix(qualifiedChartName, ":")
return qualifiedChartName, chartName, chartVersion, nil
}

View File

@ -3701,6 +3701,212 @@ func TestGetOCIQualifiedChartName(t *testing.T) {
{"registry/chart-path/chart-name", "chart-name", ""},
},
},
// Digest in version field
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name",
Version: "2.0.0@sha256:abc123",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name@sha256:abc123", "chart-name", "2.0.0"},
},
},
// Digest-only in version field
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name",
Version: "@sha256:abc123",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name@sha256:abc123", "chart-name", ""},
},
},
// Version tag in URL
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name:2.0.0",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name", "chart-name", "2.0.0"},
},
},
// Digest in URL
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name@sha256:abc123",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name@sha256:abc123", "chart-name", ""},
},
},
// Version + digest in URL
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name:2.0.0@sha256:abc123",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name@sha256:abc123", "chart-name", "2.0.0"},
},
},
// Port with digest in URL
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry:5000/chart-path/chart-name@sha256:abc123",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry:5000/chart-path/chart-name@sha256:abc123", "chart-name", ""},
},
},
// Digest in URL + version field
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name@sha256:abc123",
Version: "2.0.0",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name@sha256:abc123", "chart-name", "2.0.0"},
},
},
// Repo-aliased OCI chart with digest in version
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/chart-path",
OCI: true,
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chart-name",
Version: "2.0.0@sha256:abc123",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name@sha256:abc123", "chart-name", "2.0.0"},
},
},
// Repo-aliased OCI chart with digest-only version
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/chart-path",
OCI: true,
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chart-name",
Version: "@sha256:abc123",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name@sha256:abc123", "chart-name", ""},
},
},
}
for _, tt := range tests {

View File

@ -117,6 +117,7 @@ ${kubectl} create namespace ${test_ns} || fail "Could not create namespace ${tes
. ${dir}/test-cases/issue-2281-array-merge.sh
. ${dir}/test-cases/issue-2353-layer-array-replace.sh
. ${dir}/test-cases/issue-2247.sh
. ${dir}/test-cases/issue-2097.sh
. ${dir}/test-cases/issue-2291.sh
. ${dir}/test-cases/oci-parallel-pull.sh
. ${dir}/test-cases/issue-2297-local-chart-transformers.sh

View File

@ -0,0 +1,363 @@
#!/usr/bin/env bash
# Test for issue #2097: OCI chart digest support
# This test combines validation tests (fast) with registry tests (comprehensive)
issue_2097_input_dir="${cases_dir}/issue-2097/input"
issue_2097_chart_dir="${cases_dir}/issue-2097/chart"
issue_2097_tmp_dir=$(mktemp -d)
test_start "issue-2097: OCI chart digest support"
# ==============================================================================================================
# PART 1: Fast Validation Tests (no registry required)
# ==============================================================================================================
info "Part 1: Validation tests (no registry required)"
# Test 1.1: Digest in version field should NOT trigger semver validation error
info "Test 1.1: Verifying version with digest (2.0.0@sha256:...) does not trigger validation error"
set +e
${helmfile} -f "${issue_2097_input_dir}/helmfile-digest-in-version.yaml" template > "${issue_2097_tmp_dir}/digest-in-version.txt" 2>&1
code=$?
set -e
# The command will fail because the registry doesn't exist, but it should NOT fail with
# "semver compliant" validation error
if grep -q "semver compliant" "${issue_2097_tmp_dir}/digest-in-version.txt"; then
cat "${issue_2097_tmp_dir}/digest-in-version.txt"
rm -rf "${issue_2097_tmp_dir}"
fail "Issue #2097 regression: version with digest triggered validation error"
fi
info "SUCCESS: version with digest does not trigger validation error"
# Test 1.2: Version tag in chart URL should NOT trigger validation error
info "Test 1.2: Verifying chart URL with version tag (oci://reg/chart:2.0.0) does not trigger validation error"
set +e
${helmfile} -f "${issue_2097_input_dir}/helmfile-version-in-url.yaml" template > "${issue_2097_tmp_dir}/version-in-url.txt" 2>&1
code=$?
set -e
if grep -q "semver compliant" "${issue_2097_tmp_dir}/version-in-url.txt"; then
cat "${issue_2097_tmp_dir}/version-in-url.txt"
rm -rf "${issue_2097_tmp_dir}"
fail "Issue #2097 regression: version in chart URL triggered validation error"
fi
info "SUCCESS: version in chart URL does not trigger validation error"
# Test 1.3: Digest in chart URL should NOT trigger validation error
info "Test 1.3: Verifying chart URL with digest (oci://reg/chart@sha256:...) does not trigger validation error"
set +e
${helmfile} -f "${issue_2097_input_dir}/helmfile-digest-in-url.yaml" template > "${issue_2097_tmp_dir}/digest-in-url.txt" 2>&1
code=$?
set -e
if grep -q "semver compliant" "${issue_2097_tmp_dir}/digest-in-url.txt"; then
cat "${issue_2097_tmp_dir}/digest-in-url.txt"
rm -rf "${issue_2097_tmp_dir}"
fail "Issue #2097 regression: digest in chart URL triggered validation error"
fi
info "SUCCESS: digest in chart URL does not trigger validation error"
# ==============================================================================================================
# PART 2: Comprehensive Registry Tests (requires Docker)
# ==============================================================================================================
# Check if Docker is available
if ! command -v docker &> /dev/null; then
info "Skipping registry tests (Docker not available)"
rm -rf "${issue_2097_tmp_dir}"
trap - EXIT
test_pass "issue-2097: OCI chart digest support (validation tests only)"
return 0
fi
# Check if Docker daemon is running
if ! docker info &> /dev/null; then
info "Skipping registry tests (Docker daemon not running)"
rm -rf "${issue_2097_tmp_dir}"
trap - EXIT
test_pass "issue-2097: OCI chart digest support (validation tests only)"
return 0
fi
info "Part 2: Comprehensive tests with real OCI registry"
registry_container_name="helmfile-test-registry-2097"
registry_port=5097
# Cleanup function
cleanup_registry_2097() {
info "Cleaning up test registry (issue-2097)"
docker stop ${registry_container_name} &>/dev/null || true
docker rm ${registry_container_name} &>/dev/null || true
rm -rf "${issue_2097_tmp_dir}"
}
# Ensure cleanup on exit
trap cleanup_registry_2097 EXIT
# Test 2.1: Start local OCI registry
info "Test 2.1: Starting local OCI registry on port ${registry_port}"
docker run -d \
--name ${registry_container_name} \
-p ${registry_port}:5000 \
--rm \
registry:2 &> "${issue_2097_tmp_dir}/registry-start.log"
if [ $? -ne 0 ]; then
cat "${issue_2097_tmp_dir}/registry-start.log"
warn "Failed to start Docker registry - skipping registry tests"
rm -rf "${issue_2097_tmp_dir}"
trap - EXIT
test_pass "issue-2097: OCI chart digest support (validation tests only)"
return 0
fi
# Wait for registry to be ready
info "Waiting for registry to be ready..."
max_attempts=30
attempt=0
while [ $attempt -lt $max_attempts ]; do
if curl -s http://localhost:${registry_port}/v2/ > /dev/null 2>&1; then
info "Registry is ready"
break
fi
attempt=$((attempt + 1))
sleep 1
done
if [ $attempt -eq $max_attempts ]; then
warn "Registry did not become ready in time - skipping registry tests"
cleanup_registry_2097
trap - EXIT
test_pass "issue-2097: OCI chart digest support (validation tests only)"
return 0
fi
# Test 2.2: Package and push the test chart
info "Test 2.2: Packaging and pushing test charts"
set +e
${helm} package "${issue_2097_chart_dir}" -d "${issue_2097_tmp_dir}" > "${issue_2097_tmp_dir}/package.log" 2>&1
if [ $? -ne 0 ]; then
set -e
cat "${issue_2097_tmp_dir}/package.log"
warn "Failed to package chart - skipping registry tests"
cleanup_registry_2097
trap - EXIT
test_pass "issue-2097: OCI chart digest support (validation tests only)"
return 0
fi
set -e
info "Pushing chart version 1.0.0 to local registry"
set +e
${helm} push "${issue_2097_tmp_dir}/test-chart-2097-1.0.0.tgz" oci://localhost:${registry_port} --plain-http > "${issue_2097_tmp_dir}/push.log" 2>&1
if [ $? -ne 0 ]; then
set -e
cat "${issue_2097_tmp_dir}/push.log"
warn "Failed to push chart to registry - skipping registry tests"
cleanup_registry_2097
trap - EXIT
test_pass "issue-2097: OCI chart digest support (validation tests only)"
return 0
fi
set -e
# Create version 2.0.0 as well
info "Creating and pushing version 2.0.0"
cp -r "${issue_2097_chart_dir}" "${issue_2097_tmp_dir}/chart-v2"
sed -i.bak 's/version: 1.0.0/version: 2.0.0/' "${issue_2097_tmp_dir}/chart-v2/Chart.yaml"
set +e
${helm} package "${issue_2097_tmp_dir}/chart-v2" -d "${issue_2097_tmp_dir}" > "${issue_2097_tmp_dir}/package-v2.log" 2>&1
${helm} push "${issue_2097_tmp_dir}/test-chart-2097-2.0.0.tgz" oci://localhost:${registry_port} --plain-http > "${issue_2097_tmp_dir}/push-v2.log" 2>&1
set -e
info "Successfully pushed chart versions 1.0.0 and 2.0.0"
# Get the digest of the v1.0.0 chart
info "Fetching digest for v1.0.0"
set +e
chart_digest=$(${helm} pull oci://localhost:${registry_port}/test-chart-2097 --version 1.0.0 --plain-http -d "${issue_2097_tmp_dir}/pull-test" 2>&1 | grep -oE 'sha256:[a-f0-9]+')
set -e
if [ -z "$chart_digest" ]; then
# Try alternative: use the registry API
chart_digest=$(curl -s http://localhost:${registry_port}/v2/test-chart-2097/manifests/1.0.0 \
-H "Accept: application/vnd.oci.image.manifest.v1+json" \
-D - -o /dev/null 2>/dev/null | grep -i "docker-content-digest" | tr -d '\r' | awk '{print $2}')
fi
if [ -z "$chart_digest" ]; then
warn "Could not determine chart digest - skipping digest registry tests"
cleanup_registry_2097
trap - EXIT
test_pass "issue-2097: OCI chart digest support (validation tests only)"
return 0
fi
info "Chart v1.0.0 digest: ${chart_digest}"
# Test 2.3: Baseline - chart with version in field (should work)
info "Test 2.3: Baseline - chart with version in field"
cat > "${issue_2097_tmp_dir}/helmfile-baseline.yaml" <<EOF
helmDefaults:
plainHttp: true
releases:
- name: test-oci-baseline
namespace: default
chart: oci://localhost:${registry_port}/test-chart-2097
version: "1.0.0"
EOF
set +e
${helmfile} -f "${issue_2097_tmp_dir}/helmfile-baseline.yaml" template --skip-deps > "${issue_2097_tmp_dir}/template-baseline.yaml" 2>&1
code=$?
set -e
if [ $code -ne 0 ]; then
cat "${issue_2097_tmp_dir}/template-baseline.yaml"
cleanup_registry_2097
fail "Baseline test failed - cannot proceed with digest tests"
fi
info "SUCCESS: Baseline test passed"
if grep -q "Hello from test chart 1.0.0" "${issue_2097_tmp_dir}/template-baseline.yaml"; then
info "SUCCESS: Correctly pulled version 1.0.0"
fi
# Test 2.4: Version tag in chart URL
info "Test 2.4: Version tag in chart URL"
cat > "${issue_2097_tmp_dir}/helmfile-version-url.yaml" <<EOF
helmDefaults:
plainHttp: true
releases:
- name: test-oci-version-url
namespace: default
chart: oci://localhost:${registry_port}/test-chart-2097:1.0.0
EOF
set +e
${helmfile} -f "${issue_2097_tmp_dir}/helmfile-version-url.yaml" template --skip-deps > "${issue_2097_tmp_dir}/template-version-url.yaml" 2>&1
code=$?
set -e
if grep -q "semver compliant" "${issue_2097_tmp_dir}/template-version-url.yaml"; then
cat "${issue_2097_tmp_dir}/template-version-url.yaml"
cleanup_registry_2097
fail "Issue #2097: Version in chart URL triggered validation error"
fi
if [ $code -ne 0 ]; then
cat "${issue_2097_tmp_dir}/template-version-url.yaml"
cleanup_registry_2097
fail "Issue #2097: Version in chart URL failed"
fi
info "SUCCESS: Version in chart URL works"
if grep -q "Hello from test chart 1.0.0" "${issue_2097_tmp_dir}/template-version-url.yaml"; then
info "SUCCESS: Correctly pulled version 1.0.0 from URL tag"
fi
# Test 2.5: Digest in chart URL
info "Test 2.5: Digest in chart URL"
cat > "${issue_2097_tmp_dir}/helmfile-digest-url.yaml" <<EOF
helmDefaults:
plainHttp: true
releases:
- name: test-oci-digest-url
namespace: default
chart: oci://localhost:${registry_port}/test-chart-2097@${chart_digest}
EOF
set +e
${helmfile} -f "${issue_2097_tmp_dir}/helmfile-digest-url.yaml" template --skip-deps > "${issue_2097_tmp_dir}/template-digest-url.yaml" 2>&1
code=$?
set -e
if grep -q "semver compliant" "${issue_2097_tmp_dir}/template-digest-url.yaml"; then
cat "${issue_2097_tmp_dir}/template-digest-url.yaml"
cleanup_registry_2097
fail "Issue #2097: Digest in chart URL triggered validation error"
fi
if [ $code -ne 0 ]; then
cat "${issue_2097_tmp_dir}/template-digest-url.yaml"
cleanup_registry_2097
fail "Issue #2097: Digest in chart URL failed"
fi
info "SUCCESS: Digest in chart URL works"
# Test 2.6: Digest in version field
info "Test 2.6: Digest in version field"
cat > "${issue_2097_tmp_dir}/helmfile-digest-version.yaml" <<EOF
helmDefaults:
plainHttp: true
releases:
- name: test-oci-digest-version
namespace: default
chart: oci://localhost:${registry_port}/test-chart-2097
version: "1.0.0@${chart_digest}"
EOF
set +e
${helmfile} -f "${issue_2097_tmp_dir}/helmfile-digest-version.yaml" template --skip-deps > "${issue_2097_tmp_dir}/template-digest-version.yaml" 2>&1
code=$?
set -e
if grep -q "semver compliant" "${issue_2097_tmp_dir}/template-digest-version.yaml"; then
cat "${issue_2097_tmp_dir}/template-digest-version.yaml"
cleanup_registry_2097
fail "Issue #2097: Digest in version field triggered validation error"
fi
if [ $code -ne 0 ]; then
cat "${issue_2097_tmp_dir}/template-digest-version.yaml"
cleanup_registry_2097
fail "Issue #2097: Digest in version field failed"
fi
info "SUCCESS: Digest in version field works"
if grep -q "Hello from test chart 1.0.0" "${issue_2097_tmp_dir}/template-digest-version.yaml"; then
info "SUCCESS: Correctly pulled version 1.0.0 with digest verification"
fi
# Test 2.7: Version + digest in chart URL
info "Test 2.7: Version + digest in chart URL"
cat > "${issue_2097_tmp_dir}/helmfile-both-url.yaml" <<EOF
helmDefaults:
plainHttp: true
releases:
- name: test-oci-both-url
namespace: default
chart: oci://localhost:${registry_port}/test-chart-2097:1.0.0@${chart_digest}
EOF
set +e
${helmfile} -f "${issue_2097_tmp_dir}/helmfile-both-url.yaml" template --skip-deps > "${issue_2097_tmp_dir}/template-both-url.yaml" 2>&1
code=$?
set -e
if grep -q "semver compliant" "${issue_2097_tmp_dir}/template-both-url.yaml"; then
cat "${issue_2097_tmp_dir}/template-both-url.yaml"
cleanup_registry_2097
fail "Issue #2097: Version + digest in chart URL triggered validation error"
fi
if [ $code -ne 0 ]; then
cat "${issue_2097_tmp_dir}/template-both-url.yaml"
cleanup_registry_2097
fail "Issue #2097: Version + digest in chart URL failed"
fi
info "SUCCESS: Version + digest in chart URL works"
# All tests passed!
# Remove the EXIT trap to avoid interfering with subsequent tests
cleanup_registry_2097
trap - EXIT
test_pass "issue-2097: OCI chart digest support (all tests including registry)"

View File

@ -0,0 +1,6 @@
apiVersion: v2
name: test-chart-2097
description: Test chart for issue #2097
type: application
version: 1.0.0
appVersion: "1.0"

View File

@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Chart.Name }}-config
labels:
app: {{ .Chart.Name }}
data:
message: "Hello from test chart {{ .Chart.Version }}"

View File

@ -0,0 +1,3 @@
# Default values for test-chart-2097
# This is a minimal chart for testing OCI digest handling
replicaCount: 1

View File

@ -0,0 +1,4 @@
releases:
- name: test-oci-digest-in-url
namespace: default
chart: oci://registry.example.com/my-chart@sha256:abc123def456

View File

@ -0,0 +1,5 @@
releases:
- name: test-oci-digest-version
namespace: default
chart: oci://registry.example.com/my-chart
version: "2.0.0@sha256:abc123def456"

View File

@ -0,0 +1,4 @@
releases:
- name: test-oci-version-in-url
namespace: default
chart: oci://registry.example.com/my-chart:2.0.0