mirror of https://github.com/hashicorp/terraform
actions: make action address targetable (#37499)
* actions: make action address targetable * add missing functions * copyright headerspull/37512/head^2
parent
8b65426ecf
commit
4ed8668a8f
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in new issue