Skip to content

Commit 8986651

Browse files
authored
actions: disable support for deferrals (#37700)
1 parent ffbb62b commit 8986651

File tree

5 files changed

+111
-245
lines changed

5 files changed

+111
-245
lines changed

internal/plans/deferring/deferred.go

Lines changed: 28 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"sync"
99

10+
"github.com/hashicorp/hcl/v2"
1011
"github.com/zclconf/go-cty/cty"
1112

1213
"github.com/hashicorp/terraform/internal/addrs"
@@ -729,64 +730,51 @@ func (d *Deferred) ReportActionDeferred(addr addrs.AbsActionInstance, reason pro
729730
configMap.Put(addr, reason)
730731
}
731732

732-
// ShouldDeferActionInvocation returns true if there is a reason to defer the action invocation instance
733-
// We want to defer an action invocation if
734-
// a) the resource was deferred
735-
// or
736-
// b) a previously run action was deferred
737-
func (d *Deferred) ShouldDeferActionInvocation(ai plans.ActionInvocationInstance) bool {
733+
// ShouldDeferActionInvocation returns true if there is a reason to defer the
734+
// action invocation instance. We want to defer an action invocation only if
735+
// the triggering resource was deferred. In addition, we will check if the
736+
// underlying action was deferred via a reference, and consider it an error if
737+
// the triggering resource wasn't also deferred.
738+
//
739+
// The reason behind the slightly different behaviour here, is that if an
740+
// action invocation is deferred, then that implies the triggering action
741+
// should also be deferred.
742+
//
743+
// We don't yet have the capability to retroactively defer a resource, so for
744+
// now actions initiating deferrals themselves is considered an error.
745+
func (d *Deferred) ShouldDeferActionInvocation(ai plans.ActionInvocationInstance, triggerRange *hcl.Range) (bool, tfdiags.Diagnostics) {
738746
d.mu.Lock()
739747
defer d.mu.Unlock()
740748

741-
// The expansion of the action itself is deferred
742-
if ai.Addr.Action.Key == addrs.WildcardKey {
743-
return true
744-
}
745-
746-
if c, ok := d.actionExpansionDeferred.GetOk(ai.Addr.ConfigAction()); ok {
747-
if c.Has(ai.Addr) {
748-
return true
749-
}
750-
751-
for _, k := range c.Keys() {
752-
if k.Action.Key == addrs.WildcardKey {
753-
return true
754-
}
755-
}
756-
}
757-
758-
if d.partialExpandedActionsDeferred.Has(ai.Addr.ConfigAction()) {
759-
return true
760-
}
749+
var diags tfdiags.Diagnostics
761750

762751
// We only want to defer actions that are lifecycle triggered
763752
at, ok := ai.ActionTrigger.(*plans.LifecycleActionTrigger)
764753
if !ok {
765-
return false
754+
return false, diags
766755
}
767756

768757
// If the resource was deferred, we also need to defer any action potentially triggering from this
769758
if configResourceMap, ok := d.resourceInstancesDeferred.GetOk(at.TriggeringResourceAddr.ConfigResource()); ok {
770759
if configResourceMap.Has(at.TriggeringResourceAddr) {
771-
return true
760+
return true, diags
772761
}
773762
}
774763

775-
// Since all actions plan in order we can just check if an action for this resource instance
776-
// has been deferred already
777-
for _, deferred := range d.actionInvocationDeferred {
778-
deferredAt, deferredOk := deferred.ActionInvocationInstance.ActionTrigger.(*plans.LifecycleActionTrigger)
779-
if !deferredOk {
780-
continue // We only care about lifecycle triggered actions here
781-
}
782-
783-
if deferredAt.TriggeringResourceAddr.Equal(at.TriggeringResourceAddr) {
784-
return true
764+
if c, ok := d.actionExpansionDeferred.GetOk(ai.Addr.ConfigAction()); ok {
765+
if c.Has(ai.Addr) {
766+
// Then in this case, the resource wasn't deferred but the action
767+
// was and so we will consider this to be an error.
768+
diags = diags.Append(&hcl.Diagnostic{
769+
Severity: hcl.DiagError,
770+
Summary: "Invalid action deferral",
771+
Detail: fmt.Sprintf("The action %s was marked as deferred, but was triggered by a non-deferred resource %s. To work around this, use the -target argument to first apply only the resources that the action block depends on.", ai.Addr, at.TriggeringResourceAddr),
772+
Subject: triggerRange,
773+
})
785774
}
786775
}
787776

788-
// We found no reason, so we return false
789-
return false
777+
return false, diags
790778
}
791779

792780
// ShouldDeferAction returns true if the action should be deferred. This is the case if a

internal/terraform/context_plan_actions_test.go

Lines changed: 44 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -1818,7 +1818,7 @@ resource "test_object" "a" {
18181818
expectPlanActionCalled: true,
18191819
planOpts: &PlanOpts{
18201820
Mode: plans.NormalMode,
1821-
DeferralAllowed: true,
1821+
DeferralAllowed: true, // actions should ignore this setting
18221822
},
18231823
planActionFn: func(*testing.T, providers.PlanActionRequest) providers.PlanActionResponse {
18241824
return providers.PlanActionResponse{
@@ -1827,25 +1827,13 @@ resource "test_object" "a" {
18271827
},
18281828
}
18291829
},
1830-
1831-
assertPlan: func(t *testing.T, p *plans.Plan) {
1832-
if len(p.Changes.ActionInvocations) != 0 {
1833-
t.Fatalf("expected 0 actions in plan, got %d", len(p.Changes.ActionInvocations))
1834-
}
1835-
1836-
if len(p.DeferredActionInvocations) != 1 {
1837-
t.Fatalf("expected 1 deferred action in plan, got %d", len(p.DeferredActionInvocations))
1838-
}
1839-
deferredActionInvocation := p.DeferredActionInvocations[0]
1840-
if deferredActionInvocation.DeferredReason != providers.DeferredReasonAbsentPrereq {
1841-
t.Fatalf("expected deferred action to be deferred due to absent prereq, but got %s", deferredActionInvocation.DeferredReason)
1842-
}
1843-
if deferredActionInvocation.ActionInvocationInstanceSrc.ActionTrigger.(*plans.LifecycleActionTrigger).TriggeringResourceAddr.String() != "test_object.a" {
1844-
t.Fatalf("expected deferred action to be triggered by test_object.a, but got %s", deferredActionInvocation.ActionInvocationInstanceSrc.ActionTrigger.(*plans.LifecycleActionTrigger).TriggeringResourceAddr.String())
1845-
}
1846-
1847-
if deferredActionInvocation.ActionInvocationInstanceSrc.Addr.String() != "action.test_action.hello" {
1848-
t.Fatalf("expected deferred action to be triggered by action.test_action.hello, but got %s", deferredActionInvocation.ActionInvocationInstanceSrc.Addr.String())
1830+
expectPlanDiagnostics: func(m *configs.Config) tfdiags.Diagnostics {
1831+
return tfdiags.Diagnostics{
1832+
tfdiags.Sourceless(
1833+
tfdiags.Error,
1834+
"Provider deferred changes when Terraform did not allow deferrals",
1835+
`The provider signaled a deferred action for "action.test_action.hello", but in this context deferrals are disabled. This is a bug in the provider, please file an issue with the provider developers.`,
1836+
),
18491837
}
18501838
},
18511839
},
@@ -1888,37 +1876,17 @@ resource "test_object" "a" {
18881876
},
18891877
}
18901878
},
1891-
1892-
assertPlan: func(t *testing.T, p *plans.Plan) {
1893-
if len(p.Changes.ActionInvocations) != 0 {
1894-
t.Fatalf("expected 0 actions in plan, got %d", len(p.Changes.ActionInvocations))
1895-
}
1896-
1897-
if len(p.DeferredActionInvocations) != 2 {
1898-
t.Fatalf("expected 2 deferred actions in plan, got %d", len(p.DeferredActionInvocations))
1899-
}
1900-
firstDeferredActionInvocation := p.DeferredActionInvocations[0]
1901-
if firstDeferredActionInvocation.DeferredReason != providers.DeferredReasonAbsentPrereq {
1902-
t.Fatalf("expected deferred action to be deferred due to absent prereq, but got %s", firstDeferredActionInvocation.DeferredReason)
1903-
}
1904-
if firstDeferredActionInvocation.ActionInvocationInstanceSrc.ActionTrigger.(*plans.LifecycleActionTrigger).TriggeringResourceAddr.String() != "test_object.a" {
1905-
t.Fatalf("expected deferred action to be triggered by test_object.a, but got %s", firstDeferredActionInvocation.ActionInvocationInstanceSrc.ActionTrigger.(*plans.LifecycleActionTrigger).TriggeringResourceAddr.String())
1906-
}
1907-
1908-
if firstDeferredActionInvocation.ActionInvocationInstanceSrc.Addr.String() != "action.test_action.hello" {
1909-
t.Fatalf("expected deferred action to be triggered by action.test_action.hello, but got %s", firstDeferredActionInvocation.ActionInvocationInstanceSrc.Addr.String())
1910-
}
1911-
1912-
secondDeferredActionInvocation := p.DeferredActionInvocations[1]
1913-
if secondDeferredActionInvocation.DeferredReason != providers.DeferredReasonDeferredPrereq {
1914-
t.Fatalf("expected second deferred action to be deferred due to deferred prereq, but got %s", secondDeferredActionInvocation.DeferredReason)
1915-
}
1916-
if secondDeferredActionInvocation.ActionInvocationInstanceSrc.ActionTrigger.(*plans.LifecycleActionTrigger).TriggeringResourceAddr.String() != "test_object.a" {
1917-
t.Fatalf("expected second deferred action to be triggered by test_object.a, but got %s", secondDeferredActionInvocation.ActionInvocationInstanceSrc.ActionTrigger.(*plans.LifecycleActionTrigger).TriggeringResourceAddr.String())
1918-
}
1919-
1920-
if secondDeferredActionInvocation.ActionInvocationInstanceSrc.Addr.String() != "action.ecosystem.world" {
1921-
t.Fatalf("expected second deferred action to be triggered by action.ecosystem.world, but got %s", secondDeferredActionInvocation.ActionInvocationInstanceSrc.Addr.String())
1879+
expectPlanDiagnostics: func(m *configs.Config) tfdiags.Diagnostics {
1880+
// for now, it's just an error for any deferrals but when
1881+
// this gets implemented we should check that all the
1882+
// actions are deferred even though only one of them
1883+
// was actually marked as deferred.
1884+
return tfdiags.Diagnostics{
1885+
tfdiags.Sourceless(
1886+
tfdiags.Error,
1887+
"Provider deferred changes when Terraform did not allow deferrals",
1888+
`The provider signaled a deferred action for "action.test_action.hello", but in this context deferrals are disabled. This is a bug in the provider, please file an issue with the provider developers.`,
1889+
),
19221890
}
19231891
},
19241892
},
@@ -1965,50 +1933,17 @@ resource "test_object" "a" {
19651933
},
19661934
}
19671935
},
1968-
1969-
assertPlan: func(t *testing.T, p *plans.Plan) {
1970-
if len(p.Changes.ActionInvocations) != 0 {
1971-
t.Fatalf("expected 0 actions in plan, got %d", len(p.Changes.ActionInvocations))
1972-
}
1973-
1974-
if len(p.DeferredActionInvocations) != 2 {
1975-
t.Fatalf("expected 2 deferred actions in plan, got %d", len(p.DeferredActionInvocations))
1976-
}
1977-
firstDeferredActionInvocation := p.DeferredActionInvocations[0]
1978-
if firstDeferredActionInvocation.DeferredReason != providers.DeferredReasonAbsentPrereq {
1979-
t.Fatalf("expected deferred action to be deferred due to absent prereq, but got %s", firstDeferredActionInvocation.DeferredReason)
1980-
}
1981-
if firstDeferredActionInvocation.ActionInvocationInstanceSrc.ActionTrigger.(*plans.LifecycleActionTrigger).TriggeringResourceAddr.String() != "test_object.a" {
1982-
t.Fatalf("expected deferred action to be triggered by test_object.a, but got %s", firstDeferredActionInvocation.ActionInvocationInstanceSrc.ActionTrigger.(*plans.LifecycleActionTrigger).TriggeringResourceAddr.String())
1983-
}
1984-
1985-
if firstDeferredActionInvocation.ActionInvocationInstanceSrc.Addr.String() != "action.test_action.hello" {
1986-
t.Fatalf("expected deferred action to be triggered by action.test_action.hello, but got %s", firstDeferredActionInvocation.ActionInvocationInstanceSrc.Addr.String())
1987-
}
1988-
1989-
secondDeferredActionInvocation := p.DeferredActionInvocations[1]
1990-
if secondDeferredActionInvocation.DeferredReason != providers.DeferredReasonDeferredPrereq {
1991-
t.Fatalf("expected second deferred action to be deferred due to deferred prereq, but got %s", secondDeferredActionInvocation.DeferredReason)
1992-
}
1993-
if secondDeferredActionInvocation.ActionInvocationInstanceSrc.ActionTrigger.(*plans.LifecycleActionTrigger).TriggeringResourceAddr.String() != "test_object.a" {
1994-
t.Fatalf("expected second deferred action to be triggered by test_object.a, but got %s", secondDeferredActionInvocation.ActionInvocationInstanceSrc.ActionTrigger.(*plans.LifecycleActionTrigger).TriggeringResourceAddr.String())
1995-
}
1996-
1997-
if secondDeferredActionInvocation.ActionInvocationInstanceSrc.Addr.String() != "action.ecosystem.world" {
1998-
t.Fatalf("expected second deferred action to be triggered by action.ecosystem.world, but got %s", secondDeferredActionInvocation.ActionInvocationInstanceSrc.Addr.String())
1999-
}
2000-
2001-
if len(p.DeferredResources) != 1 {
2002-
t.Fatalf("expected 1 resource to be deferred, got %d", len(p.DeferredResources))
2003-
}
2004-
deferredResource := p.DeferredResources[0]
2005-
2006-
if deferredResource.ChangeSrc.Addr.String() != "test_object.a" {
2007-
t.Fatalf("Expected resource %s to be deferred, but it was not", deferredResource.ChangeSrc.Addr)
2008-
}
2009-
2010-
if deferredResource.DeferredReason != providers.DeferredReasonDeferredPrereq {
2011-
t.Fatalf("Expected deferred reason to be deferred prereq, got %s", deferredResource.DeferredReason)
1936+
expectPlanDiagnostics: func(m *configs.Config) tfdiags.Diagnostics {
1937+
// for now, it's just an error for any deferrals but when
1938+
// this gets implemented we should check that all the
1939+
// actions are deferred even though only one of them
1940+
// was actually marked as deferred.
1941+
return tfdiags.Diagnostics{
1942+
tfdiags.Sourceless(
1943+
tfdiags.Error,
1944+
"Provider deferred changes when Terraform did not allow deferrals",
1945+
`The provider signaled a deferred action for "action.test_action.hello", but in this context deferrals are disabled. This is a bug in the provider, please file an issue with the provider developers.`,
1946+
),
20121947
}
20131948
},
20141949
},
@@ -2130,26 +2065,17 @@ resource "test_object" "a" {
21302065
},
21312066
},
21322067
},
2133-
assertPlan: func(t *testing.T, p *plans.Plan) {
2134-
if len(p.DeferredActionInvocations) != 1 {
2135-
t.Fatalf("expected exactly one invocation, and found %d", len(p.DeferredActionInvocations))
2068+
assertPlanDiagnostics: func(t *testing.T, diagnostics tfdiags.Diagnostics) {
2069+
if len(diagnostics) != 1 {
2070+
t.Fatal("wrong number of diagnostics")
21362071
}
21372072

2138-
if p.DeferredActionInvocations[0].DeferredReason != providers.DeferredReasonDeferredPrereq {
2139-
t.Fatalf("expected.DeferredReasonDeferredPrereq, got %s", p.DeferredActionInvocations[0].DeferredReason)
2073+
if diagnostics[0].Severity() != tfdiags.Error {
2074+
t.Error("expected error severity")
21402075
}
21412076

2142-
ai := p.DeferredActionInvocations[0].ActionInvocationInstanceSrc
2143-
if ai.Addr.String() != `action.test_action.hello["a"]` {
2144-
t.Fatalf(`expected action invocation for action.test_action.hello["a"], got %s`, ai.Addr.String())
2145-
}
2146-
2147-
if len(p.DeferredResources) != 1 {
2148-
t.Fatalf("expected 1 deferred resource, got %d", len(p.DeferredResources))
2149-
}
2150-
2151-
if p.DeferredResources[0].ChangeSrc.Addr.String() != "test_object.a" {
2152-
t.Fatalf("expected test_object.a, got %s", p.DeferredResources[0].ChangeSrc.Addr.String())
2077+
if diagnostics[0].Description().Summary != "Invalid for_each argument" {
2078+
t.Errorf("expected for_each argument to be source of error but was %s", diagnostics[0].Description().Summary)
21532079
}
21542080
},
21552081
},
@@ -2335,42 +2261,17 @@ resource "test_object" "a" {
23352261
}
23362262
},
23372263

2338-
assertPlan: func(t *testing.T, p *plans.Plan) {
2339-
if len(p.DeferredActionInvocations) != 1 {
2340-
t.Errorf("Expected 1 deferred action invocation, got %d", len(p.DeferredActionInvocations))
2341-
}
2342-
if p.DeferredActionInvocations[0].ActionInvocationInstanceSrc.Addr.String() != "action.test_action.hello" {
2343-
t.Errorf("Expected action. test_action.hello, got %s", p.DeferredActionInvocations[0].ActionInvocationInstanceSrc.Addr.String())
2344-
}
2345-
if p.DeferredActionInvocations[0].DeferredReason != providers.DeferredReasonDeferredPrereq {
2346-
t.Errorf("Expected DeferredReasonDeferredPrereq, got %s", p.DeferredActionInvocations[0].DeferredReason)
2264+
assertPlanDiagnostics: func(t *testing.T, diagnostics tfdiags.Diagnostics) {
2265+
if len(diagnostics) != 1 {
2266+
t.Fatal("wrong number of diagnostics")
23472267
}
23482268

2349-
if len(p.DeferredResources) != 2 {
2350-
t.Fatalf("Expected 2 deferred resources, got %d", len(p.DeferredResources))
2269+
if diagnostics[0].Severity() != tfdiags.Error {
2270+
t.Error("expected error diagnostics")
23512271
}
23522272

2353-
slices.SortFunc(p.DeferredResources, func(a, b *plans.DeferredResourceInstanceChangeSrc) int {
2354-
if a.ChangeSrc.Addr.Less(b.ChangeSrc.Addr) {
2355-
return -1
2356-
}
2357-
if b.ChangeSrc.Addr.Less(a.ChangeSrc.Addr) {
2358-
return 1
2359-
}
2360-
return 0
2361-
})
2362-
2363-
if p.DeferredResources[0].ChangeSrc.Addr.String() != "test_object.a" {
2364-
t.Errorf("Expected test_object.a to be first, got %s", p.DeferredResources[0].ChangeSrc.Addr.String())
2365-
}
2366-
if p.DeferredResources[0].DeferredReason != providers.DeferredReasonDeferredPrereq {
2367-
t.Errorf("Expected DeferredReasonDeferredPrereq, got %s", p.DeferredResources[0].DeferredReason)
2368-
}
2369-
if p.DeferredResources[1].ChangeSrc.Addr.String() != "test_object.origin" {
2370-
t.Errorf("Expected test_object.origin to be second, got %s", p.DeferredResources[1].ChangeSrc.Addr.String())
2371-
}
2372-
if p.DeferredResources[1].DeferredReason != providers.DeferredReasonAbsentPrereq {
2373-
t.Errorf("Expected DeferredReasonAbsentPrereq, got %s", p.DeferredResources[1].DeferredReason)
2273+
if diagnostics[0].Description().Summary != "Invalid action deferral" {
2274+
t.Errorf("expected deferral to be source of error was %s", diagnostics[0].Description().Summary)
23742275
}
23752276
},
23762277
},

0 commit comments

Comments
 (0)