@@ -159,7 +159,7 @@ func (s *GRPCProviderServer) UpgradeResourceIdentity(ctx context.Context, req *t
159159
160160 // now we need to turn the state into the default json representation, so
161161 // 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 )
163163 if err != nil {
164164 resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
165165 return resp , nil
@@ -788,11 +788,43 @@ func (s *GRPCProviderServer) ConfigureProvider(ctx context.Context, req *tfproto
788788
789789func (s * GRPCProviderServer ) ReadResource (ctx context.Context , req * tfprotov5.ReadResourceRequest ) (* tfprotov5.ReadResourceResponse , error ) {
790790 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+
791823 resp := & tfprotov5.ReadResourceResponse {
792824 // helper/schema did previously handle private data during refresh, but
793825 // core is now going to expect this to be maintained in order to
794826 // persist it in the state.
795- Private : req . Private ,
827+ Private : reqPrivate ,
796828 }
797829
798830 res , ok := s .provider .ResourcesMap [req .TypeName ]
@@ -832,7 +864,7 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re
832864 }
833865 instanceState .RawState = stateVal
834866
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
836868 if req .CurrentIdentity != nil && req .CurrentIdentity .IdentityData != nil {
837869
838870 // convert req.CurrentIdentity to flat map identity structure
@@ -843,20 +875,20 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re
843875 return resp , nil
844876 }
845877
846- identityVal , err : = msgpack .Unmarshal (req .CurrentIdentity .IdentityData .MsgPack , identityBlock .ImpliedType ())
878+ currentIdentityVal , err = msgpack .Unmarshal (req .CurrentIdentity .IdentityData .MsgPack , identityBlock .ImpliedType ())
847879 if err != nil {
848880 resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
849881 return resp , nil
850882 }
851883 // Step 2: Turn cty.Value into flatmap representation
852- identityAttrs := hcl2shim .FlatmapValueFromHCL2 (identityVal )
884+ identityAttrs := hcl2shim .FlatmapValueFromHCL2 (currentIdentityVal )
853885 // Step 3: Well, set it in the instanceState
854886 instanceState .Identity = identityAttrs
855887 }
856888
857889 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 {
860892 resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
861893 return resp , nil
862894 }
@@ -929,6 +961,15 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re
929961 return resp , nil
930962 }
931963
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+
932973 newIdentityMP , err := msgpack .Marshal (newIdentityVal , identityBlock .ImpliedType ())
933974 if err != nil {
934975 resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
@@ -1052,6 +1093,7 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot
10521093 // turn the proposed state into a legacy configuration
10531094 cfg := terraform .NewResourceConfigShimmed (proposedNewStateVal , schemaBlock )
10541095
1096+ var priorIdentityVal cty.Value
10551097 // add identity data to priorState
10561098 if req .PriorIdentity != nil && req .PriorIdentity .IdentityData != nil {
10571099 // convert req.PriorIdentity to flat map identity structure
@@ -1062,13 +1104,13 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot
10621104 return resp , nil
10631105 }
10641106
1065- identityVal , err : = msgpack .Unmarshal (req .PriorIdentity .IdentityData .MsgPack , identityBlock .ImpliedType ())
1107+ priorIdentityVal , err = msgpack .Unmarshal (req .PriorIdentity .IdentityData .MsgPack , identityBlock .ImpliedType ())
10661108 if err != nil {
10671109 resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
10681110 return resp , nil
10691111 }
10701112 // Step 2: Turn cty.Value into flatmap representation
1071- identityAttrs := hcl2shim .FlatmapValueFromHCL2 (identityVal )
1113+ identityAttrs := hcl2shim .FlatmapValueFromHCL2 (priorIdentityVal )
10721114 // Step 3: Well, set it in the priorState
10731115 priorState .Identity = identityAttrs
10741116 }
@@ -1088,7 +1130,6 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot
10881130 diff .Attributes ["id" ] = & terraform.ResourceAttrDiff {
10891131 NewComputed : true ,
10901132 }
1091- // TODO: we could error here if a new Diff got no Identity set
10921133 }
10931134
10941135 if diff == nil || (len (diff .Attributes ) == 0 && len (diff .Identity ) == 0 ) {
@@ -1249,29 +1290,41 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot
12491290 }
12501291 }
12511292
1252- // TODO: if schema defines identity, we should error if there's none written to newInstanceState
12531293 if res .Identity != nil {
12541294 identityBlock , err := s .getResourceIdentitySchemaBlock (req .TypeName )
12551295 if err != nil {
12561296 resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , fmt .Errorf ("getting identity schema failed for resource '%s': %w" , req .TypeName , err ))
12571297 return resp , nil
12581298 }
12591299
1260- newIdentityVal , err := hcl2shim .HCL2ValueFromFlatmap (diff .Identity , identityBlock .ImpliedType ())
1300+ plannedIdentityVal , err := hcl2shim .HCL2ValueFromFlatmap (diff .Identity , identityBlock .ImpliedType ())
12611301 if err != nil {
12621302 resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
12631303 return resp , nil
12641304 }
12651305
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 ())
12671320 if err != nil {
12681321 resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
12691322 return resp , nil
12701323 }
12711324
12721325 resp .PlannedIdentity = & tfprotov5.ResourceIdentityData {
12731326 IdentityData : & tfprotov5.DynamicValue {
1274- MsgPack : newIdentityMP ,
1327+ MsgPack : plannedIdentityMP ,
12751328 },
12761329 }
12771330 }
@@ -1299,6 +1352,8 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro
12991352 return resp , nil
13001353 }
13011354
1355+ create := priorStateVal .IsNull ()
1356+
13021357 plannedStateVal , err := msgpack .Unmarshal (req .PlannedState .MsgPack , schemaBlock .ImpliedType ())
13031358 if err != nil {
13041359 resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
@@ -1325,6 +1380,7 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro
13251380 }
13261381 }
13271382
1383+ var plannedIdentityVal cty.Value
13281384 // add identity data to priorState
13291385 if req .PlannedIdentity != nil && req .PlannedIdentity .IdentityData != nil {
13301386 // convert req.PriorIdentity to flat map identity structure
@@ -1335,13 +1391,13 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro
13351391 return resp , nil
13361392 }
13371393
1338- identityVal , err : = msgpack .Unmarshal (req .PlannedIdentity .IdentityData .MsgPack , identityBlock .ImpliedType ())
1394+ plannedIdentityVal , err = msgpack .Unmarshal (req .PlannedIdentity .IdentityData .MsgPack , identityBlock .ImpliedType ())
13391395 if err != nil {
13401396 resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
13411397 return resp , nil
13421398 }
13431399 // Step 2: Turn cty.Value into flatmap representation
1344- identityAttrs := hcl2shim .FlatmapValueFromHCL2 (identityVal )
1400+ identityAttrs := hcl2shim .FlatmapValueFromHCL2 (plannedIdentityVal )
13451401 // Step 3: Well, set it in the priorState
13461402 priorState .Identity = identityAttrs
13471403 }
@@ -1475,7 +1531,6 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro
14751531 }
14761532 resp .Private = meta
14771533
1478- // TODO: if schema defines identity, we should error if there's none written to newInstanceState
14791534 if res .Identity != nil {
14801535 identityBlock , err := s .getResourceIdentitySchemaBlock (req .TypeName )
14811536 if err != nil {
@@ -1489,6 +1544,18 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro
14891544 return resp , nil
14901545 }
14911546
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+
14921559 newIdentityMP , err := msgpack .Marshal (newIdentityVal , identityBlock .ImpliedType ())
14931560 if err != nil {
14941561 resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
@@ -1636,6 +1703,10 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro
16361703 return resp , nil
16371704 }
16381705
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+
16391710 meta , err := json .Marshal (is .Meta )
16401711 if err != nil {
16411712 resp .Diagnostics = convert .AppendProtoDiag (ctx , resp .Diagnostics , err )
0 commit comments