Skip to content

Commit 4a92a2f

Browse files
Fix diffing assets and archives (#3115)
This PR fixes the diffing of resource properties which contain Pulumi Assets or Archives. We were not correctly handling the assets coming from the old properties both for with RawStateDeltas enabled and without. Instead we were using the new property assets in place of the old ones and that produced no diff for these properties. This fixes both: - Without raw state deltas, the asset table is parsed as part of transforming the property values to the TF domain - we can use these in the diff algorithm. - With raw state deltas, we now add a recursive pass over the old property values, which produces an asset table. fixes #3102
1 parent 50f06f7 commit 4a92a2f

File tree

6 files changed

+231
-16
lines changed

6 files changed

+231
-16
lines changed

pkg/tests/schema_pulumi_test.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,8 @@ func TestAssetDiff(t *testing.T) {
836836

837837
prev := pt.Preview(t, optpreview.Diff())
838838

839+
require.Contains(t, prev.StdOut, "- testPath: asset(file:2cf24db)")
840+
require.Contains(t, prev.StdOut, "+ testPath: asset(file:486ea46)")
839841
require.Contains(t, prev.StdOut, "~ 1 to update")
840842

841843
pt.Up(t)
@@ -875,7 +877,10 @@ func TestAssetDiff(t *testing.T) {
875877
~ prov:index/test:Test: (update)
876878
[id=newid]
877879
[urn=urn:pulumi:test::test::prov:index/test:Test::mainRes]
878-
testPath : asset(text:2cf24db) {
880+
- testPath: asset(text:2cf24db) {
881+
<contents elided>
882+
}
883+
+ testPath: asset(text:486ea46) {
879884
<contents elided>
880885
}
881886
Resources:
@@ -950,6 +955,7 @@ func TestArchiveDiff(t *testing.T) {
950955

951956
prev := pt.Preview(t, optpreview.Diff())
952957

958+
require.Contains(t, prev.StdOut, "~ testPath: archive(file:8933f25->e22d32b)")
953959
require.Contains(t, prev.StdOut, "~ 1 to update")
954960

955961
pt.Up(t)
@@ -997,12 +1003,10 @@ func TestArchiveDiff(t *testing.T) {
9971003
~ prov:index/test:Test: (update)
9981004
[id=newid]
9991005
[urn=urn:pulumi:test::test::prov:index/test:Test::mainRes]
1000-
testPath : archive(assets:2a03253) {
1001-
"file1": asset(text:2cf24db) {
1002-
<contents elided>
1006+
~ testPath: archive(assets:2a03253->4c74cbf) {
1007+
~ "file1": asset(text:2cf24db->91e9240) {"<contents elided>" => "<contents elided>"
10031008
}
1004-
"file2": asset(text:486ea46) {
1005-
<contents elided>
1009+
~ "file2": asset(text:486ea46->da4c6d4) {"<contents elided>" => "<contents elided>"
10061010
}
10071011
}
10081012
Resources:

pkg/tfbridge/detailed_diff.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -829,6 +829,7 @@ func makeDetailedDiffV2(
829829
state shim.InstanceState,
830830
diff shim.InstanceDiff,
831831
assets AssetTable,
832+
oldAssets AssetTable,
832833
supportsSecrets bool,
833834
newInputs resource.PropertyMap,
834835
replaceOverride *bool,
@@ -848,7 +849,7 @@ func makeDetailedDiffV2(
848849
if err != nil {
849850
return nil, err
850851
}
851-
priorProps, err := MakeTerraformResult(ctx, prov, prior, tfs, ps, assets, supportsSecrets)
852+
priorProps, err := MakeTerraformResult(ctx, prov, prior, tfs, ps, oldAssets, supportsSecrets)
852853
if err != nil {
853854
return nil, err
854855
}

pkg/tfbridge/property_path.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"github.com/pulumi/pulumi-terraform-bridge/v3/unstable/propertyvalue"
1212
)
1313

14+
// propertyPath is a wrapper around resource.PropertyPath that adds some convenience methods.
15+
// If the path is constructed using the propertyPath methods, it will only have string and int elements.
1416
type propertyPath resource.PropertyPath
1517

1618
func isForceNew(tfs shim.Schema, ps *SchemaInfo) bool {

pkg/tfbridge/provider.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,13 +1125,19 @@ func (p *Provider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*pulum
11251125
}
11261126

11271127
var state shim.InstanceState
1128+
var oldAssets AssetTable
11281129
if pNew, enabled := makeTerraformStateViaUpgradeEnabled(p.info, p.tf, olds); enabled {
11291130
state, err = makeTerraformStateViaUpgrade(ctx, pNew, res, olds)
11301131
if err != nil {
11311132
return nil, errors.Wrapf(err, "unmarshaling %s's instance state via upgrade", urn)
11321133
}
1134+
1135+
if res.Schema != nil {
1136+
oldAssets = getAssetTable(olds, res.Schema.Fields, res.TF.Schema())
1137+
}
11331138
} else {
1134-
state, err = makeTerraformStateWithOpts(ctx, res, req.GetId(), olds, makeTerraformStateOptions(opts))
1139+
state, oldAssets, err = makeTerraformStateWithAssetsWithOpts(
1140+
ctx, res, req.GetId(), olds, makeTerraformStateOptions(opts))
11351141
if err != nil {
11361142
return nil, errors.Wrapf(err, "unmarshaling %s's instance state", urn)
11371143
}
@@ -1176,7 +1182,7 @@ func (p *Provider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*pulum
11761182

11771183
replaceDecision := diff.RequiresNew()
11781184
detailedDiff, err = makeDetailedDiffV2(
1179-
ctx, schema, fields, res.TF, p.tf, state, diff, assets, p.supportsSecrets, news, &replaceDecision)
1185+
ctx, schema, fields, res.TF, p.tf, state, diff, assets, oldAssets, p.supportsSecrets, news, &replaceDecision)
11801186
if err != nil {
11811187
return nil, err
11821188
}

pkg/tfbridge/schema.go

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/log"
3636
shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim"
3737
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/schema"
38+
"github.com/pulumi/pulumi-terraform-bridge/v3/unstable/propertyvalue"
3839
)
3940

4041
// This file deals with translating between the Pulumi representations of a resource's configuration and state and the
@@ -1345,27 +1346,43 @@ func makeTerraformStateViaUpgradeEnabled(
13451346
return pp, true
13461347
}
13471348

1348-
// The old method used when [makeTerraformStateViaUpgrade] is not available.
1349-
func makeTerraformStateWithOpts(
1349+
// makeTerraformStateWithAssetsWithOpts is used in Diff when [makeTerraformStateViaUpgrade] is not available.
1350+
func makeTerraformStateWithAssetsWithOpts(
13501351
ctx context.Context,
13511352
res Resource,
13521353
id string,
13531354
m resource.PropertyMap,
13541355
opts makeTerraformStateOptions,
1355-
) (shim.InstanceState, error) {
1356+
) (shim.InstanceState, AssetTable, error) {
13561357
// Turn the resource properties into a map. For the most part, this is a straight
13571358
// Mappable, but we use MapReplace because we use float64s and Terraform uses
13581359
// ints, to represent numbers.
1359-
inputs, _, err := makeTerraformInputsWithOptions(ctx, nil, nil, nil, m, res.TF.Schema(), res.Schema.Fields,
1360+
inputs, assets, err := makeTerraformInputsWithOptions(ctx, nil, nil, nil, m, res.TF.Schema(), res.Schema.Fields,
13601361
makeTerraformInputsOptions{DisableDefaults: true, DisableTFDefaults: true})
13611362
if err != nil {
1362-
return nil, err
1363+
return nil, nil, err
13631364
}
13641365
meta, err := parseMeta(m, res, opts)
13651366
if err != nil {
1366-
return nil, err
1367+
return nil, nil, err
1368+
}
1369+
instanceState, err := res.TF.InstanceState(id, inputs, meta)
1370+
if err != nil {
1371+
return nil, nil, err
13671372
}
1368-
return res.TF.InstanceState(id, inputs, meta)
1373+
return instanceState, assets, nil
1374+
}
1375+
1376+
// The old method used when [makeTerraformStateViaUpgrade] is not available.
1377+
func makeTerraformStateWithOpts(
1378+
ctx context.Context,
1379+
res Resource,
1380+
id string,
1381+
m resource.PropertyMap,
1382+
opts makeTerraformStateOptions,
1383+
) (shim.InstanceState, error) {
1384+
state, _, err := makeTerraformStateWithAssetsWithOpts(ctx, res, id, m, opts)
1385+
return state, err
13691386
}
13701387

13711388
// The preferred method for recreating TF state that is used when the Pulumi state was written with a recent enough
@@ -1866,3 +1883,23 @@ func ExtractInputsFromOutputs(oldInputs, outs resource.PropertyMap,
18661883
// Otherwise, take a schema-directed approach that fills out all input-only properties.
18671884
return extractSchemaInputsObject(outs, tfs, ps), nil
18681885
}
1886+
1887+
func getAssetTable(m resource.PropertyMap, ps map[string]*SchemaInfo, tfs shim.SchemaMap) AssetTable {
1888+
assets := AssetTable{}
1889+
val := resource.NewObjectProperty(m)
1890+
_, err := propertyvalue.TransformPropertyValue(
1891+
resource.PropertyPath{},
1892+
func(p resource.PropertyPath, v resource.PropertyValue) (resource.PropertyValue, error) {
1893+
if v.IsAsset() || v.IsArchive() {
1894+
schPath := PropertyPathToSchemaPath(p, tfs, ps)
1895+
info := LookupSchemaInfoMapPath(schPath, ps)
1896+
contract.Assertf(info != nil, "unexpected asset %s", p.String())
1897+
assets[info] = v
1898+
}
1899+
return v, nil
1900+
},
1901+
val,
1902+
)
1903+
contract.AssertNoErrorf(err, "failed to transform property value")
1904+
return assets
1905+
}

pkg/tfbridge/schema_test.go

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

3838
"github.com/pulumi/pulumi-terraform-bridge/v3/internal/testprovider"
3939
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/reservedkeys"
40+
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info"
4041
shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim"
4142
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/schema"
4243
shimv1 "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/sdk-v1"
@@ -4222,3 +4223,167 @@ func Test_makeTerraformStateWithOptsMaxItemsOneAdded(t *testing.T) {
42224223
autogold.Expect(map[string]interface{}{"props": []interface{}{"X"}}).Equal(t, inputs)
42234224
})
42244225
}
4226+
4227+
func TestGetAssetTable(t *testing.T) {
4228+
t.Parallel()
4229+
4230+
t.Run("simple asset", func(t *testing.T) {
4231+
asset, err := resource.NewTextAsset("hello world")
4232+
require.NoError(t, err)
4233+
assetProp := resource.NewAssetProperty(asset)
4234+
props := resource.PropertyMap{"foo": assetProp}
4235+
ps := map[string]*SchemaInfo{"foo": {Asset: &AssetTranslation{Kind: FileAsset}}}
4236+
tfs := shimv2.NewSchemaMap(map[string]*schemav2.Schema{
4237+
"foo": {
4238+
Type: schemav2.TypeString,
4239+
Optional: true,
4240+
},
4241+
})
4242+
assets := getAssetTable(props, ps, tfs)
4243+
require.Len(t, assets, 1)
4244+
for info, v := range assets {
4245+
assert.Same(t, ps["foo"], info)
4246+
assert.True(t, v.DeepEquals(assetProp))
4247+
}
4248+
})
4249+
4250+
t.Run("no assets present", func(t *testing.T) {
4251+
props := resource.PropertyMap{"bar": resource.NewStringProperty("baz")}
4252+
ps := map[string]*SchemaInfo{"bar": {}}
4253+
tfs := shimv2.NewSchemaMap(map[string]*schemav2.Schema{
4254+
"bar": {
4255+
Type: schemav2.TypeString,
4256+
Optional: true,
4257+
},
4258+
})
4259+
assets := getAssetTable(props, ps, tfs)
4260+
assert.Empty(t, assets)
4261+
})
4262+
4263+
t.Run("archive present", func(t *testing.T) {
4264+
asset, err := resource.NewTextAsset("hello world")
4265+
require.NoError(t, err)
4266+
archive, err := resource.NewAssetArchive(map[string]interface{}{"file.txt": asset})
4267+
require.NoError(t, err)
4268+
archiveProp := resource.NewArchiveProperty(archive)
4269+
props := resource.PropertyMap{"arch": archiveProp}
4270+
ps := map[string]*SchemaInfo{"arch": {Asset: &AssetTranslation{Kind: FileArchive}}}
4271+
tfs := shimv2.NewSchemaMap(map[string]*schemav2.Schema{
4272+
"arch": {
4273+
Type: schemav2.TypeString,
4274+
Optional: true,
4275+
},
4276+
})
4277+
assets := getAssetTable(props, ps, tfs)
4278+
require.Len(t, assets, 1)
4279+
for info, v := range assets {
4280+
assert.Same(t, ps["arch"], info)
4281+
assert.True(t, v.DeepEquals(archiveProp))
4282+
}
4283+
})
4284+
4285+
t.Run("asset with no matching SchemaInfo", func(t *testing.T) {
4286+
asset, err := resource.NewTextAsset("hello world")
4287+
require.NoError(t, err)
4288+
assetProp := resource.NewAssetProperty(asset)
4289+
props := resource.PropertyMap{"missing": assetProp}
4290+
ps := map[string]*SchemaInfo{}
4291+
tfs := shimv2.NewSchemaMap(map[string]*schemav2.Schema{
4292+
"bar": {
4293+
Type: schemav2.TypeString,
4294+
Optional: true,
4295+
},
4296+
})
4297+
defer func() {
4298+
if r := recover(); r == nil {
4299+
assert.Fail(t, "panic did not occur", r)
4300+
}
4301+
}()
4302+
getAssetTable(props, ps, tfs)
4303+
})
4304+
4305+
t.Run("nested asset", func(t *testing.T) {
4306+
asset, err := resource.NewTextAsset("hello world")
4307+
require.NoError(t, err)
4308+
nestedAsset := resource.NewAssetProperty(asset)
4309+
nestedProps := resource.PropertyMap{"outer": resource.NewObjectProperty(resource.PropertyMap{"inner": nestedAsset})}
4310+
nestedPS := map[string]*SchemaInfo{
4311+
"outer": {
4312+
Elem: &info.Schema{Fields: map[string]*SchemaInfo{
4313+
"inner": {Asset: &AssetTranslation{Kind: FileAsset}},
4314+
}},
4315+
},
4316+
}
4317+
tfs := shimv2.NewSchemaMap(map[string]*schemav2.Schema{
4318+
"outer": {
4319+
Type: schemav2.TypeList,
4320+
Optional: true,
4321+
MaxItems: 1,
4322+
Elem: &schemav2.Resource{
4323+
Schema: map[string]*schemav2.Schema{
4324+
"inner": {Type: schemav2.TypeString, Optional: true},
4325+
},
4326+
},
4327+
},
4328+
})
4329+
assets := getAssetTable(nestedProps, nestedPS, tfs)
4330+
found := false
4331+
for info, v := range assets {
4332+
if info == nestedPS["outer"].Elem.Fields["inner"] && v.DeepEquals(nestedAsset) {
4333+
found = true
4334+
}
4335+
}
4336+
assert.True(t, found)
4337+
})
4338+
4339+
t.Run("multiple assets", func(t *testing.T) {
4340+
asset1, err := resource.NewTextAsset("hello world")
4341+
require.NoError(t, err)
4342+
asset2, err := resource.NewTextAsset("another asset")
4343+
require.NoError(t, err)
4344+
assetProp1 := resource.NewAssetProperty(asset1)
4345+
assetProp2 := resource.NewAssetProperty(asset2)
4346+
props := resource.PropertyMap{"foo": assetProp1, "bar": assetProp2}
4347+
ps := map[string]*SchemaInfo{
4348+
"foo": {Asset: &AssetTranslation{Kind: FileAsset}},
4349+
"bar": {Asset: &AssetTranslation{Kind: FileAsset}},
4350+
}
4351+
tfs := shimv2.NewSchemaMap(map[string]*schemav2.Schema{
4352+
"foo": {Type: schemav2.TypeString, Optional: true},
4353+
"bar": {Type: schemav2.TypeString, Optional: true},
4354+
})
4355+
assets := getAssetTable(props, ps, tfs)
4356+
assert.Len(t, assets, 2)
4357+
assert.Contains(t, assets, ps["foo"])
4358+
assert.Contains(t, assets, ps["bar"])
4359+
assert.True(t, assets[ps["foo"]].DeepEquals(assetProp1))
4360+
assert.True(t, assets[ps["bar"]].DeepEquals(assetProp2))
4361+
})
4362+
4363+
t.Run("asset with nil SchemaInfo", func(t *testing.T) {
4364+
asset, err := resource.NewTextAsset("hello world")
4365+
require.NoError(t, err)
4366+
assetProp := resource.NewAssetProperty(asset)
4367+
props := resource.PropertyMap{"foo": assetProp}
4368+
ps := map[string]*SchemaInfo{"foo": nil}
4369+
tfs := shimv2.NewSchemaMap(map[string]*schemav2.Schema{
4370+
"foo": {Type: schemav2.TypeString, Optional: true},
4371+
})
4372+
defer func() {
4373+
if r := recover(); r == nil {
4374+
assert.Fail(t, "panic did not occur", r)
4375+
}
4376+
}()
4377+
getAssetTable(props, ps, tfs)
4378+
})
4379+
4380+
t.Run("non-asset value with asset SchemaInfo", func(t *testing.T) {
4381+
props := resource.PropertyMap{"foo": resource.NewStringProperty("not an asset")}
4382+
ps := map[string]*SchemaInfo{"foo": {Asset: &AssetTranslation{Kind: FileAsset}}}
4383+
tfs := shimv2.NewSchemaMap(map[string]*schemav2.Schema{
4384+
"foo": {Type: schemav2.TypeString, Optional: true},
4385+
})
4386+
assets := getAssetTable(props, ps, tfs)
4387+
assert.Empty(t, assets)
4388+
})
4389+
}

0 commit comments

Comments
 (0)