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