diff --git a/internal/stacks/stackconfig/stackconfigtypes/provider_config.go b/internal/stacks/stackconfig/stackconfigtypes/provider_config.go index 2e9bda2c5e..99ec212ef9 100644 --- a/internal/stacks/stackconfig/stackconfigtypes/provider_config.go +++ b/internal/stacks/stackconfig/stackconfigtypes/provider_config.go @@ -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) diff --git a/internal/stacks/stackruntime/internal/stackeval/applying.go b/internal/stacks/stackruntime/internal/stackeval/applying.go index 4e3057a767..22adacb60f 100644 --- a/internal/stacks/stackruntime/internal/stackeval/applying.go +++ b/internal/stacks/stackruntime/internal/stackeval/applying.go @@ -1,3 +1,5 @@ package stackeval -type ApplyOpts struct{} +type ApplyOpts struct { + ProviderFactories ProviderFactories +} diff --git a/internal/stacks/stackruntime/internal/stackeval/component.go b/internal/stacks/stackruntime/internal/stackeval/component.go index 5bcda8f11e..a8ba10bbaa 100644 --- a/internal/stacks/stackruntime/internal/stackeval/component.go +++ b/internal/stacks/stackruntime/internal/stackeval/component.go @@ -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 } diff --git a/internal/stacks/stackruntime/internal/stackeval/component_instance.go b/internal/stacks/stackruntime/internal/stackeval/component_instance.go index 22715438d7..7d629d3041 100644 --- a/internal/stacks/stackruntime/internal/stackeval/component_instance.go +++ b/internal/stacks/stackruntime/internal/stackeval/component_instance.go @@ -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 } diff --git a/internal/stacks/stackruntime/internal/stackeval/expressions.go b/internal/stacks/stackruntime/internal/stackeval/expressions.go index fe73605788..fb37df59fe 100644 --- a/internal/stacks/stackruntime/internal/stackeval/expressions.go +++ b/internal/stacks/stackruntime/internal/stackeval/expressions.go @@ -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. diff --git a/internal/stacks/stackruntime/internal/stackeval/input_variable.go b/internal/stacks/stackruntime/internal/stackeval/input_variable.go index 04bd49bd83..69ff7d1449 100644 --- a/internal/stacks/stackruntime/internal/stackeval/input_variable.go +++ b/internal/stacks/stackruntime/internal/stackeval/input_variable.go @@ -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 +} diff --git a/internal/stacks/stackruntime/internal/stackeval/main.go b/internal/stacks/stackruntime/internal/stackeval/main.go index 2c451bfdbd..c8023af1ab 100644 --- a/internal/stacks/stackruntime/internal/stackeval/main.go +++ b/internal/stacks/stackruntime/internal/stackeval/main.go @@ -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 diff --git a/internal/stacks/stackruntime/internal/stackeval/main_plan.go b/internal/stacks/stackruntime/internal/stackeval/main_plan.go index 30f82ffd26..c147272531 100644 --- a/internal/stacks/stackruntime/internal/stackeval/main_plan.go +++ b/internal/stacks/stackruntime/internal/stackeval/main_plan.go @@ -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) diff --git a/internal/stacks/stackruntime/internal/stackeval/output_value.go b/internal/stacks/stackruntime/internal/stackeval/output_value.go index 16fc17c0b5..5ea9e8146b 100644 --- a/internal/stacks/stackruntime/internal/stackeval/output_value.go +++ b/internal/stacks/stackruntime/internal/stackeval/output_value.go @@ -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 diff --git a/internal/stacks/stackruntime/internal/stackeval/planning.go b/internal/stacks/stackruntime/internal/stackeval/planning.go index 2ac3efab41..ac6e2cd8b2 100644 --- a/internal/stacks/stackruntime/internal/stackeval/planning.go +++ b/internal/stacks/stackruntime/internal/stackeval/planning.go @@ -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. diff --git a/internal/stacks/stackruntime/internal/stackeval/provider.go b/internal/stacks/stackruntime/internal/stackeval/provider.go new file mode 100644 index 0000000000..13758306d9 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/provider.go @@ -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() +} diff --git a/internal/stacks/stackruntime/internal/stackeval/provider_config.go b/internal/stacks/stackruntime/internal/stackeval/provider_config.go new file mode 100644 index 0000000000..33ecfae1ed --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/provider_config.go @@ -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() +} diff --git a/internal/stacks/stackruntime/internal/stackeval/provider_factory.go b/internal/stacks/stackruntime/internal/stackeval/provider_factory.go new file mode 100644 index 0000000000..be2f94fcbd --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/provider_factory.go @@ -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() +} diff --git a/internal/stacks/stackruntime/internal/stackeval/provider_instance.go b/internal/stacks/stackruntime/internal/stackeval/provider_instance.go new file mode 100644 index 0000000000..b4d00639e4 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/provider_instance.go @@ -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, + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/provider_type.go b/internal/stacks/stackruntime/internal/stackeval/provider_type.go new file mode 100644 index 0000000000..8547efa2f7 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/provider_type.go @@ -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 + }) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/stack.go b/internal/stacks/stackruntime/internal/stackeval/stack.go index e838395f33..138bf3c57a 100644 --- a/internal/stacks/stackruntime/internal/stackeval/stack.go +++ b/internal/stacks/stackruntime/internal/stackeval/stack.go @@ -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, diff --git a/internal/stacks/stackruntime/internal/stackeval/stack_call.go b/internal/stacks/stackruntime/internal/stackeval/stack_call.go index dbefac588f..6bd8220ef4 100644 --- a/internal/stacks/stackruntime/internal/stackeval/stack_call.go +++ b/internal/stacks/stackruntime/internal/stackeval/stack_call.go @@ -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] diff --git a/internal/stacks/stackruntime/internal/stackeval/stack_config.go b/internal/stacks/stackruntime/internal/stackeval/stack_config.go index 33771e26b8..ada7bdb25b 100644 --- a/internal/stacks/stackruntime/internal/stackeval/stack_config.go +++ b/internal/stacks/stackruntime/internal/stackeval/stack_config.go @@ -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)) diff --git a/internal/stacks/stackruntime/internal/stackeval/validating.go b/internal/stacks/stackruntime/internal/stackeval/validating.go index 6fdc279118..cf3bd57a0b 100644 --- a/internal/stacks/stackruntime/internal/stackeval/validating.go +++ b/internal/stacks/stackruntime/internal/stackeval/validating.go @@ -7,6 +7,7 @@ import ( ) type ValidateOpts struct { + ProviderFactories ProviderFactories } // Validateable is implemented by objects that can participate in validation. diff --git a/internal/stacks/stackruntime/plan.go b/internal/stacks/stackruntime/plan.go index f52b3f3550..b630ed5d88 100644 --- a/internal/stacks/stackruntime/plan.go +++ b/internal/stacks/stackruntime/plan.go @@ -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 diff --git a/internal/stacks/stackruntime/plan_test.go b/internal/stacks/stackruntime/plan_test.go index 39ded6d445..69d71a71d2 100644 --- a/internal/stacks/stackruntime/plan_test.go +++ b/internal/stacks/stackruntime/plan_test.go @@ -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 diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-provider-config/with-provider-config.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-provider-config/with-provider-config.tfstack.hcl new file mode 100644 index 0000000000..a1858e31d4 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-provider-config/with-provider-config.tfstack.hcl @@ -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 + } +} diff --git a/internal/stacks/stackruntime/validate.go b/internal/stacks/stackruntime/validate.go index ea3696afab..d9f96080ee 100644 --- a/internal/stacks/stackruntime/validate.go +++ b/internal/stacks/stackruntime/validate.go @@ -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") }