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