Introduce separate testing scope for reference validation (#33339)

This commit is contained in:
Liam Cervante 2023-06-28 09:47:24 +02:00 committed by GitHub
parent dfc26c2ac4
commit 212ae6c4ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 477 additions and 78 deletions

View File

@ -16,10 +16,11 @@ import (
// that is defining it.
//
// This is related to but separate from ModuleCallOutput, which represents
// a module output from the perspective of its parent module. Since output
// values cannot be represented from the module where they are defined,
// OutputValue is not Referenceable, while ModuleCallOutput is.
// a module output from the perspective of its parent module. Outputs are
// referencable from the testing scope, in general terraform operation users
// will be referencing ModuleCallOutput.
type OutputValue struct {
referenceable
Name string
}
@ -27,6 +28,16 @@ func (v OutputValue) String() string {
return "output." + v.Name
}
func (v OutputValue) Equal(o OutputValue) bool {
return v.Name == o.Name
}
func (v OutputValue) UniqueKey() UniqueKey {
return v // An OutputValue is its own UniqueKey
}
func (v OutputValue) uniqueKeySigil() {}
// Absolute converts the receiver into an absolute address within the given
// module instance.
func (v OutputValue) Absolute(m ModuleInstance) AbsOutputValue {
@ -82,7 +93,7 @@ func (v AbsOutputValue) String() string {
}
func (v AbsOutputValue) Equal(o AbsOutputValue) bool {
return v.OutputValue == o.OutputValue && v.Module.Equal(o.Module)
return v.OutputValue.Equal(o.OutputValue) && v.Module.Equal(o.Module)
}
func (v AbsOutputValue) ConfigOutputValue() ConfigOutputValue {

View File

@ -9,8 +9,9 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// Reference describes a reference to an address with source location
@ -82,6 +83,47 @@ func ParseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) {
return ref, diags
}
// ParseRefFromTestingScope adds check blocks and outputs into the available
// references returned by ParseRef.
//
// The testing files and functionality have a slightly expanded referencing
// scope and so should use this function to retrieve references.
func ParseRefFromTestingScope(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) {
root := traversal.RootName()
var diags tfdiags.Diagnostics
var reference *Reference
switch root {
case "output":
name, rng, remain, outputDiags := parseSingleAttrRef(traversal)
reference = &Reference{
Subject: OutputValue{Name: name},
SourceRange: tfdiags.SourceRangeFromHCL(rng),
Remaining: remain,
}
diags = outputDiags
case "check":
name, rng, remain, checkDiags := parseSingleAttrRef(traversal)
reference = &Reference{
Subject: Check{Name: name},
SourceRange: tfdiags.SourceRangeFromHCL(rng),
Remaining: remain,
}
diags = checkDiags
}
if reference != nil {
if len(reference.Remaining) == 0 {
reference.Remaining = nil
}
return reference, diags
}
// If it's not an output or a check block, then just parse it as normal.
return ParseRef(traversal)
}
// ParseRefStr is a helper wrapper around ParseRef that takes a string
// and parses it with the HCL native syntax traversal parser before
// interpreting it.
@ -111,6 +153,22 @@ func ParseRefStr(str string) (*Reference, tfdiags.Diagnostics) {
return ref, diags
}
// ParseRefStrFromTestingScope matches ParseRefStr except it supports the
// references supported by ParseRefFromTestingScope.
func ParseRefStrFromTestingScope(str string) (*Reference, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1})
diags = diags.Append(parseDiags)
if parseDiags.HasErrors() {
return nil, diags
}
ref, targetDiags := ParseRefFromTestingScope(traversal)
diags = diags.Append(targetDiags)
return ref, diags
}
func parseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics

View File

@ -9,10 +9,117 @@ import (
"github.com/go-test/deep"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/tfdiags"
)
func TestParseRefInTestingScope(t *testing.T) {
tests := []struct {
Input string
Want *Reference
WantErr string
}{
{
`output.value`,
&Reference{
Subject: OutputValue{
Name: "value",
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 13, Byte: 12},
},
},
``,
},
{
`output`,
nil,
`The "output" object cannot be accessed directly. Instead, access one of its attributes.`,
},
{
`output["foo"]`,
nil,
`The "output" object does not support this operation.`,
},
{
`check.health`,
&Reference{
Subject: Check{
Name: "health",
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 13, Byte: 12},
},
},
``,
},
{
`check`,
nil,
`The "check" object cannot be accessed directly. Instead, access one of its attributes.`,
},
{
`check["foo"]`,
nil,
`The "check" object does not support this operation.`,
},
// Sanity check at least one of the others works to verify it does
// fall through to the core function.
{
`count.index`,
&Reference{
Subject: CountAttr{
Name: "index",
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 12, Byte: 11},
},
},
``,
},
}
for _, test := range tests {
t.Run(test.Input, func(t *testing.T) {
traversal, travDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.Pos{Line: 1, Column: 1})
if travDiags.HasErrors() {
t.Fatal(travDiags.Error())
}
got, diags := ParseRefFromTestingScope(traversal)
switch len(diags) {
case 0:
if test.WantErr != "" {
t.Fatalf("succeeded; want error: %s", test.WantErr)
}
case 1:
if test.WantErr == "" {
t.Fatalf("unexpected diagnostics: %s", diags.Err())
}
if got, want := diags[0].Description().Detail, test.WantErr; got != want {
t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want)
}
default:
t.Fatalf("too many diagnostics: %s", diags.Err())
}
if diags.HasErrors() {
return
}
for _, problem := range deep.Equal(got, test.Want) {
t.Errorf(problem)
}
})
}
}
func TestParseRef(t *testing.T) {
tests := []struct {
Input string
@ -719,6 +826,38 @@ func TestParseRef(t *testing.T) {
nil,
`A reference to a resource type must be followed by at least one attribute access, specifying the resource name.`,
},
// Should interpret checks and outputs as resource types.
{
`output.value`,
&Reference{
Subject: Resource{
Mode: ManagedResourceMode,
Type: "output",
Name: "value",
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 13, Byte: 12},
},
},
``,
},
{
`check.health`,
&Reference{
Subject: Resource{
Mode: ManagedResourceMode,
Type: "check",
Name: "health",
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 13, Byte: 12},
},
},
``,
},
}
for _, test := range tests {

View File

@ -10,12 +10,13 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/lang"
"github.com/hashicorp/terraform/internal/lang/blocktoattr"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
)
// expression represents any unparsed expression
@ -47,7 +48,7 @@ func marshalExpression(ex hcl.Expression) expression {
ret.ConstantValue = valJSON
}
refs, _ := lang.ReferencesInExpr(ex)
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, ex)
if len(refs) > 0 {
var varString []string
for _, ref := range refs {

View File

@ -53,7 +53,7 @@ func (cr *CheckRule) validateSelfReferences(checkType string, addr addrs.Resourc
if expr == nil {
continue
}
refs, _ := lang.References(expr.Variables())
refs, _ := lang.References(addrs.ParseRef, expr.Variables())
for _, ref := range refs {
var refAddr addrs.Resource

View File

@ -568,7 +568,7 @@ func decodeReplaceTriggeredBy(expr hcl.Expression) ([]hcl.Expression, hcl.Diagno
exprs[i] = expr
}
refs, refDiags := lang.ReferencesInExpr(expr)
refs, refDiags := lang.ReferencesInExpr(addrs.ParseRef, expr)
for _, diag := range refDiags {
severity := hcl.DiagError
if diag.Severity() == tfdiags.Warning {

View File

@ -4,9 +4,10 @@
package lang
import (
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// Data is an interface whose implementations can provide cty.Value
@ -33,4 +34,6 @@ type Data interface {
GetPathAttr(addrs.PathAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
GetTerraformAttr(addrs.TerraformAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
GetInputVariable(addrs.InputVariable, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
GetOutput(addrs.OutputValue, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
GetCheckBlock(addrs.Check, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
}

View File

@ -4,9 +4,10 @@
package lang
import (
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
type dataForTests struct {
@ -14,10 +15,12 @@ type dataForTests struct {
ForEachAttrs map[string]cty.Value
Resources map[string]cty.Value
LocalValues map[string]cty.Value
OutputValues map[string]cty.Value
Modules map[string]cty.Value
PathAttrs map[string]cty.Value
TerraformAttrs map[string]cty.Value
InputVariables map[string]cty.Value
CheckBlocks map[string]cty.Value
}
var _ Data = &dataForTests{}
@ -63,3 +66,11 @@ func (d *dataForTests) GetPathAttr(addr addrs.PathAttr, rng tfdiags.SourceRange)
func (d *dataForTests) GetTerraformAttr(addr addrs.TerraformAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
return d.TerraformAttrs[addr.Name], nil
}
func (d *dataForTests) GetOutput(addr addrs.OutputValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
return d.OutputValues[addr.Name], nil
}
func (d *dataForTests) GetCheckBlock(addr addrs.Check, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
return d.CheckBlocks[addr.Name], nil
}

View File

@ -9,13 +9,14 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/ext/dynblock"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/lang/blocktoattr"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
)
// ExpandBlock expands any "dynamic" blocks present in the given body. The
@ -28,7 +29,7 @@ func (s *Scope) ExpandBlock(body hcl.Body, schema *configschema.Block) (hcl.Body
spec := schema.DecoderSpec()
traversals := dynblock.ExpandVariablesHCLDec(body, spec)
refs, diags := References(traversals)
refs, diags := References(s.ParseRef, traversals)
ctx, ctxDiags := s.EvalContext(refs)
diags = diags.Append(ctxDiags)
@ -49,7 +50,7 @@ func (s *Scope) ExpandBlock(body hcl.Body, schema *configschema.Block) (hcl.Body
func (s *Scope) EvalBlock(body hcl.Body, schema *configschema.Block) (cty.Value, tfdiags.Diagnostics) {
spec := schema.DecoderSpec()
refs, diags := ReferencesInBlock(body, schema)
refs, diags := ReferencesInBlock(s.ParseRef, body, schema)
ctx, ctxDiags := s.EvalContext(refs)
diags = diags.Append(ctxDiags)
@ -96,7 +97,7 @@ func (s *Scope) EvalSelfBlock(body hcl.Body, self cty.Value, schema *configschem
})
}
refs, refDiags := References(hcldec.Variables(body, spec))
refs, refDiags := References(s.ParseRef, hcldec.Variables(body, spec))
diags = diags.Append(refDiags)
terraformAttrs := map[string]cty.Value{}
@ -161,7 +162,7 @@ func (s *Scope) EvalSelfBlock(body hcl.Body, self cty.Value, schema *configschem
// If the returned diagnostics contains errors then the result may be
// incomplete, but will always be of the requested type.
func (s *Scope) EvalExpr(expr hcl.Expression, wantType cty.Type) (cty.Value, tfdiags.Diagnostics) {
refs, diags := ReferencesInExpr(expr)
refs, diags := ReferencesInExpr(s.ParseRef, expr)
ctx, ctxDiags := s.EvalContext(refs)
diags = diags.Append(ctxDiags)
@ -281,10 +282,12 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
wholeModules := map[string]cty.Value{}
inputVariables := map[string]cty.Value{}
localValues := map[string]cty.Value{}
outputValues := map[string]cty.Value{}
pathAttrs := map[string]cty.Value{}
terraformAttrs := map[string]cty.Value{}
countAttrs := map[string]cty.Value{}
forEachAttrs := map[string]cty.Value{}
checkBlocks := map[string]cty.Value{}
var self cty.Value
for _, ref := range refs {
@ -405,6 +408,16 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
diags = diags.Append(valDiags)
forEachAttrs[subj.Name] = val
case addrs.OutputValue:
val, valDiags := normalizeRefValue(s.Data.GetOutput(subj, rng))
diags = diags.Append(valDiags)
outputValues[subj.Name] = val
case addrs.Check:
val, valDiags := normalizeRefValue(s.Data.GetCheckBlock(subj, rng))
diags = diags.Append(valDiags)
outputValues[subj.Name] = val
default:
// Should never happen
panic(fmt.Errorf("Scope.buildEvalContext cannot handle address type %T", rawSubj))
@ -429,6 +442,17 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
vals["terraform"] = cty.ObjectVal(terraformAttrs)
vals["count"] = cty.ObjectVal(countAttrs)
vals["each"] = cty.ObjectVal(forEachAttrs)
// Checks and outputs are conditionally included in the available scope, so
// we'll only write out their values if we actually have something for them.
if len(checkBlocks) > 0 {
vals["check"] = cty.ObjectVal(checkBlocks)
}
if len(outputValues) > 0 {
vals["output"] = cty.ObjectVal(outputValues)
}
if self != cty.NilVal {
vals["self"] = self
}

View File

@ -367,13 +367,14 @@ func TestScopeEvalContext(t *testing.T) {
return
}
refs, refsDiags := ReferencesInExpr(expr)
refs, refsDiags := ReferencesInExpr(addrs.ParseRef, expr)
if refsDiags.HasErrors() {
t.Fatal(refsDiags.Err())
}
scope := &Scope{
Data: data,
Data: data,
ParseRef: addrs.ParseRef,
// "self" will just be an arbitrary one of the several resource
// instances we have in our test dataset.
@ -680,7 +681,8 @@ func TestScopeExpandEvalBlock(t *testing.T) {
body := file.Body
scope := &Scope{
Data: data,
Data: data,
ParseRef: addrs.ParseRef,
}
body, expandDiags := scope.ExpandBlock(body, schema)
@ -826,7 +828,8 @@ func TestScopeEvalSelfBlock(t *testing.T) {
body := file.Body
scope := &Scope{
Data: data,
Data: data,
ParseRef: addrs.ParseRef,
}
gotVal, ctxDiags := scope.EvalSelfBlock(body, test.Self, schema, test.KeyData)

View File

@ -5,12 +5,13 @@ package globalref
import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/lang"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/gocty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/lang"
)
// MetaReferences inspects the configuration to find the references contained
@ -117,7 +118,7 @@ func (a *Analyzer) metaReferencesInputVariable(calleeAddr addrs.ModuleInstance,
if attr == nil {
return nil
}
refs, _ := lang.ReferencesInExpr(attr.Expr)
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, attr.Expr)
return absoluteRefs(callerAddr, refs)
}
@ -137,7 +138,7 @@ func (a *Analyzer) metaReferencesOutputValue(callerAddr addrs.ModuleInstance, ad
// We don't check for errors here because we'll make a best effort to
// analyze whatever partial result HCL is able to extract.
refs, _ := lang.ReferencesInExpr(oc.Expr)
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, oc.Expr)
return absoluteRefs(calleeAddr, refs)
}
@ -154,7 +155,7 @@ func (a *Analyzer) metaReferencesLocalValue(moduleAddr addrs.ModuleInstance, add
// We don't check for errors here because we'll make a best effort to
// analyze whatever partial result HCL is able to extract.
refs, _ := lang.ReferencesInExpr(local.Expr)
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, local.Expr)
return absoluteRefs(moduleAddr, refs)
}
@ -388,12 +389,12 @@ Steps:
var refs []*addrs.Reference
for _, expr := range exprs {
moreRefs, _ := lang.ReferencesInExpr(expr)
moreRefs, _ := lang.ReferencesInExpr(addrs.ParseRef, expr)
refs = append(refs, moreRefs...)
}
if schema != nil {
for _, body := range bodies {
moreRefs, _ := lang.ReferencesInBlock(body, schema)
moreRefs, _ := lang.ReferencesInBlock(addrs.ParseRef, body, schema)
refs = append(refs, moreRefs...)
}
}

View File

@ -22,7 +22,7 @@ func (a *Analyzer) ReferencesFromOutputValue(addr addrs.AbsOutputValue) []Refere
if oc == nil {
return nil
}
refs, _ := lang.ReferencesInExpr(oc.Expr)
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, oc.Expr)
return absoluteRefs(addr.Module, refs)
}
@ -79,10 +79,10 @@ func (a *Analyzer) ReferencesFromResourceRepetition(addr addrs.AbsResource) []Re
switch {
case rc.ForEach != nil:
refs, _ := lang.ReferencesInExpr(rc.ForEach)
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, rc.ForEach)
return absoluteRefs(addr.Module, refs)
case rc.Count != nil:
refs, _ := lang.ReferencesInExpr(rc.Count)
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, rc.Count)
return absoluteRefs(addr.Module, refs)
default:
return nil

View File

@ -5,6 +5,7 @@ package lang
import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/lang/blocktoattr"
@ -24,7 +25,7 @@ import (
// incomplete or invalid. Otherwise, the returned slice has one reference per
// given traversal, though it is not guaranteed that the references will
// appear in the same order as the given traversals.
func References(traversals []hcl.Traversal) ([]*addrs.Reference, tfdiags.Diagnostics) {
func References(parseRef ParseRef, traversals []hcl.Traversal) ([]*addrs.Reference, tfdiags.Diagnostics) {
if len(traversals) == 0 {
return nil, nil
}
@ -33,7 +34,7 @@ func References(traversals []hcl.Traversal) ([]*addrs.Reference, tfdiags.Diagnos
refs := make([]*addrs.Reference, 0, len(traversals))
for _, traversal := range traversals {
ref, refDiags := addrs.ParseRef(traversal)
ref, refDiags := parseRef(traversal)
diags = diags.Append(refDiags)
if ref == nil {
continue
@ -50,7 +51,7 @@ func References(traversals []hcl.Traversal) ([]*addrs.Reference, tfdiags.Diagnos
//
// A block schema must be provided so that this function can determine where in
// the body variables are expected.
func ReferencesInBlock(body hcl.Body, schema *configschema.Block) ([]*addrs.Reference, tfdiags.Diagnostics) {
func ReferencesInBlock(parseRef ParseRef, body hcl.Body, schema *configschema.Block) ([]*addrs.Reference, tfdiags.Diagnostics) {
if body == nil {
return nil, nil
}
@ -69,16 +70,16 @@ func ReferencesInBlock(body hcl.Body, schema *configschema.Block) ([]*addrs.Refe
// in a better position to test this due to having mock providers etc
// available.
traversals := blocktoattr.ExpandedVariables(body, schema)
return References(traversals)
return References(parseRef, traversals)
}
// ReferencesInExpr is a helper wrapper around References that first searches
// the given expression for traversals, before converting those traversals
// to references.
func ReferencesInExpr(expr hcl.Expression) ([]*addrs.Reference, tfdiags.Diagnostics) {
func ReferencesInExpr(parseRef ParseRef, expr hcl.Expression) ([]*addrs.Reference, tfdiags.Diagnostics) {
if expr == nil {
return nil, nil
}
traversals := expr.Variables()
return References(traversals)
return References(parseRef, traversals)
}

View File

@ -7,12 +7,16 @@ import (
"sync"
"time"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty/function"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/experiments"
"github.com/hashicorp/terraform/internal/tfdiags"
)
type ParseRef func(traversal hcl.Traversal) (*addrs.Reference, tfdiags.Diagnostics)
// Scope is the main type in this package, allowing dynamic evaluation of
// blocks and expressions based on some contextual information that informs
// which variables and functions will be available.
@ -20,6 +24,14 @@ type Scope struct {
// Data is used to resolve references in expressions.
Data Data
// ParseRef is a function that the scope uses to extract references from
// a hcl.Traversal. This controls the type of references the scope currently
// supports. As an example, the testing scope can reference outputs directly
// while the main Terraform context scope can not. This means that this
// function for the testing scope will happily return outputs, while the
// main context scope would fail if a user attempts to reference an output.
ParseRef ParseRef
// SelfAddr is the address that the "self" object should be an alias of,
// or nil if the "self" object should not be available at all.
SelfAddr addrs.Referenceable

View File

@ -73,9 +73,9 @@ type checkResult struct {
func validateCheckRule(typ addrs.CheckRuleType, rule *configs.CheckRule, ctx EvalContext, self addrs.Checkable, keyData instances.RepetitionData) (string, *hcl.EvalContext, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
refs, moreDiags := lang.ReferencesInExpr(rule.Condition)
refs, moreDiags := lang.ReferencesInExpr(addrs.ParseRef, rule.Condition)
diags = diags.Append(moreDiags)
moreRefs, moreDiags := lang.ReferencesInExpr(rule.ErrorMessage)
moreRefs, moreDiags := lang.ReferencesInExpr(addrs.ParseRef, rule.ErrorMessage)
diags = diags.Append(moreDiags)
refs = append(refs, moreRefs...)

View File

@ -7,10 +7,12 @@ import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/lang"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// evaluateForEachExpression is our standard mechanism for interpreting an
@ -44,7 +46,7 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU
return nullMap, diags
}
refs, moreDiags := lang.ReferencesInExpr(expr)
refs, moreDiags := lang.ReferencesInExpr(addrs.ParseRef, expr)
diags = diags.Append(moreDiags)
scope := ctx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey)
var hclCtx *hcl.EvalContext

View File

@ -77,6 +77,7 @@ type Evaluator struct {
func (e *Evaluator) Scope(data lang.Data, self addrs.Referenceable, source addrs.Referenceable) *lang.Scope {
return &lang.Scope{
Data: data,
ParseRef: addrs.ParseRef,
SelfAddr: self,
SourceAddr: source,
PureOnly: e.Operation != walkApply && e.Operation != walkDestroy && e.Operation != walkEval,
@ -940,6 +941,70 @@ func (d *evaluationStateData) GetTerraformAttr(addr addrs.TerraformAttr, rng tfd
}
}
func (d *evaluationStateData) GetOutput(addr addrs.OutputValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// First we'll make sure the requested value is declared in configuration,
// so we can produce a nice message if not.
moduleConfig := d.Evaluator.Config.DescendentForInstance(d.ModulePath)
if moduleConfig == nil {
// should never happen, since we can't be evaluating in a module
// that wasn't mentioned in configuration.
panic(fmt.Sprintf("output value read from %s, which has no configuration", d.ModulePath))
}
config := moduleConfig.Module.Outputs[addr.Name]
if config == nil {
var suggestions []string
for k := range moduleConfig.Module.Outputs {
suggestions = append(suggestions, k)
}
suggestion := didyoumean.NameSuggestion(addr.Name, suggestions)
if suggestion != "" {
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
}
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Reference to undeclared output value`,
Detail: fmt.Sprintf(`An output value with the name %q has not been declared.%s`, addr.Name, suggestion),
Subject: rng.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
}
output := d.Evaluator.State.OutputValue(addr.Absolute(d.ModulePath))
val := output.Value
if val == cty.NilVal {
// Not evaluated yet?
val = cty.DynamicVal
}
if output.Sensitive {
val = val.Mark(marks.Sensitive)
}
return val, diags
}
func (d *evaluationStateData) GetCheckBlock(addr addrs.Check, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
// For now, check blocks don't contain any meaningful data and can only
// be referenced from the testing scope within an expect_failures attribute.
//
// We've added them into the scope explicitly since they are referencable,
// but we'll actually just return an error message saying they can't be
// referenced in this context.
var diags tfdiags.Diagnostics
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to \"check\" in invalid context",
Detail: "The \"check\" object can only be referenced from an \"expect_failures\" attribute within a Terraform testing \"run\" block.",
Subject: rng.ToHCL().Ptr(),
})
return cty.NilVal, diags
}
// moduleDisplayAddr returns a string describing the given module instance
// address that is appropriate for returning to users in situations where the
// root module is possible. Specifically, it returns "the root module" if the

View File

@ -87,6 +87,70 @@ func TestEvaluatorGetPathAttr(t *testing.T) {
})
}
func TestEvaluatorGetOutputValue(t *testing.T) {
evaluator := &Evaluator{
Meta: &ContextMeta{
Env: "foo",
},
Config: &configs.Config{
Module: &configs.Module{
Outputs: map[string]*configs.Output{
"some_output": {
Name: "some_output",
Sensitive: true,
},
"some_other_output": {
Name: "some_other_output",
},
},
},
},
State: states.BuildState(func(state *states.SyncState) {
state.SetOutputValue(addrs.AbsOutputValue{
Module: addrs.RootModuleInstance,
OutputValue: addrs.OutputValue{
Name: "some_output",
},
}, cty.StringVal("first"), true)
state.SetOutputValue(addrs.AbsOutputValue{
Module: addrs.RootModuleInstance,
OutputValue: addrs.OutputValue{
Name: "some_other_output",
},
}, cty.StringVal("second"), false)
}).SyncWrapper(),
}
data := &evaluationStateData{
Evaluator: evaluator,
}
scope := evaluator.Scope(data, nil, nil)
want := cty.StringVal("first").Mark(marks.Sensitive)
got, diags := scope.Data.GetOutput(addrs.OutputValue{
Name: "some_output",
}, tfdiags.SourceRange{})
if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
}
if !got.RawEquals(want) {
t.Errorf("wrong result %#v; want %#v", got, want)
}
want = cty.StringVal("second")
got, diags = scope.Data.GetOutput(addrs.OutputValue{
Name: "some_other_output",
}, tfdiags.SourceRange{})
if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
}
if !got.RawEquals(want) {
t.Errorf("wrong result %#v; want %#v", got, want)
}
}
// This particularly tests that a sensitive attribute in config
// results in a value that has a "sensitive" cty Mark
func TestEvaluatorGetInputVariable(t *testing.T) {

View File

@ -115,7 +115,7 @@ For example, to correlate with indices of a referring resource, use:
t.Fatal(hclDiags.Error())
}
refs, diags := lang.References([]hcl.Traversal{traversal})
refs, diags := lang.References(addrs.ParseRef, []hcl.Traversal{traversal})
if diags.HasErrors() {
t.Fatal(diags.Err())
}

View File

@ -99,8 +99,8 @@ func (n *nodeExpandCheck) References() []*addrs.Reference {
for _, assert := range n.config.Asserts {
// Check blocks reference anything referenced by conditions or messages
// in their check rules.
condition, _ := lang.ReferencesInExpr(assert.Condition)
message, _ := lang.ReferencesInExpr(assert.ErrorMessage)
condition, _ := lang.ReferencesInExpr(addrs.ParseRef, assert.Condition)
message, _ := lang.ReferencesInExpr(addrs.ParseRef, assert.ErrorMessage)
refs = append(refs, condition...)
refs = append(refs, message...)
}

View File

@ -8,12 +8,13 @@ import (
"log"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/dag"
"github.com/hashicorp/terraform/internal/lang"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// nodeExpandLocal represents a named local value in a configuration module,
@ -61,7 +62,7 @@ func (n *nodeExpandLocal) ReferenceableAddrs() []addrs.Referenceable {
// GraphNodeReferencer
func (n *nodeExpandLocal) References() []*addrs.Reference {
refs, _ := lang.ReferencesInExpr(n.Config.Expr)
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, n.Config.Expr)
return refs
}
@ -124,7 +125,7 @@ func (n *NodeLocal) ReferenceableAddrs() []addrs.Referenceable {
// GraphNodeReferencer
func (n *NodeLocal) References() []*addrs.Reference {
refs, _ := lang.ReferencesInExpr(n.Config.Expr)
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, n.Config.Expr)
return refs
}
@ -138,7 +139,7 @@ func (n *NodeLocal) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Di
// We ignore diags here because any problems we might find will be found
// again in EvaluateExpr below.
refs, _ := lang.ReferencesInExpr(expr)
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, expr)
for _, ref := range refs {
if ref.Subject == addr {
diags = diags.Append(&hcl.Diagnostic{

View File

@ -64,11 +64,11 @@ func (n *nodeExpandModule) References() []*addrs.Reference {
// child module instances we might expand to during our evaluation.
if n.ModuleCall.Count != nil {
countRefs, _ := lang.ReferencesInExpr(n.ModuleCall.Count)
countRefs, _ := lang.ReferencesInExpr(addrs.ParseRef, n.ModuleCall.Count)
refs = append(refs, countRefs...)
}
if n.ModuleCall.ForEach != nil {
forEachRefs, _ := lang.ReferencesInExpr(n.ModuleCall.ForEach)
forEachRefs, _ := lang.ReferencesInExpr(addrs.ParseRef, n.ModuleCall.ForEach)
refs = append(refs, forEachRefs...)
}
return refs

View File

@ -8,13 +8,14 @@ import (
"log"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/dag"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/lang"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// nodeExpandModuleVariable is the placeholder for an variable that has not yet had
@ -90,7 +91,7 @@ func (n *nodeExpandModuleVariable) References() []*addrs.Reference {
// where our associated variable was declared, which is correct because
// our value expression is assigned within a "module" block in the parent
// module.
refs, _ := lang.ReferencesInExpr(n.Expr)
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, n.Expr)
return refs
}

View File

@ -282,16 +282,16 @@ func (n *NodeApplyableOutput) ReferenceableAddrs() []addrs.Referenceable {
func referencesForOutput(c *configs.Output) []*addrs.Reference {
var refs []*addrs.Reference
impRefs, _ := lang.ReferencesInExpr(c.Expr)
expRefs, _ := lang.References(c.DependsOn)
impRefs, _ := lang.ReferencesInExpr(addrs.ParseRef, c.Expr)
expRefs, _ := lang.References(addrs.ParseRef, c.DependsOn)
refs = append(refs, impRefs...)
refs = append(refs, expRefs...)
for _, check := range c.Preconditions {
condRefs, _ := lang.ReferencesInExpr(check.Condition)
condRefs, _ := lang.ReferencesInExpr(addrs.ParseRef, check.Condition)
refs = append(refs, condRefs...)
errRefs, _ := lang.ReferencesInExpr(check.ErrorMessage)
errRefs, _ := lang.ReferencesInExpr(addrs.ParseRef, check.ErrorMessage)
refs = append(refs, errRefs...)
}

View File

@ -152,25 +152,25 @@ func (n *NodeAbstractResource) References() []*addrs.Reference {
log.Printf("[WARN] no schema is attached to %s, so config references cannot be detected", n.Name())
}
refs, _ := lang.ReferencesInExpr(c.Count)
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, c.Count)
result = append(result, refs...)
refs, _ = lang.ReferencesInExpr(c.ForEach)
refs, _ = lang.ReferencesInExpr(addrs.ParseRef, c.ForEach)
result = append(result, refs...)
for _, expr := range c.TriggersReplacement {
refs, _ = lang.ReferencesInExpr(expr)
refs, _ = lang.ReferencesInExpr(addrs.ParseRef, expr)
result = append(result, refs...)
}
// ReferencesInBlock() requires a schema
if n.Schema != nil {
refs, _ = lang.ReferencesInBlock(c.Config, n.Schema)
refs, _ = lang.ReferencesInBlock(addrs.ParseRef, c.Config, n.Schema)
result = append(result, refs...)
}
if c.Managed != nil {
if c.Managed.Connection != nil {
refs, _ = lang.ReferencesInBlock(c.Managed.Connection.Config, connectionBlockSupersetSchema)
refs, _ = lang.ReferencesInBlock(addrs.ParseRef, c.Managed.Connection.Config, connectionBlockSupersetSchema)
result = append(result, refs...)
}
@ -179,7 +179,7 @@ func (n *NodeAbstractResource) References() []*addrs.Reference {
continue
}
if p.Connection != nil {
refs, _ = lang.ReferencesInBlock(p.Connection.Config, connectionBlockSupersetSchema)
refs, _ = lang.ReferencesInBlock(addrs.ParseRef, p.Connection.Config, connectionBlockSupersetSchema)
result = append(result, refs...)
}
@ -187,21 +187,21 @@ func (n *NodeAbstractResource) References() []*addrs.Reference {
if schema == nil {
log.Printf("[WARN] no schema for provisioner %q is attached to %s, so provisioner block references cannot be detected", p.Type, n.Name())
}
refs, _ = lang.ReferencesInBlock(p.Config, schema)
refs, _ = lang.ReferencesInBlock(addrs.ParseRef, p.Config, schema)
result = append(result, refs...)
}
}
for _, check := range c.Preconditions {
refs, _ := lang.ReferencesInExpr(check.Condition)
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, check.Condition)
result = append(result, refs...)
refs, _ = lang.ReferencesInExpr(check.ErrorMessage)
refs, _ = lang.ReferencesInExpr(addrs.ParseRef, check.ErrorMessage)
result = append(result, refs...)
}
for _, check := range c.Postconditions {
refs, _ := lang.ReferencesInExpr(check.Condition)
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, check.Condition)
result = append(result, refs...)
refs, _ = lang.ReferencesInExpr(check.ErrorMessage)
refs, _ = lang.ReferencesInExpr(addrs.ParseRef, check.ErrorMessage)
result = append(result, refs...)
}

View File

@ -95,9 +95,9 @@ func (n *NodeApplyableResource) References() []*addrs.Reference {
// Since this node type only updates resource-level metadata, we only
// need to worry about the parts of the configuration that affect
// our "each mode": the count and for_each meta-arguments.
refs, _ := lang.ReferencesInExpr(n.Config.Count)
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, n.Config.Count)
result = append(result, refs...)
refs, _ = lang.ReferencesInExpr(n.Config.ForEach)
refs, _ = lang.ReferencesInExpr(addrs.ParseRef, n.Config.ForEach)
result = append(result, refs...)
return result

View File

@ -8,6 +8,8 @@ import (
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
@ -17,7 +19,6 @@ import (
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/provisioners"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// NodeValidatableResource represents a resource that is used for validation
@ -469,7 +470,7 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag
func (n *NodeValidatableResource) evaluateExpr(ctx EvalContext, expr hcl.Expression, wantTy cty.Type, self addrs.Referenceable, keyData instances.RepetitionData) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
refs, refDiags := lang.ReferencesInExpr(expr)
refs, refDiags := lang.ReferencesInExpr(addrs.ParseRef, expr)
diags = diags.Append(refDiags)
scope := ctx.EvaluationScope(self, nil, keyData)

View File

@ -100,9 +100,9 @@ func (ctx *TestContext) evaluate(state *states.SyncState, changes *plans.Changes
// Now validate all the assertions within this run block.
for _, rule := range run.Config.CheckRules {
refs, moreDiags := lang.ReferencesInExpr(rule.Condition)
refs, moreDiags := lang.ReferencesInExpr(addrs.ParseRefFromTestingScope, rule.Condition)
run.Diagnostics = run.Diagnostics.Append(moreDiags)
moreRefs, moreDiags := lang.ReferencesInExpr(rule.ErrorMessage)
moreRefs, moreDiags := lang.ReferencesInExpr(addrs.ParseRefFromTestingScope, rule.ErrorMessage)
run.Diagnostics = run.Diagnostics.Append(moreDiags)
refs = append(refs, moreRefs...)

View File

@ -9,6 +9,7 @@ import (
"sort"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/dag"
@ -555,6 +556,6 @@ func ReferencesFromConfig(body hcl.Body, schema *configschema.Block) []*addrs.Re
if body == nil {
return nil
}
refs, _ := lang.ReferencesInBlock(body, schema)
refs, _ := lang.ReferencesInBlock(addrs.ParseRef, body, schema)
return refs
}

View File

@ -45,7 +45,7 @@ func validateSelfRef(addr addrs.Referenceable, config hcl.Body, providerSchema *
return diags
}
refs, _ := lang.ReferencesInBlock(config, schema)
refs, _ := lang.ReferencesInBlock(addrs.ParseRef, config, schema)
for _, ref := range refs {
for _, addrStr := range addrStrs {
if ref.Subject.String() == addrStr {