github.com/opentofu/opentofu@v1.7.1/internal/command/meta_config.go (about)

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