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 <shaansatsangi@gmail.com>
pull/32250/head
Shaan Satsangi 3 weeks ago
parent 57667c932c
commit 7f8dc21813

@ -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'.

@ -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")

@ -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

@ -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())
}

@ -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)

@ -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

@ -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 {

Loading…
Cancel
Save