Skip to content

Commit

Permalink
stacks: handle deferred actions in refresh
Browse files Browse the repository at this point in the history
Co-authored-by: Matej Risek <[email protected]>
  • Loading branch information
DanielMSchmidt and matejrisek committed Mar 26, 2024
1 parent 54e5a6e commit c20babf
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 6 deletions.
81 changes: 79 additions & 2 deletions internal/terraform/context_apply_deferred_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ type deferredActionsTestStage struct {

// Whether the plan should be completed during this stage.
complete bool

// buildOpts is an optional field, that lets the test specify additional
// options to be used when building the plan.
buildOpts func(opts *PlanOpts)
}

var (
Expand Down Expand Up @@ -340,11 +344,59 @@ output "c" {
},
},
}

// resourceReadTest is a test that covers the behavior of reading resources
// in a refresh when the refresh is responding with a deferral.
resourceReadTest = deferredActionsTest{
configs: map[string]string{
"main.tf": `
// TEMP: unknown for_each currently requires an experiment opt-in.
// We should remove this block if the experiment gets stabilized.
terraform {
experiments = [unknown_instances]
}
resource "test" "a" {
name = "deferred_read"
}
output "a" {
value = test.a
}
`,
},
stages: []deferredActionsTestStage{
{
buildOpts: func(opts *PlanOpts) {
opts.Mode = plans.RefreshOnlyMode
},
inputs: map[string]cty.Value{},
wantPlanned: map[string]cty.Value{
// The all resources will be deferred, so shouldn't
// have any action at this stage.
},

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("a"),
"upstream_names": cty.NullVal(cty.Set(cty.String)),
}),
},
complete: true,
},
},
}
)

func TestContextApply_deferredActions(t *testing.T) {
tests := map[string]deferredActionsTest{
"resource_for_each": resourceForEachTest,
"resource_read": resourceReadTest,
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
Expand Down Expand Up @@ -378,7 +430,7 @@ func TestContextApply_deferredActions(t *testing.T) {
},
})

plan, diags := ctx.Plan(cfg, state, &PlanOpts{
opts := &PlanOpts{
Mode: plans.NormalMode,
SetVariables: func() InputValues {
values := InputValues{}
Expand All @@ -390,7 +442,13 @@ func TestContextApply_deferredActions(t *testing.T) {
}
return values
}(),
})
}

if stage.buildOpts != nil {
stage.buildOpts(opts)
}

plan, diags := ctx.Plan(cfg, state, opts)
if plan.Complete != stage.complete {
t.Errorf("wrong completion status in plan: got %v, want %v", plan.Complete, stage.complete)
}
Expand All @@ -417,6 +475,11 @@ func TestContextApply_deferredActions(t *testing.T) {
return
}

if opts.Mode == plans.RefreshOnlyMode {
// Don't execute the apply stage if wantApplied is nil.
return
}

updatedState, diags := ctx.Apply(plan, cfg, nil)

// We expect the correct applied changes and no diagnostics.
Expand Down Expand Up @@ -498,6 +561,20 @@ func (provider *deferredActionsProvider) Provider() providers.Interface {
},
},
},
ReadResourceFn: func(req providers.ReadResourceRequest) providers.ReadResourceResponse {
if key := req.PriorState.GetAttr("name"); key.IsKnown() && key.AsString() == "deferred_read" {
return providers.ReadResourceResponse{
NewState: req.PriorState,
Deferred: &providers.Deferred{
Reason: providers.DEFERRED_REASON_PROVIDER_CONFIG_UNKNOWN,
},
}
}

return providers.ReadResourceResponse{
NewState: req.PriorState,
}
},
PlanResourceChangeFn: func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
key := "<unknown>"
if v := req.Config.GetAttr("name"); v.IsKnown() {
Expand Down
22 changes: 18 additions & 4 deletions internal/terraform/node_resource_abstract_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/hashicorp/terraform/internal/checks"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/experiments"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/moduletest/mocking"
"github.com/hashicorp/terraform/internal/plans"
Expand Down Expand Up @@ -640,12 +641,25 @@ func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey state
NewState: priorVal,
}
} else {
// TODO: This should also allow the option created in TF-13948
deferralAllowed := ctx.LanguageExperimentActive(experiments.UnknownInstances)
resp = provider.ReadResource(providers.ReadResourceRequest{
TypeName: n.Addr.Resource.Resource.Type,
PriorState: priorVal,
Private: state.Private,
ProviderMeta: metaConfigVal,
TypeName: n.Addr.Resource.Resource.Type,
PriorState: priorVal,
Private: state.Private,
ProviderMeta: metaConfigVal,
DeferralAllowed: deferralAllowed,
})

if resp.Deferred != nil {
deferrals := ctx.Deferrals()
expectedValue := cty.UnknownVal(cty.DynamicPseudoType)
deferrals.ReportResourceInstanceDeferred(absAddr, plans.Read, expectedValue)

return &states.ResourceInstanceObject{
Value: expectedValue,
}, diags
}
}
if n.Config != nil {
resp.Diagnostics = resp.Diagnostics.InConfigBody(n.Config.Config, n.Addr.String())
Expand Down
63 changes: 63 additions & 0 deletions internal/terraform/node_resource_abstract_instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/plans/deferring"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/states"
"github.com/zclconf/go-cty/cty"
)
Expand Down Expand Up @@ -184,3 +186,64 @@ aws_instance.foo:
provider = provider["registry.terraform.io/hashicorp/aws"]
`)
}

func TestNodeAbstractResourceInstance_refresh_with_deferred_read(t *testing.T) {
state := states.NewState()
evalCtx := &MockEvalContext{}
evalCtx.StateState = state.SyncWrapper()
evalCtx.Scope = evalContextModuleInstance{Addr: addrs.RootModuleInstance}

mockProvider := mockProviderWithResourceTypeSchema("aws_instance", &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Optional: true,
},
},
})
mockProvider.ConfigureProviderCalled = true

mockProvider.ReadResourceFn = func(providers.ReadResourceRequest) providers.ReadResourceResponse {
return providers.ReadResourceResponse{
Deferred: &providers.Deferred{
Reason: providers.DEFERRED_REASON_ABSENT_PREREQ,
},
}
}

obj := &states.ResourceInstanceObject{
Value: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-abc123"),
}),
Status: states.ObjectReady,
}

node := &NodeAbstractResourceInstance{
Addr: mustResourceInstanceAddr("aws_instance.foo"),
NodeAbstractResource: NodeAbstractResource{
ResolvedProvider: mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
},
}
evalCtx.ProviderProvider = mockProvider
evalCtx.ProviderSchemaSchema = mockProvider.GetProviderSchema()
resourceGraph := addrs.NewDirectedGraph[addrs.ConfigResource]()
evalCtx.DeferralsState = deferring.NewDeferred(resourceGraph)

rio, diags := node.refresh(evalCtx, states.NotDeposed, obj)
if diags.HasErrors() {
t.Fatal(diags.Err())
}

value := rio.Value
if value.IsKnown() {
t.Fatalf("value was known: %v", value)
}

if !evalCtx.DeferralsCalled {
t.Fatalf("expected deferral to be called")
}

if !evalCtx.DeferralsState.HaveAnyDeferrals() {
t.Fatalf("expected deferral to be present")
}
}

0 comments on commit c20babf

Please sign in to comment.