From 34cfdfdbe57ec9690113e141395c499a7c00ca6b Mon Sep 17 00:00:00 2001 From: Matthieu MOREL Date: Wed, 25 Feb 2026 08:01:45 +0100 Subject: [PATCH] chore: enable contextcheck, fatcontext and noctx linters Signed-off-by: Matthieu MOREL --- .golangci.yml | 3 + cmd/helm/helm_test.go | 2 +- internal/chart/v3/lint/rules/template.go | 3 +- internal/monocular/search.go | 10 ++- internal/monocular/search_test.go | 2 +- internal/plugin/installer/installer.go | 8 +- internal/plugin/runtime_extismv1_test.go | 2 +- internal/plugin/runtime_subprocess.go | 21 ++--- internal/plugin/runtime_subprocess_getter.go | 6 +- pkg/action/action.go | 7 +- pkg/action/action_test.go | 28 +++---- pkg/action/install.go | 2 +- pkg/action/upgrade.go | 6 +- pkg/chart/common/util/jsonschema.go | 3 +- pkg/chart/v2/lint/rules/template.go | 3 +- pkg/cmd/search_hub.go | 9 +- pkg/engine/engine.go | 36 ++++++-- pkg/engine/engine_test.go | 12 +-- pkg/engine/lookup_func.go | 8 +- pkg/getter/httpgetter.go | 3 +- pkg/kube/ready.go | 2 +- pkg/kubeenv/roundtripper_test.go | 9 +- pkg/registry/registry_test.go | 8 +- pkg/repo/v1/repotest/server.go | 3 +- pkg/repo/v1/repotest/server_test.go | 87 ++++++++------------ 25 files changed, 161 insertions(+), 122 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index e7c8b11a6..b3ced6f1a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -20,10 +20,12 @@ linters: # Keep sorted alphabetically enable: - bidichk + - contextcheck - depguard - dupl - errorlint - exhaustive + - fatcontext - gocritic - gomodguard - govet @@ -31,6 +33,7 @@ linters: - misspell - modernize - nakedret + - noctx - nolintlint - perfsprint - revive diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index a9362f772..95f6504c1 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -46,7 +46,7 @@ func TestCliPluginExitCode(t *testing.T) { // So that the second run is able to run main() and this first run can verify the exit status returned by that. // // This technique originates from https://talks.golang.org/2014/testing.slide#23. - cmd := exec.Command(os.Args[0], "-test.run=TestCliPluginExitCode") + cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=TestCliPluginExitCode") cmd.Env = append( os.Environ(), "RUN_MAIN_FOR_TESTING=1", diff --git a/internal/chart/v3/lint/rules/template.go b/internal/chart/v3/lint/rules/template.go index a8ae910eb..464acc769 100644 --- a/internal/chart/v3/lint/rules/template.go +++ b/internal/chart/v3/lint/rules/template.go @@ -19,6 +19,7 @@ package rules import ( "bufio" "bytes" + "context" "errors" "fmt" "io" @@ -104,7 +105,7 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string } var e engine.Engine e.LintMode = true - renderedContentMap, err := e.Render(chart, valuesToRender) + renderedContentMap, err := e.RenderWithContext(context.Background(), chart, valuesToRender) renderOk := linter.RunLinterRule(support.ErrorSev, fpath, err) diff --git a/internal/monocular/search.go b/internal/monocular/search.go index cfae87ded..29cc7f1a4 100644 --- a/internal/monocular/search.go +++ b/internal/monocular/search.go @@ -17,6 +17,7 @@ limitations under the License. package monocular import ( + "context" "encoding/json" "fmt" "net/http" @@ -98,7 +99,14 @@ type ChartVersion struct { } // Search performs a search against the monocular search API +// +// Deprecated: Use SearchWithContext instead. func (c *Client) Search(term string) ([]SearchResult, error) { + return c.SearchWithContext(context.Background(), term) +} + +// SearchWithContext performs a search against the monocular search API +func (c *Client) SearchWithContext(ctx context.Context, term string) ([]SearchResult, error) { // Create the URL to the search endpoint // Note, this is currently an internal API for the Hub. This should be // formatted without showing how monocular operates. @@ -113,7 +121,7 @@ func (c *Client) Search(term string) ([]SearchResult, error) { p.RawQuery = "q=" + url.QueryEscape(term) // Create request - req, err := http.NewRequest(http.MethodGet, p.String(), nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.String(), http.NoBody) if err != nil { return nil, err } diff --git a/internal/monocular/search_test.go b/internal/monocular/search_test.go index cdf67f69e..e4475f24e 100644 --- a/internal/monocular/search_test.go +++ b/internal/monocular/search_test.go @@ -37,7 +37,7 @@ func TestSearch(t *testing.T) { t.Errorf("unable to create monocular client: %s", err) } - results, err := c.Search("phpmyadmin") + results, err := c.SearchWithContext(t.Context(), "phpmyadmin") if err != nil { t.Errorf("unable to search monocular: %s", err) } diff --git a/internal/plugin/installer/installer.go b/internal/plugin/installer/installer.go index f73197629..a7ca35ad4 100644 --- a/internal/plugin/installer/installer.go +++ b/internal/plugin/installer/installer.go @@ -16,6 +16,7 @@ limitations under the License. package installer import ( + "context" "errors" "fmt" "log/slog" @@ -189,12 +190,17 @@ func isRemoteHTTPArchive(source string) bool { } // If no suffix match, try HEAD request to check content type - res, err := http.Head(source) + req, err := http.NewRequestWithContext(context.Background(), http.MethodHead, source, http.NoBody) if err != nil { // If we get an error at the network layer, we can't install it. So // we return false. return false } + res, err := http.DefaultClient.Do(req) + if err != nil { + return false + } + defer res.Body.Close() // Next, we look for the content type or content disposition headers to see // if they have matching extractors. diff --git a/internal/plugin/runtime_extismv1_test.go b/internal/plugin/runtime_extismv1_test.go index bcd791905..9451c693b 100644 --- a/internal/plugin/runtime_extismv1_test.go +++ b/internal/plugin/runtime_extismv1_test.go @@ -46,7 +46,7 @@ func buildLoadExtismPlugin(t *testing.T, dir string) pluginRaw { require.NoError(t, err) require.Equal(t, "extism/v1", m.Runtime, "expected plugin runtime to be extism/v1") - cmd := exec.Command("make", "-C", dir) + cmd := exec.CommandContext(t.Context(), "make", "-C", dir) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr require.NoError(t, cmd.Run(), "failed to build plugin in %q", dir) diff --git a/internal/plugin/runtime_subprocess.go b/internal/plugin/runtime_subprocess.go index cd1a0842c..3c0af4a4e 100644 --- a/internal/plugin/runtime_subprocess.go +++ b/internal/plugin/runtime_subprocess.go @@ -96,14 +96,14 @@ func (r *SubprocessPluginRuntime) Metadata() Metadata { return r.metadata } -func (r *SubprocessPluginRuntime) Invoke(_ context.Context, input *Input) (*Output, error) { +func (r *SubprocessPluginRuntime) Invoke(ctx context.Context, input *Input) (*Output, error) { switch input.Message.(type) { case schema.InputMessageCLIV1: - return r.runCLI(input) + return r.runCLI(ctx, input) case schema.InputMessageGetterV1: - return r.runGetter(input) + return r.runGetter(ctx, input) case schema.InputMessagePostRendererV1: - return r.runPostrenderer(input) + return r.runPostrenderer(ctx, input) default: return nil, fmt.Errorf("unsupported subprocess plugin type %q", r.metadata.Type) } @@ -113,7 +113,7 @@ func (r *SubprocessPluginRuntime) Invoke(_ context.Context, input *Input) (*Outp // This method allows execution with different command/args than the plugin's default func (r *SubprocessPluginRuntime) InvokeWithEnv(main string, argv []string, env []string, stdin io.Reader, stdout, stderr io.Writer) error { mainCmdExp := os.ExpandEnv(main) - cmd := exec.Command(mainCmdExp, argv...) + cmd := exec.CommandContext(context.Background(), mainCmdExp, argv...) cmd.Env = slices.Clone(os.Environ()) cmd.Env = append( cmd.Env, @@ -149,7 +149,7 @@ func (r *SubprocessPluginRuntime) InvokeHook(event string) error { return err } - cmd := exec.Command(main, argv...) + cmd := exec.CommandContext(context.Background(), main, argv...) cmd.Env = FormatEnv(env) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -189,7 +189,7 @@ func executeCmd(prog *exec.Cmd, pluginName string) error { return nil } -func (r *SubprocessPluginRuntime) runCLI(input *Input) (*Output, error) { +func (r *SubprocessPluginRuntime) runCLI(ctx context.Context, input *Input) (*Output, error) { if _, ok := input.Message.(schema.InputMessageCLIV1); !ok { return nil, fmt.Errorf("plugin %q input message does not implement InputMessageCLIV1", r.metadata.Name) } @@ -209,7 +209,7 @@ func (r *SubprocessPluginRuntime) runCLI(input *Input) (*Output, error) { return nil, fmt.Errorf("failed to prepare plugin command: %w", err) } - cmd := exec.Command(command, args...) + cmd := exec.CommandContext(ctx, command, args...) cmd.Env = FormatEnv(env) cmd.Stdin = input.Stdin @@ -226,7 +226,7 @@ func (r *SubprocessPluginRuntime) runCLI(input *Input) (*Output, error) { }, nil } -func (r *SubprocessPluginRuntime) runPostrenderer(input *Input) (*Output, error) { +func (r *SubprocessPluginRuntime) runPostrenderer(ctx context.Context, input *Input) (*Output, error) { if _, ok := input.Message.(schema.InputMessagePostRendererV1); !ok { return nil, fmt.Errorf("plugin %q input message does not implement InputMessagePostRendererV1", r.metadata.Name) } @@ -244,7 +244,8 @@ func (r *SubprocessPluginRuntime) runPostrenderer(input *Input) (*Output, error) return nil, fmt.Errorf("failed to prepare plugin command: %w", err) } - cmd := exec.Command( + cmd := exec.CommandContext( + ctx, command, args...) diff --git a/internal/plugin/runtime_subprocess_getter.go b/internal/plugin/runtime_subprocess_getter.go index c7262e0dd..3f07fa764 100644 --- a/internal/plugin/runtime_subprocess_getter.go +++ b/internal/plugin/runtime_subprocess_getter.go @@ -17,6 +17,7 @@ package plugin import ( "bytes" + "context" "fmt" "log/slog" "maps" @@ -40,7 +41,7 @@ func getProtocolCommand(commands []SubprocessProtocolCommand, protocol string) * } // TODO can we replace a lot of this func with RuntimeSubprocess.invokeWithEnv? -func (r *SubprocessPluginRuntime) runGetter(input *Input) (*Output, error) { +func (r *SubprocessPluginRuntime) runGetter(ctx context.Context, input *Input) (*Output, error) { msg, ok := (input.Message).(schema.InputMessageGetterV1) if !ok { return nil, fmt.Errorf("expected input type schema.InputMessageGetterV1, got %T", input) @@ -81,7 +82,8 @@ func (r *SubprocessPluginRuntime) runGetter(input *Input) (*Output, error) { buf := bytes.Buffer{} // subprocess getters are expected to write content to stdout pluginCommand := filepath.Join(r.pluginDir, command) - cmd := exec.Command( + cmd := exec.CommandContext( + ctx, pluginCommand, args...) cmd.Env = FormatEnv(env) diff --git a/pkg/action/action.go b/pkg/action/action.go index 4ed718371..6e5d8c15a 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -18,6 +18,7 @@ package action import ( "bytes" + "context" "errors" "fmt" "io" @@ -275,7 +276,7 @@ func splitAndDeannotate(postrendered, fallbackPrefix string) (map[string]string, // TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed // // This code has to do with writing files to disk. -func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret bool, postRenderStrategy PostRenderStrategy) ([]*release.Hook, *bytes.Buffer, string, error) { +func (cfg *Configuration) renderResources(ctx context.Context, ch *chart.Chart, values common.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret bool, postRenderStrategy PostRenderStrategy) ([]*release.Hook, *bytes.Buffer, string, error) { var hs []*release.Hook b := bytes.NewBuffer(nil) @@ -305,13 +306,13 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values, e.EnableDNS = enableDNS e.CustomTemplateFuncs = cfg.CustomTemplateFuncs - files, err2 = e.Render(ch, values) + files, err2 = e.RenderWithContext(ctx, ch, values) } else { var e engine.Engine e.EnableDNS = enableDNS e.CustomTemplateFuncs = cfg.CustomTemplateFuncs - files, err2 = e.Render(ch, values) + files, err2 = e.RenderWithContext(ctx, ch, values) } if err2 != nil { diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index 6a561985f..ecf160500 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -1823,7 +1823,7 @@ func TestRenderResources_PostRenderer_Success(t *testing.T) { values := map[string]any{} hooks, buf, notes, err := cfg.renderResources( - ch, values, "test-release", "", false, false, false, + t.Context(), ch, values, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategyCombined, ) @@ -1870,7 +1870,7 @@ func TestRenderResources_PostRenderer_Error(t *testing.T) { values := map[string]any{} _, _, _, err := cfg.renderResources( - ch, values, "test-release", "", false, false, false, + t.Context(), ch, values, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategyCombined, ) @@ -1898,7 +1898,7 @@ func TestRenderResources_PostRenderer_MergeError(t *testing.T) { values := map[string]any{} _, _, _, err := cfg.renderResources( - ch, values, "test-release", "", false, false, false, + t.Context(), ch, values, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategyCombined, ) @@ -1920,7 +1920,7 @@ func TestRenderResources_PostRenderer_SplitError(t *testing.T) { values := map[string]any{} _, _, _, err := cfg.renderResources( - ch, values, "test-release", "", false, false, false, + t.Context(), ch, values, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategyCombined, ) @@ -1941,7 +1941,7 @@ func TestRenderResources_PostRenderer_Integration(t *testing.T) { values := map[string]any{} hooks, buf, notes, err := cfg.renderResources( - ch, values, "test-release", "", false, false, false, + t.Context(), ch, values, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategyCombined, ) @@ -1980,7 +1980,7 @@ func TestRenderResources_NoPostRenderer(t *testing.T) { values := map[string]any{} hooks, buf, notes, err := cfg.renderResources( - ch, values, "test-release", "", false, false, false, + t.Context(), ch, values, "test-release", "", false, false, false, nil, false, false, false, PostRenderStrategyCombined, ) @@ -2041,7 +2041,7 @@ spec: } hooks, buf, _, err := cfg.renderResources( - ch, nil, "test-release", "", false, false, false, + t.Context(), ch, nil, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategySeparate, ) @@ -2083,7 +2083,7 @@ metadata: } _, _, _, err := cfg.renderResources( - ch, nil, "test-release", "", false, false, false, + t.Context(), ch, nil, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategyCombined, ) @@ -2119,7 +2119,7 @@ metadata: } _, _, _, err := cfg.renderResources( - ch, nil, "test-release", "", false, false, false, + t.Context(), ch, nil, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategy(""), ) @@ -2153,7 +2153,7 @@ metadata: } _, _, _, err := cfg.renderResources( - ch, nil, "test-release", "", false, false, false, + t.Context(), ch, nil, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategySeparate, ) @@ -2187,7 +2187,7 @@ metadata: } _, _, _, err := cfg.renderResources( - ch, nil, "test-release", "", false, false, false, + t.Context(), ch, nil, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategySeparate, ) @@ -2221,7 +2221,7 @@ metadata: } hooks, manifestDoc, _, err := cfg.renderResources( - ch, nil, "test-release", "", false, false, false, + t.Context(), ch, nil, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategyNoHooks, ) @@ -2258,7 +2258,7 @@ metadata: } _, _, _, err := cfg.renderResources( - ch, nil, "test-release", "", false, false, false, + t.Context(), ch, nil, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategyNoHooks, ) @@ -2280,7 +2280,7 @@ metadata: mockPR := &mockPostRenderer{} _, _, _, err := cfg.renderResources( - ch, nil, "test-release", "", false, false, false, + t.Context(), ch, nil, "test-release", "", false, false, false, mockPR, false, false, false, PostRenderStrategy("bogus"), ) diff --git a/pkg/action/install.go b/pkg/action/install.go index 580b8a0cb..fcfae7214 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -375,7 +375,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st rel := i.createRelease(chrt, vals, i.Labels) var manifestDoc *bytes.Buffer - rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithServer(i.DryRunStrategy), i.EnableDNS, i.HideSecret, i.PostRenderStrategy) + rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(ctx, chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithServer(i.DryRunStrategy), i.EnableDNS, i.HideSecret, i.PostRenderStrategy) // Even for errors, attach this if available if manifestDoc != nil { rel.Manifest = manifestDoc.String() diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 0d7b1b148..81d51164f 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -193,7 +193,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Char } u.cfg.Logger().Debug("preparing upgrade", "name", name) - currentRelease, upgradedRelease, serverSideApply, err := u.prepareUpgrade(name, chrt, vals) + currentRelease, upgradedRelease, serverSideApply, err := u.prepareUpgrade(ctx, name, chrt, vals) if err != nil { return nil, err } @@ -218,7 +218,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Char } // prepareUpgrade builds an upgraded release for an upgrade operation. -func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[string]any) (*release.Release, *release.Release, bool, error) { +func (u *Upgrade) prepareUpgrade(ctx context.Context, name string, chart *chartv2.Chart, vals map[string]any) (*release.Release, *release.Release, bool, error) { if chart == nil { return nil, nil, false, errMissingChart } @@ -300,7 +300,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str return nil, nil, false, err } - hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithServer(u.DryRunStrategy), u.EnableDNS, u.HideSecret, u.PostRenderStrategy) + hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(ctx, chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithServer(u.DryRunStrategy), u.EnableDNS, u.HideSecret, u.PostRenderStrategy) if err != nil { return nil, nil, false, err } diff --git a/pkg/chart/common/util/jsonschema.go b/pkg/chart/common/util/jsonschema.go index 63ca0c274..624143993 100644 --- a/pkg/chart/common/util/jsonschema.go +++ b/pkg/chart/common/util/jsonschema.go @@ -18,6 +18,7 @@ package util import ( "bytes" + "context" "crypto/tls" "errors" "fmt" @@ -41,7 +42,7 @@ type HTTPURLLoader http.Client func (l *HTTPURLLoader) Load(urlStr string) (any, error) { client := (*http.Client)(l) - req, err := http.NewRequest(http.MethodGet, urlStr, nil) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, urlStr, http.NoBody) if err != nil { return nil, fmt.Errorf("failed to create HTTP request for %s: %w", urlStr, err) } diff --git a/pkg/chart/v2/lint/rules/template.go b/pkg/chart/v2/lint/rules/template.go index 25ed31900..47209112e 100644 --- a/pkg/chart/v2/lint/rules/template.go +++ b/pkg/chart/v2/lint/rules/template.go @@ -19,6 +19,7 @@ package rules import ( "bufio" "bytes" + "context" "errors" "fmt" "io" @@ -134,7 +135,7 @@ func (t *templateLinter) Lint() { } var e engine.Engine e.LintMode = true - renderedContentMap, err := e.Render(chart, valuesToRender) + renderedContentMap, err := e.RenderWithContext(context.Background(), chart, valuesToRender) renderOk := t.linter.RunLinterRule(support.ErrorSev, templatesDir, err) diff --git a/pkg/cmd/search_hub.go b/pkg/cmd/search_hub.go index 3081961be..3f7afa3f4 100644 --- a/pkg/cmd/search_hub.go +++ b/pkg/cmd/search_hub.go @@ -17,6 +17,7 @@ limitations under the License. package cmd import ( + "context" "errors" "fmt" "io" @@ -65,8 +66,8 @@ func newSearchHubCmd(out io.Writer) *cobra.Command { Use: "hub [KEYWORD]", Short: "search for charts in the Artifact Hub or your own hub instance", Long: searchHubDesc, - RunE: func(_ *cobra.Command, args []string) error { - return o.run(out, args) + RunE: func(c *cobra.Command, args []string) error { + return o.run(c.Context(), out, args) }, } @@ -81,14 +82,14 @@ func newSearchHubCmd(out io.Writer) *cobra.Command { return cmd } -func (o *searchHubOptions) run(out io.Writer, args []string) error { +func (o *searchHubOptions) run(ctx context.Context, out io.Writer, args []string) error { c, err := monocular.New(o.searchEndpoint) if err != nil { return fmt.Errorf("unable to create connection to %q: %w", o.searchEndpoint, err) } q := strings.Join(args, " ") - results, err := c.Search(q) + results, err := c.SearchWithContext(ctx, q) if err != nil { slog.Debug("search failed", slog.Any("error", err)) return fmt.Errorf("unable to perform search against %q", o.searchEndpoint) diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 6fd2beed8..4f7fa4590 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -17,6 +17,7 @@ limitations under the License. package engine import ( + "context" "errors" "fmt" "log/slog" @@ -76,9 +77,34 @@ func New(config *rest.Config) Engine { // that section of the values will be passed into the "foo" chart. And if that // section contains a value named "bar", that value will be passed on to the // bar chart during render time. +// +// Deprecated: Use RenderWithContext instead. func (e Engine) Render(chrt ci.Charter, values common.Values) (map[string]string, error) { + return e.RenderWithContext(context.Background(), chrt, values) +} + +// RenderWithContext takes a chart, optional values, and value overrides, and attempts to render the Go templates. +// +// RenderWithContext can be called repeatedly on the same engine. +// +// This will look in the chart's 'templates' data (e.g. the 'templates/' directory) +// and attempt to render the templates there using the values passed in. +// +// Values are scoped to their templates. A dependency template will not have +// access to the values set for its parent. If chart "foo" includes chart "bar", +// "bar" will not have access to the values for "foo". +// +// Values should be prepared with something like `chartutils.ReadValues`. +// +// Values are passed through the templates according to scope. If the top layer +// chart includes the chart foo, which includes the chart bar, the values map +// will be examined for a table called "foo". If "foo" is found in vals, +// that section of the values will be passed into the "foo" chart. And if that +// section contains a value named "bar", that value will be passed on to the +// bar chart during render time. +func (e Engine) RenderWithContext(ctx context.Context, chrt ci.Charter, values common.Values) (map[string]string, error) { tmap := allTemplates(chrt, values) - return e.render(tmap) + return e.render(ctx, tmap) } // Render takes a chart, optional values, and value overrides, and attempts to @@ -195,7 +221,7 @@ func tplFun(parent *template.Template, includedNames map[string]int, strict bool } // initFunMap creates the Engine's FuncMap and adds context-specific functions. -func (e Engine) initFunMap(t *template.Template) { +func (e Engine) initFunMap(ctx context.Context, t *template.Template) { funcMap := funcMap() includedNames := make(map[string]int) @@ -238,7 +264,7 @@ func (e Engine) initFunMap(t *template.Template) { // If we are not linting and have a cluster connection, provide a Kubernetes-backed // implementation. if !e.LintMode && e.clientProvider != nil { - funcMap["lookup"] = newLookupFunction(*e.clientProvider) + funcMap["lookup"] = newLookupFunction(ctx, *e.clientProvider) } // When DNS lookups are not enabled override the sprig function and return @@ -256,7 +282,7 @@ func (e Engine) initFunMap(t *template.Template) { } // render takes a map of templates/values and renders them. -func (e Engine) render(tpls map[string]renderable) (rendered map[string]string, err error) { +func (e Engine) render(ctx context.Context, tpls map[string]renderable) (rendered map[string]string, err error) { // Basically, what we do here is start with an empty parent template and then // build up a list of templates -- one for each file. Once all of the templates // have been parsed, we loop through again and execute every template. @@ -278,7 +304,7 @@ func (e Engine) render(tpls map[string]renderable) (rendered map[string]string, t.Option("missingkey=zero") } - e.initFunMap(t) + e.initFunMap(ctx, t) // We want to parse the templates in a predictable order. The order favors // higher-level (in file system) templates over deeply nested templates. diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index 612ab85f0..f666b63a6 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -197,7 +197,7 @@ func TestRenderInternals(t *testing.T) { "three": {tpl: `{{template "two" dict "Value" "three"}}`, vals: vals}, } - out, err := new(Engine).render(tpls) + out, err := new(Engine).render(t.Context(), tpls) if err != nil { t.Fatalf("Failed template rendering: %s", err) } @@ -443,7 +443,7 @@ func TestParallelRenderInternals(t *testing.T) { vals: map[string]any{"val": tt}, }, } - out, err := e.render(tpls) + out, err := e.render(t.Context(), tpls) if err != nil { t.Errorf("Failed to render %s: %s", tt, err) } @@ -462,7 +462,7 @@ func TestParseErrors(t *testing.T) { tplsUndefinedFunction := map[string]renderable{ "undefined_function": {tpl: `{{foo}}`, vals: vals}, } - _, err := new(Engine).render(tplsUndefinedFunction) + _, err := new(Engine).render(t.Context(), tplsUndefinedFunction) if err == nil { t.Fatalf("Expected failures while rendering: %s", err) } @@ -525,7 +525,7 @@ linebreak`, for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { - _, err := new(Engine).render(tt.tpls) + _, err := new(Engine).render(t.Context(), tt.tpls) if err == nil { t.Fatalf("Expected failures while rendering: %s", err) } @@ -543,7 +543,7 @@ func TestFailErrors(t *testing.T) { tplsFailed := map[string]renderable{ "failtpl": {tpl: failtpl, vals: vals}, } - _, err := new(Engine).render(tplsFailed) + _, err := new(Engine).render(t.Context(), tplsFailed) if err == nil { t.Fatalf("Expected failures while rendering: %s", err) } @@ -554,7 +554,7 @@ func TestFailErrors(t *testing.T) { var e Engine e.LintMode = true - out, err := e.render(tplsFailed) + out, err := e.render(t.Context(), tplsFailed) if err != nil { t.Fatal(err) } diff --git a/pkg/engine/lookup_func.go b/pkg/engine/lookup_func.go index c5c441c14..b0d0224be 100644 --- a/pkg/engine/lookup_func.go +++ b/pkg/engine/lookup_func.go @@ -36,7 +36,7 @@ type lookupFunc = func(apiversion string, resource string, namespace string, nam // // If the resource does not exist, no error is raised. func NewLookupFunction(config *rest.Config) lookupFunc { //nolint:revive - return newLookupFunction(clientProviderFromConfig{config: config}) + return newLookupFunction(context.Background(), clientProviderFromConfig{config: config}) } type ClientProvider interface { @@ -54,7 +54,7 @@ func (c clientProviderFromConfig) GetClientFor(apiVersion, kind string) (dynamic return getDynamicClientOnKind(apiVersion, kind, c.config) } -func newLookupFunction(clientProvider ClientProvider) lookupFunc { +func newLookupFunction(ctx context.Context, clientProvider ClientProvider) lookupFunc { return func(apiversion string, kind string, namespace string, name string) (map[string]any, error) { var client dynamic.ResourceInterface c, namespaced, err := clientProvider.GetClientFor(apiversion, kind) @@ -68,7 +68,7 @@ func newLookupFunction(clientProvider ClientProvider) lookupFunc { } if name != "" { // this will return a single object - obj, err := client.Get(context.Background(), name, metav1.GetOptions{}) + obj, err := client.Get(ctx, name, metav1.GetOptions{}) if err != nil { if apierrors.IsNotFound(err) { // Just return an empty interface when the object was not found. @@ -86,7 +86,7 @@ func newLookupFunction(clientProvider ClientProvider) lookupFunc { return obj.UnstructuredContent(), nil } // this will return a list - obj, err := client.List(context.Background(), metav1.ListOptions{}) + obj, err := client.List(ctx, metav1.ListOptions{}) if err != nil { if apierrors.IsNotFound(err) { // Just return an empty interface when the object was not found. diff --git a/pkg/getter/httpgetter.go b/pkg/getter/httpgetter.go index 2eb2d5d8c..a72a9337a 100644 --- a/pkg/getter/httpgetter.go +++ b/pkg/getter/httpgetter.go @@ -17,6 +17,7 @@ package getter import ( "bytes" + "context" "crypto/tls" "fmt" "io" @@ -49,7 +50,7 @@ func (g *HTTPGetter) Get(href string, options ...Option) (*bytes.Buffer, error) func (g *HTTPGetter) get(href string, opts getterOptions) (*bytes.Buffer, error) { // Set a helm specific user agent so that a repo server and metrics can // separate helm calls from other tools interacting with repos. - req, err := http.NewRequest(http.MethodGet, href, nil) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, href, http.NoBody) if err != nil { return nil, err } diff --git a/pkg/kube/ready.go b/pkg/kube/ready.go index a1a3d4a9a..196fe922a 100644 --- a/pkg/kube/ready.go +++ b/pkg/kube/ready.go @@ -108,7 +108,7 @@ func (c *ReadyChecker) IsReady(ctx context.Context, v *resource.Info) (bool, err return c.pausedAsReady, nil } // Find RS associated with deployment - newReplicaSet, err := deploymentutil.GetNewReplicaSet(currentDeployment, c.client.AppsV1()) + newReplicaSet, err := deploymentutil.GetNewReplicaSet(currentDeployment, c.client.AppsV1()) //nolint:contextcheck if err != nil || newReplicaSet == nil { return false, err } diff --git a/pkg/kubeenv/roundtripper_test.go b/pkg/kubeenv/roundtripper_test.go index b921eac82..6f0caf6f8 100644 --- a/pkg/kubeenv/roundtripper_test.go +++ b/pkg/kubeenv/roundtripper_test.go @@ -25,6 +25,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type fakeRoundTripper struct { @@ -134,15 +135,15 @@ func TestRetryingRoundTripper_RoundTrip(t *testing.T) { rt := RetryingRoundTripper{ Wrapped: fakeRT, } - req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil) + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://example.com", http.NoBody) + require.NoError(t, err) resp, err := rt.RoundTrip(req) if tt.expectedErr != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.expectedErr) + require.ErrorContains(t, err, tt.expectedErr) return } - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, tt.expectedCode, resp.StatusCode) assert.Equal(t, tt.expectedCalls, fakeRT.calls) diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go index 3fc08a900..dc8118c6e 100644 --- a/pkg/registry/registry_test.go +++ b/pkg/registry/registry_test.go @@ -18,7 +18,6 @@ package registry import ( "bytes" - "context" "crypto/tls" "fmt" "io" @@ -125,7 +124,8 @@ func setup(suite *TestRegistry, tlsEnabled, insecure bool) { // Registry config config := &configuration.Configuration{} - ln, err := net.Listen("tcp", "127.0.0.1:0") + lnCfg := net.ListenConfig{} + ln, err := lnCfg.Listen(suite.T().Context(), "tcp", "127.0.0.1:0") suite.Require().NoError(err, "no error finding free port for test registry") defer func() { _ = ln.Close() }() @@ -158,7 +158,7 @@ func setup(suite *TestRegistry, tlsEnabled, insecure bool) { config.HTTP.TLS.ClientCAs = []string{tlsCA} } } - suite.dockerRegistry, err = registry.NewRegistry(context.Background(), config) + suite.dockerRegistry, err = registry.NewRegistry(suite.T().Context(), config) suite.Require().NoError(err, "no error creating test registry") suite.FakeRegistryHost = initFakeRegistryTestServer() @@ -170,7 +170,7 @@ func setup(suite *TestRegistry, tlsEnabled, insecure bool) { func teardown(suite *TestRegistry) { if suite.dockerRegistry != nil { - _ = suite.dockerRegistry.Shutdown(context.Background()) + _ = suite.dockerRegistry.Shutdown(suite.T().Context()) } } diff --git a/pkg/repo/v1/repotest/server.go b/pkg/repo/v1/repotest/server.go index f1b5e0744..61dcd6931 100644 --- a/pkg/repo/v1/repotest/server.go +++ b/pkg/repo/v1/repotest/server.go @@ -180,7 +180,8 @@ func NewOCIServer(t *testing.T, dir string) (*OCIServer, error) { // Registry config config := &configuration.Configuration{} - ln, err := net.Listen("tcp", "127.0.0.1:0") + lnCfg := net.ListenConfig{} + ln, err := lnCfg.Listen(t.Context(), "tcp", "127.0.0.1:0") if err != nil { t.Fatalf("error finding free port for test registry: %v", err) } diff --git a/pkg/repo/v1/repotest/server_test.go b/pkg/repo/v1/repotest/server_test.go index 499091a57..1364fc9c9 100644 --- a/pkg/repo/v1/repotest/server_test.go +++ b/pkg/repo/v1/repotest/server_test.go @@ -22,6 +22,7 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" "sigs.k8s.io/yaml" "helm.sh/helm/v4/internal/test/ensure" @@ -52,26 +53,24 @@ func TestServer(t *testing.T) { t.Errorf("Unexpected chart: %s", c[0]) } - res, err := http.Get(srv.URL() + "/examplechart-0.1.0.tgz") + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL()+"/examplechart-0.1.0.tgz", http.NoBody) + require.NoError(t, err) + client := http.DefaultClient + res, err := client.Do(req) + require.NoError(t, err) res.Body.Close() - if err != nil { - t.Fatal(err) - } if res.ContentLength < 500 { t.Errorf("Expected at least 500 bytes of data, got %d", res.ContentLength) } - res, err = http.Get(srv.URL() + "/index.yaml") - if err != nil { - t.Fatal(err) - } - + req, err = http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL()+"/index.yaml", http.NoBody) + require.NoError(t, err) + res, err = client.Do(req) + require.NoError(t, err) data, err := io.ReadAll(res.Body) res.Body.Close() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) m := repo.NewIndexFile() if err := yaml.Unmarshal(data, m); err != nil { @@ -87,11 +86,11 @@ func TestServer(t *testing.T) { t.Errorf("missing %q", expect) } - res, err = http.Get(srv.URL() + "/index.yaml-nosuchthing") + req, err = http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL()+"/index.yaml-nosuchthing", http.NoBody) + require.NoError(t, err) + res, err = client.Do(req) + require.NoError(t, err) res.Body.Close() - if err != nil { - t.Fatal(err) - } if res.StatusCode != http.StatusNotFound { t.Fatalf("Expected 404, got %d", res.StatusCode) } @@ -133,70 +132,56 @@ func TestNewTempServer(t *testing.T) { client := srv.Client() { - res, err := client.Head(srv.URL() + "/repositories.yaml") - if err != nil { - t.Error(err) - } - + req, err := http.NewRequestWithContext(t.Context(), http.MethodHead, srv.URL()+"/repositories.yaml", http.NoBody) + require.NoError(t, err) + res, err := client.Do(req) + require.NoError(t, err) res.Body.Close() - if res.StatusCode != http.StatusOK { t.Errorf("Expected 200, got %d", res.StatusCode) } } - { - res, err := client.Head(srv.URL() + "/examplechart-0.1.0.tgz") - if err != nil { - t.Error(err) - } + req, err := http.NewRequestWithContext(t.Context(), http.MethodHead, srv.URL()+"/examplechart-0.1.0.tgz", http.NoBody) + require.NoError(t, err) + res, err := client.Do(req) + require.NoError(t, err) res.Body.Close() - if res.StatusCode != http.StatusOK { t.Errorf("Expected 200, got %d", res.StatusCode) } } - - res, err := client.Get(srv.URL() + "/examplechart-0.1.0.tgz") + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL()+"/examplechart-0.1.0.tgz", http.NoBody) + require.NoError(t, err) + res, err := client.Do(req) + require.NoError(t, err) res.Body.Close() - if err != nil { - t.Fatal(err) - } - if res.ContentLength < 500 { t.Errorf("Expected at least 500 bytes of data, got %d", res.ContentLength) } - - res, err = client.Get(srv.URL() + "/index.yaml") - if err != nil { - t.Fatal(err) - } - + req, err = http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL()+"/index.yaml", http.NoBody) + require.NoError(t, err) + res, err = client.Do(req) + require.NoError(t, err) data, err := io.ReadAll(res.Body) res.Body.Close() - if err != nil { - t.Fatal(err) - } - + require.NoError(t, err) m := repo.NewIndexFile() if err := yaml.Unmarshal(data, m); err != nil { t.Fatal(err) } - if l := len(m.Entries); l != 1 { t.Fatalf("Expected 1 entry, got %d", l) } - expect := "examplechart" if !m.Has(expect, "0.1.0") { t.Errorf("missing %q", expect) } - - res, err = client.Get(srv.URL() + "/index.yaml-nosuchthing") + req, err = http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL()+"/index.yaml-nosuchthing", http.NoBody) + require.NoError(t, err) + res, err = client.Do(req) + require.NoError(t, err) res.Body.Close() - if err != nil { - t.Fatal(err) - } if res.StatusCode != http.StatusNotFound { t.Fatalf("Expected 404, got %d", res.StatusCode) }