mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
In #7170 we found two scenarios where the type checking done during the `context.Validate()` graph walk was circumvented, and the subsequent assumption of type safety in the provider's `Diff()` implementation caused panics. Both scenarios have to do with interpolations that reference Computed values. The sentinel we use to indicate that a value is Computed does not carry any type information with it yet. That means that an incorrect reference to a list or a map in a string attribute can "sneak through" validation only to crop up... 1. ...during Plan for Data Source References 2. ...during Apply for Resource references In order to address this, we: * add high-level tests for each of these two scenarios in `provider/test` * add context-level tests for the same two scenarios in `terraform` (these tests proved _really_ tricky to write!) * place an `EvalValidateResource` just before `EvalDiff` and `EvalApply` to catch these errors * add some plumbing to `Plan()` and `Apply()` to return validation errors, which were previously only generated during `Validate()` * wrap unit-tests around `EvalValidateResource` * add an `IgnoreWarnings` option to `EvalValidateResource` to prevent active warnings from halting execution on the second-pass validation Eventually, we might be able to attach type information to Computed values, which would allow for these errors to be caught earlier. For now, this solution keeps us safe from panics and raises the proper errors to the user. Fixes #7170
551 lines
14 KiB
Go
551 lines
14 KiB
Go
package terraform
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/hashicorp/terraform/config"
|
|
"github.com/hashicorp/terraform/config/module"
|
|
)
|
|
|
|
// InputMode defines what sort of input will be asked for when Input
|
|
// is called on Context.
|
|
type InputMode byte
|
|
|
|
const (
|
|
// InputModeVar asks for all variables
|
|
InputModeVar InputMode = 1 << iota
|
|
|
|
// InputModeVarUnset asks for variables which are not set yet
|
|
InputModeVarUnset
|
|
|
|
// InputModeProvider asks for provider variables
|
|
InputModeProvider
|
|
|
|
// InputModeStd is the standard operating mode and asks for both variables
|
|
// and providers.
|
|
InputModeStd = InputModeVar | InputModeProvider
|
|
)
|
|
|
|
// ContextOpts are the user-configurable options to create a context with
|
|
// NewContext.
|
|
type ContextOpts struct {
|
|
Destroy bool
|
|
Diff *Diff
|
|
Hooks []Hook
|
|
Module *module.Tree
|
|
Parallelism int
|
|
State *State
|
|
StateFutureAllowed bool
|
|
Providers map[string]ResourceProviderFactory
|
|
Provisioners map[string]ResourceProvisionerFactory
|
|
Targets []string
|
|
Variables map[string]string
|
|
|
|
UIInput UIInput
|
|
}
|
|
|
|
// Context represents all the context that Terraform needs in order to
|
|
// perform operations on infrastructure. This structure is built using
|
|
// NewContext. See the documentation for that.
|
|
//
|
|
// Extra functions on Context can be found in context_*.go files.
|
|
type Context struct {
|
|
destroy bool
|
|
diff *Diff
|
|
diffLock sync.RWMutex
|
|
hooks []Hook
|
|
module *module.Tree
|
|
providers map[string]ResourceProviderFactory
|
|
provisioners map[string]ResourceProvisionerFactory
|
|
sh *stopHook
|
|
state *State
|
|
stateLock sync.RWMutex
|
|
targets []string
|
|
uiInput UIInput
|
|
variables map[string]string
|
|
|
|
l sync.Mutex // Lock acquired during any task
|
|
parallelSem Semaphore
|
|
providerInputConfig map[string]map[string]interface{}
|
|
runCh <-chan struct{}
|
|
}
|
|
|
|
// NewContext creates a new Context structure.
|
|
//
|
|
// Once a Context is creator, the pointer values within ContextOpts
|
|
// should not be mutated in any way, since the pointers are copied, not
|
|
// the values themselves.
|
|
func NewContext(opts *ContextOpts) (*Context, error) {
|
|
// Copy all the hooks and add our stop hook. We don't append directly
|
|
// to the Config so that we're not modifying that in-place.
|
|
sh := new(stopHook)
|
|
hooks := make([]Hook, len(opts.Hooks)+1)
|
|
copy(hooks, opts.Hooks)
|
|
hooks[len(opts.Hooks)] = sh
|
|
|
|
state := opts.State
|
|
if state == nil {
|
|
state = new(State)
|
|
state.init()
|
|
}
|
|
|
|
// If our state is from the future, then error. Callers can avoid
|
|
// this error by explicitly setting `StateFutureAllowed`.
|
|
if !opts.StateFutureAllowed && state.FromFutureTerraform() {
|
|
return nil, fmt.Errorf(
|
|
"Terraform doesn't allow running any operations against a state\n"+
|
|
"that was written by a future Terraform version. The state is\n"+
|
|
"reporting it is written by Terraform '%s'.\n\n"+
|
|
"Please run at least that version of Terraform to continue.",
|
|
state.TFVersion)
|
|
}
|
|
|
|
// Explicitly reset our state version to our current version so that
|
|
// any operations we do will write out that our latest version
|
|
// has run.
|
|
state.TFVersion = Version
|
|
|
|
// Determine parallelism, default to 10. We do this both to limit
|
|
// CPU pressure but also to have an extra guard against rate throttling
|
|
// from providers.
|
|
par := opts.Parallelism
|
|
if par == 0 {
|
|
par = 10
|
|
}
|
|
|
|
// Setup the variables. We first take the variables given to us.
|
|
// We then merge in the variables set in the environment.
|
|
variables := make(map[string]string)
|
|
for _, v := range os.Environ() {
|
|
if !strings.HasPrefix(v, VarEnvPrefix) {
|
|
continue
|
|
}
|
|
|
|
// Strip off the prefix and get the value after the first "="
|
|
idx := strings.Index(v, "=")
|
|
k := v[len(VarEnvPrefix):idx]
|
|
v = v[idx+1:]
|
|
|
|
// Override the command-line set variable
|
|
variables[k] = v
|
|
}
|
|
for k, v := range opts.Variables {
|
|
variables[k] = v
|
|
}
|
|
|
|
return &Context{
|
|
destroy: opts.Destroy,
|
|
diff: opts.Diff,
|
|
hooks: hooks,
|
|
module: opts.Module,
|
|
providers: opts.Providers,
|
|
provisioners: opts.Provisioners,
|
|
state: state,
|
|
targets: opts.Targets,
|
|
uiInput: opts.UIInput,
|
|
variables: variables,
|
|
|
|
parallelSem: NewSemaphore(par),
|
|
providerInputConfig: make(map[string]map[string]interface{}),
|
|
sh: sh,
|
|
}, nil
|
|
}
|
|
|
|
type ContextGraphOpts struct {
|
|
Validate bool
|
|
Verbose bool
|
|
}
|
|
|
|
// Graph returns the graph for this config.
|
|
func (c *Context) Graph(g *ContextGraphOpts) (*Graph, error) {
|
|
return c.graphBuilder(g).Build(RootModulePath)
|
|
}
|
|
|
|
// GraphBuilder returns the GraphBuilder that will be used to create
|
|
// the graphs for this context.
|
|
func (c *Context) graphBuilder(g *ContextGraphOpts) GraphBuilder {
|
|
// TODO test
|
|
providers := make([]string, 0, len(c.providers))
|
|
for k, _ := range c.providers {
|
|
providers = append(providers, k)
|
|
}
|
|
|
|
provisioners := make([]string, 0, len(c.provisioners))
|
|
for k, _ := range c.provisioners {
|
|
provisioners = append(provisioners, k)
|
|
}
|
|
|
|
return &BuiltinGraphBuilder{
|
|
Root: c.module,
|
|
Diff: c.diff,
|
|
Providers: providers,
|
|
Provisioners: provisioners,
|
|
State: c.state,
|
|
Targets: c.targets,
|
|
Destroy: c.destroy,
|
|
Validate: g.Validate,
|
|
Verbose: g.Verbose,
|
|
}
|
|
}
|
|
|
|
// Input asks for input to fill variables and provider configurations.
|
|
// This modifies the configuration in-place, so asking for Input twice
|
|
// may result in different UI output showing different current values.
|
|
func (c *Context) Input(mode InputMode) error {
|
|
v := c.acquireRun()
|
|
defer c.releaseRun(v)
|
|
|
|
if mode&InputModeVar != 0 {
|
|
// Walk the variables first for the root module. We walk them in
|
|
// alphabetical order for UX reasons.
|
|
rootConf := c.module.Config()
|
|
names := make([]string, len(rootConf.Variables))
|
|
m := make(map[string]*config.Variable)
|
|
for i, v := range rootConf.Variables {
|
|
names[i] = v.Name
|
|
m[v.Name] = v
|
|
}
|
|
sort.Strings(names)
|
|
for _, n := range names {
|
|
// If we only care about unset variables, then if the variable
|
|
// is set, continue on.
|
|
if mode&InputModeVarUnset != 0 {
|
|
if _, ok := c.variables[n]; ok {
|
|
continue
|
|
}
|
|
}
|
|
|
|
v := m[n]
|
|
switch v.Type() {
|
|
case config.VariableTypeUnknown:
|
|
continue
|
|
case config.VariableTypeMap:
|
|
continue
|
|
case config.VariableTypeList:
|
|
continue
|
|
case config.VariableTypeString:
|
|
// Good!
|
|
default:
|
|
panic(fmt.Sprintf("Unknown variable type: %#v", v.Type()))
|
|
}
|
|
|
|
// If the variable is not already set, and the variable defines a
|
|
// default, use that for the value.
|
|
if _, ok := c.variables[n]; !ok {
|
|
if v.Default != nil {
|
|
c.variables[n] = v.Default.(string)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Ask the user for a value for this variable
|
|
var value string
|
|
for {
|
|
var err error
|
|
value, err = c.uiInput.Input(&InputOpts{
|
|
Id: fmt.Sprintf("var.%s", n),
|
|
Query: fmt.Sprintf("var.%s", n),
|
|
Description: v.Description,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"Error asking for %s: %s", n, err)
|
|
}
|
|
|
|
if value == "" && v.Required() {
|
|
// Redo if it is required.
|
|
continue
|
|
}
|
|
|
|
if value == "" {
|
|
// No value, just exit the loop. With no value, we just
|
|
// use whatever is currently set in variables.
|
|
break
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
if value != "" {
|
|
c.variables[n] = value
|
|
}
|
|
}
|
|
}
|
|
|
|
if mode&InputModeProvider != 0 {
|
|
// Build the graph
|
|
graph, err := c.Graph(&ContextGraphOpts{Validate: true})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Do the walk
|
|
if _, err := c.walk(graph, walkInput); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Apply applies the changes represented by this context and returns
|
|
// the resulting state.
|
|
//
|
|
// In addition to returning the resulting state, this context is updated
|
|
// with the latest state.
|
|
func (c *Context) Apply() (*State, error) {
|
|
v := c.acquireRun()
|
|
defer c.releaseRun(v)
|
|
|
|
// Copy our own state
|
|
c.state = c.state.DeepCopy()
|
|
|
|
// Build the graph
|
|
graph, err := c.Graph(&ContextGraphOpts{Validate: true})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Do the walk
|
|
var walker *ContextGraphWalker
|
|
if c.destroy {
|
|
walker, err = c.walk(graph, walkDestroy)
|
|
} else {
|
|
walker, err = c.walk(graph, walkApply)
|
|
}
|
|
|
|
if len(walker.ValidationErrors) > 0 {
|
|
err = multierror.Append(err, walker.ValidationErrors...)
|
|
}
|
|
|
|
// Clean out any unused things
|
|
c.state.prune()
|
|
|
|
return c.state, err
|
|
}
|
|
|
|
// Plan generates an execution plan for the given context.
|
|
//
|
|
// The execution plan encapsulates the context and can be stored
|
|
// in order to reinstantiate a context later for Apply.
|
|
//
|
|
// Plan also updates the diff of this context to be the diff generated
|
|
// by the plan, so Apply can be called after.
|
|
func (c *Context) Plan() (*Plan, error) {
|
|
v := c.acquireRun()
|
|
defer c.releaseRun(v)
|
|
|
|
p := &Plan{
|
|
Module: c.module,
|
|
Vars: c.variables,
|
|
State: c.state,
|
|
Targets: c.targets,
|
|
}
|
|
|
|
var operation walkOperation
|
|
if c.destroy {
|
|
operation = walkPlanDestroy
|
|
} else {
|
|
// Set our state to be something temporary. We do this so that
|
|
// the plan can update a fake state so that variables work, then
|
|
// we replace it back with our old state.
|
|
old := c.state
|
|
if old == nil {
|
|
c.state = &State{}
|
|
c.state.init()
|
|
} else {
|
|
c.state = old.DeepCopy()
|
|
}
|
|
defer func() {
|
|
c.state = old
|
|
}()
|
|
|
|
operation = walkPlan
|
|
}
|
|
|
|
// Setup our diff
|
|
c.diffLock.Lock()
|
|
c.diff = new(Diff)
|
|
c.diff.init()
|
|
c.diffLock.Unlock()
|
|
|
|
// Build the graph
|
|
graph, err := c.Graph(&ContextGraphOpts{Validate: true})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Do the walk
|
|
walker, err := c.walk(graph, operation)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p.Diff = c.diff
|
|
|
|
// Now that we have a diff, we can build the exact graph that Apply will use
|
|
// and catch any possible cycles during the Plan phase.
|
|
if _, err := c.Graph(&ContextGraphOpts{Validate: true}); err != nil {
|
|
return nil, err
|
|
}
|
|
var errs error
|
|
if len(walker.ValidationErrors) > 0 {
|
|
errs = multierror.Append(errs, walker.ValidationErrors...)
|
|
}
|
|
return p, errs
|
|
}
|
|
|
|
// Refresh goes through all the resources in the state and refreshes them
|
|
// to their latest state. This will update the state that this context
|
|
// works with, along with returning it.
|
|
//
|
|
// Even in the case an error is returned, the state will be returned and
|
|
// will potentially be partially updated.
|
|
func (c *Context) Refresh() (*State, error) {
|
|
v := c.acquireRun()
|
|
defer c.releaseRun(v)
|
|
|
|
// Copy our own state
|
|
c.state = c.state.DeepCopy()
|
|
|
|
// Build the graph
|
|
graph, err := c.Graph(&ContextGraphOpts{Validate: true})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Do the walk
|
|
if _, err := c.walk(graph, walkRefresh); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Clean out any unused things
|
|
c.state.prune()
|
|
|
|
return c.state, nil
|
|
}
|
|
|
|
// Stop stops the running task.
|
|
//
|
|
// Stop will block until the task completes.
|
|
func (c *Context) Stop() {
|
|
c.l.Lock()
|
|
ch := c.runCh
|
|
|
|
// If we aren't running, then just return
|
|
if ch == nil {
|
|
c.l.Unlock()
|
|
return
|
|
}
|
|
|
|
// Tell the hook we want to stop
|
|
c.sh.Stop()
|
|
|
|
// Wait for us to stop
|
|
c.l.Unlock()
|
|
<-ch
|
|
}
|
|
|
|
// Validate validates the configuration and returns any warnings or errors.
|
|
func (c *Context) Validate() ([]string, []error) {
|
|
v := c.acquireRun()
|
|
defer c.releaseRun(v)
|
|
|
|
var errs error
|
|
|
|
// Validate the configuration itself
|
|
if err := c.module.Validate(); err != nil {
|
|
errs = multierror.Append(errs, err)
|
|
}
|
|
|
|
// This only needs to be done for the root module, since inter-module
|
|
// variables are validated in the module tree.
|
|
if config := c.module.Config(); config != nil {
|
|
// Validate the user variables
|
|
if err := smcUserVariables(config, c.variables); len(err) > 0 {
|
|
errs = multierror.Append(errs, err...)
|
|
}
|
|
}
|
|
|
|
// If we have errors at this point, the graphing has no chance,
|
|
// so just bail early.
|
|
if errs != nil {
|
|
return nil, []error{errs}
|
|
}
|
|
|
|
// Build the graph so we can walk it and run Validate on nodes.
|
|
// We also validate the graph generated here, but this graph doesn't
|
|
// necessarily match the graph that Plan will generate, so we'll validate the
|
|
// graph again later after Planning.
|
|
graph, err := c.Graph(&ContextGraphOpts{Validate: true})
|
|
if err != nil {
|
|
return nil, []error{err}
|
|
}
|
|
|
|
// Walk
|
|
walker, err := c.walk(graph, walkValidate)
|
|
if err != nil {
|
|
return nil, multierror.Append(errs, err).Errors
|
|
}
|
|
|
|
// Return the result
|
|
rerrs := multierror.Append(errs, walker.ValidationErrors...)
|
|
return walker.ValidationWarnings, rerrs.Errors
|
|
}
|
|
|
|
// Module returns the module tree associated with this context.
|
|
func (c *Context) Module() *module.Tree {
|
|
return c.module
|
|
}
|
|
|
|
// Variables will return the mapping of variables that were defined
|
|
// for this Context. If Input was called, this mapping may be different
|
|
// than what was given.
|
|
func (c *Context) Variables() map[string]string {
|
|
return c.variables
|
|
}
|
|
|
|
// SetVariable sets a variable after a context has already been built.
|
|
func (c *Context) SetVariable(k, v string) {
|
|
c.variables[k] = v
|
|
}
|
|
|
|
func (c *Context) acquireRun() chan<- struct{} {
|
|
c.l.Lock()
|
|
defer c.l.Unlock()
|
|
|
|
// Wait for no channel to exist
|
|
for c.runCh != nil {
|
|
c.l.Unlock()
|
|
ch := c.runCh
|
|
<-ch
|
|
c.l.Lock()
|
|
}
|
|
|
|
ch := make(chan struct{})
|
|
c.runCh = ch
|
|
return ch
|
|
}
|
|
|
|
func (c *Context) releaseRun(ch chan<- struct{}) {
|
|
c.l.Lock()
|
|
defer c.l.Unlock()
|
|
|
|
close(ch)
|
|
c.runCh = nil
|
|
c.sh.Reset()
|
|
}
|
|
|
|
func (c *Context) walk(
|
|
graph *Graph, operation walkOperation) (*ContextGraphWalker, error) {
|
|
// Walk the graph
|
|
log.Printf("[DEBUG] Starting graph walk: %s", operation.String())
|
|
walker := &ContextGraphWalker{Context: c, Operation: operation}
|
|
return walker, graph.Walk(walker)
|
|
}
|