github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/command/meta_config.go (about)

     1  package command
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"sort"
     9  
    10  	"github.com/hashicorp/hcl/v2"
    11  	"github.com/hashicorp/hcl/v2/hclsyntax"
    12  	"github.com/hashicorp/terraform-config-inspect/tfconfig"
    13  	"github.com/hashicorp/terraform/internal/configs"
    14  	"github.com/hashicorp/terraform/internal/configs/configload"
    15  	"github.com/hashicorp/terraform/internal/configs/configschema"
    16  	"github.com/hashicorp/terraform/internal/earlyconfig"
    17  	"github.com/hashicorp/terraform/internal/initwd"
    18  	"github.com/hashicorp/terraform/internal/registry"
    19  	"github.com/hashicorp/terraform/internal/terraform"
    20  	"github.com/hashicorp/terraform/internal/tfdiags"
    21  	"github.com/zclconf/go-cty/cty"
    22  	"github.com/zclconf/go-cty/cty/convert"
    23  )
    24  
    25  // normalizePath normalizes a given path so that it is, if possible, relative
    26  // to the current working directory. This is primarily used to prepare
    27  // paths used to load configuration, because we want to prefer recording
    28  // relative paths in source code references within the configuration.
    29  func (m *Meta) normalizePath(path string) string {
    30  	m.fixupMissingWorkingDir()
    31  	return m.WorkingDir.NormalizePath(path)
    32  }
    33  
    34  // loadConfig reads a configuration from the given directory, which should
    35  // contain a root module and have already have any required descendent modules
    36  // installed.
    37  func (m *Meta) loadConfig(rootDir string) (*configs.Config, tfdiags.Diagnostics) {
    38  	var diags tfdiags.Diagnostics
    39  	rootDir = m.normalizePath(rootDir)
    40  
    41  	loader, err := m.initConfigLoader()
    42  	if err != nil {
    43  		diags = diags.Append(err)
    44  		return nil, diags
    45  	}
    46  
    47  	config, hclDiags := loader.LoadConfig(rootDir)
    48  	diags = diags.Append(hclDiags)
    49  	return config, diags
    50  }
    51  
    52  // loadSingleModule reads configuration from the given directory and returns
    53  // a description of that module only, without attempting to assemble a module
    54  // tree for referenced child modules.
    55  //
    56  // Most callers should use loadConfig. This method exists to support early
    57  // initialization use-cases where the root module must be inspected in order
    58  // to determine what else needs to be installed before the full configuration
    59  // can be used.
    60  func (m *Meta) loadSingleModule(dir string) (*configs.Module, tfdiags.Diagnostics) {
    61  	var diags tfdiags.Diagnostics
    62  	dir = m.normalizePath(dir)
    63  
    64  	loader, err := m.initConfigLoader()
    65  	if err != nil {
    66  		diags = diags.Append(err)
    67  		return nil, diags
    68  	}
    69  
    70  	module, hclDiags := loader.Parser().LoadConfigDir(dir)
    71  	diags = diags.Append(hclDiags)
    72  	return module, diags
    73  }
    74  
    75  // loadSingleModuleEarly is a variant of loadSingleModule that uses the special
    76  // "early config" loader that is more forgiving of unexpected constructs and
    77  // legacy syntax.
    78  //
    79  // Early-loaded config is not registered in the source code cache, so
    80  // diagnostics produced from it may render without source code snippets. In
    81  // practice this is not a big concern because the early config loader also
    82  // cannot generate detailed source locations, so it prefers to produce
    83  // diagnostics without explicit source location information and instead includes
    84  // approximate locations in the message text.
    85  //
    86  // Most callers should use loadConfig. This method exists to support early
    87  // initialization use-cases where the root module must be inspected in order
    88  // to determine what else needs to be installed before the full configuration
    89  // can be used.
    90  func (m *Meta) loadSingleModuleEarly(dir string) (*tfconfig.Module, tfdiags.Diagnostics) {
    91  	var diags tfdiags.Diagnostics
    92  	dir = m.normalizePath(dir)
    93  
    94  	module, moreDiags := earlyconfig.LoadModule(dir)
    95  	diags = diags.Append(moreDiags)
    96  
    97  	return module, diags
    98  }
    99  
   100  // dirIsConfigPath checks if the given path is a directory that contains at
   101  // least one Terraform configuration file (.tf or .tf.json), returning true
   102  // if so.
   103  //
   104  // In the unlikely event that the underlying config loader cannot be initalized,
   105  // this function optimistically returns true, assuming that the caller will
   106  // then do some other operation that requires the config loader and get an
   107  // error at that point.
   108  func (m *Meta) dirIsConfigPath(dir string) bool {
   109  	loader, err := m.initConfigLoader()
   110  	if err != nil {
   111  		return true
   112  	}
   113  
   114  	return loader.IsConfigDir(dir)
   115  }
   116  
   117  // loadBackendConfig reads configuration from the given directory and returns
   118  // the backend configuration defined by that module, if any. Nil is returned
   119  // if the specified module does not have an explicit backend configuration.
   120  //
   121  // This is a convenience method for command code that will delegate to the
   122  // configured backend to do most of its work, since in that case it is the
   123  // backend that will do the full configuration load.
   124  //
   125  // Although this method returns only the backend configuration, at present it
   126  // actually loads and validates the entire configuration first. Therefore errors
   127  // returned may be about other aspects of the configuration. This behavior may
   128  // change in future, so callers must not rely on it. (That is, they must expect
   129  // that a call to loadSingleModule or loadConfig could fail on the same
   130  // directory even if loadBackendConfig succeeded.)
   131  func (m *Meta) loadBackendConfig(rootDir string) (*configs.Backend, tfdiags.Diagnostics) {
   132  	mod, diags := m.loadSingleModule(rootDir)
   133  
   134  	// Only return error diagnostics at this point. Any warnings will be caught
   135  	// again later and duplicated in the output.
   136  	if diags.HasErrors() {
   137  		return nil, diags
   138  	}
   139  
   140  	if mod.CloudConfig != nil {
   141  		backendConfig := mod.CloudConfig.ToBackendConfig()
   142  		return &backendConfig, nil
   143  	}
   144  
   145  	return mod.Backend, nil
   146  }
   147  
   148  // loadHCLFile reads an arbitrary HCL file and returns the unprocessed body
   149  // representing its toplevel. Most callers should use one of the more
   150  // specialized "load..." methods to get a higher-level representation.
   151  func (m *Meta) loadHCLFile(filename string) (hcl.Body, tfdiags.Diagnostics) {
   152  	var diags tfdiags.Diagnostics
   153  	filename = m.normalizePath(filename)
   154  
   155  	loader, err := m.initConfigLoader()
   156  	if err != nil {
   157  		diags = diags.Append(err)
   158  		return nil, diags
   159  	}
   160  
   161  	body, hclDiags := loader.Parser().LoadHCLFile(filename)
   162  	diags = diags.Append(hclDiags)
   163  	return body, diags
   164  }
   165  
   166  // installModules reads a root module from the given directory and attempts
   167  // recursively to install all of its descendent modules.
   168  //
   169  // The given hooks object will be notified of installation progress, which
   170  // can then be relayed to the end-user. The uiModuleInstallHooks type in
   171  // this package has a reasonable implementation for displaying notifications
   172  // via a provided cli.Ui.
   173  func (m *Meta) installModules(rootDir string, upgrade bool, hooks initwd.ModuleInstallHooks) (abort bool, diags tfdiags.Diagnostics) {
   174  	rootDir = m.normalizePath(rootDir)
   175  
   176  	err := os.MkdirAll(m.modulesDir(), os.ModePerm)
   177  	if err != nil {
   178  		diags = diags.Append(fmt.Errorf("failed to create local modules directory: %s", err))
   179  		return true, diags
   180  	}
   181  
   182  	inst := m.moduleInstaller()
   183  
   184  	// Installation can be aborted by interruption signals
   185  	ctx, done := m.InterruptibleContext()
   186  	defer done()
   187  
   188  	_, moreDiags := inst.InstallModules(ctx, rootDir, upgrade, hooks)
   189  	diags = diags.Append(moreDiags)
   190  
   191  	if ctx.Err() == context.Canceled {
   192  		m.showDiagnostics(diags)
   193  		m.Ui.Error("Module installation was canceled by an interrupt signal.")
   194  		return true, diags
   195  	}
   196  
   197  	return false, diags
   198  }
   199  
   200  // initDirFromModule initializes the given directory (which should be
   201  // pre-verified as empty by the caller) by copying the source code from the
   202  // given module address.
   203  //
   204  // Internally this runs similar steps to installModules.
   205  // The given hooks object will be notified of installation progress, which
   206  // can then be relayed to the end-user. The uiModuleInstallHooks type in
   207  // this package has a reasonable implementation for displaying notifications
   208  // via a provided cli.Ui.
   209  func (m *Meta) initDirFromModule(targetDir string, addr string, hooks initwd.ModuleInstallHooks) (abort bool, diags tfdiags.Diagnostics) {
   210  	// Installation can be aborted by interruption signals
   211  	ctx, done := m.InterruptibleContext()
   212  	defer done()
   213  
   214  	targetDir = m.normalizePath(targetDir)
   215  	moreDiags := initwd.DirFromModule(ctx, targetDir, m.modulesDir(), addr, m.registryClient(), hooks)
   216  	diags = diags.Append(moreDiags)
   217  	if ctx.Err() == context.Canceled {
   218  		m.showDiagnostics(diags)
   219  		m.Ui.Error("Module initialization was canceled by an interrupt signal.")
   220  		return true, diags
   221  	}
   222  	return false, diags
   223  }
   224  
   225  // inputForSchema uses interactive prompts to try to populate any
   226  // not-yet-populated required attributes in the given object value to
   227  // comply with the given schema.
   228  //
   229  // An error will be returned if input is disabled for this meta or if
   230  // values cannot be obtained for some other operational reason. Errors are
   231  // not returned for invalid input since the input loop itself will report
   232  // that interactively.
   233  //
   234  // It is not guaranteed that the result will be valid, since certain attribute
   235  // types and nested blocks are not supported for input.
   236  //
   237  // The given value must conform to the given schema. If not, this method will
   238  // panic.
   239  func (m *Meta) inputForSchema(given cty.Value, schema *configschema.Block) (cty.Value, error) {
   240  	if given.IsNull() || !given.IsKnown() {
   241  		// This is not reasonable input, but we'll tolerate it anyway and
   242  		// just pass it through for the caller to handle downstream.
   243  		return given, nil
   244  	}
   245  
   246  	retVals := given.AsValueMap()
   247  	names := make([]string, 0, len(schema.Attributes))
   248  	for name, attrS := range schema.Attributes {
   249  		if attrS.Required && retVals[name].IsNull() && attrS.Type.IsPrimitiveType() {
   250  			names = append(names, name)
   251  		}
   252  	}
   253  	sort.Strings(names)
   254  
   255  	input := m.UIInput()
   256  	for _, name := range names {
   257  		attrS := schema.Attributes[name]
   258  
   259  		for {
   260  			strVal, err := input.Input(context.Background(), &terraform.InputOpts{
   261  				Id:          name,
   262  				Query:       name,
   263  				Description: attrS.Description,
   264  			})
   265  			if err != nil {
   266  				return cty.UnknownVal(schema.ImpliedType()), fmt.Errorf("%s: %s", name, err)
   267  			}
   268  
   269  			val := cty.StringVal(strVal)
   270  			val, err = convert.Convert(val, attrS.Type)
   271  			if err != nil {
   272  				m.showDiagnostics(fmt.Errorf("Invalid value: %s", err))
   273  				continue
   274  			}
   275  
   276  			retVals[name] = val
   277  			break
   278  		}
   279  	}
   280  
   281  	return cty.ObjectVal(retVals), nil
   282  }
   283  
   284  // configSources returns the source cache from the receiver's config loader,
   285  // which the caller must not modify.
   286  //
   287  // If a config loader has not yet been instantiated then no files could have
   288  // been loaded already, so this method returns a nil map in that case.
   289  func (m *Meta) configSources() map[string][]byte {
   290  	if m.configLoader == nil {
   291  		return nil
   292  	}
   293  
   294  	return m.configLoader.Sources()
   295  }
   296  
   297  func (m *Meta) modulesDir() string {
   298  	return filepath.Join(m.DataDir(), "modules")
   299  }
   300  
   301  // registerSynthConfigSource allows commands to add synthetic additional source
   302  // buffers to the config loader's cache of sources (as returned by
   303  // configSources), which is useful when a command is directly parsing something
   304  // from the command line that may produce diagnostics, so that diagnostic
   305  // snippets can still be produced.
   306  //
   307  // If this is called before a configLoader has been initialized then it will
   308  // try to initialize the loader but ignore any initialization failure, turning
   309  // the call into a no-op. (We presume that a caller will later call a different
   310  // function that also initializes the config loader as a side effect, at which
   311  // point those errors can be returned.)
   312  func (m *Meta) registerSynthConfigSource(filename string, src []byte) {
   313  	loader, err := m.initConfigLoader()
   314  	if err != nil || loader == nil {
   315  		return // treated as no-op, since this is best-effort
   316  	}
   317  	loader.Parser().ForceFileSource(filename, src)
   318  }
   319  
   320  // initConfigLoader initializes the shared configuration loader if it isn't
   321  // already initialized.
   322  //
   323  // If the loader cannot be created for some reason then an error is returned
   324  // and no loader is created. Subsequent calls will presumably see the same
   325  // error. Loader initialization errors will tend to prevent any further use
   326  // of most Terraform features, so callers should report any error and safely
   327  // terminate.
   328  func (m *Meta) initConfigLoader() (*configload.Loader, error) {
   329  	if m.configLoader == nil {
   330  		loader, err := configload.NewLoader(&configload.Config{
   331  			ModulesDir: m.modulesDir(),
   332  			Services:   m.Services,
   333  		})
   334  		if err != nil {
   335  			return nil, err
   336  		}
   337  		loader.AllowLanguageExperiments(m.AllowExperimentalFeatures)
   338  		m.configLoader = loader
   339  		if m.View != nil {
   340  			m.View.SetConfigSources(loader.Sources)
   341  		}
   342  	}
   343  	return m.configLoader, nil
   344  }
   345  
   346  // moduleInstaller instantiates and returns a module installer for use by
   347  // "terraform init" (directly or indirectly).
   348  func (m *Meta) moduleInstaller() *initwd.ModuleInstaller {
   349  	reg := m.registryClient()
   350  	return initwd.NewModuleInstaller(m.modulesDir(), reg)
   351  }
   352  
   353  // registryClient instantiates and returns a new Terraform Registry client.
   354  func (m *Meta) registryClient() *registry.Client {
   355  	return registry.NewClient(m.Services, nil)
   356  }
   357  
   358  // configValueFromCLI parses a configuration value that was provided in a
   359  // context in the CLI where only strings can be provided, such as on the
   360  // command line or in an environment variable, and returns the resulting
   361  // value.
   362  func configValueFromCLI(synthFilename, rawValue string, wantType cty.Type) (cty.Value, tfdiags.Diagnostics) {
   363  	var diags tfdiags.Diagnostics
   364  
   365  	switch {
   366  	case wantType.IsPrimitiveType():
   367  		// Primitive types are handled as conversions from string.
   368  		val := cty.StringVal(rawValue)
   369  		var err error
   370  		val, err = convert.Convert(val, wantType)
   371  		if err != nil {
   372  			diags = diags.Append(tfdiags.Sourceless(
   373  				tfdiags.Error,
   374  				"Invalid backend configuration value",
   375  				fmt.Sprintf("Invalid backend configuration argument %s: %s", synthFilename, err),
   376  			))
   377  			val = cty.DynamicVal // just so we return something valid-ish
   378  		}
   379  		return val, diags
   380  	default:
   381  		// Non-primitives are parsed as HCL expressions
   382  		src := []byte(rawValue)
   383  		expr, hclDiags := hclsyntax.ParseExpression(src, synthFilename, hcl.Pos{Line: 1, Column: 1})
   384  		diags = diags.Append(hclDiags)
   385  		if hclDiags.HasErrors() {
   386  			return cty.DynamicVal, diags
   387  		}
   388  		val, hclDiags := expr.Value(nil)
   389  		diags = diags.Append(hclDiags)
   390  		if hclDiags.HasErrors() {
   391  			val = cty.DynamicVal
   392  		}
   393  		return val, diags
   394  	}
   395  }
   396  
   397  // rawFlags is a flag.Value implementation that just appends raw flag
   398  // names and values to a slice.
   399  type rawFlags struct {
   400  	flagName string
   401  	items    *[]rawFlag
   402  }
   403  
   404  func newRawFlags(flagName string) rawFlags {
   405  	var items []rawFlag
   406  	return rawFlags{
   407  		flagName: flagName,
   408  		items:    &items,
   409  	}
   410  }
   411  
   412  func (f rawFlags) Empty() bool {
   413  	if f.items == nil {
   414  		return true
   415  	}
   416  	return len(*f.items) == 0
   417  }
   418  
   419  func (f rawFlags) AllItems() []rawFlag {
   420  	if f.items == nil {
   421  		return nil
   422  	}
   423  	return *f.items
   424  }
   425  
   426  func (f rawFlags) Alias(flagName string) rawFlags {
   427  	return rawFlags{
   428  		flagName: flagName,
   429  		items:    f.items,
   430  	}
   431  }
   432  
   433  func (f rawFlags) String() string {
   434  	return ""
   435  }
   436  
   437  func (f rawFlags) Set(str string) error {
   438  	*f.items = append(*f.items, rawFlag{
   439  		Name:  f.flagName,
   440  		Value: str,
   441  	})
   442  	return nil
   443  }
   444  
   445  type rawFlag struct {
   446  	Name  string
   447  	Value string
   448  }
   449  
   450  func (f rawFlag) String() string {
   451  	return fmt.Sprintf("%s=%q", f.Name, f.Value)
   452  }