stackruntime: Starting up and configuring provider plugins

Each declared provider configuration is now started up and configured
during planning. We don't actually do anything with them yet other than
shut them down again once the plan is complete, but we'll improve on that
in subsequent commits.
pull/34738/head
Martin Atkins 3 years ago
parent 48add21996
commit 3dc3781904

@ -5,7 +5,7 @@ import (
"reflect"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/zclconf/go-cty/cty"
)
@ -19,7 +19,7 @@ import (
func ProviderConfigType(providerAddr addrs.Provider) cty.Type {
return cty.CapsuleWithOps(
fmt.Sprintf("configuration for %s provider", providerAddr.ForDisplay()),
reflect.TypeOf(providers.Interface(nil)),
reflect.TypeOf(stackaddrs.AbsProviderConfigInstance{}),
&cty.CapsuleOps{
TypeGoString: func(goTy reflect.Type) string {
return fmt.Sprintf(
@ -28,13 +28,7 @@ func ProviderConfigType(providerAddr addrs.Provider) cty.Type {
)
},
RawEquals: func(a, b interface{}) bool {
// NOTE: This assumes that providers.Interface implementations
// are always comparable. That's true for the real ones we
// use to represent external plugins, since they are pointers,
// but this will fail for e.g. a mock implementation used in
// tests if it isn't a pointer and contains something
// non-comparable.
return a == b
return a.(*stackaddrs.AbsProviderConfigInstance).UniqueKey() == b.(*stackaddrs.AbsProviderConfigInstance).UniqueKey()
},
ExtensionData: func(key interface{}) interface{} {
switch key {
@ -84,6 +78,99 @@ func ProviderForProviderConfigType(ty cty.Type) addrs.Provider {
return providerAddrI.(addrs.Provider)
}
// ProviderInstanceValue encapsulates a provider config instance address in
// a cty.Value of the given provider config type, or panics if the type and
// address are inconsistent with one another.
func ProviderInstanceValue(ty cty.Type, addr stackaddrs.AbsProviderConfigInstance) cty.Value {
wantProvider := ProviderForProviderConfigType(ty)
if addr.Item.ProviderConfig.Provider != wantProvider {
panic(fmt.Sprintf("can't use %s instance for %s reference", addr.Item.ProviderConfig.Provider, wantProvider))
}
return cty.CapsuleVal(ty, &addr)
}
// ProviderInstanceForValue returns the provider configuration instance
// address encapsulated inside the given value, or panics if the value is
// not of a provider configuration reference type.
//
// Use [IsProviderConfigType] with the value's type to check first if a
// given value is suitable to pass to this function.
func ProviderInstanceForValue(v cty.Value) stackaddrs.AbsProviderConfigInstance {
if !IsProviderConfigType(v.Type()) {
panic("not a provider config value")
}
addrP := v.EncapsulatedValue().(*stackaddrs.AbsProviderConfigInstance)
return *addrP
}
// ProviderInstancePathsInValue searches the leaves of the given value,
// which can be of any type, and returns all of the paths that lead to
// provider configuration references in no particular order.
//
// This is primarily intended for returning errors when values are traversing
// out of the stacks runtime into other subsystems, since provider configuration
// references are a stacks-language-specific concept.
func ProviderInstancePathsInValue(v cty.Value) []cty.Path {
var ret []cty.Path
cty.Transform(v, func(p cty.Path, v cty.Value) (cty.Value, error) {
if IsProviderConfigType(v.Type()) {
ret = append(ret, p)
}
return cty.NilVal, nil
})
return ret
}
// ProviderConfigPathsInType searches the leaves of the given type and returns
// all of the paths that lead to provider configuration references in no
// particular order.
//
// This is a type-oriented version of [ProviderInstancePathsInValue], for
// situations in the language where an author describes a specific type
// constraint that must not include provider configuration reference types
// regardless of final value.
//
// Because this function deals in types rather than values, the returned
// paths will include unknown value placeholders for any index operations
// traversing through collections.
func ProviderConfigPathsInType(ty cty.Type) []cty.Path {
return providerConfigPathsInType(ty, make(cty.Path, 0, 2))
}
func providerConfigPathsInType(ty cty.Type, prefix cty.Path) []cty.Path {
var ret []cty.Path
switch {
case IsProviderConfigType(ty):
// The rest of our traversal is constantly modifying the
// backing array of the prefix slice, so we must make
// a snapshot copy of it here to return.
result := make(cty.Path, len(prefix))
copy(result, prefix)
ret = append(ret, result)
case ty.IsListType():
ret = providerConfigPathsInType(ty.ElementType(), prefix.Index(cty.UnknownVal(cty.Number)))
case ty.IsMapType():
ret = providerConfigPathsInType(ty.ElementType(), prefix.Index(cty.UnknownVal(cty.String)))
case ty.IsSetType():
ret = providerConfigPathsInType(ty.ElementType(), prefix.Index(cty.DynamicVal))
case ty.IsTupleType():
etys := ty.TupleElementTypes()
ret := make([]cty.Path, 0, len(etys))
for i, ety := range etys {
ret = append(ret, providerConfigPathsInType(ety, prefix.IndexInt(i))...)
}
case ty.IsObjectType():
atys := ty.AttributeTypes()
ret := make([]cty.Path, 0, len(atys))
for n, aty := range atys {
ret = append(ret, providerConfigPathsInType(aty, prefix.GetAttr(n))...)
}
default:
// No other types can potentially have nested provider configurations.
}
return ret
}
type providerConfigExtDataKeyType int
const providerConfigExtDataKey = providerConfigExtDataKeyType(0)

@ -1,3 +1,5 @@
package stackeval
type ApplyOpts struct{}
type ApplyOpts struct {
ProviderFactories ProviderFactories
}

@ -20,6 +20,7 @@ type Component struct {
main *Main
forEachValue perEvalPhase[promising.Once[withDiagnostics[cty.Value]]]
instances perEvalPhase[promising.Once[withDiagnostics[map[addrs.InstanceKey]*ComponentInstance]]]
}
var _ Plannable = (*Component)(nil)
@ -153,72 +154,83 @@ func (c *Component) CheckForEachValue(ctx context.Context, phase EvalPhase) (cty
// will visit the stack call directly and ask it to check itself, and that
// call will be the one responsible for returning any diagnostics.
func (c *Component) Instances(ctx context.Context, phase EvalPhase) map[addrs.InstanceKey]*ComponentInstance {
forEachVal := c.ForEachValue(ctx, phase)
switch {
case forEachVal == cty.NilVal:
// No for_each expression at all, then. We have exactly one instance
// without an instance key and with no repetition data.
return map[addrs.InstanceKey]*ComponentInstance{
addrs.NoKey: newComponentInstance(c, addrs.NoKey, instances.RepetitionData{
// no repetition symbols available in this case
}),
}
ret, _ := c.CheckInstances(ctx, phase)
return ret
}
case !forEachVal.IsKnown():
// The for_each expression is too invalid for us to be able to
// know which instances exist. A totally nil map (as opposed to a
// non-nil map of length zero) signals that situation.
return nil
func (c *Component) CheckInstances(ctx context.Context, phase EvalPhase) (map[addrs.InstanceKey]*ComponentInstance, tfdiags.Diagnostics) {
return doOnceWithDiags(
ctx, c.instances.For(phase), c.main,
func(ctx context.Context) (map[addrs.InstanceKey]*ComponentInstance, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
forEachVal := c.ForEachValue(ctx, phase)
default:
// Otherwise we should be able to assume the value is valid per the
// definition of [CheckForEachValue]. The following will panic if
// that other function doesn't satisfy its documented contract;
// if that happens, prefer to correct [CheckForEachValue] than to
// add additional complexity here.
// NOTE: We MUST return a non-nil map from every return path under
// this case, even if there are zero elements in it, because a nil map
// represents an _invalid_ for_each expression (handled above).
ty := forEachVal.Type()
switch {
case ty.IsObjectType() || ty.IsMapType():
elems := forEachVal.AsValueMap()
ret := make(map[addrs.InstanceKey]*ComponentInstance, len(elems))
for k, v := range elems {
ik := addrs.StringKey(k)
ret[ik] = newComponentInstance(c, ik, instances.RepetitionData{
EachKey: cty.StringVal(k),
EachValue: v,
})
}
return ret
case ty.IsSetType():
// ForEachValue should have already guaranteed us a set of strings,
// but we'll check again here just so we can panic more intellgibly
// if that function is buggy.
if ty.ElementType() != cty.String {
panic(fmt.Sprintf("ForEachValue returned invalid result %#v", forEachVal))
}
switch {
case forEachVal == cty.NilVal:
// No for_each expression at all, then. We have exactly one instance
// without an instance key and with no repetition data.
return map[addrs.InstanceKey]*ComponentInstance{
addrs.NoKey: newComponentInstance(c, addrs.NoKey, instances.RepetitionData{
// no repetition symbols available in this case
}),
}, diags
case !forEachVal.IsKnown():
// The for_each expression is too invalid for us to be able to
// know which instances exist. A totally nil map (as opposed to a
// non-nil map of length zero) signals that situation.
return nil, diags
elems := forEachVal.AsValueSlice()
ret := make(map[addrs.InstanceKey]*ComponentInstance, len(elems))
for _, sv := range elems {
k := addrs.StringKey(sv.AsString())
ret[k] = newComponentInstance(c, k, instances.RepetitionData{
EachKey: sv,
EachValue: sv,
})
default:
// Otherwise we should be able to assume the value is valid per the
// definition of [CheckForEachValue]. The following will panic if
// that other function doesn't satisfy its documented contract;
// if that happens, prefer to correct [CheckForEachValue] than to
// add additional complexity here.
// NOTE: We MUST return a non-nil map from every return path under
// this case, even if there are zero elements in it, because a nil map
// represents an _invalid_ for_each expression (handled above).
ty := forEachVal.Type()
switch {
case ty.IsObjectType() || ty.IsMapType():
elems := forEachVal.AsValueMap()
ret := make(map[addrs.InstanceKey]*ComponentInstance, len(elems))
for k, v := range elems {
ik := addrs.StringKey(k)
ret[ik] = newComponentInstance(c, ik, instances.RepetitionData{
EachKey: cty.StringVal(k),
EachValue: v,
})
}
return ret, diags
case ty.IsSetType():
// ForEachValue should have already guaranteed us a set of strings,
// but we'll check again here just so we can panic more intellgibly
// if that function is buggy.
if ty.ElementType() != cty.String {
panic(fmt.Sprintf("ForEachValue returned invalid result %#v", forEachVal))
}
elems := forEachVal.AsValueSlice()
ret := make(map[addrs.InstanceKey]*ComponentInstance, len(elems))
for _, sv := range elems {
k := addrs.StringKey(sv.AsString())
ret[k] = newComponentInstance(c, k, instances.RepetitionData{
EachKey: sv,
EachValue: sv,
})
}
return ret, diags
default:
panic(fmt.Sprintf("ForEachValue returned invalid result %#v", forEachVal))
}
}
return ret
default:
panic(fmt.Sprintf("ForEachValue returned invalid result %#v", forEachVal))
}
}
},
)
}
func (c *Component) ResultValue(ctx context.Context, phase EvalPhase) cty.Value {
@ -284,6 +296,8 @@ func (c *Component) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange,
_, moreDiags := c.CheckForEachValue(ctx, PlanPhase)
diags = diags.Append(moreDiags)
_, moreDiags = c.CheckInstances(ctx, PlanPhase)
diags = diags.Append(moreDiags)
return nil, diags
}

@ -15,6 +15,7 @@ import (
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig/stackconfigtypes"
"github.com/hashicorp/terraform/internal/stacks/stackplan"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
@ -86,6 +87,7 @@ func (c *ComponentInstance) CheckInputVariableValues(ctx context.Context, phase
expr = result.Expression
hclCtx = result.EvalContext
v = result.Value
rng = tfdiags.SourceRangeFromHCL(result.Expression.Range())
}
if defs != nil {
@ -117,6 +119,21 @@ func (c *ComponentInstance) CheckInputVariableValues(ctx context.Context, phase
return cty.DynamicVal, diags
}
for _, path := range stackconfigtypes.ProviderInstancePathsInValue(v) {
err := path.NewErrorf("cannot send provider configuration reference to Terraform module input variable")
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid inputs for component",
Detail: fmt.Sprintf(
"Invalid input variable definition object: %s.\n\nUse the separate \"providers\" argument to specify the provider configurations to use for this component's root module.",
tfdiags.FormatError(err),
),
Subject: rng.ToHCL().Ptr(),
Expression: expr,
EvalContext: hclCtx,
})
}
return v, diags
}

@ -6,6 +6,7 @@ import (
"sync"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/terraform/internal/lang"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/tfdiags"
@ -58,8 +59,17 @@ type ExpressionScope interface {
// the final step of evaluating the expression, returning both the value
// and the evaluation context that was used to build it.
func EvalContextForExpr(ctx context.Context, expr hcl.Expression, phase EvalPhase, scope ExpressionScope) (*hcl.EvalContext, tfdiags.Diagnostics) {
return evalContextForTraversals(ctx, expr.Variables(), phase, scope)
}
// EvalContextForBody produces an HCL expression context for decoding the
// given [hcl.Body] into a value using the given [hcldec.Spec].
func EvalContextForBody(ctx context.Context, body hcl.Body, spec hcldec.Spec, phase EvalPhase, scope ExpressionScope) (*hcl.EvalContext, tfdiags.Diagnostics) {
return evalContextForTraversals(ctx, hcldec.Variables(body, spec), phase, scope)
}
func evalContextForTraversals(ctx context.Context, traversals []hcl.Traversal, phase EvalPhase, scope ExpressionScope) (*hcl.EvalContext, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
traversals := expr.Variables()
refs := make(map[stackaddrs.Referenceable]Referenceable)
for _, traversal := range traversals {
ref, _, moreDiags := stackaddrs.ParseReference(traversal)
@ -174,6 +184,21 @@ func EvalExpr(ctx context.Context, expr hcl.Expression, phase EvalPhase, scope E
return result.Value, diags
}
// EvalBody evaluates the expressions in the given body using hcldec with
// the given schema, returning the resulting value.
func EvalBody(ctx context.Context, body hcl.Body, spec hcldec.Spec, phase EvalPhase, scope ExpressionScope) (cty.Value, tfdiags.Diagnostics) {
hclCtx, diags := EvalContextForBody(ctx, body, spec, phase, scope)
if hclCtx == nil {
return cty.NilVal, diags
}
val, hclDiags := hcldec.Decode(body, spec, hclCtx)
diags = diags.Append(hclDiags)
if val == cty.NilVal {
val = cty.DynamicVal // just so the caller can assume the result is always a value
}
return val, diags
}
// ExprResult bundles an arbitrary result value with the expression and
// evaluation context it was derived from, allowing the recipient to
// potentially emit additional diagnostics if the result is problematic.

@ -2,13 +2,16 @@ package stackeval
import (
"context"
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
"github.com/hashicorp/terraform/internal/stacks/stackplan"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
)
// InputVariable represents an input variable belonging to a [Stack].
@ -100,8 +103,26 @@ func (v *InputVariable) CheckValue(ctx context.Context, phase EvalPhase) (cty.Va
switch {
case v.Addr().Stack.IsRoot():
// TODO: Take input variables from the plan options, then.
return cty.UnknownVal(v.Declaration(ctx).Type.Constraint), diags
extVal := v.main.RootVariableValue(ctx, v.Addr().Item, phase)
wantTy := v.Declaration(ctx).Type.Constraint
val, err := convert.Convert(extVal.Value, wantTy)
const errSummary = "Invalid value for root input variable"
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: errSummary,
Detail: fmt.Sprintf(
"Cannot use the given value for input variable %q: %s.",
v.Addr().Item.Name, err,
),
})
val = cty.UnknownVal(wantTy)
return val, diags
}
// TODO: check the value against any custom validation rules
// declared in the configuration.
return val, diags
default:
definedByCallInst := v.DefinedByStackCallInstance(ctx, phase)
@ -145,3 +166,10 @@ func (v *InputVariable) PlanChanges(ctx context.Context) ([]stackplan.PlannedCha
func (v *InputVariable) tracingName() string {
return v.Addr().String()
}
// ExternalInputValue represents the value of an input variable provided
// from outside the stack configuration.
type ExternalInputValue struct {
Value cty.Value
DefRange tfdiags.SourceRange
}

@ -6,9 +6,13 @@ import (
"sync"
"github.com/hashicorp/go-slug/sourcebundle"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// Main is the central node of all data required for performing the major
@ -36,11 +40,17 @@ type Main struct {
// gathered during the apply process.
applying *mainApplying
// providerFactories is a set of callback functions through which the
// runtime can obtain new instances of each of the available providers.
providerFactories ProviderFactories
// The remaining fields memoize other objects we might create in response
// to method calls. Must lock "mu" before interacting with them.
mu sync.Mutex
mainStackConfig *StackConfig
mainStack *Stack
providerTypes map[addrs.Provider]*ProviderType
cleanupFuncs []func(context.Context) tfdiags.Diagnostics
}
var _ namedPromiseReporter = (*Main)(nil)
@ -63,6 +73,8 @@ func NewForValidating(config *stackconfig.Config, opts ValidateOpts) *Main {
validating: &mainValidating{
opts: opts,
},
providerFactories: opts.ProviderFactories,
providerTypes: make(map[addrs.Provider]*ProviderType),
}
}
@ -75,6 +87,8 @@ func NewForPlanning(config *stackconfig.Config, opts PlanOpts) *Main {
planning: &mainPlanning{
opts: opts,
},
providerFactories: opts.ProviderFactories,
providerTypes: make(map[addrs.Provider]*ProviderType),
}
}
@ -210,6 +224,120 @@ func (m *Main) Stack(ctx context.Context, addr stackaddrs.StackInstance, phase E
return ret
}
// ProviderFactories returns the collection of factory functions for providers
// that are available to this instance of the evaluation runtime.
func (m *Main) ProviderFactories() ProviderFactories {
return m.providerFactories
}
// ProviderType returns the [ProviderType] object representing the given
// provider source address.
//
// This does not check whether the given provider type is available in the
// current evaluation context, but attempting to create a client for a
// provider that isn't available will return an error at startup time.
func (m *Main) ProviderType(ctx context.Context, addr addrs.Provider) *ProviderType {
m.mu.Lock()
defer m.mu.Unlock()
if m.providerTypes[addr] == nil {
m.providerTypes[addr] = newProviderType(m, addr)
}
return m.providerTypes[addr]
}
// ProviderInstance returns the provider instance with the given address,
// or nil if there is no such provider instance.
//
// This function needs to evaluate the for_each expression of each stack along
// the path and of a final multi-instance provider configuration, and so will
// block on whatever those expressions depend on.
//
// If any of the objects along the path have an as-yet-unknown set of
// instances, this function will optimistically return a non-nil provider
// configuration but further operations with that configuration are likely
// to return unknown values themselves.
func (m *Main) ProviderInstance(ctx context.Context, addr stackaddrs.AbsProviderConfigInstance, phase EvalPhase) *ProviderInstance {
stack := m.Stack(ctx, addr.Stack, phase)
if stack == nil {
return nil
}
provider := stack.Provider(ctx, addr.Item.ProviderConfig)
if provider == nil {
return nil
}
insts := provider.Instances(ctx, phase)
if insts == nil {
// A nil result means that the for_each expression is unknown, and
// so we must optimistically return an instance referring to the
// given address which will then presumably yield unknown values
// of some kind when used.
return newProviderInstance(provider, addr.Item.Key, instances.RepetitionData{
EachKey: cty.UnknownVal(cty.String),
EachValue: cty.DynamicVal,
})
}
return insts[addr.Item.Key]
}
func (m *Main) RootVariableValue(ctx context.Context, addr stackaddrs.InputVariable, phase EvalPhase) ExternalInputValue {
switch phase {
case PlanPhase:
if m.planning == nil {
panic("using plan-phase input variable values when not configured for planning")
}
ret, ok := m.planning.opts.InputVariableValues[addr]
if !ok {
return ExternalInputValue{
Value: cty.NullVal(cty.DynamicPseudoType),
}
}
return ret
default:
// Root input variable values are not available in any other phase.
return ExternalInputValue{
Value: cty.DynamicVal, // placeholder value
}
}
}
// RegisterCleanup registers an arbitrary callback function to run when a
// walk driver eventually calls [Main.RunCleanup] on the same receiver.
//
// This is intended for cleaning up any resources that would not naturally
// be cleaned up as a result of garbage-collecting the [Main] object and its
// many descendents.
//
// The context passed to a callback function may be already cancelled by the
// time the callback is running, if the cleanup is running in response to
// cancellation.
func (m *Main) RegisterCleanup(cb func(ctx context.Context) tfdiags.Diagnostics) {
m.mu.Lock()
m.cleanupFuncs = append(m.cleanupFuncs, cb)
m.mu.Unlock()
}
// DoCleanup executes any cleanup functions previously registered using
// [Main.RegisterCleanup], returning any collected diagnostics.
//
// Call this only once evaluation has completed and there aren't any requests
// outstanding that might be using resources that this will free. After calling
// this, the [Main] and all other objects created through it become invalid
// and must not be used anymore.
func (m *Main) DoCleanup(ctx context.Context) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
m.mu.Lock()
funcs := m.cleanupFuncs
m.cleanupFuncs = nil
m.mu.Unlock()
for _, cb := range funcs {
diags = diags.Append(
cb(ctx),
)
}
return diags
}
// mustStackConfig is like [Main.StackConfig] except that it panics if it
// does not find a stack configuration object matching the given address,
// for situations where the absense of a stack config represents a bug

@ -188,6 +188,22 @@ func (m *Main) walkPlanChanges(ctx context.Context, walk *planWalk, stack *Stack
})
}
for _, obj := range stack.Providers(ctx) {
obj := obj // to avoid sharing obj across all iterations
m.walkPlanValidateConfig(ctx, walk, obj.Config(ctx))
m.walkPlanObjectChanges(ctx, walk, obj)
// We need to perform the instance expansion in an overall async
// task because it involves potentially evaluating a for_each expression,
// and that might depend on data from elsewhere in the same stack.
walk.AsyncTask(ctx, func(ctx context.Context) {
insts := obj.Instances(ctx, PlanPhase)
for _, inst := range insts {
m.walkPlanObjectChanges(ctx, walk, inst)
}
})
}
for _, obj := range stack.InputVariables(ctx) {
m.walkPlanValidateConfig(ctx, walk, obj.Config(ctx))
m.walkPlanObjectChanges(ctx, walk, obj)

@ -4,9 +4,11 @@ import (
"context"
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
"github.com/hashicorp/terraform/internal/stacks/stackconfig/stackconfigtypes"
"github.com/hashicorp/terraform/internal/stacks/stackconfig/typeexpr"
"github.com/hashicorp/terraform/internal/stacks/stackplan"
"github.com/hashicorp/terraform/internal/tfdiags"
@ -70,6 +72,33 @@ func (v *OutputValue) ResultType(ctx context.Context) (cty.Type, *typeexpr.Defau
return decl.Type.Constraint, decl.Type.Defaults
}
func (v *OutputValue) CheckResultType(ctx context.Context) (cty.Type, *typeexpr.Defaults, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
ty, defs := v.ResultType(ctx)
decl := v.Declaration(ctx)
if v.Addr().Stack.IsRoot() {
// A root output value cannot return provider configuration references,
// because root outputs outlive the operation that generated them but
// provider instances are live only during a single evaluation.
for _, path := range stackconfigtypes.ProviderConfigPathsInType(ty) {
// We'll construct a synthetic error so that we can conveniently
// use tfdiags.FormatError to help construct a more specific error
// message.
err := path.NewErrorf("cannot return provider configuration reference from the root stack")
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid output value type",
Detail: fmt.Sprintf(
"Unsupported output value type: %s.",
tfdiags.FormatError(err),
),
Subject: decl.Type.Expression.Range().Ptr(),
})
}
}
return ty, defs, diags
}
func (v *OutputValue) ResultValue(ctx context.Context, phase EvalPhase) cty.Value {
val, _ := v.CheckResultValue(ctx, phase)
return val
@ -120,7 +149,11 @@ func (v *OutputValue) CheckResultValue(ctx context.Context, phase EvalPhase) (ct
func (v *OutputValue) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
_, moreDiags := v.CheckResultValue(ctx, PlanPhase)
// FIXME: We should really check the type during the validation phase
// in OutputValueConfig, rather than the planning phase in OutputValue.
_, _, moreDiags := v.CheckResultType(ctx)
diags = diags.Append(moreDiags)
_, moreDiags = v.CheckResultValue(ctx, PlanPhase)
diags = diags.Append(moreDiags)
return nil, diags

@ -4,15 +4,17 @@ import (
"context"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackplan"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
type PlanOpts struct {
PlanningMode plans.Mode
InputVariableValues map[string]cty.Value
InputVariableValues map[stackaddrs.InputVariable]ExternalInputValue
ProviderFactories ProviderFactories
}
// Plannable is implemented by objects that can participate in planning.

@ -0,0 +1,298 @@
package stackeval
import (
"context"
"fmt"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
"github.com/hashicorp/terraform/internal/stacks/stackplan"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// Provider represents a provider configuration in a particular stack config.
type Provider struct {
addr stackaddrs.AbsProviderConfig
config *stackconfig.ProviderConfig
main *Main
forEachValue perEvalPhase[promising.Once[withDiagnostics[cty.Value]]]
instances perEvalPhase[promising.Once[withDiagnostics[map[addrs.InstanceKey]*ProviderInstance]]]
}
func newProvider(main *Main, addr stackaddrs.AbsProviderConfig, config *stackconfig.ProviderConfig) *Provider {
return &Provider{
addr: addr,
config: config,
main: main,
}
}
func (p *Provider) Addr() stackaddrs.AbsProviderConfig {
return p.addr
}
func (p *Provider) Declaration(ctx context.Context) *stackconfig.ProviderConfig {
return p.config
}
func (p *Provider) Config(ctx context.Context) *ProviderConfig {
configAddr := stackaddrs.ConfigForAbs(p.Addr())
stackConfig := p.main.StackConfig(ctx, configAddr.Stack)
if stackConfig == nil {
return nil
}
return stackConfig.Provider(ctx, configAddr.Item)
}
func (p *Provider) ProviderType(ctx context.Context) *ProviderType {
return p.main.ProviderType(ctx, p.Addr().Item.Provider)
}
func (p *Provider) Stack(ctx context.Context) *Stack {
// Unchecked because we should've been constructed from the same stack
// object we're about to return, and so this should be valid unless
// the original construction was from an invalid object itself.
return p.main.StackUnchecked(ctx, p.Addr().Stack)
}
// InstRefValueType returns the type of any values that represent references to
// instances of this provider configuration.
//
// All configurations for the same provider share the same type.
func (p *Provider) InstRefValueType(ctx context.Context) cty.Type {
decl := p.Declaration(ctx)
return providerInstanceRefType(decl.ProviderAddr)
}
// ForEachValue returns the result of evaluating the "for_each" expression
// for this provider configuration, with the following exceptions:
// - If the provider config doesn't use "for_each" at all, returns [cty.NilVal].
// - If the for_each expression is present but too invalid to evaluate,
// returns [cty.DynamicVal] to represent that the for_each value cannot
// be determined.
//
// A present and valid "for_each" expression produces a result that's
// guaranteed to be:
// - Either a set of strings, a map of any element type, or an object type
// - Known and not null (only the top-level value)
// - Not sensitive (only the top-level value)
func (p *Provider) ForEachValue(ctx context.Context, phase EvalPhase) cty.Value {
ret, _ := p.CheckForEachValue(ctx, phase)
return ret
}
// CheckForEachValue evaluates the "for_each" expression if present, validates
// that its value is valid, and then returns that value.
//
// If this call does not use "for_each" then this immediately returns cty.NilVal
// representing the absense of the value.
//
// If the diagnostics does not include errors and the result is not cty.NilVal
// then callers can assume that the result value will be:
// - Either a set of strings, a map of any element type, or an object type
// - Known and not null (except for nested map/object element values)
// - Not sensitive (only the top-level value)
//
// If the diagnostics _does_ include errors then the result might be
// [cty.DynamicVal], which represents that the for_each expression was so invalid
// that we cannot know the for_each value.
func (p *Provider) CheckForEachValue(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) {
val, diags := doOnceWithDiags(
ctx, p.forEachValue.For(phase), p.main,
func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
cfg := p.Declaration(ctx)
switch {
case cfg.ForEach != nil:
result, moreDiags := evaluateForEachExpr(ctx, cfg.ForEach, phase, p.Stack(ctx))
diags = diags.Append(moreDiags)
if diags.HasErrors() {
return cty.DynamicVal, diags
}
if !result.Value.IsKnown() {
// FIXME: We should somehow allow this and emit a
// "deferred change" representing all of the as-yet-unknown
// instances of this call and everything beneath it.
diags = diags.Append(result.Diagnostic(
tfdiags.Error,
"Invalid for_each value",
"The for_each value must not be derived from values that will be determined only during the apply phase.",
))
}
return result.Value, diags
default:
// This stack config doesn't use for_each at all
return cty.NilVal, diags
}
},
)
if val == cty.NilVal && diags.HasErrors() {
// We use cty.DynamicVal as the placeholder for an invalid for_each,
// to represent "unknown for_each value" as distinct from "no for_each
// expression at all".
val = cty.DynamicVal
}
return val, diags
}
// Instances returns all of the instances of the provider config known to be
// declared by the configuration.
//
// Calcluating this involves evaluating the call's for_each expression if any,
// and so this call may block on evaluation of other objects in the
// configuration.
//
// If the configuration has an invalid definition of the instances then the
// result will be nil. Callers that need to distinguish between invalid
// definitions and valid definitions of zero instances can rely on the
// result being a non-nil zero-length map in the latter case.
//
// This function doesn't return any diagnostics describing ways in which the
// for_each expression is invalid because we assume that the main plan walk
// will visit the stack call directly and ask it to check itself, and that
// call will be the one responsible for returning any diagnostics.
func (p *Provider) Instances(ctx context.Context, phase EvalPhase) map[addrs.InstanceKey]*ProviderInstance {
ret, _ := p.CheckInstances(ctx, phase)
return ret
}
func (p *Provider) CheckInstances(ctx context.Context, phase EvalPhase) (map[addrs.InstanceKey]*ProviderInstance, tfdiags.Diagnostics) {
return doOnceWithDiags(
ctx, p.instances.For(phase), p.main,
func(ctx context.Context) (map[addrs.InstanceKey]*ProviderInstance, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
forEachVal := p.ForEachValue(ctx, phase)
switch {
case forEachVal == cty.NilVal:
// No for_each expression at all, then. We have exactly one instance
// without an instance key and with no repetition data.
return map[addrs.InstanceKey]*ProviderInstance{
addrs.NoKey: newProviderInstance(p, addrs.NoKey, instances.RepetitionData{
// no repetition symbols available in this case
}),
}, diags
case !forEachVal.IsKnown():
// The for_each expression is too invalid for us to be able to
// know which instances exist. A totally nil map (as opposed to a
// non-nil map of length zero) signals that situation.
return nil, diags
default:
// Otherwise we should be able to assume the value is valid per the
// definition of [CheckForEachValue]. The following will panic if
// that other function doesn't satisfy its documented contract;
// if that happens, prefer to correct [CheckForEachValue] than to
// add additional complexity here.
// NOTE: We MUST return a non-nil map from every return path under
// this case, even if there are zero elements in it, because a nil map
// represents an _invalid_ for_each expression (handled above).
ty := forEachVal.Type()
switch {
case ty.IsObjectType() || ty.IsMapType():
elems := forEachVal.AsValueMap()
ret := make(map[addrs.InstanceKey]*ProviderInstance, len(elems))
for k, v := range elems {
ik := addrs.StringKey(k)
ret[ik] = newProviderInstance(p, ik, instances.RepetitionData{
EachKey: cty.StringVal(k),
EachValue: v,
})
}
return ret, diags
case ty.IsSetType():
// ForEachValue should have already guaranteed us a set of strings,
// but we'll check again here just so we can panic more intellgibly
// if that function is buggy.
if ty.ElementType() != cty.String {
panic(fmt.Sprintf("ForEachValue returned invalid result %#v", forEachVal))
}
elems := forEachVal.AsValueSlice()
ret := make(map[addrs.InstanceKey]*ProviderInstance, len(elems))
for _, sv := range elems {
k := addrs.StringKey(sv.AsString())
ret[k] = newProviderInstance(p, k, instances.RepetitionData{
EachKey: sv,
EachValue: sv,
})
}
return ret, diags
default:
panic(fmt.Sprintf("ForEachValue returned invalid result %#v", forEachVal))
}
}
},
)
}
// ExprReferenceValue implements Referenceable, returning a value containing
// one or more values that act as references to instances of the provider.
func (p *Provider) ExprReferenceValue(ctx context.Context, phase EvalPhase) cty.Value {
decl := p.Declaration(ctx)
insts := p.Instances(ctx, phase)
refType := p.InstRefValueType(ctx)
switch {
case decl.ForEach != nil:
if insts == nil {
return cty.UnknownVal(cty.Map(refType))
}
elems := make(map[string]cty.Value, len(insts))
for instKey := range insts {
k, ok := instKey.(addrs.StringKey)
if !ok {
panic(fmt.Sprintf("provider config with for_each has invalid instance key of type %T", instKey))
}
elems[string(k)] = cty.CapsuleVal(refType, &stackaddrs.ProviderConfigInstance{
ProviderConfig: p.Addr().Item,
Key: instKey,
})
}
return cty.MapVal(elems)
default:
if insts == nil {
return cty.UnknownVal(refType)
}
return cty.CapsuleVal(refType, &stackaddrs.ProviderConfigInstance{
ProviderConfig: p.Addr().Item,
Key: addrs.NoKey,
})
}
}
// PlanChanges implements Plannable.
func (p *Provider) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
_, moreDiags := p.CheckForEachValue(ctx, PlanPhase)
diags = diags.Append(moreDiags)
_, moreDiags = p.CheckInstances(ctx, PlanPhase)
diags = diags.Append(moreDiags)
// Everything else is instance-specific and so the plan walk driver must
// call p.Instances and ask each instance to plan itself.
return nil, diags
}
// tracingName implements Plannable.
func (p *Provider) tracingName() string {
return p.Addr().String()
}

@ -0,0 +1,183 @@
package stackeval
import (
"context"
"fmt"
"sync"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
"github.com/hashicorp/terraform/internal/stacks/stackconfig/stackconfigtypes"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// ProviderConfig represents a single "provider" block in a stack configuration.
type ProviderConfig struct {
addr stackaddrs.ConfigProviderConfig
config *stackconfig.ProviderConfig
main *Main
providerArgs promising.Once[withDiagnostics[cty.Value]]
}
func newProviderConfig(main *Main, addr stackaddrs.ConfigProviderConfig, config *stackconfig.ProviderConfig) *ProviderConfig {
return &ProviderConfig{
addr: addr,
config: config,
main: main,
}
}
func (p *ProviderConfig) Addr() stackaddrs.ConfigProviderConfig {
return p.addr
}
func (p *ProviderConfig) Declaration(ctx context.Context) *stackconfig.ProviderConfig {
return p.config
}
func (p *ProviderConfig) ProviderType(ctx context.Context) *ProviderType {
return p.main.ProviderType(ctx, p.Addr().Item.Provider)
}
func (p *ProviderConfig) InstRefValueType(ctx context.Context) cty.Type {
decl := p.Declaration(ctx)
return providerInstanceRefType(decl.ProviderAddr)
}
func (p *ProviderConfig) ProviderArgsDecoderSpec(ctx context.Context) (hcldec.Spec, error) {
providerType := p.ProviderType(ctx)
schema, err := providerType.Schema(ctx)
if err != nil {
return nil, err
}
if schema.Provider.Block == nil {
return hcldec.ObjectSpec{}, nil
}
return schema.Provider.Block.DecoderSpec(), nil
}
// ProviderArgs returns an object value representing an approximation of all
// provider instances declared by this provider configuration, or
// an unknown value (possibly [cty.DynamicVal]) if the configuration is too
// invalid to produce any answer at all.
func (p *ProviderConfig) ProviderArgs(ctx context.Context) cty.Value {
v, _ := p.CheckProviderArgs(ctx)
return v
}
func (p *ProviderConfig) CheckProviderArgs(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
return doOnceWithDiags(
ctx, &p.providerArgs, p.main,
func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
providerType := p.ProviderType(ctx)
decl := p.Declaration(ctx)
spec, err := p.ProviderArgsDecoderSpec(ctx)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Failed to read provider schema",
Detail: fmt.Sprintf(
"Error while reading the schema for %q: %s.",
providerType.Addr(), err,
),
Subject: decl.DeclRange.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
}
client, err := providerType.UnconfiguredClient(ctx)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Failed to initialize provider",
Detail: fmt.Sprintf(
"Error initializing %q to validate %s: %s.",
providerType.Addr(), p.Addr(), err,
),
Subject: decl.DeclRange.ToHCL().Ptr(),
})
return cty.UnknownVal(hcldec.ImpliedType(spec)), diags
}
defer client.Close()
configVal, moreDiags := EvalBody(ctx, decl.Config, spec, ValidatePhase, p)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return cty.UnknownVal(hcldec.ImpliedType(spec)), diags
}
validateResp := client.ValidateProviderConfig(providers.ValidateProviderConfigRequest{
Config: configVal,
})
diags = diags.Append(validateResp.Diagnostics)
if validateResp.Diagnostics.HasErrors() {
return cty.UnknownVal(hcldec.ImpliedType(spec)), diags
}
return configVal, diags
},
)
}
// ResolveExpressionReference implements ExpressionScope for the purposes
// of validating the static provider configuration before it has been expanded
// into multiple instances.
func (p *ProviderConfig) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) {
repetition := instances.RepetitionData{}
if p.Declaration(ctx).ForEach != nil {
// We're producing an approximation across all eventual instances
// of this call, so we'll set each.key and each.value to unknown
// values.
repetition.EachKey = cty.UnknownVal(cty.String).RefineNotNull()
repetition.EachValue = cty.DynamicVal
}
return p.main.
mustStackConfig(ctx, p.Addr().Stack).
resolveExpressionReference(ctx, ref, repetition, nil)
}
var providerInstanceRefTypes = map[addrs.Provider]cty.Type{}
var providerInstanceRefTypesMu sync.Mutex
// providerInstanceRefType returns the singleton cty capsule type for a given
// provider source address, creating a new type if a particular source address
// was not requested before.
func providerInstanceRefType(sourceAddr addrs.Provider) cty.Type {
providerInstanceRefTypesMu.Lock()
defer providerInstanceRefTypesMu.Unlock()
ret, ok := providerInstanceRefTypes[sourceAddr]
if ok {
return ret
}
providerInstanceRefTypes[sourceAddr] = stackconfigtypes.ProviderConfigType(sourceAddr)
return providerInstanceRefTypes[sourceAddr]
}
// Validate implements Validatable.
func (p *ProviderConfig) Validate(ctx context.Context) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
// TODO: Actually validate the configuration against the schema.
// Currently we're doing that only during the plan phase, but
// it would be better to catch statically-detectable problems
// earlier and only once per provider block, rather than repeatedly
// for each instance of a provider.
return diags
}
// tracingName implements Validatable.
func (p *ProviderConfig) tracingName() string {
return p.Addr().String()
}

@ -0,0 +1,174 @@
package stackeval
import (
"context"
"fmt"
"log"
"sync"
"sync/atomic"
"time"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/providers"
)
// ProviderFactories is a collection of factory functions for starting new
// instances of various providers.
type ProviderFactories map[addrs.Provider]providers.Factory
func (pf ProviderFactories) ProviderAvailable(providerAddr addrs.Provider) bool {
_, available := pf[providerAddr]
return available
}
// NewUnconfiguredClient launches a new instance of the requested provider,
// if available, and returns it in an unconfigured state.
//
// Callers that need a _configured_ provider can then call
// [providers.Interface.Configure] on the result to configure it, making it
// ready for the majority of operations that require a configured provider.
func (pf ProviderFactories) NewUnconfiguredClient(providerAddr addrs.Provider) (providers.Interface, error) {
f, ok := pf[providerAddr]
if !ok {
return nil, fmt.Errorf("provider is not available in this execution context")
}
return f()
}
// rcProviderClient is a reference-counting abstraction to help with sharing
// instances of providers between multiple callers and shutting them down
// once all callers have finished with them.
//
// It encapsulates the problem of tracking the number of callers, instantiating
// a provider when necessary, and closing that provider once there are no
// active references remaining.
type rcProviderClient struct {
// Factory is the function to use to create a new instance of the provider
// when needed.
//
// This function should perform all of the steps that ought to happen
// exactly once before the provider becomes useful to its possibly-many
// constituents. In particular, if multiple callers are hoping to share
// a single _configured_ provider then the factory function must be the
// one to configure it, so that the callers don't all need to race to
// be the one to do the one-time configuration themselves.
Factory providers.Factory
// must hold mu when interacting with the other fields below
mu sync.Mutex
callers int
client providers.Interface
}
func (rcpc *rcProviderClient) Borrow(ctx context.Context) (providers.Interface, error) {
rcpc.mu.Lock()
defer rcpc.mu.Unlock()
rcpc.callers++
var client providers.Interface
if rcpc.client != nil {
client = rcpc.client
} else {
var err error
client, err = rcpc.Factory()
if err != nil {
return nil, err
}
}
// each caller gets its own "closed" flag captured into its "close" closure,
// so we can silently ignore duplicate calls to "close".
var closed atomic.Bool
close := func() error {
if !closed.CompareAndSwap(false, true) {
// We silently ignore redundant calls to close.
// We intentionally don't panic here because we want to encourage
// callers to call Close liberally in every possible return
// path, rather than working hard to ensure only one call and
// potentially ending up not calling it at all in some edge cases.
return nil
}
// To reduce churn of provider clients when different callers are
// taking turns to use them, we'll decrement the caller count immediately
// but pause briefly before we check if the count has reached zero
// so that another caller has a chance to acquire the same client
// and thus avoid the overhead of starting it up again.
rcpc.mu.Lock()
rcpc.callers--
rcpc.mu.Unlock()
// We'll wait either for our anti-churn delay or until the context is
// cancelled before we test whether we should shut down the client,
// but that's an implementation detail the caller doesn't need to know
// about and so we'll deal with that in a separate goroutine.
go func() {
// This time selection is essentially arbitrary; we're aiming to
// find a happy compromise between using more RAM by keeping a
// provider active a little longer vs. spending less time on
// startup and shutdown overhead as usage of a provider passes
// between different callers that are not necessarily synchronized
// with one another.
timer := time.NewTimer(1 * time.Second)
select {
case <-timer.C:
case <-ctx.Done():
}
rcpc.mu.Lock()
if rcpc.callers > 0 {
rcpc.mu.Unlock()
return // someone else requested the client in the meantime
}
if rcpc.client == nil {
rcpc.mu.Unlock()
return // someone else already closed the client in the meantime
}
// We'll take our own private copy of the client and nil out the
// shared one so that we don't need to hold the mutex for the
// entire (possibly-time-consuming) shutdown procedure.
oldClient := rcpc.client
rcpc.client = nil
rcpc.mu.Unlock()
// NOTE: MUST NOT access p.client or p.callerCount after this point
err := oldClient.Close()
if err != nil {
// We don't really have any way to properly handle an error here,
// but "Close" is typically just sending the child process a
// kill signal and so if that fails there wouldn't be much we could
// do to recover anyway.
log.Printf("[ERROR] failed to shut down provider instance: %s", err)
}
}()
return nil
}
// To honor the providers.Interface abstraction while still allowing
// multiple callers to share a single client we wrap the real client
// to intercept the Close method and treat it as decrementing the
// reference count rather than closing the client directly.
return providerClose{
Interface: client,
close: close,
}, nil
}
// providerClose is an implementation of providers.Interface that intercepts
// the "Close" operation and diverts it into a callback function. We use this
// so that multiple callers can share a single client in a coordinated way,
// so they can all call Close as normal without interfering with one another.
type providerClose struct {
providers.Interface
close func() error
}
var _ providers.Interface = providerClose{}
func (p providerClose) Close() error {
return p.close()
}

@ -0,0 +1,461 @@
package stackeval
import (
"context"
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackplan"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/hashicorp/terraform/version"
"github.com/zclconf/go-cty/cty"
)
// ProviderInstance represents one instance of a provider.
//
// A provider configuration block with the for_each argument appears as a
// single [ProviderConfig], then one [Provider] for each stack config instance
// the provider belongs to, and then one [ProviderInstance] for each
// element of for_each for each [Provider].
type ProviderInstance struct {
provider *Provider
key addrs.InstanceKey
repetition instances.RepetitionData
main *Main
providerArgs perEvalPhase[promising.Once[withDiagnostics[cty.Value]]]
client perEvalPhase[promising.Once[withDiagnostics[providers.Interface]]]
}
func newProviderInstance(provider *Provider, key addrs.InstanceKey, repetition instances.RepetitionData) *ProviderInstance {
return &ProviderInstance{
provider: provider,
key: key,
main: provider.main,
repetition: repetition,
}
}
func (p *ProviderInstance) Addr() stackaddrs.AbsProviderConfigInstance {
providerAddr := p.provider.Addr()
return stackaddrs.AbsProviderConfigInstance{
Stack: providerAddr.Stack,
Item: stackaddrs.ProviderConfigInstance{
ProviderConfig: providerAddr.Item,
Key: p.key,
},
}
}
func (p *ProviderInstance) ProviderType(ctx context.Context) *ProviderType {
return p.main.ProviderType(ctx, p.Addr().Item.ProviderConfig.Provider)
}
func (p *ProviderInstance) ProviderArgsDecoderSpec(ctx context.Context) (hcldec.Spec, error) {
return p.provider.Config(ctx).ProviderArgsDecoderSpec(ctx)
}
// ProviderArgs returns an object value representing an approximation of all
// provider instances declared by this provider configuration, or
// an unknown value (possibly [cty.DynamicVal]) if the configuration is too
// invalid to produce any answer at all.
func (p *ProviderInstance) ProviderArgs(ctx context.Context, phase EvalPhase) cty.Value {
v, _ := p.CheckProviderArgs(ctx, phase)
return v
}
func (p *ProviderInstance) CheckProviderArgs(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) {
return doOnceWithDiags(
ctx, p.providerArgs.For(phase), p.main,
func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
providerType := p.ProviderType(ctx)
decl := p.provider.Declaration(ctx)
spec, err := p.ProviderArgsDecoderSpec(ctx)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Failed to read provider schema",
Detail: fmt.Sprintf(
"Error while reading the schema for %q: %s.",
providerType.Addr(), err,
),
Subject: decl.DeclRange.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
}
configVal, moreDiags := EvalBody(ctx, decl.Config, spec, phase, p)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return cty.UnknownVal(hcldec.ImpliedType(spec)), diags
}
unconfClient, err := providerType.UnconfiguredClient(ctx)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Failed to start provider plugin",
Detail: fmt.Sprintf(
"Error while instantiating %q: %s.",
providerType.Addr(), err,
),
Subject: decl.DeclRange.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
}
defer unconfClient.Close()
validateResp := unconfClient.ValidateProviderConfig(providers.ValidateProviderConfigRequest{
Config: configVal,
})
diags = diags.Append(validateResp.Diagnostics)
if validateResp.Diagnostics.HasErrors() {
return cty.DynamicVal, diags
}
return configVal, diags
},
)
}
// Client returns a client object for the provider instance, already configured
// per the provider configuration arguments and ready to use.
//
// If the configured arguments are invalid then this might return a stub
// provider client that implements all methods either as silent no-ops or as
// returning error diagnostics, so callers can just treat the returned client
// as always valid.
//
// Callers must call Close on the returned client once they have finished using
// the client.
func (p *ProviderInstance) Client(ctx context.Context, phase EvalPhase) providers.Interface {
ret, _ := p.CheckClient(ctx, phase)
return ret
}
func (p *ProviderInstance) CheckClient(ctx context.Context, phase EvalPhase) (providers.Interface, tfdiags.Diagnostics) {
return doOnceWithDiags(
ctx, p.client.For(phase), p.main,
func(ctx context.Context) (providers.Interface, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
if p.repetition.EachKey != cty.NilVal && !p.repetition.EachKey.IsKnown() {
// If we're a placeholder standing in for all instances of
// a provider block whose for_each is unknown then we
// can't configure.
return stubConfiguredProvider{unknown: true}, diags
}
if p.repetition.CountIndex != cty.NilVal && !p.repetition.CountIndex.IsKnown() {
// If we're a placeholder standing in for all instances of
// a provider block whose count is unknown then we
// can't configure.
return stubConfiguredProvider{unknown: true}, diags
}
args := p.ProviderArgs(ctx, phase)
if !args.IsKnown() {
// If we don't know the provider configuration at all then
// we'll just immediately return a stub client, since
// no provider can accept a wholly-unknown configuration.
// (Known objects with unknown attribute values inside are
// okay to try and so don't return immediately here.)
return stubConfiguredProvider{unknown: true}, diags
}
providerType := p.ProviderType(ctx)
decl := p.provider.Declaration(ctx)
client, err := p.main.ProviderFactories().NewUnconfiguredClient(providerType.Addr())
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Failed to start provider plugin",
Detail: fmt.Sprintf(
"Could not create an instance of %s for %s: %s.",
providerType.Addr(), p.Addr(), err,
),
Subject: decl.DeclRange.ToHCL().Ptr(),
})
return stubConfiguredProvider{unknown: false}, diags
}
// If this provider is implemented as a separate plugin then we
// must terminate its child process once evaluation is complete.
p.main.RegisterCleanup(func(ctx context.Context) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
err := client.Close()
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Failed to terminate provider plugin",
Detail: fmt.Sprintf(
"Error closing the instance of %s for %s: %s.",
providerType.Addr(), p.Addr(), err,
),
Subject: decl.DeclRange.ToHCL().Ptr(),
})
}
return diags
})
resp := client.ConfigureProvider(providers.ConfigureProviderRequest{
TerraformVersion: version.SemVer.String(),
Config: args,
})
diags = diags.Append(resp.Diagnostics)
if resp.Diagnostics.HasErrors() {
// If the provider didn't configure successfully then it won't
// meet the expectations of our callers and so we'll return a
// stub instead. (The real provider stays running until it
// gets cleaned up by the cleanup function above, despite being
// inaccessible to the caller.)
return stubConfiguredProvider{unknown: false}, diags
}
return providerClose{
close: func() error {
// We just totally ignore close for configured providers,
// because we'll deal with them in the cleanup phase instead.
return nil
},
Interface: client,
}, diags
},
)
}
// ResolveExpressionReference implements ExpressionScope for expressions other
// than the for_each argument inside a provider block, which get evaluated
// once per provider instance.
func (p *ProviderInstance) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) {
stack := p.provider.Stack(ctx)
return stack.resolveExpressionReference(ctx, ref, nil, p.repetition)
}
// PlanChanges implements Plannable.
func (p *ProviderInstance) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
_, moreDiags := p.CheckProviderArgs(ctx, PlanPhase)
diags = diags.Append(moreDiags)
// NOTE: CheckClient starts and configures the provider as a side-effect.
// If this is a plugin-based provider then the plugin process will stay
// running for the remainder of the planning phase.
_, moreDiags = p.CheckClient(ctx, PlanPhase)
diags = diags.Append(moreDiags)
return nil, diags
}
// tracingName implements Plannable.
func (p *ProviderInstance) tracingName() string {
return p.Addr().String()
}
// stubConfiguredProvider is a placeholder provider used when ConfigureProvider
// on a real provider fails, so that callers can still receieve a usable client
// that will just produce placeholder values from its operations.
//
// This is essentially the cty.DynamicVal equivalent for providers.Interface,
// allowing us to follow our usual pattern that only one return path carries
// diagnostics up to the caller and all other codepaths just do their best
// to unwind with placeholder values. It's intended only for use in situations
// that would expect an already-configured provider, so it's incorrect to call
// [ConfigureProvider] on a value of this type.
//
// Some methods of this type explicitly return errors saying that the provider
// configuration was invalid, while others just optimistically do nothing at
// all. The general rule is that anything that would for a normal provider
// be expected to perform externally-visible side effects must return an error
// to be explicit that those side effects did not occur, but we can silently
// skip anything that is a Terraform-only detail.
//
// As usual with provider calls, the returned diagnostics must be annotated
// using [tfdiags.Diagnostics.InConfigBody] with the relevant configuration body
// so that they can be attributed to the appropriate configuration element.
type stubConfiguredProvider struct {
// If unknown is true then the implementation will assume it's acting
// as a placeholder for a provider whose configuration isn't yet
// sufficiently known to be properly instantiated, which means that
// plan-time operations will return totally-unknown values.
// Otherwise any operation that is supposed to perform a side-effect
// will fail with an error saying that the provider configuration
// is invalid.
unknown bool
}
var _ providers.Interface = stubConfiguredProvider{}
// ApplyResourceChange implements providers.Interface.
func (stubConfiguredProvider) ApplyResourceChange(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Provider configuration is invalid",
"Cannot apply changes because this resource's associated provider configuration is invalid.",
nil, // nil attribute path means the overall configuration block
))
return providers.ApplyResourceChangeResponse{
Diagnostics: diags,
}
}
// Close implements providers.Interface.
func (stubConfiguredProvider) Close() error {
return nil
}
// ConfigureProvider implements providers.Interface.
func (stubConfiguredProvider) ConfigureProvider(req providers.ConfigureProviderRequest) providers.ConfigureProviderResponse {
// This provider is used only in situations where ConfigureProvider on
// a real provider fails and the recipient was expecting a configured
// provider, so it doesn't make sense to configure it.
panic("can't configure the stub provider")
}
// GetProviderSchema implements providers.Interface.
func (stubConfiguredProvider) GetProviderSchema() providers.GetProviderSchemaResponse {
return providers.GetProviderSchemaResponse{}
}
// ImportResourceState implements providers.Interface.
func (p stubConfiguredProvider) ImportResourceState(req providers.ImportResourceStateRequest) providers.ImportResourceStateResponse {
var diags tfdiags.Diagnostics
if p.unknown {
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Provider configuration is deferred",
"Cannot import an existing object into this resource because its associated provider configuration is deferred to a later operation due to unknown expansion.",
nil, // nil attribute path means the overall configuration block
))
} else {
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Provider configuration is invalid",
"Cannot import an existing object into this resource because its associated provider configuration is invalid.",
nil, // nil attribute path means the overall configuration block
))
}
return providers.ImportResourceStateResponse{
Diagnostics: diags,
}
}
// PlanResourceChange implements providers.Interface.
func (p stubConfiguredProvider) PlanResourceChange(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
if p.unknown {
return providers.PlanResourceChangeResponse{
PlannedState: cty.DynamicVal,
}
}
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Provider configuration is invalid",
"Cannot plan changes for this resource because its associated provider configuration is invalid.",
nil, // nil attribute path means the overall configuration block
))
return providers.PlanResourceChangeResponse{
Diagnostics: diags,
}
}
// ReadDataSource implements providers.Interface.
func (p stubConfiguredProvider) ReadDataSource(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
if p.unknown {
return providers.ReadDataSourceResponse{
State: cty.DynamicVal,
}
}
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Provider configuration is invalid",
"Cannot read from this data source because its associated provider configuration is invalid.",
nil, // nil attribute path means the overall configuration block
))
return providers.ReadDataSourceResponse{
Diagnostics: diags,
}
}
// ReadResource implements providers.Interface.
func (stubConfiguredProvider) ReadResource(req providers.ReadResourceRequest) providers.ReadResourceResponse {
// For this one we'll just optimistically assume that the remote object
// hasn't changed. In many cases we'll fail calling PlanResourceChange
// right afterwards anyway, and even if not we'll get another opportunity
// to refresh on a future run once the provider configuration is fixed.
return providers.ReadResourceResponse{
NewState: req.PriorState,
Private: req.Private,
}
}
// Stop implements providers.Interface.
func (stubConfiguredProvider) Stop() error {
// This stub provider never actually does any real work, so there's nothing
// for us to stop.
return nil
}
// UpgradeResourceState implements providers.Interface.
func (p stubConfiguredProvider) UpgradeResourceState(req providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse {
if p.unknown {
return providers.UpgradeResourceStateResponse{
UpgradedState: cty.DynamicVal,
}
}
// Ideally we'd just skip this altogether and echo back what the caller
// provided, but the request is in a different serialization format than
// the response and so only the real provider can deal with this one.
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Provider configuration is invalid",
"Cannot decode the prior state for this resource instance because its provider configuration is invalid.",
nil, // nil attribute path means the overall configuration block
))
return providers.UpgradeResourceStateResponse{
Diagnostics: diags,
}
}
// ValidateDataResourceConfig implements providers.Interface.
func (stubConfiguredProvider) ValidateDataResourceConfig(req providers.ValidateDataResourceConfigRequest) providers.ValidateDataResourceConfigResponse {
// We'll just optimistically assume the configuration is valid, so that
// we can progress to planning and return an error there instead.
return providers.ValidateDataResourceConfigResponse{
Diagnostics: nil,
}
}
// ValidateProviderConfig implements providers.Interface.
func (stubConfiguredProvider) ValidateProviderConfig(req providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse {
// It doesn't make sense to call this one on stubProvider, because
// we only use stubProvider for situations where ConfigureProvider failed
// on a real provider and we should already have called
// ValidateProviderConfig on that provider by then anyway.
return providers.ValidateProviderConfigResponse{
PreparedConfig: req.Config,
Diagnostics: nil,
}
}
// ValidateResourceConfig implements providers.Interface.
func (stubConfiguredProvider) ValidateResourceConfig(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse {
// We'll just optimistically assume the configuration is valid, so that
// we can progress to reading and return an error there instead.
return providers.ValidateResourceConfigResponse{
Diagnostics: nil,
}
}

@ -0,0 +1,61 @@
package stackeval
import (
"context"
"fmt"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/providers"
)
type ProviderType struct {
addr addrs.Provider
main *Main
schema promising.Once[providers.GetProviderSchemaResponse]
unconfiguredClient rcProviderClient
}
func newProviderType(main *Main, addr addrs.Provider) *ProviderType {
return &ProviderType{
addr: addr,
main: main,
unconfiguredClient: rcProviderClient{
Factory: func() (providers.Interface, error) {
return main.ProviderFactories().NewUnconfiguredClient(addr)
},
},
}
}
func (pt *ProviderType) Addr() addrs.Provider {
return pt.addr
}
// UnconfiguredClient returns the client for the singleton unconfigured
// provider of this type, initializing the provider first if necessary.
//
// Callers must call Close on the returned client once they are finished
// with it, which will internally decrement a reference count so that
// the shared provider can be eventually closed once no longer needed.
func (pt *ProviderType) UnconfiguredClient(ctx context.Context) (providers.Interface, error) {
return pt.unconfiguredClient.Borrow(ctx)
}
func (pt *ProviderType) Schema(ctx context.Context) (providers.GetProviderSchemaResponse, error) {
return pt.schema.Do(ctx, func(ctx context.Context) (providers.GetProviderSchemaResponse, error) {
client, err := pt.UnconfiguredClient(ctx)
if err != nil {
return providers.GetProviderSchemaResponse{}, fmt.Errorf("provider startup failed: %w", err)
}
defer client.Close()
ret := client.GetProviderSchema()
if ret.Diagnostics.HasErrors() {
return providers.GetProviderSchemaResponse{}, fmt.Errorf("provider failed to return its schema")
}
return ret, nil
})
}

@ -6,6 +6,7 @@ import (
"sync"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
@ -264,6 +265,74 @@ func (s *Stack) Component(ctx context.Context, addr stackaddrs.Component) *Compo
return s.Components(ctx)[addr]
}
func (s *Stack) ProviderByLocalAddr(ctx context.Context, localAddr stackaddrs.ProviderConfigRef) *Provider {
decls := s.ConfigDeclarations(ctx)
sourceAddr, ok := decls.RequiredProviders.ProviderForLocalName(localAddr.ProviderLocalName)
if !ok {
return nil
}
configAddr := stackaddrs.AbsProviderConfig{
Stack: s.Addr(),
Item: stackaddrs.ProviderConfig{
Provider: sourceAddr,
Name: localAddr.Name,
},
}
// FIXME: stackconfig borrows a type from "addrs" rather than the one
// in "stackaddrs", for no good reason other than implementation order.
// We should eventually heal this and use stackaddrs.ProviderConfigRef
// in the stackconfig API too.
k := addrs.LocalProviderConfig{
LocalName: localAddr.ProviderLocalName,
Alias: localAddr.Name,
}
decl, ok := decls.ProviderConfigs[k]
if !ok {
return nil
}
return newProvider(s.main, configAddr, decl)
}
func (s *Stack) Provider(ctx context.Context, addr stackaddrs.ProviderConfig) *Provider {
decls := s.ConfigDeclarations(ctx)
localName, ok := decls.RequiredProviders.LocalNameForProvider(addr.Provider)
if !ok {
return nil
}
return s.ProviderByLocalAddr(ctx, stackaddrs.ProviderConfigRef{
ProviderLocalName: localName,
Name: addr.Name,
})
}
func (s *Stack) Providers(ctx context.Context) map[stackaddrs.ProviderConfigRef]*Provider {
decls := s.ConfigDeclarations(ctx)
if len(decls.ProviderConfigs) == 0 {
return nil
}
ret := make(map[stackaddrs.ProviderConfigRef]*Provider, len(decls.ProviderConfigs))
// package stackconfig is using the addrs package for provider configuration
// addresses instead of stackaddrs, because it was written before we had
// stackaddrs, so we need to do some address adaptation for now.
// FIXME: Rationalize this so that stackconfig uses the stackaddrs types.
for weirdAddr := range decls.ProviderConfigs {
addr := stackaddrs.ProviderConfigRef{
ProviderLocalName: weirdAddr.LocalName,
Name: weirdAddr.Alias,
}
ret[addr] = s.ProviderByLocalAddr(ctx, addr)
// FIXME: The above doesn't deal with the case where the provider
// block refers to an undeclared provider local name. What should
// we do in that case? Maybe it doesn't matter if package stackconfig
// validates that during configuration loading anyway.
}
return ret
}
// OutputValues returns a map of all of the output values declared within
// this stack's configuration.
func (s *Stack) OutputValues(ctx context.Context) map[stackaddrs.OutputValue]*OutputValue {
@ -349,6 +418,17 @@ func (s *Stack) resolveExpressionReference(ctx context.Context, ref stackaddrs.R
})
}
return ret, diags
case stackaddrs.ProviderConfigRef:
ret := s.ProviderByLocalAddr(ctx, addr)
if ret == nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to undeclared provider configuration",
Detail: fmt.Sprintf("There is no provider %q %q block declared this stack.", addr.ProviderLocalName, addr.Name),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
}
return ret, diags
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,

@ -22,6 +22,7 @@ type StackCall struct {
main *Main
forEachValue perEvalPhase[promising.Once[withDiagnostics[cty.Value]]]
instances perEvalPhase[promising.Once[withDiagnostics[map[addrs.InstanceKey]*StackCallInstance]]]
}
var _ Plannable = (*StackCall)(nil)
@ -147,72 +148,83 @@ func (c *StackCall) CheckForEachValue(ctx context.Context, phase EvalPhase) (cty
// will visit the stack call directly and ask it to check itself, and that
// call will be the one responsible for returning any diagnostics.
func (c *StackCall) Instances(ctx context.Context, phase EvalPhase) map[addrs.InstanceKey]*StackCallInstance {
forEachVal := c.ForEachValue(ctx, phase)
switch {
case forEachVal == cty.NilVal:
// No for_each expression at all, then. We have exactly one instance
// without an instance key and with no repetition data.
return map[addrs.InstanceKey]*StackCallInstance{
addrs.NoKey: newStackCallInstance(c, addrs.NoKey, instances.RepetitionData{
// no repetition symbols available in this case
}),
}
ret, _ := c.CheckInstances(ctx, phase)
return ret
}
case !forEachVal.IsKnown():
// The for_each expression is too invalid for us to be able to
// know which instances exist. A totally nil map (as opposed to a
// non-nil map of length zero) signals that situation.
return nil
func (c *StackCall) CheckInstances(ctx context.Context, phase EvalPhase) (map[addrs.InstanceKey]*StackCallInstance, tfdiags.Diagnostics) {
return doOnceWithDiags(
ctx, c.instances.For(phase), c.main,
func(ctx context.Context) (map[addrs.InstanceKey]*StackCallInstance, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
forEachVal := c.ForEachValue(ctx, phase)
default:
// Otherwise we should be able to assume the value is valid per the
// definition of [CheckForEachValue]. The following will panic if
// that other function doesn't satisfy its documented contract;
// if that happens, prefer to correct [CheckForEachValue] than to
// add additional complexity here.
// NOTE: We MUST return a non-nil map from every return path under
// this case, even if there are zero elements in it, because a nil map
// represents an _invalid_ for_each expression (handled above).
ty := forEachVal.Type()
switch {
case ty.IsObjectType() || ty.IsMapType():
elems := forEachVal.AsValueMap()
ret := make(map[addrs.InstanceKey]*StackCallInstance, len(elems))
for k, v := range elems {
ik := addrs.StringKey(k)
ret[ik] = newStackCallInstance(c, ik, instances.RepetitionData{
EachKey: cty.StringVal(k),
EachValue: v,
})
}
return ret
case ty.IsSetType():
// ForEachValue should have already guaranteed us a set of strings,
// but we'll check again here just so we can panic more intellgibly
// if that function is buggy.
if ty.ElementType() != cty.String {
panic(fmt.Sprintf("ForEachValue returned invalid result %#v", forEachVal))
}
switch {
case forEachVal == cty.NilVal:
// No for_each expression at all, then. We have exactly one instance
// without an instance key and with no repetition data.
return map[addrs.InstanceKey]*StackCallInstance{
addrs.NoKey: newStackCallInstance(c, addrs.NoKey, instances.RepetitionData{
// no repetition symbols available in this case
}),
}, diags
case !forEachVal.IsKnown():
// The for_each expression is too invalid for us to be able to
// know which instances exist. A totally nil map (as opposed to a
// non-nil map of length zero) signals that situation.
return nil, diags
elems := forEachVal.AsValueSlice()
ret := make(map[addrs.InstanceKey]*StackCallInstance, len(elems))
for _, sv := range elems {
k := addrs.StringKey(sv.AsString())
ret[k] = newStackCallInstance(c, k, instances.RepetitionData{
EachKey: sv,
EachValue: sv,
})
default:
// Otherwise we should be able to assume the value is valid per the
// definition of [CheckForEachValue]. The following will panic if
// that other function doesn't satisfy its documented contract;
// if that happens, prefer to correct [CheckForEachValue] than to
// add additional complexity here.
// NOTE: We MUST return a non-nil map from every return path under
// this case, even if there are zero elements in it, because a nil map
// represents an _invalid_ for_each expression (handled above).
ty := forEachVal.Type()
switch {
case ty.IsObjectType() || ty.IsMapType():
elems := forEachVal.AsValueMap()
ret := make(map[addrs.InstanceKey]*StackCallInstance, len(elems))
for k, v := range elems {
ik := addrs.StringKey(k)
ret[ik] = newStackCallInstance(c, ik, instances.RepetitionData{
EachKey: cty.StringVal(k),
EachValue: v,
})
}
return ret, diags
case ty.IsSetType():
// ForEachValue should have already guaranteed us a set of strings,
// but we'll check again here just so we can panic more intellgibly
// if that function is buggy.
if ty.ElementType() != cty.String {
panic(fmt.Sprintf("ForEachValue returned invalid result %#v", forEachVal))
}
elems := forEachVal.AsValueSlice()
ret := make(map[addrs.InstanceKey]*StackCallInstance, len(elems))
for _, sv := range elems {
k := addrs.StringKey(sv.AsString())
ret[k] = newStackCallInstance(c, k, instances.RepetitionData{
EachKey: sv,
EachValue: sv,
})
}
return ret, diags
default:
panic(fmt.Sprintf("ForEachValue returned invalid result %#v", forEachVal))
}
}
return ret
default:
panic(fmt.Sprintf("ForEachValue returned invalid result %#v", forEachVal))
}
}
},
)
}
func (c *StackCall) ResultValue(ctx context.Context, phase EvalPhase) cty.Value {
@ -281,6 +293,8 @@ func (c *StackCall) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange,
_, moreDiags := c.CheckForEachValue(ctx, PlanPhase)
diags = diags.Append(moreDiags)
_, moreDiags = c.CheckInstances(ctx, PlanPhase)
diags = diags.Append(moreDiags)
// All of the other arguments in a stack call get evaluated separately
// for each instance of the call, so [StackCallInstance.PlanChanges]

@ -6,6 +6,7 @@ import (
"sync"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
@ -35,6 +36,7 @@ type StackConfig struct {
outputValues map[stackaddrs.OutputValue]*OutputValueConfig
stackCalls map[stackaddrs.StackCall]*StackCallConfig
components map[stackaddrs.Component]*ComponentConfig
providers map[stackaddrs.ProviderConfig]*ProviderConfig
}
var _ ExpressionScope = (*StackConfig)(nil)
@ -51,6 +53,7 @@ func newStackConfig(main *Main, addr stackaddrs.Stack, config *stackconfig.Confi
outputValues: make(map[stackaddrs.OutputValue]*OutputValueConfig, len(config.Stack.Declarations.OutputValues)),
stackCalls: make(map[stackaddrs.StackCall]*StackCallConfig, len(config.Stack.Declarations.EmbeddedStacks)),
components: make(map[stackaddrs.Component]*ComponentConfig, len(config.Stack.Declarations.Components)),
providers: make(map[stackaddrs.ProviderConfig]*ProviderConfig, len(config.Stack.Declarations.ProviderConfigs)),
}
}
@ -187,6 +190,36 @@ func (s *StackConfig) OutputValues(ctx context.Context) map[stackaddrs.OutputVal
return ret
}
// Provider returns a [ProviderConfig] representing the provider configuration
// block within the stack configuration that matches the given address,
// or nil if there is no such declaration.
func (s *StackConfig) Provider(ctx context.Context, addr stackaddrs.ProviderConfig) *ProviderConfig {
s.mu.Lock()
defer s.mu.Unlock()
ret, ok := s.providers[addr]
if !ok {
localName, ok := s.config.Stack.RequiredProviders.LocalNameForProvider(addr.Provider)
if !ok {
return nil
}
// FIXME: stackconfig package currently uses addrs.LocalProviderConfig
// instead of stackaddrs.ProviderConfigRef.
configAddr := addrs.LocalProviderConfig{
LocalName: localName,
Alias: addr.Name,
}
cfg, ok := s.config.Stack.ProviderConfigs[configAddr]
if !ok {
return nil
}
cfgAddr := stackaddrs.Config(s.Addr(), addr)
ret = newProviderConfig(s.main, cfgAddr, cfg)
s.providers[addr] = ret
}
return ret
}
func (s *StackConfig) ResultType(ctx context.Context) cty.Type {
os := s.OutputValues(ctx)
atys := make(map[string]cty.Type, len(os))

@ -7,6 +7,7 @@ import (
)
type ValidateOpts struct {
ProviderFactories ProviderFactories
}
// Validateable is implemented by objects that can participate in validation.

@ -4,6 +4,9 @@ import (
"context"
"sync"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
"github.com/hashicorp/terraform/internal/stacks/stackplan"
"github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval"
@ -27,7 +30,10 @@ func Plan(ctx context.Context, req *PlanRequest, resp *PlanResponse) {
var respMu sync.Mutex // must hold this when accessing fields of resp, aside from channel sends
main := stackeval.NewForPlanning(req.Config, stackeval.PlanOpts{})
main := stackeval.NewForPlanning(req.Config, stackeval.PlanOpts{
InputVariableValues: req.InputValues,
ProviderFactories: req.ProviderFactories,
})
main.PlanAll(ctx, stackeval.PlanOutput{
AnnouncePlannedChange: func(ctx context.Context, change stackplan.PlannedChange) {
resp.PlannedChanges <- change
@ -44,6 +50,14 @@ func Plan(ctx context.Context, req *PlanRequest, resp *PlanResponse) {
}
},
})
cleanupDiags := main.DoCleanup(ctx)
for _, diag := range cleanupDiags {
// cleanup diagnostics don't stop a plan from being applyable, because
// the cleanup process should not affect the content of and validity
// of the plan. This should only include transient operational errors
// such as failing to terminate a provider plugin.
resp.Diagnostics <- diag
}
// Before we return we'll emit one more special planned change just to
// remember in the raw plan sequence whether we considered this plan to be
@ -62,7 +76,8 @@ type PlanRequest struct {
Config *stackconfig.Config
// TODO: Prior state
// TODO: Provider factories and other similar such things
InputValues map[stackaddrs.InputVariable]ExternalInputValue
ProviderFactories map[addrs.Provider]providers.Factory
}
// PlanResponse is used by [Plan] to describe the results of planning.
@ -121,3 +136,5 @@ type PlanResponse struct {
// signal that planning is complete.
Diagnostics chan<- tfdiags.Diagnostic
}
type ExternalInputValue = stackeval.ExternalInputValue

@ -8,9 +8,12 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackplan"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/hashicorp/terraform/version"
"github.com/zclconf/go-cty/cty"
@ -168,6 +171,86 @@ func TestPlanVariableOutputRoundtripNested(t *testing.T) {
}
}
func TestPlanWithProviderConfig(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "with-provider-config")
providerAddr := addrs.MustParseProviderSourceString("example.com/test/test")
providerSchema := &providers.GetProviderSchemaResponse{
Provider: providers.Schema{
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"name": {
Type: cty.String,
Required: true,
},
},
},
},
}
inputVarAddr := stackaddrs.InputVariable{Name: "name"}
fakeSrcRng := tfdiags.SourceRange{
Filename: "fake-source",
}
t.Run("valid", func(t *testing.T) {
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
// FIXME: The MockProvider type is still lurking in
// the terraform package; it would make more sense for
// it to be providers.Mock, in the providers package.
provider := &terraform.MockProvider{
GetProviderSchemaResponse: providerSchema,
ValidateProviderConfigResponse: &providers.ValidateProviderConfigResponse{},
ConfigureProviderResponse: &providers.ConfigureProviderResponse{},
}
req := PlanRequest{
Config: cfg,
InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
inputVarAddr: {
Value: cty.StringVal("Jackson"),
DefRange: fakeSrcRng,
},
},
ProviderFactories: map[addrs.Provider]providers.Factory{
providerAddr: func() (providers.Interface, error) {
return provider, nil
},
},
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
_, diags := collectPlanOutput(changesCh, diagsCh)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
}
if !provider.ValidateProviderConfigCalled {
t.Error("ValidateProviderConfig wasn't called")
} else {
req := provider.ValidateProviderConfigRequest
if got, want := req.Config.GetAttr("name"), cty.StringVal("Jackson"); !got.RawEquals(want) {
t.Errorf("wrong name in ValidateProviderConfig\ngot: %#v\nwant: %#v", got, want)
}
}
if !provider.ConfigureProviderCalled {
t.Error("ConfigureProvider wasn't called")
} else {
req := provider.ConfigureProviderRequest
if got, want := req.Config.GetAttr("name"), cty.StringVal("Jackson"); !got.RawEquals(want) {
t.Errorf("wrong name in ConfigureProvider\ngot: %#v\nwant: %#v", got, want)
}
}
if !provider.CloseCalled {
t.Error("provider wasn't closed")
}
})
}
// collectPlanOutput consumes the two output channels emitting results from
// a call to [Plan], and collects all of the data written to them before
// returning once changesCh has been closed by the sender to indicate that

@ -0,0 +1,17 @@
required_providers {
test = {
source = "example.com/test/test"
version = "1.0.0"
}
}
variable "name" {
type = string
}
provider "test" "foo" {
config {
name = var.name
}
}

@ -17,6 +17,9 @@ func Validate(ctx context.Context, req *ValidateRequest) tfdiags.Diagnostics {
main := stackeval.NewForValidating(req.Config, stackeval.ValidateOpts{})
diags := main.ValidateAll(ctx)
diags = diags.Append(
main.DoCleanup(ctx),
)
if diags.HasErrors() {
span.SetStatus(codes.Error, "validation returned errors")
}

Loading…
Cancel
Save