2023-09-27 19:59:27 -04:00
|
|
|
// Copyright IBM Corp. 2014, 2026
|
|
|
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
|
|
2023-05-26 15:03:47 -04:00
|
|
|
package stackaddrs
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
|
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Reference describes a reference expression found in the configuration,
|
|
|
|
|
// capturing what it referred to and where it was found in source code.
|
|
|
|
|
type Reference struct {
|
|
|
|
|
Target Referenceable
|
|
|
|
|
SourceRange tfdiags.SourceRange
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ParseReference raises a raw absolute traversal into a higher-level reference,
|
|
|
|
|
// or returns error diagnostics explaining why it cannot.
|
|
|
|
|
//
|
|
|
|
|
// The returned traversal is a relative traversal covering the remainder of
|
|
|
|
|
// the given traversal after the part captured into the returned reference,
|
|
|
|
|
// in case the caller wants to do further validation or analysis of the
|
|
|
|
|
// subsequent steps.
|
|
|
|
|
func ParseReference(traversal hcl.Traversal) (Reference, hcl.Traversal, tfdiags.Diagnostics) {
|
|
|
|
|
var diags tfdiags.Diagnostics
|
|
|
|
|
var ret Reference
|
|
|
|
|
switch rootName := traversal.RootName(); rootName {
|
|
|
|
|
|
|
|
|
|
case "var":
|
|
|
|
|
name, rng, remain, diags := parseSingleAttrRef(traversal)
|
|
|
|
|
ret.Target = InputVariable{Name: name}
|
|
|
|
|
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
|
|
|
|
|
return ret, remain, diags
|
|
|
|
|
|
|
|
|
|
case "local":
|
|
|
|
|
name, rng, remain, diags := parseSingleAttrRef(traversal)
|
|
|
|
|
ret.Target = LocalValue{Name: name}
|
|
|
|
|
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
|
|
|
|
|
return ret, remain, diags
|
|
|
|
|
|
|
|
|
|
case "component":
|
|
|
|
|
name, rng, remain, diags := parseSingleAttrRef(traversal)
|
|
|
|
|
ret.Target = Component{Name: name}
|
|
|
|
|
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
|
|
|
|
|
return ret, remain, diags
|
|
|
|
|
|
|
|
|
|
case "stack":
|
|
|
|
|
name, rng, remain, diags := parseSingleAttrRef(traversal)
|
|
|
|
|
ret.Target = StackCall{Name: name}
|
|
|
|
|
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
|
|
|
|
|
return ret, remain, diags
|
|
|
|
|
|
2023-08-30 20:13:16 -04:00
|
|
|
case "provider":
|
|
|
|
|
target, rng, remain, diags := parseProviderRef(traversal)
|
|
|
|
|
ret.Target = target
|
|
|
|
|
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
|
|
|
|
|
return ret, remain, diags
|
|
|
|
|
|
2023-11-07 20:56:26 -05:00
|
|
|
case "each", "count":
|
|
|
|
|
attrName, rng, remain, diags := parseSingleAttrRef(traversal)
|
|
|
|
|
if diags.HasErrors() {
|
|
|
|
|
return ret, nil, diags
|
|
|
|
|
}
|
|
|
|
|
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
|
|
|
|
|
|
|
|
|
|
switch rootName {
|
|
|
|
|
case "each":
|
|
|
|
|
switch attrName {
|
|
|
|
|
case "key":
|
|
|
|
|
ret.Target = EachKey
|
|
|
|
|
return ret, remain, diags
|
|
|
|
|
case "value":
|
|
|
|
|
ret.Target = EachValue
|
|
|
|
|
return ret, remain, diags
|
|
|
|
|
}
|
|
|
|
|
case "count":
|
|
|
|
|
switch attrName {
|
|
|
|
|
case "index":
|
|
|
|
|
ret.Target = CountIndex
|
|
|
|
|
return ret, remain, diags
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// If we get here then rootName and attrName are not a valid combination.
|
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
|
Summary: "Reference to unknown symbol",
|
|
|
|
|
Detail: fmt.Sprintf("The object %q has no attribute named %q.", rootName, attrName),
|
|
|
|
|
Subject: traversal[1].SourceRange().Ptr(),
|
|
|
|
|
})
|
|
|
|
|
return ret, nil, diags
|
|
|
|
|
|
|
|
|
|
case "self":
|
|
|
|
|
ret.Target = Self
|
|
|
|
|
ret.SourceRange = tfdiags.SourceRangeFromHCL(traversal[0].SourceRange())
|
|
|
|
|
return ret, traversal[1:], diags
|
|
|
|
|
|
2024-06-14 20:31:36 -04:00
|
|
|
case "terraform":
|
|
|
|
|
attrName, rng, remain, diags := parseSingleAttrRef(traversal)
|
|
|
|
|
if diags.HasErrors() {
|
|
|
|
|
return ret, nil, diags
|
|
|
|
|
}
|
|
|
|
|
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
|
|
|
|
|
|
|
|
|
|
switch attrName {
|
|
|
|
|
case "applying":
|
|
|
|
|
ret.Target = TerraformApplying
|
|
|
|
|
return ret, remain, diags
|
|
|
|
|
default:
|
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
|
Summary: "Reference to unknown symbol",
|
|
|
|
|
Detail: fmt.Sprintf("The object %q has no attribute named %q.", rootName, attrName),
|
|
|
|
|
Subject: traversal[1].SourceRange().Ptr(),
|
|
|
|
|
})
|
|
|
|
|
return ret, remain, diags
|
|
|
|
|
}
|
|
|
|
|
|
stackeval: Test-only globals
Because many stacks language features are dependent on others to do useful
work, it's tempting to focus only on integration testing of combinations
of features used together. However, we known from our experience with the
modules runtime (the "terraform" package) that over time this becomes a
huge maintenance burden, because any non-trivial change tends to
invalidate hundreds or thousands of integration tests, and because of their
broad scope its often hard in retrospect to figure out what exactly a
particular test was aiming to test vs. what it was just relying on as a
side-effect.
To try to minimize these cross-dependencies and thus enable something
closer to unit testing, here we introduce a special kind of symbol to the
stacks language which is available only to unit tests in this package.
"Test-only globals" -- an intentionally-clunky name to avoid squatting on
useful names -- can be set as part of the Main object and, when defined,
are available for use in all situations where we perform expression
evaluation against a stack. The "globals" in the name represents that,
unlike just about everything else, they are defined once but available in
all stacks in the configuration tree.
This design is a tradeoff: it introduces a bunch of extra code that is
here entirely to support testing, but hopefully this code is segregated
enough from everything else that it's unlikely to change significantly
under future maintenance, thereby hopefully minimizing the need for future
cross-cutting test maintenance too.
2023-11-06 13:28:39 -05:00
|
|
|
case "_test_only_global":
|
|
|
|
|
name, rng, remain, diags := parseSingleAttrRef(traversal)
|
|
|
|
|
ret.Target = TestOnlyGlobal{Name: name}
|
|
|
|
|
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
|
|
|
|
|
return ret, remain, diags
|
|
|
|
|
|
2023-05-26 15:03:47 -04:00
|
|
|
default:
|
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
|
Summary: "Reference to unknown symbol",
|
|
|
|
|
Detail: fmt.Sprintf("There is no symbol %q defined in the current scope.", rootName),
|
|
|
|
|
Subject: traversal[0].SourceRange().Ptr(),
|
|
|
|
|
})
|
|
|
|
|
return ret, nil, diags
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseSingleAttrRef(traversal hcl.Traversal) (string, hcl.Range, hcl.Traversal, tfdiags.Diagnostics) {
|
|
|
|
|
var diags tfdiags.Diagnostics
|
|
|
|
|
|
|
|
|
|
root := traversal.RootName()
|
|
|
|
|
rootRange := traversal[0].SourceRange()
|
|
|
|
|
|
|
|
|
|
if len(traversal) < 2 {
|
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
|
Summary: "Invalid reference",
|
|
|
|
|
Detail: fmt.Sprintf("The %q object cannot be accessed directly. Instead, access one of its attributes.", root),
|
|
|
|
|
Subject: &rootRange,
|
|
|
|
|
})
|
|
|
|
|
return "", hcl.Range{}, nil, diags
|
|
|
|
|
}
|
|
|
|
|
if attrTrav, ok := traversal[1].(hcl.TraverseAttr); ok {
|
|
|
|
|
return attrTrav.Name, hcl.RangeBetween(rootRange, attrTrav.SrcRange), traversal[2:], diags
|
|
|
|
|
}
|
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
|
Summary: "Invalid reference",
|
|
|
|
|
Detail: fmt.Sprintf("The %q object does not support this operation.", root),
|
|
|
|
|
Subject: traversal[1].SourceRange().Ptr(),
|
|
|
|
|
})
|
|
|
|
|
return "", hcl.Range{}, nil, diags
|
|
|
|
|
}
|
2023-08-30 20:13:16 -04:00
|
|
|
|
|
|
|
|
func parseProviderRef(traversal hcl.Traversal) (ProviderConfigRef, hcl.Range, hcl.Traversal, tfdiags.Diagnostics) {
|
|
|
|
|
var diags tfdiags.Diagnostics
|
|
|
|
|
|
|
|
|
|
if len(traversal) < 3 {
|
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
|
Summary: "Invalid reference",
|
|
|
|
|
Detail: "The \"provider\" symbol must be followed by two attribute access operations, selecting a provider type and a provider configuration name.",
|
|
|
|
|
Subject: traversal.SourceRange().Ptr(),
|
|
|
|
|
})
|
|
|
|
|
return ProviderConfigRef{}, hcl.Range{}, nil, diags
|
|
|
|
|
}
|
|
|
|
|
if typeTrav, ok := traversal[1].(hcl.TraverseAttr); ok {
|
|
|
|
|
if nameTrav, ok := traversal[2].(hcl.TraverseAttr); ok {
|
|
|
|
|
ret := ProviderConfigRef{
|
|
|
|
|
ProviderLocalName: typeTrav.Name,
|
|
|
|
|
Name: nameTrav.Name,
|
|
|
|
|
}
|
|
|
|
|
return ret, traversal.SourceRange(), traversal[3:], diags
|
|
|
|
|
} else {
|
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
|
Summary: "Invalid reference",
|
|
|
|
|
Detail: "The \"provider\" object's attributes do not support this operation.",
|
|
|
|
|
Subject: traversal[1].SourceRange().Ptr(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
|
Summary: "Invalid reference",
|
|
|
|
|
Detail: "The \"provider\" object does not support this operation.",
|
|
|
|
|
Subject: traversal[1].SourceRange().Ptr(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return ProviderConfigRef{}, hcl.Range{}, nil, diags
|
|
|
|
|
}
|
2023-12-08 21:27:47 -05:00
|
|
|
|
|
|
|
|
func (r Reference) Absolute(stack StackInstance) AbsReference {
|
|
|
|
|
return AbsReference{
|
|
|
|
|
Stack: stack,
|
|
|
|
|
Ref: r,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AbsReference is an absolute form of [Reference] that is to be resolved
|
|
|
|
|
// in the global scope of a particular stack.
|
|
|
|
|
//
|
|
|
|
|
// It's not meaningful to use this type for references to objects that exist
|
|
|
|
|
// only in a more specific scope, such as each.key, each.value, etc, because
|
|
|
|
|
// those would require additional information about exactly which object
|
|
|
|
|
// they are being resolved in terms of.
|
|
|
|
|
type AbsReference struct {
|
|
|
|
|
Stack StackInstance
|
|
|
|
|
Ref Reference
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r AbsReference) Target() AbsReferenceable {
|
|
|
|
|
return AbsReferenceable{
|
|
|
|
|
Stack: r.Stack,
|
|
|
|
|
Item: r.Ref.Target,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r AbsReference) SourceRange() tfdiags.SourceRange {
|
|
|
|
|
return r.Ref.SourceRange
|
|
|
|
|
}
|