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:
parent
2f8b9cbdfb
commit
5c43fa6465
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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 }}"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Default values for test-chart-2097
|
||||
# This is a minimal chart for testing OCI digest handling
|
||||
replicaCount: 1
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
releases:
|
||||
- name: test-oci-digest-in-url
|
||||
namespace: default
|
||||
chart: oci://registry.example.com/my-chart@sha256:abc123def456
|
||||
|
|
@ -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"
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
releases:
|
||||
- name: test-oci-version-in-url
|
||||
namespace: default
|
||||
chart: oci://registry.example.com/my-chart:2.0.0
|
||||
Loading…
Reference in New Issue