github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/meta_config.go (about)

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