github.com/svenhamers/terraform@v0.11.12-beta1/command/meta.go (about)

     1  package command
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"errors"
     8  	"flag"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"log"
    13  	"os"
    14  	"path/filepath"
    15  	"sort"
    16  	"strconv"
    17  	"strings"
    18  	"time"
    19  
    20  	"github.com/hashicorp/terraform/backend"
    21  	"github.com/hashicorp/terraform/backend/local"
    22  	"github.com/hashicorp/terraform/command/format"
    23  	"github.com/hashicorp/terraform/config"
    24  	"github.com/hashicorp/terraform/config/module"
    25  	"github.com/hashicorp/terraform/helper/experiment"
    26  	"github.com/hashicorp/terraform/helper/variables"
    27  	"github.com/hashicorp/terraform/helper/wrappedstreams"
    28  	"github.com/hashicorp/terraform/svchost/disco"
    29  	"github.com/hashicorp/terraform/terraform"
    30  	"github.com/hashicorp/terraform/tfdiags"
    31  	"github.com/mitchellh/cli"
    32  	"github.com/mitchellh/colorstring"
    33  )
    34  
    35  // Meta are the meta-options that are available on all or most commands.
    36  type Meta struct {
    37  	// The exported fields below should be set by anyone using a
    38  	// command with a Meta field. These are expected to be set externally
    39  	// (not from within the command itself).
    40  
    41  	Color            bool             // True if output should be colored
    42  	GlobalPluginDirs []string         // Additional paths to search for plugins
    43  	PluginOverrides  *PluginOverrides // legacy overrides from .terraformrc file
    44  	Ui               cli.Ui           // Ui for output
    45  
    46  	// ExtraHooks are extra hooks to add to the context.
    47  	ExtraHooks []terraform.Hook
    48  
    49  	// Services provides access to remote endpoint information for
    50  	// "terraform-native' services running at a specific user-facing hostname.
    51  	Services *disco.Disco
    52  
    53  	// RunningInAutomation indicates that commands are being run by an
    54  	// automated system rather than directly at a command prompt.
    55  	//
    56  	// This is a hint to various command routines that it may be confusing
    57  	// to print out messages that suggest running specific follow-up
    58  	// commands, since the user consuming the output will not be
    59  	// in a position to run such commands.
    60  	//
    61  	// The intended use-case of this flag is when Terraform is running in
    62  	// some sort of workflow orchestration tool which is abstracting away
    63  	// the specific commands being run.
    64  	RunningInAutomation bool
    65  
    66  	// PluginCacheDir, if non-empty, enables caching of downloaded plugins
    67  	// into the given directory.
    68  	PluginCacheDir string
    69  
    70  	// OverrideDataDir, if non-empty, overrides the return value of the
    71  	// DataDir method for situations where the local .terraform/ directory
    72  	// is not suitable, e.g. because of a read-only filesystem.
    73  	OverrideDataDir string
    74  
    75  	// When this channel is closed, the command will be cancelled.
    76  	ShutdownCh <-chan struct{}
    77  
    78  	//----------------------------------------------------------
    79  	// Protected: commands can set these
    80  	//----------------------------------------------------------
    81  
    82  	// Modify the data directory location. This should be accessed through the
    83  	// DataDir method.
    84  	dataDir string
    85  
    86  	// pluginPath is a user defined set of directories to look for plugins.
    87  	// This is set during init with the `-plugin-dir` flag, saved to a file in
    88  	// the data directory.
    89  	// This overrides all other search paths when discovering plugins.
    90  	pluginPath []string
    91  
    92  	ignorePluginChecksum bool
    93  
    94  	// Override certain behavior for tests within this package
    95  	testingOverrides *testingOverrides
    96  
    97  	//----------------------------------------------------------
    98  	// Private: do not set these
    99  	//----------------------------------------------------------
   100  
   101  	// backendState is the currently active backend state
   102  	backendState *terraform.BackendState
   103  
   104  	// Variables for the context (private)
   105  	autoKey       string
   106  	autoVariables map[string]interface{}
   107  	input         bool
   108  	variables     map[string]interface{}
   109  
   110  	// Targets for this context (private)
   111  	targets []string
   112  
   113  	// Internal fields
   114  	color bool
   115  	oldUi cli.Ui
   116  
   117  	// The fields below are expected to be set by the command via
   118  	// command line flags. See the Apply command for an example.
   119  	//
   120  	// statePath is the path to the state file. If this is empty, then
   121  	// no state will be loaded. It is also okay for this to be a path to
   122  	// a file that doesn't exist; it is assumed that this means that there
   123  	// is simply no state.
   124  	//
   125  	// stateOutPath is used to override the output path for the state.
   126  	// If not provided, the StatePath is used causing the old state to
   127  	// be overriden.
   128  	//
   129  	// backupPath is used to backup the state file before writing a modified
   130  	// version. It defaults to stateOutPath + DefaultBackupExtension
   131  	//
   132  	// parallelism is used to control the number of concurrent operations
   133  	// allowed when walking the graph
   134  	//
   135  	// shadow is used to enable/disable the shadow graph
   136  	//
   137  	// provider is to specify specific resource providers
   138  	//
   139  	// stateLock is set to false to disable state locking
   140  	//
   141  	// stateLockTimeout is the optional duration to retry a state locks locks
   142  	// when it is already locked by another process.
   143  	//
   144  	// forceInitCopy suppresses confirmation for copying state data during
   145  	// init.
   146  	//
   147  	// reconfigure forces init to ignore any stored configuration.
   148  	statePath        string
   149  	stateOutPath     string
   150  	backupPath       string
   151  	parallelism      int
   152  	shadow           bool
   153  	provider         string
   154  	stateLock        bool
   155  	stateLockTimeout time.Duration
   156  	forceInitCopy    bool
   157  	reconfigure      bool
   158  
   159  	// Used with the import command to allow import of state when no matching config exists.
   160  	allowMissingConfig bool
   161  }
   162  
   163  type PluginOverrides struct {
   164  	Providers    map[string]string
   165  	Provisioners map[string]string
   166  }
   167  
   168  type testingOverrides struct {
   169  	ProviderResolver terraform.ResourceProviderResolver
   170  	Provisioners     map[string]terraform.ResourceProvisionerFactory
   171  }
   172  
   173  // initStatePaths is used to initialize the default values for
   174  // statePath, stateOutPath, and backupPath
   175  func (m *Meta) initStatePaths() {
   176  	if m.statePath == "" {
   177  		m.statePath = DefaultStateFilename
   178  	}
   179  	if m.stateOutPath == "" {
   180  		m.stateOutPath = m.statePath
   181  	}
   182  	if m.backupPath == "" {
   183  		m.backupPath = m.stateOutPath + DefaultBackupExtension
   184  	}
   185  }
   186  
   187  // StateOutPath returns the true output path for the state file
   188  func (m *Meta) StateOutPath() string {
   189  	return m.stateOutPath
   190  }
   191  
   192  // Colorize returns the colorization structure for a command.
   193  func (m *Meta) Colorize() *colorstring.Colorize {
   194  	return &colorstring.Colorize{
   195  		Colors:  colorstring.DefaultColors,
   196  		Disable: !m.color,
   197  		Reset:   true,
   198  	}
   199  }
   200  
   201  // DataDir returns the directory where local data will be stored.
   202  // Defaults to DefaultDataDir in the current working directory.
   203  func (m *Meta) DataDir() string {
   204  	if m.OverrideDataDir != "" {
   205  		return m.OverrideDataDir
   206  	}
   207  	return DefaultDataDir
   208  }
   209  
   210  const (
   211  	// InputModeEnvVar is the environment variable that, if set to "false" or
   212  	// "0", causes terraform commands to behave as if the `-input=false` flag was
   213  	// specified.
   214  	InputModeEnvVar = "TF_INPUT"
   215  )
   216  
   217  // InputMode returns the type of input we should ask for in the form of
   218  // terraform.InputMode which is passed directly to Context.Input.
   219  func (m *Meta) InputMode() terraform.InputMode {
   220  	if test || !m.input {
   221  		return 0
   222  	}
   223  
   224  	if envVar := os.Getenv(InputModeEnvVar); envVar != "" {
   225  		if v, err := strconv.ParseBool(envVar); err == nil {
   226  			if !v {
   227  				return 0
   228  			}
   229  		}
   230  	}
   231  
   232  	var mode terraform.InputMode
   233  	mode |= terraform.InputModeProvider
   234  	mode |= terraform.InputModeVar
   235  	mode |= terraform.InputModeVarUnset
   236  
   237  	return mode
   238  }
   239  
   240  // UIInput returns a UIInput object to be used for asking for input.
   241  func (m *Meta) UIInput() terraform.UIInput {
   242  	return &UIInput{
   243  		Colorize: m.Colorize(),
   244  	}
   245  }
   246  
   247  // StdinPiped returns true if the input is piped.
   248  func (m *Meta) StdinPiped() bool {
   249  	fi, err := wrappedstreams.Stdin().Stat()
   250  	if err != nil {
   251  		// If there is an error, let's just say its not piped
   252  		return false
   253  	}
   254  
   255  	return fi.Mode()&os.ModeNamedPipe != 0
   256  }
   257  
   258  func (m *Meta) RunOperation(b backend.Enhanced, opReq *backend.Operation) (*backend.RunningOperation, error) {
   259  	op, err := b.Operation(context.Background(), opReq)
   260  	if err != nil {
   261  		return nil, fmt.Errorf("error starting operation: %s", err)
   262  	}
   263  
   264  	// Wait for the operation to complete or an interrupt to occur
   265  	select {
   266  	case <-m.ShutdownCh:
   267  		// gracefully stop the operation
   268  		op.Stop()
   269  
   270  		// Notify the user
   271  		m.Ui.Output(outputInterrupt)
   272  
   273  		// Still get the result, since there is still one
   274  		select {
   275  		case <-m.ShutdownCh:
   276  			m.Ui.Error(
   277  				"Two interrupts received. Exiting immediately. Note that data\n" +
   278  					"loss may have occurred.")
   279  
   280  			// cancel the operation completely
   281  			op.Cancel()
   282  
   283  			// the operation should return asap
   284  			// but timeout just in case
   285  			select {
   286  			case <-op.Done():
   287  			case <-time.After(5 * time.Second):
   288  			}
   289  
   290  			return nil, errors.New("operation canceled")
   291  
   292  		case <-op.Done():
   293  			// operation completed after Stop
   294  		}
   295  	case <-op.Done():
   296  		// operation completed normally
   297  	}
   298  
   299  	if op.Err != nil {
   300  		return op, op.Err
   301  	}
   302  
   303  	return op, nil
   304  }
   305  
   306  const (
   307  	ProviderSkipVerifyEnvVar = "TF_SKIP_PROVIDER_VERIFY"
   308  )
   309  
   310  // contextOpts returns the options to use to initialize a Terraform
   311  // context with the settings from this Meta.
   312  func (m *Meta) contextOpts() *terraform.ContextOpts {
   313  	var opts terraform.ContextOpts
   314  	opts.Hooks = []terraform.Hook{m.uiHook(), &terraform.DebugHook{}}
   315  	opts.Hooks = append(opts.Hooks, m.ExtraHooks...)
   316  
   317  	vs := make(map[string]interface{})
   318  	for k, v := range opts.Variables {
   319  		vs[k] = v
   320  	}
   321  	for k, v := range m.autoVariables {
   322  		vs[k] = v
   323  	}
   324  	for k, v := range m.variables {
   325  		vs[k] = v
   326  	}
   327  	opts.Variables = vs
   328  
   329  	opts.Targets = m.targets
   330  	opts.UIInput = m.UIInput()
   331  	opts.Parallelism = m.parallelism
   332  	opts.Shadow = m.shadow
   333  
   334  	// If testingOverrides are set, we'll skip the plugin discovery process
   335  	// and just work with what we've been given, thus allowing the tests
   336  	// to provide mock providers and provisioners.
   337  	if m.testingOverrides != nil {
   338  		opts.ProviderResolver = m.testingOverrides.ProviderResolver
   339  		opts.Provisioners = m.testingOverrides.Provisioners
   340  	} else {
   341  		opts.ProviderResolver = m.providerResolver()
   342  		opts.Provisioners = m.provisionerFactories()
   343  	}
   344  
   345  	opts.ProviderSHA256s = m.providerPluginsLock().Read()
   346  	if v := os.Getenv(ProviderSkipVerifyEnvVar); v != "" {
   347  		opts.SkipProviderVerify = true
   348  	}
   349  
   350  	opts.Meta = &terraform.ContextMeta{
   351  		Env: m.Workspace(),
   352  	}
   353  
   354  	return &opts
   355  }
   356  
   357  // flags adds the meta flags to the given FlagSet.
   358  func (m *Meta) flagSet(n string) *flag.FlagSet {
   359  	f := flag.NewFlagSet(n, flag.ContinueOnError)
   360  	f.BoolVar(&m.input, "input", true, "input")
   361  	f.Var((*variables.Flag)(&m.variables), "var", "variables")
   362  	f.Var((*variables.FlagFile)(&m.variables), "var-file", "variable file")
   363  	f.Var((*FlagStringSlice)(&m.targets), "target", "resource to target")
   364  
   365  	if m.autoKey != "" {
   366  		f.Var((*variables.FlagFile)(&m.autoVariables), m.autoKey, "variable file")
   367  	}
   368  
   369  	// Advanced (don't need documentation, or unlikely to be set)
   370  	f.BoolVar(&m.shadow, "shadow", true, "shadow graph")
   371  
   372  	// Experimental features
   373  	experiment.Flag(f)
   374  
   375  	// Create an io.Writer that writes to our Ui properly for errors.
   376  	// This is kind of a hack, but it does the job. Basically: create
   377  	// a pipe, use a scanner to break it into lines, and output each line
   378  	// to the UI. Do this forever.
   379  	errR, errW := io.Pipe()
   380  	errScanner := bufio.NewScanner(errR)
   381  	go func() {
   382  		// This only needs to be alive long enough to write the help info if
   383  		// there is a flag error. Kill the scanner after a short duriation to
   384  		// prevent these from accumulating during tests, and cluttering up the
   385  		// stack traces.
   386  		time.AfterFunc(2*time.Second, func() {
   387  			errW.Close()
   388  		})
   389  		for errScanner.Scan() {
   390  			m.Ui.Error(errScanner.Text())
   391  		}
   392  	}()
   393  	f.SetOutput(errW)
   394  
   395  	// Set the default Usage to empty
   396  	f.Usage = func() {}
   397  
   398  	// command that bypass locking will supply their own flag on this var, but
   399  	// set the initial meta value to true as a failsafe.
   400  	m.stateLock = true
   401  
   402  	return f
   403  }
   404  
   405  // moduleStorage returns the module.Storage implementation used to store
   406  // modules for commands.
   407  func (m *Meta) moduleStorage(root string, mode module.GetMode) *module.Storage {
   408  	s := module.NewStorage(filepath.Join(root, "modules"), m.Services)
   409  	s.Ui = m.Ui
   410  	s.Mode = mode
   411  	return s
   412  }
   413  
   414  // process will process the meta-parameters out of the arguments. This
   415  // will potentially modify the args in-place. It will return the resulting
   416  // slice.
   417  //
   418  // vars says whether or not we support variables.
   419  func (m *Meta) process(args []string, vars bool) ([]string, error) {
   420  	// We do this so that we retain the ability to technically call
   421  	// process multiple times, even if we have no plans to do so
   422  	if m.oldUi != nil {
   423  		m.Ui = m.oldUi
   424  	}
   425  
   426  	// Set colorization
   427  	m.color = m.Color
   428  	for i, v := range args {
   429  		if v == "-no-color" {
   430  			m.color = false
   431  			m.Color = false
   432  			args = append(args[:i], args[i+1:]...)
   433  			break
   434  		}
   435  	}
   436  
   437  	// Set the UI
   438  	m.oldUi = m.Ui
   439  	m.Ui = &cli.ConcurrentUi{
   440  		Ui: &ColorizeUi{
   441  			Colorize:   m.Colorize(),
   442  			ErrorColor: "[red]",
   443  			WarnColor:  "[yellow]",
   444  			Ui:         m.oldUi,
   445  		},
   446  	}
   447  
   448  	// If we support vars and the default var file exists, add it to
   449  	// the args...
   450  	m.autoKey = ""
   451  	if vars {
   452  		var preArgs []string
   453  
   454  		if _, err := os.Stat(DefaultVarsFilename); err == nil {
   455  			m.autoKey = "var-file-default"
   456  			preArgs = append(preArgs, "-"+m.autoKey, DefaultVarsFilename)
   457  		}
   458  
   459  		if _, err := os.Stat(DefaultVarsFilename + ".json"); err == nil {
   460  			m.autoKey = "var-file-default"
   461  			preArgs = append(preArgs, "-"+m.autoKey, DefaultVarsFilename+".json")
   462  		}
   463  
   464  		wd, err := os.Getwd()
   465  		if err != nil {
   466  			return nil, err
   467  		}
   468  
   469  		fis, err := ioutil.ReadDir(wd)
   470  		if err != nil {
   471  			return nil, err
   472  		}
   473  
   474  		// make sure we add the files in order
   475  		sort.Slice(fis, func(i, j int) bool {
   476  			return fis[i].Name() < fis[j].Name()
   477  		})
   478  
   479  		for _, fi := range fis {
   480  			name := fi.Name()
   481  			// Ignore directories, non-var-files, and ignored files
   482  			if fi.IsDir() || !isAutoVarFile(name) || config.IsIgnoredFile(name) {
   483  				continue
   484  			}
   485  
   486  			m.autoKey = "var-file-default"
   487  			preArgs = append(preArgs, "-"+m.autoKey, name)
   488  		}
   489  
   490  		args = append(preArgs, args...)
   491  	}
   492  
   493  	return args, nil
   494  }
   495  
   496  // uiHook returns the UiHook to use with the context.
   497  func (m *Meta) uiHook() *UiHook {
   498  	return &UiHook{
   499  		Colorize: m.Colorize(),
   500  		Ui:       m.Ui,
   501  	}
   502  }
   503  
   504  // confirm asks a yes/no confirmation.
   505  func (m *Meta) confirm(opts *terraform.InputOpts) (bool, error) {
   506  	if !m.Input() {
   507  		return false, errors.New("input is disabled")
   508  	}
   509  
   510  	for i := 0; i < 2; i++ {
   511  		v, err := m.UIInput().Input(opts)
   512  		if err != nil {
   513  			return false, fmt.Errorf(
   514  				"Error asking for confirmation: %s", err)
   515  		}
   516  
   517  		switch strings.ToLower(v) {
   518  		case "no":
   519  			return false, nil
   520  		case "yes":
   521  			return true, nil
   522  		}
   523  	}
   524  	return false, nil
   525  }
   526  
   527  // showDiagnostics displays error and warning messages in the UI.
   528  //
   529  // "Diagnostics" here means the Diagnostics type from the tfdiag package,
   530  // though as a convenience this function accepts anything that could be
   531  // passed to the "Append" method on that type, converting it to Diagnostics
   532  // before displaying it.
   533  //
   534  // Internally this function uses Diagnostics.Append, and so it will panic
   535  // if given unsupported value types, just as Append does.
   536  func (m *Meta) showDiagnostics(vals ...interface{}) {
   537  	var diags tfdiags.Diagnostics
   538  	diags = diags.Append(vals...)
   539  
   540  	for _, diag := range diags {
   541  		// TODO: Actually measure the terminal width and pass it here.
   542  		// For now, we don't have easy access to the writer that
   543  		// ui.Error (etc) are writing to and thus can't interrogate
   544  		// to see if it's a terminal and what size it is.
   545  		msg := format.Diagnostic(diag, m.Colorize(), 78)
   546  		switch diag.Severity() {
   547  		case tfdiags.Error:
   548  			m.Ui.Error(msg)
   549  		case tfdiags.Warning:
   550  			m.Ui.Warn(msg)
   551  		default:
   552  			m.Ui.Output(msg)
   553  		}
   554  	}
   555  }
   556  
   557  const (
   558  	// ModuleDepthDefault is the default value for
   559  	// module depth, which can be overridden by flag
   560  	// or env var
   561  	ModuleDepthDefault = -1
   562  
   563  	// ModuleDepthEnvVar is the name of the environment variable that can be used to set module depth.
   564  	ModuleDepthEnvVar = "TF_MODULE_DEPTH"
   565  )
   566  
   567  func (m *Meta) addModuleDepthFlag(flags *flag.FlagSet, moduleDepth *int) {
   568  	flags.IntVar(moduleDepth, "module-depth", ModuleDepthDefault, "module-depth")
   569  	if envVar := os.Getenv(ModuleDepthEnvVar); envVar != "" {
   570  		if md, err := strconv.Atoi(envVar); err == nil {
   571  			*moduleDepth = md
   572  		}
   573  	}
   574  }
   575  
   576  // outputShadowError outputs the error from ctx.ShadowError. If the
   577  // error is nil then nothing happens. If output is false then it isn't
   578  // outputted to the user (you can define logic to guard against outputting).
   579  func (m *Meta) outputShadowError(err error, output bool) bool {
   580  	// Do nothing if no error
   581  	if err == nil {
   582  		return false
   583  	}
   584  
   585  	// If not outputting, do nothing
   586  	if !output {
   587  		return false
   588  	}
   589  
   590  	// Write the shadow error output to a file
   591  	path := fmt.Sprintf("terraform-error-%d.log", time.Now().UTC().Unix())
   592  	if err := ioutil.WriteFile(path, []byte(err.Error()), 0644); err != nil {
   593  		// If there is an error writing it, just let it go
   594  		log.Printf("[ERROR] Error writing shadow error: %s", err)
   595  		return false
   596  	}
   597  
   598  	// Output!
   599  	m.Ui.Output(m.Colorize().Color(fmt.Sprintf(
   600  		"[reset][bold][yellow]\nExperimental feature failure! Please report a bug.\n\n"+
   601  			"This is not an error. Your Terraform operation completed successfully.\n"+
   602  			"Your real infrastructure is unaffected by this message.\n\n"+
   603  			"[reset][yellow]While running, Terraform sometimes tests experimental features in the\n"+
   604  			"background. These features cannot affect real state and never touch\n"+
   605  			"real infrastructure. If the features work properly, you see nothing.\n"+
   606  			"If the features fail, this message appears.\n\n"+
   607  			"You can report an issue at: https://github.com/hashicorp/terraform/issues\n\n"+
   608  			"The failure was written to %q. Please\n"+
   609  			"double check this file contains no sensitive information and report\n"+
   610  			"it with your issue.\n\n"+
   611  			"This is not an error. Your terraform operation completed successfully\n"+
   612  			"and your real infrastructure is unaffected by this message.",
   613  		path,
   614  	)))
   615  
   616  	return true
   617  }
   618  
   619  // WorkspaceNameEnvVar is the name of the environment variable that can be used
   620  // to set the name of the Terraform workspace, overriding the workspace chosen
   621  // by `terraform workspace select`.
   622  //
   623  // Note that this environment variable is ignored by `terraform workspace new`
   624  // and `terraform workspace delete`.
   625  const WorkspaceNameEnvVar = "TF_WORKSPACE"
   626  
   627  // Workspace returns the name of the currently configured workspace, corresponding
   628  // to the desired named state.
   629  func (m *Meta) Workspace() string {
   630  	current, _ := m.WorkspaceOverridden()
   631  	return current
   632  }
   633  
   634  // WorkspaceOverridden returns the name of the currently configured workspace,
   635  // corresponding to the desired named state, as well as a bool saying whether
   636  // this was set via the TF_WORKSPACE environment variable.
   637  func (m *Meta) WorkspaceOverridden() (string, bool) {
   638  	if envVar := os.Getenv(WorkspaceNameEnvVar); envVar != "" {
   639  		return envVar, true
   640  	}
   641  
   642  	envData, err := ioutil.ReadFile(filepath.Join(m.DataDir(), local.DefaultWorkspaceFile))
   643  	current := string(bytes.TrimSpace(envData))
   644  	if current == "" {
   645  		current = backend.DefaultStateName
   646  	}
   647  
   648  	if err != nil && !os.IsNotExist(err) {
   649  		// always return the default if we can't get a workspace name
   650  		log.Printf("[ERROR] failed to read current workspace: %s", err)
   651  	}
   652  
   653  	return current, false
   654  }
   655  
   656  // SetWorkspace saves the given name as the current workspace in the local
   657  // filesystem.
   658  func (m *Meta) SetWorkspace(name string) error {
   659  	err := os.MkdirAll(m.DataDir(), 0755)
   660  	if err != nil {
   661  		return err
   662  	}
   663  
   664  	err = ioutil.WriteFile(filepath.Join(m.DataDir(), local.DefaultWorkspaceFile), []byte(name), 0644)
   665  	if err != nil {
   666  		return err
   667  	}
   668  	return nil
   669  }
   670  
   671  // isAutoVarFile determines if the file ends with .auto.tfvars or .auto.tfvars.json
   672  func isAutoVarFile(path string) bool {
   673  	return strings.HasSuffix(path, ".auto.tfvars") ||
   674  		strings.HasSuffix(path, ".auto.tfvars.json")
   675  }