mirror of
https://github.com/hashicorp/terraform.git
synced 2026-06-09 00:42:48 -04:00
actions: make action address targetable (#37499)
* actions: make action address targetable * add missing functions * copyright headers
This commit is contained in:
parent
8b65426ecf
commit
4ed8668a8f
5 changed files with 388 additions and 10 deletions
|
|
@ -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.
|
||||
|
|
|
|||
162
internal/addrs/parse_target_action.go
Normal file
162
internal/addrs/parse_target_action.go
Normal file
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
187
internal/addrs/parse_target_action_test.go
Normal file
187
internal/addrs/parse_target_action_test.go
Normal file
|
|
@ -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…
Reference in a new issue