github.com/opentofu/opentofu@v1.7.1/internal/tofu/context.go (about)

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