diff --git a/.changes/unreleased/FEATURES-20240506-152018.yaml b/.changes/unreleased/FEATURES-20240506-152018.yaml new file mode 100644 index 0000000000..bc64b3ba6a --- /dev/null +++ b/.changes/unreleased/FEATURES-20240506-152018.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'helper/schema: Added `(Provider).ConfigureProvider` function for configuring + providers that support additional features, such as deferred actions.' +time: 2024-05-06T15:20:18.393505-04:00 +custom: + Issue: "1335" diff --git a/.changes/unreleased/FEATURES-20240506-152135.yaml b/.changes/unreleased/FEATURES-20240506-152135.yaml new file mode 100644 index 0000000000..5e53452aaa --- /dev/null +++ b/.changes/unreleased/FEATURES-20240506-152135.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'helper/schema: Added `(Resource).ResourceBehavior` to allow additional control + over deferred action behavior during plan modification.' +time: 2024-05-06T15:21:35.304825-04:00 +custom: + Issue: "1335" diff --git a/.changes/unreleased/NOTES-20240509-134945.yaml b/.changes/unreleased/NOTES-20240509-134945.yaml new file mode 100644 index 0000000000..39f9ae9029 --- /dev/null +++ b/.changes/unreleased/NOTES-20240509-134945.yaml @@ -0,0 +1,7 @@ +kind: NOTES +body: This release contains support for deferred actions, which is an experimental + feature only available in prerelease builds of Terraform 1.9 and later. This functionality + is subject to change and is not protected by version compatibility guarantees. +time: 2024-05-09T13:49:45.38523-04:00 +custom: + Issue: "1335" diff --git a/helper/schema/deferred.go b/helper/schema/deferred.go new file mode 100644 index 0000000000..a02efef104 --- /dev/null +++ b/helper/schema/deferred.go @@ -0,0 +1,45 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +// MAINTAINER NOTE: Only PROVIDER_CONFIG_UNKNOWN (enum value 2 in the plugin-protocol) is relevant +// for SDKv2. Since (Deferred).Reason is mapped directly to the plugin-protocol, +// the other enum values are intentionally omitted here. +const ( + // DeferredReasonUnknown is used to indicate an invalid `DeferredReason`. + // Provider developers should not use it. + DeferredReasonUnknown DeferredReason = 0 + + // DeferredReasonProviderConfigUnknown represents a deferred reason caused + // by unknown provider configuration. + DeferredReasonProviderConfigUnknown DeferredReason = 2 +) + +// Deferred is used to indicate to Terraform that a resource or data source is not able +// to be applied yet and should be skipped (deferred). After completing an apply that has deferred actions, +// the practitioner can then execute additional plan and apply “rounds” to eventually reach convergence +// where there are no remaining deferred actions. +// +// NOTE: This functionality is related to deferred action support, which is currently experimental and is subject +// to change or break without warning. It is not protected by version compatibility guarantees. +type Deferred struct { + // Reason represents the deferred reason. + Reason DeferredReason +} + +// DeferredReason represents different reasons for deferring a change. +// +// NOTE: This functionality is related to deferred action support, which is currently experimental and is subject +// to change or break without warning. It is not protected by version compatibility guarantees. +type DeferredReason int32 + +func (d DeferredReason) String() string { + switch d { + case 0: + return "Unknown" + case 2: + return "Provider Config Unknown" + } + return "Unknown" +} diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index 70477da45a..ec5d74301a 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -607,12 +607,37 @@ func (s *GRPCProviderServer) ConfigureProvider(ctx context.Context, req *tfproto // request scoped contexts, however this is a large undertaking for very large providers. ctxHack := context.WithValue(ctx, StopContextKey, s.StopContext(context.Background())) + // NOTE: This is a hack to pass the deferral_allowed field from the Terraform client to the + // underlying (provider).Configure function, which cannot be changed because the function + // signature is public. (╯°□°)╯︵ ┻━┻ + s.provider.deferralAllowed = configureDeferralAllowed(req.ClientCapabilities) + logging.HelperSchemaTrace(ctx, "Calling downstream") diags := s.provider.Configure(ctxHack, config) logging.HelperSchemaTrace(ctx, "Called downstream") resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, diags) + if s.provider.providerDeferred != nil { + // Check if a deferred response was incorrectly set on the provider. This would cause an error during later RPCs. + if !s.provider.deferralAllowed { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Invalid Deferred Provider Response", + Detail: "Provider configured a deferred response for all resources and data sources but the Terraform request " + + "did not indicate support for deferred actions. This is an issue with the provider and should be reported to the provider developers.", + }) + } else { + logging.HelperSchemaDebug( + ctx, + "Provider has configured a deferred response, all associated resources and data sources will automatically return a deferred response.", + map[string]interface{}{ + logging.KeyDeferredReason: s.provider.providerDeferred.Reason.String(), + }, + ) + } + } + return resp, nil } @@ -632,6 +657,22 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re } schemaBlock := s.getResourceSchemaBlock(req.TypeName) + if s.provider.providerDeferred != nil { + logging.HelperSchemaDebug( + ctx, + "Provider has deferred response configured, automatically returning deferred response.", + map[string]interface{}{ + logging.KeyDeferredReason: s.provider.providerDeferred.Reason.String(), + }, + ) + + resp.NewState = req.CurrentState + resp.Deferred = &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(s.provider.providerDeferred.Reason), + } + return resp, nil + } + stateVal, err := msgpack.Unmarshal(req.CurrentState.MsgPack, schemaBlock.ImpliedType()) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) @@ -731,6 +772,25 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot resp.UnsafeToUseLegacyTypeSystem = true } + // Provider deferred response is present and the resource hasn't opted-in to CustomizeDiff being called, return early + // with proposed new state as a best effort for PlannedState. + if s.provider.providerDeferred != nil && !res.ResourceBehavior.ProviderDeferred.EnablePlanModification { + logging.HelperSchemaDebug( + ctx, + "Provider has deferred response configured, automatically returning deferred response.", + map[string]interface{}{ + logging.KeyDeferredReason: s.provider.providerDeferred.Reason.String(), + }, + ) + + resp.PlannedState = req.ProposedNewState + resp.PlannedPrivate = req.PriorPrivate + resp.Deferred = &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(s.provider.providerDeferred.Reason), + } + return resp, nil + } + priorStateVal, err := msgpack.Unmarshal(req.PriorState.MsgPack, schemaBlock.ImpliedType()) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) @@ -951,6 +1011,21 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot resp.RequiresReplace = append(resp.RequiresReplace, pathToAttributePath(p)) } + // Provider deferred response is present, add the deferred response alongside the provider-modified plan + if s.provider.providerDeferred != nil { + logging.HelperSchemaDebug( + ctx, + "Provider has deferred response configured, returning deferred response with modified plan.", + map[string]interface{}{ + logging.KeyDeferredReason: s.provider.providerDeferred.Reason.String(), + }, + ) + + resp.Deferred = &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(s.provider.providerDeferred.Reason), + } + } + return resp, nil } @@ -1145,6 +1220,48 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro Type: req.TypeName, } + if s.provider.providerDeferred != nil { + logging.HelperSchemaDebug( + ctx, + "Provider has deferred response configured, automatically returning deferred response.", + map[string]interface{}{ + logging.KeyDeferredReason: s.provider.providerDeferred.Reason.String(), + }, + ) + + // The logic for ensuring the resource type is supported by this provider is inside of (provider).ImportState + // We need to check to ensure the resource type is supported before using the schema + _, ok := s.provider.ResourcesMap[req.TypeName] + if !ok { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("unknown resource type: %s", req.TypeName)) + return resp, nil + } + + // Since we are automatically deferring, send back an unknown value for the imported object + schemaBlock := s.getResourceSchemaBlock(req.TypeName) + unknownVal := cty.UnknownVal(schemaBlock.ImpliedType()) + unknownStateMp, err := msgpack.Marshal(unknownVal, schemaBlock.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + + resp.ImportedResources = []*tfprotov5.ImportedResource{ + { + TypeName: req.TypeName, + State: &tfprotov5.DynamicValue{ + MsgPack: unknownStateMp, + }, + }, + } + + resp.Deferred = &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(s.provider.providerDeferred.Reason), + } + + return resp, nil + } + newInstanceStates, err := s.provider.ImportState(ctx, info, req.ID) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) @@ -1254,6 +1371,32 @@ func (s *GRPCProviderServer) ReadDataSource(ctx context.Context, req *tfprotov5. schemaBlock := s.getDatasourceSchemaBlock(req.TypeName) + if s.provider.providerDeferred != nil { + logging.HelperSchemaDebug( + ctx, + "Provider has deferred response configured, automatically returning deferred response.", + map[string]interface{}{ + logging.KeyDeferredReason: s.provider.providerDeferred.Reason.String(), + }, + ) + + // Send an unknown value for the data source + unknownVal := cty.UnknownVal(schemaBlock.ImpliedType()) + unknownStateMp, err := msgpack.Marshal(unknownVal, schemaBlock.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) + return resp, nil + } + + resp.State = &tfprotov5.DynamicValue{ + MsgPack: unknownStateMp, + } + resp.Deferred = &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(s.provider.providerDeferred.Reason), + } + return resp, nil + } + configVal, err := msgpack.Unmarshal(req.Config.MsgPack, schemaBlock.ImpliedType()) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) @@ -1674,3 +1817,14 @@ func validateConfigNulls(ctx context.Context, v cty.Value, path cty.Path) []*tfp return diags } + +// Helper function that check a ConfigureProviderClientCapabilities struct to determine if a deferred response can be +// returned to the Terraform client. If no ConfigureProviderClientCapabilities have been passed from the client, then false +// is returned. +func configureDeferralAllowed(in *tfprotov5.ConfigureProviderClientCapabilities) bool { + if in == nil { + return false + } + + return in.DeferralAllowed +} diff --git a/helper/schema/grpc_provider_test.go b/helper/schema/grpc_provider_test.go index 39b4b078a1..7dacdd6ea5 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/go-cty/cty/msgpack" "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugin/convert" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" @@ -27,15 +28,20 @@ import ( func TestGRPCProviderServerConfigureProvider(t *testing.T) { t.Parallel() + type FakeMetaStruct struct { + Attr string + } + testCases := map[string]struct { - server *GRPCProviderServer - req *tfprotov5.ConfigureProviderRequest - expected *tfprotov5.ConfigureProviderResponse - expectedMeta any + server *GRPCProviderServer + req *tfprotov5.ConfigureProviderRequest + expected *tfprotov5.ConfigureProviderResponse + expectedProviderDeferred *Deferred + expectedMeta any }{ "no-Configure-or-Schema": { server: NewGRPCProviderServer(&Provider{ - // ConfigureFunc, ConfigureContextFunc, and Schema intentionally + // ConfigureFunc, ConfigureContextFunc, ConfigureProvider, and Schema intentionally // omitted. }), req: &tfprotov5.ConfigureProviderRequest{ @@ -56,7 +62,7 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, "Schema-no-Configure": { server: NewGRPCProviderServer(&Provider{ - // ConfigureFunc and ConfigureContextFunc intentionally omitted. + // ConfigureFunc, ConfigureContextFunc, and ConfigureProvider intentionally omitted. Schema: map[string]*Schema{ "test": { Optional: true, @@ -146,6 +152,40 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, }, }, + "ConfigureProvider-error": { + server: NewGRPCProviderServer(&Provider{ + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + resp.Diagnostics = diag.Errorf("test error") + }, + Schema: map[string]*Schema{ + "test": { + Optional: true, + Type: TypeString, + }, + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("test-value"), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error", + Detail: "", + }, + }, + }, + }, "ConfigureContextFunc-warning": { server: NewGRPCProviderServer(&Provider{ ConfigureContextFunc: func(ctx context.Context, d *ResourceData) (any, diag.Diagnostics) { @@ -186,6 +226,46 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, }, }, + "ConfigureProvider-warning": { + server: NewGRPCProviderServer(&Provider{ + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + resp.Diagnostics = diag.Diagnostics{ + { + Severity: diag.Warning, + Summary: "test warning summary", + Detail: "test warning detail", + }, + } + }, + Schema: map[string]*Schema{ + "test": { + Optional: true, + Type: TypeString, + }, + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("test-value"), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning detail", + }, + }, + }, + }, "ConfigureFunc-Get-null": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ @@ -252,6 +332,37 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-Get-null": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got := req.ResourceData.Get("test").(string) + expected := "" + + if got != expected { + resp.Diagnostics = diag.Errorf("unexpected Get difference: expected: %s, got: %s", expected, got) + } + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.NullVal(cty.String), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, "ConfigureFunc-Get-null-other-value": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ @@ -330,6 +441,43 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-Get-null-other-value": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "other": { + Type: TypeString, + Optional: true, + }, + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got := req.ResourceData.Get("test").(string) + expected := "" + + if got != expected { + resp.Diagnostics = diag.Errorf("unexpected Get difference: expected: %s, got: %s", expected, got) + } + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "other": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "other": cty.StringVal("other-value"), + "test": cty.NullVal(cty.String), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, "ConfigureFunc-Get-zero-value": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ @@ -396,6 +544,37 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-Get-zero-value": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got := req.ResourceData.Get("test").(string) + expected := "" + + if got != expected { + resp.Diagnostics = diag.Errorf("unexpected Get difference: expected: %s, got: %s", expected, got) + } + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal(""), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, "ConfigureFunc-Get-zero-value-other-value": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ @@ -474,6 +653,43 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-Get-zero-value-other-value": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "other": { + Type: TypeString, + Optional: true, + }, + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got := req.ResourceData.Get("test").(string) + expected := "" + + if got != expected { + resp.Diagnostics = diag.Errorf("unexpected Get difference: expected: %s, got: %s", expected, got) + } + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "other": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "other": cty.StringVal("other-value"), + "test": cty.StringVal(""), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, "ConfigureFunc-Get-value": { server: NewGRPCProviderServer(&Provider{ ConfigureFunc: func(d *ResourceData) (any, error) { @@ -540,6 +756,37 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-Get-value": { + server: NewGRPCProviderServer(&Provider{ + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got := req.ResourceData.Get("test").(string) + expected := "test-value" + + if got != expected { + resp.Diagnostics = diag.Errorf("unexpected difference: expected: %s, got: %s", expected, got) + } + }, + Schema: map[string]*Schema{ + "test": { + Optional: true, + Type: TypeString, + }, + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("test-value"), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, "ConfigureFunc-Get-value-other-value": { server: NewGRPCProviderServer(&Provider{ ConfigureFunc: func(d *ResourceData) (any, error) { @@ -618,6 +865,43 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-Get-value-other-value": { + server: NewGRPCProviderServer(&Provider{ + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got := req.ResourceData.Get("test").(string) + expected := "test-value" + + if got != expected { + resp.Diagnostics = diag.Errorf("unexpected difference: expected: %s, got: %s", expected, got) + } + }, + Schema: map[string]*Schema{ + "other": { + Optional: true, + Type: TypeString, + }, + "test": { + Optional: true, + Type: TypeString, + }, + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "other": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "other": cty.StringVal("other-value"), + "test": cty.StringVal("test-value"), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, "ConfigureFunc-GetOk-null": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ @@ -694,19 +978,57 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, - "ConfigureFunc-GetOk-null-other-value": { + "ConfigureProvider-GetOk-null": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ - "other": { - Type: TypeString, - Optional: true, - }, "test": { Type: TypeString, Optional: true, }, }, - ConfigureFunc: func(d *ResourceData) (interface{}, error) { + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got, ok := req.ResourceData.GetOk("test") + expected := "" + expectedOk := false + + if ok != expectedOk { + resp.Diagnostics = diag.Errorf("unexpected GetOk difference: expected: %t, got: %t", expectedOk, ok) + return + } + + if got.(string) != expected { + resp.Diagnostics = diag.Errorf("unexpected GetOk difference: expected: %s, got: %s", expected, got) + return + } + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.NullVal(cty.String), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, + "ConfigureFunc-GetOk-null-other-value": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "other": { + Type: TypeString, + Optional: true, + }, + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureFunc: func(d *ResourceData) (interface{}, error) { got, ok := d.GetOk("test") expected := "" expectedOk := false @@ -782,6 +1104,50 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-GetOk-null-other-value": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "other": { + Type: TypeString, + Optional: true, + }, + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got, ok := req.ResourceData.GetOk("test") + expected := "" + expectedOk := false + + if ok != expectedOk { + resp.Diagnostics = diag.Errorf("unexpected GetOk difference: expected: %t, got: %t", expectedOk, ok) + return + } + + if got.(string) != expected { + resp.Diagnostics = diag.Errorf("unexpected GetOk difference: expected: %s, got: %s", expected, got) + return + } + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "other": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "other": cty.StringVal("other-value"), + "test": cty.NullVal(cty.String), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, "ConfigureFunc-GetOk-zero-value": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ @@ -858,6 +1224,44 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-GetOk-zero-value": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got, ok := req.ResourceData.GetOk("test") + expected := "" + expectedOk := false + + if ok != expectedOk { + resp.Diagnostics = diag.Errorf("unexpected GetOk difference: expected: %t, got: %t", expectedOk, ok) + return + } + + if got.(string) != expected { + resp.Diagnostics = diag.Errorf("unexpected GetOk difference: expected: %s, got: %s", expected, got) + return + } + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal(""), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, "ConfigureFunc-GetOk-zero-value-other-value": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ @@ -946,6 +1350,50 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-GetOk-zero-value-other-value": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "other": { + Type: TypeString, + Optional: true, + }, + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got, ok := req.ResourceData.GetOk("test") + expected := "" + expectedOk := false + + if ok != expectedOk { + resp.Diagnostics = diag.Errorf("unexpected GetOk difference: expected: %t, got: %t", expectedOk, ok) + return + } + + if got.(string) != expected { + resp.Diagnostics = diag.Errorf("unexpected GetOk difference: expected: %s, got: %s", expected, got) + return + } + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "other": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "other": cty.StringVal("other-value"), + "test": cty.StringVal(""), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, "ConfigureFunc-GetOk-value": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ @@ -1022,6 +1470,44 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-GetOk-value": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got, ok := req.ResourceData.GetOk("test") + expected := "test-value" + expectedOk := true + + if ok != expectedOk { + resp.Diagnostics = diag.Errorf("unexpected GetOk difference: expected: %t, got: %t", expectedOk, ok) + return + } + + if got.(string) != expected { + resp.Diagnostics = diag.Errorf("unexpected GetOk difference: expected: %s, got: %s", expected, got) + return + } + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("test-value"), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, "ConfigureFunc-GetOk-value-other-value": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ @@ -1110,6 +1596,50 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-GetOk-value-other-value": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "other": { + Type: TypeString, + Optional: true, + }, + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got, ok := req.ResourceData.GetOk("test") + expected := "test-value" + expectedOk := true + + if ok != expectedOk { + resp.Diagnostics = diag.Errorf("unexpected GetOk difference: expected: %t, got: %t", expectedOk, ok) + return + } + + if got.(string) != expected { + resp.Diagnostics = diag.Errorf("unexpected GetOk difference: expected: %s, got: %s", expected, got) + return + } + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "other": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "other": cty.StringVal("other-value"), + "test": cty.StringVal("test-value"), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, "ConfigureFunc-GetOkExists-null": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ @@ -1186,6 +1716,44 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-GetOkExists-null": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got, ok := req.ResourceData.GetOkExists("test") + expected := "" + expectedOk := false + + if ok != expectedOk { + resp.Diagnostics = diag.Errorf("unexpected GetOkExists difference: expected: %t, got: %t", expectedOk, ok) + return + } + + if got.(string) != expected { + resp.Diagnostics = diag.Errorf("unexpected GetOkExists difference: expected: %s, got: %s", expected, got) + return + } + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.NullVal(cty.String), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, "ConfigureFunc-GetOkExists-null-other-value": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ @@ -1274,36 +1842,80 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, - "ConfigureFunc-GetOkExists-zero-value": { + "ConfigureProvider-GetOkExists-null-other-value": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ + "other": { + Type: TypeString, + Optional: true, + }, "test": { Type: TypeString, Optional: true, }, }, - ConfigureFunc: func(d *ResourceData) (interface{}, error) { - got, ok := d.GetOkExists("test") + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got, ok := req.ResourceData.GetOkExists("test") expected := "" - expectedOk := true + expectedOk := false if ok != expectedOk { - return nil, fmt.Errorf("unexpected GetOkExists difference: expected: %t, got: %t", expectedOk, ok) + resp.Diagnostics = diag.Errorf("unexpected GetOkExists difference: expected: %t, got: %t", expectedOk, ok) + return } if got.(string) != expected { - return nil, fmt.Errorf("unexpected GetOkExists difference: expected: %s, got: %s", expected, got) + resp.Diagnostics = diag.Errorf("unexpected GetOkExists difference: expected: %s, got: %s", expected, got) + return } - - return nil, nil }, }), req: &tfprotov5.ConfigureProviderRequest{ Config: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "test": cty.String, - }), + "other": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "other": cty.StringVal("other-value"), + "test": cty.NullVal(cty.String), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, + "ConfigureFunc-GetOkExists-zero-value": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureFunc: func(d *ResourceData) (interface{}, error) { + got, ok := d.GetOkExists("test") + expected := "" + expectedOk := true + + if ok != expectedOk { + return nil, fmt.Errorf("unexpected GetOkExists difference: expected: %t, got: %t", expectedOk, ok) + } + + if got.(string) != expected { + return nil, fmt.Errorf("unexpected GetOkExists difference: expected: %s, got: %s", expected, got) + } + + return nil, nil + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), cty.ObjectVal(map[string]cty.Value{ "test": cty.StringVal(""), }), @@ -1350,6 +1962,44 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-GetOkExists-zero-value": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got, ok := req.ResourceData.GetOkExists("test") + expected := "" + expectedOk := true + + if ok != expectedOk { + resp.Diagnostics = diag.Errorf("unexpected GetOkExists difference: expected: %t, got: %t", expectedOk, ok) + return + } + + if got.(string) != expected { + resp.Diagnostics = diag.Errorf("unexpected GetOkExists difference: expected: %s, got: %s", expected, got) + return + } + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal(""), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, "ConfigureFunc-GetOkExists-zero-value-other-value": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ @@ -1438,6 +2088,50 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-GetOkExists-zero-value-other-value": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "other": { + Type: TypeString, + Optional: true, + }, + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got, ok := req.ResourceData.GetOkExists("test") + expected := "" + expectedOk := true + + if ok != expectedOk { + resp.Diagnostics = diag.Errorf("unexpected GetOkExists difference: expected: %t, got: %t", expectedOk, ok) + return + } + + if got.(string) != expected { + resp.Diagnostics = diag.Errorf("unexpected GetOkExists difference: expected: %s, got: %s", expected, got) + return + } + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "other": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "other": cty.StringVal("other-value"), + "test": cty.StringVal(""), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, "ConfigureFunc-GetOkExists-value": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ @@ -1514,6 +2208,44 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-GetOkExists-value": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got, ok := req.ResourceData.GetOkExists("test") + expected := "test-value" + expectedOk := true + + if ok != expectedOk { + resp.Diagnostics = diag.Errorf("unexpected GetOkExists difference: expected: %t, got: %t", expectedOk, ok) + return + } + + if got.(string) != expected { + resp.Diagnostics = diag.Errorf("unexpected GetOkExists difference: expected: %s, got: %s", expected, got) + return + } + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("test-value"), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, "ConfigureFunc-GetOkExists-value-other-value": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ @@ -1602,6 +2334,50 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-GetOkExists-value-other-value": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "other": { + Type: TypeString, + Optional: true, + }, + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got, ok := req.ResourceData.GetOkExists("test") + expected := "test-value" + expectedOk := true + + if ok != expectedOk { + resp.Diagnostics = diag.Errorf("unexpected GetOkExists difference: expected: %t, got: %t", expectedOk, ok) + return + } + + if got.(string) != expected { + resp.Diagnostics = diag.Errorf("unexpected GetOkExists difference: expected: %s, got: %s", expected, got) + return + } + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "other": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "other": cty.StringVal("other-value"), + "test": cty.StringVal("test-value"), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, "ConfigureFunc-GetRawConfig-null": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ @@ -1672,6 +2448,39 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-GetRawConfig-null": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got := req.ResourceData.GetRawConfig() + expected := cty.ObjectVal(map[string]cty.Value{ + "test": cty.NullVal(cty.String), + }) + + if got.Equals(expected).False() { + resp.Diagnostics = diag.Errorf("unexpected GetRawConfig difference: expected: %s, got: %s", expected, got) + } + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.NullVal(cty.String), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, "ConfigureFunc-GetRawConfig-null-other-value": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ @@ -1756,42 +2565,47 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, - "ConfigureFunc-GetRawConfig-zero-value": { + "ConfigureProvider-GetRawConfig-null-other-value": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ + "other": { + Type: TypeString, + Optional: true, + }, "test": { Type: TypeString, Optional: true, }, }, - ConfigureFunc: func(d *ResourceData) (interface{}, error) { - got := d.GetRawConfig() + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got := req.ResourceData.GetRawConfig() expected := cty.ObjectVal(map[string]cty.Value{ - "test": cty.StringVal(""), + "other": cty.StringVal("other-value"), + "test": cty.NullVal(cty.String), }) if got.Equals(expected).False() { - return nil, fmt.Errorf("unexpected GetRawConfig difference: expected: %s, got: %s", expected, got) + resp.Diagnostics = diag.Errorf("unexpected GetRawConfig difference: expected: %s, got: %s", expected, got) } - - return nil, nil }, }), req: &tfprotov5.ConfigureProviderRequest{ Config: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "test": cty.String, + "other": cty.String, + "test": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "test": cty.StringVal(""), + "other": cty.StringVal("other-value"), + "test": cty.NullVal(cty.String), }), ), }, }, expected: &tfprotov5.ConfigureProviderResponse{}, }, - "ConfigureContextFunc-GetRawConfig-zero-value": { + "ConfigureFunc-GetRawConfig-zero-value": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ "test": { @@ -1799,14 +2613,14 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { Optional: true, }, }, - ConfigureContextFunc: func(ctx context.Context, d *ResourceData) (interface{}, diag.Diagnostics) { + ConfigureFunc: func(d *ResourceData) (interface{}, error) { got := d.GetRawConfig() expected := cty.ObjectVal(map[string]cty.Value{ "test": cty.StringVal(""), }) if got.Equals(expected).False() { - return nil, diag.Errorf("unexpected GetRawConfig difference: expected: %s, got: %s", expected, got) + return nil, fmt.Errorf("unexpected GetRawConfig difference: expected: %s, got: %s", expected, got) } return nil, nil @@ -1826,27 +2640,22 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, - "ConfigureFunc-GetRawConfig-zero-value-other-value": { + "ConfigureContextFunc-GetRawConfig-zero-value": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ - "other": { - Type: TypeString, - Optional: true, - }, "test": { Type: TypeString, Optional: true, }, }, - ConfigureFunc: func(d *ResourceData) (interface{}, error) { + ConfigureContextFunc: func(ctx context.Context, d *ResourceData) (interface{}, diag.Diagnostics) { got := d.GetRawConfig() expected := cty.ObjectVal(map[string]cty.Value{ - "other": cty.StringVal("other-value"), - "test": cty.StringVal(""), + "test": cty.StringVal(""), }) if got.Equals(expected).False() { - return nil, fmt.Errorf("unexpected GetRawConfig difference: expected: %s, got: %s", expected, got) + return nil, diag.Errorf("unexpected GetRawConfig difference: expected: %s, got: %s", expected, got) } return nil, nil @@ -1856,25 +2665,98 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { Config: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ - "other": cty.String, - "test": cty.String, + "test": cty.String, }), cty.ObjectVal(map[string]cty.Value{ - "other": cty.StringVal("other-value"), - "test": cty.StringVal(""), + "test": cty.StringVal(""), }), ), }, }, expected: &tfprotov5.ConfigureProviderResponse{}, }, - "ConfigureContextFunc-GetRawConfig-zero-value-other-value": { + "ConfigureProvider-GetRawConfig-zero-value": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ - "other": { - Type: TypeString, - Optional: true, - }, + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got := req.ResourceData.GetRawConfig() + expected := cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal(""), + }) + + if got.Equals(expected).False() { + resp.Diagnostics = diag.Errorf("unexpected GetRawConfig difference: expected: %s, got: %s", expected, got) + } + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal(""), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, + "ConfigureFunc-GetRawConfig-zero-value-other-value": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "other": { + Type: TypeString, + Optional: true, + }, + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureFunc: func(d *ResourceData) (interface{}, error) { + got := d.GetRawConfig() + expected := cty.ObjectVal(map[string]cty.Value{ + "other": cty.StringVal("other-value"), + "test": cty.StringVal(""), + }) + + if got.Equals(expected).False() { + return nil, fmt.Errorf("unexpected GetRawConfig difference: expected: %s, got: %s", expected, got) + } + + return nil, nil + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "other": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "other": cty.StringVal("other-value"), + "test": cty.StringVal(""), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, + "ConfigureContextFunc-GetRawConfig-zero-value-other-value": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "other": { + Type: TypeString, + Optional: true, + }, "test": { Type: TypeString, Optional: true, @@ -1910,6 +2792,46 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-GetRawConfig-zero-value-other-value": { + server: NewGRPCProviderServer(&Provider{ + Schema: map[string]*Schema{ + "other": { + Type: TypeString, + Optional: true, + }, + "test": { + Type: TypeString, + Optional: true, + }, + }, + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got := req.ResourceData.GetRawConfig() + expected := cty.ObjectVal(map[string]cty.Value{ + "other": cty.StringVal("other-value"), + "test": cty.StringVal(""), + }) + + if got.Equals(expected).False() { + resp.Diagnostics = diag.Errorf("unexpected GetRawConfig difference: expected: %s, got: %s", expected, got) + } + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "other": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "other": cty.StringVal("other-value"), + "test": cty.StringVal(""), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, "ConfigureFunc-GetRawConfig-value": { server: NewGRPCProviderServer(&Provider{ ConfigureFunc: func(d *ResourceData) (any, error) { @@ -1980,6 +2902,39 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-GetRawConfig-value": { + server: NewGRPCProviderServer(&Provider{ + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got := req.ResourceData.GetRawConfig() + expected := cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("test-value"), + }) + + if got.Equals(expected).False() { + resp.Diagnostics = diag.Errorf("unexpected difference: expected: %s, got: %s", expected, got) + } + }, + Schema: map[string]*Schema{ + "test": { + Optional: true, + Type: TypeString, + }, + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("test-value"), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + }, "ConfigureFunc-GetRawConfig-value-other-value": { server: NewGRPCProviderServer(&Provider{ ConfigureFunc: func(d *ResourceData) (any, error) { @@ -2064,75 +3019,331 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, - } - - for name, testCase := range testCases { - name, testCase := name, testCase - - t.Run(name, func(t *testing.T) { - t.Parallel() - - resp, err := testCase.server.ConfigureProvider(context.Background(), testCase.req) - - if err != nil { - t.Fatal(err) - } - - if diff := cmp.Diff(resp, testCase.expected); diff != "" { - t.Errorf("unexpected difference: %s", diff) - } - }) - } -} - -func TestGRPCProviderServerGetMetadata(t *testing.T) { - t.Parallel() + "ConfigureProvider-GetRawConfig-value-other-value": { + server: NewGRPCProviderServer(&Provider{ + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + got := req.ResourceData.GetRawConfig() + expected := cty.ObjectVal(map[string]cty.Value{ + "other": cty.StringVal("other-value"), + "test": cty.StringVal("test-value"), + }) - testCases := map[string]struct { - Provider *Provider - Expected *tfprotov5.GetMetadataResponse - }{ - "datasources": { - Provider: &Provider{ - DataSourcesMap: map[string]*Resource{ - "test_datasource1": nil, // implementation not necessary - "test_datasource2": nil, // implementation not necessary + if got.Equals(expected).False() { + resp.Diagnostics = diag.Errorf("unexpected difference: expected: %s, got: %s", expected, got) + } }, - }, - Expected: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{ - { - TypeName: "test_datasource1", + Schema: map[string]*Schema{ + "other": { + Optional: true, + Type: TypeString, }, - { - TypeName: "test_datasource2", + "test": { + Optional: true, + Type: TypeString, }, }, - Functions: []tfprotov5.FunctionMetadata{}, - Resources: []tfprotov5.ResourceMetadata{}, - ServerCapabilities: &tfprotov5.ServerCapabilities{ - GetProviderSchemaOptional: true, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "other": cty.String, + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "other": cty.StringVal("other-value"), + "test": cty.StringVal("test-value"), + }), + ), }, }, + expected: &tfprotov5.ConfigureProviderResponse{}, }, - "datasources and resources": { - Provider: &Provider{ - DataSourcesMap: map[string]*Resource{ - "test_datasource1": nil, // implementation not necessary - "test_datasource2": nil, // implementation not necessary + "ConfigureFunc-Meta": { + server: NewGRPCProviderServer(&Provider{ + ConfigureFunc: func(d *ResourceData) (any, error) { + return &FakeMetaStruct{ + Attr: "hello world!", + }, nil }, - ResourcesMap: map[string]*Resource{ - "test_resource1": nil, // implementation not necessary - "test_resource2": nil, // implementation not necessary + Schema: map[string]*Schema{ + "test": { + Optional: true, + Type: TypeString, + }, + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("test-value"), + }), + ), }, }, - Expected: &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{ - { - TypeName: "test_datasource1", - }, - { - TypeName: "test_datasource2", + expected: &tfprotov5.ConfigureProviderResponse{}, + expectedMeta: &FakeMetaStruct{ + Attr: "hello world!", + }, + }, + "ConfigureContextFunc-Meta": { + server: NewGRPCProviderServer(&Provider{ + ConfigureContextFunc: func(ctx context.Context, d *ResourceData) (any, diag.Diagnostics) { + return &FakeMetaStruct{ + Attr: "hello world!", + }, nil + }, + Schema: map[string]*Schema{ + "test": { + Optional: true, + Type: TypeString, + }, + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("test-value"), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + expectedMeta: &FakeMetaStruct{ + Attr: "hello world!", + }, + }, + "ConfigureProvider-Meta": { + server: NewGRPCProviderServer(&Provider{ + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + resp.Meta = &FakeMetaStruct{ + Attr: "hello world!", + } + }, + Schema: map[string]*Schema{ + "test": { + Optional: true, + Type: TypeString, + }, + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("test-value"), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + expectedMeta: &FakeMetaStruct{ + Attr: "hello world!", + }, + }, + "ConfigureProvider-Deferred-Allowed": { + server: NewGRPCProviderServer(&Provider{ + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + resp.Deferred = &Deferred{ + Reason: DeferredReasonProviderConfigUnknown, + } + }, + Schema: map[string]*Schema{ + "test": { + Optional: true, + Type: TypeString, + }, + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + ClientCapabilities: &tfprotov5.ConfigureProviderClientCapabilities{ + DeferralAllowed: true, + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("test-value"), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{}, + expectedProviderDeferred: &Deferred{ + Reason: DeferredReasonProviderConfigUnknown, + }, + }, + "ConfigureProvider-Deferred-ClientCapabilities-Unset-Diagnostic": { + server: NewGRPCProviderServer(&Provider{ + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + resp.Deferred = &Deferred{ + Reason: DeferredReasonProviderConfigUnknown, + } + }, + Schema: map[string]*Schema{ + "test": { + Optional: true, + Type: TypeString, + }, + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + // No ClientCapabilities set, deferred response will cause a diagnostic to be returned + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("test-value"), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Invalid Deferred Provider Response", + Detail: "Provider configured a deferred response for all resources and data sources but the Terraform request " + + "did not indicate support for deferred actions. This is an issue with the provider and should be reported to the provider developers.", + }, + }, + }, + }, + "ConfigureProvider-Deferred-Not-Allowed-Diagnostic": { + server: NewGRPCProviderServer(&Provider{ + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + resp.Deferred = &Deferred{ + Reason: DeferredReasonProviderConfigUnknown, + } + }, + Schema: map[string]*Schema{ + "test": { + Optional: true, + Type: TypeString, + }, + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + ClientCapabilities: &tfprotov5.ConfigureProviderClientCapabilities{ + // Deferred response will cause a diagnostic to be returned + DeferralAllowed: false, + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "test": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("test-value"), + }), + ), + }, + }, + expected: &tfprotov5.ConfigureProviderResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Invalid Deferred Provider Response", + Detail: "Provider configured a deferred response for all resources and data sources but the Terraform request " + + "did not indicate support for deferred actions. This is an issue with the provider and should be reported to the provider developers.", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + resp, err := testCase.server.ConfigureProvider(context.Background(), testCase.req) + + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(resp, testCase.expected); diff != "" { + t.Fatalf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(testCase.server.provider.Meta(), testCase.expectedMeta); diff != "" { + t.Fatalf("unexpected difference: %s", diff) + } + + if len(resp.Diagnostics) == 0 { + if diff := cmp.Diff(testCase.server.provider.providerDeferred, testCase.expectedProviderDeferred); diff != "" { + t.Fatalf("unexpected difference: %s", diff) + } + } + }) + } +} + +func TestGRPCProviderServerGetMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + Provider *Provider + Expected *tfprotov5.GetMetadataResponse + }{ + "datasources": { + Provider: &Provider{ + DataSourcesMap: map[string]*Resource{ + "test_datasource1": nil, // implementation not necessary + "test_datasource2": nil, // implementation not necessary + }, + }, + Expected: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{ + { + TypeName: "test_datasource1", + }, + { + TypeName: "test_datasource2", + }, + }, + Functions: []tfprotov5.FunctionMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + }, + }, + }, + "datasources and resources": { + Provider: &Provider{ + DataSourcesMap: map[string]*Resource{ + "test_datasource1": nil, // implementation not necessary + "test_datasource2": nil, // implementation not necessary + }, + ResourcesMap: map[string]*Resource{ + "test_resource1": nil, // implementation not necessary + "test_resource2": nil, // implementation not necessary + }, + }, + Expected: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{ + { + TypeName: "test_datasource1", + }, + { + TypeName: "test_datasource2", }, }, Functions: []tfprotov5.FunctionMetadata{}, @@ -2822,36 +4033,511 @@ func TestUpgradeState_flatmapStateMissingMigrateState(t *testing.T) { } } +func TestReadResource(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + server *GRPCProviderServer + req *tfprotov5.ReadResourceRequest + expected *tfprotov5.ReadResourceResponse + }{ + "read-resource": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Required: true, + }, + "test_bool": { + Type: TypeBool, + Computed: true, + }, + "test_string": { + Type: TypeString, + Computed: true, + }, + }, + ReadContext: func(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics { + err := d.Set("test_bool", true) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("test_string", "new-state-val") + if err != nil { + return diag.FromErr(err) + } + + return nil + }, + }, + }, + }), + req: &tfprotov5.ReadResourceRequest{ + TypeName: "test", + CurrentState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test_bool": cty.Bool, + "test_string": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("test-id"), + "test_bool": cty.BoolVal(false), + "test_string": cty.StringVal("prior-state-val"), + }), + ), + }, + }, + expected: &tfprotov5.ReadResourceResponse{ + NewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test_bool": cty.Bool, + "test_string": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("test-id"), + "test_bool": cty.BoolVal(true), + "test_string": cty.StringVal("new-state-val"), + }), + ), + }, + }, + }, + "deferred-response-unknown-val": { + server: NewGRPCProviderServer(&Provider{ + // Deferred response will skip read function and return current state + providerDeferred: &Deferred{ + Reason: DeferredReasonProviderConfigUnknown, + }, + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Required: true, + }, + "test_bool": { + Type: TypeBool, + Computed: true, + }, + "test_string": { + Type: TypeString, + Computed: true, + }, + }, + ReadContext: func(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics { + return diag.Errorf("Test assertion failed: read shouldn't be called when provider deferred response is present") + }, + }, + }, + }), + req: &tfprotov5.ReadResourceRequest{ + ClientCapabilities: &tfprotov5.ReadResourceClientCapabilities{ + DeferralAllowed: true, + }, + TypeName: "test", + CurrentState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test_bool": cty.Bool, + "test_string": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("test-id"), + "test_bool": cty.BoolVal(false), + "test_string": cty.StringVal("prior-state-val"), + }), + ), + }, + }, + expected: &tfprotov5.ReadResourceResponse{ + NewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test_bool": cty.Bool, + "test_string": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("test-id"), + "test_bool": cty.BoolVal(false), + "test_string": cty.StringVal("prior-state-val"), + }), + ), + }, + Deferred: &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReasonProviderConfigUnknown, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + resp, err := testCase.server.ReadResource(context.Background(), testCase.req) + + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(resp, testCase.expected, valueComparer); diff != "" { + ty := testCase.server.getResourceSchemaBlock("test").ImpliedType() + + if resp != nil && resp.NewState != nil { + t.Logf("resp.NewState.MsgPack: %s", mustMsgpackUnmarshal(ty, resp.NewState.MsgPack)) + } + + if testCase.expected != nil && testCase.expected.NewState != nil { + t.Logf("expected: %s", mustMsgpackUnmarshal(ty, testCase.expected.NewState.MsgPack)) + } + + t.Error(diff) + } + }) + } +} + func TestPlanResourceChange(t *testing.T) { t.Parallel() testCases := map[string]struct { - TestResource *Resource - ExpectedUnsafeLegacyTypeSystem bool + server *GRPCProviderServer + req *tfprotov5.PlanResourceChangeRequest + expected *tfprotov5.PlanResourceChangeResponse }{ - "basic": { - TestResource: &Resource{ - SchemaVersion: 4, - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - Optional: true, + "basic-plan": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + Optional: true, + }, + }, }, }, + }), + req: &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "foo": cty.Number, + }), + cty.NullVal( + cty.Object(map[string]cty.Type{ + "foo": cty.Number, + }), + ), + ), + }, + ProposedNewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.Number, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.NullVal(cty.Number), + }), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.Number, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "foo": cty.NullVal(cty.Number), + }), + ), + }, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.Number, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.NullVal(cty.Number), + }), + ), + }, + RequiresReplace: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("id"), + }, + PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), + UnsafeToUseLegacyTypeSystem: true, + }, + }, + "basic-plan-EnableLegacyTypeSystemPlanErrors": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + // Will set UnsafeToUseLegacyTypeSystem to false + EnableLegacyTypeSystemPlanErrors: true, + Schema: map[string]*Schema{ + "foo": { + Type: TypeInt, + Optional: true, + }, + }, + }, + }, + }), + req: &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "foo": cty.Number, + }), + cty.NullVal( + cty.Object(map[string]cty.Type{ + "foo": cty.Number, + }), + ), + ), + }, + ProposedNewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.Number, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.NullVal(cty.Number), + }), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.Number, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "foo": cty.NullVal(cty.Number), + }), + ), + }, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.Number, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.NullVal(cty.Number), + }), + ), + }, + RequiresReplace: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("id"), + }, + PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), + UnsafeToUseLegacyTypeSystem: false, + }, + }, + "deferred-with-provider-plan-modification": { + server: NewGRPCProviderServer(&Provider{ + providerDeferred: &Deferred{ + Reason: DeferredReasonProviderConfigUnknown, + }, + ResourcesMap: map[string]*Resource{ + "test": { + ResourceBehavior: ResourceBehavior{ + ProviderDeferred: ProviderDeferredBehavior{ + // Will ensure that CustomizeDiff is called + EnablePlanModification: true, + }, + }, + SchemaVersion: 4, + CustomizeDiff: func(ctx context.Context, d *ResourceDiff, i interface{}) error { + return d.SetNew("foo", "new-foo-value") + }, + Schema: map[string]*Schema{ + "foo": { + Type: TypeString, + Optional: true, + Computed: true, + }, + }, + }, + }, + }), + req: &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + ClientCapabilities: &tfprotov5.PlanResourceChangeClientCapabilities{ + DeferralAllowed: true, + }, + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "foo": cty.String, + }), + cty.NullVal( + cty.Object(map[string]cty.Type{ + "foo": cty.String, + }), + ), + ), + }, + ProposedNewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.UnknownVal(cty.String), + }), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "foo": cty.NullVal(cty.String), + }), + ), + }, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + Deferred: &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReasonProviderConfigUnknown, + }, + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.StringVal("new-foo-value"), + }), + ), + }, + RequiresReplace: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("id"), + }, + PlannedPrivate: []byte(`{"_new_extra_shim":{}}`), + UnsafeToUseLegacyTypeSystem: true, + }, + }, + "deferred-skip-plan-modification": { + server: NewGRPCProviderServer(&Provider{ + providerDeferred: &Deferred{ + Reason: DeferredReasonProviderConfigUnknown, + }, + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + CustomizeDiff: func(ctx context.Context, d *ResourceDiff, i interface{}) error { + return errors.New("Test assertion failed: CustomizeDiff shouldn't be called") + }, + Schema: map[string]*Schema{ + "foo": { + Type: TypeString, + Optional: true, + Computed: true, + }, + }, + }, + }, + }), + req: &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + ClientCapabilities: &tfprotov5.PlanResourceChangeClientCapabilities{ + DeferralAllowed: true, + }, + PriorState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "foo": cty.String, + }), + cty.NullVal( + cty.Object(map[string]cty.Type{ + "foo": cty.String, + }), + ), + ), + }, + ProposedNewState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.StringVal("from-config!"), + }), + ), + }, + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "foo": cty.StringVal("from-config!"), + }), + ), + }, }, - ExpectedUnsafeLegacyTypeSystem: true, - }, - "EnableLegacyTypeSystemPlanErrors": { - TestResource: &Resource{ - EnableLegacyTypeSystemPlanErrors: true, - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - Optional: true, - }, + expected: &tfprotov5.PlanResourceChangeResponse{ + Deferred: &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReasonProviderConfigUnknown, + }, + // Returns proposed new state with deferred response + PlannedState: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "foo": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "foo": cty.StringVal("from-config!"), + }), + ), }, + UnsafeToUseLegacyTypeSystem: true, }, - ExpectedUnsafeLegacyTypeSystem: false, }, } @@ -2861,73 +4547,23 @@ func TestPlanResourceChange(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - server := NewGRPCProviderServer(&Provider{ - ResourcesMap: map[string]*Resource{ - "test": testCase.TestResource, - }, - }) - - schema := testCase.TestResource.CoreConfigSchema() - priorState, err := msgpack.Marshal(cty.NullVal(schema.ImpliedType()), schema.ImpliedType()) - if err != nil { - t.Fatal(err) - } - - // A propsed state with only the ID unknown will produce a nil diff, and - // should return the propsed state value. - proposedVal, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - })) - if err != nil { - t.Fatal(err) - } - proposedState, err := msgpack.Marshal(proposedVal, schema.ImpliedType()) - if err != nil { - t.Fatal(err) - } - - config, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), - })) - if err != nil { - t.Fatal(err) - } - configBytes, err := msgpack.Marshal(config, schema.ImpliedType()) + resp, err := testCase.server.PlanResourceChange(context.Background(), testCase.req) if err != nil { t.Fatal(err) } - testReq := &tfprotov5.PlanResourceChangeRequest{ - TypeName: "test", - PriorState: &tfprotov5.DynamicValue{ - MsgPack: priorState, - }, - ProposedNewState: &tfprotov5.DynamicValue{ - MsgPack: proposedState, - }, - Config: &tfprotov5.DynamicValue{ - MsgPack: configBytes, - }, - } - - resp, err := server.PlanResourceChange(context.Background(), testReq) - if err != nil { - t.Fatal(err) - } + if diff := cmp.Diff(resp, testCase.expected, valueComparer); diff != "" { + ty := testCase.server.getResourceSchemaBlock("test").ImpliedType() - plannedStateVal, err := msgpack.Unmarshal(resp.PlannedState.MsgPack, schema.ImpliedType()) - if err != nil { - t.Fatal(err) - } + if resp != nil && resp.PlannedState != nil { + t.Logf("resp.PlannedState.MsgPack: %s", mustMsgpackUnmarshal(ty, resp.PlannedState.MsgPack)) + } - if !cmp.Equal(proposedVal, plannedStateVal, valueComparer) { - t.Fatal(cmp.Diff(proposedVal, plannedStateVal, valueComparer)) - } + if testCase.expected != nil && testCase.expected.PlannedState != nil { + t.Logf("expected: %s", mustMsgpackUnmarshal(ty, testCase.expected.PlannedState.MsgPack)) + } - //nolint:staticcheck // explicitly for this SDK - if testCase.ExpectedUnsafeLegacyTypeSystem != resp.UnsafeToUseLegacyTypeSystem { - //nolint:staticcheck // explicitly for this SDK - t.Fatalf("expected UnsafeLegacyTypeSystem %t, got: %t", testCase.ExpectedUnsafeLegacyTypeSystem, resp.UnsafeToUseLegacyTypeSystem) + t.Error(diff) } }) } @@ -3337,6 +4973,234 @@ func TestApplyResourceChange_bigint(t *testing.T) { } } +func TestImportResourceState(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + server *GRPCProviderServer + req *tfprotov5.ImportResourceStateRequest + expected *tfprotov5.ImportResourceStateResponse + }{ + "basic-import": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Required: true, + }, + "test_string": { + Type: TypeString, + Computed: true, + }, + }, + Importer: &ResourceImporter{ + StateContext: func(ctx context.Context, d *ResourceData, meta interface{}) ([]*ResourceData, error) { + err := d.Set("test_string", "new-imported-val") + if err != nil { + return nil, err + } + + return []*ResourceData{d}, nil + }, + }, + }, + }, + }), + req: &tfprotov5.ImportResourceStateRequest{ + TypeName: "test", + ID: "imported-id", + }, + expected: &tfprotov5.ImportResourceStateResponse{ + ImportedResources: []*tfprotov5.ImportedResource{ + { + TypeName: "test", + State: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test_string": cty.String, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("imported-id"), + "test_string": cty.StringVal("new-imported-val"), + }), + ), + }, + Private: []byte(`{"schema_version":"1"}`), + }, + }, + }, + }, + "resource-doesnt-exist": { + server: NewGRPCProviderServer(&Provider{ + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Required: true, + }, + "test_string": { + Type: TypeString, + Computed: true, + }, + }, + Importer: &ResourceImporter{ + StateContext: func(ctx context.Context, d *ResourceData, meta interface{}) ([]*ResourceData, error) { + return nil, errors.New("Test assertion failed: import shouldn't be called") + }, + }, + }, + }, + }), + req: &tfprotov5.ImportResourceStateRequest{ + TypeName: "fake-resource", + ID: "imported-id", + }, + expected: &tfprotov5.ImportResourceStateResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "unknown resource type: fake-resource", + }, + }, + }, + }, + "deferred-response-resource-doesnt-exist": { + server: NewGRPCProviderServer(&Provider{ + providerDeferred: &Deferred{ + Reason: DeferredReasonProviderConfigUnknown, + }, + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Required: true, + }, + "test_string": { + Type: TypeString, + Computed: true, + }, + }, + Importer: &ResourceImporter{ + StateContext: func(ctx context.Context, d *ResourceData, meta interface{}) ([]*ResourceData, error) { + return nil, errors.New("Test assertion failed: import shouldn't be called") + }, + }, + }, + }, + }), + req: &tfprotov5.ImportResourceStateRequest{ + TypeName: "fake-resource", + ID: "imported-id", + ClientCapabilities: &tfprotov5.ImportResourceStateClientCapabilities{ + DeferralAllowed: true, + }, + }, + expected: &tfprotov5.ImportResourceStateResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "unknown resource type: fake-resource", + }, + }, + }, + }, + "deferred-response-unknown-val": { + server: NewGRPCProviderServer(&Provider{ + // Deferred response will skip import function and return an unknown value + providerDeferred: &Deferred{ + Reason: DeferredReasonProviderConfigUnknown, + }, + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 1, + Schema: map[string]*Schema{ + "id": { + Type: TypeString, + Required: true, + }, + "test_string": { + Type: TypeString, + Computed: true, + }, + }, + Importer: &ResourceImporter{ + StateContext: func(ctx context.Context, d *ResourceData, meta interface{}) ([]*ResourceData, error) { + return nil, errors.New("Test assertion failed: import shouldn't be called when deferred response is present") + }, + }, + }, + }, + }), + req: &tfprotov5.ImportResourceStateRequest{ + TypeName: "test", + ID: "imported-id", + ClientCapabilities: &tfprotov5.ImportResourceStateClientCapabilities{ + DeferralAllowed: true, + }, + }, + expected: &tfprotov5.ImportResourceStateResponse{ + Deferred: &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReasonProviderConfigUnknown, + }, + ImportedResources: []*tfprotov5.ImportedResource{ + { + TypeName: "test", + State: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test_string": cty.String, + }), + cty.UnknownVal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test_string": cty.String, + }), + ), + ), + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + resp, err := testCase.server.ImportResourceState(context.Background(), testCase.req) + + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(resp, testCase.expected, valueComparer); diff != "" { + ty := testCase.server.getResourceSchemaBlock("test").ImpliedType() + + if resp != nil && len(resp.ImportedResources) > 0 { + t.Logf("resp.ImportedResources[0].State.MsgPack: %s", mustMsgpackUnmarshal(ty, resp.ImportedResources[0].State.MsgPack)) + } + + if testCase.expected != nil && len(testCase.expected.ImportedResources) > 0 { + t.Logf("expected: %s", mustMsgpackUnmarshal(ty, testCase.expected.ImportedResources[0].State.MsgPack)) + } + + t.Error(diff) + } + }) + } +} + // Timeouts should never be present in imported resources. // Reference: https://github.com/hashicorp/terraform-plugin-sdk/issues/1145 func TestImportResourceState_Timeouts_None(t *testing.T) { @@ -3896,6 +5760,73 @@ func TestReadDataSource(t *testing.T) { }, }, }, + "deferred-response-unknown-val": { + server: NewGRPCProviderServer(&Provider{ + // Deferred response will skip read function and return an unknown value + providerDeferred: &Deferred{ + Reason: DeferredReasonProviderConfigUnknown, + }, + DataSourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 1, + Schema: map[string]*Schema{ + "test": { + Type: TypeString, + Required: true, + }, + "test_bool": { + Type: TypeBool, + Computed: true, + }, + }, + ReadContext: func(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics { + return diag.Errorf("Test assertion failed: read shouldn't be called when provider deferred response is present") + }, + }, + }, + }), + req: &tfprotov5.ReadDataSourceRequest{ + ClientCapabilities: &tfprotov5.ReadDataSourceClientCapabilities{ + DeferralAllowed: true, + }, + TypeName: "test", + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + "test_bool": cty.Bool, + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "test": cty.StringVal("test-string"), + "test_bool": cty.NullVal(cty.Bool), + }), + ), + }, + }, + expected: &tfprotov5.ReadDataSourceResponse{ + State: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + "test_bool": cty.Bool, + }), + cty.UnknownVal( + cty.Object(map[string]cty.Type{ + "id": cty.String, + "test": cty.String, + "test_bool": cty.Bool, + }), + ), + ), + }, + Deferred: &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReasonProviderConfigUnknown, + }, + }, + }, } for name, testCase := range testCases { @@ -3903,7 +5834,6 @@ func TestReadDataSource(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - resp, err := testCase.server.ReadDataSource(context.Background(), testCase.req) if err != nil { diff --git a/helper/schema/provider.go b/helper/schema/provider.go index 55ba6e2ce8..a75ae2fc28 100644 --- a/helper/schema/provider.go +++ b/helper/schema/provider.go @@ -92,12 +92,75 @@ type Provider struct { // cancellation signal. This function can yield Diagnostics. ConfigureContextFunc ConfigureContextFunc + // ConfigureProvider is a function for configuring the provider that + // supports additional features, such as returning a deferred response. + // + // Providers that require these additional features should use this function + // as a replacement for ConfigureContextFunc. + // + // This function receives a context.Context that will cancel when + // Terraform sends a cancellation signal. + ConfigureProvider func(context.Context, ConfigureProviderRequest, *ConfigureProviderResponse) + // configured is enabled after a Configure() call configured bool meta interface{} TerraformVersion string + + // deferralAllowed is populated by the ConfigureProvider RPC request and + // should only be used during provider configuration. + // + // MAINTAINER NOTE: Other RPCs that need to check if deferrals are allowed + // should use the relevant RPC request field in ClientCapabilities. + deferralAllowed bool + + // providerDeferred is a global deferred response that will be returned automatically + // for all resources and data sources associated to this provider server. + providerDeferred *Deferred +} + +type ConfigureProviderRequest struct { + // DeferralAllowed indicates whether the Terraform request configuring + // the provider allows a deferred response. This field should be used to determine + // if `(schema.ConfigureProviderResponse).Deferred` can be set. + // + // If true: `(schema.ConfigureProviderResponse).Deferred` can be + // set to automatically defer all resources and data sources associated + // with this provider. + // + // If false: `(schema.ConfigureProviderResponse).Deferred` + // will return an error diagnostic if set. + // + // NOTE: This functionality is related to deferred action support, which is currently experimental and is subject + // to change or break without warning. It is not protected by version compatibility guarantees. + DeferralAllowed bool + + // ResourceData is used to query and set the attributes of a resource. + ResourceData *ResourceData +} + +type ConfigureProviderResponse struct { + // Meta is stored and passed into the subsequent resources as the meta + // parameter. This return value is usually used to pass along a + // configured API client, a configuration structure, etc. + Meta interface{} + + // Diagnostics report errors or warnings related to configuring the + // provider. An empty slice indicates success, with no warnings or + // errors generated. + Diagnostics diag.Diagnostics + + // Deferred indicates that Terraform should automatically defer + // all resources and data sources for this provider. + // + // This field can only be set if + // `(schema.ConfigureProviderRequest).DeferralAllowed` is true. + // + // NOTE: This functionality is related to deferred action support, which is currently experimental and is subject + // to change or break without warning. It is not protected by version compatibility guarantees. + Deferred *Deferred } // ConfigureFunc is the function used to configure a Provider. @@ -262,7 +325,7 @@ func (p *Provider) ValidateResource( // This won't be called at all if no provider configuration is given. func (p *Provider) Configure(ctx context.Context, c *terraform.ResourceConfig) diag.Diagnostics { // No configuration - if p.ConfigureFunc == nil && p.ConfigureContextFunc == nil { + if p.ConfigureFunc == nil && p.ConfigureContextFunc == nil && p.ConfigureProvider == nil { return nil } @@ -313,6 +376,24 @@ func (p *Provider) Configure(ctx context.Context, c *terraform.ResourceConfig) d p.meta = meta } + if p.ConfigureProvider != nil { + req := ConfigureProviderRequest{ + DeferralAllowed: p.deferralAllowed, + ResourceData: data, + } + resp := ConfigureProviderResponse{} + + p.ConfigureProvider(ctx, req, &resp) + + diags = append(diags, resp.Diagnostics...) + if diags.HasError() { + return diags + } + + p.meta = resp.Meta + p.providerDeferred = resp.Deferred + } + p.configured = true return diags diff --git a/helper/schema/resource.go b/helper/schema/resource.go index 7564a0aff2..1c944c9b48 100644 --- a/helper/schema/resource.go +++ b/helper/schema/resource.go @@ -640,6 +640,34 @@ type Resource struct { // changes with it enabled. However, data-based errors typically require // logic fixes that should be applicable for both SDKs to be resolved. EnableLegacyTypeSystemPlanErrors bool + + // ResourceBehavior is used to control SDK-specific logic when + // interacting with this resource. + ResourceBehavior ResourceBehavior +} + +// ResourceBehavior controls SDK-specific logic when interacting +// with a resource. +type ResourceBehavior struct { + // ProviderDeferred enables provider-defined logic to be executed + // in the case of a deferred response from (Provider).ConfigureProvider. + // + // NOTE: This functionality is related to deferred action support, which is currently experimental and is subject + // to change or break without warning. It is not protected by version compatibility guarantees. + ProviderDeferred ProviderDeferredBehavior +} + +// ProviderDeferredBehavior enables provider-defined logic to be executed +// in the case of a deferred response from provider configuration. +// +// NOTE: This functionality is related to deferred action support, which is currently experimental and is subject +// to change or break without warning. It is not protected by version compatibility guarantees. +type ProviderDeferredBehavior struct { + // When EnablePlanModification is true, the SDK will execute provider-defined logic + // during plan (CustomizeDiff, Default, DiffSupressFunc, etc.) if ConfigureProvider + // returns a deferred response. The SDK will then automatically return a deferred response + // along with the modified plan. + EnablePlanModification bool } // SchemaMap returns the schema information for this Resource whether it is diff --git a/internal/logging/keys.go b/internal/logging/keys.go index 983fde437a..ed238ea590 100644 --- a/internal/logging/keys.go +++ b/internal/logging/keys.go @@ -28,6 +28,9 @@ const ( // The type of resource being operated on, such as "random_pet" KeyResourceType = "tf_resource_type" + // The Deferred reason for an RPC response + KeyDeferredReason = "tf_deferred_reason" + // The name of the test being executed. KeyTestName = "test_name"