diff --git a/dynamic/internal/shim/run/loader.go b/dynamic/internal/shim/run/loader.go index 34363f9cc..b12a2d7f1 100644 --- a/dynamic/internal/shim/run/loader.go +++ b/dynamic/internal/shim/run/loader.go @@ -40,6 +40,9 @@ import ( tfaddr "github.com/opentofu/registry-address" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" "github.com/pulumi/pulumi/sdk/v3/go/common/workspace" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge" ) @@ -192,6 +195,25 @@ func getProviderServer( return runProvider(ctx, p) } +func includePanic( + ctx context.Context, method string, + req, reply any, + cc *grpc.ClientConn, invoker grpc.UnaryInvoker, + opts ...grpc.CallOption, +) error { + err := invoker(ctx, method, req, reply, cc, opts...) + if status.Code(err) != codes.Unavailable { + return nil + } + + panics := logging.PluginPanics() + if len(panics) == 0 { + return err + } + + return fmt.Errorf("%w:\n%s", err, strings.Join(panics, "\n")) +} + // runProvider produces a provider factory that runs up the executable // file in the given cache package and uses go-plugin to implement // providers.Interface against it. @@ -213,6 +235,9 @@ func runProvider(ctx context.Context, meta *providercache.CachedProvider) (Provi VersionedPlugins: tfplugin.VersionedPlugins, SyncStdout: logging.PluginOutputMonitor(fmt.Sprintf("%s:stdout", meta.Provider)), SyncStderr: logging.PluginOutputMonitor(fmt.Sprintf("%s:stderr", meta.Provider)), + GRPCDialOptions: []grpc.DialOption{ + grpc.WithUnaryInterceptor(includePanic), + }, } client := plugin.NewClient(config) diff --git a/dynamic/provider_test.go b/dynamic/provider_test.go index 3fb0f7767..100cd5081 100644 --- a/dynamic/provider_test.go +++ b/dynamic/provider_test.go @@ -46,6 +46,21 @@ func TestMain(m *testing.M) { os.Exit(exitVal) } +func TestStacktraceDisplayed(t *testing.T) { + t.Parallel() + skipWindows(t) + + ctx := context.Background() + grpc := pfProviderTestServer(ctx, t) + + _, err := grpc.Create(ctx, &pulumirpc.CreateRequest{ + Urn: string(resource.NewURN( + "test", "test", "", "pfprovider:index/panic:Panic", "panic", + )), + }) + assert.ErrorContains(t, err, "PANIC MESSAGE HERE") +} + func TestPrimitiveTypes(t *testing.T) { t.Parallel() skipWindows(t) @@ -371,6 +386,7 @@ var pfProviderPath = func() func(t *testing.T) string { } }() +// grpcTestServer returns an unparameterized in-memory gRPC server. func grpcTestServer(ctx context.Context, t *testing.T) pulumirpc.ResourceProviderServer { defaultInfo, metadata, close := initialSetup() t.Cleanup(func() { assert.NoError(t, close()) }) @@ -379,6 +395,20 @@ func grpcTestServer(ctx context.Context, t *testing.T) pulumirpc.ResourceProvide return s } +// pfProviderTestServer returns an in-memory gRPC server already parameterized by the +// pfprovider test Terraform provider. +func pfProviderTestServer(ctx context.Context, t *testing.T) pulumirpc.ResourceProviderServer { + grpc := grpcTestServer(ctx, t) + t.Run("parameterize", assertGRPCCall(grpc.Parameterize, &pulumirpc.ParameterizeRequest{ + Parameters: &pulumirpc.ParameterizeRequest_Args{ + Args: &pulumirpc.ParameterizeRequest_ParametersArgs{ + Args: []string{pfProviderPath(t)}, + }, + }, + }, noParallel)) + return grpc +} + func skipWindows(t *testing.T) { t.Helper() if runtime.GOOS != "windows" { diff --git a/dynamic/test/pfprovider/panic_resource.go b/dynamic/test/pfprovider/panic_resource.go new file mode 100644 index 000000000..e2b401368 --- /dev/null +++ b/dynamic/test/pfprovider/panic_resource.go @@ -0,0 +1,79 @@ +package main + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &panicResource{} +var _ resource.ResourceWithImportState = &panicResource{} + +func NewPanicResource() resource.Resource { return &panicResource{} } + +// panicResource defines the resource implementation. +type panicResource struct{} + +func (r *panicResource) Metadata( + ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse, +) { + resp.TypeName = req.ProviderTypeName + "_panic" +} + +func (r *panicResource) Schema( + ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse, +) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "Example resource", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Example identifier", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + } +} + +func (r *panicResource) Configure( + ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse, +) { +} + +func (r *panicResource) Create( + ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse, +) { + panic("PANIC MESSAGE HERE") +} + +func (r *panicResource) Read( + ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse, +) { + panic("PANIC MESSAGE HERE") +} + +func (r *panicResource) Update( + ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse, +) { + panic("PANIC MESSAGE HERE") +} + +func (r *panicResource) Delete( + ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse, +) { + panic("PANIC MESSAGE HERE") +} + +func (r *panicResource) ImportState( + ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse, +) { + panic("PANIC MESSAGE HERE") +} diff --git a/dynamic/test/pfprovider/provider.go b/dynamic/test/pfprovider/provider.go index 4b7feaaa2..c05423dc2 100644 --- a/dynamic/test/pfprovider/provider.go +++ b/dynamic/test/pfprovider/provider.go @@ -90,6 +90,7 @@ func (p *PFProvider) Configure( func (p *PFProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ NewExampleResource, + NewPanicResource, } } diff --git a/dynamic/testdata/TestStacktraceDisplayed/parameterize.golden b/dynamic/testdata/TestStacktraceDisplayed/parameterize.golden new file mode 100644 index 000000000..e5bae4b9b --- /dev/null +++ b/dynamic/testdata/TestStacktraceDisplayed/parameterize.golden @@ -0,0 +1,4 @@ +{ + "name": "pfprovider", + "version": "0.0.0" +} \ No newline at end of file