From 666fa05c28b7dc2d62a339c1675ac904ee9568d3 Mon Sep 17 00:00:00 2001 From: iammehrabsandhu Date: Wed, 8 Apr 2026 11:26:20 +0530 Subject: [PATCH 1/3] WIP: feat: add ChartSource type and Source field to Release Signed-off-by: iammehrabsandhu --- internal/release/v2/release.go | 3 +++ pkg/release/v1/release.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/internal/release/v2/release.go b/internal/release/v2/release.go index 8b8f2ee07..e0cc402d2 100644 --- a/internal/release/v2/release.go +++ b/internal/release/v2/release.go @@ -51,6 +51,9 @@ type Release struct { // ApplyMethod stores whether server-side or client-side apply was used for the release // Unset (empty string) should be treated as the default of client-side apply ApplyMethod string `json:"apply_method,omitempty"` // "ssa" | "csa" + // Source records where the chart was fetched from. + // Nil for releases created before this field was added. + Source *common.ChartSource `json:"source,omitempty"` } // SetStatus is a helper for setting the status on a release. diff --git a/pkg/release/v1/release.go b/pkg/release/v1/release.go index 3bbc0e4ce..9287cc874 100644 --- a/pkg/release/v1/release.go +++ b/pkg/release/v1/release.go @@ -51,6 +51,9 @@ type Release struct { // ApplyMethod stores whether server-side or client-side apply was used for the release // Unset (empty string) should be treated as the default of client-side apply ApplyMethod string `json:"apply_method,omitempty"` // "ssa" | "csa" + // Source records where the chart was fetched from. + // Nil for releases created before this field was added. + Source *common.ChartSource `json:"source,omitempty"` } // SetStatus is a helper for setting the status on a release. From d5cc5137afa9fe39937e8d186246e332e6b5364e Mon Sep 17 00:00:00 2001 From: iammehrabsandhu Date: Thu, 9 Apr 2026 10:55:14 +0530 Subject: [PATCH 2/3] feat: thread chart source provenance into Release Signed-off-by: iammehrabsandhu --- pkg/action/install.go | 12 ++++++++++++ pkg/action/upgrade.go | 1 + pkg/downloader/chart_downloader.go | 21 +++++++++++++++++++++ pkg/getter/ocigetter.go | 7 +++++++ pkg/release/common/source.go | 23 +++++++++++++++++++++++ 5 files changed, 64 insertions(+) create mode 100644 pkg/release/common/source.go diff --git a/pkg/action/install.go b/pkg/action/install.go index 50df13c05..7e38de913 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -153,6 +153,9 @@ type ChartPathOptions struct { // registryClient provides a registry client but is not added with // options from a flag registryClient *registry.Client + + // resolvedSource is populated by LocateChart with chart provenance info. + resolvedSource *rcommon.ChartSource } // NewInstall creates a new Install object with the given configuration. @@ -669,6 +672,7 @@ func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]any, label Version: 1, Labels: labels, ApplyMethod: string(determineReleaseSSApplyMethod(i.ServerSideApply)), + Source: i.resolvedSource, } return r @@ -980,6 +984,14 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( return "", err } + // Capture source provenance from downloader or fallback context + if dl.ResolvedSource != nil { + c.resolvedSource = dl.ResolvedSource + } + if c.resolvedSource == nil && c.RepoURL != "" { + c.resolvedSource = &rcommon.ChartSource{RepoURL: c.RepoURL} + } + lname, err := filepath.Abs(filename) if err != nil { return filename, err diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 0f360fe37..889415aeb 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -329,6 +329,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str Hooks: hooks, Labels: mergeCustomLabels(lastRelease.Labels, u.Labels), ApplyMethod: string(determineReleaseSSApplyMethod(serverSideApply)), + Source: u.resolvedSource, } if len(notesTxt) > 0 { diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index 9c26f925e..0334b7534 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -36,6 +36,7 @@ import ( "helm.sh/helm/v4/pkg/helmpath" "helm.sh/helm/v4/pkg/provenance" "helm.sh/helm/v4/pkg/registry" + rcommon "helm.sh/helm/v4/pkg/release/common" "helm.sh/helm/v4/pkg/repo/v1" ) @@ -85,6 +86,10 @@ type ChartDownloader struct { // Cache specifies the cache implementation to use. Cache Cache + + // ResolvedSource is populated after a successful download with the chart's + // source provenance (OCI ref and digest). + ResolvedSource *rcommon.ChartSource } // DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file. @@ -152,6 +157,14 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven if err != nil { return "", nil, err } + + // Capture source provenance via duck-typing + if u.Scheme == registry.OCIScheme { + c.ResolvedSource = &rcommon.ChartSource{RegistryRef: ref} + if dg, ok := g.(interface{ Digest() string }); ok { + c.ResolvedSource.Digest = dg.Digest() + } + } } name := filepath.Base(u.Path) @@ -267,6 +280,14 @@ func (c *ChartDownloader) DownloadToCache(ref, version string) (string, *provena return "", nil, gerr } + // Capture source provenance via duck-typing + if u.Scheme == registry.OCIScheme { + c.ResolvedSource = &rcommon.ChartSource{RegistryRef: ref} + if dg, ok := g.(interface{ Digest() string }); ok { + c.ResolvedSource.Digest = dg.Digest() + } + } + // Generate the digest if len(digest) == 0 { digest32 = sha256.Sum256(data.Bytes()) diff --git a/pkg/getter/ocigetter.go b/pkg/getter/ocigetter.go index de8643bcd..ac646c2a7 100644 --- a/pkg/getter/ocigetter.go +++ b/pkg/getter/ocigetter.go @@ -36,6 +36,7 @@ type OCIGetter struct { opts getterOptions transport *http.Transport once sync.Once + digest string } // Get performs a Get from repo.Getter and returns the body. @@ -82,6 +83,7 @@ func (g *OCIGetter) get(href string) (*bytes.Buffer, error) { if err != nil { return nil, err } + g.digest = result.Manifest.Digest if requestingProv { return bytes.NewBuffer(result.Prov.Data), nil @@ -89,6 +91,11 @@ func (g *OCIGetter) get(href string) (*bytes.Buffer, error) { return bytes.NewBuffer(result.Chart.Data), nil } +// Digest returns the OCI manifest digest from the most recent pull. +func (g *OCIGetter) Digest() string { + return g.digest +} + // NewOCIGetter constructs a valid http/https client as a Getter func NewOCIGetter(ops ...Option) (Getter, error) { var client OCIGetter diff --git a/pkg/release/common/source.go b/pkg/release/common/source.go new file mode 100644 index 000000000..afa275073 --- /dev/null +++ b/pkg/release/common/source.go @@ -0,0 +1,23 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package common + +// ChartSource records where a chart was fetched from. +type ChartSource struct { + RepoURL string `json:"repo_url,omitempty"` + RegistryRef string `json:"registry_ref,omitempty"` + Digest string `json:"digest,omitempty"` +} From c48942801e3dfc6dc808e70c6d9d1e9dbbe010ce Mon Sep 17 00:00:00 2001 From: iammehrabsandhu Date: Thu, 9 Apr 2026 20:16:34 +0530 Subject: [PATCH 3/3] fix: populate chart source provenance for cached OCI charts Addresses review feedback on ResolvedSource not being populated when OCI charts are served from the local cache. Also adds a fallback for direct oci:// references when --repo is not used. - DownloadTo: populate ResolvedSource on cache-hit path - DownloadToCache: populate ResolvedSource on cache-hit path - LocateChart: add OCI ref fallback when RepoURL is empty Signed-off-by: iammehrabsandhu --- pkg/action/install.go | 5 +++++ pkg/downloader/chart_downloader.go | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/pkg/action/install.go b/pkg/action/install.go index 7e38de913..605915324 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -991,6 +991,11 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( if c.resolvedSource == nil && c.RepoURL != "" { c.resolvedSource = &rcommon.ChartSource{RepoURL: c.RepoURL} } + // Fallback for direct OCI references (e.g. oci://registry.com/chart:tag) + // where RepoURL is empty and the downloader may not have populated it. + if c.resolvedSource == nil && registry.IsOCI(name) { + c.resolvedSource = &rcommon.ChartSource{RegistryRef: name} + } lname, err := filepath.Abs(filename) if err != nil { diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index 0334b7534..99acca228 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -150,6 +150,15 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven } } + // Populate source provenance for cached OCI charts. + // The ref and digest are already known from ResolveChartVersion. + if found && u.Scheme == registry.OCIScheme { + c.ResolvedSource = &rcommon.ChartSource{ + RegistryRef: ref, + Digest: hash, + } + } + if !found { c.Options = append(c.Options, getter.WithAcceptHeader("application/gzip,application/octet-stream")) @@ -266,6 +275,14 @@ func (c *ChartDownloader) DownloadToCache(ref, version string) (string, *provena pth, err = c.Cache.Get(digest32, CacheChart) if err == nil { slog.Debug("found chart in cache", "id", digestString) + + // Populate source provenance for cached OCI charts. + if u.Scheme == registry.OCIScheme { + c.ResolvedSource = &rcommon.ChartSource{ + RegistryRef: ref, + Digest: digestString, + } + } } } if len(digest) == 0 || err != nil {