From 1f52a0e0e6f4da74e295271269c3965acbeab3c8 Mon Sep 17 00:00:00 2001 From: Luke Reed Date: Wed, 17 Nov 2021 18:08:13 -0500 Subject: [PATCH 1/4] populate chart metadata with a repoURL field Signed-off-by: Luke Reed support OCI registries as well Signed-off-by: Luke Reed revert changes to DownloadTo function and implement using the ChartDownloader object instead Signed-off-by: Luke Reed make sure update will still update repoURL Signed-off-by: Luke Reed Signed-off-by: Andy Suderman Set the repository URL to path if a URL is not found Signed-off-by: Andy Suderman Fixes Signed-off-by: Andy Suderman Signed-off-by: Aleksei Krugliak --- internal/chart/v3/metadata.go | 3 +++ pkg/action/install.go | 1 + pkg/cmd/install.go | 6 ++++++ pkg/cmd/upgrade.go | 5 +++++ pkg/downloader/chart_downloader.go | 13 ++++++------- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/internal/chart/v3/metadata.go b/internal/chart/v3/metadata.go index 4629d571b..30f433a29 100644 --- a/internal/chart/v3/metadata.go +++ b/internal/chart/v3/metadata.go @@ -81,6 +81,8 @@ type Metadata struct { Dependencies []*Dependency `json:"dependencies,omitempty"` // Specifies the chart type: application or library Type string `json:"type,omitempty"` + // Specifies the chart URL that was used to initially install a chart. + RepoURL string `json:"repoURL,omitempty"` } // Validate checks the metadata for known issues and sanitizes string @@ -98,6 +100,7 @@ func (md *Metadata) Validate() error { md.Tags = sanitizeString(md.Tags) md.AppVersion = sanitizeString(md.AppVersion) md.KubeVersion = sanitizeString(md.KubeVersion) + md.RepoURL = sanitizeString(md.RepoURL) for i := range md.Sources { md.Sources[i] = sanitizeString(md.Sources[i]) } diff --git a/pkg/action/install.go b/pkg/action/install.go index 0fe1f1a6e..776501a64 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -964,6 +964,7 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( if err != nil { return "", err } + c.RepoURL = dl.RepositoryURL lname, err := filepath.Abs(filename) if err != nil { diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index d36cd9e34..6f1576b48 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -262,6 +262,12 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options return nil, err } + if client.ChartPathOptions.RepoURL != "" { + chartRequested.Metadata.RepoURL = client.ChartPathOptions.RepoURL + } else { + chartRequested.Metadata.RepoURL = "path" + } + ac, err := chart.NewAccessor(chartRequested) if err != nil { return nil, err diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index 918d6f5b8..821cb49c1 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -197,6 +197,11 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if err != nil { return err } + if client.ChartPathOptions.RepoURL != "" { + ch.Metadata.RepoURL = client.ChartPathOptions.RepoURL + } else { + ch.Metadata.RepoURL = "path" + } ac, err := ci.NewAccessor(ch) if err != nil { diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index ee4f8abe3..c234f348c 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -84,7 +84,8 @@ type ChartDownloader struct { ContentCache string // Cache specifies the cache implementation to use. - Cache Cache + Cache Cache + RepositoryURL string } // DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file. @@ -356,12 +357,7 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (string, *url } if registry.IsOCI(u.String()) { - if c.RegistryClient == nil { - return "", nil, fmt.Errorf("unable to lookup ref %s at version '%s', missing registry client", ref, version) - } - - digest, OCIref, err := c.RegistryClient.ValidateReference(ref, version, u) - return digest, OCIref, err + return c.getOciURI(ref, version, u) } rf, err := loadRepoConfig(c.RepositoryConfig) @@ -430,6 +426,9 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (string, *url } if r != nil && r.Config != nil { + if r.Config.URL != "" { + c.RepositoryURL = r.Config.URL + } if r.Config.CertFile != "" || r.Config.KeyFile != "" || r.Config.CAFile != "" { c.Options = append(c.Options, getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile)) } From 31132a16fe722535fd593371f66c0e012934de33 Mon Sep 17 00:00:00 2001 From: Aleksei Krugliak Date: Thu, 5 Feb 2026 19:39:22 +0200 Subject: [PATCH 2/4] Fix accidentaly deleted code Signed-off-by: Aleksei Krugliak --- pkg/downloader/chart_downloader.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index c234f348c..73ade62c3 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -357,7 +357,12 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (string, *url } if registry.IsOCI(u.String()) { - return c.getOciURI(ref, version, u) + if c.RegistryClient == nil { + return "", nil, fmt.Errorf("unable to lookup ref %s at version '%s', missing registry client", ref, version) + } + + digest, OCIref, err := c.RegistryClient.ValidateReference(ref, version, u) + return digest, OCIref, err } rf, err := loadRepoConfig(c.RepositoryConfig) From 97e54c8f11195e21b479606ae762e7c36282f0c2 Mon Sep 17 00:00:00 2001 From: Aleksei Krugliak Date: Mon, 11 May 2026 21:19:12 +0300 Subject: [PATCH 3/4] fix: address review on repoURL provenance stamping - Stamp chart.Metadata.RepoURL in Install/Upgrade actions instead of pkg/cmd, so library consumers also get provenance recorded. - Don't mislabel local installs as path; fill RepoURL only for resolved repos, OCI refs, and absolute http(s) URLs. - Unexport ChartDownloader.RepositoryURL; expose via accessor to preserve ABI for downstream unkeyed-struct users. - Fix render test template to use .Chart.RepoURL. Signed-off-by: Aleksei Krugliak --- pkg/action/install.go | 35 ++++++++++- pkg/action/repourl2_test.go | 62 +++++++++++++++++++ .../chart-with-repourl-invalid/Chart.yaml | 5 ++ .../templates/configmap.yaml | 6 ++ .../chart-with-repourl-valid/Chart.yaml | 5 ++ .../templates/configmap.yaml | 6 ++ .../multiplecharts-lint-chart-2/Chart.yaml | 2 +- pkg/action/upgrade.go | 9 +++ pkg/chart/v2/metadata.go | 3 + pkg/cmd/install.go | 6 -- pkg/cmd/upgrade.go | 5 -- pkg/downloader/chart_downloader.go | 14 ++++- 12 files changed, 142 insertions(+), 16 deletions(-) create mode 100644 pkg/action/repourl2_test.go create mode 100644 pkg/action/testdata/charts/chart-with-repourl-invalid/Chart.yaml create mode 100644 pkg/action/testdata/charts/chart-with-repourl-invalid/templates/configmap.yaml create mode 100644 pkg/action/testdata/charts/chart-with-repourl-valid/Chart.yaml create mode 100644 pkg/action/testdata/charts/chart-with-repourl-valid/templates/configmap.yaml diff --git a/pkg/action/install.go b/pkg/action/install.go index 776501a64..567f8a382 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -352,6 +352,16 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st return nil, fmt.Errorf("user supplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels()) } + // Stamp provenance information into the chart metadata if available from + // the ChartPathOptions. Do not overwrite existing metadata.RepoURL set by + // the chart itself. This ensures library consumers who call actions get + // provenance filled in when LocateChart or a downloader discovered it. + if chrt.Metadata != nil && chrt.Metadata.RepoURL == "" { + if i.ChartPathOptions.RepoURL != "" { + chrt.Metadata.RepoURL = i.ChartPathOptions.RepoURL + } + } + rel := i.createRelease(chrt, vals, i.Labels) var manifestDoc *bytes.Buffer @@ -857,6 +867,20 @@ func urlEqual(u1, u2 *url.URL) bool { return u1.Scheme == u2.Scheme && u1.Hostname() == u2.Hostname() && portOrDefault(u1) == portOrDefault(u2) } +// isRemoteChartRef reports whether ref points to a remote chart (an OCI +// reference or an absolute http(s) URL), as opposed to a local path or a +// repo-by-name reference. +func isRemoteChartRef(ref string) bool { + if registry.IsOCI(ref) { + return true + } + u, err := url.Parse(ref) + if err != nil { + return false + } + return u.IsAbs() && (u.Scheme == "http" || u.Scheme == "https") +} + // LocateChart looks for a chart directory in known places, and returns either the full path or an error. // // This does not ensure that the chart is well-formed; only that the requested filename exists. @@ -964,7 +988,16 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( if err != nil { return "", err } - c.RepoURL = dl.RepositoryURL + + // Record provenance information about where the chart was resolved from. + // Prefer the URL discovered by the downloader; fall back to the original + // reference when it is an OCI ref or an absolute URL. Leave RepoURL empty + // for local paths and unresolvable references so callers can distinguish. + if u := dl.RepositoryURL(); u != "" { + c.RepoURL = u + } else if c.RepoURL == "" && isRemoteChartRef(name) { + c.RepoURL = name + } lname, err := filepath.Abs(filename) if err != nil { diff --git a/pkg/action/repourl2_test.go b/pkg/action/repourl2_test.go new file mode 100644 index 000000000..676999e88 --- /dev/null +++ b/pkg/action/repourl2_test.go @@ -0,0 +1,62 @@ +package action + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "helm.sh/helm/v4/pkg/chart/v2/loader" + chartutil "helm.sh/helm/v4/pkg/chart/v2/util" +) + +func TestRenderRepoURL2_ValidAndInvalid(t *testing.T) { + cfg := actionConfigFixture(t) + + tests := []struct { + name string + chartPath string + expectPart string + }{ + { + name: "valid repoURL", + chartPath: "testdata/charts/chart-with-repourl-valid", + expectPart: `repoURL: "https://example.com/charts"`, + }, + { + name: "invalid repoURL", + chartPath: "testdata/charts/chart-with-repourl-invalid", + expectPart: `repoURL: "ht!tp://not-a-valid-url"`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ch, err := loader.Load(tt.chartPath) + require.NoError(t, err) + if ch.Metadata == nil { + md, err := chartutil.LoadChartfile(filepath.Join(tt.chartPath, "Chart.yaml")) + require.NoError(t, err) + ch.Metadata = md + } + + _, buf, _, err := cfg.renderResources( + ch, + map[string]interface{}{}, + "test-release", + "", + false, + false, + false, + nil, + false, + false, + false, + ) + require.NoError(t, err) + require.NotNil(t, buf) + assert.Contains(t, buf.String(), tt.expectPart) + }) + } +} diff --git a/pkg/action/testdata/charts/chart-with-repourl-invalid/Chart.yaml b/pkg/action/testdata/charts/chart-with-repourl-invalid/Chart.yaml new file mode 100644 index 000000000..89a08d5ef --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-repourl-invalid/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: chart-with-repourl-invalid +version: 0.1.0 +description: Test chart containing an invalid repoURL in metadata +repoURL: "ht!tp://not-a-valid-url" diff --git a/pkg/action/testdata/charts/chart-with-repourl-invalid/templates/configmap.yaml b/pkg/action/testdata/charts/chart-with-repourl-invalid/templates/configmap.yaml new file mode 100644 index 000000000..ec74e9cb4 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-repourl-invalid/templates/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: chart-with-repourl-invalid-cm +data: + repoURL: {{ .Chart.RepoURL | quote }} diff --git a/pkg/action/testdata/charts/chart-with-repourl-valid/Chart.yaml b/pkg/action/testdata/charts/chart-with-repourl-valid/Chart.yaml new file mode 100644 index 000000000..7f4d9e0bf --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-repourl-valid/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: chart-with-repourl-valid +version: 0.1.0 +description: Test chart containing a valid repoURL in metadata +repoURL: https://example.com/charts diff --git a/pkg/action/testdata/charts/chart-with-repourl-valid/templates/configmap.yaml b/pkg/action/testdata/charts/chart-with-repourl-valid/templates/configmap.yaml new file mode 100644 index 000000000..2629a1021 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-repourl-valid/templates/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: chart-with-repourl-valid-cm +data: + repoURL: {{ .Chart.RepoURL | quote }} diff --git a/pkg/action/testdata/charts/multiplecharts-lint-chart-2/Chart.yaml b/pkg/action/testdata/charts/multiplecharts-lint-chart-2/Chart.yaml index b27de2754..7f348df6a 100644 --- a/pkg/action/testdata/charts/multiplecharts-lint-chart-2/Chart.yaml +++ b/pkg/action/testdata/charts/multiplecharts-lint-chart-2/Chart.yaml @@ -1,4 +1,4 @@ apiVersion: v1 name: multiplecharts-lint-chart-2 version: "1" -icon: "" \ No newline at end of file +icon: "" diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 4c93855b1..5df7438ac 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -312,6 +312,15 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str u.cfg.Logger().Debug("determined release apply method", slog.Bool("server_side_apply", serverSideApply), slog.String("previous_release_apply_method", lastRelease.ApplyMethod)) + // Stamp provenance information into the chart metadata if available from + // the ChartPathOptions. Only set it when chart metadata doesn't already + // contain a RepoURL so we don't overwrite embedded provenance. + if chart.Metadata != nil && chart.Metadata.RepoURL == "" { + if u.ChartPathOptions.RepoURL != "" { + chart.Metadata.RepoURL = u.ChartPathOptions.RepoURL + } + } + // Store an upgraded release. upgradedRelease := &release.Release{ Name: name, diff --git a/pkg/chart/v2/metadata.go b/pkg/chart/v2/metadata.go index c46007863..fbf19cda2 100644 --- a/pkg/chart/v2/metadata.go +++ b/pkg/chart/v2/metadata.go @@ -52,6 +52,8 @@ type Metadata struct { Home string `json:"home,omitempty"` // Source is the URL to the source code of this chart Sources []string `json:"sources,omitempty"` + // The URL to the chart's repository (provenance information) + RepoURL string `json:"repoURL,omitempty"` // A version string of the chart. Required. Version string `json:"version,omitempty"` // A one-sentence description of the chart @@ -93,6 +95,7 @@ func (md *Metadata) Validate() error { md.Name = sanitizeString(md.Name) md.Description = sanitizeString(md.Description) md.Home = sanitizeString(md.Home) + md.RepoURL = sanitizeString(md.RepoURL) md.Icon = sanitizeString(md.Icon) md.Condition = sanitizeString(md.Condition) md.Tags = sanitizeString(md.Tags) diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index 6f1576b48..d36cd9e34 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -262,12 +262,6 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options return nil, err } - if client.ChartPathOptions.RepoURL != "" { - chartRequested.Metadata.RepoURL = client.ChartPathOptions.RepoURL - } else { - chartRequested.Metadata.RepoURL = "path" - } - ac, err := chart.NewAccessor(chartRequested) if err != nil { return nil, err diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index 821cb49c1..918d6f5b8 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -197,11 +197,6 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if err != nil { return err } - if client.ChartPathOptions.RepoURL != "" { - ch.Metadata.RepoURL = client.ChartPathOptions.RepoURL - } else { - ch.Metadata.RepoURL = "path" - } ac, err := ci.NewAccessor(ch) if err != nil { diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index 73ade62c3..033efc4af 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -84,8 +84,16 @@ type ChartDownloader struct { ContentCache string // Cache specifies the cache implementation to use. - Cache Cache - RepositoryURL string + Cache Cache + + // repositoryURL is the resolved Helm repository URL, set by + // ResolveChartVersion when the chart is looked up via a configured repo. + repositoryURL string +} + +// RepositoryURL returns the resolved Helm repository URL, if any. +func (c *ChartDownloader) RepositoryURL() string { + return c.repositoryURL } // DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file. @@ -432,7 +440,7 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (string, *url if r != nil && r.Config != nil { if r.Config.URL != "" { - c.RepositoryURL = r.Config.URL + c.repositoryURL = r.Config.URL } if r.Config.CertFile != "" || r.Config.KeyFile != "" || r.Config.CAFile != "" { c.Options = append(c.Options, getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile)) From 99a52b1d4643813aa422bd9c651edfe3e538fcf1 Mon Sep 17 00:00:00 2001 From: Aleksei Krugliak Date: Mon, 11 May 2026 21:49:04 +0300 Subject: [PATCH 4/4] fix: decouple provenance from --repo input; reset downloader state - ChartDownloader.repositoryURL is now cleared at the start of ResolveChartVersion and set in the absolute-URL branch too, so reused downloaders don't report stale URLs. - New ChartPathOptions.ResolvedRepoURL() carries provenance from LocateChart so it no longer overwrites the --repo input field. Install/Upgrade actions stamp metadata.RepoURL from it first, falling back to RepoURL for consumers that bypass LocateChart. - Align RepoURL doc strings in chart v2 and v3 metadata. Signed-off-by: Aleksei Krugliak --- internal/chart/v3/metadata.go | 3 +- pkg/action/install.go | 49 ++++++++++++++++++++++-------- pkg/action/upgrade.go | 11 ++++--- pkg/chart/v2/metadata.go | 3 +- pkg/downloader/chart_downloader.go | 8 +++++ 5 files changed, 56 insertions(+), 18 deletions(-) diff --git a/internal/chart/v3/metadata.go b/internal/chart/v3/metadata.go index 30f433a29..6add6b6d6 100644 --- a/internal/chart/v3/metadata.go +++ b/internal/chart/v3/metadata.go @@ -81,7 +81,8 @@ type Metadata struct { Dependencies []*Dependency `json:"dependencies,omitempty"` // Specifies the chart type: application or library Type string `json:"type,omitempty"` - // Specifies the chart URL that was used to initially install a chart. + // The URL from which the chart was obtained (a Helm repository URL, + // an OCI reference, or a direct artifact URL). Used to record provenance. RepoURL string `json:"repoURL,omitempty"` } diff --git a/pkg/action/install.go b/pkg/action/install.go index 567f8a382..2879bdc8a 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -153,6 +153,19 @@ type ChartPathOptions struct { // registryClient provides a registry client but is not added with // options from a flag registryClient *registry.Client + + // resolvedRepoURL is the source URL from which the chart was obtained + // during the last LocateChart call. It is independent of RepoURL + // (the --repo input) and is used to record provenance into chart metadata. + resolvedRepoURL string +} + +// ResolvedRepoURL returns the source URL from which the chart was obtained +// during the last LocateChart call (a Helm repository URL, an OCI reference, +// or a direct artifact URL). It is empty for local-path installs or when +// LocateChart has not been called. +func (c *ChartPathOptions) ResolvedRepoURL() string { + return c.resolvedRepoURL } // NewInstall creates a new Install object with the given configuration. @@ -352,12 +365,14 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st return nil, fmt.Errorf("user supplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels()) } - // Stamp provenance information into the chart metadata if available from - // the ChartPathOptions. Do not overwrite existing metadata.RepoURL set by - // the chart itself. This ensures library consumers who call actions get - // provenance filled in when LocateChart or a downloader discovered it. + // Stamp provenance information into the chart metadata. Prefer the URL + // discovered by LocateChart (ResolvedRepoURL) and fall back to the --repo + // input for library consumers who set it without calling LocateChart. + // Do not overwrite a value already set in Chart.yaml. if chrt.Metadata != nil && chrt.Metadata.RepoURL == "" { - if i.ChartPathOptions.RepoURL != "" { + if u := i.ChartPathOptions.ResolvedRepoURL(); u != "" { + chrt.Metadata.RepoURL = u + } else if i.ChartPathOptions.RepoURL != "" { chrt.Metadata.RepoURL = i.ChartPathOptions.RepoURL } } @@ -896,6 +911,10 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( return "", fmt.Errorf("unable to lookup chart %q, missing registry client", name) } + // Reset any provenance recorded by a previous call so callers that reuse + // this instance do not observe stale values from an earlier chart. + c.resolvedRepoURL = "" + name = strings.TrimSpace(name) version := strings.TrimSpace(c.Version) @@ -990,13 +1009,19 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( } // Record provenance information about where the chart was resolved from. - // Prefer the URL discovered by the downloader; fall back to the original - // reference when it is an OCI ref or an absolute URL. Leave RepoURL empty - // for local paths and unresolvable references so callers can distinguish. - if u := dl.RepositoryURL(); u != "" { - c.RepoURL = u - } else if c.RepoURL == "" && isRemoteChartRef(name) { - c.RepoURL = name + // Prefer the URL discovered by the downloader, then the --repo input, + // then the original reference when it is an OCI ref or an absolute URL. + // Local paths and unresolvable references leave resolvedRepoURL empty so + // callers can distinguish them. We deliberately do not mutate RepoURL, + // because it is also the --repo flag input and controls local-path vs + // remote resolution on subsequent calls. + switch { + case dl.RepositoryURL() != "": + c.resolvedRepoURL = dl.RepositoryURL() + case c.RepoURL != "": + c.resolvedRepoURL = c.RepoURL + case isRemoteChartRef(name): + c.resolvedRepoURL = name } lname, err := filepath.Abs(filename) diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 5df7438ac..66426d973 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -312,11 +312,14 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str u.cfg.Logger().Debug("determined release apply method", slog.Bool("server_side_apply", serverSideApply), slog.String("previous_release_apply_method", lastRelease.ApplyMethod)) - // Stamp provenance information into the chart metadata if available from - // the ChartPathOptions. Only set it when chart metadata doesn't already - // contain a RepoURL so we don't overwrite embedded provenance. + // Stamp provenance information into the chart metadata. Prefer the URL + // discovered by LocateChart (ResolvedRepoURL) and fall back to the --repo + // input for library consumers who set it without calling LocateChart. + // Do not overwrite a value already set in Chart.yaml. if chart.Metadata != nil && chart.Metadata.RepoURL == "" { - if u.ChartPathOptions.RepoURL != "" { + if r := u.ChartPathOptions.ResolvedRepoURL(); r != "" { + chart.Metadata.RepoURL = r + } else if u.ChartPathOptions.RepoURL != "" { chart.Metadata.RepoURL = u.ChartPathOptions.RepoURL } } diff --git a/pkg/chart/v2/metadata.go b/pkg/chart/v2/metadata.go index fbf19cda2..be8608359 100644 --- a/pkg/chart/v2/metadata.go +++ b/pkg/chart/v2/metadata.go @@ -52,7 +52,8 @@ type Metadata struct { Home string `json:"home,omitempty"` // Source is the URL to the source code of this chart Sources []string `json:"sources,omitempty"` - // The URL to the chart's repository (provenance information) + // The URL from which the chart was obtained (a Helm repository URL, + // an OCI reference, or a direct artifact URL). Used to record provenance. RepoURL string `json:"repoURL,omitempty"` // A version string of the chart. Required. Version string `json:"version,omitempty"` diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index 033efc4af..247a20a62 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -359,6 +359,11 @@ func (c *ChartDownloader) DownloadToCache(ref, version string) (string, *provena // // TODO: support OCI hash func (c *ChartDownloader) ResolveChartVersion(ref, version string) (string, *url.URL, error) { + // Clear any URL recorded by a previous call so RepositoryURL() does not + // report stale provenance when the same ChartDownloader instance is + // reused for a chart that does not resolve to a configured repo. + c.repositoryURL = "" + u, err := url.Parse(ref) if err != nil { return "", nil, fmt.Errorf("invalid chart URL format: %s", ref) @@ -399,6 +404,9 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (string, *url // If we get here, we don't need to go through the next phase of looking // up the URL. We have it already. So we just set the parameters and return. + if rc.URL != "" { + c.repositoryURL = rc.URL + } c.Options = append( c.Options, getter.WithURL(rc.URL),