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 months ago
parent 57667c932c
commit 7f8dc21813

@ -33,6 +33,24 @@ import (
// can be surfaced by 'helm list --show-source' and 'helm get metadata'. // can be surfaced by 'helm list --show-source' and 'helm get metadata'.
const ReleaseSourceAnnotation = "meta.helm.sh/release-source" 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. // GetMetadata is the action for checking a given release's metadata.
// //
// It provides the implementation of 'helm get metadata'. // It provides the implementation of 'helm get metadata'.

@ -76,6 +76,13 @@ type Install struct {
ChartPathOptions 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. // ForceReplace will, if set to `true`, ignore certain warnings and perform the install anyway.
// //
// This should be used with caution. // 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 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 // Mark this release as in-progress
rel.SetStatus(rcommon.StatusPendingInstall, "Initial install underway") rel.SetStatus(rcommon.StatusPendingInstall, "Initial install underway")

@ -220,6 +220,37 @@ func TestInstallRelease(t *testing.T) {
is.Equal(lrel.Info.Status, rcommon.StatusDeployed) 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) { func TestInstallReleaseWithTakeOwnership_ResourceNotOwned(t *testing.T) {
// This test will test checking ownership of a resource // This test will test checking ownership of a resource
// returned by the fake client. If the resource is not // returned by the fake client. If the resource is not

@ -52,6 +52,13 @@ type Upgrade struct {
ChartPathOptions 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. // 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 // 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 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) { if driver.ContainsSystemLabels(u.Labels) {
return nil, nil, false, fmt.Errorf("user supplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels()) 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 // 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 // surface it later. The action applies this after rendering, so it is never
// the annotation survives the chart being reloaded from disk. // exposed to templates via .Chart.Annotations. Resolved after any
setReleaseSource(chartRequested, chartRef, client.RepoURL) // dependency-update reload above so it reflects the chart actually installed.
client.ChartSource = chartSource(chartRef, client.RepoURL)
ri, err := client.RunWithContext(ctx, chartRequested, vals) ri, err := client.RunWithContext(ctx, chartRequested, vals)
rel, rerr := releaserToV1Release(ri) rel, rerr := releaserToV1Release(ri)

@ -18,46 +18,20 @@ package cmd
import ( import (
"net/url" "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 // chartSource resolves the location a chart was installed or upgraded from, for
// installed or upgraded from so it can be surfaced by 'helm list --show-source'. // persistence under action.ReleaseSourceAnnotation. A repository URL (when
// A repository URL (when supplied) is preferred over the local chart reference, // supplied) is preferred over the local chart reference, and any embedded
// and any embedded credentials are stripped before the value is persisted. // credentials are stripped before the value is returned. The result is handed
// // to the install/upgrade action, which records it on the release after
// It accepts the loader's chart.Charter (which may be either a v2 or v3 chart) // rendering so it is never exposed to templates via .Chart.Annotations.
// and is a no-op for any unrecognized chart type, so callers never need to func chartSource(chartRef, repoURL string) string {
// reason about the concrete chart version.
func setReleaseSource(chrt chart.Charter, chartRef, repoURL string) {
source := chartRef source := chartRef
if repoURL != "" { if repoURL != "" {
source = repoURL source = repoURL
} }
source = sanitizeChartSource(source) return 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
}
} }
// sanitizeChartSource strips any user credentials and other potentially // 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' // Record where the chart was upgraded from so 'helm list --show-source'
// can surface it later. This is done after any dependency-update reload // can surface it later. The action applies this after rendering, so it
// above so the annotation survives the chart being reloaded from disk. // is never exposed to templates via .Chart.Annotations. Resolved after
setReleaseSource(ch, args[1], client.RepoURL) // 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) rel, err := client.RunWithContext(ctx, args[0], ch, vals)
if err != nil { if err != nil {

Loading…
Cancel
Save