Skip to content

Commit 67ba100

Browse files
Implement Dynamic Type serialization (#3099)
This PR adds support for correctly serializing and de-serializing properties with a Dynamic Type schema. This ensures that properties of this schema type can be correctly saved to state and then read back with any provider version. There are a few changes which had to be made to make this all work: 1. When marshalling values to JSON we now need to pass in the schema type in order to check if they are Dynamic type. Previously we were using the value type and assumed the two match. This is not correct for properties with a Dynamic schema type. We also thread this information through various places. 2. When recording `RawStateDeltas` (these are the "diffs" between the TF state and Pulumi state), we need to make sure that properties with a Dynamic schema type always get a `Replace` delta. This is a full recording of the TF value, instead of a "diff" with the pulumi state. This is because the JSON representation of properties with Dynamic schema type need to contain their value type as well as the value itself. This is then used to read the state back into the correct runtime type. 3. Added a `cty` and `hcty` valueshim implementations for the `cty` package and the Hashi fork of the `cty` package. This is necessary because the SDKv1 uses the `cty` package while the SDKv2 uses the `hcty` package. The two implementations are practically identical. 4. Some unit and integration tests to make sure this all works correctly and stays that way. fixes #3078 --------- Co-authored-by: Anton Tayanovskyy <[email protected]>
1 parent 246c306 commit 67ba100

31 files changed

+1375
-243
lines changed

pkg/pf/internal/pfutils/raw_state.go

Lines changed: 0 additions & 28 deletions
This file was deleted.

pkg/pf/internal/schemashim/datasource.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@
1515
package schemashim
1616

1717
import (
18+
"context"
19+
20+
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
21+
1822
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/internalinter"
1923
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/pf/internal/runtypes"
2024
shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim"
25+
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/valueshim"
2126
)
2227

2328
type schemaOnlyDataSource struct {
@@ -27,6 +32,12 @@ type schemaOnlyDataSource struct {
2732

2833
var _ shim.Resource = (*schemaOnlyDataSource)(nil)
2934

35+
func (r *schemaOnlyDataSource) SchemaType() valueshim.Type {
36+
protoSchema, err := r.tf.ResourceProtoSchema(context.Background())
37+
contract.AssertNoErrorf(err, "ResourceProtoSchema failed")
38+
return valueshim.FromTType(protoSchema.ValueType())
39+
}
40+
3041
func (r *schemaOnlyDataSource) Schema() shim.SchemaMap {
3142
return r.tf.Shim()
3243
}

pkg/pf/internal/schemashim/object_pseudoresource.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/internalinter"
2929
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/pf/internal/pfutils"
3030
shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim"
31+
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/valueshim"
3132
)
3233

3334
// An Object type that masquerades as a Resource. This is a workaround to reusing tfgen code for generating schemas,
@@ -77,6 +78,10 @@ func (r *objectPseudoResource) Schema() shim.SchemaMap {
7778
return r
7879
}
7980

81+
func (r *objectPseudoResource) SchemaType() valueshim.Type {
82+
return valueshim.FromTType(tftypes.Object{})
83+
}
84+
8085
func (*objectPseudoResource) SchemaVersion() int {
8186
panic("This is an Object type encoded as a shim.Resource, and " +
8287
"SchemaVersion() should not be called on this entity during schema generation")
@@ -200,6 +205,10 @@ func newTuplePseudoResource(t attr.TypeWithElementTypes) shim.Resource {
200205
}
201206
}
202207

208+
func (r *tuplePseudoResource) SchemaType() valueshim.Type {
209+
return valueshim.FromTType(tftypes.Object{})
210+
}
211+
203212
func (*tuplePseudoResource) SchemaVersion() int { panic("TODO") }
204213
func (*tuplePseudoResource) DeprecationMessage() string { panic("TODO") }
205214

pkg/pf/internal/schemashim/resource.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@
1515
package schemashim
1616

1717
import (
18+
"context"
19+
20+
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
21+
1822
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/internalinter"
1923
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/pf/internal/runtypes"
2024
shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim"
25+
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/valueshim"
2126
)
2227

2328
type schemaOnlyResource struct {
@@ -43,6 +48,12 @@ func (r *schemaOnlyResource) DeprecationMessage() string {
4348
return r.tf.DeprecationMessage()
4449
}
4550

51+
func (r *schemaOnlyResource) SchemaType() valueshim.Type {
52+
s, err := r.tf.ResourceProtoSchema(context.Background())
53+
contract.AssertNoErrorf(err, "failed to extract schema")
54+
return valueshim.FromTType(s.ValueType())
55+
}
56+
4657
func (*schemaOnlyResource) Importer() shim.ImportFunc {
4758
panic("schemaOnlyResource does not implement runtime operation ImporterFunc")
4859
}

pkg/pf/proto/element.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919

2020
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/internalinter"
2121
shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim"
22+
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/valueshim"
2223
)
2324

2425
var (
@@ -113,6 +114,10 @@ func (o elementObject) Schema() shim.SchemaMap {
113114
return elementObjectMap(o.typ)
114115
}
115116

117+
func (o elementObject) SchemaType() valueshim.Type {
118+
return valueshim.FromTType(o.typ)
119+
}
120+
116121
func (m elementObjectMap) Len() int { return len(m.AttributeTypes) }
117122

118123
func (m elementObjectMap) Get(key string) shim.Schema { return getSchemaMap(m, key) }

pkg/pf/proto/object.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020

2121
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/internalinter"
2222
shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim"
23+
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/valueshim"
2324
)
2425

2526
var (
@@ -33,6 +34,11 @@ type object struct {
3334
internalinter.Internal
3435
}
3536

37+
func (o object) SchemaType() valueshim.Type {
38+
ty := o.obj.ValueType()
39+
return valueshim.FromTType(ty)
40+
}
41+
3642
func (o object) Schema() shim.SchemaMap {
3743
contract.Assertf(o.obj.Nesting != tfprotov6.SchemaObjectNestingModeMap,
3844
"%T cannot be a map, since that would require `o` to represent a Map<Object> type", o)

pkg/pf/proto/resource.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020

2121
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/internalinter"
2222
shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim"
23+
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/valueshim"
2324
)
2425

2526
var (
@@ -70,6 +71,11 @@ func newResource(r *tfprotov6.Schema) *resource {
7071
return &resource{r, internalinter.Internal{}}
7172
}
7273

74+
func (r resource) SchemaType() valueshim.Type {
75+
ty := r.r.Block.ValueType()
76+
return valueshim.FromTType(ty)
77+
}
78+
7379
func (r resource) Schema() shim.SchemaMap {
7480
return blockMap{r.r.Block}
7581
}

pkg/pf/proto/schema.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,19 @@
1515
package proto
1616

1717
import (
18-
// "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
18+
"github.com/hashicorp/terraform-plugin-go/tftypes"
1919

2020
shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim"
21+
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/valueshim"
2122
)
2223

2324
// pseudoResource represents a type that must pretent to be a [shim.Resource], but does not represent a resource.
2425
type pseudoResource struct{}
2526

27+
func (pseudoResource) SchemaType() valueshim.Type {
28+
return valueshim.FromTType(tftypes.Object{}) // not a top-level resource
29+
}
30+
2631
func (pseudoResource) SchemaVersion() int { return 0 }
2732
func (pseudoResource) Importer() shim.ImportFunc { return nil }
2833
func (pseudoResource) Timeouts() *shim.ResourceTimeout { return nil }

pkg/pf/tests/diff_test/diff_test.go

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ package tfbridgetests
33
import (
44
"context"
55
"math/big"
6-
"os"
76
"testing"
87

8+
"github.com/hashicorp/terraform-plugin-framework/attr"
99
"github.com/hashicorp/terraform-plugin-framework/resource"
1010
rschema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
1111
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
1212
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
13+
"github.com/hashicorp/terraform-plugin-framework/types"
1314
"github.com/hexops/autogold/v2"
14-
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
1515
"github.com/zclconf/go-cty/cty"
1616

1717
pb "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/pf/internal/providerbuilder"
@@ -184,10 +184,6 @@ func TestPFDetailedDiffStringAttribute(t *testing.T) {
184184

185185
func TestPFDetailedDiffDynamicType(t *testing.T) {
186186
t.Parallel()
187-
if d, ok := os.LookupEnv("PULUMI_RAW_STATE_DELTA_ENABLED"); ok && cmdutil.IsTruthy(d) {
188-
// TODO[pulumi/pulumi-terraform-bridge#3078]
189-
t.Skip("Does not work with PULUMI_RAW_STATE_DELTA_ENABLED=true")
190-
}
191187

192188
attributeSchema := rschema.Schema{
193189
Attributes: map[string]rschema.Attribute{
@@ -222,7 +218,7 @@ func TestPFDetailedDiffDynamicType(t *testing.T) {
222218
})
223219

224220
t.Run("type change", func(t *testing.T) {
225-
// TODO[pulumi/pulumi-terraform-bridge#3078]
221+
// TODO[pulumi/pulumi-terraform-bridge#3122]
226222
t.Skip(`Error converting tftypes.Number<"1"> (value2) at "AttributeName(\"key\")": can't unmarshal tftypes.Number into *string, expected string`)
227223
crosstests.Diff(t, res,
228224
map[string]cty.Value{"key": cty.StringVal("value")},
@@ -231,10 +227,56 @@ func TestPFDetailedDiffDynamicType(t *testing.T) {
231227
})
232228
}
233229

230+
func TestPFDetailedDiffNestedDynamicType(t *testing.T) {
231+
t.Parallel()
232+
233+
attributeSchema := rschema.Schema{
234+
Attributes: map[string]rschema.Attribute{
235+
"key": rschema.ObjectAttribute{
236+
Optional: true,
237+
AttributeTypes: map[string]attr.Type{
238+
"nested": types.DynamicType,
239+
},
240+
},
241+
},
242+
}
243+
res := pb.NewResource(pb.NewResourceArgs{
244+
ResourceSchema: attributeSchema,
245+
})
246+
247+
t.Run("no change", func(t *testing.T) {
248+
crosstests.Diff(t, res,
249+
map[string]cty.Value{"key": cty.ObjectVal(map[string]cty.Value{"nested": cty.StringVal("value")})},
250+
map[string]cty.Value{"key": cty.ObjectVal(map[string]cty.Value{"nested": cty.StringVal("value")})},
251+
)
252+
})
253+
254+
t.Run("change", func(t *testing.T) {
255+
crosstests.Diff(t, res,
256+
map[string]cty.Value{"key": cty.ObjectVal(map[string]cty.Value{"nested": cty.StringVal("value")})},
257+
map[string]cty.Value{"key": cty.ObjectVal(map[string]cty.Value{"nested": cty.StringVal("value1")})},
258+
)
259+
})
260+
261+
t.Run("int no change", func(t *testing.T) {
262+
crosstests.Diff(t, res,
263+
map[string]cty.Value{"key": cty.ObjectVal(map[string]cty.Value{"nested": cty.NumberVal(big.NewFloat(1))})},
264+
map[string]cty.Value{"key": cty.ObjectVal(map[string]cty.Value{"nested": cty.NumberVal(big.NewFloat(1))})},
265+
)
266+
})
267+
268+
t.Run("type change", func(t *testing.T) {
269+
// TODO[pulumi/pulumi-terraform-bridge#3122]
270+
t.Skip(`Error converting tftypes.Number<"1"> (value2) at "AttributeName(\"key\")": can't unmarshal tftypes.Number into *string, expected string`)
271+
crosstests.Diff(t, res,
272+
map[string]cty.Value{"key": cty.ObjectVal(map[string]cty.Value{"nested": cty.StringVal("value")})},
273+
map[string]cty.Value{"key": cty.ObjectVal(map[string]cty.Value{"nested": cty.NumberVal(big.NewFloat(1))})},
274+
)
275+
})
276+
}
277+
234278
func TestPFDetailedDiffDynamicTypeWithMigration(t *testing.T) {
235279
t.Parallel()
236-
// TODO[pulumi/pulumi-terraform-bridge#3078]
237-
t.Skip("DynamicPseudoType is not supported")
238280

239281
attributeSchema := rschema.Schema{
240282
Attributes: map[string]rschema.Attribute{

pkg/pf/tfbridge/provider.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,8 @@ func (p *provider) returnTerraformConfig() (resource.PropertyMap, error) {
339339
}
340340
// use valueshim package to marshal tfConfigValue into raw json,
341341
// which can be unmarshaled into a map[string]interface{}
342-
configJSONMessage, err := valueshim.FromTValue(tfConfigValue).Marshal()
342+
value := valueshim.FromTValue(tfConfigValue)
343+
configJSONMessage, err := value.Marshal(value.Type())
343344
if err != nil {
344345
return nil, fmt.Errorf("error marshaling into raw JSON message: %v", err)
345346
}

0 commit comments

Comments
 (0)