github.com/hugorut/terraform@v1.1.3/src/terraform/context.go (about)

     1  package terraform
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  	"sort"
     8  	"sync"
     9  
    10  	"github.com/hugorut/terraform/src/addrs"
    11  	"github.com/hugorut/terraform/src/configs"
    12  	"github.com/hugorut/terraform/src/logging"
    13  	"github.com/hugorut/terraform/src/providers"
    14  	"github.com/hugorut/terraform/src/provisioners"
    15  	"github.com/hugorut/terraform/src/states"
    16  	"github.com/hugorut/terraform/src/tfdiags"
    17  	"github.com/zclconf/go-cty/cty"
    18  
    19  	_ "github.com/hugorut/terraform/src/logging"
    20  )
    21  
    22  // InputMode defines what sort of input will be asked for when Input
    23  // is called on Context.
    24  type InputMode byte
    25  
    26  const (
    27  	// InputModeProvider asks for provider variables
    28  	InputModeProvider InputMode = 1 << iota
    29  
    30  	// InputModeStd is the standard operating mode and asks for both variables
    31  	// and providers.
    32  	InputModeStd = InputModeProvider
    33  )
    34  
    35  // ContextOpts are the user-configurable options to create a context with
    36  // NewContext.
    37  type ContextOpts struct {
    38  	Meta         *ContextMeta
    39  	Hooks        []Hook
    40  	Parallelism  int
    41  	Providers    map[addrs.Provider]providers.Factory
    42  	Provisioners map[string]provisioners.Factory
    43  
    44  	UIInput UIInput
    45  }
    46  
    47  // ContextMeta is metadata about the running context. This is information
    48  // that this package or structure cannot determine on its own but exposes
    49  // into Terraform in various ways. This must be provided by the Context
    50  // initializer.
    51  type ContextMeta struct {
    52  	Env string // Env is the state environment
    53  
    54  	// OriginalWorkingDir is the working directory where the Terraform CLI
    55  	// was run from, which may no longer actually be the current working
    56  	// directory if the user included the -chdir=... option.
    57  	//
    58  	// If this string is empty then the original working directory is the same
    59  	// as the current working directory.
    60  	//
    61  	// In most cases we should respect the user's override by ignoring this
    62  	// path and just using the current working directory, but this is here
    63  	// for some exceptional cases where the original working directory is
    64  	// needed.
    65  	OriginalWorkingDir string
    66  }
    67  
    68  // Context represents all the context that Terraform needs in order to
    69  // perform operations on infrastructure. This structure is built using
    70  // NewContext.
    71  type Context struct {
    72  	// meta captures some misc. information about the working directory where
    73  	// we're taking these actions, and thus which should remain steady between
    74  	// operations.
    75  	meta *ContextMeta
    76  
    77  	plugins *contextPlugins
    78  
    79  	hooks   []Hook
    80  	sh      *stopHook
    81  	uiInput UIInput
    82  
    83  	l                   sync.Mutex // Lock acquired during any task
    84  	parallelSem         Semaphore
    85  	providerInputConfig map[string]map[string]cty.Value
    86  	runCond             *sync.Cond
    87  	runContext          context.Context
    88  	runContextCancel    context.CancelFunc
    89  }
    90  
    91  // (additional methods on Context can be found in context_*.go files.)
    92  
    93  // NewContext creates a new Context structure.
    94  //
    95  // Once a Context is created, the caller must not access or mutate any of
    96  // the objects referenced (directly or indirectly) by the ContextOpts fields.
    97  //
    98  // If the returned diagnostics contains errors then the resulting context is
    99  // invalid and must not be used.
   100  func NewContext(opts *ContextOpts) (*Context, tfdiags.Diagnostics) {
   101  	var diags tfdiags.Diagnostics
   102  
   103  	log.Printf("[TRACE] terraform.NewContext: starting")
   104  
   105  	// Copy all the hooks and add our stop hook. We don't append directly
   106  	// to the Config so that we're not modifying that in-place.
   107  	sh := new(stopHook)
   108  	hooks := make([]Hook, len(opts.Hooks)+1)
   109  	copy(hooks, opts.Hooks)
   110  	hooks[len(opts.Hooks)] = sh
   111  
   112  	// Determine parallelism, default to 10. We do this both to limit
   113  	// CPU pressure but also to have an extra guard against rate throttling
   114  	// from providers.
   115  	// We throw an error in case of negative parallelism
   116  	par := opts.Parallelism
   117  	if par < 0 {
   118  		diags = diags.Append(tfdiags.Sourceless(
   119  			tfdiags.Error,
   120  			"Invalid parallelism value",
   121  			fmt.Sprintf("The parallelism must be a positive value. Not %d.", par),
   122  		))
   123  		return nil, diags
   124  	}
   125  
   126  	if par == 0 {
   127  		par = 10
   128  	}
   129  
   130  	plugins := newContextPlugins(opts.Providers, opts.Provisioners)
   131  
   132  	log.Printf("[TRACE] terraform.NewContext: complete")
   133  
   134  	return &Context{
   135  		hooks:   hooks,
   136  		meta:    opts.Meta,
   137  		uiInput: opts.UIInput,
   138  
   139  		plugins: plugins,
   140  
   141  		parallelSem:         NewSemaphore(par),
   142  		providerInputConfig: make(map[string]map[string]cty.Value),
   143  		sh:                  sh,
   144  	}, diags
   145  }
   146  
   147  func (c *Context) Schemas(config *configs.Config, state *states.State) (*Schemas, tfdiags.Diagnostics) {
   148  	// TODO: This method gets called multiple times on the same context with
   149  	// the same inputs by different parts of Terraform that all need the
   150  	// schemas, and it's typically quite expensive because it has to spin up
   151  	// plugins to gather their schemas, so it'd be good to have some caching
   152  	// here to remember plugin schemas we already loaded since the plugin
   153  	// selections can't change during the life of a *Context object.
   154  
   155  	var diags tfdiags.Diagnostics
   156  
   157  	ret, err := loadSchemas(config, state, c.plugins)
   158  	if err != nil {
   159  		diags = diags.Append(tfdiags.Sourceless(
   160  			tfdiags.Error,
   161  			"Failed to load plugin schemas",
   162  			fmt.Sprintf("Error while loading schemas for plugin components: %s.", err),
   163  		))
   164  		return nil, diags
   165  	}
   166  	return ret, diags
   167  }
   168  
   169  type ContextGraphOpts struct {
   170  	// If true, validates the graph structure (checks for cycles).
   171  	Validate bool
   172  
   173  	// Legacy graphs only: won't prune the graph
   174  	Verbose bool
   175  }
   176  
   177  // Stop stops the running task.
   178  //
   179  // Stop will block until the task completes.
   180  func (c *Context) Stop() {
   181  	log.Printf("[WARN] terraform: Stop called, initiating interrupt sequence")
   182  
   183  	c.l.Lock()
   184  	defer c.l.Unlock()
   185  
   186  	// If we're running, then stop
   187  	if c.runContextCancel != nil {
   188  		log.Printf("[WARN] terraform: run context exists, stopping")
   189  
   190  		// Tell the hook we want to stop
   191  		c.sh.Stop()
   192  
   193  		// Stop the context
   194  		c.runContextCancel()
   195  		c.runContextCancel = nil
   196  	}
   197  
   198  	// Grab the condition var before we exit
   199  	if cond := c.runCond; cond != nil {
   200  		log.Printf("[INFO] terraform: waiting for graceful stop to complete")
   201  		cond.Wait()
   202  	}
   203  
   204  	log.Printf("[WARN] terraform: stop complete")
   205  }
   206  
   207  func (c *Context) acquireRun(phase string) func() {
   208  	// With the run lock held, grab the context lock to make changes
   209  	// to the run context.
   210  	c.l.Lock()
   211  	defer c.l.Unlock()
   212  
   213  	// Wait until we're no longer running
   214  	for c.runCond != nil {
   215  		c.runCond.Wait()
   216  	}
   217  
   218  	// Build our lock
   219  	c.runCond = sync.NewCond(&c.l)
   220  
   221  	// Create a new run context
   222  	c.runContext, c.runContextCancel = context.WithCancel(context.Background())
   223  
   224  	// Reset the stop hook so we're not stopped
   225  	c.sh.Reset()
   226  
   227  	return c.releaseRun
   228  }
   229  
   230  func (c *Context) releaseRun() {
   231  	// Grab the context lock so that we can make modifications to fields
   232  	c.l.Lock()
   233  	defer c.l.Unlock()
   234  
   235  	// End our run. We check if runContext is non-nil because it can be
   236  	// set to nil if it was cancelled via Stop()
   237  	if c.runContextCancel != nil {
   238  		c.runContextCancel()
   239  	}
   240  
   241  	// Unlock all waiting our condition
   242  	cond := c.runCond
   243  	c.runCond = nil
   244  	cond.Broadcast()
   245  
   246  	// Unset the context
   247  	c.runContext = nil
   248  }
   249  
   250  // watchStop immediately returns a `stop` and a `wait` chan after dispatching
   251  // the watchStop goroutine. This will watch the runContext for cancellation and
   252  // stop the providers accordingly.  When the watch is no longer needed, the
   253  // `stop` chan should be closed before waiting on the `wait` chan.
   254  // The `wait` chan is important, because without synchronizing with the end of
   255  // the watchStop goroutine, the runContext may also be closed during the select
   256  // incorrectly causing providers to be stopped. Even if the graph walk is done
   257  // at that point, stopping a provider permanently cancels its StopContext which
   258  // can cause later actions to fail.
   259  func (c *Context) watchStop(walker *ContextGraphWalker) (chan struct{}, <-chan struct{}) {
   260  	stop := make(chan struct{})
   261  	wait := make(chan struct{})
   262  
   263  	// get the runContext cancellation channel now, because releaseRun will
   264  	// write to the runContext field.
   265  	done := c.runContext.Done()
   266  
   267  	go func() {
   268  		defer logging.PanicHandler()
   269  
   270  		defer close(wait)
   271  		// Wait for a stop or completion
   272  		select {
   273  		case <-done:
   274  			// done means the context was canceled, so we need to try and stop
   275  			// providers.
   276  		case <-stop:
   277  			// our own stop channel was closed.
   278  			return
   279  		}
   280  
   281  		// If we're here, we're stopped, trigger the call.
   282  		log.Printf("[TRACE] Context: requesting providers and provisioners to gracefully stop")
   283  
   284  		{
   285  			// Copy the providers so that a misbehaved blocking Stop doesn't
   286  			// completely hang Terraform.
   287  			walker.providerLock.Lock()
   288  			ps := make([]providers.Interface, 0, len(walker.providerCache))
   289  			for _, p := range walker.providerCache {
   290  				ps = append(ps, p)
   291  			}
   292  			defer walker.providerLock.Unlock()
   293  
   294  			for _, p := range ps {
   295  				// We ignore the error for now since there isn't any reasonable
   296  				// action to take if there is an error here, since the stop is still
   297  				// advisory: Terraform will exit once the graph node completes.
   298  				p.Stop()
   299  			}
   300  		}
   301  
   302  		{
   303  			// Call stop on all the provisioners
   304  			walker.provisionerLock.Lock()
   305  			ps := make([]provisioners.Interface, 0, len(walker.provisionerCache))
   306  			for _, p := range walker.provisionerCache {
   307  				ps = append(ps, p)
   308  			}
   309  			defer walker.provisionerLock.Unlock()
   310  
   311  			for _, p := range ps {
   312  				// We ignore the error for now since there isn't any reasonable
   313  				// action to take if there is an error here, since the stop is still
   314  				// advisory: Terraform will exit once the graph node completes.
   315  				p.Stop()
   316  			}
   317  		}
   318  	}()
   319  
   320  	return stop, wait
   321  }
   322  
   323  // checkConfigDependencies checks whether the recieving context is able to
   324  // support the given configuration, returning error diagnostics if not.
   325  //
   326  // Currently this function checks whether the current Terraform CLI version
   327  // matches the version requirements of all of the modules, and whether our
   328  // plugin library contains all of the plugin names/addresses needed.
   329  //
   330  // This function does *not* check that external modules are installed (that's
   331  // the responsibility of the configuration loader) and doesn't check that the
   332  // plugins are of suitable versions to match any version constraints (which is
   333  // the responsibility of the code which installed the plugins and then
   334  // constructed the Providers/Provisioners maps passed in to NewContext).
   335  //
   336  // In most cases we should typically catch the problems this function detects
   337  // before we reach this point, but this function can come into play in some
   338  // unusual cases outside of the main workflow, and can avoid some
   339  // potentially-more-confusing errors from later operations.
   340  func (c *Context) checkConfigDependencies(config *configs.Config) tfdiags.Diagnostics {
   341  	var diags tfdiags.Diagnostics
   342  
   343  	// This checks the Terraform CLI version constraints specified in all of
   344  	// the modules.
   345  	diags = diags.Append(CheckCoreVersionRequirements(config))
   346  
   347  	// We only check that we have a factory for each required provider, and
   348  	// assume the caller already assured that any separately-installed
   349  	// plugins are of a suitable version, match expected checksums, etc.
   350  	providerReqs, hclDiags := config.ProviderRequirements()
   351  	diags = diags.Append(hclDiags)
   352  	if hclDiags.HasErrors() {
   353  		return diags
   354  	}
   355  	for providerAddr := range providerReqs {
   356  		if !c.plugins.HasProvider(providerAddr) {
   357  			if !providerAddr.IsBuiltIn() {
   358  				diags = diags.Append(tfdiags.Sourceless(
   359  					tfdiags.Error,
   360  					"Missing required provider",
   361  					fmt.Sprintf(
   362  						"This configuration requires provider %s, but that provider isn't available. You may be able to install it automatically by running:\n  terraform init",
   363  						providerAddr,
   364  					),
   365  				))
   366  			} else {
   367  				// Built-in providers can never be installed by "terraform init",
   368  				// so no point in confusing the user by suggesting that.
   369  				diags = diags.Append(tfdiags.Sourceless(
   370  					tfdiags.Error,
   371  					"Missing required provider",
   372  					fmt.Sprintf(
   373  						"This configuration requires built-in provider %s, but that provider isn't available in this Terraform version.",
   374  						providerAddr,
   375  					),
   376  				))
   377  			}
   378  		}
   379  	}
   380  
   381  	// Our handling of provisioners is much less sophisticated than providers
   382  	// because they are in many ways a legacy system. We need to go hunting
   383  	// for them more directly in the configuration.
   384  	config.DeepEach(func(modCfg *configs.Config) {
   385  		if modCfg == nil || modCfg.Module == nil {
   386  			return // should not happen, but we'll be robust
   387  		}
   388  		for _, rc := range modCfg.Module.ManagedResources {
   389  			if rc.Managed == nil {
   390  				continue // should not happen, but we'll be robust
   391  			}
   392  			for _, pc := range rc.Managed.Provisioners {
   393  				if !c.plugins.HasProvisioner(pc.Type) {
   394  					// This is not a very high-quality error, because really
   395  					// the caller of terraform.NewContext should've already
   396  					// done equivalent checks when doing plugin discovery.
   397  					// This is just to make sure we return a predictable
   398  					// error in a central place, rather than failing somewhere
   399  					// later in the non-deterministically-ordered graph walk.
   400  					diags = diags.Append(tfdiags.Sourceless(
   401  						tfdiags.Error,
   402  						"Missing required provisioner plugin",
   403  						fmt.Sprintf(
   404  							"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.",
   405  							pc.Type,
   406  						),
   407  					))
   408  				}
   409  			}
   410  		}
   411  	})
   412  
   413  	// Because we were doing a lot of map iteration above, and we're only
   414  	// generating sourceless diagnostics anyway, our diagnostics will not be
   415  	// in a deterministic order. To ensure stable output when there are
   416  	// multiple errors to report, we'll sort these particular diagnostics
   417  	// so they are at least always consistent alone. This ordering is
   418  	// arbitrary and not a compatibility constraint.
   419  	sort.Slice(diags, func(i, j int) bool {
   420  		// Because these are sourcelss diagnostics and we know they are all
   421  		// errors, we know they'll only differ in their description fields.
   422  		descI := diags[i].Description()
   423  		descJ := diags[j].Description()
   424  		switch {
   425  		case descI.Summary != descJ.Summary:
   426  			return descI.Summary < descJ.Summary
   427  		default:
   428  			return descI.Detail < descJ.Detail
   429  		}
   430  	})
   431  
   432  	return diags
   433  }