Skip to content

Commit e170b2a

Browse files
committed
Validate that identities do not change after Terraform stores it
1 parent a8969de commit e170b2a

File tree

3 files changed

+2104
-726
lines changed

3 files changed

+2104
-726
lines changed

helper/schema/grpc_provider.go

Lines changed: 88 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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 !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
}
@@ -1257,21 +1299,34 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot
12571299
return resp, nil
12581300
}
12591301

1260-
newIdentityVal, err := hcl2shim.HCL2ValueFromFlatmap(diff.Identity, identityBlock.ImpliedType())
1302+
plannedIdentityVal, err := hcl2shim.HCL2ValueFromFlatmap(diff.Identity, identityBlock.ImpliedType())
12611303
if err != nil {
12621304
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
12631305
return resp, nil
12641306
}
12651307

1266-
newIdentityMP, err := msgpack.Marshal(newIdentityVal, identityBlock.ImpliedType())
1308+
// If we're updating or deleting and we already have an identity stored, validate that the planned identity isn't changing
1309+
if !create && !priorIdentityVal.IsNull() && !priorIdentityVal.RawEquals(plannedIdentityVal) {
1310+
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf(
1311+
"Unexpected Identity Change: During the planning operation, the Terraform Provider unexpectedly returned a different identity than the previously stored one.\n\n"+
1312+
"This is always a problem with the provider and should be reported to the provider developer.\n\n"+
1313+
"Prior Identity: %s\n\nPlanned Identity: %s",
1314+
priorIdentityVal.GoString(),
1315+
plannedIdentityVal.GoString(),
1316+
))
1317+
1318+
return resp, nil
1319+
}
1320+
1321+
plannedIdentityMP, err := msgpack.Marshal(plannedIdentityVal, identityBlock.ImpliedType())
12671322
if err != nil {
12681323
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
12691324
return resp, nil
12701325
}
12711326

12721327
resp.PlannedIdentity = &tfprotov5.ResourceIdentityData{
12731328
IdentityData: &tfprotov5.DynamicValue{
1274-
MsgPack: newIdentityMP,
1329+
MsgPack: plannedIdentityMP,
12751330
},
12761331
}
12771332
}
@@ -1299,6 +1354,8 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro
12991354
return resp, nil
13001355
}
13011356

1357+
create := priorStateVal.IsNull()
1358+
13021359
plannedStateVal, err := msgpack.Unmarshal(req.PlannedState.MsgPack, schemaBlock.ImpliedType())
13031360
if err != nil {
13041361
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
@@ -1325,6 +1382,7 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro
13251382
}
13261383
}
13271384

1385+
var plannedIdentityVal cty.Value
13281386
// add identity data to priorState
13291387
if req.PlannedIdentity != nil && req.PlannedIdentity.IdentityData != nil {
13301388
// convert req.PriorIdentity to flat map identity structure
@@ -1335,13 +1393,13 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro
13351393
return resp, nil
13361394
}
13371395

1338-
identityVal, err := msgpack.Unmarshal(req.PlannedIdentity.IdentityData.MsgPack, identityBlock.ImpliedType())
1396+
plannedIdentityVal, err = msgpack.Unmarshal(req.PlannedIdentity.IdentityData.MsgPack, identityBlock.ImpliedType())
13391397
if err != nil {
13401398
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
13411399
return resp, nil
13421400
}
13431401
// Step 2: Turn cty.Value into flatmap representation
1344-
identityAttrs := hcl2shim.FlatmapValueFromHCL2(identityVal)
1402+
identityAttrs := hcl2shim.FlatmapValueFromHCL2(plannedIdentityVal)
13451403
// Step 3: Well, set it in the priorState
13461404
priorState.Identity = identityAttrs
13471405
}
@@ -1489,6 +1547,18 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro
14891547
return resp, nil
14901548
}
14911549

1550+
if !create && !plannedIdentityVal.IsNull() && !plannedIdentityVal.RawEquals(newIdentityVal) {
1551+
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf(
1552+
"Unexpected Identity Change: During the update operation, the Terraform Provider unexpectedly returned a different identity than the previously stored one.\n\n"+
1553+
"This is always a problem with the provider and should be reported to the provider developer.\n\n"+
1554+
"Planned Identity: %s\n\nNew Identity: %s",
1555+
plannedIdentityVal.GoString(),
1556+
newIdentityVal.GoString(),
1557+
))
1558+
1559+
return resp, nil
1560+
}
1561+
14921562
newIdentityMP, err := msgpack.Marshal(newIdentityVal, identityBlock.ImpliedType())
14931563
if err != nil {
14941564
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
@@ -1636,6 +1706,10 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro
16361706
return resp, nil
16371707
}
16381708

1709+
// Set an internal private field that will get sent alongside the imported resource. This will be cleared by
1710+
// the following ReadResource RPC and is primarily used to control validation of resource identities during refresh.
1711+
is.Meta[terraform.ImportBeforeReadMetaKey] = true
1712+
16391713
meta, err := json.Marshal(is.Meta)
16401714
if err != nil {
16411715
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
@@ -2262,7 +2336,7 @@ func (s *GRPCProviderServer) upgradeJSONIdentity(ctx context.Context, version in
22622336

22632337
for _, upgrader := range res.Identity.IdentityUpgraders {
22642338
if version != upgrader.Version {
2265-
continue
2339+
continue // TODO: can we replace this with an error (as we'll require all versions to be upgraded)
22662340
}
22672341

22682342
m, err = upgrader.Upgrade(ctx, m, s.provider.Meta())

0 commit comments

Comments
 (0)