actions: make action address targetable (#37499)

* actions: make action address targetable

* add missing functions

* copyright headers
pull/37512/head^2
Liam Cervante 9 months ago committed by GitHub
parent 8b65426ecf
commit 4ed8668a8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -140,6 +140,7 @@ func (a ActionInstance) Absolute(module ModuleInstance) AbsActionInstance {
// AbsAction is an absolute address for an action under a given module path.
type AbsAction struct {
targetable
Module ModuleInstance
Action Action
}
@ -172,6 +173,23 @@ func (a AbsAction) Config() ConfigAction {
}
}
// TargetContains implements Targetable
func (a AbsAction) TargetContains(other Targetable) bool {
switch to := other.(type) {
case AbsAction:
return a.Equal(to)
case AbsActionInstance:
return a.Equal(to.ContainingAction())
default:
return false
}
}
// AddrType implements Targetable
func (a AbsAction) AddrType() TargetableAddrType {
return ActionAddrType
}
func (a AbsAction) String() string {
if len(a.Module) == 0 {
return a.Action.String()
@ -179,11 +197,6 @@ func (a AbsAction) String() string {
return fmt.Sprintf("%s.%s", a.Module.String(), a.Action.String())
}
// AffectedAbsAction returns the AbsAction.
func (a AbsAction) AffectedAbsAction() AbsAction {
return a
}
func (a AbsAction) Equal(o AbsAction) bool {
return a.Module.Equal(o.Module) && a.Action.Equal(o.Action)
}
@ -211,6 +224,7 @@ func (a AbsAction) UniqueKey() UniqueKey {
// AbsActionInstance is an absolute address for an action instance under a
// given module path.
type AbsActionInstance struct {
targetable
Module ModuleInstance
Action ActionInstance
}
@ -255,14 +269,23 @@ func (a AbsActionInstance) String() string {
return fmt.Sprintf("%s.%s", a.Module.String(), a.Action.String())
}
// AffectedAbsAction returns the AbsAction for the instance.
func (a AbsActionInstance) AffectedAbsAction() AbsAction {
return AbsAction{
Module: a.Module,
Action: a.Action.Action,
// TargetContains implements Targetable
func (a AbsActionInstance) TargetContains(other Targetable) bool {
switch to := other.(type) {
case AbsAction:
return to.Equal(a.ContainingAction()) && a.Action.Key == NoKey
case AbsActionInstance:
return to.Equal(a)
default:
return false
}
}
// AddrType implements Targetable
func (a AbsActionInstance) AddrType() TargetableAddrType {
return ActionInstanceAddrType
}
func (a AbsActionInstance) Equal(o AbsActionInstance) bool {
return a.Module.Equal(o.Module) && a.Action.Equal(o.Action)
}

@ -562,6 +562,10 @@ func (t *Target) ModuleAddr() ModuleInstance {
return addr.Module
case AbsResource:
return addr.Module
case AbsAction:
return addr.Module
case AbsActionInstance:
return addr.Module
default:
// The above cases should be exhaustive for all
// implementations of Targetable.

@ -0,0 +1,162 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package addrs
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// ParseTargetAction attempts to interpret the given traversal as a targetable
// action address. The given traversal must be absolute, or this function will
// panic.
//
// If no error diagnostics are returned, the returned target includes the
// address that was extracted and the source range it was extracted from.
//
// If error diagnostics are returned then the Target value is invalid and
// must not be used.
//
// This function matches the behaviour of ParseTarget, except we are ensuring
// the caller is explicit about what kind of target they want to get. We prevent
// callers accidentally including action targets where they shouldn't be
// accessible by keeping these methods separate.
func ParseTargetAction(traversal hcl.Traversal) (*Target, tfdiags.Diagnostics) {
path, remain, diags := parseModuleInstancePrefix(traversal, false)
if diags.HasErrors() {
return nil, diags
}
if len(remain) == 0 {
return nil, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "Action addresses must contain an action reference after the module reference.",
Subject: traversal.SourceRange().Ptr(),
})
}
target, moreDiags := parseActionInstanceUnderModule(path, remain, tfdiags.SourceRangeFromHCL(traversal.SourceRange()))
return target, diags.Append(moreDiags)
}
// ParseTargetActionStr is a helper wrapper around ParseTargetAction that takes
// a string and parses it into HCL before interpreting it.
//
// All the same cautions apply to this as with the equivalent ParseTargetStr.
func ParseTargetActionStr(str string) (*Target, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1})
diags = diags.Append(parseDiags)
if parseDiags.HasErrors() {
return nil, diags
}
target, targetDiags := ParseTargetAction(traversal)
diags = diags.Append(targetDiags)
return target, diags
}
func parseActionInstanceUnderModule(moduleAddr ModuleInstance, remain hcl.Traversal, srcRng tfdiags.SourceRange) (*Target, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
if remain.RootName() != "action" {
return nil, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "Action specification must start with `action`.",
Subject: remain.SourceRange().Ptr(),
})
}
remain = remain[1:]
if len(remain) < 2 {
return nil, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "Action specification must include an action type and name.",
Subject: remain.SourceRange().Ptr(),
})
}
var typeName, name string
switch tt := remain[0].(type) {
case hcl.TraverseRoot:
typeName = tt.Name
case hcl.TraverseAttr:
typeName = tt.Name
default:
return nil, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "Action type is required.",
Subject: remain[0].SourceRange().Ptr(),
})
}
switch tt := remain[1].(type) {
case hcl.TraverseAttr:
name = tt.Name
default:
return nil, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "An action name is required.",
Subject: remain[1].SourceRange().Ptr(),
})
}
remain = remain[2:]
switch len(remain) {
case 0:
return &Target{
Subject: moduleAddr.Action(typeName, name),
SourceRange: srcRng,
}, diags
case 1:
switch tt := remain[0].(type) {
case hcl.TraverseIndex:
key, err := ParseInstanceKey(tt.Key)
if err != nil {
return nil, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: fmt.Sprintf("Invalid action instance key: %s.", err),
Subject: remain[0].SourceRange().Ptr(),
})
}
return &Target{
Subject: moduleAddr.ActionInstance(typeName, name, key),
SourceRange: srcRng,
}, diags
case hcl.TraverseSplat:
return nil, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "Action instance key must be given in square brackets.",
Subject: remain[0].SourceRange().Ptr(),
})
default:
return nil, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "Action instance key must be given in square brackets.",
Subject: remain[0].SourceRange().Ptr(),
})
}
default:
return nil, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid address",
Detail: "Unexpected extra operators after address.",
Subject: remain[1].SourceRange().Ptr(),
})
}
}

@ -0,0 +1,187 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package addrs
import (
"testing"
"github.com/go-test/deep"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/internal/tfdiags"
)
func TestParseTargetAction(t *testing.T) {
tcs := []struct {
Input string
Want *Target
WantErr string
}{
{
Input: "action.action_type.action_name",
Want: &Target{
Subject: AbsAction{
Action: Action{
Type: "action_type",
Name: "action_name",
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 31, Byte: 30},
},
},
},
{
Input: "action.action_type.action_name[0]",
Want: &Target{
Subject: AbsActionInstance{
Action: ActionInstance{
Action: Action{
Type: "action_type",
Name: "action_name",
},
Key: IntKey(0),
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 34, Byte: 33},
},
},
},
{
Input: "module.module_name.action.action_type.action_name",
Want: &Target{
Subject: AbsAction{
Module: ModuleInstance{
{
Name: "module_name",
},
},
Action: Action{
Type: "action_type",
Name: "action_name",
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 50, Byte: 49},
},
},
},
{
Input: "module.module_name.action.action_type.action_name[0]",
Want: &Target{
Subject: AbsActionInstance{
Module: ModuleInstance{
{
Name: "module_name",
},
},
Action: ActionInstance{
Action: Action{
Type: "action_type",
Name: "action_name",
},
Key: IntKey(0),
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 53, Byte: 52},
},
},
},
{
Input: "module.module_name[0].action.action_type.action_name",
Want: &Target{
Subject: AbsAction{
Module: ModuleInstance{
{
Name: "module_name",
InstanceKey: IntKey(0),
},
},
Action: Action{
Type: "action_type",
Name: "action_name",
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 53, Byte: 52},
},
},
},
{
Input: "module.module_name[0].action.action_type.action_name[0]",
Want: &Target{
Subject: AbsActionInstance{
Module: ModuleInstance{
{
Name: "module_name",
InstanceKey: IntKey(0),
},
},
Action: ActionInstance{
Action: Action{
Type: "action_type",
Name: "action_name",
},
Key: IntKey(0),
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 56, Byte: 55},
},
},
},
{
Input: "module.module_name",
WantErr: "Action addresses must contain an action reference after the module reference.",
},
{
Input: "module.module_name.resource_type.resource_name",
WantErr: "Action specification must start with `action`.",
},
}
for _, test := range tcs {
t.Run(test.Input, func(t *testing.T) {
traversal, travDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.Pos{Line: 1, Column: 1})
if travDiags.HasErrors() {
t.Fatal(travDiags.Error())
}
got, diags := ParseTargetAction(traversal)
switch len(diags) {
case 0:
if test.WantErr != "" {
t.Fatalf("succeeded; want error: %s", test.WantErr)
}
case 1:
if test.WantErr == "" {
t.Fatalf("unexpected diagnostics: %s", diags.Err())
}
if got, want := diags[0].Description().Detail, test.WantErr; got != want {
t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want)
}
default:
t.Fatalf("too many diagnostics: %s", diags.Err())
}
if diags.HasErrors() {
return
}
for _, problem := range deep.Equal(got, test.Want) {
t.Error(problem)
}
})
}
}

@ -42,4 +42,6 @@ const (
AbsResourceAddrType
ModuleAddrType
ModuleInstanceAddrType
ActionAddrType
ActionInstanceAddrType
)

Loading…
Cancel
Save