diff --git a/pkg/postrenderer/postrenderer.go b/pkg/postrenderer/postrenderer.go index 55e6d3adf..5a51e00e4 100644 --- a/pkg/postrenderer/postrenderer.go +++ b/pkg/postrenderer/postrenderer.go @@ -72,11 +72,15 @@ func (r *postRendererPlugin) Run(renderedManifests *bytes.Buffer) (*bytes.Buffer return nil, fmt.Errorf("failed to invoke post-renderer plugin %q: %w", r.plugin.Metadata().Name, err) } - outputMessage := output.Message.(schema.OutputMessagePostRendererV1) + outputMessage, ok := output.Message.(schema.OutputMessagePostRendererV1) + if !ok { + return nil, fmt.Errorf("invalid output message type from plugin %q", r.plugin.Metadata().Name) + } - // If the binary returned almost nothing, it's likely that it didn't - // successfully render anything - if len(bytes.TrimSpace(outputMessage.Manifests.Bytes())) == 0 { + // If the binary returned almost nothing (including no Manifests buffer at + // all, which a malformed or misbehaving plugin can produce), it's likely + // that it didn't successfully render anything. + if outputMessage.Manifests == nil || len(bytes.TrimSpace(outputMessage.Manifests.Bytes())) == 0 { return nil, fmt.Errorf("post-renderer %q produced empty output", r.plugin.Metadata().Name) } diff --git a/pkg/postrenderer/postrenderer_test.go b/pkg/postrenderer/postrenderer_test.go index cea6f9f76..764194dd4 100644 --- a/pkg/postrenderer/postrenderer_test.go +++ b/pkg/postrenderer/postrenderer_test.go @@ -18,15 +18,47 @@ package postrenderer import ( "bytes" + "context" "runtime" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "helm.sh/helm/v4/internal/plugin" + "helm.sh/helm/v4/internal/plugin/schema" "helm.sh/helm/v4/pkg/cli" ) +// nilManifestPlugin simulates a plugin.Plugin whose Invoke() returns a +// well-typed OutputMessagePostRendererV1 with a nil Manifests field. This is +// exactly what internal/plugin's ExtismV1PluginRuntime.Invoke produces (via +// json.Unmarshal into a freshly reflect.New'd struct) when a wasm/extism +// post-renderer plugin's JSON output omits the "manifests" key or sets it to +// null -- e.g. because the plugin errored internally but still exited 0, or +// is simply buggy/malformed. No handcrafted wasm binary is required to prove +// the bug: the defect is in postRendererPlugin.Run's handling of the output +// message, not in how a specific Runtime constructs it. +type nilManifestPlugin struct{} + +func (nilManifestPlugin) Dir() string { return "" } +func (nilManifestPlugin) Metadata() plugin.Metadata { + return plugin.Metadata{Name: "nil-manifest-plugin", Type: "postrenderer/v1"} +} + +func (nilManifestPlugin) Invoke(_ context.Context, _ *plugin.Input) (*plugin.Output, error) { + return &plugin.Output{ + Message: schema.OutputMessagePostRendererV1{Manifests: nil}, + }, nil +} + +func TestPostRendererPluginRunWithNilManifestsDoesNotPanic(t *testing.T) { + r := &postRendererPlugin{plugin: nilManifestPlugin{}} + + _, err := r.Run(bytes.NewBufferString("apiVersion: v1\nkind: ConfigMap\n")) + require.Error(t, err, "a plugin returning nil Manifests should produce a clean error, not a panic") +} + func TestNewPostRenderPluginRunWithNoOutput(t *testing.T) { if runtime.GOOS == "windows" { // the actual Run test uses a basic sed example, so skip this test on windows