From efdc7716ca6a5fc82066cedfb317a6d6fe086710 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 19 Apr 2024 11:31:19 -0400 Subject: [PATCH 01/26] update plugin-go --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 211d8c3d8cb..356f9cb2acb 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.20.0 github.com/hashicorp/terraform-json v0.21.0 - github.com/hashicorp/terraform-plugin-go v0.22.1 + github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240419152848-9a1607db1cab github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/mitchellh/copystructure v1.2.0 github.com/mitchellh/go-testing-interface v1.14.1 @@ -30,7 +30,7 @@ require ( github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/fatih/color v1.16.0 // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -51,7 +51,7 @@ require ( golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.13.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect - google.golang.org/grpc v1.62.1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/grpc v1.63.2 // indirect google.golang.org/protobuf v1.33.0 // indirect ) diff --git a/go.sum b/go.sum index c7fd73b71f1..36c5e88f49c 100644 --- a/go.sum +++ b/go.sum @@ -36,8 +36,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4er github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -72,8 +72,8 @@ github.com/hashicorp/terraform-exec v0.20.0 h1:DIZnPsqzPGuUnq6cH8jWcPunBfY+C+M8J github.com/hashicorp/terraform-exec v0.20.0/go.mod h1:ckKGkJWbsNqFKV1itgMnE0hY9IYf1HoiekpuN0eWoDw= github.com/hashicorp/terraform-json v0.21.0 h1:9NQxbLNqPbEMze+S6+YluEdXgJmhQykRyRNd+zTI05U= github.com/hashicorp/terraform-json v0.21.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk= -github.com/hashicorp/terraform-plugin-go v0.22.1 h1:iTS7WHNVrn7uhe3cojtvWWn83cm2Z6ryIUDTRO0EV7w= -github.com/hashicorp/terraform-plugin-go v0.22.1/go.mod h1:qrjnqRghvQ6KnDbB12XeZ4FluclYwptntoWCr9QaXTI= +github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240419152848-9a1607db1cab h1:Q86RQOyr+03Z+MbEi1hhlDADMwc0k24XYqASQAYPE0A= +github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240419152848-9a1607db1cab/go.mod h1:drq8Snexp9HsbFZddvyLHN6LuWHHndSQg+gV+FPkcIM= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= @@ -192,10 +192,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= -google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= -google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= From d9c69932e767a0b7b876250f83c5bcd19034a05c Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 19 Apr 2024 16:03:07 -0400 Subject: [PATCH 02/26] quick implementation of automatic deferral (no opt-in currently) --- helper/schema/deferral.go | 36 ++++++++++++++ helper/schema/grpc_provider.go | 91 ++++++++++++++++++++++++++++++++++ helper/schema/provider.go | 69 +++++++++++++++++++++++++- 3 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 helper/schema/deferral.go diff --git a/helper/schema/deferral.go b/helper/schema/deferral.go new file mode 100644 index 00000000000..d626449ff3e --- /dev/null +++ b/helper/schema/deferral.go @@ -0,0 +1,36 @@ +package schema + +// MAINTAINER NOTE: Only PROVIDER_CONFIG_UNKNOWN (enum value 2 in the plugin-protocol) is relevant +// for SDKv2. Since (DeferralResponse).DeferralReason is mapped directly to the plugin-protocol, +// the other enum values are intentionally omitted here. +const ( + // DeferralReasonUnknown represents an undefined deferral reason. + DeferralReasonUnknown DeferralReason = 0 + + // DeferralReasonProviderConfigUnknown represents a deferral reason caused + // by unknown provider configuration. + DeferralReasonProviderConfigUnknown DeferralReason = 2 +) + +// DeferralResponse 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. +type DeferralResponse struct { + // Reason represents the deferral reason. + Reason DeferralReason +} + +// TODO: doc +type DeferralReason int32 + +// TODO: doc +func (d DeferralReason) 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 70477da45aa..6cbe4db00e5 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -6,6 +6,7 @@ package schema import ( "context" "encoding/json" + "errors" "fmt" "strconv" "sync" @@ -632,6 +633,26 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re } schemaBlock := s.getResourceSchemaBlock(req.TypeName) + if s.provider.providerDeferral != nil { + // TODO: This is currently hardcoded, so provider developers can't protect themselves from accidently returning + // a deferral response for older Terraform versions that don't support it. + if req.DeferralAllowed { + // TODO: Is this okay to set to CurrentState? + resp.NewState = req.CurrentState + resp.Deferred = &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(s.provider.providerDeferral.Reason), + } + return resp, nil + } + + resp.Diagnostics = convert.AppendProtoDiag( + ctx, + resp.Diagnostics, + errors.New("provider attempted to return a deferral response for ReadResource, but the Terraform client doesn't support it."), + ) + 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 +752,27 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot resp.UnsafeToUseLegacyTypeSystem = true } + if s.provider.providerDeferral != nil { + // TODO: This is currently hardcoded, so provider developers can't protect themselves from accidently returning + // a deferral response for older Terraform versions that don't support it. + if req.DeferralAllowed { + // TODO: Is this okay to use as planned state? + resp.PlannedState = req.ProposedNewState + resp.PlannedPrivate = req.PriorPrivate + resp.Deferred = &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(s.provider.providerDeferral.Reason), + } + return resp, nil + } + + resp.Diagnostics = convert.AppendProtoDiag( + ctx, + resp.Diagnostics, + errors.New("provider attempted to return a deferral response for PlanResourceChange, but the Terraform client doesn't support it."), + ) + return resp, nil + } + priorStateVal, err := msgpack.Unmarshal(req.PriorState.MsgPack, schemaBlock.ImpliedType()) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) @@ -1145,6 +1187,25 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro Type: req.TypeName, } + if s.provider.providerDeferral != nil { + // TODO: This is currently hardcoded, so provider developers can't protect themselves from accidently returning + // a deferral response for older Terraform versions that don't support it. + if req.DeferralAllowed { + // TODO: Set resp.ImportedResources? + resp.Deferred = &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(s.provider.providerDeferral.Reason), + } + return resp, nil + } + + resp.Diagnostics = convert.AppendProtoDiag( + ctx, + resp.Diagnostics, + errors.New("provider attempted to return a deferral response for PlanResourceChange, but the Terraform client doesn't support it."), + ) + 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 +1315,36 @@ func (s *GRPCProviderServer) ReadDataSource(ctx context.Context, req *tfprotov5. schemaBlock := s.getDatasourceSchemaBlock(req.TypeName) + if s.provider.providerDeferral != nil { + // TODO: This is currently hardcoded, so provider developers can't protect themselves from accidently returning + // a deferral response for older Terraform versions that don't support it. + if req.DeferralAllowed { + // TODO: Is this the correct way to return all unknowns? + // TODO: Should we pass back config values as known? (core isn't using it right now, but will eventually be displayed) + 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.providerDeferral.Reason), + } + return resp, nil + } + + resp.Diagnostics = convert.AppendProtoDiag( + ctx, + resp.Diagnostics, + errors.New("provider attempted to return a deferral response for ReadResource, but the Terraform client doesn't support it."), + ) + return resp, nil + } + configVal, err := msgpack.Unmarshal(req.Config.MsgPack, schemaBlock.ImpliedType()) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) diff --git a/helper/schema/provider.go b/helper/schema/provider.go index 55ba6e2ce80..3647f0dd9b8 100644 --- a/helper/schema/provider.go +++ b/helper/schema/provider.go @@ -92,12 +92,60 @@ 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 deferral 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, ConfigureRequest, *ConfigureResponse) + // configured is enabled after a Configure() call configured bool meta interface{} TerraformVersion string + + // providerDeferral is a global deferral response that will be used for all + // resources and data sources associated to this provider server. + providerDeferral *DeferralResponse +} + +type ConfigureRequest struct { + // DeferralAllowed indicates whether the Terraform client initiating + // the read request allows a deferral response. + // + // If true: `(schema.ConfigureResponse).DeferralResponse` can be + // set. + // + // If false: `(schema.ConfigureResponse).DeferralResponse` + // will return an error diagnostic if set. + DeferralAllowed bool + + // ResourceData is used to query and set the attributes of a resource. + ResourceData *ResourceData +} + +type ConfigureResponse 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 + + // DeferralResponse indicates that Terraform should automatically defer + // all resources and data sources for this provider. + // + // This field can only be set if + // `(schema.ConfigureRequest).DeferralAllowed` is true. + DeferralResponse *DeferralResponse } // ConfigureFunc is the function used to configure a Provider. @@ -262,7 +310,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 +361,25 @@ func (p *Provider) Configure(ctx context.Context, c *terraform.ResourceConfig) d p.meta = meta } + if p.ConfigureProvider != nil { + req := ConfigureRequest{ + // TODO: Populate from the protocol when available + DeferralAllowed: true, + ResourceData: data, + } + resp := ConfigureResponse{} + + p.ConfigureProvider(ctx, req, &resp) + + diags = append(diags, resp.Diagnostics...) + if diags.HasError() { + return diags + } + + p.meta = resp.Meta + p.providerDeferral = resp.DeferralResponse + } + p.configured = true return diags From b7c02ce1c290b967441ae489ba9d7d3cfa777d6f Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 22 Apr 2024 12:24:59 -0400 Subject: [PATCH 03/26] add opt-in logic with ResourceBehavior --- helper/schema/grpc_provider.go | 130 +++++++++++++++++---------------- helper/schema/resource.go | 22 ++++++ 2 files changed, 91 insertions(+), 61 deletions(-) diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index 6cbe4db00e5..08f613ba624 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -634,22 +634,22 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re schemaBlock := s.getResourceSchemaBlock(req.TypeName) if s.provider.providerDeferral != nil { - // TODO: This is currently hardcoded, so provider developers can't protect themselves from accidently returning - // a deferral response for older Terraform versions that don't support it. - if req.DeferralAllowed { - // TODO: Is this okay to set to CurrentState? - resp.NewState = req.CurrentState - resp.Deferred = &tfprotov5.Deferred{ - Reason: tfprotov5.DeferredReason(s.provider.providerDeferral.Reason), - } + // This scenario should be prevented by ConfigureProvider, but we check here to ensure we don't + // incorrectly return a deferral response when Terraform doesn't support it. + if !req.DeferralAllowed { + resp.Diagnostics = convert.AppendProtoDiag( + ctx, + resp.Diagnostics, + errors.New("Provider attempted to return a deferral response for ReadResource, but the Terraform client doesn't support it."), + ) return resp, nil } - resp.Diagnostics = convert.AppendProtoDiag( - ctx, - resp.Diagnostics, - errors.New("provider attempted to return a deferral response for ReadResource, but the Terraform client doesn't support it."), - ) + // TODO: Is this okay to set to CurrentState? + resp.NewState = req.CurrentState + resp.Deferred = &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(s.provider.providerDeferral.Reason), + } return resp, nil } @@ -752,27 +752,28 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot resp.UnsafeToUseLegacyTypeSystem = true } - if s.provider.providerDeferral != nil { - // TODO: This is currently hardcoded, so provider developers can't protect themselves from accidently returning - // a deferral response for older Terraform versions that don't support it. - if req.DeferralAllowed { - // TODO: Is this okay to use as planned state? - resp.PlannedState = req.ProposedNewState - resp.PlannedPrivate = req.PriorPrivate - resp.Deferred = &tfprotov5.Deferred{ - Reason: tfprotov5.DeferredReason(s.provider.providerDeferral.Reason), - } - return resp, nil - } - + // This scenario should be prevented by ConfigureProvider, but we check here to ensure we don't + // incorrectly return a deferral response when Terraform doesn't support it. + if s.provider.providerDeferral != nil && !req.DeferralAllowed { resp.Diagnostics = convert.AppendProtoDiag( ctx, resp.Diagnostics, - errors.New("provider attempted to return a deferral response for PlanResourceChange, but the Terraform client doesn't support it."), + errors.New("Provider attempted to return a deferral response for PlanResourceChange, but the Terraform client doesn't support it."), ) return resp, nil } + // Automatic deferral is present and the resource hasn't opted-in to CustomizeDiff being called, return early. + if s.provider.providerDeferral != nil && !res.ResourceBehavior.ProviderDeferral.EnableCustomizeDiff { + // TODO: Is this okay to use as planned state? + resp.PlannedState = req.ProposedNewState + resp.PlannedPrivate = req.PriorPrivate + resp.Deferred = &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(s.provider.providerDeferral.Reason), + } + return resp, nil + } + priorStateVal, err := msgpack.Unmarshal(req.PriorState.MsgPack, schemaBlock.ImpliedType()) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err) @@ -993,6 +994,13 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot resp.RequiresReplace = append(resp.RequiresReplace, pathToAttributePath(p)) } + // Automatic deferral is present, add the deferral response alongside the provider-modified plan + if s.provider.providerDeferral != nil { + resp.Deferred = &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(s.provider.providerDeferral.Reason), + } + } + return resp, nil } @@ -1188,21 +1196,21 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro } if s.provider.providerDeferral != nil { - // TODO: This is currently hardcoded, so provider developers can't protect themselves from accidently returning - // a deferral response for older Terraform versions that don't support it. - if req.DeferralAllowed { - // TODO: Set resp.ImportedResources? - resp.Deferred = &tfprotov5.Deferred{ - Reason: tfprotov5.DeferredReason(s.provider.providerDeferral.Reason), - } + // This scenario should be prevented by ConfigureProvider, but we check here to ensure we don't + // incorrectly return a deferral response when Terraform doesn't support it. + if !req.DeferralAllowed { + resp.Diagnostics = convert.AppendProtoDiag( + ctx, + resp.Diagnostics, + errors.New("Provider attempted to return a deferral response for ImportResourceState, but the Terraform client doesn't support it."), + ) return resp, nil } - resp.Diagnostics = convert.AppendProtoDiag( - ctx, - resp.Diagnostics, - errors.New("provider attempted to return a deferral response for PlanResourceChange, but the Terraform client doesn't support it."), - ) + // TODO: Set resp.ImportedResources? Currently sending back an empty slice by default + resp.Deferred = &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(s.provider.providerDeferral.Reason), + } return resp, nil } @@ -1316,32 +1324,32 @@ func (s *GRPCProviderServer) ReadDataSource(ctx context.Context, req *tfprotov5. schemaBlock := s.getDatasourceSchemaBlock(req.TypeName) if s.provider.providerDeferral != nil { - // TODO: This is currently hardcoded, so provider developers can't protect themselves from accidently returning - // a deferral response for older Terraform versions that don't support it. - if req.DeferralAllowed { - // TODO: Is this the correct way to return all unknowns? - // TODO: Should we pass back config values as known? (core isn't using it right now, but will eventually be displayed) - 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, - } + // This scenario should be prevented by ConfigureProvider, but we check here to ensure we don't + // incorrectly return a deferral response when Terraform doesn't support it. + if !req.DeferralAllowed { + resp.Diagnostics = convert.AppendProtoDiag( + ctx, + resp.Diagnostics, + errors.New("Provider attempted to return a deferral response for ReadDataSource, but the Terraform client doesn't support it."), + ) + return resp, nil + } - resp.Deferred = &tfprotov5.Deferred{ - Reason: tfprotov5.DeferredReason(s.provider.providerDeferral.Reason), - } + // TODO: Is this the correct way to return all unknowns? + // TODO: Should we pass back config values as known? (core isn't using it right now, but will eventually be displayed) + 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.Diagnostics = convert.AppendProtoDiag( - ctx, - resp.Diagnostics, - errors.New("provider attempted to return a deferral response for ReadResource, but the Terraform client doesn't support it."), - ) + resp.Deferred = &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReason(s.provider.providerDeferral.Reason), + } return resp, nil } diff --git a/helper/schema/resource.go b/helper/schema/resource.go index 7564a0aff23..aa89eef51db 100644 --- a/helper/schema/resource.go +++ b/helper/schema/resource.go @@ -640,6 +640,28 @@ 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 { + // ProviderDeferral enables provider-defined logic to be executed + // in the case of automatic deferral from (Provider).ConfigureProvider. + ProviderDeferral ProviderDeferralBehavior +} + +// ProviderDeferral enables provider-defined logic to be executed +// in the case of automatic deferral from provider configure. +type ProviderDeferralBehavior struct { + // When EnableCustomizeDiff is true, the SDK will execute CustomizeDiff + // if ConfigureProvider returns a deferral response. The SDK will + // then automatically return a deferral response along with the + // modified plan. + EnableCustomizeDiff bool } // SchemaMap returns the schema information for this Resource whether it is From f44c9fefbb073a05b7dfe1daa4d0eba8d0e9abb0 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 22 Apr 2024 18:09:42 -0400 Subject: [PATCH 04/26] update naming on exported structs/fields --- helper/schema/grpc_provider.go | 2 +- helper/schema/grpc_provider_test.go | 1173 ++++++++++++++++++++++++++- helper/schema/provider.go | 18 +- helper/schema/resource.go | 10 +- 4 files changed, 1166 insertions(+), 37 deletions(-) diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index 08f613ba624..f11d701b5d5 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -764,7 +764,7 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot } // Automatic deferral is present and the resource hasn't opted-in to CustomizeDiff being called, return early. - if s.provider.providerDeferral != nil && !res.ResourceBehavior.ProviderDeferral.EnableCustomizeDiff { + if s.provider.providerDeferral != nil && !res.ResourceBehavior.ProviderDeferral.EnablePlanModification { // TODO: Is this okay to use as planned state? resp.PlannedState = req.ProposedNewState resp.PlannedPrivate = req.PriorPrivate diff --git a/helper/schema/grpc_provider_test.go b/helper/schema/grpc_provider_test.go index 39b4b078a12..62845d8ae68 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -27,15 +27,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 + expectedProviderDeferral *DeferralResponse + 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 +61,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 +151,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 +225,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 +331,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 +440,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 +543,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 +652,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 +755,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 +864,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 +977,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 +1103,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 +1223,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 +1349,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 +1469,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 +1595,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 +1715,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 +1841,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 +1961,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 +2087,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 +2207,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 +2333,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 +2447,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,6 +2564,46 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-GetRawConfig-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.GetRawConfig() + expected := cty.ObjectVal(map[string]cty.Value{ + "other": cty.StringVal("other-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{ + "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-GetRawConfig-zero-value": { server: NewGRPCProviderServer(&Provider{ Schema: map[string]*Schema{ @@ -1826,6 +2674,39 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "ConfigureProvider-GetRawConfig-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.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{ @@ -1910,6 +2791,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 +2901,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,6 +3018,171 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, expected: &tfprotov5.ConfigureProviderResponse{}, }, + "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"), + }) + + if got.Equals(expected).False() { + 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-Meta": { + server: NewGRPCProviderServer(&Provider{ + ConfigureFunc: func(d *ResourceData) (any, error) { + 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!", + }, + }, + "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-DeferralResponse-Allowed": { + server: NewGRPCProviderServer(&Provider{ + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + resp.DeferralResponse = &DeferralResponse{ + Reason: DeferralReasonProviderConfigUnknown, + } + }, + 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{}, + expectedProviderDeferral: &DeferralResponse{ + Reason: DeferralReasonProviderConfigUnknown, + }, + }, + // TODO: Add tests for diagnostics when a deferral is incorrectly returned } for name, testCase := range testCases { @@ -2079,7 +3198,15 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { } if diff := cmp.Diff(resp, testCase.expected); diff != "" { - t.Errorf("unexpected difference: %s", 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 diff := cmp.Diff(testCase.server.provider.providerDeferral, testCase.expectedProviderDeferral); diff != "" { + t.Fatalf("unexpected difference: %s", diff) } }) } diff --git a/helper/schema/provider.go b/helper/schema/provider.go index 3647f0dd9b8..36a2b542037 100644 --- a/helper/schema/provider.go +++ b/helper/schema/provider.go @@ -100,7 +100,7 @@ type Provider struct { // // This function receives a context.Context that will cancel when // Terraform sends a cancellation signal. - ConfigureProvider func(context.Context, ConfigureRequest, *ConfigureResponse) + ConfigureProvider func(context.Context, ConfigureProviderRequest, *ConfigureProviderResponse) // configured is enabled after a Configure() call configured bool @@ -114,14 +114,14 @@ type Provider struct { providerDeferral *DeferralResponse } -type ConfigureRequest struct { +type ConfigureProviderRequest struct { // DeferralAllowed indicates whether the Terraform client initiating // the read request allows a deferral response. // - // If true: `(schema.ConfigureResponse).DeferralResponse` can be + // If true: `(schema.ConfigureProviderResponse).DeferralResponse` can be // set. // - // If false: `(schema.ConfigureResponse).DeferralResponse` + // If false: `(schema.ConfigureProviderResponse).DeferralResponse` // will return an error diagnostic if set. DeferralAllowed bool @@ -129,7 +129,7 @@ type ConfigureRequest struct { ResourceData *ResourceData } -type ConfigureResponse struct { +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. @@ -144,7 +144,7 @@ type ConfigureResponse struct { // all resources and data sources for this provider. // // This field can only be set if - // `(schema.ConfigureRequest).DeferralAllowed` is true. + // `(schema.ConfigureProviderRequest).DeferralAllowed` is true. DeferralResponse *DeferralResponse } @@ -362,12 +362,12 @@ func (p *Provider) Configure(ctx context.Context, c *terraform.ResourceConfig) d } if p.ConfigureProvider != nil { - req := ConfigureRequest{ + req := ConfigureProviderRequest{ // TODO: Populate from the protocol when available DeferralAllowed: true, ResourceData: data, } - resp := ConfigureResponse{} + resp := ConfigureProviderResponse{} p.ConfigureProvider(ctx, req, &resp) @@ -376,6 +376,8 @@ func (p *Provider) Configure(ctx context.Context, c *terraform.ResourceConfig) d return diags } + // TODO: Add logic to return a diagnostic if DeferralAllowed == false and a deferral is returned + p.meta = resp.Meta p.providerDeferral = resp.DeferralResponse } diff --git a/helper/schema/resource.go b/helper/schema/resource.go index aa89eef51db..ed0da6ef7f3 100644 --- a/helper/schema/resource.go +++ b/helper/schema/resource.go @@ -657,11 +657,11 @@ type ResourceBehavior struct { // ProviderDeferral enables provider-defined logic to be executed // in the case of automatic deferral from provider configure. type ProviderDeferralBehavior struct { - // When EnableCustomizeDiff is true, the SDK will execute CustomizeDiff - // if ConfigureProvider returns a deferral response. The SDK will - // then automatically return a deferral response along with the - // modified plan. - EnableCustomizeDiff bool + // When EnablePlanModification is true, the SDK will execute provider-defined logic + // during plan (CustomizeDiff, Default, DiffSupressFunc, etc.) if ConfigureProvider + // returns a deferral response. The SDK will then automatically return a deferral response + // along with the modified plan. + EnablePlanModification bool } // SchemaMap returns the schema information for this Resource whether it is From 1f768dce38e302a293a42a3cf3d524bab8fa5b5e Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 23 Apr 2024 12:20:53 -0400 Subject: [PATCH 05/26] add data source tests --- helper/schema/grpc_provider.go | 5 +- helper/schema/grpc_provider_test.go | 139 ++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 3 deletions(-) diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index f11d701b5d5..92bad6175a0 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -1335,18 +1335,17 @@ func (s *GRPCProviderServer) ReadDataSource(ctx context.Context, req *tfprotov5. return resp, nil } - // TODO: Is this the correct way to return all unknowns? - // TODO: Should we pass back config values as known? (core isn't using it right now, but will eventually be displayed) + // 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.providerDeferral.Reason), } diff --git a/helper/schema/grpc_provider_test.go b/helper/schema/grpc_provider_test.go index 62845d8ae68..5d56759e015 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -5023,6 +5023,131 @@ func TestReadDataSource(t *testing.T) { }, }, }, + "deferral-unknown-val": { + server: NewGRPCProviderServer(&Provider{ + // Deferral will skip read function and return an unknown value + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + resp.DeferralResponse = &DeferralResponse{ + Reason: DeferralReasonProviderConfigUnknown, + } + }, + 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 { + t.Fatal("Test failed, read shouldn't be called when automatic deferral is present") + + return nil + }, + }, + }, + }), + req: &tfprotov5.ReadDataSourceRequest{ + 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, + }, + }, + }, + "deferral-not-allowed-diagnostic": { + server: NewGRPCProviderServer(&Provider{ + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + resp.DeferralResponse = &DeferralResponse{ + Reason: DeferralReasonProviderConfigUnknown, + } + }, + 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 { + t.Fatal("Test failed, read shouldn't be called when automatic deferral is present") + + return nil + }, + }, + }, + }), + req: &tfprotov5.ReadDataSourceRequest{ + // Will cause a diagnostic to be returned + DeferralAllowed: false, + 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{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Provider attempted to return a deferral response for ReadDataSource, but the Terraform client doesn't support it.", + }, + }, + }, + }, } for name, testCase := range testCases { @@ -5031,6 +5156,20 @@ func TestReadDataSource(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() + // Configure provider will setup automatic deferral if present + _, err := testCase.server.ConfigureProvider(context.Background(), &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.EmptyObject, + cty.EmptyObjectVal, + ), + }, + // TODO: Add deferral allowed here + }) + if err != nil { + t.Fatal(err) + } + resp, err := testCase.server.ReadDataSource(context.Background(), testCase.req) if err != nil { From 07e816a9b8dee3192835b04a91322bb28a982c41 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 23 Apr 2024 14:47:39 -0400 Subject: [PATCH 06/26] add readresource tests --- helper/schema/grpc_provider.go | 1 - helper/schema/grpc_provider_test.go | 253 ++++++++++++++++++++++++++++ 2 files changed, 253 insertions(+), 1 deletion(-) diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index 92bad6175a0..492da064ea0 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -645,7 +645,6 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re return resp, nil } - // TODO: Is this okay to set to CurrentState? resp.NewState = req.CurrentState resp.Deferred = &tfprotov5.Deferred{ Reason: tfprotov5.DeferredReason(s.provider.providerDeferral.Reason), diff --git a/helper/schema/grpc_provider_test.go b/helper/schema/grpc_provider_test.go index 5d56759e015..6d58cc0ec36 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -3949,6 +3949,259 @@ 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"), + }), + ), + }, + }, + }, + "deferral-unknown-val": { + server: NewGRPCProviderServer(&Provider{ + // Deferral will skip read function and return current state + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + resp.DeferralResponse = &DeferralResponse{ + Reason: DeferralReasonProviderConfigUnknown, + } + }, + 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 { + t.Fatal("Test failed, read shouldn't be called when automatic deferral is present") + + return nil + }, + }, + }, + }), + req: &tfprotov5.ReadResourceRequest{ + 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, + }, + }, + }, + "deferral-not-allowed-diagnostic": { + server: NewGRPCProviderServer(&Provider{ + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + resp.DeferralResponse = &DeferralResponse{ + Reason: DeferralReasonProviderConfigUnknown, + } + }, + 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 { + t.Fatal("Test failed, read shouldn't be called when automatic deferral is present") + + return nil + }, + }, + }, + }), + req: &tfprotov5.ReadResourceRequest{ + // Will cause a diagnostic to be returned + DeferralAllowed: false, + 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{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Provider attempted to return a deferral response for ReadResource, but the Terraform client doesn't support it.", + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + // Configure provider will setup automatic deferral if present + _, err := testCase.server.ConfigureProvider(context.Background(), &tfprotov5.ConfigureProviderRequest{ + Config: &tfprotov5.DynamicValue{ + MsgPack: mustMsgpackMarshal( + cty.EmptyObject, + cty.EmptyObjectVal, + ), + }, + // TODO: Add deferral allowed here + }) + if err != nil { + t.Fatal(err) + } + + 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() From 39bf46966c8cc15cbddcfb148f0f60e08a73d34b Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 23 Apr 2024 15:50:31 -0400 Subject: [PATCH 07/26] refactor planresourcechange tests --- helper/schema/grpc_provider_test.go | 231 ++++++++++++++++++---------- 1 file changed, 153 insertions(+), 78 deletions(-) diff --git a/helper/schema/grpc_provider_test.go b/helper/schema/grpc_provider_test.go index 6d58cc0ec36..11b82d57681 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" @@ -4206,32 +4207,156 @@ 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, }, - ExpectedUnsafeLegacyTypeSystem: true, }, - "EnableLegacyTypeSystemPlanErrors": { - TestResource: &Resource{ - EnableLegacyTypeSystemPlanErrors: true, - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - Optional: 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, }, - ExpectedUnsafeLegacyTypeSystem: false, }, } @@ -4241,73 +4366,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()) + resp, err := testCase.server.PlanResourceChange(context.Background(), testCase.req) 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()) - 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) } }) } From d16367cd78ef83fe22b8ad3bff4a70b281126bc8 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 23 Apr 2024 15:56:30 -0400 Subject: [PATCH 08/26] remove configure from tests --- helper/schema/grpc_provider_test.go | 58 +++++------------------------ 1 file changed, 10 insertions(+), 48 deletions(-) diff --git a/helper/schema/grpc_provider_test.go b/helper/schema/grpc_provider_test.go index 11b82d57681..0c39982f49c 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -4030,10 +4030,8 @@ func TestReadResource(t *testing.T) { "deferral-unknown-val": { server: NewGRPCProviderServer(&Provider{ // Deferral will skip read function and return current state - ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { - resp.DeferralResponse = &DeferralResponse{ - Reason: DeferralReasonProviderConfigUnknown, - } + providerDeferral: &DeferralResponse{ + Reason: DeferralReasonProviderConfigUnknown, }, ResourcesMap: map[string]*Resource{ "test": { @@ -4100,10 +4098,8 @@ func TestReadResource(t *testing.T) { }, "deferral-not-allowed-diagnostic": { server: NewGRPCProviderServer(&Provider{ - ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { - resp.DeferralResponse = &DeferralResponse{ - Reason: DeferralReasonProviderConfigUnknown, - } + providerDeferral: &DeferralResponse{ + Reason: DeferralReasonProviderConfigUnknown, }, ResourcesMap: map[string]*Resource{ "test": { @@ -4131,7 +4127,7 @@ func TestReadResource(t *testing.T) { }, }), req: &tfprotov5.ReadResourceRequest{ - // Will cause a diagnostic to be returned + // Deferral will cause a diagnostic to be returned DeferralAllowed: false, TypeName: "test", CurrentState: &tfprotov5.DynamicValue{ @@ -4165,21 +4161,6 @@ func TestReadResource(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - - // Configure provider will setup automatic deferral if present - _, err := testCase.server.ConfigureProvider(context.Background(), &tfprotov5.ConfigureProviderRequest{ - Config: &tfprotov5.DynamicValue{ - MsgPack: mustMsgpackMarshal( - cty.EmptyObject, - cty.EmptyObjectVal, - ), - }, - // TODO: Add deferral allowed here - }) - if err != nil { - t.Fatal(err) - } - resp, err := testCase.server.ReadResource(context.Background(), testCase.req) if err != nil { @@ -5354,10 +5335,8 @@ func TestReadDataSource(t *testing.T) { "deferral-unknown-val": { server: NewGRPCProviderServer(&Provider{ // Deferral will skip read function and return an unknown value - ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { - resp.DeferralResponse = &DeferralResponse{ - Reason: DeferralReasonProviderConfigUnknown, - } + providerDeferral: &DeferralResponse{ + Reason: DeferralReasonProviderConfigUnknown, }, DataSourcesMap: map[string]*Resource{ "test": { @@ -5422,10 +5401,8 @@ func TestReadDataSource(t *testing.T) { }, "deferral-not-allowed-diagnostic": { server: NewGRPCProviderServer(&Provider{ - ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { - resp.DeferralResponse = &DeferralResponse{ - Reason: DeferralReasonProviderConfigUnknown, - } + providerDeferral: &DeferralResponse{ + Reason: DeferralReasonProviderConfigUnknown, }, DataSourcesMap: map[string]*Resource{ "test": { @@ -5449,7 +5426,7 @@ func TestReadDataSource(t *testing.T) { }, }), req: &tfprotov5.ReadDataSourceRequest{ - // Will cause a diagnostic to be returned + // Deferral will cause a diagnostic to be returned DeferralAllowed: false, TypeName: "test", Config: &tfprotov5.DynamicValue{ @@ -5483,21 +5460,6 @@ func TestReadDataSource(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - - // Configure provider will setup automatic deferral if present - _, err := testCase.server.ConfigureProvider(context.Background(), &tfprotov5.ConfigureProviderRequest{ - Config: &tfprotov5.DynamicValue{ - MsgPack: mustMsgpackMarshal( - cty.EmptyObject, - cty.EmptyObjectVal, - ), - }, - // TODO: Add deferral allowed here - }) - if err != nil { - t.Fatal(err) - } - resp, err := testCase.server.ReadDataSource(context.Background(), testCase.req) if err != nil { From 90c72f9696500b7b26c8784239e20e7aed18b6d5 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 23 Apr 2024 16:36:38 -0400 Subject: [PATCH 09/26] add new PlanResourceChange RPC tests --- helper/schema/grpc_provider.go | 4 +- helper/schema/grpc_provider_test.go | 247 ++++++++++++++++++++++++++++ 2 files changed, 249 insertions(+), 2 deletions(-) diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index 492da064ea0..13e06def18b 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -762,9 +762,9 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot return resp, nil } - // Automatic deferral is present and the resource hasn't opted-in to CustomizeDiff being called, return early. + // Automatic deferral 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.providerDeferral != nil && !res.ResourceBehavior.ProviderDeferral.EnablePlanModification { - // TODO: Is this okay to use as planned state? resp.PlannedState = req.ProposedNewState resp.PlannedPrivate = req.PriorPrivate resp.Deferred = &tfprotov5.Deferred{ diff --git a/helper/schema/grpc_provider_test.go b/helper/schema/grpc_provider_test.go index 0c39982f49c..db6a053ac9f 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -4339,6 +4339,253 @@ func TestPlanResourceChange(t *testing.T) { UnsafeToUseLegacyTypeSystem: false, }, }, + "deferral-with-provider-plan-modification": { + server: NewGRPCProviderServer(&Provider{ + providerDeferral: &DeferralResponse{ + Reason: DeferralReasonProviderConfigUnknown, + }, + ResourcesMap: map[string]*Resource{ + "test": { + ResourceBehavior: ResourceBehavior{ + ProviderDeferral: ProviderDeferralBehavior{ + // 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", + 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, + }, + }, + "deferral-skip-plan-modification": { + server: NewGRPCProviderServer(&Provider{ + providerDeferral: &DeferralResponse{ + Reason: DeferralReasonProviderConfigUnknown, + }, + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + CustomizeDiff: func(ctx context.Context, d *ResourceDiff, i interface{}) error { + t.Fatal("Test failed, CustomizeDiff shouldn't be called") + + return nil + }, + Schema: map[string]*Schema{ + "foo": { + Type: TypeString, + Optional: true, + Computed: true, + }, + }, + }, + }, + }), + req: &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + 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!"), + }), + ), + }, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + Deferred: &tfprotov5.Deferred{ + Reason: tfprotov5.DeferredReasonProviderConfigUnknown, + }, + // Returns proposed new state with deferral + 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, + }, + }, + "deferral-not-allowed-diagnostic": { + server: NewGRPCProviderServer(&Provider{ + providerDeferral: &DeferralResponse{ + Reason: DeferralReasonProviderConfigUnknown, + }, + ResourcesMap: map[string]*Resource{ + "test": { + SchemaVersion: 4, + CustomizeDiff: func(ctx context.Context, d *ResourceDiff, i interface{}) error { + t.Fatal("Test failed, CustomizeDiff shouldn't be called") + + return nil + }, + Schema: map[string]*Schema{ + "foo": { + Type: TypeString, + Optional: true, + Computed: true, + }, + }, + }, + }, + }), + req: &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test", + // Deferral will cause a diagnostic to be returned + DeferralAllowed: false, + 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{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Provider attempted to return a deferral response for PlanResourceChange, but the Terraform client doesn't support it.", + }, + }, + UnsafeToUseLegacyTypeSystem: true, + }, + }, } for name, testCase := range testCases { From 0cac5905413a87cc0bb3540f9787389cd28bae34 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Tue, 23 Apr 2024 17:33:00 -0400 Subject: [PATCH 10/26] update import logic + add tests for ImportResourceState --- helper/schema/grpc_provider.go | 40 +++- helper/schema/grpc_provider_test.go | 273 ++++++++++++++++++++++++++++ 2 files changed, 312 insertions(+), 1 deletion(-) diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index 13e06def18b..8ee7aae196f 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -1206,10 +1206,48 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro return resp, nil } - // TODO: Set resp.ImportedResources? Currently sending back an empty slice by default + // 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 + } + + // TODO: We need to make a decision on how we want to handle this scenario. If a deferral is happening for an import + // because of PROVIDER_CONFIG_UNKNOWN, the returned import object will still be in the rendered output in the plan + // (when using new import blocks) + // + // We can: + // - Send back an unknown value (preferred by core because it semantically makes sense) + // - Send back a null value (matches the general behavior of import today, i.e. fill what you can, rest are null) + // + // Regardless of what we send back, Core will send nulls to future "ApplyResourceChange" RPC calls to keep compatibility + // since "Update" methods of providers are not meant to receive Unknown values. + // + // The current approach below sends back an unknown value + // + 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.providerDeferral.Reason), } + return resp, nil } diff --git a/helper/schema/grpc_provider_test.go b/helper/schema/grpc_provider_test.go index db6a053ac9f..bb20b46db34 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -5020,6 +5020,279 @@ 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) { + t.Fatal("Test failed, import shouldn't be called") + + return nil, nil + }, + }, + }, + }, + }), + req: &tfprotov5.ImportResourceStateRequest{ + TypeName: "fake-resource", + ID: "imported-id", + }, + expected: &tfprotov5.ImportResourceStateResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "unknown resource type: fake-resource", + }, + }, + }, + }, + "deferral-resource-doesnt-exist": { + server: NewGRPCProviderServer(&Provider{ + providerDeferral: &DeferralResponse{ + Reason: DeferralReasonProviderConfigUnknown, + }, + 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) { + t.Fatal("Test failed, import shouldn't be called") + + return nil, nil + }, + }, + }, + }, + }), + req: &tfprotov5.ImportResourceStateRequest{ + TypeName: "fake-resource", + ID: "imported-id", + DeferralAllowed: true, + }, + expected: &tfprotov5.ImportResourceStateResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "unknown resource type: fake-resource", + }, + }, + }, + }, + "deferral-unknown-val": { + server: NewGRPCProviderServer(&Provider{ + // Deferral will skip import function and return an unknown value + providerDeferral: &DeferralResponse{ + Reason: DeferralReasonProviderConfigUnknown, + }, + 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) { + t.Fatal("Test failed, import shouldn't be called when automatic deferral is present") + + return nil, nil + }, + }, + }, + }, + }), + req: &tfprotov5.ImportResourceStateRequest{ + TypeName: "test", + ID: "imported-id", + 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, + }), + ), + ), + }, + }, + }, + }, + }, + "deferral-not-allowed-diagnostic": { + server: NewGRPCProviderServer(&Provider{ + providerDeferral: &DeferralResponse{ + Reason: DeferralReasonProviderConfigUnknown, + }, + 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) { + t.Fatal("Test failed, import shouldn't be called when automatic deferral is present") + + return nil, nil + }, + }, + }, + }, + }), + req: &tfprotov5.ImportResourceStateRequest{ + TypeName: "test", + ID: "imported-id", + // Deferral will cause a diagnostic to be returned + DeferralAllowed: false, + }, + expected: &tfprotov5.ImportResourceStateResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Provider attempted to return a deferral response for ImportResourceState, but the Terraform client doesn't support it.", + }, + }, + }, + }, + } + + 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) { From fe1ea3100ce3cdc30499595689bae1b24bb37561 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Wed, 24 Apr 2024 15:38:33 -0400 Subject: [PATCH 11/26] update sdkv2 to match new protocol changes + pass deferral allowed to configureprovider --- go.mod | 2 +- go.sum | 4 +- helper/schema/grpc_provider.go | 33 +++++- helper/schema/grpc_provider_test.go | 153 +++++++++++++++++++++++----- helper/schema/provider.go | 9 +- 5 files changed, 163 insertions(+), 38 deletions(-) diff --git a/go.mod b/go.mod index 66c77b59cb3..f954b28665f 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.20.0 github.com/hashicorp/terraform-json v0.21.0 - github.com/hashicorp/terraform-plugin-go v0.22.2 + github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240424182422-dba624001d4f github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/mitchellh/copystructure v1.2.0 github.com/mitchellh/go-testing-interface v1.14.1 diff --git a/go.sum b/go.sum index 168b37639c7..96aec9b41af 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,8 @@ github.com/hashicorp/terraform-exec v0.20.0 h1:DIZnPsqzPGuUnq6cH8jWcPunBfY+C+M8J github.com/hashicorp/terraform-exec v0.20.0/go.mod h1:ckKGkJWbsNqFKV1itgMnE0hY9IYf1HoiekpuN0eWoDw= github.com/hashicorp/terraform-json v0.21.0 h1:9NQxbLNqPbEMze+S6+YluEdXgJmhQykRyRNd+zTI05U= github.com/hashicorp/terraform-json v0.21.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk= -github.com/hashicorp/terraform-plugin-go v0.22.2 h1:5o8uveu6eZUf5J7xGPV0eY0TPXg3qpmwX9sce03Bxnc= -github.com/hashicorp/terraform-plugin-go v0.22.2/go.mod h1:drq8Snexp9HsbFZddvyLHN6LuWHHndSQg+gV+FPkcIM= +github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240424182422-dba624001d4f h1:r6EtFgZbSf+VeNrWqFwTFl5mKBdlFl+bpufwgaNBeQA= +github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240424182422-dba624001d4f/go.mod h1:DbW1zoh21fsPD35whjnDA5aR7bXQV+k+lnkZrn8ZrFM= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index 8ee7aae196f..aa6cfd7fcb7 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -608,12 +608,26 @@ 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 = deferralAllowed(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) + // Check if an automatic deferral response was incorrectly set on the provider. This would cause an error during later RPCs. + if s.provider.providerDeferral != nil && !deferralAllowed(req.ClientCapabilities) { + resp.Diagnostics = convert.AppendProtoDiag( + ctx, + resp.Diagnostics, + errors.New("Provider attempted to configure a deferral response for all resources and data sources during ConfigureProvider, but the Terraform client doesn't support it."), + ) + } + return resp, nil } @@ -636,7 +650,7 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re if s.provider.providerDeferral != nil { // This scenario should be prevented by ConfigureProvider, but we check here to ensure we don't // incorrectly return a deferral response when Terraform doesn't support it. - if !req.DeferralAllowed { + if !deferralAllowed(req.ClientCapabilities) { resp.Diagnostics = convert.AppendProtoDiag( ctx, resp.Diagnostics, @@ -753,7 +767,7 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot // This scenario should be prevented by ConfigureProvider, but we check here to ensure we don't // incorrectly return a deferral response when Terraform doesn't support it. - if s.provider.providerDeferral != nil && !req.DeferralAllowed { + if s.provider.providerDeferral != nil && !deferralAllowed(req.ClientCapabilities) { resp.Diagnostics = convert.AppendProtoDiag( ctx, resp.Diagnostics, @@ -1197,7 +1211,7 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro if s.provider.providerDeferral != nil { // This scenario should be prevented by ConfigureProvider, but we check here to ensure we don't // incorrectly return a deferral response when Terraform doesn't support it. - if !req.DeferralAllowed { + if !deferralAllowed(req.ClientCapabilities) { resp.Diagnostics = convert.AppendProtoDiag( ctx, resp.Diagnostics, @@ -1363,7 +1377,7 @@ func (s *GRPCProviderServer) ReadDataSource(ctx context.Context, req *tfprotov5. if s.provider.providerDeferral != nil { // This scenario should be prevented by ConfigureProvider, but we check here to ensure we don't // incorrectly return a deferral response when Terraform doesn't support it. - if !req.DeferralAllowed { + if !deferralAllowed(req.ClientCapabilities) { resp.Diagnostics = convert.AppendProtoDiag( ctx, resp.Diagnostics, @@ -1809,3 +1823,14 @@ func validateConfigNulls(ctx context.Context, v cty.Value, path cty.Path) []*tfp return diags } + +// Helper function that checks a ClientCapabilities struct to determine if a deferral response can be +// returned to the Terraform client. If no ClientCapabilities have been passed from the client, then false +// is returned. +func deferralAllowed(in *tfprotov5.ClientCapabilities) 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 bb20b46db34..1ec601bce53 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -3167,6 +3167,9 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, }), req: &tfprotov5.ConfigureProviderRequest{ + ClientCapabilities: &tfprotov5.ClientCapabilities{ + DeferralAllowed: true, + }, Config: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ @@ -3183,7 +3186,81 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { Reason: DeferralReasonProviderConfigUnknown, }, }, - // TODO: Add tests for diagnostics when a deferral is incorrectly returned + "ConfigureProvider-DeferralResponse-ClientCapabilities-Unset-Diagnostic": { + server: NewGRPCProviderServer(&Provider{ + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + resp.DeferralResponse = &DeferralResponse{ + Reason: DeferralReasonProviderConfigUnknown, + } + }, + Schema: map[string]*Schema{ + "test": { + Optional: true, + Type: TypeString, + }, + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + // No ClientCapabilities set, Deferral 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: "Provider attempted to configure a deferral response for all resources and data sources during ConfigureProvider, but the Terraform client doesn't support it.", + }, + }, + }, + }, + "ConfigureProvider-DeferralResponse-Not-Allowed-Diagnostic": { + server: NewGRPCProviderServer(&Provider{ + ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { + resp.DeferralResponse = &DeferralResponse{ + Reason: DeferralReasonProviderConfigUnknown, + } + }, + Schema: map[string]*Schema{ + "test": { + Optional: true, + Type: TypeString, + }, + }, + }), + req: &tfprotov5.ConfigureProviderRequest{ + ClientCapabilities: &tfprotov5.ClientCapabilities{ + // Deferral 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: "Provider attempted to configure a deferral response for all resources and data sources during ConfigureProvider, but the Terraform client doesn't support it.", + }, + }, + }, + }, } for name, testCase := range testCases { @@ -3206,8 +3283,10 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { t.Fatalf("unexpected difference: %s", diff) } - if diff := cmp.Diff(testCase.server.provider.providerDeferral, testCase.expectedProviderDeferral); diff != "" { - t.Fatalf("unexpected difference: %s", diff) + if len(resp.Diagnostics) == 0 { + if diff := cmp.Diff(testCase.server.provider.providerDeferral, testCase.expectedProviderDeferral); diff != "" { + t.Fatalf("unexpected difference: %s", diff) + } } }) } @@ -4059,8 +4138,10 @@ func TestReadResource(t *testing.T) { }, }), req: &tfprotov5.ReadResourceRequest{ - DeferralAllowed: true, - TypeName: "test", + ClientCapabilities: &tfprotov5.ClientCapabilities{ + DeferralAllowed: true, + }, + TypeName: "test", CurrentState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ @@ -4127,9 +4208,11 @@ func TestReadResource(t *testing.T) { }, }), req: &tfprotov5.ReadResourceRequest{ - // Deferral will cause a diagnostic to be returned - DeferralAllowed: false, - TypeName: "test", + ClientCapabilities: &tfprotov5.ClientCapabilities{ + // Deferral will cause a diagnostic to be returned + DeferralAllowed: false, + }, + TypeName: "test", CurrentState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ @@ -4367,8 +4450,10 @@ func TestPlanResourceChange(t *testing.T) { }, }), req: &tfprotov5.PlanResourceChangeRequest{ - TypeName: "test", - DeferralAllowed: true, + TypeName: "test", + ClientCapabilities: &tfprotov5.ClientCapabilities{ + DeferralAllowed: true, + }, PriorState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ @@ -4453,8 +4538,10 @@ func TestPlanResourceChange(t *testing.T) { }, }), req: &tfprotov5.PlanResourceChangeRequest{ - TypeName: "test", - DeferralAllowed: true, + TypeName: "test", + ClientCapabilities: &tfprotov5.ClientCapabilities{ + DeferralAllowed: true, + }, PriorState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ @@ -4537,8 +4624,10 @@ func TestPlanResourceChange(t *testing.T) { }), req: &tfprotov5.PlanResourceChangeRequest{ TypeName: "test", - // Deferral will cause a diagnostic to be returned - DeferralAllowed: false, + ClientCapabilities: &tfprotov5.ClientCapabilities{ + // Deferral will cause a diagnostic to be returned + DeferralAllowed: false, + }, PriorState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ @@ -5148,9 +5237,11 @@ func TestImportResourceState(t *testing.T) { }, }), req: &tfprotov5.ImportResourceStateRequest{ - TypeName: "fake-resource", - ID: "imported-id", - DeferralAllowed: true, + TypeName: "fake-resource", + ID: "imported-id", + ClientCapabilities: &tfprotov5.ClientCapabilities{ + DeferralAllowed: true, + }, }, expected: &tfprotov5.ImportResourceStateResponse{ Diagnostics: []*tfprotov5.Diagnostic{ @@ -5191,9 +5282,11 @@ func TestImportResourceState(t *testing.T) { }, }), req: &tfprotov5.ImportResourceStateRequest{ - TypeName: "test", - ID: "imported-id", - DeferralAllowed: true, + TypeName: "test", + ID: "imported-id", + ClientCapabilities: &tfprotov5.ClientCapabilities{ + DeferralAllowed: true, + }, }, expected: &tfprotov5.ImportResourceStateResponse{ Deferred: &tfprotov5.Deferred{ @@ -5251,8 +5344,10 @@ func TestImportResourceState(t *testing.T) { req: &tfprotov5.ImportResourceStateRequest{ TypeName: "test", ID: "imported-id", - // Deferral will cause a diagnostic to be returned - DeferralAllowed: false, + ClientCapabilities: &tfprotov5.ClientCapabilities{ + // Deferral will cause a diagnostic to be returned + DeferralAllowed: false, + }, }, expected: &tfprotov5.ImportResourceStateResponse{ Diagnostics: []*tfprotov5.Diagnostic{ @@ -5880,8 +5975,10 @@ func TestReadDataSource(t *testing.T) { }, }), req: &tfprotov5.ReadDataSourceRequest{ - DeferralAllowed: true, - TypeName: "test", + ClientCapabilities: &tfprotov5.ClientCapabilities{ + DeferralAllowed: true, + }, + TypeName: "test", Config: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ @@ -5946,9 +6043,11 @@ func TestReadDataSource(t *testing.T) { }, }), req: &tfprotov5.ReadDataSourceRequest{ - // Deferral will cause a diagnostic to be returned - DeferralAllowed: false, - TypeName: "test", + ClientCapabilities: &tfprotov5.ClientCapabilities{ + // Deferral will cause a diagnostic to be returned + DeferralAllowed: false, + }, + TypeName: "test", Config: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ diff --git a/helper/schema/provider.go b/helper/schema/provider.go index 36a2b542037..308bc33366a 100644 --- a/helper/schema/provider.go +++ b/helper/schema/provider.go @@ -109,6 +109,10 @@ type Provider struct { TerraformVersion string + // deferralAllowed is populated by the ConfigureProvider RPC request, however + // it should only be used during provider configuration. + deferralAllowed bool + // providerDeferral is a global deferral response that will be used for all // resources and data sources associated to this provider server. providerDeferral *DeferralResponse @@ -363,8 +367,7 @@ func (p *Provider) Configure(ctx context.Context, c *terraform.ResourceConfig) d if p.ConfigureProvider != nil { req := ConfigureProviderRequest{ - // TODO: Populate from the protocol when available - DeferralAllowed: true, + DeferralAllowed: p.deferralAllowed, ResourceData: data, } resp := ConfigureProviderResponse{} @@ -376,8 +379,6 @@ func (p *Provider) Configure(ctx context.Context, c *terraform.ResourceConfig) d return diags } - // TODO: Add logic to return a diagnostic if DeferralAllowed == false and a deferral is returned - p.meta = resp.Meta p.providerDeferral = resp.DeferralResponse } From bf74443fe6c7ea5f401a4ec7c686a5beb108b026 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 3 May 2024 16:54:31 -0400 Subject: [PATCH 12/26] update terraform-plugin-go --- go.mod | 4 +- go.sum | 8 ++-- helper/schema/grpc_provider.go | 62 ++++++++++++++++++++++++----- helper/schema/grpc_provider_test.go | 24 +++++------ 4 files changed, 71 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index f954b28665f..25cccc77cde 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.20.0 github.com/hashicorp/terraform-json v0.21.0 - github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240424182422-dba624001d4f + github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240503133000-f7070c19a38c github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/mitchellh/copystructure v1.2.0 github.com/mitchellh/go-testing-interface v1.14.1 @@ -55,5 +55,5 @@ require ( google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/grpc v1.63.2 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/protobuf v1.34.0 // indirect ) diff --git a/go.sum b/go.sum index 96aec9b41af..791ddb36dcc 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,8 @@ github.com/hashicorp/terraform-exec v0.20.0 h1:DIZnPsqzPGuUnq6cH8jWcPunBfY+C+M8J github.com/hashicorp/terraform-exec v0.20.0/go.mod h1:ckKGkJWbsNqFKV1itgMnE0hY9IYf1HoiekpuN0eWoDw= github.com/hashicorp/terraform-json v0.21.0 h1:9NQxbLNqPbEMze+S6+YluEdXgJmhQykRyRNd+zTI05U= github.com/hashicorp/terraform-json v0.21.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk= -github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240424182422-dba624001d4f h1:r6EtFgZbSf+VeNrWqFwTFl5mKBdlFl+bpufwgaNBeQA= -github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240424182422-dba624001d4f/go.mod h1:DbW1zoh21fsPD35whjnDA5aR7bXQV+k+lnkZrn8ZrFM= +github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240503133000-f7070c19a38c h1:TG3MBcQOW3+h7QjtY61HicrSbcyALuJdCF7wgjQIz7w= +github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240503133000-f7070c19a38c/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= @@ -198,8 +198,8 @@ google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= +google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index aa6cfd7fcb7..4c42c6fbdce 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -611,7 +611,7 @@ func (s *GRPCProviderServer) ConfigureProvider(ctx context.Context, req *tfproto // 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 = deferralAllowed(req.ClientCapabilities) + s.provider.deferralAllowed = configureDeferralAllowed(req.ClientCapabilities) logging.HelperSchemaTrace(ctx, "Calling downstream") diags := s.provider.Configure(ctxHack, config) @@ -620,7 +620,7 @@ func (s *GRPCProviderServer) ConfigureProvider(ctx context.Context, req *tfproto resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, diags) // Check if an automatic deferral response was incorrectly set on the provider. This would cause an error during later RPCs. - if s.provider.providerDeferral != nil && !deferralAllowed(req.ClientCapabilities) { + if s.provider.providerDeferral != nil && !configureDeferralAllowed(req.ClientCapabilities) { resp.Diagnostics = convert.AppendProtoDiag( ctx, resp.Diagnostics, @@ -650,7 +650,7 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re if s.provider.providerDeferral != nil { // This scenario should be prevented by ConfigureProvider, but we check here to ensure we don't // incorrectly return a deferral response when Terraform doesn't support it. - if !deferralAllowed(req.ClientCapabilities) { + if !readResourceDeferralAllowed(req.ClientCapabilities) { resp.Diagnostics = convert.AppendProtoDiag( ctx, resp.Diagnostics, @@ -767,7 +767,7 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot // This scenario should be prevented by ConfigureProvider, but we check here to ensure we don't // incorrectly return a deferral response when Terraform doesn't support it. - if s.provider.providerDeferral != nil && !deferralAllowed(req.ClientCapabilities) { + if s.provider.providerDeferral != nil && !planDeferralAllowed(req.ClientCapabilities) { resp.Diagnostics = convert.AppendProtoDiag( ctx, resp.Diagnostics, @@ -1211,7 +1211,7 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro if s.provider.providerDeferral != nil { // This scenario should be prevented by ConfigureProvider, but we check here to ensure we don't // incorrectly return a deferral response when Terraform doesn't support it. - if !deferralAllowed(req.ClientCapabilities) { + if !importDeferralAllowed(req.ClientCapabilities) { resp.Diagnostics = convert.AppendProtoDiag( ctx, resp.Diagnostics, @@ -1377,7 +1377,7 @@ func (s *GRPCProviderServer) ReadDataSource(ctx context.Context, req *tfprotov5. if s.provider.providerDeferral != nil { // This scenario should be prevented by ConfigureProvider, but we check here to ensure we don't // incorrectly return a deferral response when Terraform doesn't support it. - if !deferralAllowed(req.ClientCapabilities) { + if !readDataSourceDeferralAllowed(req.ClientCapabilities) { resp.Diagnostics = convert.AppendProtoDiag( ctx, resp.Diagnostics, @@ -1824,10 +1824,54 @@ func validateConfigNulls(ctx context.Context, v cty.Value, path cty.Path) []*tfp return diags } -// Helper function that checks a ClientCapabilities struct to determine if a deferral response can be -// returned to the Terraform client. If no ClientCapabilities have been passed from the client, then false +// Helper function that check a ConfigureProviderClientCapabilities struct to determine if a deferral response can be +// returned to the Terraform client. If no ConfigureProviderClientCapabilities have been passed from the client, then false // is returned. -func deferralAllowed(in *tfprotov5.ClientCapabilities) bool { +func configureDeferralAllowed(in *tfprotov5.ConfigureProviderClientCapabilities) bool { + if in == nil { + return false + } + + return in.DeferralAllowed +} + +// Helper function that check a ReadDataSourceClientCapabilities struct to determine if a deferral response can be +// returned to the Terraform client. If no ReadDataSourceClientCapabilities have been passed from the client, then false +// is returned. +func readDataSourceDeferralAllowed(in *tfprotov5.ReadDataSourceClientCapabilities) bool { + if in == nil { + return false + } + + return in.DeferralAllowed +} + +// Helper function that check a ReadResourceClientCapabilities struct to determine if a deferral response can be +// returned to the Terraform client. If no ReadResourceClientCapabilities have been passed from the client, then false +// is returned. +func readResourceDeferralAllowed(in *tfprotov5.ReadResourceClientCapabilities) bool { + if in == nil { + return false + } + + return in.DeferralAllowed +} + +// Helper function that check a ImportResourceStateClientCapabilities struct to determine if a deferral response can be +// returned to the Terraform client. If no ImportResourceStateClientCapabilities have been passed from the client, then false +// is returned. +func importDeferralAllowed(in *tfprotov5.ImportResourceStateClientCapabilities) bool { + if in == nil { + return false + } + + return in.DeferralAllowed +} + +// Helper function that check a PlanResourceChangeClientCapabilities struct to determine if a deferral response can be +// returned to the Terraform client. If no PlanResourceChangeClientCapabilities have been passed from the client, then false +// is returned. +func planDeferralAllowed(in *tfprotov5.PlanResourceChangeClientCapabilities) bool { if in == nil { return false } diff --git a/helper/schema/grpc_provider_test.go b/helper/schema/grpc_provider_test.go index 1ec601bce53..4b17f7190dc 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -3167,7 +3167,7 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, }), req: &tfprotov5.ConfigureProviderRequest{ - ClientCapabilities: &tfprotov5.ClientCapabilities{ + ClientCapabilities: &tfprotov5.ConfigureProviderClientCapabilities{ DeferralAllowed: true, }, Config: &tfprotov5.DynamicValue{ @@ -3237,7 +3237,7 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, }), req: &tfprotov5.ConfigureProviderRequest{ - ClientCapabilities: &tfprotov5.ClientCapabilities{ + ClientCapabilities: &tfprotov5.ConfigureProviderClientCapabilities{ // Deferral will cause a diagnostic to be returned DeferralAllowed: false, }, @@ -4138,7 +4138,7 @@ func TestReadResource(t *testing.T) { }, }), req: &tfprotov5.ReadResourceRequest{ - ClientCapabilities: &tfprotov5.ClientCapabilities{ + ClientCapabilities: &tfprotov5.ReadResourceClientCapabilities{ DeferralAllowed: true, }, TypeName: "test", @@ -4208,7 +4208,7 @@ func TestReadResource(t *testing.T) { }, }), req: &tfprotov5.ReadResourceRequest{ - ClientCapabilities: &tfprotov5.ClientCapabilities{ + ClientCapabilities: &tfprotov5.ReadResourceClientCapabilities{ // Deferral will cause a diagnostic to be returned DeferralAllowed: false, }, @@ -4451,7 +4451,7 @@ func TestPlanResourceChange(t *testing.T) { }), req: &tfprotov5.PlanResourceChangeRequest{ TypeName: "test", - ClientCapabilities: &tfprotov5.ClientCapabilities{ + ClientCapabilities: &tfprotov5.PlanResourceChangeClientCapabilities{ DeferralAllowed: true, }, PriorState: &tfprotov5.DynamicValue{ @@ -4539,7 +4539,7 @@ func TestPlanResourceChange(t *testing.T) { }), req: &tfprotov5.PlanResourceChangeRequest{ TypeName: "test", - ClientCapabilities: &tfprotov5.ClientCapabilities{ + ClientCapabilities: &tfprotov5.PlanResourceChangeClientCapabilities{ DeferralAllowed: true, }, PriorState: &tfprotov5.DynamicValue{ @@ -4624,7 +4624,7 @@ func TestPlanResourceChange(t *testing.T) { }), req: &tfprotov5.PlanResourceChangeRequest{ TypeName: "test", - ClientCapabilities: &tfprotov5.ClientCapabilities{ + ClientCapabilities: &tfprotov5.PlanResourceChangeClientCapabilities{ // Deferral will cause a diagnostic to be returned DeferralAllowed: false, }, @@ -5239,7 +5239,7 @@ func TestImportResourceState(t *testing.T) { req: &tfprotov5.ImportResourceStateRequest{ TypeName: "fake-resource", ID: "imported-id", - ClientCapabilities: &tfprotov5.ClientCapabilities{ + ClientCapabilities: &tfprotov5.ImportResourceStateClientCapabilities{ DeferralAllowed: true, }, }, @@ -5284,7 +5284,7 @@ func TestImportResourceState(t *testing.T) { req: &tfprotov5.ImportResourceStateRequest{ TypeName: "test", ID: "imported-id", - ClientCapabilities: &tfprotov5.ClientCapabilities{ + ClientCapabilities: &tfprotov5.ImportResourceStateClientCapabilities{ DeferralAllowed: true, }, }, @@ -5344,7 +5344,7 @@ func TestImportResourceState(t *testing.T) { req: &tfprotov5.ImportResourceStateRequest{ TypeName: "test", ID: "imported-id", - ClientCapabilities: &tfprotov5.ClientCapabilities{ + ClientCapabilities: &tfprotov5.ImportResourceStateClientCapabilities{ // Deferral will cause a diagnostic to be returned DeferralAllowed: false, }, @@ -5975,7 +5975,7 @@ func TestReadDataSource(t *testing.T) { }, }), req: &tfprotov5.ReadDataSourceRequest{ - ClientCapabilities: &tfprotov5.ClientCapabilities{ + ClientCapabilities: &tfprotov5.ReadDataSourceClientCapabilities{ DeferralAllowed: true, }, TypeName: "test", @@ -6043,7 +6043,7 @@ func TestReadDataSource(t *testing.T) { }, }), req: &tfprotov5.ReadDataSourceRequest{ - ClientCapabilities: &tfprotov5.ClientCapabilities{ + ClientCapabilities: &tfprotov5.ReadDataSourceClientCapabilities{ // Deferral will cause a diagnostic to be returned DeferralAllowed: false, }, From 4dcc2ad4e607a7267b564598b64a31b66bfa9249 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 6 May 2024 11:52:31 -0400 Subject: [PATCH 13/26] update plugin-go --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 25cccc77cde..4540d04e4be 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/hashicorp/logutils v1.0.0 github.com/hashicorp/terraform-exec v0.20.0 github.com/hashicorp/terraform-json v0.21.0 - github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240503133000-f7070c19a38c + github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240506143625-1296f392daef github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/mitchellh/copystructure v1.2.0 github.com/mitchellh/go-testing-interface v1.14.1 diff --git a/go.sum b/go.sum index 791ddb36dcc..04b97a8f9b4 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,8 @@ github.com/hashicorp/terraform-json v0.21.0 h1:9NQxbLNqPbEMze+S6+YluEdXgJmhQykRy github.com/hashicorp/terraform-json v0.21.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk= github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240503133000-f7070c19a38c h1:TG3MBcQOW3+h7QjtY61HicrSbcyALuJdCF7wgjQIz7w= github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240503133000-f7070c19a38c/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= +github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240506143625-1296f392daef h1:EaFkrtMi2qVaWkyeGENcbRSEoNiQ78QJtNB/BSzLtMs= +github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240506143625-1296f392daef/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= From e3e7e45b6c5d65e3f5fef3eae79bf1f66fef0e31 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 6 May 2024 12:18:04 -0400 Subject: [PATCH 14/26] name changes + doc updates + remove duplicate diags --- .../{deferral.go => deferred_response.go} | 26 +- helper/schema/grpc_provider.go | 130 ++----- helper/schema/grpc_provider_test.go | 327 +++--------------- helper/schema/provider.go | 20 +- helper/schema/resource.go | 14 +- 5 files changed, 98 insertions(+), 419 deletions(-) rename helper/schema/{deferral.go => deferred_response.go} (51%) diff --git a/helper/schema/deferral.go b/helper/schema/deferred_response.go similarity index 51% rename from helper/schema/deferral.go rename to helper/schema/deferred_response.go index d626449ff3e..e3911648ebe 100644 --- a/helper/schema/deferral.go +++ b/helper/schema/deferred_response.go @@ -1,31 +1,31 @@ package schema // MAINTAINER NOTE: Only PROVIDER_CONFIG_UNKNOWN (enum value 2 in the plugin-protocol) is relevant -// for SDKv2. Since (DeferralResponse).DeferralReason is mapped directly to the plugin-protocol, +// for SDKv2. Since (DeferredResponse).DeferredReason is mapped directly to the plugin-protocol, // the other enum values are intentionally omitted here. const ( - // DeferralReasonUnknown represents an undefined deferral reason. - DeferralReasonUnknown DeferralReason = 0 + // DeferredReasonUnknown is used to indicate an invalid `DeferredReason`. + // Provider developers should not use it. + DeferredReasonUnknown DeferredReason = 0 - // DeferralReasonProviderConfigUnknown represents a deferral reason caused + // DeferredReasonProviderConfigUnknown represents a deferred reason caused // by unknown provider configuration. - DeferralReasonProviderConfigUnknown DeferralReason = 2 + DeferredReasonProviderConfigUnknown DeferredReason = 2 ) -// DeferralResponse is used to indicate to Terraform that a resource or data source is not able +// DeferredResponse 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. -type DeferralResponse struct { - // Reason represents the deferral reason. - Reason DeferralReason +type DeferredResponse struct { + // Reason represents the deferred reason. + Reason DeferredReason } -// TODO: doc -type DeferralReason int32 +// DeferredReason represents different reasons for deferring a change. +type DeferredReason int32 -// TODO: doc -func (d DeferralReason) String() string { +func (d DeferredReason) String() string { switch d { case 0: return "Unknown" diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index 4c42c6fbdce..38e673266f0 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -6,7 +6,6 @@ package schema import ( "context" "encoding/json" - "errors" "fmt" "strconv" "sync" @@ -619,13 +618,14 @@ func (s *GRPCProviderServer) ConfigureProvider(ctx context.Context, req *tfproto resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, diags) - // Check if an automatic deferral response was incorrectly set on the provider. This would cause an error during later RPCs. - if s.provider.providerDeferral != nil && !configureDeferralAllowed(req.ClientCapabilities) { - resp.Diagnostics = convert.AppendProtoDiag( - ctx, - resp.Diagnostics, - errors.New("Provider attempted to configure a deferral response for all resources and data sources during ConfigureProvider, but the Terraform client doesn't support it."), - ) + // Check if a deferred response was incorrectly set on the provider. This would cause an error during later RPCs. + if s.provider.providerDeferred != nil && !configureDeferralAllowed(req.ClientCapabilities) { + 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 during ConfigureProvider " + + "but the Terraform request did not indicate support for deferred actions.", + }) } return resp, nil @@ -647,21 +647,10 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re } schemaBlock := s.getResourceSchemaBlock(req.TypeName) - if s.provider.providerDeferral != nil { - // This scenario should be prevented by ConfigureProvider, but we check here to ensure we don't - // incorrectly return a deferral response when Terraform doesn't support it. - if !readResourceDeferralAllowed(req.ClientCapabilities) { - resp.Diagnostics = convert.AppendProtoDiag( - ctx, - resp.Diagnostics, - errors.New("Provider attempted to return a deferral response for ReadResource, but the Terraform client doesn't support it."), - ) - return resp, nil - } - + if s.provider.providerDeferred != nil { resp.NewState = req.CurrentState resp.Deferred = &tfprotov5.Deferred{ - Reason: tfprotov5.DeferredReason(s.provider.providerDeferral.Reason), + Reason: tfprotov5.DeferredReason(s.provider.providerDeferred.Reason), } return resp, nil } @@ -765,24 +754,13 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot resp.UnsafeToUseLegacyTypeSystem = true } - // This scenario should be prevented by ConfigureProvider, but we check here to ensure we don't - // incorrectly return a deferral response when Terraform doesn't support it. - if s.provider.providerDeferral != nil && !planDeferralAllowed(req.ClientCapabilities) { - resp.Diagnostics = convert.AppendProtoDiag( - ctx, - resp.Diagnostics, - errors.New("Provider attempted to return a deferral response for PlanResourceChange, but the Terraform client doesn't support it."), - ) - return resp, nil - } - - // Automatic deferral is present and the resource hasn't opted-in to CustomizeDiff being called, return early + // 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.providerDeferral != nil && !res.ResourceBehavior.ProviderDeferral.EnablePlanModification { + if s.provider.providerDeferred != nil && !res.ResourceBehavior.ProviderDeferred.EnablePlanModification { resp.PlannedState = req.ProposedNewState resp.PlannedPrivate = req.PriorPrivate resp.Deferred = &tfprotov5.Deferred{ - Reason: tfprotov5.DeferredReason(s.provider.providerDeferral.Reason), + Reason: tfprotov5.DeferredReason(s.provider.providerDeferred.Reason), } return resp, nil } @@ -1007,10 +985,10 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot resp.RequiresReplace = append(resp.RequiresReplace, pathToAttributePath(p)) } - // Automatic deferral is present, add the deferral response alongside the provider-modified plan - if s.provider.providerDeferral != nil { + // Provider deferred response is present, add the deferred response alongside the provider-modified plan + if s.provider.providerDeferred != nil { resp.Deferred = &tfprotov5.Deferred{ - Reason: tfprotov5.DeferredReason(s.provider.providerDeferral.Reason), + Reason: tfprotov5.DeferredReason(s.provider.providerDeferred.Reason), } } @@ -1208,18 +1186,7 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro Type: req.TypeName, } - if s.provider.providerDeferral != nil { - // This scenario should be prevented by ConfigureProvider, but we check here to ensure we don't - // incorrectly return a deferral response when Terraform doesn't support it. - if !importDeferralAllowed(req.ClientCapabilities) { - resp.Diagnostics = convert.AppendProtoDiag( - ctx, - resp.Diagnostics, - errors.New("Provider attempted to return a deferral response for ImportResourceState, but the Terraform client doesn't support it."), - ) - return resp, nil - } - + if s.provider.providerDeferred != nil { // 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] @@ -1259,7 +1226,7 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro } resp.Deferred = &tfprotov5.Deferred{ - Reason: tfprotov5.DeferredReason(s.provider.providerDeferral.Reason), + Reason: tfprotov5.DeferredReason(s.provider.providerDeferred.Reason), } return resp, nil @@ -1374,18 +1341,7 @@ func (s *GRPCProviderServer) ReadDataSource(ctx context.Context, req *tfprotov5. schemaBlock := s.getDatasourceSchemaBlock(req.TypeName) - if s.provider.providerDeferral != nil { - // This scenario should be prevented by ConfigureProvider, but we check here to ensure we don't - // incorrectly return a deferral response when Terraform doesn't support it. - if !readDataSourceDeferralAllowed(req.ClientCapabilities) { - resp.Diagnostics = convert.AppendProtoDiag( - ctx, - resp.Diagnostics, - errors.New("Provider attempted to return a deferral response for ReadDataSource, but the Terraform client doesn't support it."), - ) - return resp, nil - } - + if s.provider.providerDeferred != nil { // Send an unknown value for the data source unknownVal := cty.UnknownVal(schemaBlock.ImpliedType()) unknownStateMp, err := msgpack.Marshal(unknownVal, schemaBlock.ImpliedType()) @@ -1398,7 +1354,7 @@ func (s *GRPCProviderServer) ReadDataSource(ctx context.Context, req *tfprotov5. MsgPack: unknownStateMp, } resp.Deferred = &tfprotov5.Deferred{ - Reason: tfprotov5.DeferredReason(s.provider.providerDeferral.Reason), + Reason: tfprotov5.DeferredReason(s.provider.providerDeferred.Reason), } return resp, nil } @@ -1824,7 +1780,7 @@ 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 deferral response can be +// 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 { @@ -1834,47 +1790,3 @@ func configureDeferralAllowed(in *tfprotov5.ConfigureProviderClientCapabilities) return in.DeferralAllowed } - -// Helper function that check a ReadDataSourceClientCapabilities struct to determine if a deferral response can be -// returned to the Terraform client. If no ReadDataSourceClientCapabilities have been passed from the client, then false -// is returned. -func readDataSourceDeferralAllowed(in *tfprotov5.ReadDataSourceClientCapabilities) bool { - if in == nil { - return false - } - - return in.DeferralAllowed -} - -// Helper function that check a ReadResourceClientCapabilities struct to determine if a deferral response can be -// returned to the Terraform client. If no ReadResourceClientCapabilities have been passed from the client, then false -// is returned. -func readResourceDeferralAllowed(in *tfprotov5.ReadResourceClientCapabilities) bool { - if in == nil { - return false - } - - return in.DeferralAllowed -} - -// Helper function that check a ImportResourceStateClientCapabilities struct to determine if a deferral response can be -// returned to the Terraform client. If no ImportResourceStateClientCapabilities have been passed from the client, then false -// is returned. -func importDeferralAllowed(in *tfprotov5.ImportResourceStateClientCapabilities) bool { - if in == nil { - return false - } - - return in.DeferralAllowed -} - -// Helper function that check a PlanResourceChangeClientCapabilities struct to determine if a deferral response can be -// returned to the Terraform client. If no PlanResourceChangeClientCapabilities have been passed from the client, then false -// is returned. -func planDeferralAllowed(in *tfprotov5.PlanResourceChangeClientCapabilities) 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 4b17f7190dc..0368d670fe2 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -36,7 +36,7 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { server *GRPCProviderServer req *tfprotov5.ConfigureProviderRequest expected *tfprotov5.ConfigureProviderResponse - expectedProviderDeferral *DeferralResponse + expectedProviderDeferred *DeferredResponse expectedMeta any }{ "no-Configure-or-Schema": { @@ -3152,11 +3152,11 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { Attr: "hello world!", }, }, - "ConfigureProvider-DeferralResponse-Allowed": { + "ConfigureProvider-DeferredResponse-Allowed": { server: NewGRPCProviderServer(&Provider{ ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { - resp.DeferralResponse = &DeferralResponse{ - Reason: DeferralReasonProviderConfigUnknown, + resp.DeferredResponse = &DeferredResponse{ + Reason: DeferredReasonProviderConfigUnknown, } }, Schema: map[string]*Schema{ @@ -3182,15 +3182,15 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, }, expected: &tfprotov5.ConfigureProviderResponse{}, - expectedProviderDeferral: &DeferralResponse{ - Reason: DeferralReasonProviderConfigUnknown, + expectedProviderDeferred: &DeferredResponse{ + Reason: DeferredReasonProviderConfigUnknown, }, }, - "ConfigureProvider-DeferralResponse-ClientCapabilities-Unset-Diagnostic": { + "ConfigureProvider-DeferredResponse-ClientCapabilities-Unset-Diagnostic": { server: NewGRPCProviderServer(&Provider{ ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { - resp.DeferralResponse = &DeferralResponse{ - Reason: DeferralReasonProviderConfigUnknown, + resp.DeferredResponse = &DeferredResponse{ + Reason: DeferredReasonProviderConfigUnknown, } }, Schema: map[string]*Schema{ @@ -3201,7 +3201,7 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, }), req: &tfprotov5.ConfigureProviderRequest{ - // No ClientCapabilities set, Deferral will cause a diagnostic to be returned + // No ClientCapabilities set, deferred response will cause a diagnostic to be returned Config: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ @@ -3217,16 +3217,18 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Provider attempted to configure a deferral response for all resources and data sources during ConfigureProvider, but the Terraform client doesn't support it.", + Summary: "Invalid Deferred Provider Response", + Detail: "Provider configured a deferred response for all resources and data sources during ConfigureProvider " + + "but the Terraform request did not indicate support for deferred actions.", }, }, }, }, - "ConfigureProvider-DeferralResponse-Not-Allowed-Diagnostic": { + "ConfigureProvider-DeferredResponse-Not-Allowed-Diagnostic": { server: NewGRPCProviderServer(&Provider{ ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { - resp.DeferralResponse = &DeferralResponse{ - Reason: DeferralReasonProviderConfigUnknown, + resp.DeferredResponse = &DeferredResponse{ + Reason: DeferredReasonProviderConfigUnknown, } }, Schema: map[string]*Schema{ @@ -3238,7 +3240,7 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }), req: &tfprotov5.ConfigureProviderRequest{ ClientCapabilities: &tfprotov5.ConfigureProviderClientCapabilities{ - // Deferral will cause a diagnostic to be returned + // Deferred response will cause a diagnostic to be returned DeferralAllowed: false, }, Config: &tfprotov5.DynamicValue{ @@ -3256,7 +3258,9 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { Diagnostics: []*tfprotov5.Diagnostic{ { Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Provider attempted to configure a deferral response for all resources and data sources during ConfigureProvider, but the Terraform client doesn't support it.", + Summary: "Invalid Deferred Provider Response", + Detail: "Provider configured a deferred response for all resources and data sources during ConfigureProvider " + + "but the Terraform request did not indicate support for deferred actions.", }, }, }, @@ -3284,7 +3288,7 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { } if len(resp.Diagnostics) == 0 { - if diff := cmp.Diff(testCase.server.provider.providerDeferral, testCase.expectedProviderDeferral); diff != "" { + if diff := cmp.Diff(testCase.server.provider.providerDeferred, testCase.expectedProviderDeferred); diff != "" { t.Fatalf("unexpected difference: %s", diff) } } @@ -4106,11 +4110,11 @@ func TestReadResource(t *testing.T) { }, }, }, - "deferral-unknown-val": { + "deferred-response-unknown-val": { server: NewGRPCProviderServer(&Provider{ - // Deferral will skip read function and return current state - providerDeferral: &DeferralResponse{ - Reason: DeferralReasonProviderConfigUnknown, + // Deferred response will skip read function and return current state + providerDeferred: &DeferredResponse{ + Reason: DeferredReasonProviderConfigUnknown, }, ResourcesMap: map[string]*Resource{ "test": { @@ -4130,7 +4134,7 @@ func TestReadResource(t *testing.T) { }, }, ReadContext: func(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics { - t.Fatal("Test failed, read shouldn't be called when automatic deferral is present") + t.Fatal("Test failed, read shouldn't be called when provider deferred response is present") return nil }, @@ -4177,66 +4181,6 @@ func TestReadResource(t *testing.T) { }, }, }, - "deferral-not-allowed-diagnostic": { - server: NewGRPCProviderServer(&Provider{ - providerDeferral: &DeferralResponse{ - Reason: DeferralReasonProviderConfigUnknown, - }, - 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 { - t.Fatal("Test failed, read shouldn't be called when automatic deferral is present") - - return nil - }, - }, - }, - }), - req: &tfprotov5.ReadResourceRequest{ - ClientCapabilities: &tfprotov5.ReadResourceClientCapabilities{ - // Deferral will cause a diagnostic to be returned - DeferralAllowed: false, - }, - 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{ - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Provider attempted to return a deferral response for ReadResource, but the Terraform client doesn't support it.", - }, - }, - }, - }, } for name, testCase := range testCases { @@ -4422,15 +4366,15 @@ func TestPlanResourceChange(t *testing.T) { UnsafeToUseLegacyTypeSystem: false, }, }, - "deferral-with-provider-plan-modification": { + "deferred-with-provider-plan-modification": { server: NewGRPCProviderServer(&Provider{ - providerDeferral: &DeferralResponse{ - Reason: DeferralReasonProviderConfigUnknown, + providerDeferred: &DeferredResponse{ + Reason: DeferredReasonProviderConfigUnknown, }, ResourcesMap: map[string]*Resource{ "test": { ResourceBehavior: ResourceBehavior{ - ProviderDeferral: ProviderDeferralBehavior{ + ProviderDeferred: ProviderDeferredBehavior{ // Will ensure that CustomizeDiff is called EnablePlanModification: true, }, @@ -4514,10 +4458,10 @@ func TestPlanResourceChange(t *testing.T) { UnsafeToUseLegacyTypeSystem: true, }, }, - "deferral-skip-plan-modification": { + "deferred-skip-plan-modification": { server: NewGRPCProviderServer(&Provider{ - providerDeferral: &DeferralResponse{ - Reason: DeferralReasonProviderConfigUnknown, + providerDeferred: &DeferredResponse{ + Reason: DeferredReasonProviderConfigUnknown, }, ResourcesMap: map[string]*Resource{ "test": { @@ -4583,7 +4527,7 @@ func TestPlanResourceChange(t *testing.T) { Deferred: &tfprotov5.Deferred{ Reason: tfprotov5.DeferredReasonProviderConfigUnknown, }, - // Returns proposed new state with deferral + // Returns proposed new state with deferred response PlannedState: &tfprotov5.DynamicValue{ MsgPack: mustMsgpackMarshal( cty.Object(map[string]cty.Type{ @@ -4599,82 +4543,6 @@ func TestPlanResourceChange(t *testing.T) { UnsafeToUseLegacyTypeSystem: true, }, }, - "deferral-not-allowed-diagnostic": { - server: NewGRPCProviderServer(&Provider{ - providerDeferral: &DeferralResponse{ - Reason: DeferralReasonProviderConfigUnknown, - }, - ResourcesMap: map[string]*Resource{ - "test": { - SchemaVersion: 4, - CustomizeDiff: func(ctx context.Context, d *ResourceDiff, i interface{}) error { - t.Fatal("Test failed, CustomizeDiff shouldn't be called") - - return nil - }, - Schema: map[string]*Schema{ - "foo": { - Type: TypeString, - Optional: true, - Computed: true, - }, - }, - }, - }, - }), - req: &tfprotov5.PlanResourceChangeRequest{ - TypeName: "test", - ClientCapabilities: &tfprotov5.PlanResourceChangeClientCapabilities{ - // Deferral will cause a diagnostic to be returned - DeferralAllowed: false, - }, - 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{ - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Provider attempted to return a deferral response for PlanResourceChange, but the Terraform client doesn't support it.", - }, - }, - UnsafeToUseLegacyTypeSystem: true, - }, - }, } for name, testCase := range testCases { @@ -5208,10 +5076,10 @@ func TestImportResourceState(t *testing.T) { }, }, }, - "deferral-resource-doesnt-exist": { + "deferred-response-resource-doesnt-exist": { server: NewGRPCProviderServer(&Provider{ - providerDeferral: &DeferralResponse{ - Reason: DeferralReasonProviderConfigUnknown, + providerDeferred: &DeferredResponse{ + Reason: DeferredReasonProviderConfigUnknown, }, ResourcesMap: map[string]*Resource{ "test": { @@ -5252,11 +5120,11 @@ func TestImportResourceState(t *testing.T) { }, }, }, - "deferral-unknown-val": { + "deferred-response-unknown-val": { server: NewGRPCProviderServer(&Provider{ - // Deferral will skip import function and return an unknown value - providerDeferral: &DeferralResponse{ - Reason: DeferralReasonProviderConfigUnknown, + // Deferred response will skip import function and return an unknown value + providerDeferred: &DeferredResponse{ + Reason: DeferredReasonProviderConfigUnknown, }, ResourcesMap: map[string]*Resource{ "test": { @@ -5273,7 +5141,7 @@ func TestImportResourceState(t *testing.T) { }, Importer: &ResourceImporter{ StateContext: func(ctx context.Context, d *ResourceData, meta interface{}) ([]*ResourceData, error) { - t.Fatal("Test failed, import shouldn't be called when automatic deferral is present") + t.Fatal("Test failed, import shouldn't be called when deferred response is present") return nil, nil }, @@ -5313,51 +5181,6 @@ func TestImportResourceState(t *testing.T) { }, }, }, - "deferral-not-allowed-diagnostic": { - server: NewGRPCProviderServer(&Provider{ - providerDeferral: &DeferralResponse{ - Reason: DeferralReasonProviderConfigUnknown, - }, - 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) { - t.Fatal("Test failed, import shouldn't be called when automatic deferral is present") - - return nil, nil - }, - }, - }, - }, - }), - req: &tfprotov5.ImportResourceStateRequest{ - TypeName: "test", - ID: "imported-id", - ClientCapabilities: &tfprotov5.ImportResourceStateClientCapabilities{ - // Deferral will cause a diagnostic to be returned - DeferralAllowed: false, - }, - }, - expected: &tfprotov5.ImportResourceStateResponse{ - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Provider attempted to return a deferral response for ImportResourceState, but the Terraform client doesn't support it.", - }, - }, - }, - }, } for name, testCase := range testCases { @@ -5947,11 +5770,11 @@ func TestReadDataSource(t *testing.T) { }, }, }, - "deferral-unknown-val": { + "deferred-response-unknown-val": { server: NewGRPCProviderServer(&Provider{ - // Deferral will skip read function and return an unknown value - providerDeferral: &DeferralResponse{ - Reason: DeferralReasonProviderConfigUnknown, + // Deferred response will skip read function and return an unknown value + providerDeferred: &DeferredResponse{ + Reason: DeferredReasonProviderConfigUnknown, }, DataSourcesMap: map[string]*Resource{ "test": { @@ -5967,7 +5790,7 @@ func TestReadDataSource(t *testing.T) { }, }, ReadContext: func(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics { - t.Fatal("Test failed, read shouldn't be called when automatic deferral is present") + t.Fatal("Test failed, read shouldn't be called when provider deferred response is present") return nil }, @@ -6016,62 +5839,6 @@ func TestReadDataSource(t *testing.T) { }, }, }, - "deferral-not-allowed-diagnostic": { - server: NewGRPCProviderServer(&Provider{ - providerDeferral: &DeferralResponse{ - Reason: DeferralReasonProviderConfigUnknown, - }, - 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 { - t.Fatal("Test failed, read shouldn't be called when automatic deferral is present") - - return nil - }, - }, - }, - }), - req: &tfprotov5.ReadDataSourceRequest{ - ClientCapabilities: &tfprotov5.ReadDataSourceClientCapabilities{ - // Deferral will cause a diagnostic to be returned - DeferralAllowed: false, - }, - 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{ - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Provider attempted to return a deferral response for ReadDataSource, but the Terraform client doesn't support it.", - }, - }, - }, - }, } for name, testCase := range testCases { diff --git a/helper/schema/provider.go b/helper/schema/provider.go index 308bc33366a..dbe15b0c63d 100644 --- a/helper/schema/provider.go +++ b/helper/schema/provider.go @@ -93,7 +93,7 @@ type Provider struct { ConfigureContextFunc ConfigureContextFunc // ConfigureProvider is a function for configuring the provider that - // supports additional features, such as returning a deferral response. + // supports additional features, such as returning a deferred response. // // Providers that require these additional features should use this function // as a replacement for ConfigureContextFunc. @@ -113,19 +113,19 @@ type Provider struct { // it should only be used during provider configuration. deferralAllowed bool - // providerDeferral is a global deferral response that will be used for all - // resources and data sources associated to this provider server. - providerDeferral *DeferralResponse + // providerDeferred is a global deferred response that will be returned automatically + // for all resources and data sources associated to this provider server. + providerDeferred *DeferredResponse } type ConfigureProviderRequest struct { // DeferralAllowed indicates whether the Terraform client initiating - // the read request allows a deferral response. + // the read request allows a deferred response. // - // If true: `(schema.ConfigureProviderResponse).DeferralResponse` can be + // If true: `(schema.ConfigureProviderResponse).DeferredResponse` can be // set. // - // If false: `(schema.ConfigureProviderResponse).DeferralResponse` + // If false: `(schema.ConfigureProviderResponse).DeferredResponse` // will return an error diagnostic if set. DeferralAllowed bool @@ -144,12 +144,12 @@ type ConfigureProviderResponse struct { // errors generated. Diagnostics diag.Diagnostics - // DeferralResponse indicates that Terraform should automatically defer + // DeferredResponse 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. - DeferralResponse *DeferralResponse + DeferredResponse *DeferredResponse } // ConfigureFunc is the function used to configure a Provider. @@ -380,7 +380,7 @@ func (p *Provider) Configure(ctx context.Context, c *terraform.ResourceConfig) d } p.meta = resp.Meta - p.providerDeferral = resp.DeferralResponse + p.providerDeferred = resp.DeferredResponse } p.configured = true diff --git a/helper/schema/resource.go b/helper/schema/resource.go index ed0da6ef7f3..32934f40e60 100644 --- a/helper/schema/resource.go +++ b/helper/schema/resource.go @@ -649,17 +649,17 @@ type Resource struct { // ResourceBehavior controls SDK-specific logic when interacting // with a resource. type ResourceBehavior struct { - // ProviderDeferral enables provider-defined logic to be executed - // in the case of automatic deferral from (Provider).ConfigureProvider. - ProviderDeferral ProviderDeferralBehavior + // ProviderDeferred enables provider-defined logic to be executed + // in the case of a deferred response from (Provider).ConfigureProvider. + ProviderDeferred ProviderDeferredBehavior } -// ProviderDeferral enables provider-defined logic to be executed -// in the case of automatic deferral from provider configure. -type ProviderDeferralBehavior struct { +// ProviderDeferredBehavior enables provider-defined logic to be executed +// in the case of a deferred response from provider configuration. +type ProviderDeferredBehavior struct { // When EnablePlanModification is true, the SDK will execute provider-defined logic // during plan (CustomizeDiff, Default, DiffSupressFunc, etc.) if ConfigureProvider - // returns a deferral response. The SDK will then automatically return a deferral response + // returns a deferred response. The SDK will then automatically return a deferred response // along with the modified plan. EnablePlanModification bool } From be070c1a12ae1dd6227646a4dd529c77a3a373ad Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 6 May 2024 12:38:18 -0400 Subject: [PATCH 15/26] add logging --- helper/schema/grpc_provider.go | 66 +++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index 38e673266f0..1b52cedf3fd 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -618,14 +618,24 @@ func (s *GRPCProviderServer) ConfigureProvider(ctx context.Context, req *tfproto resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, diags) - // Check if a deferred response was incorrectly set on the provider. This would cause an error during later RPCs. - if s.provider.providerDeferred != nil && !configureDeferralAllowed(req.ClientCapabilities) { - 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 during ConfigureProvider " + - "but the Terraform request did not indicate support for deferred actions.", - }) + 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 !configureDeferralAllowed(req.ClientCapabilities) { + 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 during ConfigureProvider " + + "but the Terraform request did not indicate support for deferred actions.", + }) + } 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{}{ + "deferred_reason": s.provider.providerDeferred.Reason.String(), + }, + ) + } } return resp, nil @@ -648,6 +658,14 @@ 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{}{ + "deferred_reason": s.provider.providerDeferred.Reason.String(), + }, + ) + resp.NewState = req.CurrentState resp.Deferred = &tfprotov5.Deferred{ Reason: tfprotov5.DeferredReason(s.provider.providerDeferred.Reason), @@ -757,6 +775,14 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot // 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{}{ + "deferred_reason": s.provider.providerDeferred.Reason.String(), + }, + ) + resp.PlannedState = req.ProposedNewState resp.PlannedPrivate = req.PriorPrivate resp.Deferred = &tfprotov5.Deferred{ @@ -987,6 +1013,14 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot // 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{}{ + "deferred_reason": s.provider.providerDeferred.Reason.String(), + }, + ) + resp.Deferred = &tfprotov5.Deferred{ Reason: tfprotov5.DeferredReason(s.provider.providerDeferred.Reason), } @@ -1187,6 +1221,14 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro } if s.provider.providerDeferred != nil { + logging.HelperSchemaDebug( + ctx, + "Provider has deferred response configured, automatically returning deferred response.", + map[string]interface{}{ + "deferred_reason": 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] @@ -1342,6 +1384,14 @@ 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{}{ + "deferred_reason": 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()) From eb082737598108280746a854c655ab7e34481f93 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 6 May 2024 12:43:35 -0400 Subject: [PATCH 16/26] error message update --- helper/schema/grpc_provider.go | 4 ++-- helper/schema/grpc_provider_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index 1b52cedf3fd..863673f507a 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -624,8 +624,8 @@ func (s *GRPCProviderServer) ConfigureProvider(ctx context.Context, req *tfproto 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 during ConfigureProvider " + - "but the Terraform request did not indicate support for deferred actions.", + 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( diff --git a/helper/schema/grpc_provider_test.go b/helper/schema/grpc_provider_test.go index 0368d670fe2..03f26a8a311 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -3218,8 +3218,8 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { { Severity: tfprotov5.DiagnosticSeverityError, Summary: "Invalid Deferred Provider Response", - Detail: "Provider configured a deferred response for all resources and data sources during ConfigureProvider " + - "but the Terraform request did not indicate support for deferred actions.", + 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.", }, }, }, @@ -3259,8 +3259,8 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { { Severity: tfprotov5.DiagnosticSeverityError, Summary: "Invalid Deferred Provider Response", - Detail: "Provider configured a deferred response for all resources and data sources during ConfigureProvider " + - "but the Terraform request did not indicate support for deferred actions.", + 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.", }, }, }, From 9450487ed60ec2ff1feb9a77b8f59a6c8ff1e94b Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 6 May 2024 12:53:37 -0400 Subject: [PATCH 17/26] pkg doc updates --- helper/schema/provider.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/helper/schema/provider.go b/helper/schema/provider.go index dbe15b0c63d..80c9e294cf9 100644 --- a/helper/schema/provider.go +++ b/helper/schema/provider.go @@ -109,8 +109,11 @@ type Provider struct { TerraformVersion string - // deferralAllowed is populated by the ConfigureProvider RPC request, however - // it should only be used during provider configuration. + // 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 @@ -119,11 +122,13 @@ type Provider struct { } type ConfigureProviderRequest struct { - // DeferralAllowed indicates whether the Terraform client initiating - // the read request allows a deferred response. + // DeferralAllowed indicates whether the Terraform request configuring + // the provider allows a deferred response. This field should be used to determine + // if `(schema.ConfigureProviderResponse).DeferredResponse` can be set. // // If true: `(schema.ConfigureProviderResponse).DeferredResponse` can be - // set. + // set to automatically defer all resources and data sources associated + // with this provider. // // If false: `(schema.ConfigureProviderResponse).DeferredResponse` // will return an error diagnostic if set. From 3ad892a47a2a3a43971987bcca44add42e7550e2 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 6 May 2024 15:21:58 -0400 Subject: [PATCH 18/26] add changelogs --- .changes/unreleased/FEATURES-20240506-152018.yaml | 6 ++++++ .changes/unreleased/FEATURES-20240506-152135.yaml | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 .changes/unreleased/FEATURES-20240506-152018.yaml create mode 100644 .changes/unreleased/FEATURES-20240506-152135.yaml diff --git a/.changes/unreleased/FEATURES-20240506-152018.yaml b/.changes/unreleased/FEATURES-20240506-152018.yaml new file mode 100644 index 00000000000..bc64b3ba6a5 --- /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 00000000000..5e53452aaa0 --- /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" From 8251ffce5c023b591fce59bd9a446585946f5716 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 6 May 2024 15:22:13 -0400 Subject: [PATCH 19/26] add copywrite header --- helper/schema/deferred_response.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/helper/schema/deferred_response.go b/helper/schema/deferred_response.go index e3911648ebe..55cd37cc014 100644 --- a/helper/schema/deferred_response.go +++ b/helper/schema/deferred_response.go @@ -1,3 +1,6 @@ +// 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 From 276a4215d88b0af9e0ea3797ed2a3f5e25770a9d Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 6 May 2024 15:28:20 -0400 Subject: [PATCH 20/26] go mod tidy :) --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index 04b97a8f9b4..3c4f1a37afd 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,6 @@ github.com/hashicorp/terraform-exec v0.20.0 h1:DIZnPsqzPGuUnq6cH8jWcPunBfY+C+M8J github.com/hashicorp/terraform-exec v0.20.0/go.mod h1:ckKGkJWbsNqFKV1itgMnE0hY9IYf1HoiekpuN0eWoDw= github.com/hashicorp/terraform-json v0.21.0 h1:9NQxbLNqPbEMze+S6+YluEdXgJmhQykRyRNd+zTI05U= github.com/hashicorp/terraform-json v0.21.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk= -github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240503133000-f7070c19a38c h1:TG3MBcQOW3+h7QjtY61HicrSbcyALuJdCF7wgjQIz7w= -github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240503133000-f7070c19a38c/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240506143625-1296f392daef h1:EaFkrtMi2qVaWkyeGENcbRSEoNiQ78QJtNB/BSzLtMs= github.com/hashicorp/terraform-plugin-go v0.22.3-0.20240506143625-1296f392daef/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= From aed628d9a0a01c4f6943aa6a344c16a1c918ae31 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 9 May 2024 13:21:24 -0400 Subject: [PATCH 21/26] replace TODO comment on import --- helper/schema/grpc_provider.go | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index 863673f507a..9c8968f4b7a 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -1237,19 +1237,7 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro return resp, nil } - // TODO: We need to make a decision on how we want to handle this scenario. If a deferral is happening for an import - // because of PROVIDER_CONFIG_UNKNOWN, the returned import object will still be in the rendered output in the plan - // (when using new import blocks) - // - // We can: - // - Send back an unknown value (preferred by core because it semantically makes sense) - // - Send back a null value (matches the general behavior of import today, i.e. fill what you can, rest are null) - // - // Regardless of what we send back, Core will send nulls to future "ApplyResourceChange" RPC calls to keep compatibility - // since "Update" methods of providers are not meant to receive Unknown values. - // - // The current approach below sends back an unknown value - // + // 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()) From 05985636e2f256b17c2f7149a624c4e084ea57b8 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 9 May 2024 13:24:36 -0400 Subject: [PATCH 22/26] replace if check for deferralAllowed --- helper/schema/grpc_provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index 9c8968f4b7a..a8b6c908b93 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -620,7 +620,7 @@ func (s *GRPCProviderServer) ConfigureProvider(ctx context.Context, req *tfproto 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 !configureDeferralAllowed(req.ClientCapabilities) { + if !s.provider.deferralAllowed { resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{ Severity: tfprotov5.DiagnosticSeverityError, Summary: "Invalid Deferred Provider Response", From bd21c5e19779c42be5748e84b62dff2bd77cee01 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 9 May 2024 13:29:18 -0400 Subject: [PATCH 23/26] replace logging key with a constant --- helper/schema/grpc_provider.go | 12 ++++++------ internal/logging/keys.go | 3 +++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/helper/schema/grpc_provider.go b/helper/schema/grpc_provider.go index a8b6c908b93..ec5d74301a7 100644 --- a/helper/schema/grpc_provider.go +++ b/helper/schema/grpc_provider.go @@ -632,7 +632,7 @@ func (s *GRPCProviderServer) ConfigureProvider(ctx context.Context, req *tfproto ctx, "Provider has configured a deferred response, all associated resources and data sources will automatically return a deferred response.", map[string]interface{}{ - "deferred_reason": s.provider.providerDeferred.Reason.String(), + logging.KeyDeferredReason: s.provider.providerDeferred.Reason.String(), }, ) } @@ -662,7 +662,7 @@ func (s *GRPCProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Re ctx, "Provider has deferred response configured, automatically returning deferred response.", map[string]interface{}{ - "deferred_reason": s.provider.providerDeferred.Reason.String(), + logging.KeyDeferredReason: s.provider.providerDeferred.Reason.String(), }, ) @@ -779,7 +779,7 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot ctx, "Provider has deferred response configured, automatically returning deferred response.", map[string]interface{}{ - "deferred_reason": s.provider.providerDeferred.Reason.String(), + logging.KeyDeferredReason: s.provider.providerDeferred.Reason.String(), }, ) @@ -1017,7 +1017,7 @@ func (s *GRPCProviderServer) PlanResourceChange(ctx context.Context, req *tfprot ctx, "Provider has deferred response configured, returning deferred response with modified plan.", map[string]interface{}{ - "deferred_reason": s.provider.providerDeferred.Reason.String(), + logging.KeyDeferredReason: s.provider.providerDeferred.Reason.String(), }, ) @@ -1225,7 +1225,7 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro ctx, "Provider has deferred response configured, automatically returning deferred response.", map[string]interface{}{ - "deferred_reason": s.provider.providerDeferred.Reason.String(), + logging.KeyDeferredReason: s.provider.providerDeferred.Reason.String(), }, ) @@ -1376,7 +1376,7 @@ func (s *GRPCProviderServer) ReadDataSource(ctx context.Context, req *tfprotov5. ctx, "Provider has deferred response configured, automatically returning deferred response.", map[string]interface{}{ - "deferred_reason": s.provider.providerDeferred.Reason.String(), + logging.KeyDeferredReason: s.provider.providerDeferred.Reason.String(), }, ) diff --git a/internal/logging/keys.go b/internal/logging/keys.go index 983fde437a2..ed238ea5902 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" From c31bc0060e839dcc7dd7432c2a580be51a28f99c Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 9 May 2024 13:38:03 -0400 Subject: [PATCH 24/26] use the proper error returning method for test assertions --- helper/schema/grpc_provider_test.go | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/helper/schema/grpc_provider_test.go b/helper/schema/grpc_provider_test.go index 03f26a8a311..b8084233d58 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -4134,9 +4134,7 @@ func TestReadResource(t *testing.T) { }, }, ReadContext: func(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics { - t.Fatal("Test failed, read shouldn't be called when provider deferred response is present") - - return nil + return diag.Errorf("Test assertion failed: read shouldn't be called when provider deferred response is present") }, }, }, @@ -4467,9 +4465,7 @@ func TestPlanResourceChange(t *testing.T) { "test": { SchemaVersion: 4, CustomizeDiff: func(ctx context.Context, d *ResourceDiff, i interface{}) error { - t.Fatal("Test failed, CustomizeDiff shouldn't be called") - - return nil + return errors.New("Test assertion failed: CustomizeDiff shouldn't be called") }, Schema: map[string]*Schema{ "foo": { @@ -5055,9 +5051,7 @@ func TestImportResourceState(t *testing.T) { }, Importer: &ResourceImporter{ StateContext: func(ctx context.Context, d *ResourceData, meta interface{}) ([]*ResourceData, error) { - t.Fatal("Test failed, import shouldn't be called") - - return nil, nil + return nil, errors.New("Test assertion failed: import shouldn't be called") }, }, }, @@ -5096,9 +5090,7 @@ func TestImportResourceState(t *testing.T) { }, Importer: &ResourceImporter{ StateContext: func(ctx context.Context, d *ResourceData, meta interface{}) ([]*ResourceData, error) { - t.Fatal("Test failed, import shouldn't be called") - - return nil, nil + return nil, errors.New("Test assertion failed: import shouldn't be called") }, }, }, @@ -5141,9 +5133,7 @@ func TestImportResourceState(t *testing.T) { }, Importer: &ResourceImporter{ StateContext: func(ctx context.Context, d *ResourceData, meta interface{}) ([]*ResourceData, error) { - t.Fatal("Test failed, import shouldn't be called when deferred response is present") - - return nil, nil + return nil, errors.New("Test assertion failed: import shouldn't be called when deferred response is present") }, }, }, @@ -5790,9 +5780,7 @@ func TestReadDataSource(t *testing.T) { }, }, ReadContext: func(ctx context.Context, d *ResourceData, meta interface{}) diag.Diagnostics { - t.Fatal("Test failed, read shouldn't be called when provider deferred response is present") - - return nil + return diag.Errorf("Test assertion failed: read shouldn't be called when provider deferred response is present") }, }, }, From 207c950e468b7ff8eeae648afaa35f88cee7c5b9 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 9 May 2024 13:52:10 -0400 Subject: [PATCH 25/26] add experimental verbiage --- .changes/unreleased/NOTES-20240509-134945.yaml | 7 +++++++ helper/schema/deferred_response.go | 6 ++++++ helper/schema/provider.go | 6 ++++++ helper/schema/resource.go | 6 ++++++ 4 files changed, 25 insertions(+) create mode 100644 .changes/unreleased/NOTES-20240509-134945.yaml diff --git a/.changes/unreleased/NOTES-20240509-134945.yaml b/.changes/unreleased/NOTES-20240509-134945.yaml new file mode 100644 index 00000000000..39f9ae90292 --- /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_response.go b/helper/schema/deferred_response.go index 55cd37cc014..aeafc4cfd39 100644 --- a/helper/schema/deferred_response.go +++ b/helper/schema/deferred_response.go @@ -20,12 +20,18 @@ const ( // 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 DeferredResponse 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 { diff --git a/helper/schema/provider.go b/helper/schema/provider.go index 80c9e294cf9..92adff2dbd4 100644 --- a/helper/schema/provider.go +++ b/helper/schema/provider.go @@ -132,6 +132,9 @@ type ConfigureProviderRequest struct { // // If false: `(schema.ConfigureProviderResponse).DeferredResponse` // 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. @@ -154,6 +157,9 @@ type ConfigureProviderResponse struct { // // 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. DeferredResponse *DeferredResponse } diff --git a/helper/schema/resource.go b/helper/schema/resource.go index 32934f40e60..1c944c9b481 100644 --- a/helper/schema/resource.go +++ b/helper/schema/resource.go @@ -651,11 +651,17 @@ type Resource struct { 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 From 470473db8076f33acdc9372c7e60e43dde738caf Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 10 May 2024 10:25:55 -0400 Subject: [PATCH 26/26] DeferredResponse -> Deferred --- .../{deferred_response.go => deferred.go} | 6 ++-- helper/schema/grpc_provider_test.go | 28 +++++++++---------- helper/schema/provider.go | 14 +++++----- 3 files changed, 24 insertions(+), 24 deletions(-) rename helper/schema/{deferred_response.go => deferred.go} (87%) diff --git a/helper/schema/deferred_response.go b/helper/schema/deferred.go similarity index 87% rename from helper/schema/deferred_response.go rename to helper/schema/deferred.go index aeafc4cfd39..a02efef104e 100644 --- a/helper/schema/deferred_response.go +++ b/helper/schema/deferred.go @@ -4,7 +4,7 @@ package schema // MAINTAINER NOTE: Only PROVIDER_CONFIG_UNKNOWN (enum value 2 in the plugin-protocol) is relevant -// for SDKv2. Since (DeferredResponse).DeferredReason is mapped directly to the plugin-protocol, +// 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`. @@ -16,14 +16,14 @@ const ( DeferredReasonProviderConfigUnknown DeferredReason = 2 ) -// DeferredResponse is used to indicate to Terraform that a resource or data source is not able +// 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 DeferredResponse struct { +type Deferred struct { // Reason represents the deferred reason. Reason DeferredReason } diff --git a/helper/schema/grpc_provider_test.go b/helper/schema/grpc_provider_test.go index b8084233d58..7dacdd6ea5c 100644 --- a/helper/schema/grpc_provider_test.go +++ b/helper/schema/grpc_provider_test.go @@ -36,7 +36,7 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { server *GRPCProviderServer req *tfprotov5.ConfigureProviderRequest expected *tfprotov5.ConfigureProviderResponse - expectedProviderDeferred *DeferredResponse + expectedProviderDeferred *Deferred expectedMeta any }{ "no-Configure-or-Schema": { @@ -3152,10 +3152,10 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { Attr: "hello world!", }, }, - "ConfigureProvider-DeferredResponse-Allowed": { + "ConfigureProvider-Deferred-Allowed": { server: NewGRPCProviderServer(&Provider{ ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { - resp.DeferredResponse = &DeferredResponse{ + resp.Deferred = &Deferred{ Reason: DeferredReasonProviderConfigUnknown, } }, @@ -3182,14 +3182,14 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, }, expected: &tfprotov5.ConfigureProviderResponse{}, - expectedProviderDeferred: &DeferredResponse{ + expectedProviderDeferred: &Deferred{ Reason: DeferredReasonProviderConfigUnknown, }, }, - "ConfigureProvider-DeferredResponse-ClientCapabilities-Unset-Diagnostic": { + "ConfigureProvider-Deferred-ClientCapabilities-Unset-Diagnostic": { server: NewGRPCProviderServer(&Provider{ ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { - resp.DeferredResponse = &DeferredResponse{ + resp.Deferred = &Deferred{ Reason: DeferredReasonProviderConfigUnknown, } }, @@ -3224,10 +3224,10 @@ func TestGRPCProviderServerConfigureProvider(t *testing.T) { }, }, }, - "ConfigureProvider-DeferredResponse-Not-Allowed-Diagnostic": { + "ConfigureProvider-Deferred-Not-Allowed-Diagnostic": { server: NewGRPCProviderServer(&Provider{ ConfigureProvider: func(ctx context.Context, req ConfigureProviderRequest, resp *ConfigureProviderResponse) { - resp.DeferredResponse = &DeferredResponse{ + resp.Deferred = &Deferred{ Reason: DeferredReasonProviderConfigUnknown, } }, @@ -4113,7 +4113,7 @@ func TestReadResource(t *testing.T) { "deferred-response-unknown-val": { server: NewGRPCProviderServer(&Provider{ // Deferred response will skip read function and return current state - providerDeferred: &DeferredResponse{ + providerDeferred: &Deferred{ Reason: DeferredReasonProviderConfigUnknown, }, ResourcesMap: map[string]*Resource{ @@ -4366,7 +4366,7 @@ func TestPlanResourceChange(t *testing.T) { }, "deferred-with-provider-plan-modification": { server: NewGRPCProviderServer(&Provider{ - providerDeferred: &DeferredResponse{ + providerDeferred: &Deferred{ Reason: DeferredReasonProviderConfigUnknown, }, ResourcesMap: map[string]*Resource{ @@ -4458,7 +4458,7 @@ func TestPlanResourceChange(t *testing.T) { }, "deferred-skip-plan-modification": { server: NewGRPCProviderServer(&Provider{ - providerDeferred: &DeferredResponse{ + providerDeferred: &Deferred{ Reason: DeferredReasonProviderConfigUnknown, }, ResourcesMap: map[string]*Resource{ @@ -5072,7 +5072,7 @@ func TestImportResourceState(t *testing.T) { }, "deferred-response-resource-doesnt-exist": { server: NewGRPCProviderServer(&Provider{ - providerDeferred: &DeferredResponse{ + providerDeferred: &Deferred{ Reason: DeferredReasonProviderConfigUnknown, }, ResourcesMap: map[string]*Resource{ @@ -5115,7 +5115,7 @@ func TestImportResourceState(t *testing.T) { "deferred-response-unknown-val": { server: NewGRPCProviderServer(&Provider{ // Deferred response will skip import function and return an unknown value - providerDeferred: &DeferredResponse{ + providerDeferred: &Deferred{ Reason: DeferredReasonProviderConfigUnknown, }, ResourcesMap: map[string]*Resource{ @@ -5763,7 +5763,7 @@ func TestReadDataSource(t *testing.T) { "deferred-response-unknown-val": { server: NewGRPCProviderServer(&Provider{ // Deferred response will skip read function and return an unknown value - providerDeferred: &DeferredResponse{ + providerDeferred: &Deferred{ Reason: DeferredReasonProviderConfigUnknown, }, DataSourcesMap: map[string]*Resource{ diff --git a/helper/schema/provider.go b/helper/schema/provider.go index 92adff2dbd4..a75ae2fc28b 100644 --- a/helper/schema/provider.go +++ b/helper/schema/provider.go @@ -118,19 +118,19 @@ type Provider struct { // providerDeferred is a global deferred response that will be returned automatically // for all resources and data sources associated to this provider server. - providerDeferred *DeferredResponse + 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).DeferredResponse` can be set. + // if `(schema.ConfigureProviderResponse).Deferred` can be set. // - // If true: `(schema.ConfigureProviderResponse).DeferredResponse` can be + // If true: `(schema.ConfigureProviderResponse).Deferred` can be // set to automatically defer all resources and data sources associated // with this provider. // - // If false: `(schema.ConfigureProviderResponse).DeferredResponse` + // 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 @@ -152,7 +152,7 @@ type ConfigureProviderResponse struct { // errors generated. Diagnostics diag.Diagnostics - // DeferredResponse indicates that Terraform should automatically defer + // Deferred indicates that Terraform should automatically defer // all resources and data sources for this provider. // // This field can only be set if @@ -160,7 +160,7 @@ type ConfigureProviderResponse struct { // // 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. - DeferredResponse *DeferredResponse + Deferred *Deferred } // ConfigureFunc is the function used to configure a Provider. @@ -391,7 +391,7 @@ func (p *Provider) Configure(ctx context.Context, c *terraform.ResourceConfig) d } p.meta = resp.Meta - p.providerDeferred = resp.DeferredResponse + p.providerDeferred = resp.Deferred } p.configured = true