kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/terraform/context.go (about)

     1  package terraform
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  	"strings"
     8  	"sync"
     9  
    10  	"github.com/apparentlymart/go-versions/versions"
    11  	"kubeform.dev/terraform-backend-sdk/addrs"
    12  	"kubeform.dev/terraform-backend-sdk/configs"
    13  	"kubeform.dev/terraform-backend-sdk/providers"
    14  	"kubeform.dev/terraform-backend-sdk/provisioners"
    15  	"kubeform.dev/terraform-backend-sdk/states"
    16  	"kubeform.dev/terraform-backend-sdk/tfdiags"
    17  	"github.com/zclconf/go-cty/cty"
    18  
    19  	"kubeform.dev/terraform-backend-sdk/depsfile"
    20  	"kubeform.dev/terraform-backend-sdk/getproviders"
    21  	_ "kubeform.dev/terraform-backend-sdk/logging"
    22  )
    23  
    24  // InputMode defines what sort of input will be asked for when Input
    25  // is called on Context.
    26  type InputMode byte
    27  
    28  const (
    29  	// InputModeProvider asks for provider variables
    30  	InputModeProvider InputMode = 1 << iota
    31  
    32  	// InputModeStd is the standard operating mode and asks for both variables
    33  	// and providers.
    34  	InputModeStd = InputModeProvider
    35  )
    36  
    37  // ContextOpts are the user-configurable options to create a context with
    38  // NewContext.
    39  type ContextOpts struct {
    40  	Meta         *ContextMeta
    41  	Hooks        []Hook
    42  	Parallelism  int
    43  	Providers    map[addrs.Provider]providers.Factory
    44  	Provisioners map[string]provisioners.Factory
    45  
    46  	// If non-nil, will apply as additional constraints on the provider
    47  	// plugins that will be requested from the provider resolver.
    48  	ProviderSHA256s map[string][]byte
    49  
    50  	// If non-nil, will be verified to ensure that provider requirements from
    51  	// configuration can be satisfied by the set of locked dependencies.
    52  	LockedDependencies *depsfile.Locks
    53  
    54  	// Set of providers to exclude from the requirements check process, as they
    55  	// are marked as in local development.
    56  	ProvidersInDevelopment map[addrs.Provider]struct{}
    57  
    58  	UIInput UIInput
    59  }
    60  
    61  // ContextMeta is metadata about the running context. This is information
    62  // that this package or structure cannot determine on its own but exposes
    63  // into Terraform in various ways. This must be provided by the Context
    64  // initializer.
    65  type ContextMeta struct {
    66  	Env string // Env is the state environment
    67  
    68  	// OriginalWorkingDir is the working directory where the Terraform CLI
    69  	// was run from, which may no longer actually be the current working
    70  	// directory if the user included the -chdir=... option.
    71  	//
    72  	// If this string is empty then the original working directory is the same
    73  	// as the current working directory.
    74  	//
    75  	// In most cases we should respect the user's override by ignoring this
    76  	// path and just using the current working directory, but this is here
    77  	// for some exceptional cases where the original working directory is
    78  	// needed.
    79  	OriginalWorkingDir string
    80  }
    81  
    82  // Context represents all the context that Terraform needs in order to
    83  // perform operations on infrastructure. This structure is built using
    84  // NewContext.
    85  type Context struct {
    86  	// meta captures some misc. information about the working directory where
    87  	// we're taking these actions, and thus which should remain steady between
    88  	// operations.
    89  	meta *ContextMeta
    90  
    91  	plugins                *contextPlugins
    92  	dependencyLocks        *depsfile.Locks
    93  	providersInDevelopment map[addrs.Provider]struct{}
    94  
    95  	hooks   []Hook
    96  	sh      *stopHook
    97  	uiInput UIInput
    98  
    99  	l                   sync.Mutex // Lock acquired during any task
   100  	parallelSem         Semaphore
   101  	providerInputConfig map[string]map[string]cty.Value
   102  	providerSHA256s     map[string][]byte
   103  	runCond             *sync.Cond
   104  	runContext          context.Context
   105  	runContextCancel    context.CancelFunc
   106  }
   107  
   108  // (additional methods on Context can be found in context_*.go files.)
   109  
   110  // NewContext creates a new Context structure.
   111  //
   112  // Once a Context is created, the caller must not access or mutate any of
   113  // the objects referenced (directly or indirectly) by the ContextOpts fields.
   114  //
   115  // If the returned diagnostics contains errors then the resulting context is
   116  // invalid and must not be used.
   117  func NewContext(opts *ContextOpts) (*Context, tfdiags.Diagnostics) {
   118  	var diags tfdiags.Diagnostics
   119  
   120  	log.Printf("[TRACE] terraform.NewContext: starting")
   121  
   122  	// Copy all the hooks and add our stop hook. We don't append directly
   123  	// to the Config so that we're not modifying that in-place.
   124  	sh := new(stopHook)
   125  	hooks := make([]Hook, len(opts.Hooks)+1)
   126  	copy(hooks, opts.Hooks)
   127  	hooks[len(opts.Hooks)] = sh
   128  
   129  	// Determine parallelism, default to 10. We do this both to limit
   130  	// CPU pressure but also to have an extra guard against rate throttling
   131  	// from providers.
   132  	// We throw an error in case of negative parallelism
   133  	par := opts.Parallelism
   134  	if par < 0 {
   135  		diags = diags.Append(tfdiags.Sourceless(
   136  			tfdiags.Error,
   137  			"Invalid parallelism value",
   138  			fmt.Sprintf("The parallelism must be a positive value. Not %d.", par),
   139  		))
   140  		return nil, diags
   141  	}
   142  
   143  	if par == 0 {
   144  		par = 10
   145  	}
   146  
   147  	plugins := newContextPlugins(opts.Providers, opts.Provisioners)
   148  
   149  	log.Printf("[TRACE] terraform.NewContext: complete")
   150  
   151  	return &Context{
   152  		hooks:   hooks,
   153  		meta:    opts.Meta,
   154  		uiInput: opts.UIInput,
   155  
   156  		plugins:                plugins,
   157  		dependencyLocks:        opts.LockedDependencies,
   158  		providersInDevelopment: opts.ProvidersInDevelopment,
   159  
   160  		parallelSem:         NewSemaphore(par),
   161  		providerInputConfig: make(map[string]map[string]cty.Value),
   162  		providerSHA256s:     opts.ProviderSHA256s,
   163  		sh:                  sh,
   164  	}, diags
   165  }
   166  
   167  func (c *Context) Schemas(config *configs.Config, state *states.State) (*Schemas, tfdiags.Diagnostics) {
   168  	// TODO: This method gets called multiple times on the same context with
   169  	// the same inputs by different parts of Terraform that all need the
   170  	// schemas, and it's typically quite expensive because it has to spin up
   171  	// plugins to gather their schemas, so it'd be good to have some caching
   172  	// here to remember plugin schemas we already loaded since the plugin
   173  	// selections can't change during the life of a *Context object.
   174  
   175  	var diags tfdiags.Diagnostics
   176  
   177  	// If we have a configuration and a set of locked dependencies, verify that
   178  	// the provider requirements from the configuration can be satisfied by the
   179  	// locked dependencies.
   180  	if c.dependencyLocks != nil && config != nil {
   181  		reqs, providerDiags := config.ProviderRequirements()
   182  		diags = diags.Append(providerDiags)
   183  
   184  		locked := c.dependencyLocks.AllProviders()
   185  		unmetReqs := make(getproviders.Requirements)
   186  		for provider, versionConstraints := range reqs {
   187  			// Builtin providers are not listed in the locks file
   188  			if provider.IsBuiltIn() {
   189  				continue
   190  			}
   191  			// Development providers must be excluded from this check
   192  			if _, ok := c.providersInDevelopment[provider]; ok {
   193  				continue
   194  			}
   195  			// If the required provider doesn't exist in the lock, or the
   196  			// locked version doesn't meet the constraints, mark the
   197  			// requirement unmet
   198  			acceptable := versions.MeetingConstraints(versionConstraints)
   199  			if lock, ok := locked[provider]; !ok || !acceptable.Has(lock.Version()) {
   200  				unmetReqs[provider] = versionConstraints
   201  			}
   202  		}
   203  
   204  		if len(unmetReqs) > 0 {
   205  			var buf strings.Builder
   206  			for provider, versionConstraints := range unmetReqs {
   207  				fmt.Fprintf(&buf, "\n- %s", provider)
   208  				if len(versionConstraints) > 0 {
   209  					fmt.Fprintf(&buf, " (%s)", getproviders.VersionConstraintsString(versionConstraints))
   210  				}
   211  			}
   212  			diags = diags.Append(tfdiags.Sourceless(
   213  				tfdiags.Error,
   214  				"Provider requirements cannot be satisfied by locked dependencies",
   215  				fmt.Sprintf("The following required providers are not installed:\n%s\n\nPlease run \"terraform init\".", buf.String()),
   216  			))
   217  			return nil, diags
   218  		}
   219  	}
   220  
   221  	ret, err := loadSchemas(config, state, c.plugins)
   222  	if err != nil {
   223  		diags = diags.Append(tfdiags.Sourceless(
   224  			tfdiags.Error,
   225  			"Failed to load plugin schemas",
   226  			fmt.Sprintf("Error while loading schemas for plugin components: %s.", err),
   227  		))
   228  		return nil, diags
   229  	}
   230  	return ret, diags
   231  }
   232  
   233  type ContextGraphOpts struct {
   234  	// If true, validates the graph structure (checks for cycles).
   235  	Validate bool
   236  
   237  	// Legacy graphs only: won't prune the graph
   238  	Verbose bool
   239  }
   240  
   241  // Stop stops the running task.
   242  //
   243  // Stop will block until the task completes.
   244  func (c *Context) Stop() {
   245  	log.Printf("[WARN] terraform: Stop called, initiating interrupt sequence")
   246  
   247  	c.l.Lock()
   248  	defer c.l.Unlock()
   249  
   250  	// If we're running, then stop
   251  	if c.runContextCancel != nil {
   252  		log.Printf("[WARN] terraform: run context exists, stopping")
   253  
   254  		// Tell the hook we want to stop
   255  		c.sh.Stop()
   256  
   257  		// Stop the context
   258  		c.runContextCancel()
   259  		c.runContextCancel = nil
   260  	}
   261  
   262  	// Grab the condition var before we exit
   263  	if cond := c.runCond; cond != nil {
   264  		log.Printf("[INFO] terraform: waiting for graceful stop to complete")
   265  		cond.Wait()
   266  	}
   267  
   268  	log.Printf("[WARN] terraform: stop complete")
   269  }
   270  
   271  func (c *Context) acquireRun(phase string) func() {
   272  	// With the run lock held, grab the context lock to make changes
   273  	// to the run context.
   274  	c.l.Lock()
   275  	defer c.l.Unlock()
   276  
   277  	// Wait until we're no longer running
   278  	for c.runCond != nil {
   279  		c.runCond.Wait()
   280  	}
   281  
   282  	// Build our lock
   283  	c.runCond = sync.NewCond(&c.l)
   284  
   285  	// Create a new run context
   286  	c.runContext, c.runContextCancel = context.WithCancel(context.Background())
   287  
   288  	// Reset the stop hook so we're not stopped
   289  	c.sh.Reset()
   290  
   291  	return c.releaseRun
   292  }
   293  
   294  func (c *Context) releaseRun() {
   295  	// Grab the context lock so that we can make modifications to fields
   296  	c.l.Lock()
   297  	defer c.l.Unlock()
   298  
   299  	// End our run. We check if runContext is non-nil because it can be
   300  	// set to nil if it was cancelled via Stop()
   301  	if c.runContextCancel != nil {
   302  		c.runContextCancel()
   303  	}
   304  
   305  	// Unlock all waiting our condition
   306  	cond := c.runCond
   307  	c.runCond = nil
   308  	cond.Broadcast()
   309  
   310  	// Unset the context
   311  	c.runContext = nil
   312  }
   313  
   314  // watchStop immediately returns a `stop` and a `wait` chan after dispatching
   315  // the watchStop goroutine. This will watch the runContext for cancellation and
   316  // stop the providers accordingly.  When the watch is no longer needed, the
   317  // `stop` chan should be closed before waiting on the `wait` chan.
   318  // The `wait` chan is important, because without synchronizing with the end of
   319  // the watchStop goroutine, the runContext may also be closed during the select
   320  // incorrectly causing providers to be stopped. Even if the graph walk is done
   321  // at that point, stopping a provider permanently cancels its StopContext which
   322  // can cause later actions to fail.
   323  func (c *Context) watchStop(walker *ContextGraphWalker) (chan struct{}, <-chan struct{}) {
   324  	stop := make(chan struct{})
   325  	wait := make(chan struct{})
   326  
   327  	// get the runContext cancellation channel now, because releaseRun will
   328  	// write to the runContext field.
   329  	done := c.runContext.Done()
   330  
   331  	go func() {
   332  		defer close(wait)
   333  		// Wait for a stop or completion
   334  		select {
   335  		case <-done:
   336  			// done means the context was canceled, so we need to try and stop
   337  			// providers.
   338  		case <-stop:
   339  			// our own stop channel was closed.
   340  			return
   341  		}
   342  
   343  		// If we're here, we're stopped, trigger the call.
   344  		log.Printf("[TRACE] Context: requesting providers and provisioners to gracefully stop")
   345  
   346  		{
   347  			// Copy the providers so that a misbehaved blocking Stop doesn't
   348  			// completely hang Terraform.
   349  			walker.providerLock.Lock()
   350  			ps := make([]providers.Interface, 0, len(walker.providerCache))
   351  			for _, p := range walker.providerCache {
   352  				ps = append(ps, p)
   353  			}
   354  			defer walker.providerLock.Unlock()
   355  
   356  			for _, p := range ps {
   357  				// We ignore the error for now since there isn't any reasonable
   358  				// action to take if there is an error here, since the stop is still
   359  				// advisory: Terraform will exit once the graph node completes.
   360  				p.Stop()
   361  			}
   362  		}
   363  
   364  		{
   365  			// Call stop on all the provisioners
   366  			walker.provisionerLock.Lock()
   367  			ps := make([]provisioners.Interface, 0, len(walker.provisionerCache))
   368  			for _, p := range walker.provisionerCache {
   369  				ps = append(ps, p)
   370  			}
   371  			defer walker.provisionerLock.Unlock()
   372  
   373  			for _, p := range ps {
   374  				// We ignore the error for now since there isn't any reasonable
   375  				// action to take if there is an error here, since the stop is still
   376  				// advisory: Terraform will exit once the graph node completes.
   377  				p.Stop()
   378  			}
   379  		}
   380  	}()
   381  
   382  	return stop, wait
   383  }