@@ -159,7 +159,7 @@ func (s *GRPCProviderServer) UpgradeResourceIdentity(ctx context.Context, req *t
159
159
160
160
// now we need to turn the state into the default json representation, so
161
161
// that it can be re-decoded using the actual schema.
162
- val , err := JSONMapToStateValue (jsonMap , schemaBlock ) // TODO: Find out if we need resource identity version here
162
+ val , err := JSONMapToStateValue (jsonMap , schemaBlock )
163
163
if err != nil {
164
164
resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
165
165
return resp , nil
@@ -788,11 +788,43 @@ func (s *GRPCProviderServer) ConfigureProvider(ctx context.Context, req *tfproto
788
788
789
789
func (s * GRPCProviderServer ) ReadResource (ctx context.Context , req * tfprotov5.ReadResourceRequest ) (* tfprotov5.ReadResourceResponse , error ) {
790
790
ctx = logging .InitContext (ctx )
791
+ readFollowingImport := false
792
+
793
+ reqPrivate := req .Private
794
+
795
+ if reqPrivate != nil {
796
+ // unmarshal the private data
797
+ if len (reqPrivate ) > 0 {
798
+ newReqPrivate := make (map [string ]interface {})
799
+ if err := json .Unmarshal (reqPrivate , & newReqPrivate ); err != nil {
800
+ return nil , err
801
+ }
802
+ // This internal private field is set on a resource during ImportResourceState to help framework determine if
803
+ // the resource has been recently imported. We only need to read this once, so we immediately clear it after.
804
+ if _ , ok := newReqPrivate [terraform .ImportBeforeReadMetaKey ]; ok {
805
+ readFollowingImport = true
806
+ delete (newReqPrivate , terraform .ImportBeforeReadMetaKey )
807
+
808
+ if len (newReqPrivate ) == 0 {
809
+ // if there are no other private data, set the private data to nil
810
+ reqPrivate = nil
811
+ } else {
812
+ // set the new private data without the import key
813
+ bytes , err := json .Marshal (newReqPrivate )
814
+ if err != nil {
815
+ return nil , err
816
+ }
817
+ reqPrivate = bytes
818
+ }
819
+ }
820
+ }
821
+ }
822
+
791
823
resp := & tfprotov5.ReadResourceResponse {
792
824
// helper/schema did previously handle private data during refresh, but
793
825
// core is now going to expect this to be maintained in order to
794
826
// persist it in the state.
795
- Private : req . Private ,
827
+ Private : reqPrivate ,
796
828
}
797
829
798
830
res , ok := s .provider .ResourcesMap [req .TypeName ]
@@ -832,7 +864,7 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re
832
864
}
833
865
instanceState .RawState = stateVal
834
866
835
- // TODO: is there a more elegant way to do this? this requires us to look for the identity schema block again
867
+ var currentIdentityVal cty. Value
836
868
if req .CurrentIdentity != nil && req .CurrentIdentity .IdentityData != nil {
837
869
838
870
// convert req.CurrentIdentity to flat map identity structure
@@ -843,20 +875,20 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re
843
875
return resp , nil
844
876
}
845
877
846
- identityVal , err : = msgpack .Unmarshal (req .CurrentIdentity .IdentityData .MsgPack , identityBlock .ImpliedType ())
878
+ currentIdentityVal , err = msgpack .Unmarshal (req .CurrentIdentity .IdentityData .MsgPack , identityBlock .ImpliedType ())
847
879
if err != nil {
848
880
resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
849
881
return resp , nil
850
882
}
851
883
// Step 2: Turn cty.Value into flatmap representation
852
- identityAttrs := hcl2shim .FlatmapValueFromHCL2 (identityVal )
884
+ identityAttrs := hcl2shim .FlatmapValueFromHCL2 (currentIdentityVal )
853
885
// Step 3: Well, set it in the instanceState
854
886
instanceState .Identity = identityAttrs
855
887
}
856
888
857
889
private := make (map [string ]interface {})
858
- if len (req . Private ) > 0 {
859
- if err := json .Unmarshal (req . Private , & private ); err != nil {
890
+ if len (reqPrivate ) > 0 {
891
+ if err := json .Unmarshal (reqPrivate , & private ); err != nil {
860
892
resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
861
893
return resp , nil
862
894
}
@@ -929,6 +961,15 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re
929
961
return resp , nil
930
962
}
931
963
964
+ // If we're refreshing the resource state (excluding a recently imported resource), validate that the new identity isn't changing
965
+ if ! res .ResourceBehavior .MutableIdentity && ! readFollowingImport && ! currentIdentityVal .IsNull () && ! currentIdentityVal .RawEquals (newIdentityVal ) {
966
+ resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , fmt .Errorf ("Unexpected Identity Change: %s" , "During the read operation, the Terraform Provider unexpectedly returned a different identity then the previously stored one.\n \n " +
967
+ "This is always a problem with the provider and should be reported to the provider developer.\n \n " +
968
+ fmt .Sprintf ("Current Identity: %s\n \n " , currentIdentityVal .GoString ())+
969
+ fmt .Sprintf ("New Identity: %s" , newIdentityVal .GoString ())))
970
+ return resp , nil
971
+ }
972
+
932
973
newIdentityMP , err := msgpack .Marshal (newIdentityVal , identityBlock .ImpliedType ())
933
974
if err != nil {
934
975
resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
@@ -1052,6 +1093,7 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot
1052
1093
// turn the proposed state into a legacy configuration
1053
1094
cfg := terraform .NewResourceConfigShimmed (proposedNewStateVal , schemaBlock )
1054
1095
1096
+ var priorIdentityVal cty.Value
1055
1097
// add identity data to priorState
1056
1098
if req .PriorIdentity != nil && req .PriorIdentity .IdentityData != nil {
1057
1099
// convert req.PriorIdentity to flat map identity structure
@@ -1062,13 +1104,13 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot
1062
1104
return resp , nil
1063
1105
}
1064
1106
1065
- identityVal , err : = msgpack .Unmarshal (req .PriorIdentity .IdentityData .MsgPack , identityBlock .ImpliedType ())
1107
+ priorIdentityVal , err = msgpack .Unmarshal (req .PriorIdentity .IdentityData .MsgPack , identityBlock .ImpliedType ())
1066
1108
if err != nil {
1067
1109
resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
1068
1110
return resp , nil
1069
1111
}
1070
1112
// Step 2: Turn cty.Value into flatmap representation
1071
- identityAttrs := hcl2shim .FlatmapValueFromHCL2 (identityVal )
1113
+ identityAttrs := hcl2shim .FlatmapValueFromHCL2 (priorIdentityVal )
1072
1114
// Step 3: Well, set it in the priorState
1073
1115
priorState .Identity = identityAttrs
1074
1116
}
@@ -1088,7 +1130,6 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot
1088
1130
diff .Attributes ["id" ] = & terraform.ResourceAttrDiff {
1089
1131
NewComputed : true ,
1090
1132
}
1091
- // TODO: we could error here if a new Diff got no Identity set
1092
1133
}
1093
1134
1094
1135
if diff == nil || (len (diff .Attributes ) == 0 && len (diff .Identity ) == 0 ) {
@@ -1249,29 +1290,41 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot
1249
1290
}
1250
1291
}
1251
1292
1252
- // TODO: if schema defines identity, we should error if there's none written to newInstanceState
1253
1293
if res .Identity != nil {
1254
1294
identityBlock , err := s .getResourceIdentitySchemaBlock (req .TypeName )
1255
1295
if err != nil {
1256
1296
resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , fmt .Errorf ("getting identity schema failed for resource '%s': %w" , req .TypeName , err ))
1257
1297
return resp , nil
1258
1298
}
1259
1299
1260
- newIdentityVal , err := hcl2shim .HCL2ValueFromFlatmap (diff .Identity , identityBlock .ImpliedType ())
1300
+ plannedIdentityVal , err := hcl2shim .HCL2ValueFromFlatmap (diff .Identity , identityBlock .ImpliedType ())
1261
1301
if err != nil {
1262
1302
resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
1263
1303
return resp , nil
1264
1304
}
1265
1305
1266
- newIdentityMP , err := msgpack .Marshal (newIdentityVal , identityBlock .ImpliedType ())
1306
+ // If we're updating or deleting and we already have an identity stored, validate that the planned identity isn't changing
1307
+ if ! res .ResourceBehavior .MutableIdentity && ! create && ! priorIdentityVal .IsNull () && ! priorIdentityVal .RawEquals (plannedIdentityVal ) {
1308
+ resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , fmt .Errorf (
1309
+ "Unexpected Identity Change: During the planning operation, the Terraform Provider unexpectedly returned a different identity than the previously stored one.\n \n " +
1310
+ "This is always a problem with the provider and should be reported to the provider developer.\n \n " +
1311
+ "Prior Identity: %s\n \n Planned Identity: %s" ,
1312
+ priorIdentityVal .GoString (),
1313
+ plannedIdentityVal .GoString (),
1314
+ ))
1315
+
1316
+ return resp , nil
1317
+ }
1318
+
1319
+ plannedIdentityMP , err := msgpack .Marshal (plannedIdentityVal , identityBlock .ImpliedType ())
1267
1320
if err != nil {
1268
1321
resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
1269
1322
return resp , nil
1270
1323
}
1271
1324
1272
1325
resp .PlannedIdentity = & tfprotov5.ResourceIdentityData {
1273
1326
IdentityData : & tfprotov5.DynamicValue {
1274
- MsgPack : newIdentityMP ,
1327
+ MsgPack : plannedIdentityMP ,
1275
1328
},
1276
1329
}
1277
1330
}
@@ -1299,6 +1352,8 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro
1299
1352
return resp , nil
1300
1353
}
1301
1354
1355
+ create := priorStateVal .IsNull ()
1356
+
1302
1357
plannedStateVal , err := msgpack .Unmarshal (req .PlannedState .MsgPack , schemaBlock .ImpliedType ())
1303
1358
if err != nil {
1304
1359
resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
@@ -1325,6 +1380,7 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro
1325
1380
}
1326
1381
}
1327
1382
1383
+ var plannedIdentityVal cty.Value
1328
1384
// add identity data to priorState
1329
1385
if req .PlannedIdentity != nil && req .PlannedIdentity .IdentityData != nil {
1330
1386
// convert req.PriorIdentity to flat map identity structure
@@ -1335,13 +1391,13 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro
1335
1391
return resp , nil
1336
1392
}
1337
1393
1338
- identityVal , err : = msgpack .Unmarshal (req .PlannedIdentity .IdentityData .MsgPack , identityBlock .ImpliedType ())
1394
+ plannedIdentityVal , err = msgpack .Unmarshal (req .PlannedIdentity .IdentityData .MsgPack , identityBlock .ImpliedType ())
1339
1395
if err != nil {
1340
1396
resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
1341
1397
return resp , nil
1342
1398
}
1343
1399
// Step 2: Turn cty.Value into flatmap representation
1344
- identityAttrs := hcl2shim .FlatmapValueFromHCL2 (identityVal )
1400
+ identityAttrs := hcl2shim .FlatmapValueFromHCL2 (plannedIdentityVal )
1345
1401
// Step 3: Well, set it in the priorState
1346
1402
priorState .Identity = identityAttrs
1347
1403
}
@@ -1475,7 +1531,6 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro
1475
1531
}
1476
1532
resp .Private = meta
1477
1533
1478
- // TODO: if schema defines identity, we should error if there's none written to newInstanceState
1479
1534
if res .Identity != nil {
1480
1535
identityBlock , err := s .getResourceIdentitySchemaBlock (req .TypeName )
1481
1536
if err != nil {
@@ -1489,6 +1544,18 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro
1489
1544
return resp , nil
1490
1545
}
1491
1546
1547
+ if ! res .ResourceBehavior .MutableIdentity && ! create && ! plannedIdentityVal .IsNull () && ! plannedIdentityVal .RawEquals (newIdentityVal ) {
1548
+ resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , fmt .Errorf (
1549
+ "Unexpected Identity Change: During the update operation, the Terraform Provider unexpectedly returned a different identity than the previously stored one.\n \n " +
1550
+ "This is always a problem with the provider and should be reported to the provider developer.\n \n " +
1551
+ "Planned Identity: %s\n \n New Identity: %s" ,
1552
+ plannedIdentityVal .GoString (),
1553
+ newIdentityVal .GoString (),
1554
+ ))
1555
+
1556
+ return resp , nil
1557
+ }
1558
+
1492
1559
newIdentityMP , err := msgpack .Marshal (newIdentityVal , identityBlock .ImpliedType ())
1493
1560
if err != nil {
1494
1561
resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
@@ -1636,6 +1703,10 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro
1636
1703
return resp , nil
1637
1704
}
1638
1705
1706
+ // Set an internal private field that will get sent alongside the imported resource. This will be cleared by
1707
+ // the following ReadResource RPC and is primarily used to control validation of resource identities during refresh.
1708
+ is .Meta [terraform .ImportBeforeReadMetaKey ] = true
1709
+
1639
1710
meta , err := json .Marshal (is .Meta )
1640
1711
if err != nil {
1641
1712
resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
0 commit comments