blob: d62085c54de8210a96906ad7a5b6e41508a35588 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package stackeval
import (
"context"
"fmt"
"github.com/zclconf/go-cty/cty"
"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/stackplan"
"github.com/hashicorp/terraform/internal/stacks/stackstate"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// Provider represents a provider configuration in a particular stack config.
type Provider struct {
addr stackaddrs.AbsProviderConfig
config *ProviderConfig
stack *Stack
main *Main
forEachValue perEvalPhase[promising.Once[withDiagnostics[cty.Value]]]
instances perEvalPhase[promising.Once[withDiagnostics[instancesResult[*ProviderInstance]]]]
}
func newProvider(main *Main, addr stackaddrs.AbsProviderConfig, stack *Stack, config *ProviderConfig) *Provider {
return &Provider{
addr: addr,
stack: stack,
config: config,
main: main,
}
}
func (p *Provider) ProviderType() *ProviderType {
return p.main.ProviderType(p.addr.Item.Provider)
}
// 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() cty.Type {
decl := p.config.config
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.tracingName()+" for_each", p.forEachValue.For(phase),
func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
cfg := p.config.config
switch {
case cfg.ForEach != nil:
result, moreDiags := evaluateForEachExpr(ctx, cfg.ForEach, phase, p.stack, "provider")
diags = diags.Append(moreDiags)
if diags.HasErrors() {
return cty.DynamicVal, diags
}
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, bool) {
ret, unknown, _ := p.CheckInstances(ctx, phase)
return ret, unknown
}
func (p *Provider) CheckInstances(ctx context.Context, phase EvalPhase) (map[addrs.InstanceKey]*ProviderInstance, bool, tfdiags.Diagnostics) {
result, diags := doOnceWithDiags(
ctx, p.tracingName()+" instances", p.instances.For(phase),
func(ctx context.Context) (instancesResult[*ProviderInstance], tfdiags.Diagnostics) {
forEachVal, diags := p.CheckForEachValue(ctx, phase)
if diags.HasErrors() {
return instancesResult[*ProviderInstance]{}, diags
}
return instancesMap(forEachVal, func(ik addrs.InstanceKey, rd instances.RepetitionData) *ProviderInstance {
return newProviderInstance(p, stackaddrs.AbsProviderToInstance(p.addr, ik), rd)
}), diags
},
)
return result.insts, result.unknown, diags
}
// 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.config.config
insts, unknown := p.Instances(ctx, phase)
refType := p.InstRefValueType()
switch {
case decl.ForEach != nil:
if unknown {
return cty.UnknownVal(cty.Map(refType))
}
if insts == nil {
// Then we errored during instance calculation, this should have
// been caught before we got here.
return cty.NilVal
}
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.AbsProviderConfigInstance{
Stack: p.addr.Stack,
Item: stackaddrs.ProviderConfigInstance{
ProviderConfig: p.addr.Item,
Key: instKey,
},
})
}
if len(elems) == 0 {
return cty.MapValEmpty(refType)
}
return cty.MapVal(elems)
default:
if insts == nil {
return cty.UnknownVal(refType)
}
return cty.CapsuleVal(refType, &stackaddrs.AbsProviderConfigInstance{
Stack: p.addr.Stack,
Item: stackaddrs.ProviderConfigInstance{
ProviderConfig: p.addr.Item,
Key: addrs.NoKey,
},
})
}
}
func (p *Provider) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
_, moreDiags := p.CheckForEachValue(ctx, phase)
diags = diags.Append(moreDiags)
_, _, moreDiags = p.CheckInstances(ctx, phase)
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 diags
}
// PlanChanges implements Plannable.
func (p *Provider) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) {
return nil, p.checkValid(ctx, PlanPhase)
}
// References implements Referrer
func (p *Provider) References(ctx context.Context) []stackaddrs.AbsReference {
cfg := p.config.config
var ret []stackaddrs.Reference
ret = append(ret, ReferencesInExpr(cfg.ForEach)...)
if schema, err := p.ProviderType().Schema(ctx); err == nil {
ret = append(ret, ReferencesInBody(cfg.Config, schema.Provider.Body.DecoderSpec())...)
}
return makeReferencesAbsolute(ret, p.addr.Stack)
}
// CheckApply implements ApplyChecker.
func (p *Provider) CheckApply(ctx context.Context) ([]stackstate.AppliedChange, tfdiags.Diagnostics) {
return nil, p.checkValid(ctx, ApplyPhase)
}
// tracingName implements Plannable.
func (p *Provider) tracingName() string {
return p.addr.String()
}