Skip to content

Commit 2bbfe8e

Browse files
committed
helper/schema: Add Resource type flags for disabling legacy type system protocol flags
Reference: #1227 Introduces new `Resource` type `EnableLegacyTypeSystemApplyErrors` and `EnableLegacyTypeSystemPlanErrors` fields that prevent the `PlanResourceChange` and `ApplyResourceChange` protocol operation responses from setting the `UnsafeLegacyTypeSystem` flag. When Terraform encounters the `UnsafeLegacyTypeSystem` flag, it will demote certain data consistency issues from error diagnostics that would immediately prevent Terraform workflows from completing to warning logs, which can be difficult for provider developers to discover. Provider developers are not generally expected to try enabling these settings unless discovering these data consistency issues upfront is desired before migrating resources to terraform-plugin-framework. This setting is on the individual resource level because these errors can be quite prevalent in existing terraform-plugin-sdk resources and therefore it would be problematic to not give provider developers more control over the behavior. The settings are split between plan and apply because the protocol flag is on two separate protocol operations and because certain errors for one operation may be unavoidable, so it leaves provider developers the option to still raise errors for the other operation. This change set includes new website documentation to discuss these data consistency issues and how to potentially resolve them more in-depth. It is expected that the framework migration guide will be updated to recommend enabling these settings before migrating to help provider developers understand the issues before migrating. The error resolution section has one particular error to start and it is expected that this section will grow over time as provider developers report the various data consistency errors that Terraform can raise.
1 parent bb15948 commit 2bbfe8e

9 files changed

+345
-84
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
kind: ENHANCEMENTS
2+
body: 'helper/schema: Added `Resource` type `EnableLegacyTypeSystemApplyErrors`
3+
field, which will prevent Terraform from demoting data consistency errors
4+
to warning logs during `ApplyResourceChange` (`Create`, `Update`, and `Delete`)
5+
operations with the resource'
6+
time: 2023-08-08T11:19:40.137598-04:00
7+
custom:
8+
Issue: "1227"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
kind: ENHANCEMENTS
2+
body: 'helper/schema: Added `Resource` type `EnableLegacyTypeSystemPlanErrors`
3+
field, which can be used to prevent Terraform from demoting data consistency
4+
errors to warning logs during `PlanResourceChange` operations with the resource'
5+
time: 2023-08-08T11:19:41.137598-04:00
6+
custom:
7+
Issue: "1227"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
kind: NOTES
2+
body: 'helper/schema: The `Resource` type `EnableApplyLegacyTypeSystemErrors`
3+
and `EnablePlanLegacyTypeSystemErrors` fields can be enabled to more easily
4+
discover resource data consistency errors which Terraform would normally
5+
demote to warning logs. Before enabling the flag in a production release for
6+
a resource, the resource should be exhaustively acceptance tested as there may
7+
be unrecoverable error situations for practitioners. It is recommended to
8+
first enable and test in environments where it is easy to clean up resources,
9+
potentially outside of Terraform.'
10+
time: 2023-08-08T10:37:24.017324-04:00
11+
custom:
12+
Issue: "1227"

helper/schema/grpc_provider.go

+14-8
Original file line numberDiff line numberDiff line change
@@ -662,20 +662,23 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot
662662
ctx = logging.InitContext(ctx)
663663
resp := &tfprotov5.PlanResourceChangeResponse{}
664664

665+
res, ok := s.provider.ResourcesMap[req.TypeName]
666+
if !ok {
667+
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("unknown resource type: %s", req.TypeName))
668+
return resp, nil
669+
}
670+
schemaBlock := s.getResourceSchemaBlock(req.TypeName)
671+
665672
// This is a signal to Terraform Core that we're doing the best we can to
666673
// shim the legacy type system of the SDK onto the Terraform type system
667674
// but we need it to cut us some slack. This setting should not be taken
668675
// forward to any new SDK implementations, since setting it prevents us
669676
// from catching certain classes of provider bug that can lead to
670677
// confusing downstream errors.
671-
resp.UnsafeToUseLegacyTypeSystem = true //nolint:staticcheck
672-
673-
res, ok := s.provider.ResourcesMap[req.TypeName]
674-
if !ok {
675-
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("unknown resource type: %s", req.TypeName))
676-
return resp, nil
678+
if !res.EnableLegacyTypeSystemPlanErrors {
679+
//nolint:staticcheck // explicitly for this SDK
680+
resp.UnsafeToUseLegacyTypeSystem = true
677681
}
678-
schemaBlock := s.getResourceSchemaBlock(req.TypeName)
679682

680683
priorStateVal, err := msgpack.Unmarshal(req.PriorState.MsgPack, schemaBlock.ImpliedType())
681684
if err != nil {
@@ -1075,7 +1078,10 @@ func (s *GRPCProviderServer) ApplyResourceChange(ctx context.Context, req *tfpro
10751078
// forward to any new SDK implementations, since setting it prevents us
10761079
// from catching certain classes of provider bug that can lead to
10771080
// confusing downstream errors.
1078-
resp.UnsafeToUseLegacyTypeSystem = true //nolint:staticcheck
1081+
if !res.EnableLegacyTypeSystemApplyErrors {
1082+
//nolint:staticcheck // explicitly for this SDK
1083+
resp.UnsafeToUseLegacyTypeSystem = true
1084+
}
10791085

10801086
return resp, nil
10811087
}

helper/schema/grpc_provider_test.go

+139-76
Original file line numberDiff line numberDiff line change
@@ -575,77 +575,113 @@ func TestUpgradeState_flatmapStateMissingMigrateState(t *testing.T) {
575575
}
576576

577577
func TestPlanResourceChange(t *testing.T) {
578-
r := &Resource{
579-
SchemaVersion: 4,
580-
Schema: map[string]*Schema{
581-
"foo": {
582-
Type: TypeInt,
583-
Optional: true,
578+
t.Parallel()
579+
580+
testCases := map[string]struct {
581+
TestResource *Resource
582+
ExpectedUnsafeLegacyTypeSystem bool
583+
}{
584+
"basic": {
585+
TestResource: &Resource{
586+
SchemaVersion: 4,
587+
Schema: map[string]*Schema{
588+
"foo": {
589+
Type: TypeInt,
590+
Optional: true,
591+
},
592+
},
593+
},
594+
ExpectedUnsafeLegacyTypeSystem: true,
595+
},
596+
"EnableLegacyTypeSystemPlanErrors": {
597+
TestResource: &Resource{
598+
EnableLegacyTypeSystemPlanErrors: true,
599+
Schema: map[string]*Schema{
600+
"foo": {
601+
Type: TypeInt,
602+
Optional: true,
603+
},
604+
},
584605
},
606+
ExpectedUnsafeLegacyTypeSystem: false,
585607
},
586608
}
587609

588-
server := NewGRPCProviderServer(&Provider{
589-
ResourcesMap: map[string]*Resource{
590-
"test": r,
591-
},
592-
})
610+
for name, testCase := range testCases {
611+
name, testCase := name, testCase
593612

594-
schema := r.CoreConfigSchema()
595-
priorState, err := msgpack.Marshal(cty.NullVal(schema.ImpliedType()), schema.ImpliedType())
596-
if err != nil {
597-
t.Fatal(err)
598-
}
613+
t.Run(name, func(t *testing.T) {
614+
t.Parallel()
599615

600-
// A propsed state with only the ID unknown will produce a nil diff, and
601-
// should return the propsed state value.
602-
proposedVal, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
603-
"id": cty.UnknownVal(cty.String),
604-
}))
605-
if err != nil {
606-
t.Fatal(err)
607-
}
608-
proposedState, err := msgpack.Marshal(proposedVal, schema.ImpliedType())
609-
if err != nil {
610-
t.Fatal(err)
611-
}
616+
server := NewGRPCProviderServer(&Provider{
617+
ResourcesMap: map[string]*Resource{
618+
"test": testCase.TestResource,
619+
},
620+
})
612621

613-
config, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
614-
"id": cty.NullVal(cty.String),
615-
}))
616-
if err != nil {
617-
t.Fatal(err)
618-
}
619-
configBytes, err := msgpack.Marshal(config, schema.ImpliedType())
620-
if err != nil {
621-
t.Fatal(err)
622-
}
622+
schema := testCase.TestResource.CoreConfigSchema()
623+
priorState, err := msgpack.Marshal(cty.NullVal(schema.ImpliedType()), schema.ImpliedType())
624+
if err != nil {
625+
t.Fatal(err)
626+
}
623627

624-
testReq := &tfprotov5.PlanResourceChangeRequest{
625-
TypeName: "test",
626-
PriorState: &tfprotov5.DynamicValue{
627-
MsgPack: priorState,
628-
},
629-
ProposedNewState: &tfprotov5.DynamicValue{
630-
MsgPack: proposedState,
631-
},
632-
Config: &tfprotov5.DynamicValue{
633-
MsgPack: configBytes,
634-
},
635-
}
628+
// A propsed state with only the ID unknown will produce a nil diff, and
629+
// should return the propsed state value.
630+
proposedVal, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
631+
"id": cty.UnknownVal(cty.String),
632+
}))
633+
if err != nil {
634+
t.Fatal(err)
635+
}
636+
proposedState, err := msgpack.Marshal(proposedVal, schema.ImpliedType())
637+
if err != nil {
638+
t.Fatal(err)
639+
}
636640

637-
resp, err := server.PlanResourceChange(context.Background(), testReq)
638-
if err != nil {
639-
t.Fatal(err)
640-
}
641+
config, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
642+
"id": cty.NullVal(cty.String),
643+
}))
644+
if err != nil {
645+
t.Fatal(err)
646+
}
647+
configBytes, err := msgpack.Marshal(config, schema.ImpliedType())
648+
if err != nil {
649+
t.Fatal(err)
650+
}
641651

642-
plannedStateVal, err := msgpack.Unmarshal(resp.PlannedState.MsgPack, schema.ImpliedType())
643-
if err != nil {
644-
t.Fatal(err)
645-
}
652+
testReq := &tfprotov5.PlanResourceChangeRequest{
653+
TypeName: "test",
654+
PriorState: &tfprotov5.DynamicValue{
655+
MsgPack: priorState,
656+
},
657+
ProposedNewState: &tfprotov5.DynamicValue{
658+
MsgPack: proposedState,
659+
},
660+
Config: &tfprotov5.DynamicValue{
661+
MsgPack: configBytes,
662+
},
663+
}
646664

647-
if !cmp.Equal(proposedVal, plannedStateVal, valueComparer) {
648-
t.Fatal(cmp.Diff(proposedVal, plannedStateVal, valueComparer))
665+
resp, err := server.PlanResourceChange(context.Background(), testReq)
666+
if err != nil {
667+
t.Fatal(err)
668+
}
669+
670+
plannedStateVal, err := msgpack.Unmarshal(resp.PlannedState.MsgPack, schema.ImpliedType())
671+
if err != nil {
672+
t.Fatal(err)
673+
}
674+
675+
if !cmp.Equal(proposedVal, plannedStateVal, valueComparer) {
676+
t.Fatal(cmp.Diff(proposedVal, plannedStateVal, valueComparer))
677+
}
678+
679+
//nolint:staticcheck // explicitly for this SDK
680+
if testCase.ExpectedUnsafeLegacyTypeSystem != resp.UnsafeToUseLegacyTypeSystem {
681+
//nolint:staticcheck // explicitly for this SDK
682+
t.Fatalf("expected UnsafeLegacyTypeSystem %t, got: %t", testCase.ExpectedUnsafeLegacyTypeSystem, resp.UnsafeToUseLegacyTypeSystem)
683+
}
684+
})
649685
}
650686
}
651687

@@ -730,12 +766,13 @@ func TestPlanResourceChange_bigint(t *testing.T) {
730766
}
731767

732768
func TestApplyResourceChange(t *testing.T) {
733-
testCases := []struct {
734-
Description string
735-
TestResource *Resource
769+
t.Parallel()
770+
771+
testCases := map[string]struct {
772+
TestResource *Resource
773+
ExpectedUnsafeLegacyTypeSystem bool
736774
}{
737-
{
738-
Description: "Create",
775+
"Create": {
739776
TestResource: &Resource{
740777
SchemaVersion: 4,
741778
Schema: map[string]*Schema{
@@ -749,9 +786,9 @@ func TestApplyResourceChange(t *testing.T) {
749786
return nil
750787
},
751788
},
789+
ExpectedUnsafeLegacyTypeSystem: true,
752790
},
753-
{
754-
Description: "CreateContext",
791+
"CreateContext": {
755792
TestResource: &Resource{
756793
SchemaVersion: 4,
757794
Schema: map[string]*Schema{
@@ -765,9 +802,9 @@ func TestApplyResourceChange(t *testing.T) {
765802
return nil
766803
},
767804
},
805+
ExpectedUnsafeLegacyTypeSystem: true,
768806
},
769-
{
770-
Description: "CreateWithoutTimeout",
807+
"CreateWithoutTimeout": {
771808
TestResource: &Resource{
772809
SchemaVersion: 4,
773810
Schema: map[string]*Schema{
@@ -781,9 +818,9 @@ func TestApplyResourceChange(t *testing.T) {
781818
return nil
782819
},
783820
},
821+
ExpectedUnsafeLegacyTypeSystem: true,
784822
},
785-
{
786-
Description: "Create_cty",
823+
"Create_cty": {
787824
TestResource: &Resource{
788825
SchemaVersion: 4,
789826
Schema: map[string]*Schema{
@@ -806,9 +843,9 @@ func TestApplyResourceChange(t *testing.T) {
806843
return nil
807844
},
808845
},
846+
ExpectedUnsafeLegacyTypeSystem: true,
809847
},
810-
{
811-
Description: "CreateContext_SchemaFunc",
848+
"CreateContext_SchemaFunc": {
812849
TestResource: &Resource{
813850
SchemaFunc: func() map[string]*Schema {
814851
return map[string]*Schema{
@@ -823,12 +860,32 @@ func TestApplyResourceChange(t *testing.T) {
823860
return nil
824861
},
825862
},
863+
ExpectedUnsafeLegacyTypeSystem: true,
864+
},
865+
"EnableLegacyTypeSystemApplyErrors": {
866+
TestResource: &Resource{
867+
EnableLegacyTypeSystemApplyErrors: true,
868+
Schema: map[string]*Schema{
869+
"foo": {
870+
Type: TypeInt,
871+
Optional: true,
872+
},
873+
},
874+
CreateContext: func(_ context.Context, rd *ResourceData, _ interface{}) diag.Diagnostics {
875+
rd.SetId("bar")
876+
return nil
877+
},
878+
},
879+
ExpectedUnsafeLegacyTypeSystem: false,
826880
},
827881
}
828882

829-
for _, testCase := range testCases {
830-
testCase := testCase
831-
t.Run(testCase.Description, func(t *testing.T) {
883+
for name, testCase := range testCases {
884+
name, testCase := name, testCase
885+
886+
t.Run(name, func(t *testing.T) {
887+
t.Parallel()
888+
832889
server := NewGRPCProviderServer(&Provider{
833890
ResourcesMap: map[string]*Resource{
834891
"test": testCase.TestResource,
@@ -892,6 +949,12 @@ func TestApplyResourceChange(t *testing.T) {
892949
if id != "bar" {
893950
t.Fatalf("incorrect final state: %#v\n", newStateVal)
894951
}
952+
953+
//nolint:staticcheck // explicitly for this SDK
954+
if testCase.ExpectedUnsafeLegacyTypeSystem != resp.UnsafeToUseLegacyTypeSystem {
955+
//nolint:staticcheck // explicitly for this SDK
956+
t.Fatalf("expected UnsafeLegacyTypeSystem %t, got: %t", testCase.ExpectedUnsafeLegacyTypeSystem, resp.UnsafeToUseLegacyTypeSystem)
957+
}
895958
})
896959
}
897960
}

0 commit comments

Comments
 (0)