pull/31792/merge
Aleksei Krugliak 3 days ago committed by GitHub
commit 384efb8009
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -81,6 +81,9 @@ type Metadata struct {
Dependencies []*Dependency `json:"dependencies,omitempty"` Dependencies []*Dependency `json:"dependencies,omitempty"`
// Specifies the chart type: application or library // Specifies the chart type: application or library
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
// 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"`
} }
// Validate checks the metadata for known issues and sanitizes string // Validate checks the metadata for known issues and sanitizes string
@ -98,6 +101,7 @@ func (md *Metadata) Validate() error {
md.Tags = sanitizeString(md.Tags) md.Tags = sanitizeString(md.Tags)
md.AppVersion = sanitizeString(md.AppVersion) md.AppVersion = sanitizeString(md.AppVersion)
md.KubeVersion = sanitizeString(md.KubeVersion) md.KubeVersion = sanitizeString(md.KubeVersion)
md.RepoURL = sanitizeString(md.RepoURL)
for i := range md.Sources { for i := range md.Sources {
md.Sources[i] = sanitizeString(md.Sources[i]) md.Sources[i] = sanitizeString(md.Sources[i])
} }

@ -157,6 +157,19 @@ type ChartPathOptions struct {
// registryClient provides a registry client but is not added with // registryClient provides a registry client but is not added with
// options from a flag // options from a flag
registryClient *registry.Client 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. // NewInstall creates a new Install object with the given configuration.
@ -372,6 +385,18 @@ 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()) return nil, fmt.Errorf("user supplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels())
} }
// 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 u := i.ChartPathOptions.ResolvedRepoURL(); u != "" {
chrt.Metadata.RepoURL = u
} else if i.ChartPathOptions.RepoURL != "" {
chrt.Metadata.RepoURL = i.ChartPathOptions.RepoURL
}
}
rel := i.createRelease(chrt, vals, i.Labels) rel := i.createRelease(chrt, vals, i.Labels)
var manifestDoc *bytes.Buffer var manifestDoc *bytes.Buffer
@ -876,6 +901,20 @@ func urlEqual(u1, u2 *url.URL) bool {
return u1.Scheme == u2.Scheme && u1.Hostname() == u2.Hostname() && portOrDefault(u1) == portOrDefault(u2) 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. // 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. // This does not ensure that the chart is well-formed; only that the requested filename exists.
@ -891,6 +930,10 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
return "", fmt.Errorf("unable to lookup chart %q, missing registry client", name) 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) name = strings.TrimSpace(name)
version := strings.TrimSpace(c.Version) version := strings.TrimSpace(c.Version)
@ -984,6 +1027,22 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
return "", err return "", err
} }
// Record provenance information about where the chart was resolved from.
// 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) lname, err := filepath.Abs(filename)
if err != nil { if err != nil {
return filename, err return filename, err

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

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

@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: chart-with-repourl-invalid-cm
data:
repoURL: {{ .Chart.RepoURL | quote }}

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

@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: chart-with-repourl-valid-cm
data:
repoURL: {{ .Chart.RepoURL | quote }}

@ -1,4 +1,4 @@
apiVersion: v1 apiVersion: v1
name: multiplecharts-lint-chart-2 name: multiplecharts-lint-chart-2
version: "1" version: "1"
icon: "" icon: ""

@ -315,6 +315,18 @@ func (u *Upgrade) prepareUpgrade(ctx context.Context, name string, chart *chartv
u.cfg.Logger().Debug("determined release apply method", slog.Bool("server_side_apply", serverSideApply), slog.String("previous_release_apply_method", lastRelease.ApplyMethod)) 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. 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 r := u.ChartPathOptions.ResolvedRepoURL(); r != "" {
chart.Metadata.RepoURL = r
} else if u.ChartPathOptions.RepoURL != "" {
chart.Metadata.RepoURL = u.ChartPathOptions.RepoURL
}
}
// Store an upgraded release. // Store an upgraded release.
upgradedRelease := &release.Release{ upgradedRelease := &release.Release{
Name: name, Name: name,

@ -52,6 +52,9 @@ type Metadata struct {
Home string `json:"home,omitempty"` Home string `json:"home,omitempty"`
// Source is the URL to the source code of this chart // Source is the URL to the source code of this chart
Sources []string `json:"sources,omitempty"` Sources []string `json:"sources,omitempty"`
// 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. // A version string of the chart. Required.
Version string `json:"version,omitempty"` Version string `json:"version,omitempty"`
// A one-sentence description of the chart // A one-sentence description of the chart
@ -93,6 +96,7 @@ func (md *Metadata) Validate() error {
md.Name = sanitizeString(md.Name) md.Name = sanitizeString(md.Name)
md.Description = sanitizeString(md.Description) md.Description = sanitizeString(md.Description)
md.Home = sanitizeString(md.Home) md.Home = sanitizeString(md.Home)
md.RepoURL = sanitizeString(md.RepoURL)
md.Icon = sanitizeString(md.Icon) md.Icon = sanitizeString(md.Icon)
md.Condition = sanitizeString(md.Condition) md.Condition = sanitizeString(md.Condition)
md.Tags = sanitizeString(md.Tags) md.Tags = sanitizeString(md.Tags)

@ -85,6 +85,15 @@ type ChartDownloader struct {
// Cache specifies the cache implementation to use. // Cache specifies the cache implementation to use.
Cache Cache 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. // DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file.
@ -363,6 +372,11 @@ func (c *ChartDownloader) DownloadToCache(ref, version string) (string, *provena
// //
// TODO: support OCI hash // TODO: support OCI hash
func (c *ChartDownloader) ResolveChartVersion(ref, version string) (string, *url.URL, error) { 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) u, err := url.Parse(ref)
if err != nil { if err != nil {
return "", nil, fmt.Errorf("invalid chart URL format: %s", ref) return "", nil, fmt.Errorf("invalid chart URL format: %s", ref)
@ -403,6 +417,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 // 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. // 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 = append(
c.Options, c.Options,
getter.WithURL(rc.URL), getter.WithURL(rc.URL),
@ -443,6 +460,9 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (string, *url
} }
if r != nil && r.Config != nil { 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 != "" { 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)) c.Options = append(c.Options, getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile))
} }

Loading…
Cancel
Save