opentofu/terraform/eval_context_builtin.go
Martin Atkins 0dd7677d1f core: fail gracefully if provider schemas aren't available
The only reason these cases are arising right now is because we have tests
that haven't yet been updated to properly support schema, but it can't
hurt to add some robustness here to reduce the risk of real crashes.
2018-10-16 18:46:46 -07:00

366 lines
9.8 KiB
Go

package terraform
import (
"context"
"fmt"
"log"
"sync"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/terraform/config/configschema"
"github.com/hashicorp/terraform/lang"
"github.com/hashicorp/terraform/tfdiags"
"github.com/hashicorp/terraform/addrs"
"github.com/zclconf/go-cty/cty"
)
// BuiltinEvalContext is an EvalContext implementation that is used by
// Terraform by default.
type BuiltinEvalContext struct {
// StopContext is the context used to track whether we're complete
StopContext context.Context
// PathValue is the Path that this context is operating within.
PathValue addrs.ModuleInstance
// Evaluator is used for evaluating expressions within the scope of this
// eval context.
Evaluator *Evaluator
// VariableValues contains the variable values across all modules. This
// structure is shared across the entire containing context, and so it
// may be accessed only when holding VariableValuesLock.
// The keys of the first level of VariableValues are the string
// representations of addrs.ModuleInstance values. The second-level keys
// are variable names within each module instance.
VariableValues map[string]map[string]cty.Value
VariableValuesLock *sync.Mutex
Components contextComponentFactory
Hooks []Hook
InputValue UIInput
ProviderCache map[string]ResourceProvider
ProviderSchemas map[string]*ProviderSchema
ProviderInputConfig map[string]map[string]cty.Value
ProviderLock *sync.Mutex
ProvisionerCache map[string]ResourceProvisioner
ProvisionerSchemas map[string]*configschema.Block
ProvisionerLock *sync.Mutex
DiffValue *Diff
DiffLock *sync.RWMutex
StateValue *State
StateLock *sync.RWMutex
once sync.Once
}
// BuiltinEvalContext implements EvalContext
var _ EvalContext = (*BuiltinEvalContext)(nil)
func (ctx *BuiltinEvalContext) Stopped() <-chan struct{} {
// This can happen during tests. During tests, we just block forever.
if ctx.StopContext == nil {
return nil
}
return ctx.StopContext.Done()
}
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 EvalEarlyExitError{}
}
}
return nil
}
func (ctx *BuiltinEvalContext) Input() UIInput {
return ctx.InputValue
}
func (ctx *BuiltinEvalContext) InitProvider(typeName string, addr addrs.ProviderConfig) (ResourceProvider, error) {
ctx.once.Do(ctx.init)
absAddr := addr.Absolute(ctx.Path())
// If we already initialized, it is an error
if p := ctx.Provider(absAddr); 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()
p, err := ctx.Components.ResourceProvider(typeName, key)
if err != nil {
return nil, err
}
ctx.ProviderCache[key] = p
// Also fetch and cache the provider's schema.
// FIXME: This is using a non-ideal provider API that requires us to
// request specific resource types, but we actually just want _all_ the
// resource types, so we'll list these first. Once the provider API is
// updated we'll get enough data to populate this whole structure in
// a single call.
resourceTypes := p.Resources()
dataSources := p.DataSources()
resourceTypeNames := make([]string, len(resourceTypes))
for i, t := range resourceTypes {
resourceTypeNames[i] = t.Name
}
dataSourceNames := make([]string, len(dataSources))
for i, t := range dataSources {
dataSourceNames[i] = t.Name
}
schema, err := p.GetSchema(&ProviderSchemaRequest{
DataSources: dataSourceNames,
ResourceTypes: resourceTypeNames,
})
if err != nil {
return nil, fmt.Errorf("error fetching schema for %s: %s", key, err)
}
if ctx.ProviderSchemas == nil {
ctx.ProviderSchemas = make(map[string]*ProviderSchema)
}
ctx.ProviderSchemas[key] = schema
return p, nil
}
func (ctx *BuiltinEvalContext) Provider(addr addrs.AbsProviderConfig) ResourceProvider {
ctx.once.Do(ctx.init)
ctx.ProviderLock.Lock()
defer ctx.ProviderLock.Unlock()
return ctx.ProviderCache[addr.String()]
}
func (ctx *BuiltinEvalContext) ProviderSchema(addr addrs.AbsProviderConfig) *ProviderSchema {
ctx.once.Do(ctx.init)
ctx.ProviderLock.Lock()
defer ctx.ProviderLock.Unlock()
return ctx.ProviderSchemas[addr.String()]
}
func (ctx *BuiltinEvalContext) CloseProvider(addr addrs.ProviderConfig) error {
ctx.once.Do(ctx.init)
ctx.ProviderLock.Lock()
defer ctx.ProviderLock.Unlock()
key := addr.String()
var provider interface{}
provider = ctx.ProviderCache[key]
if provider != nil {
if p, ok := provider.(ResourceProviderCloser); ok {
delete(ctx.ProviderCache, key)
return p.Close()
}
}
return nil
}
func (ctx *BuiltinEvalContext) ConfigureProvider(addr addrs.ProviderConfig, cfg cty.Value) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
absAddr := addr.Absolute(ctx.Path())
p := ctx.Provider(absAddr)
if p == nil {
diags = diags.Append(fmt.Errorf("%s not initialized", addr))
return diags
}
providerSchema := ctx.ProviderSchema(absAddr)
if providerSchema == nil {
diags = diags.Append(fmt.Errorf("schema for %s is not available", absAddr))
return diags
}
// FIXME: The provider API isn't yet updated to take a cty.Value directly.
rc := NewResourceConfigShimmed(cfg, providerSchema.Provider)
err := p.Configure(rc)
if err != nil {
diags = diags.Append(err)
}
return diags
}
func (ctx *BuiltinEvalContext) ProviderInput(pc addrs.ProviderConfig) map[string]cty.Value {
ctx.ProviderLock.Lock()
defer ctx.ProviderLock.Unlock()
// Go up the module tree, looking for input results for the given provider
// configuration.
path := ctx.Path()
for i := len(path); i >= 0; i-- {
k := pc.Absolute(path[:i]).String()
if v, ok := ctx.ProviderInputConfig[k]; ok {
return v
}
}
return nil
}
func (ctx *BuiltinEvalContext) SetProviderInput(pc addrs.ProviderConfig, c map[string]cty.Value) {
absProvider := pc.Absolute(ctx.Path())
// Save the configuration
ctx.ProviderLock.Lock()
ctx.ProviderInputConfig[absProvider.String()] = c
ctx.ProviderLock.Unlock()
}
func (ctx *BuiltinEvalContext) InitProvisioner(n string) (ResourceProvisioner, error) {
ctx.once.Do(ctx.init)
// If we already initialized, it is an error
if p := ctx.Provisioner(n); p != nil {
return nil, fmt.Errorf("Provisioner '%s' already initialized", n)
}
// Warning: make sure to acquire these locks AFTER the call to Provisioner
// above, since it also acquires locks.
ctx.ProvisionerLock.Lock()
defer ctx.ProvisionerLock.Unlock()
key := PathObjectCacheKey(ctx.Path(), n)
p, err := ctx.Components.ResourceProvisioner(n, key)
if err != nil {
return nil, err
}
ctx.ProvisionerCache[key] = p
// Also fetch the provisioner's schema
schema, err := p.GetConfigSchema()
if err != nil {
return nil, fmt.Errorf("error getting schema for provisioner %q: %s", n, err)
}
if ctx.ProvisionerSchemas == nil {
ctx.ProvisionerSchemas = make(map[string]*configschema.Block)
}
ctx.ProvisionerSchemas[n] = schema
return p, nil
}
func (ctx *BuiltinEvalContext) Provisioner(n string) ResourceProvisioner {
ctx.once.Do(ctx.init)
ctx.ProvisionerLock.Lock()
defer ctx.ProvisionerLock.Unlock()
key := PathObjectCacheKey(ctx.Path(), n)
return ctx.ProvisionerCache[key]
}
func (ctx *BuiltinEvalContext) ProvisionerSchema(n string) *configschema.Block {
ctx.once.Do(ctx.init)
ctx.ProviderLock.Lock()
defer ctx.ProviderLock.Unlock()
return ctx.ProvisionerSchemas[n]
}
func (ctx *BuiltinEvalContext) CloseProvisioner(n string) error {
ctx.once.Do(ctx.init)
ctx.ProvisionerLock.Lock()
defer ctx.ProvisionerLock.Unlock()
key := PathObjectCacheKey(ctx.Path(), n)
var prov interface{}
prov = ctx.ProvisionerCache[key]
if prov != nil {
if p, ok := prov.(ResourceProvisionerCloser); ok {
delete(ctx.ProvisionerCache, key)
return p.Close()
}
}
return nil
}
func (ctx *BuiltinEvalContext) EvaluateBlock(body hcl.Body, schema *configschema.Block, self addrs.Referenceable, key addrs.InstanceKey) (cty.Value, hcl.Body, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
scope := ctx.EvaluationScope(self, key)
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, addrs.NoKey)
return scope.EvalExpr(expr, wantType)
}
func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, key addrs.InstanceKey) *lang.Scope {
data := &evaluationStateData{
Evaluator: ctx.Evaluator,
ModulePath: ctx.PathValue,
InstanceKey: key,
}
return ctx.Evaluator.Scope(data, self)
}
func (ctx *BuiltinEvalContext) Path() addrs.ModuleInstance {
return ctx.PathValue
}
func (ctx *BuiltinEvalContext) SetModuleCallArguments(n addrs.ModuleCallInstance, vals map[string]cty.Value) {
ctx.VariableValuesLock.Lock()
defer ctx.VariableValuesLock.Unlock()
childPath := n.ModuleInstance(ctx.PathValue)
key := childPath.String()
args := ctx.VariableValues[key]
if args == nil {
args = make(map[string]cty.Value)
ctx.VariableValues[key] = vals
return
}
for k, v := range vals {
args[k] = v
}
}
func (ctx *BuiltinEvalContext) Diff() (*Diff, *sync.RWMutex) {
return ctx.DiffValue, ctx.DiffLock
}
func (ctx *BuiltinEvalContext) State() (*State, *sync.RWMutex) {
return ctx.StateValue, ctx.StateLock
}
func (ctx *BuiltinEvalContext) init() {
}