| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| package terraform |
| |
| import ( |
| "context" |
| "fmt" |
| "log" |
| "sync" |
| |
| "github.com/hashicorp/hcl/v2" |
| "github.com/zclconf/go-cty/cty" |
| "github.com/zclconf/go-cty/cty/function" |
| |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/checks" |
| "github.com/hashicorp/terraform/internal/configs" |
| "github.com/hashicorp/terraform/internal/configs/configschema" |
| "github.com/hashicorp/terraform/internal/experiments" |
| "github.com/hashicorp/terraform/internal/instances" |
| "github.com/hashicorp/terraform/internal/lang" |
| "github.com/hashicorp/terraform/internal/moduletest/mocking" |
| "github.com/hashicorp/terraform/internal/namedvals" |
| "github.com/hashicorp/terraform/internal/plans" |
| "github.com/hashicorp/terraform/internal/plans/deferring" |
| "github.com/hashicorp/terraform/internal/providers" |
| "github.com/hashicorp/terraform/internal/provisioners" |
| "github.com/hashicorp/terraform/internal/refactoring" |
| "github.com/hashicorp/terraform/internal/resources/ephemeral" |
| "github.com/hashicorp/terraform/internal/states" |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| "github.com/hashicorp/terraform/version" |
| ) |
| |
| // BuiltinEvalContext is an EvalContext implementation that is used by |
| // Terraform by default. |
| type BuiltinEvalContext struct { |
| // scope is the scope (module instance or set of possible module instances) |
| // that this context is operating within. |
| // |
| // Note: this can be evalContextGlobal (i.e. nil) when visiting a graph |
| // node that doesn't belong to a particular module, in which case any |
| // method using it will panic. |
| scope evalContextScope |
| |
| // StopContext is the context used to track whether we're complete |
| StopContext context.Context |
| |
| // Evaluator is used for evaluating expressions within the scope of this |
| // eval context. |
| Evaluator *Evaluator |
| |
| // NamedValuesValue is where we keep the values of already-evaluated input |
| // variables, local values, and output values. |
| NamedValuesValue *namedvals.State |
| |
| // Plugins is a library of plugin components (providers and provisioners) |
| // available for use during a graph walk. |
| Plugins *contextPlugins |
| |
| // ExternalProviderConfigs are pre-configured provider instances passed |
| // in by the caller, for situations like Stack components where the |
| // root module isn't designed to be planned and applied in isolation and |
| // instead expects to recieve certain provider configurations from the |
| // stack configuration. |
| ExternalProviderConfigs map[addrs.RootProviderConfig]providers.Interface |
| |
| // DeferralsValue is the object returned by [BuiltinEvalContext.Deferrals]. |
| DeferralsValue *deferring.Deferred |
| |
| // forget if set to true will cause the plan to forget all resources. This is |
| // only allowd in the context of a destroy plan. |
| forget bool |
| |
| Hooks []Hook |
| InputValue UIInput |
| ProviderCache map[string]providers.Interface |
| ProviderFuncCache map[string]providers.Interface |
| ProviderFuncResults *providers.FunctionResults |
| ProviderInputConfig map[string]map[string]cty.Value |
| ProviderLock *sync.Mutex |
| ProvisionerCache map[string]provisioners.Interface |
| ProvisionerLock *sync.Mutex |
| ChangesValue *plans.ChangesSync |
| StateValue *states.SyncState |
| ChecksValue *checks.State |
| EphemeralResourcesValue *ephemeral.Resources |
| RefreshStateValue *states.SyncState |
| PrevRunStateValue *states.SyncState |
| InstanceExpanderValue *instances.Expander |
| MoveResultsValue refactoring.MoveResults |
| OverrideValues *mocking.Overrides |
| } |
| |
| // BuiltinEvalContext implements EvalContext |
| var _ EvalContext = (*BuiltinEvalContext)(nil) |
| |
| func (ctx *BuiltinEvalContext) withScope(scope evalContextScope) EvalContext { |
| newCtx := *ctx |
| newCtx.scope = scope |
| return &newCtx |
| } |
| |
| func (ctx *BuiltinEvalContext) StopCtx() context.Context { |
| // This can happen during tests. During tests, we just block forever. |
| if ctx.StopContext == nil { |
| return context.TODO() |
| } |
| |
| return ctx.StopContext |
| } |
| |
| func (ctx *BuiltinEvalContext) Hook(fn func(Hook) (HookAction, error)) error { |
| for _, h := range ctx.Hooks { |
| action, err := fn(h) |
| if err != nil { |
| return err |
| } |
| |
| switch action { |
| case HookActionContinue: |
| continue |
| case HookActionHalt: |
| // Return an early exit error to trigger an early exit |
| log.Printf("[WARN] Early exit triggered by hook: %T", h) |
| return nil |
| } |
| } |
| |
| return nil |
| } |
| |
| func (ctx *BuiltinEvalContext) Input() UIInput { |
| return ctx.InputValue |
| } |
| |
| func (ctx *BuiltinEvalContext) InitProvider(addr addrs.AbsProviderConfig, config *configs.Provider) (providers.Interface, error) { |
| // If we already initialized, it is an error |
| if p := ctx.Provider(addr); p != nil { |
| return nil, fmt.Errorf("%s is already initialized", addr) |
| } |
| |
| // Warning: make sure to acquire these locks AFTER the call to Provider |
| // above, since it also acquires locks. |
| ctx.ProviderLock.Lock() |
| defer ctx.ProviderLock.Unlock() |
| |
| key := addr.String() |
| |
| if addr.Module.IsRoot() { |
| rootAddr := addrs.RootProviderConfig{ |
| Provider: addr.Provider, |
| Alias: addr.Alias, |
| } |
| if external, isExternal := ctx.ExternalProviderConfigs[rootAddr]; isExternal { |
| // External providers should always be pre-configured by the |
| // external caller, and so we'll wrap them in a type that |
| // makes operations like ConfigureProvider and Close be no-op. |
| wrapped := externalProviderWrapper{external} |
| ctx.ProviderCache[key] = wrapped |
| return wrapped, nil |
| } |
| } |
| |
| p, err := ctx.Plugins.NewProviderInstance(addr.Provider) |
| if err != nil { |
| return nil, err |
| } |
| |
| log.Printf("[TRACE] BuiltinEvalContext: Initialized %q provider for %s", addr.String(), addr) |
| |
| // The config might be nil, if there was no config block defined for this |
| // provider. |
| if config != nil && config.Mock { |
| log.Printf("[TRACE] BuiltinEvalContext: Mocked %q provider for %s", addr.String(), addr) |
| p = &providers.Mock{ |
| Provider: p, |
| Data: config.MockData, |
| } |
| } |
| |
| ctx.ProviderCache[key] = p |
| |
| return p, nil |
| } |
| |
| func (ctx *BuiltinEvalContext) Provider(addr addrs.AbsProviderConfig) providers.Interface { |
| ctx.ProviderLock.Lock() |
| defer ctx.ProviderLock.Unlock() |
| |
| return ctx.ProviderCache[addr.String()] |
| } |
| |
| func (ctx *BuiltinEvalContext) ProviderSchema(addr addrs.AbsProviderConfig) (providers.ProviderSchema, error) { |
| return ctx.Plugins.ProviderSchema(addr.Provider) |
| } |
| |
| func (ctx *BuiltinEvalContext) CloseProvider(addr addrs.AbsProviderConfig) error { |
| ctx.ProviderLock.Lock() |
| defer ctx.ProviderLock.Unlock() |
| |
| key := addr.String() |
| provider := ctx.ProviderCache[key] |
| if provider != nil { |
| delete(ctx.ProviderCache, key) |
| return provider.Close() |
| } |
| |
| return nil |
| } |
| |
| func (ctx *BuiltinEvalContext) ConfigureProvider(addr addrs.AbsProviderConfig, cfg cty.Value) tfdiags.Diagnostics { |
| var diags tfdiags.Diagnostics |
| if !addr.Module.Equal(ctx.Path().Module()) { |
| // This indicates incorrect use of ConfigureProvider: it should be used |
| // only from the module that the provider configuration belongs to. |
| panic(fmt.Sprintf("%s configured by wrong module %s", addr, ctx.Path())) |
| } |
| |
| p := ctx.Provider(addr) |
| if p == nil { |
| diags = diags.Append(fmt.Errorf("%s not initialized", addr)) |
| return diags |
| } |
| |
| req := providers.ConfigureProviderRequest{ |
| TerraformVersion: version.String(), |
| Config: cfg, |
| ClientCapabilities: providers.ClientCapabilities{ |
| DeferralAllowed: ctx.Deferrals().DeferralAllowed(), |
| WriteOnlyAttributesAllowed: true, |
| }, |
| } |
| |
| resp := p.ConfigureProvider(req) |
| return resp.Diagnostics |
| } |
| |
| func (ctx *BuiltinEvalContext) ProviderInput(pc addrs.AbsProviderConfig) map[string]cty.Value { |
| ctx.ProviderLock.Lock() |
| defer ctx.ProviderLock.Unlock() |
| |
| if !pc.Module.Equal(ctx.Path().Module()) { |
| // This indicates incorrect use of InitProvider: it should be used |
| // only from the module that the provider configuration belongs to. |
| panic(fmt.Sprintf("%s initialized by wrong module %s", pc, ctx.Path())) |
| } |
| |
| if !ctx.Path().IsRoot() { |
| // Only root module provider configurations can have input. |
| return nil |
| } |
| |
| return ctx.ProviderInputConfig[pc.String()] |
| } |
| |
| func (ctx *BuiltinEvalContext) SetProviderInput(pc addrs.AbsProviderConfig, c map[string]cty.Value) { |
| absProvider := pc |
| if !pc.Module.IsRoot() { |
| // Only root module provider configurations can have input. |
| log.Printf("[WARN] BuiltinEvalContext: attempt to SetProviderInput for non-root module") |
| return |
| } |
| |
| // Save the configuration |
| ctx.ProviderLock.Lock() |
| ctx.ProviderInputConfig[absProvider.String()] = c |
| ctx.ProviderLock.Unlock() |
| } |
| |
| func (ctx *BuiltinEvalContext) Provisioner(n string) (provisioners.Interface, error) { |
| ctx.ProvisionerLock.Lock() |
| defer ctx.ProvisionerLock.Unlock() |
| |
| p, ok := ctx.ProvisionerCache[n] |
| if !ok { |
| var err error |
| p, err = ctx.Plugins.NewProvisionerInstance(n) |
| if err != nil { |
| return nil, err |
| } |
| |
| ctx.ProvisionerCache[n] = p |
| } |
| |
| return p, nil |
| } |
| |
| func (ctx *BuiltinEvalContext) ProvisionerSchema(n string) (*configschema.Block, error) { |
| return ctx.Plugins.ProvisionerSchema(n) |
| } |
| |
| func (ctx *BuiltinEvalContext) ClosePlugins() error { |
| var diags tfdiags.Diagnostics |
| ctx.ProvisionerLock.Lock() |
| defer ctx.ProvisionerLock.Unlock() |
| |
| for name, prov := range ctx.ProvisionerCache { |
| err := prov.Close() |
| if err != nil { |
| diags = diags.Append(fmt.Errorf("provisioner.Close %s: %s", name, err)) |
| } |
| delete(ctx.ProvisionerCache, name) |
| } |
| |
| ctx.ProviderLock.Lock() |
| defer ctx.ProviderLock.Unlock() |
| for name, prov := range ctx.ProviderFuncCache { |
| err := prov.Close() |
| if err != nil { |
| diags = diags.Append(fmt.Errorf("provider.Close %s: %s", name, err)) |
| } |
| delete(ctx.ProviderFuncCache, name) |
| } |
| |
| return diags.Err() |
| } |
| |
| func (ctx *BuiltinEvalContext) EvaluateBlock(body hcl.Body, schema *configschema.Block, self addrs.Referenceable, keyData InstanceKeyEvalData) (cty.Value, hcl.Body, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| scope := ctx.EvaluationScope(self, nil, keyData) |
| body, evalDiags := scope.ExpandBlock(body, schema) |
| diags = diags.Append(evalDiags) |
| val, evalDiags := scope.EvalBlock(body, schema) |
| diags = diags.Append(evalDiags) |
| return val, body, diags |
| } |
| |
| func (ctx *BuiltinEvalContext) EvaluateExpr(expr hcl.Expression, wantType cty.Type, self addrs.Referenceable) (cty.Value, tfdiags.Diagnostics) { |
| scope := ctx.EvaluationScope(self, nil, EvalDataForNoInstanceKey) |
| return scope.EvalExpr(expr, wantType) |
| } |
| |
| func (ctx *BuiltinEvalContext) EvaluateReplaceTriggeredBy(expr hcl.Expression, repData instances.RepetitionData) (*addrs.Reference, bool, tfdiags.Diagnostics) { |
| |
| // get the reference to lookup changes in the plan |
| ref, diags := evalReplaceTriggeredByExpr(expr, repData) |
| if diags.HasErrors() { |
| return nil, false, diags |
| } |
| |
| var changes []*plans.ResourceInstanceChange |
| // store the address once we get it for validation |
| var resourceAddr addrs.Resource |
| |
| // The reference is either a resource or resource instance |
| switch sub := ref.Subject.(type) { |
| case addrs.Resource: |
| resourceAddr = sub |
| rc := sub.Absolute(ctx.Path()) |
| changes = ctx.Changes().GetChangesForAbsResource(rc) |
| case addrs.ResourceInstance: |
| resourceAddr = sub.ContainingResource() |
| rc := sub.Absolute(ctx.Path()) |
| change := ctx.Changes().GetResourceInstanceChange(rc, addrs.NotDeposed) |
| if change != nil { |
| // we'll generate an error below if there was no change |
| changes = append(changes, change) |
| } |
| } |
| |
| // Do some validation to make sure we are expecting a change at all |
| cfg := ctx.Evaluator.Config.Descendant(ctx.Path().Module()) |
| resCfg := cfg.Module.ResourceByAddr(resourceAddr) |
| if resCfg == nil { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: `Reference to undeclared resource`, |
| Detail: fmt.Sprintf(`A resource %s has not been declared in %s`, ref.Subject, moduleDisplayAddr(ctx.Path())), |
| Subject: expr.Range().Ptr(), |
| }) |
| return nil, false, diags |
| } |
| |
| if len(changes) == 0 { |
| // If the resource is valid there should always be at least one change. |
| diags = diags.Append(fmt.Errorf("no change found for %s in %s", ref.Subject, moduleDisplayAddr(ctx.Path()))) |
| return nil, false, diags |
| } |
| |
| // If we don't have a traversal beyond the resource, then we can just look |
| // for any change. |
| if len(ref.Remaining) == 0 { |
| for _, c := range changes { |
| switch c.Change.Action { |
| // Only immediate changes to the resource will trigger replacement. |
| case plans.Update, plans.DeleteThenCreate, plans.CreateThenDelete: |
| return ref, true, diags |
| } |
| } |
| |
| // no change triggered |
| return nil, false, diags |
| } |
| |
| // This must be an instances to have a remaining traversal, which means a |
| // single change. |
| change := changes[0] |
| |
| // Make sure the change is actionable. A create or delete action will have |
| // a change in value, but are not valid for our purposes here. |
| switch change.Change.Action { |
| case plans.Update, plans.DeleteThenCreate, plans.CreateThenDelete: |
| // OK |
| default: |
| return nil, false, diags |
| } |
| |
| path, _ := traversalToPath(ref.Remaining) |
| attrBefore, _ := path.Apply(change.Before) |
| attrAfter, _ := path.Apply(change.After) |
| |
| if attrBefore == cty.NilVal || attrAfter == cty.NilVal { |
| replace := attrBefore != attrAfter |
| return ref, replace, diags |
| } |
| |
| replace := !attrBefore.RawEquals(attrAfter) |
| |
| return ref, replace, diags |
| } |
| |
| func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, source addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope { |
| switch scope := ctx.scope.(type) { |
| case evalContextModuleInstance: |
| data := &evaluationStateData{ |
| evaluationData: &evaluationData{ |
| Evaluator: ctx.Evaluator, |
| Module: scope.Addr.Module(), |
| }, |
| ModulePath: scope.Addr, |
| InstanceKeyData: keyData, |
| Operation: ctx.Evaluator.Operation, |
| } |
| evalScope := ctx.Evaluator.Scope(data, self, source, ctx.evaluationExternalFunctions()) |
| |
| // ctx.PathValue is the path of the module that contains whatever |
| // expression the caller will be trying to evaluate, so this will |
| // activate only the experiments from that particular module, to |
| // be consistent with how experiment checking in the "configs" |
| // package itself works. The nil check here is for robustness in |
| // incompletely-mocked testing situations; mc should never be nil in |
| // real situations. |
| if mc := ctx.Evaluator.Config.DescendantForInstance(scope.Addr); mc != nil { |
| evalScope.SetActiveExperiments(mc.Module.ActiveExperiments) |
| } |
| return evalScope |
| case evalContextPartialExpandedModule: |
| data := &evaluationPlaceholderData{ |
| evaluationData: &evaluationData{ |
| Evaluator: ctx.Evaluator, |
| Module: scope.Addr.Module(), |
| }, |
| ModulePath: scope.Addr, |
| CountAvailable: keyData.CountIndex != cty.NilVal, |
| EachAvailable: keyData.EachKey != cty.NilVal, |
| Operation: ctx.Evaluator.Operation, |
| } |
| evalScope := ctx.Evaluator.Scope(data, self, source, ctx.evaluationExternalFunctions()) |
| if mc := ctx.Evaluator.Config.Descendant(scope.Addr.Module()); mc != nil { |
| evalScope.SetActiveExperiments(mc.Module.ActiveExperiments) |
| } |
| return evalScope |
| default: |
| // This method is valid only for module-scoped EvalContext objects. |
| panic("no evaluation scope available: not in module context") |
| } |
| |
| } |
| |
| // evaluationExternalFunctions is a helper for method EvaluationScope which |
| // determines the set of external functions that should be available for |
| // evaluation in this EvalContext, based on declarations in the configuration. |
| func (ctx *BuiltinEvalContext) evaluationExternalFunctions() lang.ExternalFuncs { |
| // The set of functions in scope includes the functions contributed by |
| // every provider that the current module has as a requirement. |
| // |
| // We expose them under the local name for each provider that was selected |
| // by the module author. |
| ret := lang.ExternalFuncs{} |
| |
| cfg := ctx.Evaluator.Config.Descendant(ctx.scope.evalContextScopeModule()) |
| if cfg == nil { |
| // It's weird to not have a configuration by this point, but we'll |
| // tolerate it for robustness and just return no functions at all. |
| return ret |
| } |
| if cfg.Module.ProviderRequirements == nil { |
| // A module with no provider requirements can't have any |
| // provider-contributed functions. |
| return ret |
| } |
| |
| reqs := cfg.Module.ProviderRequirements.RequiredProviders |
| ret.Provider = make(map[string]map[string]function.Function, len(reqs)) |
| |
| for localName, req := range reqs { |
| providerAddr := req.Type |
| funcDecls, err := ctx.Plugins.ProviderFunctionDecls(providerAddr) |
| if err != nil { |
| // If a particular provider can't return schema then we'll catch |
| // it in plenty other places where it's more reasonable for us |
| // to return an error, so here we'll just treat it as having |
| // no functions. |
| log.Printf("[WARN] Error loading schema for %s to determine its functions: %s", providerAddr, err) |
| continue |
| } |
| |
| ret.Provider[localName] = make(map[string]function.Function, len(funcDecls)) |
| funcs := ret.Provider[localName] |
| for name, decl := range funcDecls { |
| funcs[name] = decl.BuildFunction(providerAddr, name, ctx.ProviderFuncResults, func() (providers.Interface, error) { |
| return ctx.functionProvider(providerAddr) |
| }) |
| } |
| } |
| |
| return ret |
| } |
| |
| // functionProvider fetches a running provider instance for evaluating |
| // functions from the cache, or starts a new instance and adds it to the cache. |
| func (ctx *BuiltinEvalContext) functionProvider(addr addrs.Provider) (providers.Interface, error) { |
| ctx.ProviderLock.Lock() |
| defer ctx.ProviderLock.Unlock() |
| |
| p, ok := ctx.ProviderFuncCache[addr.String()] |
| if ok { |
| return p, nil |
| } |
| |
| log.Printf("[TRACE] starting function provider instance for %s", addr) |
| p, err := ctx.Plugins.NewProviderInstance(addr) |
| if err == nil { |
| ctx.ProviderFuncCache[addr.String()] = p |
| } |
| |
| return p, err |
| } |
| |
| func (ctx *BuiltinEvalContext) Path() addrs.ModuleInstance { |
| if scope, ok := ctx.scope.(evalContextModuleInstance); ok { |
| return scope.Addr |
| } |
| panic("not evaluating in the scope of a fully-expanded module") |
| } |
| |
| func (ctx *BuiltinEvalContext) LanguageExperimentActive(experiment experiments.Experiment) bool { |
| if ctx.Evaluator == nil || ctx.Evaluator.Config == nil { |
| // Should not get here in normal code, but might get here in test code |
| // if the context isn't fully populated. |
| return false |
| } |
| scope := ctx.scope |
| if scope == evalContextGlobal { |
| // If we're not associated with a specific module then there can't |
| // be any language experiments in play, because experiment activation |
| // is module-scoped. |
| return false |
| } |
| cfg := ctx.Evaluator.Config.Descendant(scope.evalContextScopeModule()) |
| if cfg == nil { |
| return false |
| } |
| return cfg.Module.ActiveExperiments.Has(experiment) |
| } |
| |
| func (ctx *BuiltinEvalContext) NamedValues() *namedvals.State { |
| return ctx.NamedValuesValue |
| } |
| |
| func (ctx *BuiltinEvalContext) Deferrals() *deferring.Deferred { |
| return ctx.DeferralsValue |
| } |
| |
| func (ctx *BuiltinEvalContext) Changes() *plans.ChangesSync { |
| return ctx.ChangesValue |
| } |
| |
| func (ctx *BuiltinEvalContext) State() *states.SyncState { |
| return ctx.StateValue |
| } |
| |
| func (ctx *BuiltinEvalContext) Checks() *checks.State { |
| return ctx.ChecksValue |
| } |
| |
| func (ctx *BuiltinEvalContext) RefreshState() *states.SyncState { |
| return ctx.RefreshStateValue |
| } |
| |
| func (ctx *BuiltinEvalContext) PrevRunState() *states.SyncState { |
| return ctx.PrevRunStateValue |
| } |
| |
| func (ctx *BuiltinEvalContext) InstanceExpander() *instances.Expander { |
| return ctx.InstanceExpanderValue |
| } |
| |
| func (ctx *BuiltinEvalContext) MoveResults() refactoring.MoveResults { |
| return ctx.MoveResultsValue |
| } |
| |
| func (ctx *BuiltinEvalContext) Overrides() *mocking.Overrides { |
| return ctx.OverrideValues |
| } |
| |
| func (ctx *BuiltinEvalContext) Forget() bool { |
| return ctx.forget |
| } |
| |
| func (ctx *BuiltinEvalContext) EphemeralResources() *ephemeral.Resources { |
| return ctx.EphemeralResourcesValue |
| } |
| |
| func (ctx *BuiltinEvalContext) ClientCapabilities() providers.ClientCapabilities { |
| return providers.ClientCapabilities{ |
| DeferralAllowed: ctx.Deferrals().DeferralAllowed(), |
| WriteOnlyAttributesAllowed: true, |
| } |
| } |