From 7f8dc218137be16f36b04ece649aa06cd64444c8 Mon Sep 17 00:00:00 2001 From: Shaan Satsangi Date: Tue, 23 Jun 2026 04:59:39 +0530 Subject: [PATCH] fix(action): apply release-source annotation after rendering Previously the chart source was recorded by mutating the chart's metadata annotations in pkg/cmd before calling RunWithContext. Because the render values expose chart metadata to templates (.Chart comes from accessor.MetadataAsMap()), this injected meta.helm.sh/release-source into .Chart.Annotations during template rendering, which could change rendered manifests for charts that enumerate or branch on chart annotations. Move the annotation to the action layer and apply it only after rendering completes, via a new Install.ChartSource / Upgrade.ChartSource field. The annotation lands on the persisted release's chart metadata (the same chart pointer stored on the release) but is never present in the template render context, so 'helm list --show-source' and 'helm get metadata' still surface the source while rendered output is unaffected. The cmd helper is reduced to a pure chartSource() resolver (repo URL preferred, credentials stripped); sanitizeChartSource is unchanged. Adds an action-level test asserting the source is absent from the rendered manifest but present on the stored release. Signed-off-by: Shaan Satsangi --- pkg/action/get_metadata.go | 18 ++++++++++++++++ pkg/action/install.go | 13 ++++++++++++ pkg/action/install_test.go | 31 ++++++++++++++++++++++++++++ pkg/action/upgrade.go | 14 +++++++++++++ pkg/cmd/install.go | 7 ++++--- pkg/cmd/release_source.go | 42 ++++++++------------------------------ pkg/cmd/upgrade.go | 7 ++++--- 7 files changed, 92 insertions(+), 40 deletions(-) diff --git a/pkg/action/get_metadata.go b/pkg/action/get_metadata.go index 3754811eb..cc5705143 100644 --- a/pkg/action/get_metadata.go +++ b/pkg/action/get_metadata.go @@ -33,6 +33,24 @@ import ( // can be surfaced by 'helm list --show-source' and 'helm get metadata'. const ReleaseSourceAnnotation = "meta.helm.sh/release-source" +// setChartSourceAnnotation records source on the chart's metadata under +// ReleaseSourceAnnotation. It is a no-op when source is empty. Callers must +// invoke it only after render values have been computed/rendered, so the +// annotation is persisted on the stored release without leaking into the +// template render context via .Chart.Annotations. +func setChartSourceAnnotation(chrt *chart.Chart, source string) { + if source == "" { + return + } + if chrt.Metadata == nil { + chrt.Metadata = &chart.Metadata{} + } + if chrt.Metadata.Annotations == nil { + chrt.Metadata.Annotations = make(map[string]string) + } + chrt.Metadata.Annotations[ReleaseSourceAnnotation] = source +} + // GetMetadata is the action for checking a given release's metadata. // // It provides the implementation of 'helm get metadata'. diff --git a/pkg/action/install.go b/pkg/action/install.go index fcfae7214..457292d23 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -76,6 +76,13 @@ type Install struct { ChartPathOptions + // ChartSource records where the chart was obtained from (a repository URL or + // a local chart reference). When non-empty it is persisted on the release's + // chart metadata under ReleaseSourceAnnotation after rendering, so it is + // surfaced by 'helm list --show-source' and 'helm get metadata' without ever + // being exposed to templates via .Chart.Annotations. + ChartSource string + // ForceReplace will, if set to `true`, ignore certain warnings and perform the install anyway. // // This should be used with caution. @@ -387,6 +394,12 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st return rel, err } + // Record the chart source on the persisted release. This is done after + // rendering so the annotation is not visible to templates via + // .Chart.Annotations, while still being stored on rel.Chart (which shares + // the same chart pointer) for 'helm list'/'helm get metadata'. + setChartSourceAnnotation(chrt, i.ChartSource) + // Mark this release as in-progress rel.SetStatus(rcommon.StatusPendingInstall, "Initial install underway") diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 705a8e11d..723600ffa 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -220,6 +220,37 @@ func TestInstallRelease(t *testing.T) { is.Equal(lrel.Info.Status, rcommon.StatusDeployed) } +func TestInstallReleaseRecordsChartSourceAfterRender(t *testing.T) { + is := assert.New(t) + req := require.New(t) + + instAction := installAction(t) + instAction.ChartSource = "https://charts.example.com/repo" + + // This template renders the chart's annotations. If the source were recorded + // before rendering, it would leak into the manifest via .Chart.Annotations. + tmpl := []*common.File{ + {Name: "templates/cfg", ModTime: time.Now(), Data: []byte("annotations: {{ .Chart.Annotations }}")}, + } + resi, err := instAction.RunWithContext(t.Context(), buildChartWithTemplates(tmpl), map[string]any{}) + req.NoError(err) + res, err := releaserToV1Release(resi) + req.NoError(err) + + // The rendered manifest must not contain the recorded source: the annotation + // is applied only after rendering, so it never reaches .Chart.Annotations. + is.NotContains(res.Manifest, "https://charts.example.com/repo") + is.NotContains(res.Manifest, ReleaseSourceAnnotation) + + // The persisted release, however, must carry the source annotation so it can + // be surfaced by 'helm list --show-source' and 'helm get metadata'. + stored, err := instAction.cfg.Releases.Get(res.Name, res.Version) + req.NoError(err) + rel, err := releaserToV1Release(stored) + req.NoError(err) + is.Equal("https://charts.example.com/repo", rel.Chart.Metadata.Annotations[ReleaseSourceAnnotation]) +} + func TestInstallReleaseWithTakeOwnership_ResourceNotOwned(t *testing.T) { // This test will test checking ownership of a resource // returned by the fake client. If the resource is not diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 81d51164f..e2c013261 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -52,6 +52,13 @@ type Upgrade struct { ChartPathOptions + // ChartSource records where the chart was obtained from (a repository URL or + // a local chart reference). When non-empty it is persisted on the upgraded + // release's chart metadata under ReleaseSourceAnnotation after rendering, so + // it is surfaced by 'helm list --show-source' and 'helm get metadata' + // without ever being exposed to templates via .Chart.Annotations. + ChartSource string + // Install is a purely informative flag that indicates whether this upgrade was done in "install" mode. // // Applications may use this to determine whether this Upgrade operation was done as part of a @@ -305,6 +312,13 @@ func (u *Upgrade) prepareUpgrade(ctx context.Context, name string, chart *chartv return nil, nil, false, err } + // Record the chart source on the persisted release. This is done after + // rendering so the annotation is not visible to templates via + // .Chart.Annotations, while still being stored on the upgraded release's + // chart (which shares the same chart pointer) for 'helm list'/'helm get + // metadata'. + setChartSourceAnnotation(chart, u.ChartSource) + if driver.ContainsSystemLabels(u.Labels) { return nil, nil, false, fmt.Errorf("user supplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels()) } diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index 801527f91..e2a350d03 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -345,9 +345,10 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options }() // Record where the chart was installed from so 'helm list --show-source' can - // surface it later. This is done after any dependency-update reload above so - // the annotation survives the chart being reloaded from disk. - setReleaseSource(chartRequested, chartRef, client.RepoURL) + // surface it later. The action applies this after rendering, so it is never + // exposed to templates via .Chart.Annotations. Resolved after any + // dependency-update reload above so it reflects the chart actually installed. + client.ChartSource = chartSource(chartRef, client.RepoURL) ri, err := client.RunWithContext(ctx, chartRequested, vals) rel, rerr := releaserToV1Release(ri) diff --git a/pkg/cmd/release_source.go b/pkg/cmd/release_source.go index 0bd00df03..a934fa0a4 100644 --- a/pkg/cmd/release_source.go +++ b/pkg/cmd/release_source.go @@ -18,46 +18,20 @@ package cmd import ( "net/url" - - v3chart "helm.sh/helm/v4/internal/chart/v3" - "helm.sh/helm/v4/pkg/action" - "helm.sh/helm/v4/pkg/chart" - v2chart "helm.sh/helm/v4/pkg/chart/v2" ) -// setReleaseSource records, on the chart's metadata, the location the chart was -// installed or upgraded from so it can be surfaced by 'helm list --show-source'. -// A repository URL (when supplied) is preferred over the local chart reference, -// and any embedded credentials are stripped before the value is persisted. -// -// It accepts the loader's chart.Charter (which may be either a v2 or v3 chart) -// and is a no-op for any unrecognized chart type, so callers never need to -// reason about the concrete chart version. -func setReleaseSource(chrt chart.Charter, chartRef, repoURL string) { +// chartSource resolves the location a chart was installed or upgraded from, for +// persistence under action.ReleaseSourceAnnotation. A repository URL (when +// supplied) is preferred over the local chart reference, and any embedded +// credentials are stripped before the value is returned. The result is handed +// to the install/upgrade action, which records it on the release after +// rendering so it is never exposed to templates via .Chart.Annotations. +func chartSource(chartRef, repoURL string) string { source := chartRef if repoURL != "" { source = repoURL } - source = sanitizeChartSource(source) - - switch c := chrt.(type) { - case *v2chart.Chart: - if c.Metadata == nil { - c.Metadata = &v2chart.Metadata{} - } - if c.Metadata.Annotations == nil { - c.Metadata.Annotations = make(map[string]string) - } - c.Metadata.Annotations[action.ReleaseSourceAnnotation] = source - case *v3chart.Chart: - if c.Metadata == nil { - c.Metadata = &v3chart.Metadata{} - } - if c.Metadata.Annotations == nil { - c.Metadata.Annotations = make(map[string]string) - } - c.Metadata.Annotations[action.ReleaseSourceAnnotation] = source - } + return sanitizeChartSource(source) } // sanitizeChartSource strips any user credentials and other potentially diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index 6afe01666..b3efbc81c 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -251,9 +251,10 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { }() // Record where the chart was upgraded from so 'helm list --show-source' - // can surface it later. This is done after any dependency-update reload - // above so the annotation survives the chart being reloaded from disk. - setReleaseSource(ch, args[1], client.RepoURL) + // can surface it later. The action applies this after rendering, so it + // is never exposed to templates via .Chart.Annotations. Resolved after + // any dependency-update reload above so it reflects the actual chart. + client.ChartSource = chartSource(args[1], client.RepoURL) rel, err := client.RunWithContext(ctx, args[0], ch, vals) if err != nil {