// Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2023 HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package lang import ( "context" "fmt" "log" "maps" "reflect" "strings" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/ext/dynblock" "github.com/hashicorp/hcl/v2/hcldec" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/configs/configschema" "github.com/opentofu/opentofu/internal/instances" "github.com/opentofu/opentofu/internal/lang/blocktoattr" "github.com/opentofu/opentofu/internal/lang/marks" "github.com/opentofu/opentofu/internal/tfdiags" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" "github.com/zclconf/go-cty/cty/function" ) // ExpandBlock expands any "dynamic" blocks present in the given body. The // result is a body with those blocks expanded, ready to be evaluated with // EvalBlock. // // If the returned diagnostics contains errors then the result may be // incomplete or invalid. func (s *Scope) ExpandBlock(ctx context.Context, body hcl.Body, schema *configschema.Block) (hcl.Body, tfdiags.Diagnostics) { spec := schema.DecoderSpec() traversals := dynblock.ExpandVariablesHCLDec(body, spec) // using ExpandFunctionsHCLDec to extract strictly the functions that are referenced inside the `dynamic` // block, since that is what is needed to be injected into the expansion evalCtx for the expansion to work traversals = append(traversals, filterProviderFunctions(dynblock.ExpandFunctionsHCLDec(body, spec))...) refs, diags := References(s.ParseRef, traversals) hclCtx, ctxDiags := s.EvalContext(ctx, refs) diags = diags.Append(ctxDiags) return dynblock.Expand(body, hclCtx), diags } // EvalBlock evaluates the given body using the given block schema and returns // a cty object value representing its contents. The type of the result conforms // to the implied type of the given schema. // // This function does not automatically expand "dynamic" blocks within the // body. If that is desired, first call the ExpandBlock method to obtain // an expanded body to pass to this method. // // If the returned diagnostics contains errors then the result may be // incomplete or invalid. func (s *Scope) EvalBlock(ctx context.Context, body hcl.Body, schema *configschema.Block) (cty.Value, tfdiags.Diagnostics) { spec := schema.DecoderSpec() refs, diags := ReferencesInBlock(s.ParseRef, body, schema) hclCtx, ctxDiags := s.EvalContext(ctx, refs) diags = diags.Append(ctxDiags) if diags.HasErrors() { // We'll stop early if we found problems in the references, because // it's likely evaluation will produce redundant copies of the same errors. return cty.UnknownVal(schema.ImpliedType()), diags } // HACK: In order to remain compatible with some assumptions made in // Terraform v0.11 and earlier about the approximate equivalence of // attribute vs. block syntax, we do a just-in-time fixup here to allow // any attribute in the schema that has a list-of-objects or set-of-objects // kind to potentially be populated instead by one or more nested blocks // whose type is the attribute name. body = blocktoattr.FixUpBlockAttrs(body, schema) val, evalDiags := hcldec.Decode(body, spec, hclCtx) diags = diags.Append(enhanceFunctionDiags(evalDiags)) diags = diags.Append(validEphemeralReferences(schema, val)) val, depDiags := marks.ExtractDeprecationDiagnosticsWithBody(val, body) diags = diags.Append(depDiags) return val, diags } // EvalSelfBlock evaluates the given body only within the scope of the provided // object and instance key data. References to the object must use self, and the // key data will only contain count.index or each.key. The static values for // terraform and path will also be available in this context. func (s *Scope) EvalSelfBlock(ctx context.Context, body hcl.Body, self cty.Value, schema *configschema.Block, keyData instances.RepetitionData) (cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics spec := schema.DecoderSpec() vals := make(map[string]cty.Value) vals["self"] = self if !keyData.CountIndex.IsNull() { vals["count"] = cty.ObjectVal(map[string]cty.Value{ "index": keyData.CountIndex, }) } if !keyData.EachKey.IsNull() { vals["each"] = cty.ObjectVal(map[string]cty.Value{ "key": keyData.EachKey, }) } refs, refDiags := References(s.ParseRef, hcldec.Variables(body, spec)) diags = diags.Append(refDiags) terraformAttrs := map[string]cty.Value{} pathAttrs := map[string]cty.Value{} // We could always load the static values for Path and Terraform values, // but we want to parse the references so that we can get source ranges for // user diagnostics. for _, ref := range refs { // we already loaded the self value if ref.Subject == addrs.Self { continue } switch subj := ref.Subject.(type) { case addrs.PathAttr: val, valDiags := normalizeRefValue(s.Data.GetPathAttr(ctx, subj, ref.SourceRange)) diags = diags.Append(valDiags) pathAttrs[subj.Name] = val case addrs.TerraformAttr: val, valDiags := normalizeRefValue(s.Data.GetTerraformAttr(ctx, subj, ref.SourceRange)) diags = diags.Append(valDiags) terraformAttrs[subj.Name] = val case addrs.CountAttr, addrs.ForEachAttr: // each and count have already been handled. default: // This should have been caught in validation, but point the user // to the correct location in case something slipped through. diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: `Invalid reference`, Detail: fmt.Sprintf("The reference to %q is not valid in this context", ref.Subject), Subject: ref.SourceRange.ToHCL().Ptr(), }) } } vals["path"] = cty.ObjectVal(pathAttrs) vals["terraform"] = cty.ObjectVal(terraformAttrs) vals["tofu"] = cty.ObjectVal(terraformAttrs) hclCtx := &hcl.EvalContext{ Variables: vals, // TODO consider if any provider functions make sense here Functions: s.Functions(), } val, decDiags := hcldec.Decode(body, schema.DecoderSpec(), hclCtx) diags = diags.Append(enhanceFunctionDiags(decDiags)) return val, diags } // EvalExpr evaluates a single expression in the receiving context and returns // the resulting value. The value will be converted to the given type before // it is returned if possible, or else an error diagnostic will be produced // describing the conversion error. // // Pass an expected type of cty.DynamicPseudoType to skip automatic conversion // and just obtain the returned value directly. // // If the returned diagnostics contains errors then the result may be // incomplete, but will always be of the requested type. func (s *Scope) EvalExpr(ctx context.Context, expr hcl.Expression, wantType cty.Type) (cty.Value, tfdiags.Diagnostics) { refs, diags := ReferencesInExpr(s.ParseRef, expr) hclCtx, ctxDiags := s.EvalContext(ctx, refs) diags = diags.Append(ctxDiags) if diags.HasErrors() { // We'll stop early if we found problems in the references, because // it's likely evaluation will produce redundant copies of the same errors. return cty.UnknownVal(wantType), diags } val, evalDiags := expr.Value(hclCtx) diags = diags.Append(enhanceFunctionDiags(evalDiags)) if wantType != cty.DynamicPseudoType { var convErr error val, convErr = convert.Convert(val, wantType) if convErr != nil { val = cty.UnknownVal(wantType) diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Incorrect value type", Detail: fmt.Sprintf("Invalid expression value: %s.", tfdiags.FormatError(convErr)), Subject: expr.Range().Ptr(), Expression: expr, EvalContext: hclCtx, }) } } val, depDiags := marks.ExtractDeprecatedDiagnosticsWithExpr(val, expr) diags = diags.Append(depDiags) return val, diags } // Identify and enhance any function related dialogs produced by a hcl.EvalContext func enhanceFunctionDiags(diags hcl.Diagnostics) hcl.Diagnostics { out := make(hcl.Diagnostics, len(diags)) for i, diag := range diags { out[i] = diag if funcExtra, ok := diag.Extra.(hclsyntax.FunctionCallUnknownDiagExtra); ok { out[i] = enhanceFunctionDiag(diag, funcExtra) } } return out } // enhanceFunctionDiag returns a potentially-improved version of the given diagnostic // based on the information in funcExtra. func enhanceFunctionDiag(diag *hcl.Diagnostic, funcExtra hclsyntax.FunctionCallUnknownDiagExtra) *hcl.Diagnostic { funcName := funcExtra.CalledFunctionName() // prefix::stuff:: fullNamespace := funcExtra.CalledFunctionNamespace() if len(fullNamespace) == 0 { // Not a namespaced function, no enhancements necessary return diag } // Shallow copy of the given diagnostic so that we can modify it without affecting // anyone else that might be holding a pointer to it. enhanced := *diag // Update enhanced with additional details fn := addrs.ParseFunction(fullNamespace + funcName) if fn.IsNamespace(addrs.FunctionNamespaceCore) { // Error is in core namespace, mirror non-core equivalent enhanced.Summary = "Call to unknown function" enhanced.Detail = fmt.Sprintf("There is no builtin (%s::) function named %q.", addrs.FunctionNamespaceCore, funcName) } else if fn.IsNamespace(addrs.FunctionNamespaceProvider) { if _, err := fn.AsProviderFunction(); err != nil { // complete mismatch or invalid prefix enhanced.Summary = "Invalid function format" enhanced.Detail = err.Error() } } else { enhanced.Summary = "Unknown function namespace" enhanced.Detail = fmt.Sprintf("Function %q does not exist within a valid namespace (%s)", fn, strings.Join(addrs.FunctionNamespaces, ",")) } // Function / Provider not found handled by eval_context_builtin.go return &enhanced } // EvalReference evaluates the given reference in the receiving scope and // returns the resulting value. The value will be converted to the given type before // it is returned if possible, or else an error diagnostic will be produced // describing the conversion error. // // Pass an expected type of cty.DynamicPseudoType to skip automatic conversion // and just obtain the returned value directly. // // If the returned diagnostics contains errors then the result may be // incomplete, but will always be of the requested type. func (s *Scope) EvalReference(ctx context.Context, ref *addrs.Reference, wantType cty.Type) (cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // We cheat a bit here and just build an EvalContext for our requested // reference with the "self" address overridden, and then pull the "self" // result out of it to return. hclCtx, ctxDiags := s.evalContext(ctx, nil, []*addrs.Reference{ref}, ref.Subject) diags = diags.Append(ctxDiags) val := hclCtx.Variables["self"] if val == cty.NilVal { val = cty.DynamicVal } var convErr error val, convErr = convert.Convert(val, wantType) if convErr != nil { val = cty.UnknownVal(wantType) diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Incorrect value type", Detail: fmt.Sprintf("Invalid expression value: %s.", tfdiags.FormatError(convErr)), Subject: ref.SourceRange.ToHCL().Ptr(), }) } return val, diags } // EvalContext constructs a HCL expression evaluation context whose variable // scope contains sufficient values to satisfy the given set of references. // // Most callers should prefer to use the evaluation helper methods that // this type offers, but this is here for less common situations where the // caller will handle the evaluation calls itself. // // The [context.Context] passed to this function is the one that will be used // when making requests to providers if any of the references are to // provider-defined functions, and so callers should typically use // the resulting evaluation context immediately after the call and not // share it with other parts of the system where a different [context.Context] // would be more appropriate to use. func (s *Scope) EvalContext(ctx context.Context, refs []*addrs.Reference) (*hcl.EvalContext, tfdiags.Diagnostics) { return s.evalContext(ctx, nil, refs, s.SelfAddr) } // EvalContextWithParent is exactly the same as EvalContext except the resulting hcl.EvalContext // will be derived from the given parental hcl.EvalContext. It will enable different hcl mechanisms // to iteratively lookup target functions and variables in EvalContext's parent. // See Traversal.TraverseAbs (hcl) or FunctionCallExpr.Value (hcl/hclsyntax) for more details. func (s *Scope) EvalContextWithParent(ctx context.Context, p *hcl.EvalContext, refs []*addrs.Reference) (*hcl.EvalContext, tfdiags.Diagnostics) { return s.evalContext(ctx, p, refs, s.SelfAddr) } func (s *Scope) evalContext(ctx context.Context, parent *hcl.EvalContext, refs []*addrs.Reference, selfAddr addrs.Referenceable) (*hcl.EvalContext, tfdiags.Diagnostics) { if s == nil { panic("attempt to construct EvalContext for nil Scope") } var diags tfdiags.Diagnostics // Calling NewChild() on a nil parent will // produce an EvalContext with no parent. hclCtx := parent.NewChild() hclCtx.Functions = make(map[string]function.Function) hclCtx.Variables = make(map[string]cty.Value) // The built-in functions are our starting point, but we might add extra // provider-defined functions below. maps.Copy(hclCtx.Functions, s.Functions()) // Easy path for common case where there are no references at all. if len(refs) == 0 { return hclCtx, diags } // First we'll do static validation of the references. This catches things // early that might otherwise not get caught due to unknown values being // present in the scope during planning. staticDiags := s.Data.StaticValidateReferences(ctx, refs, selfAddr, s.SourceAddr) diags = diags.Append(staticDiags) if staticDiags.HasErrors() { return hclCtx, diags } // The reference set we are given has not been de-duped, and so there can // be redundant requests in it for two reasons: // - The same item is referenced multiple times // - Both an item and that item's container are separately referenced. // We will still visit every reference here and ask our data source for // 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. varBuilder := s.newEvalVarBuilder() for _, ref := range refs { if ref.Subject == addrs.Self { diags.Append(varBuilder.putSelfValue(ctx, selfAddr, ref)) continue } if subj, ok := ref.Subject.(addrs.ProviderFunction); ok { // Inject function directly into context if _, ok := hclCtx.Functions[subj.String()]; !ok { fn, fnDiags := s.ProviderFunctions(ctx, subj, ref.SourceRange) diags = diags.Append(fnDiags) if !fnDiags.HasErrors() { hclCtx.Functions[subj.String()] = *fn } } continue } diags = diags.Append(varBuilder.putValueBySubject(ctx, ref)) } varBuilder.buildAllVariablesInto(hclCtx.Variables) return hclCtx, diags } type evalVarBuilder struct { s *Scope dataResources map[string]map[string]cty.Value managedResources map[string]map[string]cty.Value ephemeralResources 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{}, ephemeralResources: 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(ctx context.Context, 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(ctx, 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(ctx context.Context, 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(ctx, subj, rng)) case addrs.ModuleCall: b.wholeModules[subj.Name], normDiags = normalizeRefValue(b.s.Data.GetModule(ctx, subj, rng)) case addrs.InputVariable: b.inputVariables[subj.Name], normDiags = normalizeRefValue(b.s.Data.GetInputVariable(ctx, subj, rng)) case addrs.LocalValue: b.localValues[subj.Name], normDiags = normalizeRefValue(b.s.Data.GetLocalValue(ctx, subj, rng)) case addrs.PathAttr: b.pathAttrs[subj.Name], normDiags = normalizeRefValue(b.s.Data.GetPathAttr(ctx, subj, rng)) case addrs.TerraformAttr: b.terraformAttrs[subj.Name], normDiags = normalizeRefValue(b.s.Data.GetTerraformAttr(ctx, subj, rng)) case addrs.CountAttr: b.countAttrs[subj.Name], normDiags = normalizeRefValue(b.s.Data.GetCountAttr(ctx, subj, rng)) case addrs.ForEachAttr: b.forEachAttrs[subj.Name], normDiags = normalizeRefValue(b.s.Data.GetForEachAttr(ctx, subj, rng)) case addrs.OutputValue: b.outputValues[subj.Name], normDiags = normalizeRefValue(b.s.Data.GetOutput(ctx, subj, rng)) case addrs.Check: b.outputValues[subj.Name], normDiags = normalizeRefValue(b.s.Data.GetCheckBlock(ctx, 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(ctx context.Context, 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.EphemeralResourceMode: into = b.ephemeralResources 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(ctx, 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(b.managedResources) { vals[k] = v } vals["resource"] = cty.ObjectVal(buildResourceObjects(b.managedResources)) vals["data"] = cty.ObjectVal(buildResourceObjects(b.dataResources)) vals["ephemeral"] = cty.ObjectVal(buildResourceObjects(b.ephemeralResources)) 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["tofu"] = 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(b.checkBlocks) > 0 { vals["check"] = cty.ObjectVal(b.checkBlocks) } if len(b.outputValues) > 0 { vals["output"] = cty.ObjectVal(b.outputValues) } if b.self != cty.NilVal { vals["self"] = b.self } } func buildResourceObjects(resources map[string]map[string]cty.Value) map[string]cty.Value { vals := make(map[string]cty.Value) for typeName, nameVals := range resources { vals[typeName] = cty.ObjectVal(nameVals) } return vals } func normalizeRefValue(val cty.Value, diags tfdiags.Diagnostics) (cty.Value, tfdiags.Diagnostics) { if diags.HasErrors() { // If there are errors then we will force an unknown result so that // we can still evaluate and catch type errors but we'll avoid // producing redundant re-statements of the same errors we've already // dealt with here. return cty.UnknownVal(val.Type()), diags } return val, diags } // validEphemeralReferences is checking if val is containing ephemeral marks. // The schema argument is used to figure out if the value is for an ephemeral // context. If it is, then we don't even validate ephemeral marks. // If val contains any ephemeral mark, we check if the attribute containing // an ephemeral value is a write-only one. If not, we generate a diagnostic. // The diagnostics returned by this method need to go through InConfigBody // by the caller of the evaluator to append additional context to the diagnostic // for an enhanced feedback to the user. // // A nil schema will handle the value as unable to hold any ephemeral mark. func validEphemeralReferences(schema *configschema.Block, val cty.Value) (diags tfdiags.Diagnostics) { // Ephemeral resources can reference values with any mark, so ignore this validation for ephemeral blocks if schema != nil && schema.Ephemeral { return diags } // This is the function for schema != nil. // In the case of schema == nil, the function is recreated below. // // In cases of DynamicPseudoType attribute in the schema, the attribute that is actually // referencing an ephemeral value might be missing from the schema. // Therefore, we search for the first ancestor that exists in the schema. attrFromSchema := func(path cty.Path) (*configschema.Attribute, cty.Path) { attrPath := path attr := schema.AttributeByPath(attrPath) for attr == nil { if len(attrPath) == 0 { log.Printf("[WARN] no valid path found in schema for path \"%#v\"", path) return nil, path } attrPath = attrPath[:len(attrPath)-1] attr = schema.AttributeByPath(attrPath) } return attr, attrPath } // We recreate the attribute search in the schema here purely for being sure // that the logic below can run even when the schema is nil. // When there is no schema, there should be no ephemeral value in the block. if schema == nil { attrFromSchema = func(path cty.Path) (*configschema.Attribute, cty.Path) { return nil, path } } _, valueMarks := val.UnmarkDeepWithPaths() for _, pathMark := range valueMarks { _, ok := pathMark.Marks[marks.Ephemeral] if !ok { continue } // If the block is not ephemeral, then only its write-only attributes can reference ephemeral values. // To figure it out, we need to find the attribute by the mark path. attr, foundPath := attrFromSchema(pathMark.Path) if attr != nil && attr.WriteOnly { continue } diags = diags.Append(tfdiags.AttributeValue( tfdiags.Error, "Ephemeral value used in non-ephemeral context", fmt.Sprintf("Attribute %q is referencing an ephemeral value but ephemeral values can be referenced only by other ephemeral attributes or by write-only ones.", tfdiags.FormatCtyPath(foundPath)), foundPath, )) } return diags }