Skip to content

Commit 386988c

Browse files
authored
Decode action blocks and action_triggers inside resource blocks. (hashicorp#37030)
* Decode action blocks and action_triggers inside resource blocks. This commit adds decoding of action and action_triggers inside terraform configuration. I added an Actions experiment as a hacky way of keeping the functionality out of main until we're ready for the alpha; this may never be an experiment but it's a handy feature flag so we don't have to do all the work in a long-lived feature branch. * remove legacy shim handling * validate that the referenced entry in actions is indeed an action
1 parent 9ced999 commit 386988c

File tree

13 files changed

+595
-8
lines changed

13 files changed

+595
-8
lines changed

internal/configs/action.go

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package configs
5+
6+
import (
7+
"fmt"
8+
9+
"github.com/hashicorp/hcl/v2"
10+
"github.com/hashicorp/hcl/v2/hclsyntax"
11+
12+
"github.com/hashicorp/terraform/internal/addrs"
13+
)
14+
15+
// Action represents an "action" block inside a configuration
16+
type Action struct {
17+
Name string
18+
Type string
19+
Config hcl.Body
20+
Count hcl.Expression
21+
ForEach hcl.Expression
22+
DependsOn []hcl.Traversal
23+
24+
ProviderConfigRef *ProviderConfigRef
25+
Provider addrs.Provider
26+
27+
DeclRange hcl.Range
28+
TypeRange hcl.Range
29+
}
30+
31+
// ActionTrigger represents a configured "action_trigger" inside the lifecycle
32+
// block of a managed resource.
33+
type ActionTrigger struct {
34+
Condition hcl.Expression
35+
Events []ActionTriggerEvent
36+
Actions []ActionRef
37+
38+
DeclRange hcl.Range
39+
}
40+
41+
// ActionTriggerEvent is an enum for valid values for events for action
42+
// triggers.
43+
type ActionTriggerEvent int
44+
45+
//go:generate go tool golang.org/x/tools/cmd/stringer -type ActionTriggerEvent
46+
47+
const (
48+
BeforeCreate ActionTriggerEvent = iota
49+
AfterCreate
50+
BeforeUpdate
51+
AfterUpdate
52+
BeforeDestroy
53+
AfterDestroy
54+
)
55+
56+
// ActionRef represents a reference to a configured Action
57+
// copypasta of providerconfigref; not sure what's needed.
58+
type ActionRef struct {
59+
Traversal hcl.Traversal
60+
Range hcl.Range
61+
}
62+
63+
func decodeActionTriggerBlock(block *hcl.Block) (*ActionTrigger, hcl.Diagnostics) {
64+
var diags hcl.Diagnostics
65+
a := &ActionTrigger{
66+
Events: []ActionTriggerEvent{},
67+
Actions: []ActionRef{},
68+
Condition: nil,
69+
}
70+
71+
content, bodyDiags := block.Body.Content(actionTriggerSchema)
72+
diags = append(diags, bodyDiags...)
73+
74+
if attr, exists := content.Attributes["condition"]; exists {
75+
a.Condition = attr.Expr
76+
}
77+
78+
// this is parsing events like expressions, so it's angry when there's quotes
79+
// needs to parse strings:
80+
// Quoted references are deprecated; In this context, references are expected literally rather than in quotes.
81+
if attr, exists := content.Attributes["events"]; exists {
82+
exprs, ediags := hcl.ExprList(attr.Expr)
83+
diags = append(diags, ediags...)
84+
85+
events := []ActionTriggerEvent{}
86+
87+
for _, expr := range exprs {
88+
var event ActionTriggerEvent
89+
switch hcl.ExprAsKeyword(expr) {
90+
case "before_create":
91+
event = BeforeCreate
92+
case "after_create":
93+
event = AfterCreate
94+
case "before_update":
95+
event = BeforeUpdate
96+
case "after_update":
97+
event = AfterUpdate
98+
case "before_destroy":
99+
event = BeforeDestroy
100+
case "after_destroy":
101+
event = AfterDestroy
102+
default:
103+
diags = append(diags, &hcl.Diagnostic{
104+
Severity: hcl.DiagError,
105+
Summary: fmt.Sprintf("Invalid \"event\" value %s", hcl.ExprAsKeyword(expr)),
106+
Detail: "The \"event\" argument supports the following values: before_create, after_create, before_update, after_update, before_destroy, after_destroy.",
107+
Subject: expr.Range().Ptr(),
108+
})
109+
}
110+
events = append(events, event)
111+
}
112+
a.Events = events
113+
}
114+
115+
if attr, exists := content.Attributes["actions"]; exists {
116+
exprs, ediags := hcl.ExprList(attr.Expr)
117+
diags = append(diags, ediags...)
118+
actions := []ActionRef{}
119+
for _, expr := range exprs {
120+
traversal, travDiags := hcl.AbsTraversalForExpr(expr)
121+
diags = append(diags, travDiags...)
122+
// verify that the traversal is an action
123+
if traversal.RootName() != "action" {
124+
diags = append(diags, &hcl.Diagnostic{
125+
Severity: hcl.DiagError,
126+
Summary: "Invalid actions argument inside action_triggers",
127+
Detail: "action_triggers.actions accepts a list of one or more actions",
128+
Subject: block.DefRange.Ptr(),
129+
})
130+
}
131+
if len(traversal) != 0 {
132+
actionRef := ActionRef{
133+
Traversal: traversal,
134+
Range: expr.Range(),
135+
}
136+
actions = append(actions, actionRef)
137+
}
138+
}
139+
a.Actions = actions
140+
}
141+
142+
if len(a.Actions) == 0 {
143+
diags = append(diags, &hcl.Diagnostic{
144+
Severity: hcl.DiagError,
145+
Summary: "No actions specified",
146+
Detail: "At least one action must be specified for an action_trigger.",
147+
Subject: block.DefRange.Ptr(),
148+
})
149+
}
150+
151+
if len(a.Events) == 0 {
152+
diags = append(diags, &hcl.Diagnostic{
153+
Severity: hcl.DiagError,
154+
Summary: "No events specified",
155+
Detail: "At least one event must be specified for an action_trigger.",
156+
Subject: block.DefRange.Ptr(),
157+
})
158+
}
159+
return a, diags
160+
}
161+
162+
func decodeActionBlock(block *hcl.Block) (*Action, hcl.Diagnostics) {
163+
var diags hcl.Diagnostics
164+
a := &Action{
165+
Type: block.Labels[0],
166+
Name: block.Labels[1],
167+
DeclRange: block.DefRange,
168+
TypeRange: block.LabelRanges[0],
169+
}
170+
171+
if !hclsyntax.ValidIdentifier(a.Type) {
172+
diags = append(diags, &hcl.Diagnostic{
173+
Severity: hcl.DiagError,
174+
Summary: "Invalid action type name",
175+
Detail: badIdentifierDetail,
176+
Subject: &block.LabelRanges[0],
177+
})
178+
}
179+
if !hclsyntax.ValidIdentifier(a.Name) {
180+
diags = append(diags, &hcl.Diagnostic{
181+
Severity: hcl.DiagError,
182+
Summary: "Invalid action name",
183+
Detail: badIdentifierDetail,
184+
Subject: &block.LabelRanges[1],
185+
})
186+
}
187+
188+
content, remain, moreDiags := block.Body.PartialContent(actionBlockSchema)
189+
diags = append(diags, moreDiags...)
190+
a.Config = remain
191+
192+
if attr, exists := content.Attributes["count"]; exists {
193+
a.Count = attr.Expr
194+
}
195+
196+
if attr, exists := content.Attributes["for_each"]; exists {
197+
a.ForEach = attr.Expr
198+
// Cannot have count and for_each on the same action block
199+
if a.Count != nil {
200+
diags = append(diags, &hcl.Diagnostic{
201+
Severity: hcl.DiagError,
202+
Summary: `Invalid combination of "count" and "for_each"`,
203+
Detail: `The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used.`,
204+
Subject: &attr.NameRange,
205+
})
206+
}
207+
}
208+
209+
if attr, exists := content.Attributes["provider"]; exists {
210+
var providerDiags hcl.Diagnostics
211+
a.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider")
212+
diags = append(diags, providerDiags...)
213+
}
214+
215+
if attr, exists := content.Attributes["depends_on"]; exists {
216+
deps, depsDiags := DecodeDependsOn(attr)
217+
diags = append(diags, depsDiags...)
218+
a.DependsOn = append(a.DependsOn, deps...)
219+
}
220+
221+
return a, diags
222+
}
223+
224+
// actionBlockSchema is the schema for an action type within terraform.
225+
var actionBlockSchema = &hcl.BodySchema{
226+
Attributes: commonResourceAttributes,
227+
}
228+
229+
var actionTriggerSchema = &hcl.BodySchema{
230+
Attributes: []hcl.AttributeSchema{
231+
{
232+
Name: "events",
233+
Required: true,
234+
},
235+
{
236+
Name: "condition",
237+
Required: false,
238+
},
239+
{
240+
Name: "actions",
241+
Required: true,
242+
},
243+
},
244+
}
245+
246+
func (a *Action) moduleUniqueKey() string {
247+
return a.Addr().String()
248+
}
249+
250+
// Addr returns a resource address for the receiver that is relative to the
251+
// resource's containing module.
252+
func (a *Action) Addr() addrs.Action {
253+
return addrs.Action{
254+
Type: a.Type,
255+
Name: a.Name,
256+
}
257+
}
258+
259+
// ProviderConfigAddr returns the address for the provider configuration that
260+
// should be used for this action. This function returns a default provider
261+
// config addr if an explicit "provider" argument was not provided.
262+
func (a *Action) ProviderConfigAddr() addrs.LocalProviderConfig {
263+
if a.ProviderConfigRef == nil {
264+
// If no specific "provider" argument is given, we want to look up the
265+
// provider config where the local name matches the implied provider
266+
// from the resource type. This may be different from the resource's
267+
// provider type.
268+
return addrs.LocalProviderConfig{
269+
LocalName: a.Addr().ImpliedProvider(),
270+
}
271+
}
272+
273+
return addrs.LocalProviderConfig{
274+
LocalName: a.ProviderConfigRef.Name,
275+
Alias: a.ProviderConfigRef.Alias,
276+
}
277+
}

internal/configs/actiontriggerevent_string.go

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

internal/configs/module.go

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type Module struct {
4949
ManagedResources map[string]*Resource
5050
DataResources map[string]*Resource
5151
EphemeralResources map[string]*Resource
52+
Actions map[string]*Action
5253

5354
Moved []*Moved
5455
Removed []*Removed
@@ -95,7 +96,8 @@ type File struct {
9596
Removed []*Removed
9697
Import []*Import
9798

98-
Checks []*Check
99+
Checks []*Check
100+
Actions []*Action
99101
}
100102

101103
// NewModuleWithTests matches NewModule except it will also load in the provided
@@ -131,6 +133,7 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) {
131133
Checks: map[string]*Check{},
132134
ProviderMetas: map[addrs.Provider]*ProviderMeta{},
133135
Tests: map[string]*TestFile{},
136+
Actions: map[string]*Action{},
134137
}
135138

136139
// Process the required_providers blocks first, to ensure that all
@@ -491,6 +494,35 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics {
491494
m.Import = append(m.Import, i)
492495
}
493496

497+
for _, a := range file.Actions {
498+
key := a.moduleUniqueKey()
499+
if existing, exists := m.Actions[key]; exists {
500+
diags = append(diags, &hcl.Diagnostic{
501+
Severity: hcl.DiagError,
502+
Summary: fmt.Sprintf("Duplicate action %q configuration", existing.Type),
503+
Detail: fmt.Sprintf("An action named %q was already declared at %s. Resource names must be unique per type in each module.", existing.Name, existing.DeclRange),
504+
Subject: &a.DeclRange,
505+
})
506+
continue
507+
}
508+
m.Actions[key] = a
509+
510+
// set the provider FQN for the action
511+
if a.ProviderConfigRef != nil {
512+
a.Provider = m.ProviderForLocalConfig(a.ProviderConfigAddr())
513+
} else {
514+
// an invalid resource name (for e.g. "null resource" instead of
515+
// "null_resource") can cause a panic down the line in addrs:
516+
// https://github.com/hashicorp/terraform/issues/25560
517+
implied, err := addrs.ParseProviderPart(a.Addr().ImpliedProvider())
518+
if err == nil {
519+
a.Provider = m.ImpliedProviderForUnqualifiedType(implied)
520+
}
521+
// We don't return a diagnostic because the invalid resource name
522+
// will already have been caught.
523+
}
524+
}
525+
494526
return diags
495527
}
496528

@@ -681,6 +713,22 @@ func (m *Module) mergeFile(file *File) hcl.Diagnostics {
681713
})
682714
}
683715

716+
for _, a := range file.Actions {
717+
key := a.moduleUniqueKey()
718+
existing, exists := m.Actions[key]
719+
if !exists {
720+
diags = append(diags, &hcl.Diagnostic{
721+
Severity: hcl.DiagError,
722+
Summary: "Missing resource to override",
723+
Detail: fmt.Sprintf("There is no action named %q. An override file can only override a resource block defined in a primary configuration file.", a.Name),
724+
Subject: &a.DeclRange,
725+
})
726+
continue
727+
}
728+
mergeDiags := existing.merge(a, m.ProviderRequirements.RequiredProviders)
729+
diags = append(diags, mergeDiags...)
730+
}
731+
684732
return diags
685733
}
686734

0 commit comments

Comments
 (0)