terraform/internal/terraform/context.go

434 lines
14 KiB
Go

package terraform
import (
"context"
"fmt"
"log"
"sort"
"sync"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/logging"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/provisioners"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
_ "github.com/hashicorp/terraform/internal/logging"
)
// InputMode defines what sort of input will be asked for when Input
// is called on Context.
type InputMode byte
const (
// InputModeProvider asks for provider variables
InputModeProvider InputMode = 1 << iota
// InputModeStd is the standard operating mode and asks for both variables
// and providers.
InputModeStd = InputModeProvider
)
// ContextOpts are the user-configurable options to create a context with
// NewContext.
type ContextOpts struct {
Meta *ContextMeta
Hooks []Hook
Parallelism int
Providers map[addrs.Provider]providers.Factory
Provisioners map[string]provisioners.Factory
UIInput UIInput
}
// ContextMeta is metadata about the running context. This is information
// that this package or structure cannot determine on its own but exposes
// into Terraform in various ways. This must be provided by the Context
// initializer.
type ContextMeta struct {
Env string // Env is the state environment
// OriginalWorkingDir is the working directory where the Terraform CLI
// was run from, which may no longer actually be the current working
// directory if the user included the -chdir=... option.
//
// If this string is empty then the original working directory is the same
// as the current working directory.
//
// In most cases we should respect the user's override by ignoring this
// path and just using the current working directory, but this is here
// for some exceptional cases where the original working directory is
// needed.
OriginalWorkingDir string
}
// Context represents all the context that Terraform needs in order to
// perform operations on infrastructure. This structure is built using
// NewContext.
type Context struct {
// meta captures some misc. information about the working directory where
// we're taking these actions, and thus which should remain steady between
// operations.
meta *ContextMeta
plugins *contextPlugins
hooks []Hook
sh *stopHook
uiInput UIInput
l sync.Mutex // Lock acquired during any task
parallelSem Semaphore
providerInputConfig map[string]map[string]cty.Value
runCond *sync.Cond
runContext context.Context
runContextCancel context.CancelFunc
}
// (additional methods on Context can be found in context_*.go files.)
// NewContext creates a new Context structure.
//
// Once a Context is created, the caller must not access or mutate any of
// the objects referenced (directly or indirectly) by the ContextOpts fields.
//
// If the returned diagnostics contains errors then the resulting context is
// invalid and must not be used.
func NewContext(opts *ContextOpts) (*Context, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
log.Printf("[TRACE] terraform.NewContext: starting")
// 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
// 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.
// We throw an error in case of negative parallelism
par := opts.Parallelism
if par < 0 {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid parallelism value",
fmt.Sprintf("The parallelism must be a positive value. Not %d.", par),
))
return nil, diags
}
if par == 0 {
par = 10
}
plugins := newContextPlugins(opts.Providers, opts.Provisioners)
log.Printf("[TRACE] terraform.NewContext: complete")
return &Context{
hooks: hooks,
meta: opts.Meta,
uiInput: opts.UIInput,
plugins: plugins,
parallelSem: NewSemaphore(par),
providerInputConfig: make(map[string]map[string]cty.Value),
sh: sh,
}, diags
}
func (c *Context) Schemas(config *configs.Config, state *states.State) (*Schemas, tfdiags.Diagnostics) {
// TODO: This method gets called multiple times on the same context with
// the same inputs by different parts of Terraform that all need the
// schemas, and it's typically quite expensive because it has to spin up
// plugins to gather their schemas, so it'd be good to have some caching
// here to remember plugin schemas we already loaded since the plugin
// selections can't change during the life of a *Context object.
var diags tfdiags.Diagnostics
ret, err := loadSchemas(config, state, c.plugins)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to load plugin schemas",
fmt.Sprintf("Error while loading schemas for plugin components: %s.", err),
))
return nil, diags
}
return ret, diags
}
type ContextGraphOpts struct {
// If true, validates the graph structure (checks for cycles).
Validate bool
// Legacy graphs only: won't prune the graph
Verbose bool
}
// Stop stops the running task.
//
// Stop will block until the task completes.
func (c *Context) Stop() {
log.Printf("[WARN] terraform: Stop called, initiating interrupt sequence")
c.l.Lock()
defer c.l.Unlock()
// If we're running, then stop
if c.runContextCancel != nil {
log.Printf("[WARN] terraform: run context exists, stopping")
// Tell the hook we want to stop
c.sh.Stop()
// Stop the context
c.runContextCancel()
c.runContextCancel = nil
}
// Grab the condition var before we exit
if cond := c.runCond; cond != nil {
log.Printf("[INFO] terraform: waiting for graceful stop to complete")
cond.Wait()
}
log.Printf("[WARN] terraform: stop complete")
}
func (c *Context) acquireRun(phase string) func() {
// With the run lock held, grab the context lock to make changes
// to the run context.
c.l.Lock()
defer c.l.Unlock()
// Wait until we're no longer running
for c.runCond != nil {
c.runCond.Wait()
}
// Build our lock
c.runCond = sync.NewCond(&c.l)
// Create a new run context
c.runContext, c.runContextCancel = context.WithCancel(context.Background())
// Reset the stop hook so we're not stopped
c.sh.Reset()
return c.releaseRun
}
func (c *Context) releaseRun() {
// Grab the context lock so that we can make modifications to fields
c.l.Lock()
defer c.l.Unlock()
// End our run. We check if runContext is non-nil because it can be
// set to nil if it was cancelled via Stop()
if c.runContextCancel != nil {
c.runContextCancel()
}
// Unlock all waiting our condition
cond := c.runCond
c.runCond = nil
cond.Broadcast()
// Unset the context
c.runContext = nil
}
// watchStop immediately returns a `stop` and a `wait` chan after dispatching
// the watchStop goroutine. This will watch the runContext for cancellation and
// stop the providers accordingly. When the watch is no longer needed, the
// `stop` chan should be closed before waiting on the `wait` chan.
// The `wait` chan is important, because without synchronizing with the end of
// the watchStop goroutine, the runContext may also be closed during the select
// incorrectly causing providers to be stopped. Even if the graph walk is done
// at that point, stopping a provider permanently cancels its StopContext which
// can cause later actions to fail.
func (c *Context) watchStop(walker *ContextGraphWalker) (chan struct{}, <-chan struct{}) {
stop := make(chan struct{})
wait := make(chan struct{})
// get the runContext cancellation channel now, because releaseRun will
// write to the runContext field.
done := c.runContext.Done()
go func() {
defer logging.PanicHandler()
defer close(wait)
// Wait for a stop or completion
select {
case <-done:
// done means the context was canceled, so we need to try and stop
// providers.
case <-stop:
// our own stop channel was closed.
return
}
// If we're here, we're stopped, trigger the call.
log.Printf("[TRACE] Context: requesting providers and provisioners to gracefully stop")
{
// Copy the providers so that a misbehaved blocking Stop doesn't
// completely hang Terraform.
walker.providerLock.Lock()
ps := make([]providers.Interface, 0, len(walker.providerCache))
for _, p := range walker.providerCache {
ps = append(ps, p)
}
defer walker.providerLock.Unlock()
for _, p := range ps {
// We ignore the error for now since there isn't any reasonable
// action to take if there is an error here, since the stop is still
// advisory: Terraform will exit once the graph node completes.
p.Stop()
}
}
{
// Call stop on all the provisioners
walker.provisionerLock.Lock()
ps := make([]provisioners.Interface, 0, len(walker.provisionerCache))
for _, p := range walker.provisionerCache {
ps = append(ps, p)
}
defer walker.provisionerLock.Unlock()
for _, p := range ps {
// We ignore the error for now since there isn't any reasonable
// action to take if there is an error here, since the stop is still
// advisory: Terraform will exit once the graph node completes.
p.Stop()
}
}
}()
return stop, wait
}
// checkConfigDependencies checks whether the recieving context is able to
// support the given configuration, returning error diagnostics if not.
//
// Currently this function checks whether the current Terraform CLI version
// matches the version requirements of all of the modules, and whether our
// plugin library contains all of the plugin names/addresses needed.
//
// This function does *not* check that external modules are installed (that's
// the responsibility of the configuration loader) and doesn't check that the
// plugins are of suitable versions to match any version constraints (which is
// the responsibility of the code which installed the plugins and then
// constructed the Providers/Provisioners maps passed in to NewContext).
//
// In most cases we should typically catch the problems this function detects
// before we reach this point, but this function can come into play in some
// unusual cases outside of the main workflow, and can avoid some
// potentially-more-confusing errors from later operations.
func (c *Context) checkConfigDependencies(config *configs.Config) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
// This checks the Terraform CLI version constraints specified in all of
// the modules.
diags = diags.Append(CheckCoreVersionRequirements(config))
// We only check that we have a factory for each required provider, and
// assume the caller already assured that any separately-installed
// plugins are of a suitable version, match expected checksums, etc.
providerReqs, hclDiags := config.ProviderRequirements()
diags = diags.Append(hclDiags)
if hclDiags.HasErrors() {
return diags
}
for providerAddr := range providerReqs {
if !c.plugins.HasProvider(providerAddr) {
if !providerAddr.IsBuiltIn() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Missing required provider",
fmt.Sprintf(
"This configuration requires provider %s, but that provider isn't available. You may be able to install it automatically by running:\n terraform init",
providerAddr,
),
))
} else {
// Built-in providers can never be installed by "terraform init",
// so no point in confusing the user by suggesting that.
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Missing required provider",
fmt.Sprintf(
"This configuration requires built-in provider %s, but that provider isn't available in this Terraform version.",
providerAddr,
),
))
}
}
}
// Our handling of provisioners is much less sophisticated than providers
// because they are in many ways a legacy system. We need to go hunting
// for them more directly in the configuration.
config.DeepEach(func(modCfg *configs.Config) {
if modCfg == nil || modCfg.Module == nil {
return // should not happen, but we'll be robust
}
for _, rc := range modCfg.Module.ManagedResources {
if rc.Managed == nil {
continue // should not happen, but we'll be robust
}
for _, pc := range rc.Managed.Provisioners {
if !c.plugins.HasProvisioner(pc.Type) {
// This is not a very high-quality error, because really
// the caller of terraform.NewContext should've already
// done equivalent checks when doing plugin discovery.
// This is just to make sure we return a predictable
// error in a central place, rather than failing somewhere
// later in the non-deterministically-ordered graph walk.
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Missing required provisioner plugin",
fmt.Sprintf(
"This configuration requires provisioner plugin %q, which isn't available. If you're intending to use an external provisioner plugin, you must install it manually into one of the plugin search directories before running Terraform.",
pc.Type,
),
))
}
}
}
})
// Because we were doing a lot of map iteration above, and we're only
// generating sourceless diagnostics anyway, our diagnostics will not be
// in a deterministic order. To ensure stable output when there are
// multiple errors to report, we'll sort these particular diagnostics
// so they are at least always consistent alone. This ordering is
// arbitrary and not a compatibility constraint.
sort.Slice(diags, func(i, j int) bool {
// Because these are sourcelss diagnostics and we know they are all
// errors, we know they'll only differ in their description fields.
descI := diags[i].Description()
descJ := diags[j].Description()
switch {
case descI.Summary != descJ.Summary:
return descI.Summary < descJ.Summary
default:
return descI.Detail < descJ.Detail
}
})
return diags
}