github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/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  	return mod.Backend, nil
   140  }
   141  
   142  // loadHCLFile reads an arbitrary HCL file and returns the unprocessed body
   143  // representing its toplevel. Most callers should use one of the more
   144  // specialized "load..." methods to get a higher-level representation.
   145  func (m *Meta) loadHCLFile(filename string) (hcl.Body, tfdiags.Diagnostics) {
   146  	var diags tfdiags.Diagnostics
   147  	filename = m.normalizePath(filename)
   148  
   149  	loader, err := m.initConfigLoader()
   150  	if err != nil {
   151  		diags = diags.Append(err)
   152  		return nil, diags
   153  	}
   154  
   155  	body, hclDiags := loader.Parser().LoadHCLFile(filename)
   156  	diags = diags.Append(hclDiags)
   157  	return body, diags
   158  }
   159  
   160  // installModules reads a root module from the given directory and attempts
   161  // recursively install all of its descendent modules.
   162  //
   163  // The given hooks object will be notified of installation progress, which
   164  // can then be relayed to the end-user. The moduleUiInstallHooks type in
   165  // this package has a reasonable implementation for displaying notifications
   166  // via a provided cli.Ui.
   167  func (m *Meta) installModules(rootDir string, upgrade bool, hooks initwd.ModuleInstallHooks) tfdiags.Diagnostics {
   168  	var diags tfdiags.Diagnostics
   169  	rootDir = m.normalizePath(rootDir)
   170  
   171  	err := os.MkdirAll(m.modulesDir(), os.ModePerm)
   172  	if err != nil {
   173  		diags = diags.Append(fmt.Errorf("failed to create local modules directory: %s", err))
   174  		return diags
   175  	}
   176  
   177  	inst := m.moduleInstaller()
   178  	_, moreDiags := inst.InstallModules(rootDir, upgrade, hooks)
   179  	diags = diags.Append(moreDiags)
   180  	return diags
   181  }
   182  
   183  // initDirFromModule initializes the given directory (which should be
   184  // pre-verified as empty by the caller) by copying the source code from the
   185  // given module address.
   186  //
   187  // Internally this runs similar steps to installModules.
   188  // The given hooks object will be notified of installation progress, which
   189  // can then be relayed to the end-user. The moduleUiInstallHooks type in
   190  // this package has a reasonable implementation for displaying notifications
   191  // via a provided cli.Ui.
   192  func (m *Meta) initDirFromModule(targetDir string, addr string, hooks initwd.ModuleInstallHooks) tfdiags.Diagnostics {
   193  	var diags tfdiags.Diagnostics
   194  	targetDir = m.normalizePath(targetDir)
   195  	moreDiags := initwd.DirFromModule(targetDir, m.modulesDir(), addr, m.registryClient(), hooks)
   196  	diags = diags.Append(moreDiags)
   197  	return diags
   198  }
   199  
   200  // inputForSchema uses interactive prompts to try to populate any
   201  // not-yet-populated required attributes in the given object value to
   202  // comply with the given schema.
   203  //
   204  // An error will be returned if input is disabled for this meta or if
   205  // values cannot be obtained for some other operational reason. Errors are
   206  // not returned for invalid input since the input loop itself will report
   207  // that interactively.
   208  //
   209  // It is not guaranteed that the result will be valid, since certain attribute
   210  // types and nested blocks are not supported for input.
   211  //
   212  // The given value must conform to the given schema. If not, this method will
   213  // panic.
   214  func (m *Meta) inputForSchema(given cty.Value, schema *configschema.Block) (cty.Value, error) {
   215  	if given.IsNull() || !given.IsKnown() {
   216  		// This is not reasonable input, but we'll tolerate it anyway and
   217  		// just pass it through for the caller to handle downstream.
   218  		return given, nil
   219  	}
   220  
   221  	retVals := given.AsValueMap()
   222  	names := make([]string, 0, len(schema.Attributes))
   223  	for name, attrS := range schema.Attributes {
   224  		if attrS.Required && retVals[name].IsNull() && attrS.Type.IsPrimitiveType() {
   225  			names = append(names, name)
   226  		}
   227  	}
   228  	sort.Strings(names)
   229  
   230  	input := m.UIInput()
   231  	for _, name := range names {
   232  		attrS := schema.Attributes[name]
   233  
   234  		for {
   235  			strVal, err := input.Input(context.Background(), &terraform.InputOpts{
   236  				Id:          name,
   237  				Query:       name,
   238  				Description: attrS.Description,
   239  			})
   240  			if err != nil {
   241  				return cty.UnknownVal(schema.ImpliedType()), fmt.Errorf("%s: %s", name, err)
   242  			}
   243  
   244  			val := cty.StringVal(strVal)
   245  			val, err = convert.Convert(val, attrS.Type)
   246  			if err != nil {
   247  				m.showDiagnostics(fmt.Errorf("Invalid value: %s", err))
   248  				continue
   249  			}
   250  
   251  			retVals[name] = val
   252  			break
   253  		}
   254  	}
   255  
   256  	return cty.ObjectVal(retVals), nil
   257  }
   258  
   259  // configSources returns the source cache from the receiver's config loader,
   260  // which the caller must not modify.
   261  //
   262  // If a config loader has not yet been instantiated then no files could have
   263  // been loaded already, so this method returns a nil map in that case.
   264  func (m *Meta) configSources() map[string][]byte {
   265  	if m.configLoader == nil {
   266  		return nil
   267  	}
   268  
   269  	return m.configLoader.Sources()
   270  }
   271  
   272  func (m *Meta) modulesDir() string {
   273  	return filepath.Join(m.DataDir(), "modules")
   274  }
   275  
   276  // registerSynthConfigSource allows commands to add synthetic additional source
   277  // buffers to the config loader's cache of sources (as returned by
   278  // configSources), which is useful when a command is directly parsing something
   279  // from the command line that may produce diagnostics, so that diagnostic
   280  // snippets can still be produced.
   281  //
   282  // If this is called before a configLoader has been initialized then it will
   283  // try to initialize the loader but ignore any initialization failure, turning
   284  // the call into a no-op. (We presume that a caller will later call a different
   285  // function that also initializes the config loader as a side effect, at which
   286  // point those errors can be returned.)
   287  func (m *Meta) registerSynthConfigSource(filename string, src []byte) {
   288  	loader, err := m.initConfigLoader()
   289  	if err != nil || loader == nil {
   290  		return // treated as no-op, since this is best-effort
   291  	}
   292  	loader.Parser().ForceFileSource(filename, src)
   293  }
   294  
   295  // initConfigLoader initializes the shared configuration loader if it isn't
   296  // already initialized.
   297  //
   298  // If the loader cannot be created for some reason then an error is returned
   299  // and no loader is created. Subsequent calls will presumably see the same
   300  // error. Loader initialization errors will tend to prevent any further use
   301  // of most Terraform features, so callers should report any error and safely
   302  // terminate.
   303  func (m *Meta) initConfigLoader() (*configload.Loader, error) {
   304  	if m.configLoader == nil {
   305  		loader, err := configload.NewLoader(&configload.Config{
   306  			ModulesDir: m.modulesDir(),
   307  			Services:   m.Services,
   308  		})
   309  		if err != nil {
   310  			return nil, err
   311  		}
   312  		m.configLoader = loader
   313  		if m.View != nil {
   314  			m.View.SetConfigSources(loader.Sources)
   315  		}
   316  	}
   317  	return m.configLoader, nil
   318  }
   319  
   320  // moduleInstaller instantiates and returns a module installer for use by
   321  // "terraform init" (directly or indirectly).
   322  func (m *Meta) moduleInstaller() *initwd.ModuleInstaller {
   323  	reg := m.registryClient()
   324  	return initwd.NewModuleInstaller(m.modulesDir(), reg)
   325  }
   326  
   327  // registryClient instantiates and returns a new Terraform Registry client.
   328  func (m *Meta) registryClient() *registry.Client {
   329  	return registry.NewClient(m.Services, nil)
   330  }
   331  
   332  // configValueFromCLI parses a configuration value that was provided in a
   333  // context in the CLI where only strings can be provided, such as on the
   334  // command line or in an environment variable, and returns the resulting
   335  // value.
   336  func configValueFromCLI(synthFilename, rawValue string, wantType cty.Type) (cty.Value, tfdiags.Diagnostics) {
   337  	var diags tfdiags.Diagnostics
   338  
   339  	switch {
   340  	case wantType.IsPrimitiveType():
   341  		// Primitive types are handled as conversions from string.
   342  		val := cty.StringVal(rawValue)
   343  		var err error
   344  		val, err = convert.Convert(val, wantType)
   345  		if err != nil {
   346  			diags = diags.Append(tfdiags.Sourceless(
   347  				tfdiags.Error,
   348  				"Invalid backend configuration value",
   349  				fmt.Sprintf("Invalid backend configuration argument %s: %s", synthFilename, err),
   350  			))
   351  			val = cty.DynamicVal // just so we return something valid-ish
   352  		}
   353  		return val, diags
   354  	default:
   355  		// Non-primitives are parsed as HCL expressions
   356  		src := []byte(rawValue)
   357  		expr, hclDiags := hclsyntax.ParseExpression(src, synthFilename, hcl.Pos{Line: 1, Column: 1})
   358  		diags = diags.Append(hclDiags)
   359  		if hclDiags.HasErrors() {
   360  			return cty.DynamicVal, diags
   361  		}
   362  		val, hclDiags := expr.Value(nil)
   363  		diags = diags.Append(hclDiags)
   364  		if hclDiags.HasErrors() {
   365  			val = cty.DynamicVal
   366  		}
   367  		return val, diags
   368  	}
   369  }
   370  
   371  // rawFlags is a flag.Value implementation that just appends raw flag
   372  // names and values to a slice.
   373  type rawFlags struct {
   374  	flagName string
   375  	items    *[]rawFlag
   376  }
   377  
   378  func newRawFlags(flagName string) rawFlags {
   379  	var items []rawFlag
   380  	return rawFlags{
   381  		flagName: flagName,
   382  		items:    &items,
   383  	}
   384  }
   385  
   386  func (f rawFlags) Empty() bool {
   387  	if f.items == nil {
   388  		return true
   389  	}
   390  	return len(*f.items) == 0
   391  }
   392  
   393  func (f rawFlags) AllItems() []rawFlag {
   394  	if f.items == nil {
   395  		return nil
   396  	}
   397  	return *f.items
   398  }
   399  
   400  func (f rawFlags) Alias(flagName string) rawFlags {
   401  	return rawFlags{
   402  		flagName: flagName,
   403  		items:    f.items,
   404  	}
   405  }
   406  
   407  func (f rawFlags) String() string {
   408  	return ""
   409  }
   410  
   411  func (f rawFlags) Set(str string) error {
   412  	*f.items = append(*f.items, rawFlag{
   413  		Name:  f.flagName,
   414  		Value: str,
   415  	})
   416  	return nil
   417  }
   418  
   419  type rawFlag struct {
   420  	Name  string
   421  	Value string
   422  }
   423  
   424  func (f rawFlag) String() string {
   425  	return fmt.Sprintf("%s=%q", f.Name, f.Value)
   426  }