diff --git a/internal/addrs/action.go b/internal/addrs/action.go index c9cac36fe832..3fdc951597df 100644 --- a/internal/addrs/action.go +++ b/internal/addrs/action.go @@ -382,117 +382,7 @@ func ParseAbsActionInstanceStr(str string) (AbsActionInstance, tfdiags.Diagnosti // If error diagnostics are returned then the AbsResource value is invalid and // must not be used. func ParseAbsActionInstance(traversal hcl.Traversal) (AbsActionInstance, tfdiags.Diagnostics) { - moduleAddr, remain, diags := parseModuleInstancePrefix(traversal, false) - if diags.HasErrors() { - return AbsActionInstance{}, diags - } - - if remain.IsRelative() { - // (relative means that there's either nothing left or what's next isn't an identifier) - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid action address", - Detail: "Module path must be followed by an action instance address.", - Subject: remain.SourceRange().Ptr(), - }) - return AbsActionInstance{}, diags - } - - if remain.RootName() != "action" { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "Action address must start with \"action.\".", - Subject: remain[0].SourceRange().Ptr(), - }) - return AbsActionInstance{}, diags - } - remain = remain[1:] - - if len(remain) < 2 { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "Action specification must include an action type and name.", - Subject: remain.SourceRange().Ptr(), - }) - return AbsActionInstance{}, diags - } - - var actionType, name string - switch tt := remain[0].(type) { - case hcl.TraverseRoot: - actionType = tt.Name - case hcl.TraverseAttr: - actionType = tt.Name - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid action address", - Detail: "An action name is required.", - Subject: remain[0].SourceRange().Ptr(), - }) - return AbsActionInstance{}, diags - } - - switch tt := remain[1].(type) { - case hcl.TraverseAttr: - name = tt.Name - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "An action name is required.", - Subject: remain[1].SourceRange().Ptr(), - }) - return AbsActionInstance{}, diags - } - - remain = remain[2:] - switch len(remain) { - case 0: - return moduleAddr.ActionInstance(actionType, name, NoKey), diags - case 1: - switch tt := remain[0].(type) { - case hcl.TraverseIndex: - key, err := ParseInstanceKey(tt.Key) - if err != nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: fmt.Sprintf("Invalid resource instance key: %s.", err), - Subject: remain[0].SourceRange().Ptr(), - }) - return AbsActionInstance{}, diags - } - return moduleAddr.ActionInstance(actionType, name, key), diags - case hcl.TraverseSplat: - // Not yet supported! - diags = 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(), - }) - return AbsActionInstance{}, diags - default: - diags = 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(), - }) - return AbsActionInstance{}, diags - } - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "Unexpected extra operators after address.", - Subject: remain[1].SourceRange().Ptr(), - }) - return AbsActionInstance{}, diags - } + return parseTargetAction(traversal, knownInstanceKeys) } // ParseAbsAction attempts to interpret the given traversal as an absolute diff --git a/internal/addrs/address_expr.go b/internal/addrs/address_expr.go new file mode 100644 index 000000000000..5f6515da71cc --- /dev/null +++ b/internal/addrs/address_expr.go @@ -0,0 +1,100 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" +) + +// AddressExpr is an expression referring to an address, in which each +// instance key can be given by an arbitrary expression rather than only by a +// constant, such as aws_instance.example[each.key]. +// +// An AddressExpr has the same steps as the traversal for the address, except +// that each instance key given by an expression is kept as that expression +// for the caller to evaluate. +type AddressExpr []AddressExprStep + +// AddressExprStep is a single step of an AddressExpr, which is either a +// static traversal step or an instance key given by an expression. +type AddressExprStep struct { + // Step is the static traversal step, when Key is nil. + Step hcl.Traverser + + // Key is the expression for an instance key which isn't a constant. + Key hcl.Expression +} + +// ParseAddressExpr returns the AddressExpr for the given expression. +// +// Expressions which aren't from the HCL native syntax must be static +// traversals, without any instance keys given by expressions. +func ParseAddressExpr(expr hcl.Expression) (AddressExpr, hcl.Diagnostics) { + switch e := expr.(type) { + case *hclsyntax.RelativeTraversalExpr: + ret, diags := ParseAddressExpr(e.Source) + if diags.HasErrors() { + return nil, diags + } + return ret.appendTraversal(e.Traversal), diags + + case *hclsyntax.ScopeTraversalExpr: + return AddressExpr(nil).appendTraversal(e.Traversal), nil + + case *hclsyntax.IndexExpr: + ret, diags := ParseAddressExpr(e.Collection) + if diags.HasErrors() { + return nil, diags + } + return append(ret, AddressExprStep{Key: e.Key}), diags + + default: + traversal, diags := hcl.AbsTraversalForExpr(expr) + if diags.HasErrors() { + return nil, diags + } + return AddressExpr(nil).appendTraversal(traversal), diags + } +} + +func (e AddressExpr) appendTraversal(traversal hcl.Traversal) AddressExpr { + for _, step := range traversal { + e = append(e, AddressExprStep{Step: step}) + } + return e +} + +// Traversal returns the traversal for the address, where each instance key +// given by an expression is replaced with the value returned by evalKey. If +// that value is unknown, the key is a wildcard when the traversal is parsed +// as a pattern or partial-expanded address. +// +// If evalKey is nil, each instance key given by an expression is instead a +// wildcard, as if written as [*]. +func (e AddressExpr) Traversal(evalKey func(hcl.Expression) (cty.Value, hcl.Diagnostics)) (hcl.Traversal, hcl.Diagnostics) { + var diags hcl.Diagnostics + ret := make(hcl.Traversal, 0, len(e)) + for _, step := range e { + if step.Key == nil { + ret = append(ret, step.Step) + continue + } + + rng := step.Key.Range() + if evalKey == nil { + ret = append(ret, hcl.TraverseSplat{SrcRange: rng}) + continue + } + + key, moreDiags := evalKey(step.Key) + diags = append(diags, moreDiags...) + if moreDiags.HasErrors() { + return nil, diags + } + ret = append(ret, hcl.TraverseIndex{Key: key, SrcRange: rng}) + } + return ret, diags +} diff --git a/internal/addrs/address_expr_test.go b/internal/addrs/address_expr_test.go new file mode 100644 index 000000000000..5817bd39c88c --- /dev/null +++ b/internal/addrs/address_expr_test.go @@ -0,0 +1,93 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import ( + "testing" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" +) + +func TestAddressExpr(t *testing.T) { + // evalKey evaluates each key with a fixed value for each.key. + evalKey := func(eachKey cty.Value) func(hcl.Expression) (cty.Value, hcl.Diagnostics) { + return func(expr hcl.Expression) (cty.Value, hcl.Diagnostics) { + return expr.Value(&hcl.EvalContext{ + Variables: map[string]cty.Value{ + "each": cty.ObjectVal(map[string]cty.Value{"key": eachKey}), + }, + }) + } + } + + for name, tc := range map[string]struct { + expr string + evalKey func(hcl.Expression) (cty.Value, hcl.Diagnostics) + want string + wantErr string + }{ + "static address": { + expr: `module.m[0].test_instance.foo["a"]`, + want: `module.m[0].test_instance.foo["a"]`, + }, + "evaluated keys": { + expr: `module.m[each.key].test_instance.foo[each.key]`, + evalKey: evalKey(cty.StringVal("a")), + want: `module.m["a"].test_instance.foo["a"]`, + }, + "unknown keys": { + expr: `module.m[each.key].test_instance.foo`, + evalKey: evalKey(cty.UnknownVal(cty.String)), + want: `module.m[*].test_instance.foo`, + }, + "keys without an evaluator": { + expr: `module.m[each.key].module.n[0].test_instance.foo[each.key]`, + want: `module.m[*].module.n[0].test_instance.foo[*]`, + }, + "key evaluation error": { + expr: `test_instance.foo[local.key]`, + evalKey: evalKey(cty.StringVal("a")), + wantErr: "Unknown variable", + }, + "not an address": { + expr: `upper(test_instance.foo)`, + wantErr: "Invalid expression", + }, + } { + t.Run(name, func(t *testing.T) { + expr, hclDiags := hclsyntax.ParseExpression([]byte(tc.expr), "", hcl.InitialPos) + if hclDiags.HasErrors() { + t.Fatal(hclDiags.Error()) + } + + addrExpr, diags := ParseAddressExpr(expr) + var traversal hcl.Traversal + if !diags.HasErrors() { + traversal, diags = addrExpr.Traversal(tc.evalKey) + } + if tc.wantErr != "" { + if !diags.HasErrors() { + t.Fatalf("unexpected success\nwant error: %s", tc.wantErr) + } + if got := diags[0].Summary; got != tc.wantErr { + t.Fatalf("wrong error\ngot: %s\nwant: %s", got, tc.wantErr) + } + return + } + if diags.HasErrors() { + t.Fatal(diags.Error()) + } + + target, targetDiags := ParseTarget(traversal) + if targetDiags.HasErrors() { + t.Fatal(targetDiags.Err()) + } + if got := target.String(); got != tc.want { + t.Errorf("wrong address\ngot: %s\nwant: %s", got, tc.want) + } + }) + } +} diff --git a/internal/addrs/checkable.go b/internal/addrs/checkable.go index 772325c51008..ff38e94a1748 100644 --- a/internal/addrs/checkable.go +++ b/internal/addrs/checkable.go @@ -101,7 +101,7 @@ func ParseCheckableStr(kind CheckableKind, src string) (Checkable, tfdiags.Diagn return nil, diags } - path, remain, diags := parseModuleInstancePrefix(traversal, false) + path, remain, diags := parseModuleInstancePrefix(traversal, knownInstanceKeys) if diags.HasErrors() { return nil, diags } @@ -158,7 +158,7 @@ func ParseCheckableStr(kind CheckableKind, src string) (Checkable, tfdiags.Diagn // might be a resource whose type is literally "output". switch kind { case CheckableResource: - riAddr, moreDiags := parseResourceInstanceUnderModule(path, false, remain) + riAddr, moreDiags := parseResourceInstanceUnderModule(path, knownInstanceKeys, remain) diags = diags.Append(moreDiags) if diags.HasErrors() { return nil, diags diff --git a/internal/addrs/module.go b/internal/addrs/module.go index 7d2c510d2ed8..904fa406fb71 100644 --- a/internal/addrs/module.go +++ b/internal/addrs/module.go @@ -151,92 +151,6 @@ func (m Module) configMoveableSigil() { // fails: in particular if the traversal represents a keyed module instance // address rather than an unkeyed module. func parseModulePrefix(traversal hcl.Traversal) (Module, hcl.Traversal, tfdiags.Diagnostics) { - remain := traversal - var mod Module - var diags tfdiags.Diagnostics - -LOOP: - for len(remain) > 0 { - var next string - switch tt := remain[0].(type) { - case hcl.TraverseRoot: - next = tt.Name - case hcl.TraverseAttr: - next = tt.Name - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address operator", - Detail: "Module address prefix must be followed by dot and then a name.", - Subject: remain[0].SourceRange().Ptr(), - }) - break LOOP - } - - if next != "module" { - break - } - - kwRange := remain[0].SourceRange() - remain = remain[1:] - // If we have the prefix "module" then we should be followed by an - // module call name, as an attribute. - if len(remain) == 0 { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address operator", - Detail: "Prefix \"module.\" must be followed by a module name.", - Subject: &kwRange, - }) - break - } - - var moduleName string - switch tt := remain[0].(type) { - case hcl.TraverseAttr: - moduleName = tt.Name - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address operator", - Detail: "Prefix \"module.\" must be followed by a module name.", - Subject: remain[0].SourceRange().Ptr(), - }) - break LOOP - } - remain = remain[1:] - - if len(remain) > 0 { - if _, ok := remain[0].(hcl.TraverseIndex); ok { - // Then we have a module instance key, which is invalid - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Module instance keys not allowed", - Detail: "Module address must be a module (e.g. \"module.foo\"), not a module instance (e.g. \"module.foo[1]\").", - Subject: remain[0].SourceRange().Ptr(), - }) - break LOOP - } - } - - mod = append(mod, moduleName) - } - - var retRemain hcl.Traversal - if len(remain) > 0 { - retRemain = make(hcl.Traversal, len(remain)) - copy(retRemain, remain) - // The first element here might be either a TraverseRoot or a - // TraverseAttr, depending on whether we had a module address on the - // front. To make life easier for callers, we'll normalize to always - // start with a TraverseRoot. - if tt, ok := retRemain[0].(hcl.TraverseAttr); ok { - retRemain[0] = hcl.TraverseRoot{ - Name: tt.Name, - SrcRange: tt.SrcRange, - } - } - } - - return mod, retRemain, diags + path, remain, diags := parseModuleInstancePrefix(traversal, noInstanceKeys) + return path.Module(), remain, diags } diff --git a/internal/addrs/module_instance.go b/internal/addrs/module_instance.go index c14fedf3a82d..bae9f55418a6 100644 --- a/internal/addrs/module_instance.go +++ b/internal/addrs/module_instance.go @@ -4,13 +4,10 @@ package addrs import ( - "fmt" "strings" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/gocty" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -30,7 +27,7 @@ var ( ) func ParseModuleInstance(traversal hcl.Traversal) (ModuleInstance, tfdiags.Diagnostics) { - mi, remain, diags := parseModuleInstancePrefix(traversal, false) + mi, remain, diags := parseModuleInstancePrefix(traversal, knownInstanceKeys) if len(remain) != 0 { if len(remain) == len(traversal) { diags = diags.Append(&hcl.Diagnostic{ @@ -80,127 +77,6 @@ func ParseModuleInstanceStr(str string) (ModuleInstance, tfdiags.Diagnostics) { return addr, diags } -func parseModuleInstancePrefix(traversal hcl.Traversal, allowPartial bool) (ModuleInstance, hcl.Traversal, tfdiags.Diagnostics) { - remain := traversal - var mi ModuleInstance - var diags tfdiags.Diagnostics - -LOOP: - for len(remain) > 0 { - var next string - switch tt := remain[0].(type) { - case hcl.TraverseRoot: - next = tt.Name - case hcl.TraverseAttr: - next = tt.Name - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address operator", - Detail: "Module address prefix must be followed by dot and then a name.", - Subject: remain[0].SourceRange().Ptr(), - }) - break LOOP - } - - if next != "module" { - break - } - - kwRange := remain[0].SourceRange() - remain = remain[1:] - // If we have the prefix "module" then we should be followed by an - // module call name, as an attribute, and then optionally an index step - // giving the instance key. - if len(remain) == 0 { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address operator", - Detail: "Prefix \"module.\" must be followed by a module name.", - Subject: &kwRange, - }) - break - } - - var moduleName string - switch tt := remain[0].(type) { - case hcl.TraverseAttr: - moduleName = tt.Name - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address operator", - Detail: "Prefix \"module.\" must be followed by a module name.", - Subject: remain[0].SourceRange().Ptr(), - }) - break LOOP - } - remain = remain[1:] - step := ModuleInstanceStep{ - Name: moduleName, - } - - if len(remain) > 0 { - switch idx := remain[0].(type) { - case hcl.TraverseIndex: - remain = remain[1:] - - switch idx.Key.Type() { - case cty.String: - step.InstanceKey = StringKey(idx.Key.AsString()) - case cty.Number: - var idxInt int - err := gocty.FromCtyValue(idx.Key, &idxInt) - if err == nil { - step.InstanceKey = IntKey(idxInt) - } else { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address operator", - Detail: fmt.Sprintf("Invalid module index: %s.", err), - Subject: idx.SourceRange().Ptr(), - }) - } - default: - // Should never happen, because no other types are allowed in traversal indices. - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address operator", - Detail: "Invalid module key: must be either a string or an integer.", - Subject: idx.SourceRange().Ptr(), - }) - } - - case hcl.TraverseSplat: - if allowPartial { - remain = remain[1:] - step.InstanceKey = WildcardKey - } - } - } - - mi = append(mi, step) - } - - var retRemain hcl.Traversal - if len(remain) > 0 { - retRemain = make(hcl.Traversal, len(remain)) - copy(retRemain, remain) - // The first element here might be either a TraverseRoot or a - // TraverseAttr, depending on whether we had a module address on the - // front. To make life easier for callers, we'll normalize to always - // start with a TraverseRoot. - if tt, ok := retRemain[0].(hcl.TraverseAttr); ok { - retRemain[0] = hcl.TraverseRoot{ - Name: tt.Name, - SrcRange: tt.SrcRange, - } - } - } - - return mi, retRemain, diags -} - // UnkeyedInstanceShim is a shim method for converting a Module address to the // equivalent ModuleInstance address that assumes that no modules have // keyed instances. diff --git a/internal/addrs/most_specific.go b/internal/addrs/most_specific.go new file mode 100644 index 000000000000..2430a4732584 --- /dev/null +++ b/internal/addrs/most_specific.go @@ -0,0 +1,29 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +// MostSpecific returns the most specific of the given candidates, which is the +// one contained by all of the others according to contains, which returns +// true if its first argument contains its second. +// +// Each candidate replaces the result so far if the result contains it but it +// doesn't contain the result, so when the candidates aren't all nested within +// each other, earlier candidates take precedence. Callers which need a +// deterministic result must give the candidates in a deterministic order. +// +// The second return value is false if there are no candidates. +func MostSpecific[T any](candidates []T, contains func(a, b T) bool) (T, bool) { + var best T + if len(candidates) == 0 { + return best, false + } + + best = candidates[0] + for _, candidate := range candidates[1:] { + if contains(best, candidate) && !contains(candidate, best) { + best = candidate + } + } + return best, true +} diff --git a/internal/addrs/most_specific_test.go b/internal/addrs/most_specific_test.go new file mode 100644 index 000000000000..03b4739607eb --- /dev/null +++ b/internal/addrs/most_specific_test.go @@ -0,0 +1,58 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import ( + "testing" +) + +func TestMostSpecific(t *testing.T) { + contains := func(a, b TargetPattern) bool { + return a.Contains(b) + } + + for name, tc := range map[string]struct { + candidates []string + want string + }{ + "none": {}, + "one": { + candidates: []string{"module.a"}, + want: "module.a", + }, + "nested": { + candidates: []string{"module.a", "module.a[1].test.b", "module.a[1]", "module.a.test.b"}, + want: "module.a[1].test.b", + }, + "nested in reverse": { + candidates: []string{"module.a[1].test.b", "module.a[1]", "module.a"}, + want: "module.a[1].test.b", + }, + "equivalent": { + // both select every instance, so the first takes precedence + candidates: []string{"module.a", "module.a[*]"}, + want: "module.a", + }, + "not nested": { + // neither contains the other, so the first takes precedence + candidates: []string{"module.a[0].test.b", "module.a[1].test.b"}, + want: "module.a[0].test.b", + }, + } { + t.Run(name, func(t *testing.T) { + var candidates []TargetPattern + for _, s := range tc.candidates { + candidates = append(candidates, mustParseTargetPattern(s).(TargetPattern)) + } + + got, ok := MostSpecific(candidates, contains) + if ok != (tc.want != "") { + t.Fatalf("wrong ok: got %t", ok) + } + if ok && got.String() != tc.want { + t.Errorf("wrong result\ngot: %s\nwant: %s", got, tc.want) + } + }) + } +} diff --git a/internal/addrs/move_endpoint.go b/internal/addrs/move_endpoint.go index ad9c357b2ab2..606631a797ab 100644 --- a/internal/addrs/move_endpoint.go +++ b/internal/addrs/move_endpoint.go @@ -127,7 +127,7 @@ func (e *MoveEndpoint) ConfigMoveable(baseModule Module) ConfigMoveable { // it with the address of the module where it was declared in order to get // an absolute address relative to the root module. func ParseMoveEndpoint(traversal hcl.Traversal) (*MoveEndpoint, tfdiags.Diagnostics) { - path, remain, diags := parseModuleInstancePrefix(traversal, false) + path, remain, diags := parseModuleInstancePrefix(traversal, knownInstanceKeys) if diags.HasErrors() { return nil, diags } @@ -141,7 +141,7 @@ func ParseMoveEndpoint(traversal hcl.Traversal) (*MoveEndpoint, tfdiags.Diagnost }, diags } - riAddr, moreDiags := parseResourceInstanceUnderModule(path, false, remain) + riAddr, moreDiags := parseResourceInstanceUnderModule(path, knownInstanceKeys, remain) diags = diags.Append(moreDiags) if diags.HasErrors() { return nil, diags diff --git a/internal/addrs/move_endpoint_module.go b/internal/addrs/move_endpoint_module.go index ddff2e968af0..8641b976cc2b 100644 --- a/internal/addrs/move_endpoint_module.go +++ b/internal/addrs/move_endpoint_module.go @@ -8,31 +8,9 @@ import ( "reflect" "strings" - "github.com/zclconf/go-cty/cty" - "github.com/hashicorp/terraform/internal/tfdiags" ) -// anyKeyImpl is the InstanceKey representation indicating a wildcard, which -// matches all possible keys. This is only used internally for matching -// combinations of address types, where only portions of the path contain key -// information. -type anyKeyImpl rune - -func (k anyKeyImpl) instanceKeySigil() { -} - -func (k anyKeyImpl) String() string { - return fmt.Sprintf("[%s]", string(k)) -} - -func (k anyKeyImpl) Value() cty.Value { - return cty.StringVal(string(k)) -} - -// anyKey is the only valid value of anyKeyImpl -var anyKey = anyKeyImpl('*') - // MoveEndpointInModule annotates a MoveEndpoint with the address of the // module where it was declared, which is the form we use for resolving // whether move statements chain from or are nested within other move @@ -213,34 +191,25 @@ func (e *MoveEndpointInModule) ModuleCallTraversals() (Module, []ModuleCall) { return e.module, ret } -// synthModuleInstance constructs a module instance out of the module path and -// any module portion of the relSubject, substituting Module and Call segments -// with ModuleInstanceStep using the anyKey value. -// This is only used internally for comparison of these complete paths, but -// does not represent how the individual parts are handled elsewhere in the -// code. -func (e *MoveEndpointInModule) synthModuleInstance() ModuleInstance { - var inst ModuleInstance - - for _, mod := range e.module { - inst = append(inst, ModuleInstanceStep{Name: mod, InstanceKey: anyKey}) - } +// targetShape returns the shape of the objects which the receiver selects. +// The module where the endpoint was declared, and a module call, select every +// one of their instances. +func (e *MoveEndpointInModule) targetShape() targetShape { + module := allModuleInstances(e.module) switch sub := e.relSubject.(type) { case ModuleInstance: - inst = append(inst, sub...) + return append(module, sub...) case AbsModuleCall: - inst = append(inst, sub.Module...) - inst = append(inst, ModuleInstanceStep{Name: sub.Call.Name, InstanceKey: anyKey}) + module = append(module, sub.Module...) + return append(module, ModuleInstanceStep{Name: sub.Call.Name, InstanceKey: WildcardKey}) case AbsResource: - inst = append(inst, sub.Module...) + return sub.Resource.Instance(WildcardKey).Absolute(append(module, sub.Module...)) case AbsResourceInstance: - inst = append(inst, sub.Module...) + return sub.Resource.Absolute(append(module, sub.Module...)) default: panic(fmt.Sprintf("unhandled relative address type %T", sub)) } - - return inst } // SelectsModule returns true if the reciever directly selects either @@ -252,81 +221,21 @@ func (e *MoveEndpointInModule) synthModuleInstance() ModuleInstance { // resource move indicates that we should search each of the resources in // the given module to see if they match. func (e *MoveEndpointInModule) SelectsModule(addr ModuleInstance) bool { - synthInst := e.synthModuleInstance() - - // In order to match the given module instance, our combined path must be - // equal in length. - if len(synthInst) != len(addr) { - return false - } - - for i, step := range synthInst { - switch step.InstanceKey { - case anyKey: - // we can match any key as long as the name matches - if step.Name != addr[i].Name { - return false - } - default: - if step != addr[i] { - return false - } - } - } - return true + module := e.targetShape().shapeModule() + return len(module) == len(addr) && shapeContains(module, addr) } // SelectsResource returns true if the receiver directly selects either // the given resource or one of its instances. func (e *MoveEndpointInModule) SelectsResource(addr AbsResource) bool { - // Only a subset of subject types can possibly select a resource, so - // we'll take care of those quickly before we do anything more expensive. - switch e.relSubject.(type) { - case AbsResource, AbsResourceInstance: - // okay - default: - return false // can't possibly match - } - - if !e.SelectsModule(addr.Module) { + shape, ok := e.targetShape().(AbsResourceInstance) + if !ok { return false } - // If we get here then we know the module part matches, so we only need - // to worry about the relative resource part. - switch relSubject := e.relSubject.(type) { - case AbsResource: - return addr.Resource.Equal(relSubject.Resource) - case AbsResourceInstance: - // We intentionally ignore the instance key, because we consider - // instances to be part of the resource they belong to. - return addr.Resource.Equal(relSubject.Resource.Resource) - default: - // We should've filtered out all other types above - panic(fmt.Sprintf("unsupported relSubject type %T", relSubject)) - } -} - -// moduleInstanceCanMatch indicates that modA can match modB taking into -// account steps with an anyKey InstanceKey as wildcards. The comparison of -// wildcard steps is done symmetrically, because varying portions of either -// instance's path could have been derived from configuration vs evaluation. -// The length of modA must be equal or shorter than the length of modB. -func moduleInstanceCanMatch(modA, modB ModuleInstance) bool { - for i, step := range modA { - switch { - case step.InstanceKey == anyKey || modB[i].InstanceKey == anyKey: - // we can match any key as long as the names match - if step.Name != modB[i].Name { - return false - } - default: - if step != modB[i] { - return false - } - } - } - return true + // We intentionally ignore the instance key, because we consider + // instances to be part of the resource they belong to. + return shapeContains(shape.ContainingResource().Instance(WildcardKey), addr.Instance(WildcardKey)) } // CanChainFrom returns true if the reciever describes an address that could @@ -336,100 +245,52 @@ func moduleInstanceCanMatch(modA, modB ModuleInstance) bool { // the reciever is the "to" from one statement and the other given address // is the "from" of another statement. func (e *MoveEndpointInModule) CanChainFrom(other *MoveEndpointInModule) bool { - eMod := e.synthModuleInstance() - oMod := other.synthModuleInstance() - - // if the complete paths are different lengths, these cannot refer to the - // same value. - if len(eMod) != len(oMod) { + eShape := e.targetShape() + oShape := other.targetShape() + + // The endpoints must both select either modules or resources, in modules + // at the same depth, where a module call and a module instance are both + // considered to be a module. + _, eResource := eShape.(AbsResourceInstance) + _, oResource := oShape.(AbsResourceInstance) + if len(eShape.shapeModule()) != len(oShape.shapeModule()) || eResource != oResource { return false } - if !moduleInstanceCanMatch(oMod, eMod) { + // A whole resource can only chain with a whole resource, and a resource + // instance with a resource instance. + if eResource && (shapeKey(eShape) == WildcardKey) != (shapeKey(oShape) == WildcardKey) { return false } - eSub := e.relSubject - oSub := other.relSubject - - switch oSub := oSub.(type) { - case AbsModuleCall, ModuleInstance: - switch eSub.(type) { - case AbsModuleCall, ModuleInstance: - // we already know the complete module path including any final - // module call name is equal. - return true - } - - case AbsResource: - switch eSub := eSub.(type) { - case AbsResource: - return eSub.Resource.Equal(oSub.Resource) - } - - case AbsResourceInstance: - switch eSub := eSub.(type) { - case AbsResourceInstance: - return eSub.Resource.Equal(oSub.Resource) - } - } - - return false + return shapeCouldContain(oShape, eShape) } // NestedWithin returns true if the receiver describes an address that is // contained within one of the objects that the given other address could // select. func (e *MoveEndpointInModule) NestedWithin(other *MoveEndpointInModule) bool { - eMod := e.synthModuleInstance() - oMod := other.synthModuleInstance() + eShape := e.targetShape() + oShape := other.targetShape() - // In order to be nested within the given endpoint, the module path must be - // shorter or equal. - if len(oMod) > len(eMod) { + if !shapeCouldContain(oShape, eShape) { return false } - if !moduleInstanceCanMatch(oMod, eMod) { - return false - } - - eSub := e.relSubject - oSub := other.relSubject - - switch oSub := oSub.(type) { - case AbsModuleCall: - switch eSub.(type) { - case AbsModuleCall: - // we know the other endpoint selects our module, but if we are - // also a module call our path must be longer to be nested. - return len(eMod) > len(oMod) - } - - return true - - case ModuleInstance: - switch eSub.(type) { - case ModuleInstance, AbsModuleCall: - // a nested module must have a longer path - return len(eMod) > len(oMod) - } - + _, eModule := eShape.(ModuleInstance) + _, oResource := oShape.(AbsResourceInstance) + switch { + case oResource: + // A whole resource contains only its instances. + return shapeKey(oShape) == WildcardKey && shapeKey(eShape) != WildcardKey + case eModule && len(eShape.shapeModule()) == len(oShape.shapeModule()): + // A module call contains its instances, but otherwise a nested + // module must have a longer path. + _, oCall := other.relSubject.(AbsModuleCall) + _, eInst := e.relSubject.(ModuleInstance) + return oCall && eInst + default: return true - - case AbsResource: - if len(eMod) != len(oMod) { - // these resources are from different modules - return false - } - - // A resource can only contain a resource instance. - switch eSub := eSub.(type) { - case AbsResourceInstance: - return eSub.Resource.Resource.Equal(oSub.Resource) - } } - - return false } // matchModuleInstancePrefix is an internal helper to decide whether the given diff --git a/internal/addrs/output_value.go b/internal/addrs/output_value.go index b05b87fe1577..34a824221971 100644 --- a/internal/addrs/output_value.go +++ b/internal/addrs/output_value.go @@ -131,7 +131,7 @@ type absOutputValueUniqueKey string func (k absOutputValueUniqueKey) uniqueKeySigil() {} func ParseAbsOutputValue(traversal hcl.Traversal) (AbsOutputValue, tfdiags.Diagnostics) { - path, remain, diags := parseModuleInstancePrefix(traversal, false) + path, remain, diags := parseModuleInstancePrefix(traversal, knownInstanceKeys) if diags.HasErrors() { return AbsOutputValue{}, diags } diff --git a/internal/addrs/parse_address.go b/internal/addrs/parse_address.go new file mode 100644 index 000000000000..49ebc703f477 --- /dev/null +++ b/internal/addrs/parse_address.go @@ -0,0 +1,338 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// instanceKeys describes which instance keys are valid in an address being +// parsed from a traversal. +type instanceKeys int + +const ( + // noInstanceKeys is for configuration addresses, which can't have any + // instance keys. + noInstanceKeys instanceKeys = iota + + // knownInstanceKeys is for concrete addresses, where every instance key + // must be known. + knownInstanceKeys + + // wildcardInstanceKeys is for patterns and partial-expanded addresses, + // where any instance key can also be a wildcard. A wildcard is written as + // [*] in a traversal pattern, or is an index with an unknown value, and + // is parsed as WildcardKey. + wildcardInstanceKeys +) + +// parseModuleInstancePrefix parses the module path at the start of the given +// traversal, with instance keys as allowed by keys. +// +// The remainder of the traversal is returned for the caller to interpret as +// an object within the module. It always starts with a TraverseRoot if it's +// not empty. +func parseModuleInstancePrefix(traversal hcl.Traversal, keys instanceKeys) (ModuleInstance, hcl.Traversal, tfdiags.Diagnostics) { + remain := traversal + var mi ModuleInstance + var diags tfdiags.Diagnostics + +LOOP: + for len(remain) > 0 { + var next string + switch tt := remain[0].(type) { + case hcl.TraverseRoot: + next = tt.Name + case hcl.TraverseAttr: + next = tt.Name + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address operator", + Detail: "Module address prefix must be followed by dot and then a name.", + Subject: remain[0].SourceRange().Ptr(), + }) + break LOOP + } + + if next != "module" { + break + } + + kwRange := remain[0].SourceRange() + remain = remain[1:] + // If we have the prefix "module" then we should be followed by an + // module call name, as an attribute, and then optionally an index step + // giving the instance key. + if len(remain) == 0 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address operator", + Detail: "Prefix \"module.\" must be followed by a module name.", + Subject: &kwRange, + }) + break + } + + var moduleName string + switch tt := remain[0].(type) { + case hcl.TraverseAttr: + moduleName = tt.Name + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address operator", + Detail: "Prefix \"module.\" must be followed by a module name.", + Subject: remain[0].SourceRange().Ptr(), + }) + break LOOP + } + remain = remain[1:] + step := ModuleInstanceStep{ + Name: moduleName, + } + + if len(remain) > 0 && isInstanceKeyStep(remain[0]) { + key, keyDiags := parseInstanceKey(remain[0], keys, "module") + diags = diags.Append(keyDiags) + step.InstanceKey = key + remain = remain[1:] + } + + mi = append(mi, step) + } + + var retRemain hcl.Traversal + if len(remain) > 0 { + retRemain = make(hcl.Traversal, len(remain)) + copy(retRemain, remain) + // The first element here might be either a TraverseRoot or a + // TraverseAttr, depending on whether we had a module address on the + // front. To make life easier for callers, we'll normalize to always + // start with a TraverseRoot. + if tt, ok := retRemain[0].(hcl.TraverseAttr); ok { + retRemain[0] = hcl.TraverseRoot{ + Name: tt.Name, + SrcRange: tt.SrcRange, + } + } + } + + return mi, retRemain, diags +} + +// parseResourceUnderModule parses the resource address at the start of the +// given traversal, which is the remainder after parsing its module path, and +// its instance key if it has one, as allowed by keys. +// +// If allowExtra is set, the traversal after the resource address is returned +// for the caller to interpret. Otherwise there must be nothing else in the +// traversal. +func parseResourceUnderModule(remain hcl.Traversal, keys instanceKeys, allowExtra bool) (Resource, InstanceKey, hcl.Traversal, tfdiags.Diagnostics) { + // Note that this helper is used for all of the address types which + // include a resource, such as targets, move endpoints, and removed blocks, + // so its error messages should be generic enough to suit all of them. + + var diags tfdiags.Diagnostics + + mode := ManagedResourceMode + switch remain.RootName() { + case "data": + mode = DataResourceMode + remain = remain[1:] + case "ephemeral": + mode = EphemeralResourceMode + remain = remain[1:] + case "list": + mode = ListResourceMode + remain = remain[1:] + case "resource": + // Starting a resource address with "resource" is optional, so we'll + // just ignore it. + remain = remain[1:] + case "count", "each", "local", "module", "path", "self", "terraform", "var", "template", "lazy", "arg": + // These are all reserved words that are not valid as resource types. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: fmt.Sprintf("The keyword %q is reserved and cannot be used to target a resource address. If you are targeting a resource type that uses a reserved keyword, please prefix your address with \"resource.\".", remain.RootName()), + Subject: remain.SourceRange().Ptr(), + }) + return Resource{}, NoKey, nil, diags + } + + if len(remain) < 2 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: "Resource specification must include a resource type and name.", + Subject: remain.SourceRange().Ptr(), + }) + return Resource{}, NoKey, nil, diags + } + + var typeName, name string + switch tt := remain[0].(type) { + case hcl.TraverseRoot: + typeName = tt.Name + case hcl.TraverseAttr: + typeName = tt.Name + default: + var detail string + switch mode { + case ManagedResourceMode: + detail = "A resource type name is required." + case DataResourceMode: + detail = "A data source name is required." + case EphemeralResourceMode: + detail = "An ephemeral resource type name is required." + case ListResourceMode: + detail = "A list resource type name is required." + default: + panic("unknown mode") + } + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: detail, + Subject: remain[0].SourceRange().Ptr(), + }) + return Resource{}, NoKey, nil, diags + } + + switch tt := remain[1].(type) { + case hcl.TraverseAttr: + name = tt.Name + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: "A resource name is required.", + Subject: remain[1].SourceRange().Ptr(), + }) + return Resource{}, NoKey, nil, diags + } + + resource := Resource{ + Mode: mode, + Type: typeName, + Name: name, + } + remain = remain[2:] + + key := NoKey + keyed := len(remain) > 0 && isInstanceKeyStep(remain[0]) + if keyed { + var keyDiags tfdiags.Diagnostics + key, keyDiags = parseInstanceKey(remain[0], keys, "resource") + diags = diags.Append(keyDiags) + if keyDiags.HasErrors() { + return Resource{}, NoKey, nil, diags + } + remain = remain[1:] + } + + if allowExtra || len(remain) == 0 { + return resource, key, remain, diags + } + + switch { + case !keyed && keys != noInstanceKeys && len(remain) == 1: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: "Resource instance key must be given in square brackets.", + Subject: remain[0].SourceRange().Ptr(), + }) + case !keyed && keys != noInstanceKeys: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: "Unexpected extra operators after address.", + Subject: remain[1].SourceRange().Ptr(), + }) + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: "Unexpected extra operators after address.", + Subject: remain[0].SourceRange().Ptr(), + }) + } + return Resource{}, NoKey, nil, diags +} + +// isInstanceKeyStep returns true if the given traversal step can represent an +// instance key. +func isInstanceKeyStep(step hcl.Traverser) bool { + switch step.(type) { + case hcl.TraverseIndex, hcl.TraverseSplat: + return true + default: + return false + } +} + +// parseInstanceKey parses the given index or splat traversal step as the +// instance key of an object of the given kind, which is used only in error +// messages, as allowed by keys. +func parseInstanceKey(step hcl.Traverser, keys instanceKeys, kind string) (InstanceKey, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + rng := step.SourceRange().Ptr() + + if keys == noInstanceKeys { + switch kind { + case "module": + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Module instance keys not allowed", + Detail: "Module address must be a module (e.g. \"module.foo\"), not a module instance (e.g. \"module.foo[1]\").", + Subject: rng, + }) + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Resource instance keys not allowed", + Detail: "Resource address must be a resource (e.g. \"test_instance.foo\"), not a resource instance (e.g. \"test_instance.foo[1]\").", + Subject: rng, + }) + } + return NoKey, diags + } + + idx, isIndex := step.(hcl.TraverseIndex) + if !isIndex || !idx.Key.IsKnown() { + if keys == wildcardInstanceKeys { + return WildcardKey, diags + } + + detail := fmt.Sprintf("The %s instance key cannot be a wildcard in this address.", kind) + if isIndex { + detail = fmt.Sprintf("The %s instance key must be known in this address.", kind) + } + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: detail, + Subject: rng, + }) + return NoKey, diags + } + + key, err := ParseInstanceKey(idx.Key) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: fmt.Sprintf("Invalid %s instance key: %s.", kind, err), + Subject: rng, + }) + return NoKey, diags + } + return key, diags +} diff --git a/internal/addrs/parse_address_test.go b/internal/addrs/parse_address_test.go new file mode 100644 index 000000000000..d5b63c67332e --- /dev/null +++ b/internal/addrs/parse_address_test.go @@ -0,0 +1,173 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import ( + "testing" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// TestParseAddressInstanceKeys checks that each kind of address accepts or +// rejects wildcard and unknown instance keys, which are parsed by the same +// shared functions for every address type. +func TestParseAddressInstanceKeys(t *testing.T) { + unknownKey := func(t *testing.T, addr string, step int) hcl.Traversal { + traversal := mustParseTraversalPattern(t, addr) + traversal[step] = hcl.TraverseIndex{Key: cty.UnknownVal(cty.String)} + return traversal + } + + for name, tc := range map[string]struct { + traversal func(t *testing.T) hcl.Traversal + parse func(hcl.Traversal) (string, tfdiags.Diagnostics) + want string + wantErr string + }{ + "target pattern": { + traversal: func(t *testing.T) hcl.Traversal { + return mustParseTraversalPattern(t, "module.a[*].test_instance.foo[*]") + }, + parse: func(traversal hcl.Traversal) (string, tfdiags.Diagnostics) { + target, diags := ParseTarget(traversal) + return target.String(), diags + }, + want: "module.a[*].test_instance.foo[*]", + }, + "wildcard module in concrete target": { + traversal: func(t *testing.T) hcl.Traversal { + return mustParseTraversalPattern(t, "module.a[*].test_instance.foo") + }, + parse: func(traversal hcl.Traversal) (string, tfdiags.Diagnostics) { + _, diags := ParseAbsTargetable(traversal) + return "", diags + }, + wantErr: "Invalid address: The module instance key cannot be a wildcard in this address.", + }, + "unknown module key in concrete target": { + traversal: func(t *testing.T) hcl.Traversal { + return unknownKey(t, "module.a[0].test_instance.foo", 2) + }, + parse: func(traversal hcl.Traversal) (string, tfdiags.Diagnostics) { + _, diags := ParseAbsTargetable(traversal) + return "", diags + }, + wantErr: "Invalid address: The module instance key must be known in this address.", + }, + "invalid module key": { + traversal: func(t *testing.T) hcl.Traversal { + traversal := mustParseTraversalPattern(t, "module.a[0]") + traversal[2] = hcl.TraverseIndex{Key: cty.True} + return traversal + }, + parse: func(traversal hcl.Traversal) (string, tfdiags.Diagnostics) { + _, diags := ParseModuleInstance(traversal) + return "", diags + }, + wantErr: "Invalid address: Invalid module instance key: either a string or an integer is required.", + }, + "wildcard resource in move endpoint": { + traversal: func(t *testing.T) hcl.Traversal { + return mustParseTraversalPattern(t, "test_instance.foo[*]") + }, + parse: func(traversal hcl.Traversal) (string, tfdiags.Diagnostics) { + _, diags := ParseMoveEndpoint(traversal) + return "", diags + }, + wantErr: "Invalid address: The resource instance key cannot be a wildcard in this address.", + }, + "wildcard module in move endpoint": { + traversal: func(t *testing.T) hcl.Traversal { + return mustParseTraversalPattern(t, "module.a[*]") + }, + parse: func(traversal hcl.Traversal) (string, tfdiags.Diagnostics) { + _, diags := ParseMoveEndpoint(traversal) + return "", diags + }, + wantErr: "Invalid address: The module instance key cannot be a wildcard in this address.", + }, + "wildcard action instance": { + traversal: func(t *testing.T) hcl.Traversal { + return mustParseTraversalPattern(t, "action.test_action.foo[*]") + }, + parse: func(traversal hcl.Traversal) (string, tfdiags.Diagnostics) { + _, diags := ParseAbsActionInstance(traversal) + return "", diags + }, + wantErr: "Invalid address: The action instance key cannot be a wildcard in this address.", + }, + "partial-expanded resource with wildcards": { + traversal: func(t *testing.T) hcl.Traversal { + return mustParseTraversalPattern(t, `module.a[*].module.b["x"].test_instance.foo[1]`) + }, + parse: func(traversal hcl.Traversal) (string, tfdiags.Diagnostics) { + addr, diags := ParsePartialResourceInstance(traversal) + return addr.PartialResource().String(), diags + }, + want: "module.a[*].module.b[*].test_instance.foo[*]", + }, + "partial-expanded resource with an unknown key": { + traversal: func(t *testing.T) hcl.Traversal { + return unknownKey(t, `module.a[0].module.b["x"].test_instance.foo`, 5) + }, + parse: func(traversal hcl.Traversal) (string, tfdiags.Diagnostics) { + addr, diags := ParsePartialResourceInstance(traversal) + return addr.PartialResource().String(), diags + }, + want: "module.a[0].module.b[*].test_instance.foo[*]", + }, + "partial-expanded resource with the resource prefix": { + traversal: func(t *testing.T) hcl.Traversal { + return mustParseTraversalPattern(t, "module.a.resource.test_instance.foo") + }, + parse: func(traversal hcl.Traversal) (string, tfdiags.Diagnostics) { + addr, diags := ParsePartialResourceInstance(traversal) + return addr.PartialResource().String(), diags + }, + want: "module.a.test_instance.foo[*]", + }, + "partial-expanded data resource": { + traversal: func(t *testing.T) hcl.Traversal { + return unknownKey(t, "module.a[0].data.test_instance.foo", 2) + }, + parse: func(traversal hcl.Traversal) (string, tfdiags.Diagnostics) { + addr, diags := ParsePartialResourceInstance(traversal) + return addr.PartialResource().String(), diags + }, + want: "module.a[*].data.test_instance.foo[*]", + }, + } { + t.Run(name, func(t *testing.T) { + got, diags := tc.parse(tc.traversal(t)) + if tc.wantErr != "" { + if !diags.HasErrors() { + t.Fatalf("unexpected success\nwant error: %s", tc.wantErr) + } + if gotErr := diags.Err().Error(); gotErr != tc.wantErr { + t.Fatalf("wrong error\ngot: %s\nwant: %s", gotErr, tc.wantErr) + } + return + } + if diags.HasErrors() { + t.Fatalf("unexpected error: %s", diags.Err()) + } + if got != tc.want { + t.Errorf("wrong result\ngot: %s\nwant: %s", got, tc.want) + } + }) + } +} + +func mustParseTraversalPattern(t *testing.T, s string) hcl.Traversal { + t.Helper() + traversal, diags := hclsyntax.ParseTraversalPartial([]byte(s), "", hcl.InitialPos) + if diags.HasErrors() { + t.Fatalf("invalid traversal %q: %s", s, diags.Error()) + } + return traversal +} diff --git a/internal/addrs/parse_target.go b/internal/addrs/parse_target.go index e84ddca0b831..3c2c63543046 100644 --- a/internal/addrs/parse_target.go +++ b/internal/addrs/parse_target.go @@ -4,8 +4,6 @@ package addrs import ( - "fmt" - "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" @@ -25,7 +23,7 @@ import ( // If error diagnostics are returned then the address is invalid and must not // be used. func ParseAbsTargetable(traversal hcl.Traversal) (Targetable, tfdiags.Diagnostics) { - return parseAbsTarget(traversal, false) + return parseAbsTarget(traversal, knownInstanceKeys) } // ParseTarget attempts to interpret the given traversal as a target address. @@ -44,30 +42,31 @@ func ParseAbsTargetable(traversal hcl.Traversal) (Targetable, tfdiags.Diagnostic // If error diagnostics are returned then the TargetPattern is invalid and must // not be used. func ParseTarget(traversal hcl.Traversal) (TargetPattern, tfdiags.Diagnostics) { - path, remain, diags := parseModuleInstancePrefix(traversal, true) + path, remain, diags := parseModuleInstancePrefix(traversal, wildcardInstanceKeys) if diags.HasErrors() { return TargetPattern{}, diags } if len(remain) == 0 { - return newTargetPattern(path, moduleTargetShape, Resource{}, Action{}, NoKey), diags + return newTargetPattern(path), diags } - riAddr, moreDiags := parseResourceInstanceUnderModule(path, true, remain) + riAddr, moreDiags := parseResourceInstanceUnderModule(path, wildcardInstanceKeys, remain) diags = diags.Append(moreDiags) if diags.HasErrors() { return TargetPattern{}, diags } - return newTargetPattern(riAddr.Module, resourceTargetShape, riAddr.Resource.Resource, Action{}, riAddr.Resource.Key), diags + return newTargetPattern(riAddr), diags } // parseAbsTarget parses the concrete address of a module instance, resource, -// or resource instance. If allowPartial is set, any of the instance keys may -// be WildcardKey, for steps written as [*], which indicate a "partial" address -// that refers to all potential instances. -func parseAbsTarget(traversal hcl.Traversal, allowPartial bool) (Targetable, tfdiags.Diagnostics) { - path, remain, diags := parseModuleInstancePrefix(traversal, allowPartial) +// or resource instance, with instance keys as allowed by keys. If wildcards +// are allowed, any of the instance keys may be WildcardKey, for steps written +// as [*], which indicate a "partial" address that refers to all potential +// instances. +func parseAbsTarget(traversal hcl.Traversal, keys instanceKeys) (Targetable, tfdiags.Diagnostics) { + path, remain, diags := parseModuleInstancePrefix(traversal, keys) if diags.HasErrors() { return nil, diags } @@ -76,7 +75,7 @@ func parseAbsTarget(traversal hcl.Traversal, allowPartial bool) (Targetable, tfd return path, diags } - riAddr, moreDiags := parseResourceInstanceUnderModule(path, allowPartial, remain) + riAddr, moreDiags := parseResourceInstanceUnderModule(path, keys, remain) diags = diags.Append(moreDiags) if diags.HasErrors() { return nil, diags @@ -96,233 +95,23 @@ func parseAbsTarget(traversal hcl.Traversal, allowPartial bool) (Targetable, tfd // Error diagnostics are returned if the resource address contains an instance // key. func parseConfigResourceUnderModule(moduleAddr Module, remain hcl.Traversal) (ConfigResource, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - - mode := ManagedResourceMode - if remain.RootName() == "data" { - mode = DataResourceMode - remain = remain[1:] - } - - if len(remain) < 2 { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "Resource specification must include a resource type and name.", - Subject: remain.SourceRange().Ptr(), - }) - return ConfigResource{}, diags - } - - var typeName, name string - switch tt := remain[0].(type) { - case hcl.TraverseRoot: - typeName = tt.Name - case hcl.TraverseAttr: - typeName = tt.Name - default: - switch mode { - case ManagedResourceMode: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "A resource type name is required.", - Subject: remain[0].SourceRange().Ptr(), - }) - case DataResourceMode: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "A data source name is required.", - Subject: remain[0].SourceRange().Ptr(), - }) - default: - panic("unknown mode") - } - return ConfigResource{}, diags - } - - switch tt := remain[1].(type) { - case hcl.TraverseAttr: - name = tt.Name - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "A resource name is required.", - Subject: remain[1].SourceRange().Ptr(), - }) - return ConfigResource{}, diags - } - - remain = remain[2:] - if len(remain) > 0 { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Resource instance keys not allowed", - Detail: "Resource address must be a resource (e.g. \"test_instance.foo\"), not a resource instance (e.g. \"test_instance.foo[1]\").", - Subject: remain[0].SourceRange().Ptr(), - }) + resource, _, _, diags := parseResourceUnderModule(remain, noInstanceKeys, false) + if diags.HasErrors() { return ConfigResource{}, diags } - return ConfigResource{ - Module: moduleAddr, - Resource: Resource{ - Mode: mode, - Type: typeName, - Name: name, - }, - }, diags + return resource.InModule(moduleAddr), diags } -func parseResourceInstanceUnderModule(moduleAddr ModuleInstance, allowPartial bool, remain hcl.Traversal) (AbsResourceInstance, tfdiags.Diagnostics) { - // Note that this helper is used as part of both ParseTarget and - // ParseMoveEndpoint, so its error messages should be generic - // enough to suit both situations. - - var diags tfdiags.Diagnostics - - mode := ManagedResourceMode - switch remain.RootName() { - case "data": - mode = DataResourceMode - remain = remain[1:] - case "ephemeral": - mode = EphemeralResourceMode - remain = remain[1:] - case "list": - mode = ListResourceMode - remain = remain[1:] - case "resource": - // Starting a resource address with "resource" is optional, so we'll - // just ignore it. - remain = remain[1:] - case "count", "each", "local", "module", "path", "self", "terraform", "var", "template", "lazy", "arg": - // These are all reserved words that are not valid as resource types. - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: fmt.Sprintf("The keyword %q is reserved and cannot be used to target a resource address. If you are targeting a resource type that uses a reserved keyword, please prefix your address with \"resource.\".", remain.RootName()), - Subject: remain.SourceRange().Ptr(), - }) - return AbsResourceInstance{}, diags - } - - if len(remain) < 2 { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "Resource specification must include a resource type and name.", - Subject: remain.SourceRange().Ptr(), - }) - return AbsResourceInstance{}, diags - } - - var typeName, name string - switch tt := remain[0].(type) { - case hcl.TraverseRoot: - typeName = tt.Name - case hcl.TraverseAttr: - typeName = tt.Name - default: - switch mode { - case ManagedResourceMode: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "A resource type name is required.", - Subject: remain[0].SourceRange().Ptr(), - }) - case DataResourceMode: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "A data source name is required.", - Subject: remain[0].SourceRange().Ptr(), - }) - case EphemeralResourceMode: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "An ephemeral resource type name is required.", - Subject: remain[0].SourceRange().Ptr(), - }) - case ListResourceMode: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "A list resource type name is required.", - Subject: remain[0].SourceRange().Ptr(), - }) - default: - panic("unknown mode") - } - return AbsResourceInstance{}, diags - } - - switch tt := remain[1].(type) { - case hcl.TraverseAttr: - name = tt.Name - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "A resource name is required.", - Subject: remain[1].SourceRange().Ptr(), - }) - return AbsResourceInstance{}, diags - } - - remain = remain[2:] - switch len(remain) { - case 0: - return moduleAddr.ResourceInstance(mode, typeName, name, NoKey), diags - case 1: - switch tt := remain[0].(type) { - case hcl.TraverseIndex: - key, err := ParseInstanceKey(tt.Key) - if err != nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: fmt.Sprintf("Invalid resource instance key: %s.", err), - Subject: remain[0].SourceRange().Ptr(), - }) - return AbsResourceInstance{}, diags - } - - return moduleAddr.ResourceInstance(mode, typeName, name, key), diags - case hcl.TraverseSplat: - if allowPartial { - return moduleAddr.ResourceInstance(mode, typeName, name, WildcardKey), diags - } - - // Otherwise, return an error. - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "Resource instance key must be given in square brackets.", - Subject: remain[0].SourceRange().Ptr(), - }) - return AbsResourceInstance{}, diags - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "Resource instance key must be given in square brackets.", - Subject: remain[0].SourceRange().Ptr(), - }) - return AbsResourceInstance{}, diags - } - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "Unexpected extra operators after address.", - Subject: remain[1].SourceRange().Ptr(), - }) +// parseResourceInstanceUnderModule attempts to parse the given traversal as +// the address of a resource instance within the given module instance, with +// instance keys as allowed by keys. An address without an instance key is +// parsed as the instance with NoKey. +func parseResourceInstanceUnderModule(moduleAddr ModuleInstance, keys instanceKeys, remain hcl.Traversal) (AbsResourceInstance, tfdiags.Diagnostics) { + resource, key, _, diags := parseResourceUnderModule(remain, keys, false) + if diags.HasErrors() { return AbsResourceInstance{}, diags } + return resource.Instance(key).Absolute(moduleAddr), diags } // ParseAbsTargetableStr is a helper wrapper around ParseAbsTargetable that @@ -491,7 +280,7 @@ func ParseAbsResourceInstance(traversal hcl.Traversal) (AbsResourceInstance, tfd // If error diagnostics are returned then the AbsResource value is invalid and // must not be used. func ParsePartialResourceInstance(traversal hcl.Traversal) (AbsResourceInstance, tfdiags.Diagnostics) { - subject, diags := parseAbsTarget(traversal, true) + subject, diags := parseAbsTarget(traversal, wildcardInstanceKeys) if diags.HasErrors() { return AbsResourceInstance{}, diags } diff --git a/internal/addrs/parse_target_action.go b/internal/addrs/parse_target_action.go index 53a536cf9161..90052eaa048d 100644 --- a/internal/addrs/parse_target_action.go +++ b/internal/addrs/parse_target_action.go @@ -4,8 +4,6 @@ package addrs import ( - "fmt" - "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" @@ -27,7 +25,7 @@ import ( // We prevent callers accidentally including actions where they shouldn't be // accessible by keeping these methods separate. func parseAbsActionTarget(traversal hcl.Traversal) (Targetable, tfdiags.Diagnostics) { - addr, diags := parseTargetAction(traversal, false) + addr, diags := parseTargetAction(traversal, knownInstanceKeys) if diags.HasErrors() { return nil, diags } @@ -59,18 +57,18 @@ func parseAbsActionTarget(traversal hcl.Traversal) (Targetable, tfdiags.Diagnost // callers accidentally including action targets where they shouldn't be // accessible by keeping these methods separate. func ParseTargetAction(traversal hcl.Traversal) (TargetPattern, tfdiags.Diagnostics) { - addr, diags := parseTargetAction(traversal, true) + addr, diags := parseTargetAction(traversal, wildcardInstanceKeys) if diags.HasErrors() { return TargetPattern{}, diags } - return newTargetPattern(addr.Module, actionTargetShape, Resource{}, addr.Action.Action, addr.Action.Key), diags + return newTargetPattern(addr), diags } -// parseTargetAction parses an action instance address. If allowPattern is set, -// any of the instance keys may be WildcardKey, for steps written as [*]. -func parseTargetAction(traversal hcl.Traversal, allowPattern bool) (AbsActionInstance, tfdiags.Diagnostics) { - path, remain, diags := parseModuleInstancePrefix(traversal, allowPattern) +// parseTargetAction parses an action instance address, with instance keys as +// allowed by keys. +func parseTargetAction(traversal hcl.Traversal, keys instanceKeys) (AbsActionInstance, tfdiags.Diagnostics) { + path, remain, diags := parseModuleInstancePrefix(traversal, keys) if diags.HasErrors() { return AbsActionInstance{}, diags } @@ -84,7 +82,7 @@ func parseTargetAction(traversal hcl.Traversal, allowPattern bool) (AbsActionIns }) } - addr, moreDiags := parseActionInstanceUnderModule(path, remain, allowPattern) + addr, moreDiags := parseActionInstanceUnderModule(path, remain, keys) return addr, diags.Append(moreDiags) } @@ -107,7 +105,7 @@ func ParseTargetActionStr(str string) (TargetPattern, tfdiags.Diagnostics) { return target, diags } -func parseActionInstanceUnderModule(moduleAddr ModuleInstance, remain hcl.Traversal, allowPattern bool) (AbsActionInstance, tfdiags.Diagnostics) { +func parseActionInstanceUnderModule(moduleAddr ModuleInstance, remain hcl.Traversal, keys instanceKeys) (AbsActionInstance, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics if remain.RootName() != "action" { @@ -158,46 +156,41 @@ func parseActionInstanceUnderModule(moduleAddr ModuleInstance, remain hcl.Traver } remain = remain[2:] - switch len(remain) { - case 0: - return moduleAddr.ActionInstance(typeName, name, NoKey), diags - case 1: - switch tt := remain[0].(type) { - case hcl.TraverseIndex: - key, err := ParseInstanceKey(tt.Key) - if err != nil { - return AbsActionInstance{}, 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 moduleAddr.ActionInstance(typeName, name, key), diags - case hcl.TraverseSplat: - if allowPattern { - return moduleAddr.ActionInstance(typeName, name, WildcardKey), diags - } - return AbsActionInstance{}, 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 AbsActionInstance{}, 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(), - }) + key := NoKey + keyed := len(remain) > 0 && isInstanceKeyStep(remain[0]) + if keyed { + var keyDiags tfdiags.Diagnostics + key, keyDiags = parseInstanceKey(remain[0], keys, "action") + diags = diags.Append(keyDiags) + if keyDiags.HasErrors() { + return AbsActionInstance{}, diags } - default: + remain = remain[1:] + } + + switch { + case len(remain) == 0: + return moduleAddr.ActionInstance(typeName, name, key), diags + case !keyed && len(remain) == 1: + return AbsActionInstance{}, 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(), + }) + case !keyed: return AbsActionInstance{}, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid address", Detail: "Unexpected extra operators after address.", Subject: remain[1].SourceRange().Ptr(), }) + default: + return AbsActionInstance{}, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: "Unexpected extra operators after address.", + Subject: remain[0].SourceRange().Ptr(), + }) } } diff --git a/internal/addrs/partial_expanded.go b/internal/addrs/partial_expanded.go index 598ace392a9c..f1368d7c9df1 100644 --- a/internal/addrs/partial_expanded.go +++ b/internal/addrs/partial_expanded.go @@ -5,217 +5,70 @@ package addrs import ( "fmt" - "strings" - - "github.com/hashicorp/hcl/v2" - "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/gocty" - - "github.com/hashicorp/terraform/internal/tfdiags" ) // PartialExpandedModule represents a set of module instances which all share // a common known parent module instance but the remaining call instance keys // are not yet known. type PartialExpandedModule struct { - // expandedPrefix is the initial part of the module address whose expansion - // is already complete and so has exact instance keys. - expandedPrefix ModuleInstance - - // unexpandedSuffix is the remainder of the module address whose instance - // keys are not known yet. This is a slight abuse of type [Module] because - // it's representing a relative path from expandedPrefix rather than a - // path from the root module as usual, so this value must never be exposed - // in the public API of this package. + // module is the full module instance address, where each step whose + // instance key is not known yet has an instance key of WildcardKey. // - // This can be zero-length in PartialExpandedModule values used as part - // of the internals of a PartialExpandedResource, but should never be - // zero-length in a publicly-exposed PartialExpandedModule because that - // would make this just a degenerate ModuleInstance. - unexpandedSuffix Module + // The module instance keys are known for an initial prefix of the + // address, and the remainder of the address is not expanded yet, so once + // a step has an unknown instance key, every step after it does too. All + // values must be constructed with newPartialExpandedModule to enforce this. + // + // There can be no unknown steps in PartialExpandedModule values used as + // part of the internals of a PartialExpandedResource, but there should + // always be at least one in a publicly-exposed PartialExpandedModule, + // because otherwise it would be just a degenerate ModuleInstance. + module ModuleInstance } -// ParsePartialExpandedModule parses a module address traversal and returns a -// PartialExpandedModule representing the known and unknown parts of the -// address. -// -// It returns the parsed PartialExpandedModule, the remaining traversal steps -// that were not consumed by this function, and any diagnostics that were -// generated during parsing. -func ParsePartialExpandedModule(traversal hcl.Traversal) (PartialExpandedModule, hcl.Traversal, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - - remain := traversal - var partial PartialExpandedModule - - // We'll step through the traversal steps and build up the known prefix - // of the module address. When we reach a call with an unknown index, we'll - // switch to building up the unexpanded suffix. - expanded := true - -LOOP: - for len(remain) > 0 { - var next string - switch tt := remain[0].(type) { - case hcl.TraverseRoot: - next = tt.Name - case hcl.TraverseAttr: - next = tt.Name - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address operator", - Detail: "Module address prefix must be followed by dot and then a name.", - Subject: remain[0].SourceRange().Ptr(), - }) - break LOOP - } - - if next != "module" { - break - } - - kwRange := remain[0].SourceRange() - remain = remain[1:] - if len(remain) == 0 { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address operator", - Detail: "Prefix \"module.\" must be followed by a module name.", - Subject: &kwRange, - }) - break - } - - var moduleName string - switch tt := remain[0].(type) { - case hcl.TraverseAttr: - moduleName = tt.Name - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address operator", - Detail: "Prefix \"module.\" must be followed by a module name.", - Subject: remain[0].SourceRange().Ptr(), - }) - break LOOP - } - remain = remain[1:] - - if expanded { - - step := ModuleInstanceStep{ - Name: moduleName, - } - - if len(remain) > 0 { - if idx, ok := remain[0].(hcl.TraverseIndex); ok { - remain = remain[1:] - - if !idx.Key.IsKnown() { - // We'll switch to building up the unexpanded suffix - // starting with this step. - expanded = false - partial.unexpandedSuffix = append(partial.unexpandedSuffix, moduleName) - continue - } - - switch idx.Key.Type() { - case cty.String: - step.InstanceKey = StringKey(idx.Key.AsString()) - case cty.Number: - var idxInt int - err := gocty.FromCtyValue(idx.Key, &idxInt) - if err == nil { - step.InstanceKey = IntKey(idxInt) - } else { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address operator", - Detail: fmt.Sprintf("Invalid module index: %s.", err), - Subject: idx.SourceRange().Ptr(), - }) - } - default: - // Should never happen, because no other types are allowed in traversal indices. - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address operator", - Detail: "Invalid module key: must be either a string or an integer.", - Subject: idx.SourceRange().Ptr(), - }) - } - } - } - - partial.expandedPrefix = append(partial.expandedPrefix, step) - continue - } - - // Otherwise, we'll process this as an unexpanded suffix. - partial.unexpandedSuffix = append(partial.unexpandedSuffix, moduleName) - - if len(remain) > 0 { - if _, ok := remain[0].(hcl.TraverseIndex); ok { - // Then we have a module instance key. We're now parsing the - // unexpanded suffix of the module address, so we'll just - // ignore it. - remain = remain[1:] - } - } +// newPartialExpandedModule returns the PartialExpandedModule for the given +// module instance address, where each step with an instance key of +// WildcardKey has not been expanded yet. None of the steps after the first +// step which has not been expanded can be expanded either, so their instance +// keys are replaced with WildcardKey. +func newPartialExpandedModule(module ModuleInstance) PartialExpandedModule { + if len(module) == 0 { + return PartialExpandedModule{} } - var retRemain hcl.Traversal - if len(remain) > 0 { - retRemain = make(hcl.Traversal, len(remain)) - copy(retRemain, remain) - // The first element here might be either a TraverseRoot or a - // TraverseAttr, depending on whether we had a module address on the - // front. To make life easier for callers, we'll normalize to always - // start with a TraverseRoot. - if tt, ok := retRemain[0].(hcl.TraverseAttr); ok { - retRemain[0] = hcl.TraverseRoot{ - Name: tt.Name, - SrcRange: tt.SrcRange, - } + ret := make(ModuleInstance, len(module)) + expanded := true + for i, step := range module { + expanded = expanded && step.InstanceKey != WildcardKey + if !expanded { + step.InstanceKey = WildcardKey } + ret[i] = step } - - return partial, retRemain, diags + return PartialExpandedModule{module: ret} } +// UnexpandedChild returns the address of the instances of the given module +// call within the receiver, whose instance keys are not known yet. func (m ModuleInstance) UnexpandedChild(call ModuleCall) PartialExpandedModule { - return PartialExpandedModule{ - expandedPrefix: m, - unexpandedSuffix: Module{call.Name}, - } + return newPartialExpandedModule(m.Child(call.Name, WildcardKey)) } // PartialModule reverses the process of UnknownModuleInstance by converting a // ModuleInstance back into a PartialExpandedModule. +// +// Each step with an instance key of WildcardKey is not yet expanded, and so +// is every step after the first of them, regardless of its instance key. func (m ModuleInstance) PartialModule() PartialExpandedModule { - pem := PartialExpandedModule{} - for _, step := range m { - if step.InstanceKey == WildcardKey { - pem.unexpandedSuffix = append(pem.unexpandedSuffix, step.Name) - continue - } - pem.expandedPrefix = append(pem.expandedPrefix, step) - } - return pem + return newPartialExpandedModule(m) } // UnknownModuleInstance expands the receiver to a full ModuleInstance by // replacing the unknown instance keys with a wildcard value. func (pem PartialExpandedModule) UnknownModuleInstance() ModuleInstance { - base := pem.expandedPrefix - for _, call := range pem.unexpandedSuffix { - base = append(base, ModuleInstanceStep{ - Name: call, - InstanceKey: WildcardKey, - }) - } - return base + // As with KnownPrefix, we expose our buffer directly but with no unused + // capacity, so that the caller can safely construct child addresses. + return pem.module[:len(pem.module):len(pem.module)] } // LevelsKnown returns the number of module path segments of the address that @@ -225,91 +78,81 @@ func (pem PartialExpandedModule) UnknownModuleInstance() ModuleInstance { // address over a less-specifically-known one when selecting a placeholder // value to use to represent an object beneath an unexpanded module address. func (pem PartialExpandedModule) LevelsKnown() int { - return len(pem.expandedPrefix) + for i, step := range pem.module { + if step.InstanceKey == WildcardKey { + return i + } + } + return len(pem.module) +} + +// fullyExpanded returns true if all of the instance keys of the receiver are +// known, which is only possible for the module of a PartialExpandedResource or +// PartialExpandedAction. +func (pem PartialExpandedModule) fullyExpanded() bool { + return pem.LevelsKnown() == len(pem.module) } // MatchesInstance returns true if and only if the given module instance // belongs to the recieving partially-expanded module address pattern. func (pem PartialExpandedModule) MatchesInstance(inst ModuleInstance) bool { - // Total length must always match. - if len(inst) != (len(pem.expandedPrefix) + len(pem.unexpandedSuffix)) { - return false - } - - // The known prefix must match exactly. - givenExpandedPrefix := inst[:len(pem.expandedPrefix)] - if !givenExpandedPrefix.Equal(pem.expandedPrefix) { - return false - } - - // The known suffix must match the call names, even though we don't yet - // know the specific instance keys. - givenExpandedSuffix := inst[len(pem.expandedPrefix):] - for i := range pem.unexpandedSuffix { - if pem.unexpandedSuffix[i] != givenExpandedSuffix[i].Name { - return false - } - } - - // If we passed all the filters above then it's a match. - return true + return pem.matches(inst) } // MatchesPartial returns true if and only if the receiver represents the same // static module as the other given module and the receiver's known instance // keys are a prefix of the other module's. func (pem PartialExpandedModule) MatchesPartial(other PartialExpandedModule) bool { - // The two addresses must represent the same static module, regardless - // of the instance keys of those modules. - if !pem.Module().Equal(other.Module()) { - return false - } + return pem.matches(other.module) +} - if len(pem.expandedPrefix) > len(other.expandedPrefix) { +// matches returns true if every module instance represented by the given +// address, whose unknown instance keys are WildcardKey, is also represented by +// the receiver. Unlike containment for targeting, the module instances must be +// at the same depth, rather than any descendants of the receiver. +func (pem PartialExpandedModule) matches(other ModuleInstance) bool { + if len(other) != len(pem.module) { return false } - - thisPrefix := pem.expandedPrefix - otherPrefix := other.expandedPrefix[:len(pem.expandedPrefix)] - return thisPrefix.Equal(otherPrefix) + return shapeContains(pem.module, other) } // Module returns the unexpanded module address that this pattern originated // from. func (pem PartialExpandedModule) Module() Module { - ret := pem.expandedPrefix.Module() - return append(ret, pem.unexpandedSuffix...) + return pem.module.Module() } // KnownPrefix returns the longest possible ModuleInstance address made of // known segments of this partially-expanded module instance address. func (pem PartialExpandedModule) KnownPrefix() ModuleInstance { - if len(pem.expandedPrefix) == 0 { + known := pem.LevelsKnown() + if known == 0 { return nil } // Although we can't enforce it with the Go compiler, our convention is // that we never mutate address values outside of this package and so - // we'll expose our pem.expandedPrefix buffer directly here and trust that - // the caller will play nice with it. However, we do force the unused - // capacity to zero so that the caller can safely construct child addresses, - // which would append new steps to the end. - return pem.expandedPrefix[:len(pem.expandedPrefix):len(pem.expandedPrefix)] + // we'll expose our pem.module buffer directly here and trust that the + // caller will play nice with it. However, we do force the unused capacity + // to zero so that the caller can safely construct child addresses, which + // would append new steps to the end. + return pem.module[:known:known] } // FirstUnexpandedCall returns the address of the first step in the module // path whose instance keys are not yet known, discarding any subsequent // calls beneath it. func (pem PartialExpandedModule) FirstUnexpandedCall() AbsModuleCall { - // NOTE: This assumes that there's always at least one element in - // unexpandedSuffix because it should only be used with the public-facing + // NOTE: This assumes that there's always at least one step with unknown + // instance keys because it should only be used with the public-facing // version of PartialExpandedModule where that contract always holds. It's // not safe to use this for the PartialExpandedModule value hidden in the // internals of PartialExpandedResource. return AbsModuleCall{ Module: pem.KnownPrefix(), Call: ModuleCall{ - Name: pem.unexpandedSuffix[0], + Name: pem.module[pem.LevelsKnown()].Name, }, } } @@ -321,7 +164,8 @@ func (pem PartialExpandedModule) FirstUnexpandedCall() AbsModuleCall { // actually possible) represents the whole module path that the // PartialExpandedModule encapsulates. func (pem PartialExpandedModule) UnexpandedSuffix() []ModuleCall { - if len(pem.unexpandedSuffix) == 0 { + unexpanded := pem.module[pem.LevelsKnown():] + if len(unexpanded) == 0 { // Should never happen for any publicly-visible value of this type, // because we should always have at least one unexpanded call, // but we'll allow it anyway since we have a reasonable return value @@ -329,13 +173,9 @@ func (pem PartialExpandedModule) UnexpandedSuffix() []ModuleCall { return nil } - // A []ModuleCall is the only representation of a non-rooted chain of - // module calls that we're allowed to export in our public API, and so - // we'll transform our not-quite-allowed unrooted "Module" value in that - // form externally. - ret := make([]ModuleCall, len(pem.unexpandedSuffix)) - for i, name := range pem.unexpandedSuffix { - ret[i].Name = name + ret := make([]ModuleCall, len(unexpanded)) + for i, step := range unexpanded { + ret[i].Name = step.Name } return ret } @@ -343,10 +183,7 @@ func (pem PartialExpandedModule) UnexpandedSuffix() []ModuleCall { // Child returns the address of a child of the receiver that belongs to the // given module call. func (pem PartialExpandedModule) Child(call ModuleCall) PartialExpandedModule { - return PartialExpandedModule{ - expandedPrefix: pem.expandedPrefix, - unexpandedSuffix: append(pem.unexpandedSuffix, call.Name), - } + return newPartialExpandedModule(pem.module.Child(call.Name, WildcardKey)) } // Resource returns the address of a resource within the receiver. @@ -370,19 +207,7 @@ func (pem PartialExpandedModule) Action(action Action) PartialExpandedAction { // suffix steps use a similar syntax but with "[*]" as a placeholder to // represent instance keys that aren't yet known. func (pem PartialExpandedModule) String() string { - var buf strings.Builder - if len(pem.expandedPrefix) != 0 { - buf.WriteString(pem.expandedPrefix.String()) - } - for i, callName := range pem.unexpandedSuffix { - if i > 0 || len(pem.expandedPrefix) != 0 { - buf.WriteByte('.') - } - buf.WriteString("module.") - buf.WriteString(callName) - buf.WriteString("[*]") - } - return buf.String() + return pem.module.String() } func (pem PartialExpandedModule) UniqueKey() UniqueKey { @@ -409,10 +234,10 @@ type PartialExpandedResource struct { // module is the partially-expanded module instance address that this // resource belongs to. // - // This value can actually represent a fully-expanded module if its - // unexpandedSuffix field is zero-length, in which case it's only the - // resource itself that's unexpanded, which would make this equivalent - // to an AbsResource. + // This value can actually represent a fully-expanded module if none of + // its instance keys are unknown, in which case it's only the resource + // itself that's unexpanded, which would make this equivalent to an + // AbsResource. // // We mustn't directly expose this value in the public API because // external callers must never see a PartialExpandedModule that is @@ -421,107 +246,6 @@ type PartialExpandedResource struct { resource Resource } -// ParsePartialExpandedResource parses a resource address traversal and returns -// a PartialExpandedResource representing the known and unknown parts of the -// address. -func ParsePartialExpandedResource(traversal hcl.Traversal) (PartialExpandedResource, hcl.Traversal, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - - pem, remain, diags := ParsePartialExpandedModule(traversal) - if len(remain) == 0 { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "Resource address must be a module address followed by a resource address.", - Subject: traversal.SourceRange().Ptr(), - }) - return PartialExpandedResource{}, nil, diags - } - - // We know that remain[0] is a hcl.TraverseRoot object as the - // ParsePartialExpandedModule function always returns a hcl.TraverseRoot - // object as the first element in the remain slice. - - mode := ManagedResourceMode - if remain.RootName() == "data" { - mode = DataResourceMode - remain = remain[1:] - } else if remain.RootName() == "resource" { - // Starting a resource address with "resource" is optional, so we'll - // just ignore it if it's present. - remain = remain[1:] - } - - if len(remain) < 2 { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "Resource specification must include a resource type and name.", - Subject: remain.SourceRange().Ptr(), - }) - return PartialExpandedResource{}, nil, diags - } - - var typeName, name string - switch tt := remain[0].(type) { - case hcl.TraverseRoot: - typeName = tt.Name - case hcl.TraverseAttr: - typeName = tt.Name - default: - switch mode { - case ManagedResourceMode: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "A resource type name is required.", - Subject: remain[0].SourceRange().Ptr(), - }) - case DataResourceMode: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "A data source name is required.", - Subject: remain[0].SourceRange().Ptr(), - }) - default: - panic("unknown mode") - } - return PartialExpandedResource{}, nil, diags - } - - switch tt := remain[1].(type) { - case hcl.TraverseAttr: - name = tt.Name - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid address", - Detail: "A resource name is required.", - Subject: remain[1].SourceRange().Ptr(), - }) - return PartialExpandedResource{}, nil, diags - } - - remain = remain[2:] - if len(remain) > 0 { - if _, ok := remain[0].(hcl.TraverseIndex); ok { - // Then we have a resource instance key. Since, we're building a - // PartialExpandedResource, we'll just ignore it. - remain = remain[1:] - } - } - - return PartialExpandedResource{ - module: pem, - resource: Resource{ - Mode: mode, - Type: typeName, - Name: name, - }, - }, remain, diags -} - // UnexpandedResource returns the address of a child resource expressed as a // [PartialExpandedResource]. // @@ -532,9 +256,7 @@ func ParsePartialExpandedResource(traversal hcl.Traversal) (PartialExpandedResou // address type for all of them. func (m ModuleInstance) UnexpandedResource(resource Resource) PartialExpandedResource { return PartialExpandedResource{ - module: PartialExpandedModule{ - expandedPrefix: m, - }, + module: newPartialExpandedModule(m), resource: resource, } } @@ -544,12 +266,7 @@ func (m ModuleInstance) UnexpandedResource(resource Resource) PartialExpandedRes // where we might also need to mix in resources belonging to not-yet-fully-known // module instance addresses. func (r AbsResource) UnexpandedResource() PartialExpandedResource { - return PartialExpandedResource{ - module: PartialExpandedModule{ - expandedPrefix: r.Module, - }, - resource: r.Resource, - } + return r.Module.UnexpandedResource(r.Resource) } // PartialResource reverses UnknownResourceInstance by converting the @@ -574,29 +291,27 @@ func (per PartialExpandedResource) UnknownResourceInstance() AbsResourceInstance // MatchesInstance returns true if and only if the given resource instance // belongs to the recieving partially-expanded resource address pattern. func (per PartialExpandedResource) MatchesInstance(inst AbsResourceInstance) bool { - if !per.module.MatchesInstance(inst.Module) { - return false - } - return inst.Resource.Resource.Equal(per.resource) + return per.matches(inst) } // MatchesResource returns true if and only if the given resource belongs to // the recieving partially-expanded resource address pattern. func (per PartialExpandedResource) MatchesResource(inst AbsResource) bool { - if !per.module.MatchesInstance(inst.Module) { - return false - } - return inst.Resource.Equal(per.resource) + return per.matches(inst) } // MatchesPartial returns true if the underlying partial module address matches // the given partial module address and the resource type and name match the // receiver's resource type and name. func (per PartialExpandedResource) MatchesPartial(other PartialExpandedResource) bool { - if !per.module.MatchesPartial(other.module) { - return false - } - return per.resource.Equal(other.resource) + return shapeContains(per.targetShape(), other.targetShape()) +} + +// matches returns true if every instance selected by the given address is +// represented by the receiver. +func (per PartialExpandedResource) matches(addr Targetable) bool { + other, ok := shapeOf(addr) + return ok && shapeContains(per.targetShape(), other) } // AbsResource returns the single [AbsResource] that this address represents @@ -605,12 +320,13 @@ func (per PartialExpandedResource) MatchesPartial(other PartialExpandedResource) // // The second return value is true if and only if the returned address is valid. func (per PartialExpandedResource) AbsResource() (AbsResource, bool) { - if len(per.module.unexpandedSuffix) != 0 { + module, ok := per.ModuleInstance() + if !ok { return AbsResource{}, false } return AbsResource{ - Module: per.module.expandedPrefix, + Module: module, Resource: per.resource, }, true } @@ -651,10 +367,10 @@ func (per PartialExpandedResource) KnownModuleInstancePrefix() ModuleInstance { // fully expanded, in which case the first return value is invalid. Use // [PartialExpandedResource.PartialExpandedModule] instead in that case. func (per PartialExpandedResource) ModuleInstance() (ModuleInstance, bool) { - if len(per.module.unexpandedSuffix) != 0 { + if !per.module.fullyExpanded() { return nil, false } - return per.module.expandedPrefix, true + return per.module.KnownPrefix(), true } // PartialExpandedModule returns a [PartialExpandedModule] address describing @@ -665,7 +381,7 @@ func (per PartialExpandedResource) ModuleInstance() (ModuleInstance, bool) { // fully expanded, in which case the first return value is invalid. Use // [PartialExpandedResource.ModuleInstance] instead in that case. func (per PartialExpandedResource) PartialExpandedModule() (PartialExpandedModule, bool) { - if len(per.module.unexpandedSuffix) == 0 { + if per.module.fullyExpanded() { return PartialExpandedModule{}, false } return per.module, true @@ -679,23 +395,13 @@ func (per PartialExpandedResource) IsTargetedBy(addr Targetable) bool { if !ok { return false } - return target.couldContain(per.targetShape()) + return shapeCouldContain(target, per.targetShape()) } // targetShape returns the shape of the resource instances the receiver could // represent, using WildcardKey for each instance key which is not yet known. func (per PartialExpandedResource) targetShape() targetShape { - module := make(ModuleInstance, 0, len(per.module.expandedPrefix)+len(per.module.unexpandedSuffix)) - module = append(module, per.module.expandedPrefix...) - for _, name := range per.module.unexpandedSuffix { - module = append(module, ModuleInstanceStep{Name: name, InstanceKey: WildcardKey}) - } - return targetShape{ - module: module, - kind: resourceTargetShape, - resource: per.resource, - key: WildcardKey, - } + return per.resource.Instance(WildcardKey).Absolute(per.module.module) } // String returns a string representation of the pattern which uses the special @@ -723,7 +429,7 @@ func (per PartialExpandedResource) UniqueKey() UniqueKey { type partialExpandedResourceKey string -var _ UniqueKey = partialExpandedModuleKey("") +var _ UniqueKey = partialExpandedResourceKey("") func (partialExpandedResourceKey) uniqueKeySigil() {} @@ -765,16 +471,6 @@ func ObjectInPartialExpandedModule[T interface { var _ UniqueKeyer = InPartialExpandedModule[LocalValue]{} -// ModuleLevelsKnown returns the number of module path segments of the address -// that have known instance keys. -// -// This might be useful, for example, for preferring a more-specifically-known -// address over a less-specifically-known one when selecting a placeholder -// value to use to represent an object beneath an unexpanded module address. -func (in InPartialExpandedModule[T]) ModuleLevelsKnown() int { - return in.Module.LevelsKnown() -} - // String returns a string representation of the pattern which uses the special // placeholder "[*]" to represent positions where module instance keys are not // yet known. @@ -808,12 +504,13 @@ type PartialExpandedAction struct { } func (per PartialExpandedAction) AbsAction() (AbsAction, bool) { - if len(per.module.unexpandedSuffix) != 0 { + module, ok := per.ModuleInstance() + if !ok { return AbsAction{}, false } return AbsAction{ - Module: per.module.expandedPrefix, + Module: module, Action: per.action, }, true } @@ -828,26 +525,15 @@ func (per PartialExpandedAction) ConfigAction() ConfigAction { } func (per PartialExpandedAction) ModuleInstance() (ModuleInstance, bool) { - if len(per.module.unexpandedSuffix) != 0 { + if !per.module.fullyExpanded() { return nil, false } - return per.module.expandedPrefix, true + return per.module.KnownPrefix(), true } func (m ModuleInstance) UnexpandedAction(action Action) PartialExpandedAction { return PartialExpandedAction{ - module: PartialExpandedModule{ - expandedPrefix: m, - }, - action: action, - } -} - -func (a *AbsAction) UnexpandedAction(action Action) PartialExpandedAction { - return PartialExpandedAction{ - module: PartialExpandedModule{ - expandedPrefix: a.Module, - }, + module: newPartialExpandedModule(m), action: action, } } @@ -871,7 +557,7 @@ func (pea PartialExpandedAction) String() string { } func (pea PartialExpandedAction) Equal(other PartialExpandedAction) bool { - return pea.module.MatchesPartial(other.module.expandedPrefix.PartialModule()) && pea.action.Equal(other.action) + return pea.module.module.Equal(other.module.module) && pea.action.Equal(other.action) } func (pea PartialExpandedAction) UniqueKey() UniqueKey { @@ -898,7 +584,7 @@ func (partialExpandedActionKey) uniqueKeySigil() {} // fully expanded, in which case the first return value is invalid. Use // [PartialExpandedAction.ModuleInstance] instead in that case. func (per PartialExpandedAction) PartialExpandedModule() (PartialExpandedModule, bool) { - if len(per.module.unexpandedSuffix) == 0 { + if per.module.fullyExpanded() { return PartialExpandedModule{}, false } return per.module, true @@ -907,8 +593,12 @@ func (per PartialExpandedAction) PartialExpandedModule() (PartialExpandedModule, // MatchesAction returns true if and only if the given action belongs to // the recieving partially-expanded action address pattern. func (per PartialExpandedAction) MatchesAction(inst AbsAction) bool { - if !per.module.MatchesInstance(inst.Module) { - return false - } - return inst.Action.Equal(per.action) + other, ok := shapeOf(inst) + return ok && shapeContains(per.targetShape(), other) +} + +// targetShape returns the shape of the action instances the receiver could +// represent, using WildcardKey for each instance key which is not yet known. +func (per PartialExpandedAction) targetShape() targetShape { + return per.action.Instance(WildcardKey).Absolute(per.module.module) } diff --git a/internal/addrs/partial_expanded_test.go b/internal/addrs/partial_expanded_test.go index 0e5e3133bae6..5cb3befabcfd 100644 --- a/internal/addrs/partial_expanded_test.go +++ b/internal/addrs/partial_expanded_test.go @@ -5,11 +5,11 @@ package addrs import ( "fmt" + "slices" "testing" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/zclconf/go-cty/cty" ) func TestPartialExpandedResourceIsTargetedBy(t *testing.T) { @@ -187,293 +187,285 @@ func TestPartialExpandedResourceIsTargetedBy(t *testing.T) { } } -func TestParsePartialExpandedModule(t *testing.T) { +func mustParsePartialResourceInstanceStr(s string) AbsResourceInstance { + r, diags := ParsePartialResourceInstanceStr(s) + if diags.HasErrors() { + panic(diags.ErrWithWarnings().Error()) + } + return r +} - // these functions are a bit weird, as the normal parsing supported by - // HCL can't put unknown values into the instance keys. So we need to - // build the traversals in the same way the thing that is calling these - // functions does. +func TestPartialExpandedModule(t *testing.T) { + pem := mustParsePartialExpandedModule("module.a[0].module.b[*].module.c[*]") - tcs := []struct { - traversal func(t *testing.T) (string, hcl.Traversal) - want PartialExpandedModule - remain int + if got, want := pem.String(), "module.a[0].module.b[*].module.c[*]"; got != want { + t.Errorf("wrong String\ngot: %s\nwant: %s", got, want) + } + if got, want := pem.LevelsKnown(), 1; got != want { + t.Errorf("wrong LevelsKnown: got %d, want %d", got, want) + } + if got, want := pem.KnownPrefix(), mustParseModuleInstanceStr("module.a[0]"); !got.Equal(want) { + t.Errorf("wrong KnownPrefix\ngot: %s\nwant: %s", got, want) + } + if got, want := pem.UnexpandedSuffix(), []ModuleCall{{Name: "b"}, {Name: "c"}}; !slices.Equal(got, want) { + t.Errorf("wrong UnexpandedSuffix\ngot: %#v\nwant: %#v", got, want) + } + if got, want := pem.FirstUnexpandedCall().String(), "module.a[0].module.b"; got != want { + t.Errorf("wrong FirstUnexpandedCall\ngot: %s\nwant: %s", got, want) + } + if got, want := pem.Module().String(), "module.a.module.b.module.c"; got != want { + t.Errorf("wrong Module\ngot: %s\nwant: %s", got, want) + } + if got, want := pem.UnknownModuleInstance().String(), "module.a[0].module.b[*].module.c[*]"; got != want { + t.Errorf("wrong UnknownModuleInstance\ngot: %s\nwant: %s", got, want) + } + if got, want := pem.Child(ModuleCall{Name: "d"}).String(), "module.a[0].module.b[*].module.c[*].module.d[*]"; got != want { + t.Errorf("wrong Child\ngot: %s\nwant: %s", got, want) + } + if got, want := mustParseModuleInstanceStr("module.a[0]").UnexpandedChild(ModuleCall{Name: "b"}).String(), "module.a[0].module.b[*]"; got != want { + t.Errorf("wrong UnexpandedChild\ngot: %s\nwant: %s", got, want) + } + + root := RootModuleInstance.UnexpandedChild(ModuleCall{Name: "a"}) + if got := root.KnownPrefix(); got != nil { + t.Errorf("wrong KnownPrefix for unexpanded root call: got %#v, want nil", got) + } + if got, want := root.FirstUnexpandedCall().String(), "module.a"; got != want { + t.Errorf("wrong FirstUnexpandedCall\ngot: %s\nwant: %s", got, want) + } +} + +func TestPartialExpandedModuleMatches(t *testing.T) { + for _, tc := range []struct { + pem string + other string + want bool }{ - { - traversal: func(t *testing.T) (string, hcl.Traversal) { - addr := "module.mod" - traversal, diags := hclsyntax.ParseTraversalAbs([]byte(addr), "", hcl.InitialPos) - if len(diags) > 0 { - t.Fatalf("unexpected diagnostics: %v", diags) - } - return addr, traversal - }, - want: PartialExpandedModule{ - expandedPrefix: ModuleInstance{ - { - Name: "mod", - }, - }, - }, - remain: 0, - }, - { - traversal: func(t *testing.T) (string, hcl.Traversal) { - addr := "module.mod[0]" - traversal, diags := hclsyntax.ParseTraversalAbs([]byte(addr), "", hcl.InitialPos) - if len(diags) > 0 { - t.Fatalf("unexpected diagnostics: %v", diags) - } - // Hack the key into an unknown value. - traversal[2] = hcl.TraverseIndex{ - Key: cty.UnknownVal(cty.Number), - } - return "module.mod[*]", traversal - }, - want: PartialExpandedModule{ - unexpandedSuffix: Module{ - "mod", - }, - }, - remain: 0, - }, - { - traversal: func(t *testing.T) (string, hcl.Traversal) { - addr := "module.child.module.grandchild" - traversal, diags := hclsyntax.ParseTraversalAbs([]byte(addr), "", hcl.InitialPos) - if len(diags) > 0 { - t.Fatalf("unexpected diagnostics: %v", diags) - } - return addr, traversal - }, - want: PartialExpandedModule{ - expandedPrefix: ModuleInstance{ - { - Name: "child", - }, - { - Name: "grandchild", - }, - }, - }, - remain: 0, - }, - { - traversal: func(t *testing.T) (string, hcl.Traversal) { - addr := "module.child[0].module.grandchild" - traversal, diags := hclsyntax.ParseTraversalAbs([]byte(addr), "", hcl.InitialPos) - if len(diags) > 0 { - t.Fatalf("unexpected diagnostics: %v", diags) - } - return addr, traversal - }, - want: PartialExpandedModule{ - expandedPrefix: ModuleInstance{ - { - Name: "child", - InstanceKey: IntKey(0), - }, - { - Name: "grandchild", - }, - }, - }, - remain: 0, - }, - { - traversal: func(t *testing.T) (string, hcl.Traversal) { - addr := "module.child[0].module.grandchild" - traversal, diags := hclsyntax.ParseTraversalAbs([]byte(addr), "", hcl.InitialPos) - if len(diags) > 0 { - t.Fatalf("unexpected diagnostics: %v", diags) - } - traversal[2] = hcl.TraverseIndex{ - Key: cty.UnknownVal(cty.Number), - } - return "module.child[*].module.grandchild", traversal - }, - want: PartialExpandedModule{ - unexpandedSuffix: Module{ - "child", - "grandchild", - }, - }, - remain: 0, - }, - { - traversal: func(t *testing.T) (string, hcl.Traversal) { - addr := "module.child.module.grandchild[0]" - traversal, diags := hclsyntax.ParseTraversalAbs([]byte(addr), "", hcl.InitialPos) - if len(diags) > 0 { - t.Fatalf("unexpected diagnostics: %v", diags) - } - traversal[4] = hcl.TraverseIndex{ - Key: cty.UnknownVal(cty.Number), - } - return "module.child.module.grandchild[*]", traversal - }, - want: PartialExpandedModule{ - expandedPrefix: ModuleInstance{ - { - Name: "child", - }, - }, - unexpandedSuffix: Module{ - "grandchild", - }, - }, - remain: 0, - }, - { - traversal: func(t *testing.T) (string, hcl.Traversal) { - addr := "module.child.module.grandchild[0].resource_type.resource_name" - traversal, diags := hclsyntax.ParseTraversalAbs([]byte(addr), "", hcl.InitialPos) - if len(diags) > 0 { - t.Fatalf("unexpected diagnostics: %v", diags) - } - traversal[4] = hcl.TraverseIndex{ - Key: cty.UnknownVal(cty.Number), - } - return "module.child.module.grandchild[*].resource_type.resource_name", traversal - }, - want: PartialExpandedModule{ - expandedPrefix: ModuleInstance{ - { - Name: "child", - }, - }, - unexpandedSuffix: Module{ - "grandchild", - }, - }, - remain: 2, + {"module.a[*]", "module.a[0]", true}, + {"module.a[*]", "module.a", true}, + {"module.a[*]", "module.b[0]", false}, + { + // only the module instances themselves match, not their + // descendants + "module.a[*]", + "module.a[0].module.b[0]", + false, }, - } + {"module.a[0].module.b[*]", `module.a[0].module.b["x"]`, true}, + {"module.a[0].module.b[*]", `module.a[1].module.b["x"]`, false}, + {"module.a.module.b[*]", "module.a.module.b[0]", true}, + {"module.a.module.b[*]", "module.a[0].module.b[0]", false}, - for _, tc := range tcs { - addr, traversal := tc.traversal(t) - t.Run(addr, func(t *testing.T) { - module, rest, diags := ParsePartialExpandedModule(traversal) - if len(diags) > 0 { - t.Fatalf("unexpected diagnostics: %s", diags) - } + // matching other partial-expanded modules + {"module.a[*].module.b[*]", "module.a[0].module.b[*]", true}, + {"module.a[0].module.b[*]", "module.a[0].module.b[*]", true}, + {"module.a[0].module.b[*]", "module.a[*].module.b[*]", false}, + {"module.a[0].module.b[*]", "module.a[1].module.b[*]", false}, + } { + t.Run(fmt.Sprintf("%s-matches-%s", tc.pem, tc.other), func(t *testing.T) { + pem := mustParsePartialExpandedModule(tc.pem) + other := mustParsePartialModuleInstance(tc.other) - if !module.expandedPrefix.Equal(tc.want.expandedPrefix) { - t.Errorf("got expandedPrefix %v; want %v", module.expandedPrefix, tc.want.expandedPrefix) - } - if !module.unexpandedSuffix.Equal(tc.want.unexpandedSuffix) { - t.Errorf("got unexpandedSuffix %v; want %v", module.unexpandedSuffix, tc.want.unexpandedSuffix) + if got := pem.MatchesPartial(other.PartialModule()); got != tc.want { + t.Errorf("MatchesPartial: got %t, want %t", got, tc.want) } - if len(rest) != tc.remain { - t.Errorf("got %d remaining traversals; want %d", len(rest), tc.remain) + isPartial := slices.ContainsFunc(other, func(step ModuleInstanceStep) bool { + return step.InstanceKey == WildcardKey + }) + if !isPartial { + if got := pem.MatchesInstance(other); got != tc.want { + t.Errorf("MatchesInstance: got %t, want %t", got, tc.want) + } } }) } - } -func TestParsePartialExpandedResource(t *testing.T) { - - tcs := []struct { - addr string - want PartialExpandedResource - remain int +func TestModuleInstancePartialModule(t *testing.T) { + for _, tc := range []struct { + addr string + want string }{ + {"module.a", "module.a"}, + {"module.a[*]", "module.a[*]"}, + {"module.a[0].module.b[*]", "module.a[0].module.b[*]"}, + {"module.a[*].module.b[*]", "module.a[*].module.b[*]"}, { - addr: "resource_type.resource_name", - want: PartialExpandedResource{ - resource: Resource{ - Mode: ManagedResourceMode, - Type: "resource_type", - Name: "resource_name", - }, - }, - remain: 0, + // once a step is not expanded, none of the steps after it can be + // expanded either + "module.a[*].module.b[0]", + "module.a[*].module.b[*]", }, - { - addr: "module.mod.resource_type.resource_name", - want: PartialExpandedResource{ - module: PartialExpandedModule{ - expandedPrefix: ModuleInstance{ - { - Name: "mod", - }, - }, - }, - resource: Resource{ - Mode: ManagedResourceMode, - Type: "resource_type", - Name: "resource_name", - }, - }, - }, - { - addr: "resource_type.resource_name[0]", - want: PartialExpandedResource{ - resource: Resource{ - Mode: ManagedResourceMode, - Type: "resource_type", - Name: "resource_name", - }, - }, - remain: 0, - }, - { - addr: "resource_type.resource_name[0].attr", - want: PartialExpandedResource{ - resource: Resource{ - Mode: ManagedResourceMode, - Type: "resource_type", - Name: "resource_name", - }, - }, - remain: 1, - }, - { - addr: "resource.resource_type.resource_name", - want: PartialExpandedResource{ - resource: Resource{ - Mode: ManagedResourceMode, - Type: "resource_type", - Name: "resource_name", - }, - }, - remain: 0, - }, - } - - for _, tc := range tcs { + } { t.Run(tc.addr, func(t *testing.T) { - traversal, traversalDiags := hclsyntax.ParseTraversalAbs([]byte(tc.addr), "", hcl.InitialPos) - if len(traversalDiags) > 0 { - t.Fatalf("unexpected diagnostics: %v", traversalDiags) + got := mustParsePartialModuleInstance(tc.addr).PartialModule() + if got.String() != tc.want { + t.Errorf("wrong result\ngot: %s\nwant: %s", got, tc.want) } - - partial, rest, diags := ParsePartialExpandedResource(traversal) - if len(diags) > 0 { - t.Fatalf("unexpected diagnostics: %s", diags) + if !got.UnknownModuleInstance().PartialModule().MatchesPartial(got) { + t.Errorf("%s does not round-trip through UnknownModuleInstance", got) } + }) + } +} - if !partial.module.expandedPrefix.Equal(tc.want.module.expandedPrefix) { - t.Errorf("got expandedPrefix %v; want %v", partial.module.expandedPrefix, tc.want.module.expandedPrefix) - } - if !partial.module.unexpandedSuffix.Equal(tc.want.module.unexpandedSuffix) { - t.Errorf("got unexpandedSuffix %v; want %v", partial.module.unexpandedSuffix, tc.want.module.unexpandedSuffix) - } - if !partial.resource.Equal(tc.want.resource) { - t.Errorf("got resource %v; want %v", partial.resource, tc.want.resource) +func TestPartialExpandedResourceMatches(t *testing.T) { + for _, tc := range []struct { + per string + other string + want bool + }{ + {"test.a[*]", "test.a[0]", true}, + {"test.a[*]", "test.a", true}, + {"test.a[*]", "test.b[0]", false}, + {"test.a[*]", "data.test.a[0]", false}, + {"module.m[*].test.a[*]", "module.m[0].test.a[1]", true}, + {"module.m[*].test.a[*]", "test.a[1]", false}, + {"module.m[*].test.a[*]", "module.m[0].module.n.test.a", false}, + {"module.m[0].test.a[*]", "module.m[1].test.a[0]", false}, + {"module.m[0].module.n[*].test.a[*]", `module.m[0].module.n["x"].test.a`, true}, + + // matching other partial-expanded resources + {"module.m[*].test.a[*]", "module.m[0].test.a[*]", true}, + {"module.m[*].test.a[*]", "module.m[*].test.a[*]", true}, + {"module.m[0].test.a[*]", "module.m[*].test.a[*]", false}, + } { + t.Run(fmt.Sprintf("%s-matches-%s", tc.per, tc.other), func(t *testing.T) { + per := mustParsePartialResourceInstanceStr(tc.per).PartialResource() + other := mustParsePartialResourceInstanceStr(tc.other) + + if got := per.MatchesPartial(other.PartialResource()); got != tc.want { + t.Errorf("MatchesPartial: got %t, want %t", got, tc.want) } - if len(rest) != tc.remain { - t.Errorf("got %d remaining traversals; want %d", len(rest), tc.remain) + if other.Resource.Key != WildcardKey { + if got := per.MatchesInstance(other); got != tc.want { + t.Errorf("MatchesInstance: got %t, want %t", got, tc.want) + } + if got := per.MatchesResource(other.ContainingResource()); got != tc.want { + t.Errorf("MatchesResource: got %t, want %t", got, tc.want) + } } }) } } -func mustParsePartialResourceInstanceStr(s string) AbsResourceInstance { - r, diags := ParsePartialResourceInstanceStr(s) +func TestPartialExpandedResource(t *testing.T) { + partial := mustParsePartialResourceInstanceStr("module.m[0].module.n[*].test.a[*]").PartialResource() + known := mustParsePartialResourceInstanceStr("module.m[0].test.a[*]").PartialResource() + + if got, want := partial.String(), "module.m[0].module.n[*].test.a[*]"; got != want { + t.Errorf("wrong String\ngot: %s\nwant: %s", got, want) + } + if got, want := partial.UnknownResourceInstance().String(), "module.m[0].module.n[*].test.a[*]"; got != want { + t.Errorf("wrong UnknownResourceInstance\ngot: %s\nwant: %s", got, want) + } + if got, want := partial.ConfigResource().String(), "module.m.module.n.test.a"; got != want { + t.Errorf("wrong ConfigResource\ngot: %s\nwant: %s", got, want) + } + if got, want := partial.KnownModuleInstancePrefix().String(), "module.m[0]"; got != want { + t.Errorf("wrong KnownModuleInstancePrefix\ngot: %s\nwant: %s", got, want) + } + if _, ok := partial.AbsResource(); ok { + t.Errorf("partial-expanded module has an AbsResource") + } + if _, ok := partial.ModuleInstance(); ok { + t.Errorf("partial-expanded module has a ModuleInstance") + } + if got, ok := partial.PartialExpandedModule(); !ok || got.String() != "module.m[0].module.n[*]" { + t.Errorf("wrong PartialExpandedModule: got %s, %t", got, ok) + } + + if got, ok := known.AbsResource(); !ok || got.String() != "module.m[0].test.a" { + t.Errorf("wrong AbsResource: got %s, %t", got, ok) + } + if got, ok := known.ModuleInstance(); !ok || got.String() != "module.m[0]" { + t.Errorf("wrong ModuleInstance: got %s, %t", got, ok) + } + if _, ok := known.PartialExpandedModule(); ok { + t.Errorf("fully-expanded module has a PartialExpandedModule") + } + if got, want := known.UniqueKey(), mustParseAbsResourceInstanceStr("module.m[0].test.a").ContainingResource().UniqueKey(); got != want { + t.Errorf("wrong UniqueKey: got %#v, want %#v", got, want) + } +} + +func TestPartialExpandedAction(t *testing.T) { + act := Action{Type: "act", Name: "a"} + partial := mustParsePartialExpandedModule("module.m[0].module.n[*]").Action(act) + known := mustParseModuleInstanceStr("module.m[0]").UnexpandedAction(act) + + if got, want := partial.String(), "module.m[0].module.n[*].action.act.a[*]"; got != want { + t.Errorf("wrong String\ngot: %s\nwant: %s", got, want) + } + if got, want := partial.UnknownActionInstance().String(), "module.m[0].module.n[*].action.act.a[*]"; got != want { + t.Errorf("wrong UnknownActionInstance\ngot: %s\nwant: %s", got, want) + } + if got, want := partial.ConfigAction().String(), "module.m.module.n.action.act.a"; got != want { + t.Errorf("wrong ConfigAction\ngot: %s\nwant: %s", got, want) + } + if _, ok := partial.AbsAction(); ok { + t.Errorf("partial-expanded module has an AbsAction") + } + if got, ok := partial.PartialExpandedModule(); !ok || got.String() != "module.m[0].module.n[*]" { + t.Errorf("wrong PartialExpandedModule: got %s, %t", got, ok) + } + if got, ok := known.AbsAction(); !ok || got.String() != "module.m[0].action.act.a" { + t.Errorf("wrong AbsAction: got %s, %t", got, ok) + } + + for _, tc := range []struct { + a, b PartialExpandedAction + want bool + }{ + {partial, partial, true}, + {partial, mustParsePartialExpandedModule("module.m[0].module.n[*]").Action(act), true}, + {known, known, true}, + {partial, known, false}, + {partial, mustParsePartialExpandedModule("module.m[1].module.n[*]").Action(act), false}, + {partial, mustParsePartialExpandedModule("module.m[0].module.n[*]").Action(Action{Type: "act", Name: "b"}), false}, + } { + if got := tc.a.Equal(tc.b); got != tc.want { + t.Errorf("%s.Equal(%s): got %t, want %t", tc.a, tc.b, got, tc.want) + } + } + + for _, tc := range []struct { + pea PartialExpandedAction + other AbsAction + want bool + }{ + {partial, mustParseModuleInstanceStr(`module.m[0].module.n["x"]`).Action("act", "a"), true}, + {partial, mustParseModuleInstanceStr(`module.m[1].module.n["x"]`).Action("act", "a"), false}, + {partial, mustParseModuleInstanceStr(`module.m[0].module.n["x"]`).Action("act", "b"), false}, + {partial, mustParseModuleInstanceStr("module.m[0]").Action("act", "a"), false}, + {known, mustParseModuleInstanceStr("module.m[0]").Action("act", "a"), true}, + {known, mustParseModuleInstanceStr("module.m[1]").Action("act", "a"), false}, + } { + if got := tc.pea.MatchesAction(tc.other); got != tc.want { + t.Errorf("%s.MatchesAction(%s): got %t, want %t", tc.pea, tc.other, got, tc.want) + } + } +} + +// mustParsePartialModuleInstance parses a module instance address which may +// use the [*] wildcard for any instance key. +func mustParsePartialModuleInstance(s string) ModuleInstance { + traversal, hclDiags := hclsyntax.ParseTraversalPartial([]byte(s), "", hcl.InitialPos) + if hclDiags.HasErrors() { + panic(hclDiags.Error()) + } + addr, remain, diags := parseModuleInstancePrefix(traversal, wildcardInstanceKeys) if diags.HasErrors() { panic(diags.ErrWithWarnings().Error()) } - return r + if len(remain) != 0 { + panic(fmt.Sprintf("%s is not a module instance address", s)) + } + return addr +} + +// mustParsePartialExpandedModule parses a partial-expanded module address, +// using [*] for each instance key which is not known. +func mustParsePartialExpandedModule(s string) PartialExpandedModule { + return mustParsePartialModuleInstance(s).PartialModule() } diff --git a/internal/addrs/provider_config.go b/internal/addrs/provider_config.go index ce845f979067..9d6a02663617 100644 --- a/internal/addrs/provider_config.go +++ b/internal/addrs/provider_config.go @@ -122,7 +122,7 @@ var _ UniqueKeyer = AbsProviderConfig{} // This type of address is typically not used prominently in the UI, except in // error messages that refer to provider configurations. func ParseAbsProviderConfig(traversal hcl.Traversal) (AbsProviderConfig, tfdiags.Diagnostics) { - modInst, remain, diags := parseModuleInstancePrefix(traversal, false) + modInst, remain, diags := parseModuleInstancePrefix(traversal, knownInstanceKeys) var ret AbsProviderConfig // Providers cannot resolve within module instances, so verify that there @@ -261,7 +261,7 @@ func ParseLegacyAbsProviderConfigStr(str string) (AbsProviderConfig, tfdiags.Dia // // We will not use this address form for any new file formats. func ParseLegacyAbsProviderConfig(traversal hcl.Traversal) (AbsProviderConfig, tfdiags.Diagnostics) { - modInst, remain, diags := parseModuleInstancePrefix(traversal, false) + modInst, remain, diags := parseModuleInstancePrefix(traversal, knownInstanceKeys) var ret AbsProviderConfig // Providers cannot resolve within module instances, so verify that there diff --git a/internal/addrs/remove_target.go b/internal/addrs/remove_target.go index 3041540b127a..140f7a4a862e 100644 --- a/internal/addrs/remove_target.go +++ b/internal/addrs/remove_target.go @@ -81,13 +81,28 @@ func ParseRemoveTarget(traversal hcl.Traversal) (*RemoveTarget, tfdiags.Diagnost return nil, diags } - if rAddr.Resource.Mode == DataResourceMode { + switch rAddr.Resource.Mode { + case DataResourceMode: diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Data source address not allowed", Detail: "Data sources are never destroyed, so they are not valid targets of removed blocks. To remove the data source from state, remove the data source block from configuration.", Subject: rng.ToHCL().Ptr(), }) + case EphemeralResourceMode: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral resource address not allowed", + Detail: "Ephemeral resources are never persisted, so they are not valid targets of removed blocks. To remove an ephemeral resource, remove its block from configuration.", + Subject: rng.ToHCL().Ptr(), + }) + case ListResourceMode: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "List resource address not allowed", + Detail: "List resources are never persisted, so they are not valid targets of removed blocks.", + Subject: rng.ToHCL().Ptr(), + }) } return &RemoveTarget{ diff --git a/internal/addrs/remove_target_test.go b/internal/addrs/remove_target_test.go index 2e129bae24e3..736e31d6ea24 100644 --- a/internal/addrs/remove_target_test.go +++ b/internal/addrs/remove_target_test.go @@ -78,11 +78,53 @@ func TestParseRemoveTarget(t *testing.T) { nil, `Resource instance keys not allowed: Resource address must be a resource (e.g. "test_instance.foo"), not a resource instance (e.g. "test_instance.foo[1]").`, }, + { + `resource.test_instance.bar`, + ConfigResource{ + Module: RootModule, + Resource: Resource{ + Mode: ManagedResourceMode, + Type: "test_instance", + Name: "bar", + }, + }, + ``, + }, + { + `module.foo[*].test_instance.bar`, + nil, + `Module instance keys not allowed: Module address must be a module (e.g. "module.foo"), not a module instance (e.g. "module.foo[1]").`, + }, + { + `test_instance.bar[*]`, + nil, + `Resource instance keys not allowed: Resource address must be a resource (e.g. "test_instance.foo"), not a resource instance (e.g. "test_instance.foo[1]").`, + }, + { + `test_instance.bar.baz`, + nil, + `Invalid address: Unexpected extra operators after address.`, + }, + { + `ephemeral.test_eph.bar`, + nil, + `Ephemeral resource address not allowed: Ephemeral resources are never persisted, so they are not valid targets of removed blocks. To remove an ephemeral resource, remove its block from configuration.`, + }, + { + `list.test_list.bar`, + nil, + `List resource address not allowed: List resources are never persisted, so they are not valid targets of removed blocks.`, + }, + { + `each.key`, + nil, + `Invalid address: The keyword "each" is reserved and cannot be used to target a resource address. If you are targeting a resource type that uses a reserved keyword, please prefix your address with "resource.".`, + }, } for _, test := range tests { t.Run(test.Input, func(t *testing.T) { - traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.InitialPos) + traversal, hclDiags := hclsyntax.ParseTraversalPartial([]byte(test.Input), "", hcl.InitialPos) if hclDiags.HasErrors() { // We're not trying to test the HCL parser here, so any // failures at this point are likely to be bugs in the diff --git a/internal/addrs/target_pattern.go b/internal/addrs/target_pattern.go index 6bec72db51b4..c780bfff290e 100644 --- a/internal/addrs/target_pattern.go +++ b/internal/addrs/target_pattern.go @@ -38,33 +38,35 @@ var _ Targetable = TargetPattern{} // newTargetPattern converts an address returned from parsing a traversal // pattern into a TargetPattern. Instance keys of WildcardKey were written as // [*], while those of NoKey were omitted, and both select every instance. -func newTargetPattern(module ModuleInstance, kind targetShapeKind, resource Resource, action Action, key InstanceKey) TargetPattern { +func newTargetPattern(addr targetShape) TargetPattern { + parsed := addr.shapeModule() p := TargetPattern{ - shape: targetShape{ - module: make(ModuleInstance, len(module)), - kind: kind, - resource: resource, - action: action, - }, - explicitModule: make([]bool, len(module)), + explicitModule: make([]bool, len(parsed)), } - for i, step := range module { + module := make(ModuleInstance, len(parsed)) + for i, step := range parsed { p.explicitModule[i] = step.InstanceKey == WildcardKey if step.InstanceKey == NoKey { step.InstanceKey = WildcardKey } - p.shape.module[i] = step + module[i] = step } - if kind != moduleTargetShape { - p.explicitKey = key == WildcardKey - if key == NoKey { - key = WildcardKey - } - p.shape.key = key + key := shapeKey(addr) + p.explicitKey = key == WildcardKey + if key == NoKey { + key = WildcardKey } + switch addr := addr.(type) { + case AbsResourceInstance: + p.shape = addr.Resource.Resource.Instance(key).Absolute(module) + case AbsActionInstance: + p.shape = addr.Action.Action.Instance(key).Absolute(module) + default: + p.shape = module + } return p } @@ -78,14 +80,15 @@ func (p TargetPattern) Contains(other Targetable) bool { // all of the instances selected by the pattern, which is either a Module, a // ConfigResource, or a ConfigAction. func (p TargetPattern) ConfigAddr() Targetable { - mod := p.shape.module.Module() - switch p.shape.kind { - case resourceTargetShape: - return ConfigResource{Module: mod, Resource: p.shape.resource} - case actionTargetShape: - return ConfigAction{Module: mod, Action: p.shape.action} + switch shape := p.shape.(type) { + case AbsResourceInstance: + return shape.ConfigResource() + case AbsActionInstance: + return shape.ConfigAction() + case ModuleInstance: + return shape.Module() default: - return mod + return RootModule } } @@ -93,7 +96,7 @@ func (p TargetPattern) ConfigAddr() Targetable { // the pattern, which is WildcardKey when every instance is selected. Patterns // selecting a module always return NoKey. func (p TargetPattern) InstanceKey() InstanceKey { - return p.shape.key + return shapeKey(p.shape) } // String returns the pattern using the same syntax it was parsed from, which @@ -135,7 +138,11 @@ func (p TargetPattern) format(allExplicit bool) string { } } - for i, step := range p.shape.module { + if p.shape == nil { + return "" + } + + for i, step := range p.shape.shapeModule() { if i > 0 { buf.WriteByte('.') } @@ -145,11 +152,11 @@ func (p TargetPattern) format(allExplicit bool) string { } var object string - switch p.shape.kind { - case resourceTargetShape: - object = p.shape.resource.String() - case actionTargetShape: - object = p.shape.action.String() + switch shape := p.shape.(type) { + case AbsResourceInstance: + object = shape.Resource.Resource.String() + case AbsActionInstance: + object = shape.Action.Action.String() default: return buf.String() } @@ -157,32 +164,49 @@ func (p TargetPattern) format(allExplicit bool) string { buf.WriteByte('.') } buf.WriteString(object) - formatKey(p.shape.key, p.explicitKey) + formatKey(shapeKey(p.shape), p.explicitKey) return buf.String() } -type targetShapeKind int +// targetShape is a common representation of every Targetable address, which +// allows Contains to use the same rules for all address types. It's +// implemented only by ModuleInstance, AbsResourceInstance, and +// AbsActionInstance. +// +// Any step of the module path, or the instance key of the resource or action, +// can be WildcardKey to select every instance. Configuration addresses select +// every instance of every module, and so all of their steps are wildcards. +// Addresses of a whole resource or action select every instance, so use +// WildcardKey for its instance key. +type targetShape interface { + Targetable + + // shapeModule returns the module path of the address. Only the address + // types which can be a targetShape implement it. + shapeModule() ModuleInstance +} -const ( - moduleTargetShape targetShapeKind = iota - resourceTargetShape - actionTargetShape +var ( + _ targetShape = ModuleInstance(nil) + _ targetShape = AbsResourceInstance{} + _ targetShape = AbsActionInstance{} ) -// targetShape is a common representation of every Targetable address, which -// allows Contains to use the same rules for all address types. -// -// Any step of the module path, or the instance key, can be WildcardKey to -// select every instance. Configuration addresses select every instance of -// every module, and so all of their steps are wildcards. Addresses of a whole -// resource or action select every instance, so use WildcardKey for the key. -type targetShape struct { - module ModuleInstance - kind targetShapeKind - resource Resource - action Action - // key is only used for resources and actions - key InstanceKey +func (m ModuleInstance) shapeModule() ModuleInstance { return m } +func (r AbsResourceInstance) shapeModule() ModuleInstance { return r.Module } +func (a AbsActionInstance) shapeModule() ModuleInstance { return a.Module } + +// shapeKey returns the instance key of the resource or action in the given +// shape, or NoKey for a module. +func shapeKey(s targetShape) InstanceKey { + switch s := s.(type) { + case AbsResourceInstance: + return s.Resource.Key + case AbsActionInstance: + return s.Action.Key + default: + return NoKey + } } // targetContains implements Contains for every Targetable address type, @@ -193,7 +217,7 @@ func targetContains(container, other Targetable) bool { return false } o, ok := shapeOf(other) - return ok && c.contains(o) + return ok && shapeContains(c, o) } // CouldContain returns true if the container could contain any of the @@ -211,31 +235,31 @@ func CouldContain(container, other Targetable) bool { return false } o, ok := shapeOf(other) - return ok && c.couldContain(o) + return ok && shapeCouldContain(c, o) } func shapeOf(addr Targetable) (targetShape, bool) { switch addr := addr.(type) { case TargetPattern: - return addr.shape, true + return addr.shape, addr.shape != nil case Module: - return targetShape{module: allModuleInstances(addr)}, true + return allModuleInstances(addr), true case ModuleInstance: - return targetShape{module: addr}, true + return addr, true case ConfigResource: - return targetShape{module: allModuleInstances(addr.Module), kind: resourceTargetShape, resource: addr.Resource, key: WildcardKey}, true + return addr.Resource.Instance(WildcardKey).Absolute(allModuleInstances(addr.Module)), true case AbsResource: - return targetShape{module: addr.Module, kind: resourceTargetShape, resource: addr.Resource, key: WildcardKey}, true + return addr.Instance(WildcardKey), true case AbsResourceInstance: - return targetShape{module: addr.Module, kind: resourceTargetShape, resource: addr.Resource.Resource, key: addr.Resource.Key}, true + return addr, true case ConfigAction: - return targetShape{module: allModuleInstances(addr.Module), kind: actionTargetShape, action: addr.Action, key: WildcardKey}, true + return addr.Action.Instance(WildcardKey).Absolute(allModuleInstances(addr.Module)), true case AbsAction: - return targetShape{module: addr.Module, kind: actionTargetShape, action: addr.Action, key: WildcardKey}, true + return addr.Instance(WildcardKey), true case AbsActionInstance: - return targetShape{module: addr.Module, kind: actionTargetShape, action: addr.Action.Action, key: addr.Action.Key}, true + return addr, true default: - return targetShape{}, false + return nil, false } } @@ -249,49 +273,50 @@ func allModuleInstances(m Module) ModuleInstance { return ret } -// contains returns true if every address selected by other is also selected -// by the receiver. A module contains everything within it, while resources -// and actions only contain their own instances. -func (s targetShape) contains(other targetShape) bool { - return s.compare(other, func(key, otherKey InstanceKey) bool { +// shapeContains returns true if every address selected by other is also +// selected by s. A module contains everything within it, while resources and +// actions only contain their own instances. +func shapeContains(s, other targetShape) bool { + return shapeCompare(s, other, func(key, otherKey InstanceKey) bool { return key == WildcardKey || key == otherKey }) } -// couldContain is like contains, but treats any wildcards in other as -// instance keys which are not yet known, and so returns true if any of the -// addresses which other could eventually represent might be contained by the -// receiver. -func (s targetShape) couldContain(other targetShape) bool { - return s.compare(other, func(key, otherKey InstanceKey) bool { +// shapeCouldContain is like shapeContains, but treats any wildcards in other +// as instance keys which are not yet known, and so returns true if any of the +// addresses which other could eventually represent might be contained by s. +func shapeCouldContain(s, other targetShape) bool { + return shapeCompare(s, other, func(key, otherKey InstanceKey) bool { return key == WildcardKey || otherKey == WildcardKey || key == otherKey }) } -func (s targetShape) compare(other targetShape, keysMatch func(key, otherKey InstanceKey) bool) bool { - if len(other.module) < len(s.module) { +func shapeCompare(s, other targetShape, keysMatch func(key, otherKey InstanceKey) bool) bool { + module, otherModule := s.shapeModule(), other.shapeModule() + if len(otherModule) < len(module) { return false } - for i, step := range s.module { - otherStep := other.module[i] + for i, step := range module { + otherStep := otherModule[i] if step.Name != otherStep.Name || !keysMatch(step.InstanceKey, otherStep.InstanceKey) { return false } } - switch s.kind { - case moduleTargetShape: + switch s := s.(type) { + case ModuleInstance: + // A module contains everything within it. return true - case resourceTargetShape: - return other.kind == resourceTargetShape && - len(other.module) == len(s.module) && - s.resource.Equal(other.resource) && - keysMatch(s.key, other.key) - case actionTargetShape: - return other.kind == actionTargetShape && - len(other.module) == len(s.module) && - s.action.Equal(other.action) && - keysMatch(s.key, other.key) + case AbsResourceInstance: + o, ok := other.(AbsResourceInstance) + return ok && len(o.Module) == len(s.Module) && + s.Resource.Resource.Equal(o.Resource.Resource) && + keysMatch(s.Resource.Key, o.Resource.Key) + case AbsActionInstance: + o, ok := other.(AbsActionInstance) + return ok && len(o.Module) == len(s.Module) && + s.Action.Action.Equal(o.Action.Action) && + keysMatch(s.Action.Key, o.Action.Key) default: return false } diff --git a/internal/configs/import.go b/internal/configs/import.go index 8bfbdbdc34fe..b482ac542358 100644 --- a/internal/configs/import.go +++ b/internal/configs/import.go @@ -141,12 +141,19 @@ var importBlockSchema = &hcl.BodySchema{ // block's "to" to the configuration address before the full instance // expressions are evaluated. func parseConfigResourceFromExpression(expr hcl.Expression) (addrs.ConfigResource, tfdiags.Diagnostics) { - traversal, hcdiags := exprToResourceTraversal(expr) + // The "to" address can use each.key and each.value in its instance keys, + // which aren't needed to find the configuration address, so they are + // treated as wildcards here. + addrExpr, hcdiags := addrs.ParseAddressExpr(expr) + if hcdiags.HasErrors() { + return addrs.ConfigResource{}, tfdiags.Diagnostics(nil).Append(hcdiags) + } + traversal, hcdiags := addrExpr.Traversal(nil) if hcdiags.HasErrors() { return addrs.ConfigResource{}, tfdiags.Diagnostics(nil).Append(hcdiags) } - addr, diags := addrs.ParseAbsResourceInstance(traversal) + addr, diags := addrs.ParsePartialResourceInstance(traversal) if diags.HasErrors() { return addrs.ConfigResource{}, diags } @@ -189,47 +196,6 @@ func unwrapJSONRefExpr(expr hcl.Expression) (hcl.Expression, hcl.Diagnostics) { return expr, diags } -// exprToResourceTraversal is used to parse the import block's to expression, -// which must be a resource instance, but may contain limited variables with -// index expressions. Since we only need the ConfigResource to connect the -// import to the configuration, we skip any index expressions. -func exprToResourceTraversal(expr hcl.Expression) (hcl.Traversal, hcl.Diagnostics) { - var trav hcl.Traversal - var diags hcl.Diagnostics - - switch e := expr.(type) { - case *hclsyntax.RelativeTraversalExpr: - t, d := exprToResourceTraversal(e.Source) - diags = diags.Extend(d) - trav = append(trav, t...) - trav = append(trav, e.Traversal...) - - case *hclsyntax.ScopeTraversalExpr: - // a static reference, we can just append the traversal - trav = append(trav, e.Traversal...) - - case *hclsyntax.IndexExpr: - // Get the collection from the index expression, we don't need the - // index for a ConfigResource - t, d := exprToResourceTraversal(e.Collection) - diags = diags.Extend(d) - if diags.HasErrors() { - return nil, diags - } - trav = append(trav, t...) - - default: - // if we don't recognise the expression type (which means we are likely - // dealing with a test mock), try and interpret this as an absolute - // traversal - t, d := hcl.AbsTraversalForExpr(e) - diags = diags.Extend(d) - trav = append(trav, t...) - } - - return trav, diags -} - // parseImportToStatic attempts to parse the To address of an import block // statically to get the resource address. This returns false when the address // cannot be parsed, which is usually a result of dynamic index expressions diff --git a/internal/moduletest/mocking/overrides.go b/internal/moduletest/mocking/overrides.go index e9ff0b602f4a..28241520c478 100644 --- a/internal/moduletest/mocking/overrides.go +++ b/internal/moduletest/mocking/overrides.go @@ -199,17 +199,10 @@ func matchingOverrides(overrides addrs.Map[addrs.TargetPattern, *configs.Overrid // is contained by the targets of the others. If targets overlap without // either containing the other, the earliest candidate takes precedence. func mostSpecificOverride(candidates []addrs.MapElem[addrs.TargetPattern, *configs.Override]) (*configs.Override, bool) { - if len(candidates) == 0 { - return nil, false - } - - best := candidates[0] - for _, candidate := range candidates[1:] { - if best.Key.Contains(candidate.Key) && !candidate.Key.Contains(best.Key) { - best = candidate - } - } - return best.Value, true + best, ok := addrs.MostSpecific(candidates, func(a, b addrs.MapElem[addrs.TargetPattern, *configs.Override]) bool { + return a.Key.Contains(b.Key) + }) + return best.Value, ok } // ProviderMatch returns true if we have overrides for the given provider. diff --git a/internal/namedvals/values.go b/internal/namedvals/values.go index e97f79c1d38c..b79499899e8f 100644 --- a/internal/namedvals/values.go +++ b/internal/namedvals/values.go @@ -114,28 +114,22 @@ func (v *values[LocalType, AbsType]) GetPlaceholderResult(addr addrs.InPartialEx if !v.placeholder.Has(modAddr) { return cty.DynamicVal } - placeholders := v.placeholder.Get(modAddr) - - // We'll now search the placeholders for just the ones that match the - // given address, and take the one that has the longest known module prefix. - longestVal := cty.DynamicVal - longestLen := -1 - for _, elem := range placeholders.Elems { - candidate := elem.Key - lenKnown := candidate.ModuleLevelsKnown() - if lenKnown < longestLen { - continue - } - if !addrs.Equivalent(candidate.Local, addr.Local) { - continue + // We'll now search the placeholders for just the ones that include the + // given address, and take the most specific of them, which is the one with + // the longest known module prefix. + var candidates []addrs.MapElem[addrs.InPartialExpandedModule[LocalType], cty.Value] + for _, elem := range v.placeholder.Get(modAddr).Elems { + if addrs.Equivalent(elem.Key.Local, addr.Local) && elem.Key.Module.MatchesPartial(addr.Module) { + candidates = append(candidates, elem) } - if !candidate.Module.MatchesPartial(addr.Module) { - continue - } - longestVal = elem.Value - longestLen = lenKnown } - return longestVal + best, ok := addrs.MostSpecific(candidates, func(a, b addrs.MapElem[addrs.InPartialExpandedModule[LocalType], cty.Value]) bool { + return a.Key.Module.MatchesPartial(b.Key.Module) + }) + if !ok { + return cty.DynamicVal + } + return best.Value } diff --git a/internal/stacks/stackaddrs/removed.go b/internal/stacks/stackaddrs/removed.go index edbd13a5a426..bf01ec91eb0c 100644 --- a/internal/stacks/stackaddrs/removed.go +++ b/internal/stacks/stackaddrs/removed.go @@ -7,7 +7,6 @@ import ( "fmt" "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" @@ -137,108 +136,61 @@ func ParseRemovedFrom(expr hcl.Expression) (RemovedFrom, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - removedFrom := RemovedFrom{} - - current, moreDiags := exprToComponentTraversal(expr) + // The instance keys can be expressions, which are evaluated later with + // the context of the removed block. + steps, moreDiags := addrs.ParseAddressExpr(expr) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { return RemovedFrom{}, diags } - for current != nil { - - // we're going to parse the traversal in sets of 2-3 depending on - // the indices, so we'll check that now. - nextTraversal := current.Current - - for len(nextTraversal) > 0 { - var currentTraversal hcl.Traversal - var indexExpr hcl.Expression - - switch { - case len(nextTraversal) < 2: - // this is simply an error, we always need at least 2 values - // for either stack.name or component.name. - return RemovedFrom{}, diags.Append(diag) - case len(nextTraversal) == 2: - indexExpr = current.Index - currentTraversal = nextTraversal - nextTraversal = nil - case len(nextTraversal) == 3: - if current.Index != nil { - // this is an error, the last traversal should be taking - // its index from the outer value if it exists, and to be - // exactly three means something is invalid somewhere. - return RemovedFrom{}, diags.Append(diag) - } - - index, ok := nextTraversal[2].(hcl.TraverseIndex) - if !ok { - // This is an error, with exactly 3 we don't have another - // traversal to go to after this so the last entry must - // be the index. - return RemovedFrom{}, diags.Append(diag) - } - - currentTraversal = nextTraversal - nextTraversal = nil - indexExpr = hcl.StaticExpr(index.Key, index.SrcRange) - - default: // len(nextTraversal) > 3 - if index, ok := nextTraversal[2].(hcl.TraverseIndex); ok { - currentTraversal = nextTraversal[:3] - nextTraversal = nextTraversal[3:] - indexExpr = hcl.StaticExpr(index.Key, index.SrcRange) - break - } - currentTraversal = nextTraversal[:2] - nextTraversal = nextTraversal[2:] + removedFrom := RemovedFrom{} + for len(steps) > 0 { + // Each part of the address is either "stack" or "component", followed + // by a name and an optional instance key. + if len(steps) < 2 { + return RemovedFrom{}, diags.Append(diag) + } + kind, ok := addressExprStepName(steps[0]) + if !ok { + return RemovedFrom{}, diags.Append(diag) + } + name, ok := steps[1].Step.(hcl.TraverseAttr) + if !ok { + return RemovedFrom{}, diags.Append(diag) + } + steps = steps[2:] + + var index hcl.Expression + if len(steps) > 0 { + if steps[0].Key != nil { + index = steps[0].Key + steps = steps[1:] + } else if idx, ok := steps[0].Step.(hcl.TraverseIndex); ok { + index = hcl.StaticExpr(idx.Key, idx.SrcRange) + steps = steps[1:] } + } - var name string - - switch root := currentTraversal[0].(type) { - case hcl.TraverseRoot: - name = root.Name - case hcl.TraverseAttr: - name = root.Name - default: + switch kind { + case "component": + // A component must be the last part of the address. + if len(steps) > 0 { return RemovedFrom{}, diags.Append(diag) } - - switch name { - case "component": - name, ok := currentTraversal[1].(hcl.TraverseAttr) - if !ok { - return RemovedFrom{}, diags.Append(diag) - } - - if len(nextTraversal) > 0 || current.Rest != nil { - return RemovedFrom{}, diags.Append(diag) - } - - removedFrom.Component = &ComponentRemovedFrom{ - Name: name.Name, - Index: indexExpr, - } - return removedFrom, diags - case "stack": - name, ok := currentTraversal[1].(hcl.TraverseAttr) - if !ok { - return RemovedFrom{}, diags.Append(diag) - } - - removedFrom.Stack = append(removedFrom.Stack, StackRemovedFrom{ - Name: name.Name, - Index: indexExpr, - }) - - default: - return RemovedFrom{}, diags.Append(diag) + removedFrom.Component = &ComponentRemovedFrom{ + Name: name.Name, + Index: index, } + return removedFrom, diags + case "stack": + removedFrom.Stack = append(removedFrom.Stack, StackRemovedFrom{ + Name: name.Name, + Index: index, + }) + default: + return RemovedFrom{}, diags.Append(diag) } - - current = current.Rest } // if we fall out, then we're just targeting a stack directly instead of a @@ -246,70 +198,16 @@ func ParseRemovedFrom(expr hcl.Expression) (RemovedFrom, tfdiags.Diagnostics) { return removedFrom, diags } -type parsedFromExpr struct { - Current hcl.Traversal - Index hcl.Expression - Rest *parsedFromExpr -} - -// exprToComponentTraversal converts an HCL expression into a traversal that -// represents the component being targeted. We have to handle parsing this -// ourselves because removed block from arguments can contain index expressions -// which are not supported by hcl.AbsTraversalForExpr. -// -// The return values are (1) the part of the expression that can be converted -// into a traversal, (2) the index at the end of the traversal if it is an -// expression, (3) the remainder of the expression that needs to be parsed -// after (1) has been, and (4) the diagnostics. -func exprToComponentTraversal(expr hcl.Expression) (*parsedFromExpr, hcl.Diagnostics) { - switch e := expr.(type) { - case *hclsyntax.IndexExpr: - - current, diags := exprToComponentTraversal(e.Collection) - if diags.HasErrors() { - return nil, diags - } - - for next := current; next != nil; next = next.Rest { - if next.Rest == nil { - next.Index = e.Key - } - } - - return current, diags - - case *hclsyntax.RelativeTraversalExpr: - - current, diags := exprToComponentTraversal(e.Source) - if diags.HasErrors() { - return nil, diags - } - - for next := current; next != nil; next = next.Rest { - if next.Rest == nil { - next.Rest = &parsedFromExpr{ - Current: e.Traversal, - } - break - } - } - - return current, diags - +// addressExprStepName returns the name in the given step of an address, if it +// isn't an instance key. +func addressExprStepName(step addrs.AddressExprStep) (string, bool) { + switch step := step.Step.(type) { + case hcl.TraverseRoot: + return step.Name, true + case hcl.TraverseAttr: + return step.Name, true default: - - // For anything else, just rely on the default traversal logic. - - t, diags := hcl.AbsTraversalForExpr(expr) - if diags.HasErrors() { - return nil, diags - } - return &parsedFromExpr{ - Current: t, - Index: nil, - Rest: nil, - }, diags - + return "", false } } diff --git a/internal/terraform/eval_import.go b/internal/terraform/eval_import.go index 2592b81e9997..573eca855c3b 100644 --- a/internal/terraform/eval_import.go +++ b/internal/terraform/eval_import.go @@ -7,7 +7,6 @@ import ( "fmt" "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" @@ -149,73 +148,43 @@ func evalImportToExpression(expr hcl.Expression, keyData instances.RepetitionDat } func evalImportUnknownToExpression(expr hcl.Expression) (addrs.PartialExpandedResource, tfdiags.Diagnostics) { - var per addrs.PartialExpandedResource - var diags tfdiags.Diagnostics - traversal, diags := importToExprToTraversal(expr, instances.UnknownForEachRepetitionData(cty.DynamicPseudoType)) if diags.HasErrors() { - return per, diags + return addrs.PartialExpandedResource{}, diags } - per, moreDiags := parseImportToPartialAddress(traversal) + // The unknown keys are wildcards in the partial address. + addr, moreDiags := addrs.ParsePartialResourceInstance(traversal) diags = diags.Append(moreDiags) - return per, diags + if moreDiags.HasErrors() { + return addrs.PartialExpandedResource{}, diags + } + return addr.PartialResource(), diags } -// trggersExprToTraversal takes an hcl expression limited to the syntax allowed -// in replace_triggered_by, and converts it to a static traversal. The -// RepetitionData contains the data necessary to evaluate the only allowed -// variables in the expression, count.index and each.key. +// importToExprToTraversal converts the "to" expression of an import block to a +// traversal, by evaluating any instance keys which reference each.key or +// each.value using the given repetition data. func importToExprToTraversal(expr hcl.Expression, keyData instances.RepetitionData) (hcl.Traversal, tfdiags.Diagnostics) { - var trav hcl.Traversal var diags tfdiags.Diagnostics - switch e := expr.(type) { - case *hclsyntax.RelativeTraversalExpr: - t, d := importToExprToTraversal(e.Source, keyData) - diags = diags.Append(d) - trav = append(trav, t...) - trav = append(trav, e.Traversal...) - - case *hclsyntax.ScopeTraversalExpr: - // a static reference, we can just append the traversal - trav = append(trav, e.Traversal...) - - case *hclsyntax.IndexExpr: - // Get the collection from the index expression - t, d := importToExprToTraversal(e.Collection, keyData) - diags = diags.Append(d) - if diags.HasErrors() { - return nil, diags - } - trav = append(trav, t...) - - // The index key is the only place where we could have variables that - // reference count and each, so we need to parse those independently. - idx, hclDiags := parseImportToKeyExpression(e.Key, keyData) - diags = diags.Append(hclDiags) - - trav = append(trav, idx) - - default: - // if we don't recognise the expression type (which means we are likely - // dealing with a test mock), try and interpret this as an absolute - // traversal - t, d := hcl.AbsTraversalForExpr(e) - diags = diags.Append(d) - trav = append(trav, t...) + addrExpr, hclDiags := addrs.ParseAddressExpr(expr) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + return nil, diags } - return trav, diags + traversal, hclDiags := addrExpr.Traversal(func(key hcl.Expression) (cty.Value, hcl.Diagnostics) { + return evalImportToKeyExpression(key, keyData) + }) + diags = diags.Append(hclDiags) + return traversal, diags } -// parseImportToKeyExpression takes an hcl.Expression and parses it as an index key, while -// evaluating any references to count.index or each.key. -func parseImportToKeyExpression(expr hcl.Expression, keyData instances.RepetitionData) (hcl.TraverseIndex, hcl.Diagnostics) { - idx := hcl.TraverseIndex{ - SrcRange: expr.Range(), - } - +// evalImportToKeyExpression evaluates an instance key expression in the "to" +// expression of an import block, where the only valid references are to +// each.key and each.value. +func evalImportToKeyExpression(expr hcl.Expression, keyData instances.RepetitionData) (cty.Value, hcl.Diagnostics) { ctx := &hcl.EvalContext{ Variables: map[string]cty.Value{ "each": cty.ObjectVal(map[string]cty.Value{ @@ -235,7 +204,7 @@ func parseImportToKeyExpression(expr hcl.Expression, keyData instances.Repetitio } } - return idx, diags + return cty.NilVal, diags } if marks.Has(val, marks.Sensitive) { @@ -245,28 +214,8 @@ func parseImportToKeyExpression(expr hcl.Expression, keyData instances.Repetitio Detail: "Import address index expression cannot be sensitive.", Subject: expr.Range().Ptr(), }) - return idx, diags - } - - idx.Key = val - return idx, nil - -} - -func parseImportToPartialAddress(traversal hcl.Traversal) (addrs.PartialExpandedResource, tfdiags.Diagnostics) { - partial, rest, diags := addrs.ParsePartialExpandedResource(traversal) - if diags.HasErrors() { - return addrs.PartialExpandedResource{}, diags - } - - if len(rest) > 0 { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid import 'to' expression", - Detail: "The import block 'to' argument does not resolve to a single resource instance.", - Subject: traversal.SourceRange().Ptr(), - }) + return cty.NilVal, diags } - return partial, diags + return val, nil }