Skip to content

Commit 34b8c8c

Browse files
enable unknown conditionals
1 parent 697d9d7 commit 34b8c8c

File tree

10 files changed

+170
-25
lines changed

10 files changed

+170
-25
lines changed

internal/command/jsonplan/action_invocations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ type LifecycleActionTrigger struct {
4444
ActionTriggerEvent string `json:"action_trigger_event,omitempty"`
4545
ActionTriggerBlockIndex int `json:"action_trigger_block_index"`
4646
ActionsListIndex int `json:"actions_list_index"`
47+
Tentative bool `json:"tentative"`
4748
}
4849

4950
type InvokeCmdActionTrigger struct {
@@ -135,6 +136,7 @@ func MarshalActionInvocation(action *plans.ActionInvocationInstanceSrc, schemas
135136
ActionTriggerEvent: at.TriggerEvent().String(),
136137
ActionTriggerBlockIndex: at.ActionTriggerBlockIndex,
137138
ActionsListIndex: at.ActionsListIndex,
139+
Tentative: at.Tentative,
138140
}
139141
default:
140142
return ai, fmt.Errorf("unsupported action trigger type: %T", at)

internal/command/views/hook_json_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,15 @@ func testJSONHookResourceID(addr addrs.AbsResourceInstance) terraform.HookResour
3131
}
3232
}
3333

34-
func testJSONHookActionID(actionAddr addrs.AbsActionInstance, triggeringResourceAddr addrs.AbsResourceInstance, actionTriggerIndex int, actionsListIndex int) terraform.HookActionIdentity {
34+
func testJSONHookActionID(actionAddr addrs.AbsActionInstance, triggeringResourceAddr addrs.AbsResourceInstance, actionTriggerIndex int, actionsListIndex int, certain bool) terraform.HookActionIdentity {
3535
return terraform.HookActionIdentity{
3636
Addr: actionAddr,
3737
ActionTrigger: plans.LifecycleActionTrigger{
3838
TriggeringResourceAddr: triggeringResourceAddr,
3939
ActionTriggerBlockIndex: actionTriggerIndex,
4040
ActionsListIndex: actionsListIndex,
4141
ActionTriggerEvent: configs.AfterCreate,
42+
Tentative: certain,
4243
},
4344
}
4445
}
@@ -610,8 +611,8 @@ func TestJSONHook_actions(t *testing.T) {
610611
Name: "boop",
611612
}.Instance(addrs.NoKey).Absolute(subModule)
612613

613-
actionAHookId := testJSONHookActionID(actionA, resourceA, 0, 1)
614-
actionBHookId := testJSONHookActionID(actionB, resourceB, 2, 3)
614+
actionAHookId := testJSONHookActionID(actionA, resourceA, 0, 1, true)
615+
actionBHookId := testJSONHookActionID(actionB, resourceB, 2, 3, false)
615616

616617
action, err := hook.StartAction(actionAHookId)
617618
testHookReturnValues(t, action, err)

internal/command/views/json/change.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ func NewPlannedActionInvocation(aiSrc *plans.ActionInvocationInstanceSrc) *Actio
7070
TriggeringEvent: at.ActionTriggerEvent.String(),
7171
ActionTriggerBlockIndex: at.ActionTriggerBlockIndex,
7272
ActionsListIndex: at.ActionsListIndex,
73+
Tentative: at.Tentative,
7374
}
7475
}
7576

@@ -93,6 +94,7 @@ type ActionInvocationLifecycleTrigger struct {
9394
TriggeringEvent string `json:"triggering_event"`
9495
ActionTriggerBlockIndex int `json:"action_trigger_block_index"`
9596
ActionsListIndex int `json:"actions_list_index"`
97+
Tentative bool `json:"tentative"`
9698
}
9799

98100
type ChangeAction string

internal/command/views/operation_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,7 @@ func TestOperationJSON_plan_with_actions(t *testing.T) {
680680
"lifecycle_trigger": map[string]interface{}{
681681
"action_trigger_block_index": float64(0),
682682
"actions_list_index": float64(0),
683+
"tentative": false,
683684
"triggering_event": "AfterCreate",
684685
"triggering_resource": map[string]interface{}{
685686
"addr": `test_resource.boop`,
@@ -712,6 +713,7 @@ func TestOperationJSON_plan_with_actions(t *testing.T) {
712713
"lifecycle_trigger": map[string]interface{}{
713714
"action_trigger_block_index": float64(0),
714715
"actions_list_index": float64(1),
716+
"tentative": false,
715717
"triggering_event": "AfterCreate",
716718
"triggering_resource": map[string]interface{}{
717719
"addr": `test_resource.boop`,
@@ -744,6 +746,7 @@ func TestOperationJSON_plan_with_actions(t *testing.T) {
744746
"lifecycle_trigger": map[string]interface{}{
745747
"action_trigger_block_index": float64(1),
746748
"actions_list_index": float64(0),
749+
"tentative": false,
747750
"triggering_event": "BeforeUpdate",
748751
"triggering_resource": map[string]interface{}{
749752
"addr": `module.vpc.test_resource.beep[0]`,

internal/plans/action_invocation.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ type LifecycleActionTrigger struct {
5757
ActionTriggerBlockIndex int
5858
// The index of the action in the events list of the action_trigger block
5959
ActionsListIndex int
60+
// Set to false if the condition is unknown, true otherwise
61+
Tentative bool
6062
}
6163

6264
func (t LifecycleActionTrigger) TriggerEvent() configs.ActionTriggerEvent {

internal/plans/planfile/tfplan.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,6 +1347,7 @@ func actionInvocationFromTfplan(rawAction *planproto.ActionInvocationInstance) (
13471347
ActionTriggerBlockIndex: int(at.LifecycleActionTrigger.ActionTriggerBlockIndex),
13481348
ActionsListIndex: int(at.LifecycleActionTrigger.ActionsListIndex),
13491349
ActionTriggerEvent: ate,
1350+
Tentative: at.LifecycleActionTrigger.Tentative,
13501351
}
13511352
default:
13521353
// This should be exhaustive
@@ -1408,6 +1409,7 @@ func actionInvocationToTfPlan(action *plans.ActionInvocationInstanceSrc) (*planp
14081409
TriggeringResourceAddr: at.TriggeringResourceAddr.String(),
14091410
ActionTriggerBlockIndex: int64(at.ActionTriggerBlockIndex),
14101411
ActionsListIndex: int64(at.ActionsListIndex),
1412+
Tentative: at.Tentative,
14111413
},
14121414
}
14131415
default:

internal/plans/planproto/planfile.pb.go

Lines changed: 11 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/plans/planproto/planfile.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,7 @@ message LifecycleActionTrigger {
485485
ActionTriggerEvent trigger_event = 2;
486486
int64 action_trigger_block_index = 3;
487487
int64 actions_list_index = 4;
488+
bool tentative = 5;
488489
}
489490

490491
message ResourceInstanceActionChange {

internal/terraform/context_plan_actions_test.go

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1759,7 +1759,6 @@ resource "test_object" "a" {
17591759
},
17601760

17611761
"unknown condition": {
1762-
toBeImplemented: true,
17631762
module: map[string]string{
17641763
"main.tf": `
17651764
variable "cond" {
@@ -1867,6 +1866,132 @@ resource "test_object" "a" {
18671866
}
18681867
},
18691868
},
1869+
1870+
"non-boolean unknown condition": {
1871+
module: map[string]string{
1872+
"main.tf": `
1873+
variable "cond" {
1874+
type = number
1875+
}
1876+
action "test_unlinked" "hello" {}
1877+
resource "test_object" "a" {
1878+
lifecycle {
1879+
action_trigger {
1880+
events = [before_create]
1881+
condition = var.cond + 12
1882+
actions = [action.test_unlinked.hello]
1883+
}
1884+
}
1885+
}
1886+
`,
1887+
},
1888+
expectPlanActionCalled: false,
1889+
planOpts: &PlanOpts{
1890+
Mode: plans.NormalMode,
1891+
SetVariables: InputValues{
1892+
"cond": &InputValue{
1893+
Value: cty.UnknownVal(cty.Number),
1894+
SourceType: ValueFromCaller,
1895+
},
1896+
},
1897+
},
1898+
1899+
expectPlanDiagnostics: func(m *configs.Config) tfdiags.Diagnostics {
1900+
return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
1901+
Severity: hcl.DiagError,
1902+
Summary: "Incorrect value type",
1903+
Detail: "Invalid expression value: bool required, but have number.",
1904+
Subject: &hcl.Range{
1905+
Filename: filepath.Join(m.Module.SourceDir, "main.tf"),
1906+
Start: hcl.Pos{Line: 10, Column: 19, Byte: 186},
1907+
End: hcl.Pos{Line: 10, Column: 39, Byte: 199},
1908+
},
1909+
})
1910+
},
1911+
},
1912+
1913+
"using count in condition": {
1914+
toBeImplemented: true,
1915+
module: map[string]string{
1916+
"main.tf": `
1917+
1918+
action "test_unlinked" "hello" {}
1919+
action "test_unlinked" "world" {}
1920+
1921+
resource "test_object" "a" {
1922+
count = 2
1923+
name = "foo"
1924+
lifecycle {
1925+
action_trigger {
1926+
events = [before_create]
1927+
condition = count.index == 1
1928+
actions = [action.test_unlinked.hello]
1929+
}
1930+
action_trigger {
1931+
events = [before_create]
1932+
condition = count.index == 2
1933+
actions = [action.test_unlinked.world]
1934+
}
1935+
}
1936+
}
1937+
`,
1938+
},
1939+
expectPlanActionCalled: true,
1940+
1941+
assertPlan: func(t *testing.T, p *plans.Plan) {
1942+
if len(p.Changes.ActionInvocations) != 1 {
1943+
t.Fatalf("expected 1 actions in plan, got %d", len(p.Changes.ActionInvocations))
1944+
}
1945+
if p.Changes.ActionInvocations[0].Addr.String() != "action.test_unlinked.hello" {
1946+
t.Fatalf("expected action.test_unlinked.hello, got %s", p.Changes.ActionInvocations[0].Addr.String())
1947+
}
1948+
at := p.Changes.ActionInvocations[0].ActionTrigger.(plans.LifecycleActionTrigger)
1949+
if !at.TriggeringResourceAddr.Equal(mustResourceInstanceAddr("test_object.a[1]")) {
1950+
t.Fatalf("expected test_object.a[1], got %s", at.TriggeringResourceAddr.String())
1951+
}
1952+
},
1953+
},
1954+
"using for_each in condition": {
1955+
toBeImplemented: true,
1956+
module: map[string]string{
1957+
"main.tf": `
1958+
1959+
action "test_unlinked" "hello" {}
1960+
action "test_unlinked" "world" {}
1961+
1962+
resource "test_object" "a" {
1963+
for_each = toset(["foo", "bar"])
1964+
name = "foo"
1965+
lifecycle {
1966+
action_trigger {
1967+
events = [before_create]
1968+
condition = for_each.key == "bar"
1969+
actions = [action.test_unlinked.hello]
1970+
}
1971+
action_trigger {
1972+
events = [before_create]
1973+
condition = for_each.key == "baz"
1974+
actions = [action.test_unlinked.world]
1975+
}
1976+
}
1977+
}
1978+
`,
1979+
},
1980+
expectPlanActionCalled: true,
1981+
1982+
assertPlan: func(t *testing.T, p *plans.Plan) {
1983+
if len(p.Changes.ActionInvocations) != 1 {
1984+
t.Fatalf("expected 1 actions in plan, got %d", len(p.Changes.ActionInvocations))
1985+
}
1986+
if p.Changes.ActionInvocations[0].Addr.String() != "action.test_unlinked.hello" {
1987+
t.Fatalf("expected action.test_unlinked.hello, got %s", p.Changes.ActionInvocations[0].Addr.String())
1988+
}
1989+
at := p.Changes.ActionInvocations[0].ActionTrigger.(plans.LifecycleActionTrigger)
1990+
if !at.TriggeringResourceAddr.Equal(mustResourceInstanceAddr(`test_object.a["bar"]`)) {
1991+
t.Fatalf(`expected test_object.a["bar"], got %s`, at.TriggeringResourceAddr.String())
1992+
}
1993+
},
1994+
},
18701995
} {
18711996
t.Run(name, func(t *testing.T) {
18721997
if tc.toBeImplemented {

internal/terraform/node_action_trigger_instance_plan.go

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ func (at *lifecycleActionTriggerInstance) Name() string {
3939
return fmt.Sprintf("%s.lifecycle.action_trigger[%d].actions[%d]", at.resourceAddress.String(), at.actionTriggerBlockIndex, at.actionListIndex)
4040
}
4141

42-
func (at *lifecycleActionTriggerInstance) ActionTrigger(triggeringEvent configs.ActionTriggerEvent) plans.LifecycleActionTrigger {
42+
func (at *lifecycleActionTriggerInstance) ActionTrigger(triggeringEvent configs.ActionTriggerEvent, Tentative bool) plans.LifecycleActionTrigger {
4343
return plans.LifecycleActionTrigger{
4444
TriggeringResourceAddr: at.resourceAddress,
4545
ActionTriggerBlockIndex: at.actionTriggerBlockIndex,
4646
ActionsListIndex: at.actionListIndex,
4747
ActionTriggerEvent: triggeringEvent,
48+
Tentative: Tentative,
4849
}
4950
}
5051

@@ -87,7 +88,7 @@ func (n *nodeActionTriggerPlanInstance) Execute(ctx EvalContext, operation walkO
8788
ai := plans.ActionInvocationInstance{
8889
Addr: n.actionAddress,
8990
ProviderAddr: actionInstance.ProviderAddr,
90-
ActionTrigger: n.lifecycleActionTrigger.ActionTrigger(configs.Unknown),
91+
ActionTrigger: n.lifecycleActionTrigger.ActionTrigger(configs.Unknown, false),
9192
ConfigValue: actionInstance.ConfigValue,
9293
}
9394

@@ -109,22 +110,30 @@ func (n *nodeActionTriggerPlanInstance) Execute(ctx EvalContext, operation walkO
109110
panic("triggeringEvent cannot be nil")
110111
}
111112

112-
// Evaluate the condition expression if it exists (otherwise it's true)
113+
// It is only uncertain an action will be triggered if the condition is unknown
114+
tentative := false
115+
// Evaluate the condition expression if it exists
113116
if n.lifecycleActionTrigger != nil && n.lifecycleActionTrigger.conditionExpr != nil {
114117
condition, conditionDiags := evaluateCondition(ctx, n.lifecycleActionTrigger.conditionExpr)
115118
diags = diags.Append(conditionDiags)
116119
if conditionDiags.HasErrors() {
117120
return conditionDiags
118121
}
119122

120-
// The condition is false so we skip the action
121-
if condition.False() {
122-
return diags
123+
if condition.IsWhollyKnown() {
124+
// The condition is false so we skip the action
125+
if condition.False() {
126+
return diags
127+
}
128+
} else {
129+
// If the condition is unknown, we cannot be certain the action will be triggered
130+
// but we still need to plan the action as if it will be triggered
131+
tentative = true
123132
}
124133
}
125134

126135
// We need to set the triggering event on the action invocation
127-
ai.ActionTrigger = n.lifecycleActionTrigger.ActionTrigger(*triggeringEvent)
136+
ai.ActionTrigger = n.lifecycleActionTrigger.ActionTrigger(*triggeringEvent, tentative)
128137

129138
provider, _, err := getProvider(ctx, actionInstance.ProviderAddr)
130139
if err != nil {
@@ -214,19 +223,8 @@ func evaluateCondition(ctx EvalContext, conditionExpr hcl.Expression) (cty.Value
214223
return cty.False, diags
215224
}
216225

217-
// TODO: Support unknown condition values
218226
if !val.IsWhollyKnown() {
219-
panic("condition is not wholly known")
220-
}
221-
// If the condition is neither true nor false, it's an error
222-
if !(val.True() || val.False()) {
223-
diags = diags.Append(&hcl.Diagnostic{
224-
Severity: hcl.DiagError,
225-
Summary: "Invalid condition",
226-
Detail: "The condition must be either true or false",
227-
Subject: conditionExpr.Range().Ptr(),
228-
})
229-
return cty.False, diags
227+
return val, diags
230228
}
231229

232230
return val, nil

0 commit comments

Comments
 (0)