Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 94 additions & 37 deletions store/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,15 @@ import (
)

const (
helmOperationTimeout = 5 * time.Minute
helmInstallTimeout = 10 * time.Minute
helmCompatibilityTimeout = 2 * time.Minute
helmDiagnosticsTimeout = 15 * time.Second
helmDiagnosticsMaxEvents = 20
helmDiagnosticsMessageLen = 240
helmDiagnosticsEventLen = 360
helmOperationTimeout = 5 * time.Minute
helmInstallTimeout = 10 * time.Minute
helmCompatibilityTimeout = 2 * time.Minute
helmChartLoadTimeout = 2 * time.Minute
helmDiagnosticsTimeout = 15 * time.Second
helmInstallOperationTimeout = helmChartLoadTimeout + helmCompatibilityTimeout + helmInstallTimeout + helmDiagnosticsTimeout
helmDiagnosticsMaxEvents = 20
helmDiagnosticsMessageLen = 240
helmDiagnosticsEventLen = 360
)

// ---------- Types ----------
Expand Down Expand Up @@ -150,8 +152,8 @@ func newHelmConfigWithLog(cfg *rest.Config, namespace string, logFn func(string,
return actionConfig, nil
}

func attachHelmCapabilities(actionConfig *action.Configuration, cfg *rest.Config, namespace string, logFn func(string, ...interface{})) {
capabilities, err := buildHelmCapabilities(cfg, namespace, logFn)
func attachHelmCapabilities(ctx context.Context, actionConfig *action.Configuration, cfg *rest.Config, logFn func(string, ...interface{})) {
capabilities, err := buildHelmCapabilities(ctx, cfg, logFn)
if err != nil {
logFn("WARNING: failed to build helm capabilities, using defaults: %v", err)
capabilities = chartutil.DefaultCapabilities
Expand All @@ -163,11 +165,19 @@ func helmWarningLog(format string, args ...interface{}) {
logrus.Warnf(format, args...)
}

func buildHelmCapabilities(cfg *rest.Config, namespace string, logFn func(string, ...interface{})) (*chartutil.Capabilities, error) {
dc, err := newRESTClientGetter(cfg, namespace).ToDiscoveryClient()
func buildHelmCapabilities(ctx context.Context, cfg *rest.Config, logFn func(string, ...interface{})) (*chartutil.Capabilities, error) {
if ctx == nil {
ctx = context.Background()
}
httpClient, err := rest.HTTPClientFor(cfg)
if err != nil {
return nil, fmt.Errorf("helm discovery HTTP client: %w", err)
}
discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(cfg, httpClientWithContext(ctx, httpClient))
if err != nil {
return nil, fmt.Errorf("helm discovery client: %w", err)
}
dc := memory.NewMemCacheClient(discoveryClient)
dc.Invalidate()

kubeVersion, err := dc.ServerVersion()
Expand Down Expand Up @@ -248,8 +258,33 @@ func shouldKeepHelmKubePrerelease(preRelease string) bool {

// ---------- HTTP helper ----------

func helmGet(url string) (*http.Response, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
type contextRoundTripper struct {
ctx context.Context
next http.RoundTripper
}

func (t contextRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return t.next.RoundTrip(req.Clone(t.ctx))
}

func httpClientWithContext(ctx context.Context, base *http.Client) *http.Client {
if ctx == nil {
ctx = context.Background()
}
if base == nil {
base = http.DefaultClient
}
client := *base
transport := base.Transport
if transport == nil {
transport = http.DefaultTransport
}
client.Transport = contextRoundTripper{ctx: ctx, next: transport}
return &client
}

func helmGet(ctx context.Context, url string) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
Expand All @@ -259,9 +294,9 @@ func helmGet(url string) (*http.Response, error) {

// ---------- Repo index ----------

func fetchIndexFile(repoURL string) (*repo.IndexFile, error) {
func fetchIndexFile(ctx context.Context, repoURL string) (*repo.IndexFile, error) {
indexURL := strings.TrimRight(repoURL, "/") + "/index.yaml"
resp, err := helmGet(indexURL)
resp, err := helmGet(ctx, indexURL)
if err != nil {
return nil, fmt.Errorf(
"fetch index %q: %w",
Expand Down Expand Up @@ -304,11 +339,13 @@ func fetchIndexFile(repoURL string) (*repo.IndexFile, error) {
// FetchRepoIndex returns all charts listed in a Helm repo's index.yaml, or, for an
// "oci://" repoURL, the single chart hosted at that OCI reference.
func FetchRepoIndex(repoURL string) ([]HelmChartSummary, error) {
ctx, cancel := context.WithTimeout(context.Background(), helmChartLoadTimeout)
defer cancel()
if isOCIRepo(repoURL) {
return fetchOCIChartSummary(repoURL)
return fetchOCIChartSummary(ctx, repoURL)
}

idx, err := fetchIndexFile(repoURL)
idx, err := fetchIndexFile(ctx, repoURL)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -388,16 +425,16 @@ func isOCIChartTag(tag string) bool {
return true
}

func newOCIRegistryClient() (*registry.Client, error) {
return registry.NewClient(registry.ClientOptHTTPClient(proxypkg.ProxyHttpClient))
func newOCIRegistryClient(ctx context.Context) (*registry.Client, error) {
return registry.NewClient(registry.ClientOptHTTPClient(httpClientWithContext(ctx, proxypkg.ProxyHttpClient)))
}

// pullOCIChart pulls the chart hosted at repoURL, resolving to the newest published
// semver tag when version is empty.
func pullOCIChart(repoURL, version string) (*registry.PullResult, error) {
func pullOCIChart(ctx context.Context, repoURL, version string) (*registry.PullResult, error) {
ref, resolvedVersion := resolveOCIChartRef(repoURL, version)

rc, err := newOCIRegistryClient()
rc, err := newOCIRegistryClient(ctx)
if err != nil {
return nil, fmt.Errorf("oci registry client: %w", err)
}
Expand Down Expand Up @@ -465,16 +502,16 @@ func latestOCISemverTag(tags []string) string {
return versionedTags[0].tag
}

func loadOCIChart(repoURL, version string) (*chart.Chart, error) {
pull, err := pullOCIChart(repoURL, version)
func loadOCIChart(ctx context.Context, repoURL, version string) (*chart.Chart, error) {
pull, err := pullOCIChart(ctx, repoURL, version)
if err != nil {
return nil, err
}
return loader.LoadArchive(bytes.NewReader(pull.Chart.Data))
}

func fetchOCIChartSummary(repoURL string) ([]HelmChartSummary, error) {
pull, err := pullOCIChart(repoURL, "")
func fetchOCIChartSummary(ctx context.Context, repoURL string) ([]HelmChartSummary, error) {
pull, err := pullOCIChart(ctx, repoURL, "")
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -682,8 +719,18 @@ func redactionCandidates(raw string) []string {
}

func loadChart(chartName, repoURL, version string) (*chart.Chart, error) {
return loadChartWithContext(context.Background(), chartName, repoURL, version)
}

func loadChartWithContext(parent context.Context, chartName, repoURL, version string) (*chart.Chart, error) {
if parent == nil {
parent = context.Background()
}
ctx, cancel := context.WithTimeout(parent, helmChartLoadTimeout)
defer cancel()

if isOCIRepo(repoURL) {
ch, err := loadOCIChart(repoURL, version)
ch, err := loadOCIChart(ctx, repoURL, version)
if err != nil {
return nil, fmt.Errorf(
"load chart %q from OCI repo %q version %q: %w",
Expand All @@ -696,7 +743,7 @@ func loadChart(chartName, repoURL, version string) (*chart.Chart, error) {
return ch, nil
}

idx, err := fetchIndexFile(repoURL)
idx, err := fetchIndexFile(ctx, repoURL)
if err != nil {
return nil, fmt.Errorf("load chart %q from repo %q version %q: fetch index.yaml failed: %w", chartName, redactURLForError(repoURL), version, err)
}
Expand Down Expand Up @@ -726,7 +773,7 @@ func loadChart(chartName, repoURL, version string) (*chart.Chart, error) {
if ociVersion == "" {
ociVersion = entry.Version
}
ch, err := loadOCIChart(chartURL, ociVersion)
ch, err := loadOCIChart(ctx, chartURL, ociVersion)
if err != nil {
return nil, fmt.Errorf(
"load chart %q from repo %q version %q: index.yaml resolved to OCI chart URL %q: %w",
Expand All @@ -743,7 +790,7 @@ func loadChart(chartName, repoURL, version string) (*chart.Chart, error) {
chartURL = strings.TrimRight(repoURL, "/") + "/" + strings.TrimLeft(chartURL, "/")
}

resp, err := helmGet(chartURL)
resp, err := helmGet(ctx, chartURL)
if err != nil {
return nil, fmt.Errorf(
"load chart %q from repo %q version %q: download chart archive %q failed: %w",
Expand Down Expand Up @@ -1218,8 +1265,11 @@ func InstallHelmChart(cfg *rest.Config, releaseName, namespace, chartName, repoU
return err
}

attachHelmCapabilities(actionConfig, cfg, namespace, helmWarningLog)
if err := validateHelmChartCompatibility(context.Background(), cfg, actionConfig, releaseName, namespace, ch, vals); err != nil {
compatibilityCtx, cancelCompatibility := context.WithTimeout(context.Background(), helmCompatibilityTimeout)
attachHelmCapabilities(compatibilityCtx, actionConfig, cfg, helmWarningLog)
err = validateHelmChartCompatibility(compatibilityCtx, cfg, actionConfig, releaseName, namespace, ch, vals)
cancelCompatibility()
if err != nil {
return err
}
install := action.NewInstall(actionConfig)
Expand Down Expand Up @@ -1264,7 +1314,8 @@ func InstallHelmChartStream(ctx context.Context, lifecycle HelmInstallLifecycle,
if streamCtx == nil {
streamCtx = context.Background()
}
installCtx := context.WithoutCancel(streamCtx)
installCtx, cancelInstall := context.WithTimeout(context.WithoutCancel(streamCtx), helmInstallOperationTimeout)
defer cancelInstall()
send := func(line string) bool {
if err := lifecycle.RecordLog(line); err != nil {
logrus.Warnf("failed to persist Helm operation log: %v", err)
Expand Down Expand Up @@ -1294,7 +1345,7 @@ func InstallHelmChartStream(ctx context.Context, lifecycle HelmInstallLifecycle,
}
return
}
helmChart, err := loadChart(chartName, repoURL, version)
helmChart, err := loadChartWithContext(installCtx, chartName, repoURL, version)
if err != nil {
send("ERROR: " + err.Error())
if finishErr := lifecycle.Finish(err); finishErr != nil {
Expand All @@ -1310,8 +1361,11 @@ func InstallHelmChartStream(ctx context.Context, lifecycle HelmInstallLifecycle,
}
return
}
attachHelmCapabilities(actionConfig, cfg, namespace, logFn)
if err := validateHelmChartCompatibility(installCtx, cfg, actionConfig, releaseName, namespace, helmChart, vals); err != nil {
compatibilityCtx, cancelCompatibility := context.WithTimeout(installCtx, helmCompatibilityTimeout)
attachHelmCapabilities(compatibilityCtx, actionConfig, cfg, logFn)
err = validateHelmChartCompatibility(compatibilityCtx, cfg, actionConfig, releaseName, namespace, helmChart, vals)
cancelCompatibility()
if err != nil {
send("ERROR: " + err.Error())
_ = lifecycle.Finish(err)
return
Expand Down Expand Up @@ -1374,8 +1428,11 @@ func UpgradeHelmRelease(cfg *rest.Config, releaseName, namespace, chartName, rep
return err
}

attachHelmCapabilities(actionConfig, cfg, namespace, helmWarningLog)
if err := validateHelmReleaseCompatibility(context.Background(), actionConfig, releaseName, namespace, ch, vals); err != nil {
compatibilityCtx, cancelCompatibility := context.WithTimeout(context.Background(), helmCompatibilityTimeout)
attachHelmCapabilities(compatibilityCtx, actionConfig, cfg, helmWarningLog)
err = validateHelmReleaseCompatibility(compatibilityCtx, actionConfig, releaseName, namespace, ch, vals)
cancelCompatibility()
if err != nil {
return err
}
upgrade := action.NewUpgrade(actionConfig)
Expand Down
Loading
Loading