refactor Scope.evalContext to meet linting criteria (#1750)

Signed-off-by: ollevche <ollevche@gmail.com>
This commit is contained in:
Oleksandr Levchenkov 2024-07-01 16:19:29 +03:00 committed by GitHub
parent e2b6b46d43
commit 374c9eedd3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -7,6 +7,7 @@ package lang
import (
"fmt"
"reflect"
"strings"
"github.com/hashicorp/hcl/v2"
@ -300,7 +301,6 @@ func (s *Scope) EvalContextWithParent(p *hcl.EvalContext, refs []*addrs.Referenc
return s.evalContext(p, refs, s.SelfAddr)
}
//nolint:funlen,gocyclo,cyclop // TODO: refactor this function to match linting requirements
func (s *Scope) evalContext(parent *hcl.EvalContext, refs []*addrs.Reference, selfAddr addrs.Referenceable) (*hcl.EvalContext, tfdiags.Diagnostics) {
if s == nil {
panic("attempt to construct EvalContext for nil Scope")
@ -308,21 +308,18 @@ func (s *Scope) evalContext(parent *hcl.EvalContext, refs []*addrs.Reference, se
var diags tfdiags.Diagnostics
vals := make(map[string]cty.Value)
funcs := make(map[string]function.Function)
// Calling NewChild() on a nil parent will
// produce an EvalContext with no parent.
ctx := parent.NewChild()
ctx.Variables = vals
ctx.Functions = funcs
ctx.Functions = make(map[string]function.Function)
ctx.Variables = make(map[string]cty.Value)
for name, fn := range s.Functions() {
funcs[name] = fn
ctx.Functions[name] = fn
}
// Easy path for common case where there are no references at all.
if len(refs) == 0 {
// Easy path for common case where there are no references at all.
return ctx, diags
}
@ -343,196 +340,239 @@ func (s *Scope) evalContext(parent *hcl.EvalContext, refs []*addrs.Reference, se
// it, since that allows us to gather a full set of any errors and
// warnings, but once we've gathered all the data we'll then skip anything
// that's redundant in the process of populating our values map.
dataResources := map[string]map[string]cty.Value{}
managedResources := map[string]map[string]cty.Value{}
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
varBuilder := s.newEvalVarBuilder()
for _, ref := range refs {
rng := ref.SourceRange
rawSubj := ref.Subject
if rawSubj == addrs.Self {
if selfAddr == nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid "self" reference`,
// This detail message mentions some current practice that
// this codepath doesn't really "know about". If the "self"
// object starts being supported in more contexts later then
// we'll need to adjust this message.
Detail: `The "self" object is not available in this context. This object can be used only in resource provisioner, connection, and postcondition blocks.`,
Subject: ref.SourceRange.ToHCL().Ptr(),
})
continue
}
if selfAddr == addrs.Self {
// Programming error: the self address cannot alias itself.
panic("scope SelfAddr attempting to alias itself")
}
// self can only be used within a resource instance
subj := selfAddr.(addrs.ResourceInstance)
val, valDiags := normalizeRefValue(s.Data.GetResource(subj.ContainingResource(), rng))
diags = diags.Append(valDiags)
// Self is an exception in that it must always resolve to a
// particular instance. We will still insert the full resource into
// the context below.
var hclDiags hcl.Diagnostics
// We should always have a valid self index by this point, but in
// the case of an error, self may end up as a cty.DynamicValue.
switch k := subj.Key.(type) {
case addrs.IntKey:
self, hclDiags = hcl.Index(val, cty.NumberIntVal(int64(k)), ref.SourceRange.ToHCL().Ptr())
diags = diags.Append(hclDiags)
case addrs.StringKey:
self, hclDiags = hcl.Index(val, cty.StringVal(string(k)), ref.SourceRange.ToHCL().Ptr())
diags = diags.Append(hclDiags)
default:
self = val
}
if ref.Subject == addrs.Self {
diags.Append(varBuilder.putSelfValue(selfAddr, ref))
continue
}
// This type switch must cover all of the "Referenceable" implementations
// in package addrs, however we are removing the possibility of
// Instances beforehand.
switch addr := rawSubj.(type) {
case addrs.ResourceInstance:
rawSubj = addr.ContainingResource()
case addrs.ModuleCallInstance:
rawSubj = addr.Call
case addrs.ModuleCallInstanceOutput:
rawSubj = addr.Call.Call
}
switch subj := rawSubj.(type) {
case addrs.Resource:
var into map[string]map[string]cty.Value
switch subj.Mode {
case addrs.ManagedResourceMode:
into = managedResources
case addrs.DataResourceMode:
into = dataResources
default:
panic(fmt.Errorf("unsupported ResourceMode %s", subj.Mode))
}
val, valDiags := normalizeRefValue(s.Data.GetResource(subj, rng))
diags = diags.Append(valDiags)
r := subj
if into[r.Type] == nil {
into[r.Type] = make(map[string]cty.Value)
}
into[r.Type][r.Name] = val
case addrs.ModuleCall:
val, valDiags := normalizeRefValue(s.Data.GetModule(subj, rng))
diags = diags.Append(valDiags)
wholeModules[subj.Name] = val
case addrs.InputVariable:
val, valDiags := normalizeRefValue(s.Data.GetInputVariable(subj, rng))
diags = diags.Append(valDiags)
inputVariables[subj.Name] = val
case addrs.LocalValue:
val, valDiags := normalizeRefValue(s.Data.GetLocalValue(subj, rng))
diags = diags.Append(valDiags)
localValues[subj.Name] = val
case addrs.PathAttr:
val, valDiags := normalizeRefValue(s.Data.GetPathAttr(subj, rng))
diags = diags.Append(valDiags)
pathAttrs[subj.Name] = val
case addrs.TerraformAttr:
val, valDiags := normalizeRefValue(s.Data.GetTerraformAttr(subj, rng))
diags = diags.Append(valDiags)
terraformAttrs[subj.Name] = val
case addrs.CountAttr:
val, valDiags := normalizeRefValue(s.Data.GetCountAttr(subj, rng))
diags = diags.Append(valDiags)
countAttrs[subj.Name] = val
case addrs.ForEachAttr:
val, valDiags := normalizeRefValue(s.Data.GetForEachAttr(subj, rng))
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
case addrs.ProviderFunction:
if subj, ok := ref.Subject.(addrs.ProviderFunction); ok {
// Inject function directly into context
if _, ok := ctx.Functions[subj.String()]; !ok {
fn, fnDiags := s.ProviderFunctions(subj, rng)
fn, fnDiags := s.ProviderFunctions(subj, ref.SourceRange)
diags = diags.Append(fnDiags)
if !fnDiags.HasErrors() {
ctx.Functions[subj.String()] = *fn
}
}
default:
// Should never happen
panic(fmt.Errorf("Scope.buildEvalContext cannot handle address type %T", rawSubj))
continue
}
diags = diags.Append(varBuilder.putValueBySubject(ref))
}
varBuilder.buildAllVariablesInto(ctx.Variables)
return ctx, diags
}
type evalVarBuilder struct {
s *Scope
dataResources map[string]map[string]cty.Value
managedResources map[string]map[string]cty.Value
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
self cty.Value
}
func (s *Scope) newEvalVarBuilder() *evalVarBuilder {
return &evalVarBuilder{
s: s,
dataResources: map[string]map[string]cty.Value{},
managedResources: map[string]map[string]cty.Value{},
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{},
}
}
func (b *evalVarBuilder) putSelfValue(selfAddr addrs.Referenceable, ref *addrs.Reference) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
if selfAddr == nil {
return diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid "self" reference`,
// This detail message mentions some current practice that
// this codepath doesn't really "know about". If the "self"
// object starts being supported in more contexts later then
// we'll need to adjust this message.
Detail: `The "self" object is not available in this context. This object can be used only in resource provisioner, connection, and postcondition blocks.`,
Subject: ref.SourceRange.ToHCL().Ptr(),
})
}
if selfAddr == addrs.Self {
// Programming error: the self address cannot alias itself.
panic("scope SelfAddr attempting to alias itself")
}
// self can only be used within a resource instance
subj, ok := selfAddr.(addrs.ResourceInstance)
if !ok {
panic("BUG: self addr must be a resource instance, got " + reflect.TypeOf(selfAddr).String())
}
val, valDiags := normalizeRefValue(b.s.Data.GetResource(subj.ContainingResource(), ref.SourceRange))
diags = diags.Append(valDiags)
// Self is an exception in that it must always resolve to a
// particular instance. We will still insert the full resource into
// the context below.
var hclDiags hcl.Diagnostics
// We should always have a valid self index by this point, but in
// the case of an error, self may end up as a cty.DynamicValue.
switch k := subj.Key.(type) {
case addrs.IntKey:
b.self, hclDiags = hcl.Index(val, cty.NumberIntVal(int64(k)), ref.SourceRange.ToHCL().Ptr())
case addrs.StringKey:
b.self, hclDiags = hcl.Index(val, cty.StringVal(string(k)), ref.SourceRange.ToHCL().Ptr())
default:
b.self = val
}
diags = diags.Append(hclDiags)
return diags
}
func (b *evalVarBuilder) putValueBySubject(ref *addrs.Reference) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
rawSubj := ref.Subject
rng := ref.SourceRange
// This type switch must cover all of the "Referenceable" implementations
// in package addrs, however we are removing the possibility of
// Instances beforehand.
switch addr := rawSubj.(type) {
case addrs.ResourceInstance:
rawSubj = addr.ContainingResource()
case addrs.ModuleCallInstance:
rawSubj = addr.Call
case addrs.ModuleCallInstanceOutput:
rawSubj = addr.Call.Call
}
var normDiags tfdiags.Diagnostics
switch subj := rawSubj.(type) {
case addrs.Resource:
diags = diags.Append(b.putResourceValue(subj, rng))
case addrs.ModuleCall:
b.wholeModules[subj.Name], normDiags = normalizeRefValue(b.s.Data.GetModule(subj, rng))
case addrs.InputVariable:
b.inputVariables[subj.Name], normDiags = normalizeRefValue(b.s.Data.GetInputVariable(subj, rng))
case addrs.LocalValue:
b.localValues[subj.Name], normDiags = normalizeRefValue(b.s.Data.GetLocalValue(subj, rng))
case addrs.PathAttr:
b.pathAttrs[subj.Name], normDiags = normalizeRefValue(b.s.Data.GetPathAttr(subj, rng))
case addrs.TerraformAttr:
b.terraformAttrs[subj.Name], normDiags = normalizeRefValue(b.s.Data.GetTerraformAttr(subj, rng))
case addrs.CountAttr:
b.countAttrs[subj.Name], normDiags = normalizeRefValue(b.s.Data.GetCountAttr(subj, rng))
case addrs.ForEachAttr:
b.forEachAttrs[subj.Name], normDiags = normalizeRefValue(b.s.Data.GetForEachAttr(subj, rng))
case addrs.OutputValue:
b.outputValues[subj.Name], normDiags = normalizeRefValue(b.s.Data.GetOutput(subj, rng))
case addrs.Check:
b.outputValues[subj.Name], normDiags = normalizeRefValue(b.s.Data.GetCheckBlock(subj, rng))
default:
// Should never happen
panic(fmt.Errorf("Scope.buildEvalContext cannot handle address type %T", rawSubj))
}
diags = diags.Append(normDiags)
return diags
}
func (b *evalVarBuilder) putResourceValue(res addrs.Resource, rng tfdiags.SourceRange) tfdiags.Diagnostics {
var into map[string]map[string]cty.Value
switch res.Mode {
case addrs.ManagedResourceMode:
into = b.managedResources
case addrs.DataResourceMode:
into = b.dataResources
case addrs.InvalidResourceMode:
panic("BUG: got invalid resource mode")
default:
panic(fmt.Errorf("BUG: got undefined ResourceMode %s", res.Mode))
}
val, diags := normalizeRefValue(b.s.Data.GetResource(res, rng))
if into[res.Type] == nil {
into[res.Type] = make(map[string]cty.Value)
}
into[res.Type][res.Name] = val
return diags
}
func (b *evalVarBuilder) buildAllVariablesInto(vals map[string]cty.Value) {
// Managed resources are exposed in two different locations. The primary
// is at the top level where the resource type name is the root of the
// traversal, but we also expose them under "resource" as an escaping
// technique if we add a reserved name in a future language edition which
// conflicts with someone's existing provider.
for k, v := range buildResourceObjects(managedResources) {
for k, v := range buildResourceObjects(b.managedResources) {
vals[k] = v
}
vals["resource"] = cty.ObjectVal(buildResourceObjects(managedResources))
vals["resource"] = cty.ObjectVal(buildResourceObjects(b.managedResources))
vals["data"] = cty.ObjectVal(buildResourceObjects(dataResources))
vals["module"] = cty.ObjectVal(wholeModules)
vals["var"] = cty.ObjectVal(inputVariables)
vals["local"] = cty.ObjectVal(localValues)
vals["path"] = cty.ObjectVal(pathAttrs)
vals["terraform"] = cty.ObjectVal(terraformAttrs)
vals["count"] = cty.ObjectVal(countAttrs)
vals["each"] = cty.ObjectVal(forEachAttrs)
vals["data"] = cty.ObjectVal(buildResourceObjects(b.dataResources))
vals["module"] = cty.ObjectVal(b.wholeModules)
vals["var"] = cty.ObjectVal(b.inputVariables)
vals["local"] = cty.ObjectVal(b.localValues)
vals["path"] = cty.ObjectVal(b.pathAttrs)
vals["terraform"] = cty.ObjectVal(b.terraformAttrs)
vals["count"] = cty.ObjectVal(b.countAttrs)
vals["each"] = cty.ObjectVal(b.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(b.checkBlocks) > 0 {
vals["check"] = cty.ObjectVal(b.checkBlocks)
}
if len(outputValues) > 0 {
vals["output"] = cty.ObjectVal(outputValues)
if len(b.outputValues) > 0 {
vals["output"] = cty.ObjectVal(b.outputValues)
}
if self != cty.NilVal {
vals["self"] = self
if b.self != cty.NilVal {
vals["self"] = b.self
}
return ctx, diags
}
func buildResourceObjects(resources map[string]map[string]cty.Value) map[string]cty.Value {