From e6f4ffa8a37eec612dd7f5eb3f92c52942a68b1e Mon Sep 17 00:00:00 2001 From: Mark DeCrane Date: Thu, 4 Apr 2024 11:03:39 -0400 Subject: [PATCH] stacks: adding deferred action to datasource reads --- internal/plugin/grpc_provider.go | 2 + internal/plugin6/grpc_provider.go | 2 + internal/providers/provider.go | 8 ++ .../terraform/context_apply_deferred_test.go | 80 ++++++++++++++++++- .../node_resource_abstract_instance.go | 19 ++++- 5 files changed, 106 insertions(+), 5 deletions(-) diff --git a/internal/plugin/grpc_provider.go b/internal/plugin/grpc_provider.go index b4801db616..b70dd7dcd6 100644 --- a/internal/plugin/grpc_provider.go +++ b/internal/plugin/grpc_provider.go @@ -721,6 +721,7 @@ func (p *GRPCProvider) ReadDataSource(r providers.ReadDataSourceRequest) (resp p Config: &proto.DynamicValue{ Msgpack: config, }, + DeferralAllowed: r.DeferralAllowed, } if metaSchema.Block != nil { @@ -745,6 +746,7 @@ func (p *GRPCProvider) ReadDataSource(r providers.ReadDataSourceRequest) (resp p return resp } resp.State = state + resp.Deferred = convert.ProtoToDeferred(protoResp.Deferred) return resp } diff --git a/internal/plugin6/grpc_provider.go b/internal/plugin6/grpc_provider.go index 47f2ae29a6..43ea86b883 100644 --- a/internal/plugin6/grpc_provider.go +++ b/internal/plugin6/grpc_provider.go @@ -710,6 +710,7 @@ func (p *GRPCProvider) ReadDataSource(r providers.ReadDataSourceRequest) (resp p Config: &proto6.DynamicValue{ Msgpack: config, }, + DeferralAllowed: r.DeferralAllowed, } if metaSchema.Block != nil { @@ -734,6 +735,7 @@ func (p *GRPCProvider) ReadDataSource(r providers.ReadDataSourceRequest) (resp p return resp } resp.State = state + resp.Deferred = convert.ProtoToDeferred(protoResp.Deferred) return resp } diff --git a/internal/providers/provider.go b/internal/providers/provider.go index f37a9049ed..b57da8de89 100644 --- a/internal/providers/provider.go +++ b/internal/providers/provider.go @@ -508,6 +508,10 @@ type ReadDataSourceRequest struct { // each provider, and it should not be used without coordination with // HashiCorp. It is considered experimental and subject to change. ProviderMeta cty.Value + + // DeferralAllowed signals that the provider is allowed to defer the + // changes. If set the caller needs to handle the deferred response. + DeferralAllowed bool } type ReadDataSourceResponse struct { @@ -516,6 +520,10 @@ type ReadDataSourceResponse struct { // Diagnostics contains any warnings or errors from the method call. Diagnostics tfdiags.Diagnostics + + // Deferred if present signals that the provider was not able to fully + // complete this operation and a susequent run is required. + Deferred *Deferred } type CallFunctionRequest struct { diff --git a/internal/terraform/context_apply_deferred_test.go b/internal/terraform/context_apply_deferred_test.go index c2c3464092..6d44d078a9 100644 --- a/internal/terraform/context_apply_deferred_test.go +++ b/internal/terraform/context_apply_deferred_test.go @@ -933,7 +933,7 @@ resource "test" "c" { targetResourceThatDependsOnDeferredResourceTest = deferredActionsTest{ configs: map[string]string{ "main.tf": ` -variable "resource_count" { +variable "resource_count" { type = number } @@ -1581,11 +1581,61 @@ output "a" { }, }, } + + readDataSourceTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +data "test" "a" { + name = "deferred_read" +} + +resource "test" "b" { + name = data.test.a.name +} + +output "a" { + value = data.test.a +} + +output "b" { + value = test.b +} + `, + }, + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{}, + wantPlanned: map[string]cty.Value{ + "deferred_read": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("deferred_read"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + + wantActions: map[string]plans.Action{}, + wantApplied: map[string]cty.Value{ + // The all resources will be deferred, so shouldn't + // have any action at this stage. + }, + wantOutputs: map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("deferred_read"), + }), + "b": cty.NullVal(cty.DynamicPseudoType), + }, + wantDeferred: map[string]providers.DeferredReason{ + "data.test.a": providers.DeferredReasonProviderConfigUnknown, + "test.b": providers.DeferredReasonDeferredPrereq, + }, + complete: false, + }, + }, + } ) func TestContextApply_deferredActions(t *testing.T) { tests := map[string]deferredActionsTest{ - "resource_for_each": resourceForEachTest, "resource_in_module_for_each": resourceInModuleForEachTest, "resource_count": resourceCountTest, @@ -1599,6 +1649,7 @@ func TestContextApply_deferredActions(t *testing.T) { "custom_conditions": customConditionsTest, "custom_conditions_with_orphans": customConditionsWithOrphansTest, "resource_read": resourceReadTest, + "data_read": readDataSourceTest, } for name, test := range tests { t.Run(name, func(t *testing.T) { @@ -1784,6 +1835,18 @@ func (provider *deferredActionsProvider) Provider() providers.Interface { }, }, }, + DataSources: map[string]providers.Schema{ + "test": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, }, ReadResourceFn: func(req providers.ReadResourceRequest) providers.ReadResourceResponse { if key := req.PriorState.GetAttr("name"); key.IsKnown() && key.AsString() == "deferred_read" { @@ -1799,6 +1862,19 @@ func (provider *deferredActionsProvider) Provider() providers.Interface { NewState: req.PriorState, } }, + ReadDataSourceFn: func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + if key := req.Config.GetAttr("name"); key.IsKnown() && key.AsString() == "deferred_read" { + return providers.ReadDataSourceResponse{ + State: req.Config, + Deferred: &providers.Deferred{ + Reason: providers.DeferredReasonProviderConfigUnknown, + }, + } + } + return providers.ReadDataSourceResponse{ + State: req.Config, + } + }, PlanResourceChangeFn: func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { if req.ProposedNewState.IsNull() { // Then we're deleting a concrete instance. diff --git a/internal/terraform/node_resource_abstract_instance.go b/internal/terraform/node_resource_abstract_instance.go index 43ca0281d1..b17240f7a6 100644 --- a/internal/terraform/node_resource_abstract_instance.go +++ b/internal/terraform/node_resource_abstract_instance.go @@ -1594,10 +1594,23 @@ func (n *NodeAbstractResourceInstance) readDataSource(ctx EvalContext, configVal } } else { resp = provider.ReadDataSource(providers.ReadDataSourceRequest{ - TypeName: n.Addr.ContainingResource().Resource.Type, - Config: configVal, - ProviderMeta: metaConfigVal, + TypeName: n.Addr.ContainingResource().Resource.Type, + Config: configVal, + ProviderMeta: metaConfigVal, + DeferralAllowed: ctx.Deferrals().DeferralAllowed(), }) + + if resp.Deferred != nil { + deffered := ctx.Deferrals() + deffered.ReportResourceInstanceDeferred(n.Addr, resp.Deferred.Reason, &plans.ResourceInstanceChange{ + Addr: n.Addr, + Change: plans.Change{ + Action: plans.Read, + Before: cty.DynamicVal, + After: resp.State, + }, + }) + } } diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config, n.Addr.String())) if diags.HasErrors() {