blob: 46f97b0834e4936728e8a989d619535f70f36fb9 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package stackeval
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/terraform/internal/collections"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// Referrer is implemented by types that have expressions that can refer to
// [Referenceable] objects.
type Referrer interface {
// References returns descriptions of all of the expression references
// made from the configuration of the receiver.
References(ctx context.Context) []stackaddrs.AbsReference
}
// ReferencesInExpr returns all of the valid references contained in the given
// HCL expression.
//
// It ignores any invalid references, on the assumption that the expression
// will eventually be evaluated and then those invalid references would be
// reported as errors at that point.
func ReferencesInExpr(expr hcl.Expression) []stackaddrs.Reference {
if expr == nil {
return nil
}
return referencesInTraversals(expr.Variables())
}
// ReferencesInBody returns all of the valid references contained in the given
// HCL body.
//
// It ignores any invalid references, on the assumption that the body
// will eventually be evaluated and then those invalid references would be
// reported as errors at that point.
func ReferencesInBody(body hcl.Body, spec hcldec.Spec) []stackaddrs.Reference {
if body == nil {
return nil
}
return referencesInTraversals(hcldec.Variables(body, spec))
}
func referencesInTraversals(traversals []hcl.Traversal) []stackaddrs.Reference {
if len(traversals) == 0 {
return nil
}
ret := make([]stackaddrs.Reference, 0, len(traversals))
for _, traversal := range traversals {
ref, _, moreDiags := stackaddrs.ParseReference(traversal)
if moreDiags.HasErrors() {
// We'll ignore any traversals that are not valid references,
// on the assumption that we'd catch them during a subsequent
// evaluation of the same expression/body/etc.
continue
}
ret = append(ret, ref)
}
return ret
}
func makeReferencesAbsolute(localRefs []stackaddrs.Reference, stackAddr stackaddrs.StackInstance) []stackaddrs.AbsReference {
if len(localRefs) == 0 {
return nil
}
ret := make([]stackaddrs.AbsReference, 0, len(localRefs))
for _, localRef := range localRefs {
// contextual refs require a more specific scope than an entire
// stack, so they can't be represented as [AbsReference].
if _, isContextual := localRef.Target.(stackaddrs.ContextualRef); isContextual {
continue
}
ret = append(ret, localRef.Absolute(stackAddr))
}
return ret
}
// requiredComponentsForReferrer is the main underlying implementation
// of Applyable.RequiredComponents, allowing the types which directly implement
// that interface to worry only about their own unique way of gathering up
// the relevant references from their configuration, since the work of
// peeling away references until we've found all of the components is the
// same regardless of where the references came from.
//
// This is a best-effort which will produce a complete result only if the
// configuration is completely valid. If not, the result is likely to be
// incomplete, which we accept on the assumption that the invalidity would
// also make the resulting plan non-applyable and thus it doesn't actually
// matter what the required components are.
func (m *Main) requiredComponentsForReferrer(ctx context.Context, obj Referrer, phase EvalPhase) collections.Set[stackaddrs.AbsComponent] {
ret := collections.NewSet[stackaddrs.AbsComponent]()
initialRefs := obj.References(ctx)
// queued tracks objects we've previously queued -- which may or may not
// still be in the queue -- so that we can avoid re-visiting the same
// object multiple times and thus ensure the following loop will definitely
// eventually terminate, even in the presence of reference cycles, because
// the number of unique reference addresses in the configuration is
// finite.
queued := collections.NewSet[stackaddrs.AbsReferenceable]()
queue := make([]stackaddrs.AbsReferenceable, len(initialRefs))
for i, ref := range initialRefs {
queue[i] = ref.Target()
queued.Add(queue[i])
}
for len(queue) != 0 {
targetAddr, remain := queue[0], queue[1:]
queue = remain
// If this is a direct reference to a component then we can just
// add it and continue.
if componentAddr, ok := targetAddr.Item.(stackaddrs.Component); ok {
ret.Add(stackaddrs.AbsComponent{
Stack: targetAddr.Stack,
Item: componentAddr,
})
continue
}
// A stack call reference is also special, as we now want all the
// components of this stack call to be added to the queue as well.
// This doesn't happen automatically with the references as stack calls
// do not have a direct reference to their internal components (it
// actually goes the other way).
if stackCallAddr, ok := targetAddr.Item.(stackaddrs.StackCall); ok {
// We're just adding all the components within the stack to the
// queue. We could be a bit clever if, for example, the reference
// is to an output of the stack call. We could only add the
// components needed by that output. This is an okay compromise for
// now, in which the apply will wait for the whole stack to finish
// before moving on.
currentStack := m.Stack(ctx, targetAddr.Stack, phase)
if currentStack != nil {
next := currentStack.EmbeddedStackCall(stackCallAddr)
instances, _ := next.Instances(ctx, phase)
for _, instance := range instances {
nextStack := instance.Stack(ctx, phase)
for _, component := range nextStack.Components() {
ref := stackaddrs.AbsReferenceable{
Stack: component.addr.Stack,
Item: stackaddrs.Component{
Name: component.addr.Item.Name,
},
}
if !queued.Has(ref) {
queue = append(queue, ref)
queued.Add(ref)
}
}
// We'll also include any other stack calls within the embedded
// stack.
for _, call := range nextStack.EmbeddedStackCalls() {
ref := stackaddrs.AbsReferenceable{
Stack: call.addr.Stack,
Item: call.addr.Item,
}
if !queued.Has(ref) {
queue = append(queue, ref)
queued.Add(ref)
}
}
}
}
// We don't continue here, as we still want to add anything that
// the stack call references below.
}
// For all other address types, we need to find the corresponding
// object and, if it's also Applyable, ask it for its references.
//
// For all of the fallible situations below, we'll just skip over
// this item on failure, because it's not this function's responsibility
// to report problems with the configuration.
//
// Since we're going to ignore all errors anyway, we can safely use
// a reference with no source location information.
ref := stackaddrs.AbsReference{
Stack: targetAddr.Stack,
Ref: stackaddrs.Reference{
Target: targetAddr.Item,
},
}
target, _ := m.ResolveAbsExpressionReference(ctx, ref, phase)
if target == nil {
continue
}
targetReferrer, ok := target.(Referrer)
if !ok {
// Anything that isn't a referer cannot possibly indirectly
// refer to a component.
continue
}
for _, newRef := range targetReferrer.References(ctx) {
newTargetAddr := newRef.Target()
if !queued.Has(newTargetAddr) {
queue = append(queue, newTargetAddr)
queued.Add(newTargetAddr)
}
}
}
return ret
}
// ValidateDependsOn is a helper function that can be used to validate the
// DependsOn field of a component or an embedded stack. It returns diagnostics
// for any invalid references.
//
// The StackConfig argument should be the stack that the component or embedded
// stack is a part of. It is used to validate any references actually exist.
func ValidateDependsOn(source *StackConfig, traversals []hcl.Traversal) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
for _, traversal := range traversals {
// We don't actually care about the result here, only that it has no
// errors.
ref, rest, moreDiags := stackaddrs.ParseReference(traversal)
if moreDiags.HasErrors() {
diags = diags.Append(moreDiags)
continue
}
switch addr := ref.Target.(type) {
case stackaddrs.StackCall:
// Make sure this stack call exists.
if source.StackCall(addr) == nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid depends_on target",
Detail: fmt.Sprintf("The depends_on reference %q does not exist.", addr),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
}
case stackaddrs.Component:
// Make sure this component exists.
if source.Component(addr) == nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid depends_on target",
Detail: fmt.Sprintf("The depends_on reference %q does not exist.", addr),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
}
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid depends_on target",
Detail: fmt.Sprintf("The depends_on argument must refer to an embedded stack or component, but this reference refers to %q.", addr),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
continue // don't do the rest of the checks
}
if len(rest) > 0 {
// for now, we can only reference components and stacks in
// configuration, and not instances of them or outputs from them.
// eg. component.self is valid, but component.self[0] is not.
//
// we'll add a warning, as we don't want users thinking the
// dependency is more precise than it is. But, we'll allow the
// reference as we can still use it just by ignoring the rest.
//
// FIXME: Allowing more fine grained references requires updating
// the requiredComponentsForReferrer function (above) to support
// AbsComponentInstance instead of AbsComponent. This is a
// potentially large refactor, and so only worth it for good
// reason and this isn't really that.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Non-valid depends_on target",
Detail: fmt.Sprintf(DependsOnDeepReferenceDetail, ref.Target),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
}
}
return diags
}
var (
DependsOnDeepReferenceDetail = strings.TrimSpace(`
The depends_on argument should refer directly to an embedded stack or component in configuration, but this reference is too deep.
Terraform Stacks has simplified the reference to the nearest valid target, %q. To remove this warning, update the configuration to the same target.
`)
)