diff --git a/internal/lang/eval.go b/internal/lang/eval.go index 61f2e94216..49141308bf 100644 --- a/internal/lang/eval.go +++ b/internal/lang/eval.go @@ -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 {