github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/terraform/eval_context_builtin.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package terraform
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"log"
    10  	"sync"
    11  
    12  	"github.com/hashicorp/hcl/v2"
    13  	"github.com/zclconf/go-cty/cty"
    14  
    15  	"github.com/terramate-io/tf/addrs"
    16  	"github.com/terramate-io/tf/checks"
    17  	"github.com/terramate-io/tf/configs/configschema"
    18  	"github.com/terramate-io/tf/instances"
    19  	"github.com/terramate-io/tf/lang"
    20  	"github.com/terramate-io/tf/plans"
    21  	"github.com/terramate-io/tf/providers"
    22  	"github.com/terramate-io/tf/provisioners"
    23  	"github.com/terramate-io/tf/refactoring"
    24  	"github.com/terramate-io/tf/states"
    25  	"github.com/terramate-io/tf/tfdiags"
    26  	"github.com/terramate-io/tf/version"
    27  )
    28  
    29  // BuiltinEvalContext is an EvalContext implementation that is used by
    30  // Terraform by default.
    31  type BuiltinEvalContext struct {
    32  	// StopContext is the context used to track whether we're complete
    33  	StopContext context.Context
    34  
    35  	// PathValue is the Path that this context is operating within.
    36  	PathValue addrs.ModuleInstance
    37  
    38  	// pathSet indicates that this context was explicitly created for a
    39  	// specific path, and can be safely used for evaluation. This lets us
    40  	// differentiate between PathValue being unset, and the zero value which is
    41  	// equivalent to RootModuleInstance.  Path and Evaluation methods will
    42  	// panic if this is not set.
    43  	pathSet bool
    44  
    45  	// Evaluator is used for evaluating expressions within the scope of this
    46  	// eval context.
    47  	Evaluator *Evaluator
    48  
    49  	// VariableValues contains the variable values across all modules. This
    50  	// structure is shared across the entire containing context, and so it
    51  	// may be accessed only when holding VariableValuesLock.
    52  	// The keys of the first level of VariableValues are the string
    53  	// representations of addrs.ModuleInstance values. The second-level keys
    54  	// are variable names within each module instance.
    55  	VariableValues     map[string]map[string]cty.Value
    56  	VariableValuesLock *sync.Mutex
    57  
    58  	// Plugins is a library of plugin components (providers and provisioners)
    59  	// available for use during a graph walk.
    60  	Plugins *contextPlugins
    61  
    62  	Hooks                 []Hook
    63  	InputValue            UIInput
    64  	ProviderCache         map[string]providers.Interface
    65  	ProviderInputConfig   map[string]map[string]cty.Value
    66  	ProviderLock          *sync.Mutex
    67  	ProvisionerCache      map[string]provisioners.Interface
    68  	ProvisionerLock       *sync.Mutex
    69  	ChangesValue          *plans.ChangesSync
    70  	StateValue            *states.SyncState
    71  	ChecksValue           *checks.State
    72  	RefreshStateValue     *states.SyncState
    73  	PrevRunStateValue     *states.SyncState
    74  	InstanceExpanderValue *instances.Expander
    75  	MoveResultsValue      refactoring.MoveResults
    76  }
    77  
    78  // BuiltinEvalContext implements EvalContext
    79  var _ EvalContext = (*BuiltinEvalContext)(nil)
    80  
    81  func (ctx *BuiltinEvalContext) WithPath(path addrs.ModuleInstance) EvalContext {
    82  	newCtx := *ctx
    83  	newCtx.pathSet = true
    84  	newCtx.PathValue = path
    85  	return &newCtx
    86  }
    87  
    88  func (ctx *BuiltinEvalContext) Stopped() <-chan struct{} {
    89  	// This can happen during tests. During tests, we just block forever.
    90  	if ctx.StopContext == nil {
    91  		return nil
    92  	}
    93  
    94  	return ctx.StopContext.Done()
    95  }
    96  
    97  func (ctx *BuiltinEvalContext) Hook(fn func(Hook) (HookAction, error)) error {
    98  	for _, h := range ctx.Hooks {
    99  		action, err := fn(h)
   100  		if err != nil {
   101  			return err
   102  		}
   103  
   104  		switch action {
   105  		case HookActionContinue:
   106  			continue
   107  		case HookActionHalt:
   108  			// Return an early exit error to trigger an early exit
   109  			log.Printf("[WARN] Early exit triggered by hook: %T", h)
   110  			return nil
   111  		}
   112  	}
   113  
   114  	return nil
   115  }
   116  
   117  func (ctx *BuiltinEvalContext) Input() UIInput {
   118  	return ctx.InputValue
   119  }
   120  
   121  func (ctx *BuiltinEvalContext) InitProvider(addr addrs.AbsProviderConfig) (providers.Interface, error) {
   122  	// If we already initialized, it is an error
   123  	if p := ctx.Provider(addr); p != nil {
   124  		return nil, fmt.Errorf("%s is already initialized", addr)
   125  	}
   126  
   127  	// Warning: make sure to acquire these locks AFTER the call to Provider
   128  	// above, since it also acquires locks.
   129  	ctx.ProviderLock.Lock()
   130  	defer ctx.ProviderLock.Unlock()
   131  
   132  	key := addr.String()
   133  
   134  	p, err := ctx.Plugins.NewProviderInstance(addr.Provider)
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  
   139  	log.Printf("[TRACE] BuiltinEvalContext: Initialized %q provider for %s", addr.String(), addr)
   140  	ctx.ProviderCache[key] = p
   141  
   142  	return p, nil
   143  }
   144  
   145  func (ctx *BuiltinEvalContext) Provider(addr addrs.AbsProviderConfig) providers.Interface {
   146  	ctx.ProviderLock.Lock()
   147  	defer ctx.ProviderLock.Unlock()
   148  
   149  	return ctx.ProviderCache[addr.String()]
   150  }
   151  
   152  func (ctx *BuiltinEvalContext) ProviderSchema(addr addrs.AbsProviderConfig) (providers.ProviderSchema, error) {
   153  	// first see if we have already have an initialized provider to avoid
   154  	// re-loading it only for the schema
   155  	p := ctx.Provider(addr)
   156  	if p != nil {
   157  		resp := p.GetProviderSchema()
   158  		// convert any diagnostics here in case this is the first call
   159  		// FIXME: better control provider instantiation so we can be sure this
   160  		// won't be the first call to ProviderSchema
   161  		var err error
   162  		if resp.Diagnostics.HasErrors() {
   163  			err = resp.Diagnostics.ErrWithWarnings()
   164  		}
   165  		return resp, err
   166  	}
   167  
   168  	return ctx.Plugins.ProviderSchema(addr.Provider)
   169  }
   170  
   171  func (ctx *BuiltinEvalContext) CloseProvider(addr addrs.AbsProviderConfig) error {
   172  	ctx.ProviderLock.Lock()
   173  	defer ctx.ProviderLock.Unlock()
   174  
   175  	key := addr.String()
   176  	provider := ctx.ProviderCache[key]
   177  	if provider != nil {
   178  		delete(ctx.ProviderCache, key)
   179  		return provider.Close()
   180  	}
   181  
   182  	return nil
   183  }
   184  
   185  func (ctx *BuiltinEvalContext) ConfigureProvider(addr addrs.AbsProviderConfig, cfg cty.Value) tfdiags.Diagnostics {
   186  	var diags tfdiags.Diagnostics
   187  	if !addr.Module.Equal(ctx.Path().Module()) {
   188  		// This indicates incorrect use of ConfigureProvider: it should be used
   189  		// only from the module that the provider configuration belongs to.
   190  		panic(fmt.Sprintf("%s configured by wrong module %s", addr, ctx.Path()))
   191  	}
   192  
   193  	p := ctx.Provider(addr)
   194  	if p == nil {
   195  		diags = diags.Append(fmt.Errorf("%s not initialized", addr))
   196  		return diags
   197  	}
   198  
   199  	req := providers.ConfigureProviderRequest{
   200  		TerraformVersion: version.String(),
   201  		Config:           cfg,
   202  	}
   203  
   204  	resp := p.ConfigureProvider(req)
   205  	return resp.Diagnostics
   206  }
   207  
   208  func (ctx *BuiltinEvalContext) ProviderInput(pc addrs.AbsProviderConfig) map[string]cty.Value {
   209  	ctx.ProviderLock.Lock()
   210  	defer ctx.ProviderLock.Unlock()
   211  
   212  	if !pc.Module.Equal(ctx.Path().Module()) {
   213  		// This indicates incorrect use of InitProvider: it should be used
   214  		// only from the module that the provider configuration belongs to.
   215  		panic(fmt.Sprintf("%s initialized by wrong module %s", pc, ctx.Path()))
   216  	}
   217  
   218  	if !ctx.Path().IsRoot() {
   219  		// Only root module provider configurations can have input.
   220  		return nil
   221  	}
   222  
   223  	return ctx.ProviderInputConfig[pc.String()]
   224  }
   225  
   226  func (ctx *BuiltinEvalContext) SetProviderInput(pc addrs.AbsProviderConfig, c map[string]cty.Value) {
   227  	absProvider := pc
   228  	if !pc.Module.IsRoot() {
   229  		// Only root module provider configurations can have input.
   230  		log.Printf("[WARN] BuiltinEvalContext: attempt to SetProviderInput for non-root module")
   231  		return
   232  	}
   233  
   234  	// Save the configuration
   235  	ctx.ProviderLock.Lock()
   236  	ctx.ProviderInputConfig[absProvider.String()] = c
   237  	ctx.ProviderLock.Unlock()
   238  }
   239  
   240  func (ctx *BuiltinEvalContext) Provisioner(n string) (provisioners.Interface, error) {
   241  	ctx.ProvisionerLock.Lock()
   242  	defer ctx.ProvisionerLock.Unlock()
   243  
   244  	p, ok := ctx.ProvisionerCache[n]
   245  	if !ok {
   246  		var err error
   247  		p, err = ctx.Plugins.NewProvisionerInstance(n)
   248  		if err != nil {
   249  			return nil, err
   250  		}
   251  
   252  		ctx.ProvisionerCache[n] = p
   253  	}
   254  
   255  	return p, nil
   256  }
   257  
   258  func (ctx *BuiltinEvalContext) ProvisionerSchema(n string) (*configschema.Block, error) {
   259  	return ctx.Plugins.ProvisionerSchema(n)
   260  }
   261  
   262  func (ctx *BuiltinEvalContext) CloseProvisioners() error {
   263  	var diags tfdiags.Diagnostics
   264  	ctx.ProvisionerLock.Lock()
   265  	defer ctx.ProvisionerLock.Unlock()
   266  
   267  	for name, prov := range ctx.ProvisionerCache {
   268  		err := prov.Close()
   269  		if err != nil {
   270  			diags = diags.Append(fmt.Errorf("provisioner.Close %s: %s", name, err))
   271  		}
   272  	}
   273  
   274  	return diags.Err()
   275  }
   276  
   277  func (ctx *BuiltinEvalContext) EvaluateBlock(body hcl.Body, schema *configschema.Block, self addrs.Referenceable, keyData InstanceKeyEvalData) (cty.Value, hcl.Body, tfdiags.Diagnostics) {
   278  	var diags tfdiags.Diagnostics
   279  	scope := ctx.EvaluationScope(self, nil, keyData)
   280  	body, evalDiags := scope.ExpandBlock(body, schema)
   281  	diags = diags.Append(evalDiags)
   282  	val, evalDiags := scope.EvalBlock(body, schema)
   283  	diags = diags.Append(evalDiags)
   284  	return val, body, diags
   285  }
   286  
   287  func (ctx *BuiltinEvalContext) EvaluateExpr(expr hcl.Expression, wantType cty.Type, self addrs.Referenceable) (cty.Value, tfdiags.Diagnostics) {
   288  	scope := ctx.EvaluationScope(self, nil, EvalDataForNoInstanceKey)
   289  	return scope.EvalExpr(expr, wantType)
   290  }
   291  
   292  func (ctx *BuiltinEvalContext) EvaluateReplaceTriggeredBy(expr hcl.Expression, repData instances.RepetitionData) (*addrs.Reference, bool, tfdiags.Diagnostics) {
   293  
   294  	// get the reference to lookup changes in the plan
   295  	ref, diags := evalReplaceTriggeredByExpr(expr, repData)
   296  	if diags.HasErrors() {
   297  		return nil, false, diags
   298  	}
   299  
   300  	var changes []*plans.ResourceInstanceChangeSrc
   301  	// store the address once we get it for validation
   302  	var resourceAddr addrs.Resource
   303  
   304  	// The reference is either a resource or resource instance
   305  	switch sub := ref.Subject.(type) {
   306  	case addrs.Resource:
   307  		resourceAddr = sub
   308  		rc := sub.Absolute(ctx.Path())
   309  		changes = ctx.Changes().GetChangesForAbsResource(rc)
   310  	case addrs.ResourceInstance:
   311  		resourceAddr = sub.ContainingResource()
   312  		rc := sub.Absolute(ctx.Path())
   313  		change := ctx.Changes().GetResourceInstanceChange(rc, states.CurrentGen)
   314  		if change != nil {
   315  			// we'll generate an error below if there was no change
   316  			changes = append(changes, change)
   317  		}
   318  	}
   319  
   320  	// Do some validation to make sure we are expecting a change at all
   321  	cfg := ctx.Evaluator.Config.Descendent(ctx.Path().Module())
   322  	resCfg := cfg.Module.ResourceByAddr(resourceAddr)
   323  	if resCfg == nil {
   324  		diags = diags.Append(&hcl.Diagnostic{
   325  			Severity: hcl.DiagError,
   326  			Summary:  `Reference to undeclared resource`,
   327  			Detail:   fmt.Sprintf(`A resource %s has not been declared in %s`, ref.Subject, moduleDisplayAddr(ctx.Path())),
   328  			Subject:  expr.Range().Ptr(),
   329  		})
   330  		return nil, false, diags
   331  	}
   332  
   333  	if len(changes) == 0 {
   334  		// If the resource is valid there should always be at least one change.
   335  		diags = diags.Append(fmt.Errorf("no change found for %s in %s", ref.Subject, moduleDisplayAddr(ctx.Path())))
   336  		return nil, false, diags
   337  	}
   338  
   339  	// If we don't have a traversal beyond the resource, then we can just look
   340  	// for any change.
   341  	if len(ref.Remaining) == 0 {
   342  		for _, c := range changes {
   343  			switch c.ChangeSrc.Action {
   344  			// Only immediate changes to the resource will trigger replacement.
   345  			case plans.Update, plans.DeleteThenCreate, plans.CreateThenDelete:
   346  				return ref, true, diags
   347  			}
   348  		}
   349  
   350  		// no change triggered
   351  		return nil, false, diags
   352  	}
   353  
   354  	// This must be an instances to have a remaining traversal, which means a
   355  	// single change.
   356  	change := changes[0]
   357  
   358  	// Make sure the change is actionable. A create or delete action will have
   359  	// a change in value, but are not valid for our purposes here.
   360  	switch change.ChangeSrc.Action {
   361  	case plans.Update, plans.DeleteThenCreate, plans.CreateThenDelete:
   362  		// OK
   363  	default:
   364  		return nil, false, diags
   365  	}
   366  
   367  	// Since we have a traversal after the resource reference, we will need to
   368  	// decode the changes, which means we need a schema.
   369  	providerAddr := change.ProviderAddr
   370  	schema, err := ctx.ProviderSchema(providerAddr)
   371  	if err != nil {
   372  		diags = diags.Append(err)
   373  		return nil, false, diags
   374  	}
   375  
   376  	resAddr := change.Addr.ContainingResource().Resource
   377  	resSchema, _ := schema.SchemaForResourceType(resAddr.Mode, resAddr.Type)
   378  	ty := resSchema.ImpliedType()
   379  
   380  	before, err := change.ChangeSrc.Before.Decode(ty)
   381  	if err != nil {
   382  		diags = diags.Append(err)
   383  		return nil, false, diags
   384  	}
   385  
   386  	after, err := change.ChangeSrc.After.Decode(ty)
   387  	if err != nil {
   388  		diags = diags.Append(err)
   389  		return nil, false, diags
   390  	}
   391  
   392  	path := traversalToPath(ref.Remaining)
   393  	attrBefore, _ := path.Apply(before)
   394  	attrAfter, _ := path.Apply(after)
   395  
   396  	if attrBefore == cty.NilVal || attrAfter == cty.NilVal {
   397  		replace := attrBefore != attrAfter
   398  		return ref, replace, diags
   399  	}
   400  
   401  	replace := !attrBefore.RawEquals(attrAfter)
   402  
   403  	return ref, replace, diags
   404  }
   405  
   406  func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, source addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope {
   407  	if !ctx.pathSet {
   408  		panic("context path not set")
   409  	}
   410  	data := &evaluationStateData{
   411  		Evaluator:       ctx.Evaluator,
   412  		ModulePath:      ctx.PathValue,
   413  		InstanceKeyData: keyData,
   414  		Operation:       ctx.Evaluator.Operation,
   415  	}
   416  	scope := ctx.Evaluator.Scope(data, self, source)
   417  
   418  	// ctx.PathValue is the path of the module that contains whatever
   419  	// expression the caller will be trying to evaluate, so this will
   420  	// activate only the experiments from that particular module, to
   421  	// be consistent with how experiment checking in the "configs"
   422  	// package itself works. The nil check here is for robustness in
   423  	// incompletely-mocked testing situations; mc should never be nil in
   424  	// real situations.
   425  	if mc := ctx.Evaluator.Config.DescendentForInstance(ctx.PathValue); mc != nil {
   426  		scope.SetActiveExperiments(mc.Module.ActiveExperiments)
   427  	}
   428  	return scope
   429  }
   430  
   431  func (ctx *BuiltinEvalContext) Path() addrs.ModuleInstance {
   432  	if !ctx.pathSet {
   433  		panic("context path not set")
   434  	}
   435  	return ctx.PathValue
   436  }
   437  
   438  func (ctx *BuiltinEvalContext) SetRootModuleArgument(addr addrs.InputVariable, v cty.Value) {
   439  	ctx.VariableValuesLock.Lock()
   440  	defer ctx.VariableValuesLock.Unlock()
   441  
   442  	log.Printf("[TRACE] BuiltinEvalContext: Storing final value for variable %s", addr.Absolute(addrs.RootModuleInstance))
   443  	key := addrs.RootModuleInstance.String()
   444  	args := ctx.VariableValues[key]
   445  	if args == nil {
   446  		args = make(map[string]cty.Value)
   447  		ctx.VariableValues[key] = args
   448  	}
   449  	args[addr.Name] = v
   450  }
   451  
   452  func (ctx *BuiltinEvalContext) SetModuleCallArgument(callAddr addrs.ModuleCallInstance, varAddr addrs.InputVariable, v cty.Value) {
   453  	ctx.VariableValuesLock.Lock()
   454  	defer ctx.VariableValuesLock.Unlock()
   455  
   456  	if !ctx.pathSet {
   457  		panic("context path not set")
   458  	}
   459  
   460  	childPath := callAddr.ModuleInstance(ctx.PathValue)
   461  	log.Printf("[TRACE] BuiltinEvalContext: Storing final value for variable %s", varAddr.Absolute(childPath))
   462  	key := childPath.String()
   463  	args := ctx.VariableValues[key]
   464  	if args == nil {
   465  		args = make(map[string]cty.Value)
   466  		ctx.VariableValues[key] = args
   467  	}
   468  	args[varAddr.Name] = v
   469  }
   470  
   471  func (ctx *BuiltinEvalContext) GetVariableValue(addr addrs.AbsInputVariableInstance) cty.Value {
   472  	ctx.VariableValuesLock.Lock()
   473  	defer ctx.VariableValuesLock.Unlock()
   474  
   475  	modKey := addr.Module.String()
   476  	modVars := ctx.VariableValues[modKey]
   477  	val, ok := modVars[addr.Variable.Name]
   478  	if !ok {
   479  		return cty.DynamicVal
   480  	}
   481  	return val
   482  }
   483  
   484  func (ctx *BuiltinEvalContext) Changes() *plans.ChangesSync {
   485  	return ctx.ChangesValue
   486  }
   487  
   488  func (ctx *BuiltinEvalContext) State() *states.SyncState {
   489  	return ctx.StateValue
   490  }
   491  
   492  func (ctx *BuiltinEvalContext) Checks() *checks.State {
   493  	return ctx.ChecksValue
   494  }
   495  
   496  func (ctx *BuiltinEvalContext) RefreshState() *states.SyncState {
   497  	return ctx.RefreshStateValue
   498  }
   499  
   500  func (ctx *BuiltinEvalContext) PrevRunState() *states.SyncState {
   501  	return ctx.PrevRunStateValue
   502  }
   503  
   504  func (ctx *BuiltinEvalContext) InstanceExpander() *instances.Expander {
   505  	return ctx.InstanceExpanderValue
   506  }
   507  
   508  func (ctx *BuiltinEvalContext) MoveResults() refactoring.MoveResults {
   509  	return ctx.MoveResultsValue
   510  }