From d320cc6f7b6fa2abaf4d4fd0f167b7ceb762afea Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin Date: Fri, 5 Jun 2026 13:24:39 +0300 Subject: [PATCH] fix(engine): require a template before rendering the chart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Render rendered the entire chart and only afterwards checked that a template was actually requested. When none was, the render was wasted — its output is discarded — and online it first fired live lookups against the target node. An operator who forgot --file / --template therefore saw a misleading "connection refused" from discovery instead of the actionable "templates are not set" error. Move the empty-TemplateFiles guard ahead of the chart load and render (after the online multi-node precondition, which does no network I/O). The command now fails fast with the correct, actionable error in both offline and online modes, and never renders or dials a node when there is nothing to select. Assisted-By: Claude Signed-off-by: Aleksei Sviridkin --- pkg/engine/contract_render_test.go | 27 +++++++++++++++++++++++++++ pkg/engine/engine.go | 15 +++++++++++---- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/pkg/engine/contract_render_test.go b/pkg/engine/contract_render_test.go index f77cb5c3..ec4b7ecd 100644 --- a/pkg/engine/contract_render_test.go +++ b/pkg/engine/contract_render_test.go @@ -56,6 +56,33 @@ func TestContract_Render_NoTemplateFilesError(t *testing.T) { } } +// Contract: the empty-TemplateFiles guard fires BEFORE any chart +// rendering. When no template is requested, talm must surface the +// actionable "templates are not set" error without first rendering the +// chart — otherwise a render-time failure (here a `fail` directive, +// online a live `lookup` against an unreachable node) masks the real +// problem with a misleading error. The chart body below errors if it is +// ever executed; the guard must short-circuit before that happens. +func TestContract_Render_NoTemplateFilesShortCircuitsBeforeRender(t *testing.T) { + chartRoot := createTestChart(t, "tc", "config.yaml", + `{{ fail "chart body must not render when no templates are requested" }}`) + _, err := Render(context.Background(), nil, Options{ + Offline: true, + Root: chartRoot, + // TemplateFiles intentionally empty + }) + if err == nil { + t.Fatal("expected error for empty TemplateFiles") + } + msg := err.Error() + if strings.Contains(msg, "chart body must not render") { + t.Errorf("chart was rendered before the no-templates guard fired; the guard must run first. got: %s", msg) + } + if !strings.Contains(msg, "templates are not set") { + t.Errorf("expected the 'templates are not set' guard error, got: %s", msg) + } +} + // Contract: Render with a TemplateFiles entry that does not exist in // the chart surfaces an error naming the missing template. Operators // hit this when they typo a path or when a template file is renamed diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index de9480c0..e575a5a2 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -1602,6 +1602,17 @@ func Render(ctx context.Context, c *client.Client, opts Options) ([]byte, error) helmEngine.LookupFunc = newLookupFunction(ctx, c, cmdName, opts.TalosEndpoints) } + // Require at least one template before loading and rendering the chart. + // Rendering it first would be wasted (the output is discarded when no + // template is selected) and, worse, the render fires live lookups whose + // "connection refused" then masks the real problem: the operator forgot + // --file / --template. Fail fast with the actionable error instead. This + // runs after the online multi-node guard, which is a cheaper precondition + // with no network I/O. + if len(opts.TemplateFiles) == 0 { + return nil, errors.New("templates are not set for the command: please use `--file` or `--template` flag") + } + chartPath, err := os.Getwd() if err != nil { return nil, errors.Wrap(err, "resolving working directory") @@ -1633,10 +1644,6 @@ func Render(ctx context.Context, c *client.Client, opts Options) ([]byte, error) return nil, errors.Wrap(err, "rendering chart") } - if len(opts.TemplateFiles) == 0 { - return nil, errors.New("templates are not set for the command: please use `--file` or `--template` flag") - } - configPatches := []string{} for _, templateFile := range opts.TemplateFiles {