Skip to content

Commit 104e551

Browse files
authored
ResourceIdentity: Validate that identities do not change after Terraform stores it (#1478)
* Validate that identities do not change after Terraform stores it * introduce MutableIdentity resource behaviour and remove obsolete todos * add tests for MutableIdentity resource behaviour * address review feedback
1 parent d40c432 commit 104e551

File tree

4 files changed

+2651
-830
lines changed

4 files changed

+2651
-830
lines changed

helper/schema/grpc_provider.go

Lines changed: 88 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -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

789789
func (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\nPlanned 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\nNew 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

Comments
 (0)