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 {