Skip to content

Commit db63709

Browse files
committed
properly parse actions during configuration loading
1 parent 423cacd commit db63709

File tree

3 files changed

+262
-109
lines changed

3 files changed

+262
-109
lines changed

internal/command/jsonconfig/expression.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ func marshalExpression(ex hcl.Expression) expression {
5353
var varString []string
5454
for _, ref := range refs {
5555
// We work backwards here, starting with the full reference +
56-
// reamining traversal, and then unwrapping the remaining traversals
57-
// into parts until we end up at the smallest referencable address.
56+
// remaining traversal, and then unwrapping the remaining traversals
57+
// into parts until we end up at the smallest referenceable address.
5858
remains := ref.Remaining
5959
for len(remains) > 0 {
6060
varString = append(varString, fmt.Sprintf("%s%s", ref.Subject, traversalStr(remains)))
@@ -76,6 +76,11 @@ func marshalExpression(ex hcl.Expression) expression {
7676
case addrs.ModuleCallInstanceOutput:
7777
// Include the module name, without the output name
7878
varString = append(varString, ref.Subject.(addrs.ModuleCallInstanceOutput).Call.String())
79+
case addrs.ActionInstance:
80+
if ref.Subject.(addrs.ActionInstance).Key != addrs.NoKey {
81+
// Include the action, without the key
82+
varString = append(varString, ref.Subject.(addrs.ActionInstance).Action.String())
83+
}
7984
}
8085
}
8186
ret.References = varString

internal/configs/action.go

Lines changed: 224 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,37 @@ import (
1010
"github.com/hashicorp/hcl/v2/hclsyntax"
1111

1212
"github.com/hashicorp/terraform/internal/addrs"
13+
"github.com/hashicorp/terraform/internal/lang/langrefs"
14+
"github.com/hashicorp/terraform/internal/tfdiags"
1315
)
1416

17+
// There are many ways of handling plurality in error messages (linked_resource
18+
// vs linked_resources); this is one of them.
19+
type diagFn func(*hcl.Range) *hcl.Diagnostic
20+
1521
func invalidLinkedResourceDiag(subj *hcl.Range) *hcl.Diagnostic {
1622
return &hcl.Diagnostic{
1723
Severity: hcl.DiagError,
18-
Summary: `Invalid "linked_resource"`,
19-
Detail: `"linked_resource" must only refer to a managed resource in the current module.`,
24+
Summary: `Invalid linked_resource`,
25+
Detail: `linked_resource must only refer to a managed resource in the current module.`,
2026
Subject: subj,
2127
}
2228
}
2329

2430
func invalidLinkedResourcesDiag(subj *hcl.Range) *hcl.Diagnostic {
2531
return &hcl.Diagnostic{
2632
Severity: hcl.DiagError,
27-
Summary: `Invalid "linked_resources"`,
28-
Detail: `"linked_resources" must only refer to managed resources in the current module.`,
33+
Summary: `Invalid linked_resources`,
34+
Detail: `linked_resources must only refer to managed resources in the current module.`,
35+
Subject: subj,
36+
}
37+
}
38+
39+
func invalidActionDiag(subj *hcl.Range) *hcl.Diagnostic {
40+
return &hcl.Diagnostic{
41+
Severity: hcl.DiagError,
42+
Summary: `Invalid action argument inside action_triggers`,
43+
Detail: `action_triggers.actions must only refer to actions in the current module.`,
2944
Subject: subj,
3045
}
3146
}
@@ -39,7 +54,7 @@ type Action struct {
3954
ForEach hcl.Expression
4055
// DependsOn []hcl.Traversal // not yet supported
4156

42-
LinkedResources []hcl.Traversal
57+
LinkedResources []hcl.Expression
4358

4459
ProviderConfigRef *ProviderConfigRef
4560
Provider addrs.Provider
@@ -76,8 +91,7 @@ const (
7691

7792
// ActionRef represents a reference to a configured Action
7893
type ActionRef struct {
79-
Traversal hcl.Traversal
80-
94+
Expr hcl.Expression
8195
Range hcl.Range
8296
}
8397

@@ -142,37 +156,9 @@ func decodeActionTriggerBlock(block *hcl.Block) (*ActionTrigger, hcl.Diagnostics
142156
}
143157

144158
if attr, exists := content.Attributes["actions"]; exists {
145-
exprs, ediags := hcl.ExprList(attr.Expr)
159+
actionRefs, ediags := decodeActionTriggerRef(attr.Expr)
146160
diags = append(diags, ediags...)
147-
actions := []ActionRef{}
148-
for _, expr := range exprs {
149-
traversal, travDiags := hcl.AbsTraversalForExpr(expr)
150-
diags = append(diags, travDiags...)
151-
152-
if len(traversal) > 0 {
153-
// verify that the traversal is an action
154-
ref, refDiags := addrs.ParseRef(traversal)
155-
diags = append(diags, refDiags.ToHCL()...)
156-
157-
switch ref.Subject.(type) {
158-
case addrs.ActionInstance, addrs.Action:
159-
actionRef := ActionRef{
160-
Traversal: traversal,
161-
Range: expr.Range(),
162-
}
163-
actions = append(actions, actionRef)
164-
default:
165-
diags = append(diags, &hcl.Diagnostic{
166-
Severity: hcl.DiagError,
167-
Summary: "Invalid actions argument inside action_triggers",
168-
Detail: "action_triggers.actions accepts a list of one or more actions, which must be in the current module.",
169-
Subject: expr.Range().Ptr(),
170-
})
171-
continue
172-
}
173-
}
174-
}
175-
a.Actions = actions
161+
a.Actions = actionRefs
176162
}
177163

178164
if len(a.Actions) == 0 {
@@ -250,30 +236,9 @@ func decodeActionBlock(block *hcl.Block) (*Action, hcl.Diagnostics) {
250236
Subject: &attr.NameRange,
251237
})
252238
}
253-
254-
traversal, travDiags := hcl.AbsTraversalForExpr(attr.Expr)
255-
diags = append(diags, travDiags...)
256-
if len(traversal) != 0 {
257-
ref, refDiags := addrs.ParseRef(traversal)
258-
diags = append(diags, refDiags.ToHCL()...)
259-
260-
switch res := ref.Subject.(type) {
261-
case addrs.Resource:
262-
if res.Mode != addrs.ManagedResourceMode {
263-
diags = append(diags, invalidLinkedResourceDiag(&attr.NameRange))
264-
} else {
265-
a.LinkedResources = []hcl.Traversal{traversal}
266-
}
267-
case addrs.ResourceInstance:
268-
if res.Resource.Mode != addrs.ManagedResourceMode {
269-
diags = append(diags, invalidLinkedResourceDiag(&attr.NameRange))
270-
} else {
271-
a.LinkedResources = []hcl.Traversal{traversal}
272-
}
273-
default:
274-
diags = append(diags, invalidLinkedResourceDiag(&attr.NameRange))
275-
}
276-
}
239+
lr, lrDiags := decodeLinkedResource(attr.Expr)
240+
diags = append(diags, lrDiags...)
241+
a.LinkedResources = []hcl.Expression{lr}
277242
}
278243

279244
if attr, exists := content.Attributes["linked_resources"]; exists {
@@ -286,29 +251,9 @@ func decodeActionBlock(block *hcl.Block) (*Action, hcl.Diagnostics) {
286251
})
287252
}
288253

289-
exprs, exprDiags := hcl.ExprList(attr.Expr)
290-
diags = append(diags, exprDiags...)
291-
292-
if len(exprs) > 0 {
293-
lrs := make([]hcl.Traversal, 0, len(exprs))
294-
for _, expr := range exprs {
295-
traversal, travDiags := hcl.AbsTraversalForExpr(expr)
296-
diags = append(diags, travDiags...)
297-
298-
if len(traversal) != 0 {
299-
ref, refDiags := addrs.ParseRef(traversal)
300-
diags = append(diags, refDiags.ToHCL()...)
301-
302-
switch ref.Subject.(type) {
303-
case addrs.Resource, addrs.ResourceInstance:
304-
lrs = append(lrs, traversal)
305-
default:
306-
diags = append(diags, invalidLinkedResourcesDiag(&attr.NameRange))
307-
}
308-
}
309-
}
310-
a.LinkedResources = lrs
311-
}
254+
lrs, lrDiags := decodeLinkedResources(attr.Expr)
255+
diags = append(diags, lrDiags...)
256+
a.LinkedResources = lrs
312257
}
313258

314259
for _, block := range content.Blocks {
@@ -422,3 +367,198 @@ func (a *Action) ProviderConfigAddr() addrs.LocalProviderConfig {
422367
Alias: a.ProviderConfigRef.Alias,
423368
}
424369
}
370+
371+
// decodeActionTriggerRef decodes and does basic validation of the Actions
372+
// expression list inside a resource's ActionTrigger block, ensuring each only
373+
// reference a single action. This function was largely copied from
374+
// decodeReplaceTriggeredBy, but is much more permissive in what References are
375+
// allowed.
376+
func decodeActionTriggerRef(expr hcl.Expression) ([]ActionRef, hcl.Diagnostics) {
377+
exprs, diags := hcl.ExprList(expr)
378+
if diags.HasErrors() {
379+
return nil, diags
380+
}
381+
actionRefs := make([]ActionRef, len(exprs))
382+
383+
for i, expr := range exprs {
384+
// Since we are manually parsing the action_trigger.Actions argument, we
385+
// need to specially handle json configs, in which case the values will
386+
// be json strings rather than hcl. To simplify parsing however we will
387+
// decode the individual list elements, rather than the entire
388+
// expression.
389+
var jsDiags hcl.Diagnostics
390+
expr, jsDiags = unwrapJSONRefExpr(expr)
391+
diags = diags.Extend(jsDiags)
392+
if diags.HasErrors() {
393+
continue
394+
}
395+
actionRefs[i] = ActionRef{
396+
Expr: expr,
397+
Range: expr.Range(),
398+
}
399+
400+
refs, refDiags := langrefs.ReferencesInExpr(addrs.ParseRef, expr)
401+
for _, diag := range refDiags {
402+
severity := hcl.DiagError
403+
if diag.Severity() == tfdiags.Warning {
404+
severity = hcl.DiagWarning
405+
}
406+
407+
diags = append(diags, &hcl.Diagnostic{
408+
Severity: severity,
409+
Summary: diag.Description().Summary,
410+
Detail: diag.Description().Detail,
411+
Subject: expr.Range().Ptr(),
412+
})
413+
}
414+
415+
if refDiags.HasErrors() {
416+
continue
417+
}
418+
419+
actionCount := 0
420+
for _, ref := range refs {
421+
switch ref.Subject.(type) {
422+
case addrs.Action, addrs.ActionInstance:
423+
actionCount++
424+
case addrs.ModuleCall, addrs.ModuleCallInstance, addrs.ModuleCallInstanceOutput:
425+
diags = append(diags, &hcl.Diagnostic{
426+
Severity: hcl.DiagError,
427+
Summary: "Invalid reference to action outside this module",
428+
Detail: "Actions can only be referenced in the module they are declared in.",
429+
Subject: expr.Range().Ptr(),
430+
})
431+
continue
432+
case addrs.Resource, addrs.ResourceInstance:
433+
// definitely not an action
434+
diags = append(diags, invalidActionDiag(expr.Range().Ptr()))
435+
continue
436+
default:
437+
// we've checked what we can
438+
}
439+
}
440+
441+
switch {
442+
case actionCount == 0:
443+
diags = append(diags, &hcl.Diagnostic{
444+
Severity: hcl.DiagError,
445+
Summary: "No actions specified",
446+
Detail: "At least one action must be specified for an action_trigger.",
447+
Subject: expr.Range().Ptr(),
448+
})
449+
case actionCount > 1:
450+
diags = append(diags, &hcl.Diagnostic{
451+
Severity: hcl.DiagError,
452+
Summary: "Invalid action expression",
453+
Detail: "Multiple action references in actions expression.",
454+
Subject: expr.Range().Ptr(),
455+
})
456+
}
457+
458+
}
459+
460+
return actionRefs, diags
461+
}
462+
463+
// decodeLinkedResources decodes and does basic validation of an Action's
464+
// LinkedResources.
465+
func decodeLinkedResources(expr hcl.Expression) ([]hcl.Expression, hcl.Diagnostics) {
466+
exprs, diags := hcl.ExprList(expr)
467+
if diags.HasErrors() {
468+
return nil, diags
469+
}
470+
471+
for i, expr := range exprs {
472+
// We are manually parsing config, so we need to handle json configs, in
473+
// which case the values will be json strings rather than hcl.
474+
var jsDiags hcl.Diagnostics
475+
expr, jsDiags = unwrapJSONRefExpr(expr)
476+
diags = diags.Extend(jsDiags)
477+
if diags.HasErrors() {
478+
continue
479+
}
480+
481+
// re-assign the value in case it was modified by unwrapJSONRefExpr
482+
exprs[i] = expr
483+
484+
_, lrDiags := decodeUnwrappedLinkedResource(expr, invalidLinkedResourcesDiag)
485+
diags = append(diags, lrDiags...)
486+
487+
}
488+
489+
return exprs, diags
490+
}
491+
492+
func decodeLinkedResource(expr hcl.Expression) (hcl.Expression, hcl.Diagnostics) {
493+
// Handle possible json configs
494+
expr, diags := unwrapJSONRefExpr(expr)
495+
if diags.HasErrors() {
496+
return expr, diags
497+
}
498+
499+
return decodeUnwrappedLinkedResource(expr, invalidLinkedResourceDiag)
500+
}
501+
502+
func decodeUnwrappedLinkedResource(expr hcl.Expression, diagFunc diagFn) (hcl.Expression, hcl.Diagnostics) {
503+
var diags hcl.Diagnostics
504+
505+
refs, refDiags := langrefs.ReferencesInExpr(addrs.ParseRef, expr)
506+
for _, diag := range refDiags {
507+
severity := hcl.DiagError
508+
if diag.Severity() == tfdiags.Warning {
509+
severity = hcl.DiagWarning
510+
}
511+
512+
diags = append(diags, &hcl.Diagnostic{
513+
Severity: severity,
514+
Summary: diag.Description().Summary,
515+
Detail: diag.Description().Detail,
516+
Subject: expr.Range().Ptr(),
517+
})
518+
}
519+
520+
if refDiags.HasErrors() {
521+
return expr, diags
522+
}
523+
524+
resourceCount := 0
525+
for _, ref := range refs {
526+
switch sub := ref.Subject.(type) {
527+
case addrs.ResourceInstance:
528+
if sub.Resource.Mode == addrs.ManagedResourceMode {
529+
diags = append(diags, diagFunc(expr.Range().Ptr()))
530+
} else {
531+
resourceCount++
532+
}
533+
case addrs.Resource:
534+
if sub.Mode != addrs.ManagedResourceMode {
535+
diags = append(diags, diagFunc(expr.Range().Ptr()))
536+
} else {
537+
resourceCount++
538+
}
539+
case addrs.ModuleCall, addrs.ModuleCallInstance, addrs.ModuleCallInstanceOutput:
540+
diags = append(diags, diagFunc(expr.Range().Ptr()))
541+
default:
542+
// we've checked what we can without evaluating references!
543+
}
544+
}
545+
546+
switch {
547+
case resourceCount == 0:
548+
diags = append(diags, &hcl.Diagnostic{
549+
Severity: hcl.DiagError,
550+
Summary: "Invalid linked_resource expression",
551+
Detail: "Missing resource reference in linked_resource expression.",
552+
Subject: expr.Range().Ptr(),
553+
})
554+
case resourceCount > 1:
555+
diags = append(diags, &hcl.Diagnostic{
556+
Severity: hcl.DiagError,
557+
Summary: "Invalid linked_resource expression",
558+
Detail: "Multiple resource references in linked_resource expression.",
559+
Subject: expr.Range().Ptr(),
560+
})
561+
}
562+
563+
return expr, diags
564+
}

0 commit comments

Comments
 (0)