Skip to content

Commit 66ab90d

Browse files
authored
[dynamic] Support delete on sdkv1 based providers (#2513)
Explicitly set `PlannedState: null` instead of leaving it empty. As described by https://github.com/hashicorp/terraform-plugin-framework/blob/ce2519cf40d45d28eebd81776019e68d1bddca6f/internal/fwserver/server_applyresourcechange.go#L63, PF allows Null (not just nil) values to be used to send a delete. SDKv1 providers *always* attempt to deserialize planned state. This is fine if it's null, but hits a nil pointer panic when it is nil. Fixes pulumi/pulumi-terraform-provider#36
1 parent a1be35d commit 66ab90d

File tree

8 files changed

+183
-34
lines changed

8 files changed

+183
-34
lines changed

dynamic/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ replace (
1515

1616
require (
1717
github.com/blang/semver v3.5.1+incompatible
18+
github.com/hashicorp/terraform-plugin-sdk v1.7.0
1819
github.com/hexops/autogold/v2 v2.2.1
1920
github.com/opentofu/opentofu/shim v0.0.0-00010101000000-000000000000
2021
github.com/pulumi/pulumi-terraform-bridge/v3 v3.92.0

dynamic/go.sum

Lines changed: 62 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,44 @@
11
package testing
22

3-
import "testing"
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"sync"
9+
"testing"
10+
11+
"github.com/stretchr/testify/require"
12+
)
413

514
func Integration(t *testing.T) {
615
t.Helper()
716
if testing.Short() {
817
t.Skipf("Skipping integration test during -short")
918
}
1019
}
20+
21+
func BuildOnce(globalTempDir *string, dir, name string) func(t *testing.T) string {
22+
mkBin := sync.OnceValues(func() (string, error) {
23+
wd, err := os.Getwd()
24+
if err != nil {
25+
return "", err
26+
}
27+
28+
out := filepath.Join(*globalTempDir, name)
29+
cmd := exec.Command("go", "build", "-o", out, ".")
30+
cmd.Dir = filepath.Join(wd, dir)
31+
stdoutput, err := cmd.CombinedOutput()
32+
if err != nil {
33+
return "", fmt.Errorf("failed to build provider: %w:\n%s", err, string(stdoutput))
34+
}
35+
return out, nil
36+
})
37+
38+
return func(t *testing.T) string {
39+
t.Helper()
40+
path, err := mkBin()
41+
require.NoErrorf(t, err, "failed find provider path")
42+
return path
43+
}
44+
}

dynamic/provider_test.go

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@ import (
66
"encoding/json"
77
"fmt"
88
"os"
9-
"os/exec"
109
"path/filepath"
1110
"runtime"
1211
"strings"
13-
"sync"
1412
"testing"
1513

1614
"github.com/hexops/autogold/v2"
@@ -53,7 +51,7 @@ func TestStacktraceDisplayed(t *testing.T) {
5351
skipWindows(t)
5452

5553
ctx := context.Background()
56-
grpc := pfProviderTestServer(ctx, t)
54+
grpc := parameterizedTestServer(ctx, t, pfProviderPath)
5755

5856
_, err := grpc.Create(ctx, &pulumirpc.CreateRequest{
5957
Urn: string(resource.NewURN(
@@ -366,30 +364,12 @@ func assertGRPC(t *testing.T, msg proto.Message, v autogold.Value) {
366364
// pfProviderPath returns the path the the PF provider binary for use in testing.
367365
//
368366
// It builds the binary running "go build" once per session.
369-
var pfProviderPath = func() func(t *testing.T) string {
370-
mkBin := sync.OnceValues(func() (string, error) {
371-
wd, err := os.Getwd()
372-
if err != nil {
373-
return "", err
374-
}
375-
376-
out := filepath.Join(globalTempDir, "terraform-provider-pfprovider")
377-
cmd := exec.Command("go", "build", "-o", out, "github.com/pulumi/pulumi-terraform-bridge/dynamic/tests/pfprovider")
378-
cmd.Dir = filepath.Join(wd, "test", "pfprovider")
379-
stdoutput, err := cmd.CombinedOutput()
380-
if err != nil {
381-
return "", fmt.Errorf("failed to build provider: %w:\n%s", err, string(stdoutput))
382-
}
383-
return out, nil
384-
})
385-
386-
return func(t *testing.T) string {
387-
t.Helper()
388-
path, err := mkBin()
389-
require.NoErrorf(t, err, "failed find provider path")
390-
return path
391-
}
392-
}()
367+
var (
368+
pfProviderPath = helper.BuildOnce(&globalTempDir,
369+
"test/pfprovider", "terraform-provider-pfprovider")
370+
sdkv1ProviderPath = helper.BuildOnce(&globalTempDir,
371+
"test/sdkv1provider", "terraform-provider-sdkv1")
372+
)
393373

394374
// grpcTestServer returns an unparameterized in-memory gRPC server.
395375
func grpcTestServer(ctx context.Context, t *testing.T) pulumirpc.ResourceProviderServer {
@@ -400,14 +380,15 @@ func grpcTestServer(ctx context.Context, t *testing.T) pulumirpc.ResourceProvide
400380
return s
401381
}
402382

403-
// pfProviderTestServer returns an in-memory gRPC server already parameterized by the
404-
// pfprovider test Terraform provider.
405-
func pfProviderTestServer(ctx context.Context, t *testing.T) pulumirpc.ResourceProviderServer {
383+
func parameterizedTestServer(
384+
ctx context.Context, t *testing.T,
385+
pathHelper func(t *testing.T) string,
386+
) pulumirpc.ResourceProviderServer {
406387
grpc := grpcTestServer(ctx, t)
407388
t.Run("parameterize", assertGRPCCall(grpc.Parameterize, &pulumirpc.ParameterizeRequest{
408389
Parameters: &pulumirpc.ParameterizeRequest_Args{
409390
Args: &pulumirpc.ParameterizeRequest_ParametersArgs{
410-
Args: []string{pfProviderPath(t)},
391+
Args: []string{pathHelper(t)},
411392
},
412393
},
413394
}, noParallel))
@@ -536,6 +517,30 @@ func TestRandomCreate(t *testing.T) {
536517
})
537518
}
538519

520+
func TestSDKv1Provider(t *testing.T) {
521+
t.Parallel()
522+
helper.Integration(t)
523+
skipWindows(t)
524+
525+
ctx := context.Background()
526+
527+
server := parameterizedTestServer(ctx, t, sdkv1ProviderPath)
528+
529+
const typ = "sdkv1:index/res:Res"
530+
urn := string(resource.NewURN(
531+
"test", "test", "", typ, "res",
532+
))
533+
534+
t.Run("delete", assertGRPCCall(server.Delete, &pulumirpc.DeleteRequest{
535+
Id: "example-id-delete",
536+
Urn: urn,
537+
Properties: marshal(resource.PropertyMap{
538+
"f0": resource.NewProperty("123"),
539+
"f1": resource.NewProperty(123.0),
540+
}),
541+
}))
542+
}
543+
539544
func must[T any](v T, err error) T {
540545
if err != nil {
541546
panic(err)

dynamic/test/sdkv1provider/main.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package main
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
5+
"github.com/hashicorp/terraform-plugin-sdk/plugin"
6+
"github.com/hashicorp/terraform-plugin-sdk/terraform"
7+
)
8+
9+
func Provider() *schema.Provider {
10+
return &schema.Provider{
11+
ConfigureFunc: func(*schema.ResourceData) (any, error) {
12+
return nil, nil
13+
},
14+
ResourcesMap: map[string]*schema.Resource{
15+
"test_res": {
16+
Schema: map[string]*schema.Schema{
17+
"f0": {Type: schema.TypeString, Required: true},
18+
"f1": {Type: schema.TypeInt, Computed: true},
19+
},
20+
Update: func(*schema.ResourceData, interface{}) error {
21+
return nil
22+
},
23+
Delete: func(*schema.ResourceData, any) error {
24+
return nil
25+
},
26+
},
27+
},
28+
}
29+
}
30+
31+
func main() {
32+
plugin.Serve(&plugin.ServeOpts{
33+
ProviderFunc: func() terraform.ResourceProvider {
34+
return Provider()
35+
},
36+
})
37+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "sdkv1",
3+
"version": "0.0.0"
4+
}

pkg/pf/tfbridge/provider_delete.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
2121
"github.com/hashicorp/terraform-plugin-go/tftypes"
2222
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
23+
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
2324

2425
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/convert"
2526
)
@@ -52,13 +53,17 @@ func (p *provider) DeleteWithContext(
5253
return resource.StatusOK, err
5354
}
5455

56+
plannedState, err := tfprotov6.NewDynamicValue(tfType, tftypes.NewValue(tfType, nil))
57+
contract.AssertNoErrorf(err, "nil is always a valid value to marshal to a dynamic state")
58+
5559
// terraform-plugin-framework recognizes PlannedState=nil ApplyResourceChangeRequest request as DELETE.
5660
//
5761
//nolint:lll // See
5862
// https://github.com/hashicorp/terraform-plugin-framework/blob/ce2519cf40d45d28eebd81776019e68d1bddca6f/internal/fwserver/server_applyresourcechange.go#L63
5963
req := tfprotov6.ApplyResourceChangeRequest{
60-
TypeName: rh.terraformResourceName,
61-
PriorState: priorState,
64+
TypeName: rh.terraformResourceName,
65+
PriorState: priorState,
66+
PlannedState: &plannedState,
6267
}
6368

6469
resp, err := p.tfServer.ApplyResourceChange(ctx, &req)

0 commit comments

Comments
 (0)