mirror of https://github.com/hashicorp/terraform
To figure out the required order in which we'll apply the plans for all components we need to know how data flows between them. We'll use what's added here during the plan phase to record in the plan which components depend on which other components, and then the apply phase will use that coarse information to schedule the individual component apply operations into a suitable order. None of that is implemented here though, and will follow in future commits. This commit includes new documentation about the apply scheduling approach which includes some mechanisms that are not actually yet included in this commit, but will follow shortly after. This is just a pragmatic compromise to avoid spending time authoring short-lived partial documentation drafts for the intermediate steps while still keeping these increments small enough for practical review.pull/34505/head
parent
5bfdb12098
commit
524da4f278
@ -0,0 +1,162 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package stackeval
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hcldec"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/collections"
|
||||
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
|
||||
)
|
||||
|
||||
// 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(ctx context.Context, expr hcl.Expression) []stackaddrs.Reference {
|
||||
if expr == nil {
|
||||
return nil
|
||||
}
|
||||
return referencesInTraversals(ctx, 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(ctx context.Context, body hcl.Body, spec hcldec.Spec) []stackaddrs.Reference {
|
||||
if body == nil {
|
||||
return nil
|
||||
}
|
||||
return referencesInTraversals(ctx, hcldec.Variables(body, spec))
|
||||
}
|
||||
|
||||
func referencesInTraversals(ctx context.Context, 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
variable "in" {
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
output "out" {
|
||||
type = string
|
||||
value = var.in
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
variable "in" {
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
output "out" {
|
||||
value = var.in
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
|
||||
required_providers {
|
||||
foo = {
|
||||
source = "terraform.io/builtin/foo"
|
||||
}
|
||||
}
|
||||
|
||||
component "a" {
|
||||
source = "./module"
|
||||
}
|
||||
|
||||
component "b" {
|
||||
source = "./module"
|
||||
|
||||
inputs = {
|
||||
in = component.a.out
|
||||
}
|
||||
}
|
||||
|
||||
output "out" {
|
||||
type = string
|
||||
value = component.a.out
|
||||
}
|
||||
|
||||
provider "foo" "bar" {
|
||||
config {
|
||||
in = {
|
||||
a = component.a.out
|
||||
b = component.b.out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stack "child" {
|
||||
source = "./child"
|
||||
|
||||
inputs = {
|
||||
in = component.b.out
|
||||
}
|
||||
}
|
||||
|
||||
component "c" {
|
||||
source = "./module"
|
||||
|
||||
inputs = {
|
||||
# stack.child.out depends indirectly on component.b, so therefore
|
||||
# component.c should transitively depend on component.b.
|
||||
in = "${component.a.out}-${stack.child.out}"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue