opentofu/terraform/context.go
Paul Hinze 0fff7d1673 core: validate graph w/ diff during plan phase
This reimplements my prior attempt at nipping issues where a plan did
not yield the same cycle an apply did. My prior attempt was to have
ctx.Validate generate a "Verbose" worst-case graph. It turns out that
skipping PruneDestroyTransformer to generate this graph misses important
heuristics that prevent cycles by dropping destroy nodes that are
determined to be unused.

This resulted in Validate improperly failing in scenarios where these
heuristics would have broken the cycle.

We detected the problem during the work on #1781 and worked around the
issue by reverting to the non-Verbose graph in Validate.

This commit accomplishes the original goal in a better way - by
generating the full graph and checking it once Plan has calculated the
diff. This guarantees that any graph issue that would be caught by Apply
will be caught by Plan.
2015-05-05 17:24:44 -05:00

485 lines
12 KiB
Go

package terraform
import (
"fmt"
"log"
"sort"
"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
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.
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 {
// 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()
}
// 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
}
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: opts.Variables,
parallelSem: NewSemaphore(par),
providerInputConfig: make(map[string]map[string]interface{}),
sh: sh,
}
}
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 variabel
// 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.VariableTypeString:
// Good!
default:
panic(fmt.Sprintf("Unknown variable type: %#v", v.Type()))
}
var defaultString string
if v.Default != nil {
defaultString = v.Default.(string)
}
// 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),
Default: defaultString,
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
_, err = c.walk(graph, walkApply)
// 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,
}
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
if _, err := c.walk(graph, operation); 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
}
return p, nil
}
// 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...)
}
}
// 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("[INFO] Starting graph walk: %s", operation.String())
walker := &ContextGraphWalker{Context: c, Operation: operation}
return walker, graph.Walk(walker)
}