command: new Meta methods for accessing the new config loader

We need to share a single config loader across all callers because that
allows us to maintain the source code cache we'll use for snippets in
error messages.

Nothing calls this yet. Callers will be gradually updated away from Module
and Config in subsequent commits.
This commit is contained in:
Martin Atkins 2018-02-28 17:09:48 -08:00
parent bd10b84a8e
commit fd5b7b42b8
4 changed files with 241 additions and 4 deletions

View File

@ -0,0 +1,33 @@
package command
import (
"fmt"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/configs/configload"
"github.com/mitchellh/cli"
)
type uiModuleInstallHooks struct {
configload.InstallHooksImpl
Ui cli.Ui
ShowLocalPaths bool
}
var _ configload.InstallHooks = uiModuleInstallHooks{}
func (h uiModuleInstallHooks) Download(modulePath, packageAddr string, v *version.Version) {
if v != nil {
h.Ui.Info(fmt.Sprintf("Downloading %s %s for %s...", packageAddr, v, modulePath))
} else {
h.Ui.Info(fmt.Sprintf("Downloading %s for %s...", packageAddr, modulePath))
}
}
func (h uiModuleInstallHooks) Install(modulePath string, v *version.Version, localDir string) {
if h.ShowLocalPaths {
h.Ui.Info(fmt.Sprintf("- %s in %s", modulePath, localDir))
} else {
h.Ui.Info(fmt.Sprintf("- %s", modulePath))
}
}

View File

@ -22,6 +22,7 @@ import (
"github.com/hashicorp/terraform/command/format"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/configs/configload"
"github.com/hashicorp/terraform/helper/experiment"
"github.com/hashicorp/terraform/helper/variables"
"github.com/hashicorp/terraform/helper/wrappedstreams"
@ -98,6 +99,11 @@ type Meta struct {
// Private: do not set these
//----------------------------------------------------------
// configLoader is a shared configuration loader that is used by
// LoadConfig and other commands that access configuration files.
// It is initialized on first use.
configLoader *configload.Loader
// backendState is the currently active backend state
backendState *terraform.BackendState
@ -542,7 +548,7 @@ func (m *Meta) showDiagnostics(vals ...interface{}) {
// For now, we don't have easy access to the writer that
// ui.Error (etc) are writing to and thus can't interrogate
// to see if it's a terminal and what size it is.
msg := format.Diagnostic(diag, nil, m.Colorize(), 78)
msg := format.Diagnostic(diag, m.configSources(), m.Colorize(), 78)
switch diag.Severity() {
case tfdiags.Error:
m.Ui.Error(msg)

194
command/meta_config.go Normal file
View File

@ -0,0 +1,194 @@
package command
import (
"fmt"
"os"
"path/filepath"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/configs/configload"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// normalizePath normalizes a given path so that it is, if possible, relative
// to the current working directory. This is primarily used to prepare
// paths used to load configuration, because we want to prefer recording
// relative paths in source code references within the configuration.
func (m *Meta) normalizePath(path string) string {
var err error
// First we will make it absolute so that we have a consistent place
// to start.
path, err = filepath.Abs(path)
if err != nil {
// We'll just accept what we were given, then.
return path
}
cwd, err := os.Getwd()
if err != nil || !filepath.IsAbs(cwd) {
return path
}
ret, err := filepath.Rel(cwd, path)
if err != nil {
return path
}
return ret
}
// loadConfig reads a configuration from the given directory, which should
// contain a root module and have already have any required descendent modules
// installed.
func (m *Meta) loadConfig(rootDir string) (*configs.Config, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
rootDir = m.normalizePath(rootDir)
loader, err := m.initConfigLoader()
if err != nil {
diags = diags.Append(err)
return nil, diags
}
config, hclDiags := loader.LoadConfig(rootDir)
diags = diags.Append(hclDiags)
return config, diags
}
// loadSingleModule reads configuration from the given directory and returns
// a description of that module only, without attempting to assemble a module
// tree for referenced child modules.
//
// Most callers should use loadConfig. This method exists to support early
// initialization use-cases where the root module must be inspected in order
// to determine what else needs to be installed before the full configuration
// can be used.
func (m *Meta) loadSingleModule(dir string) (*configs.Module, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
dir = m.normalizePath(dir)
loader, err := m.initConfigLoader()
if err != nil {
diags = diags.Append(err)
return nil, diags
}
module, hclDiags := loader.Parser().LoadConfigDir(dir)
diags = diags.Append(hclDiags)
return module, diags
}
// installModules reads a root module from the given directory and attempts
// recursively install all of its descendent modules.
//
// The given hooks object will be notified of installation progress, which
// can then be relayed to the end-user. The moduleUiInstallHooks type in
// this package has a reasonable implementation for displaying notifications
// via a provided cli.Ui.
func (m *Meta) installModules(rootDir string, upgrade bool, hooks configload.InstallHooks) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
rootDir = m.normalizePath(rootDir)
err := os.MkdirAll(m.modulesDir(), os.ModePerm)
if err != nil {
diags = diags.Append(fmt.Errorf("failed to create local modules directory: %s", err))
return diags
}
loader, err := m.initConfigLoader()
if err != nil {
diags = diags.Append(err)
return diags
}
hclDiags := loader.InstallModules(rootDir, upgrade, hooks)
diags = diags.Append(hclDiags)
return diags
}
// initDirFromModule initializes the given directory (which should be
// pre-verified as empty by the caller) by copying the source code from the
// given module address.
//
// Internally this runs similar steps to installModules.
// The given hooks object will be notified of installation progress, which
// can then be relayed to the end-user. The moduleUiInstallHooks type in
// this package has a reasonable implementation for displaying notifications
// via a provided cli.Ui.
func (m *Meta) initDirFromModule(targetDir string, addr string, hooks configload.InstallHooks) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
targetDir = m.normalizePath(targetDir)
loader, err := m.initConfigLoader()
if err != nil {
diags = diags.Append(err)
return diags
}
hclDiags := loader.InitDirFromModule(targetDir, addr, hooks)
diags = diags.Append(hclDiags)
return diags
}
// loadVarsFile reads a file from the given path and interprets it as a
// "vars file", returning the contained values as a map.
//
// The file is read using the parser associated with the receiver's
// configuration loader, which means that the file's contents will be added
// to the source cache that is used for config snippets in diagnostic messages.
func (m *Meta) loadVarsFile(filename string) (map[string]cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
loader, err := m.initConfigLoader()
if err != nil {
diags = diags.Append(err)
return nil, diags
}
parser := loader.Parser()
ret, hclDiags := parser.LoadValuesFile(filename)
diags = diags.Append(hclDiags)
return ret, diags
}
// configSources returns the source cache from the receiver's config loader,
// which the caller must not modify.
//
// If a config loader has not yet been instantiated then no files could have
// been loaded already, so this method returns a nil map in that case.
func (m *Meta) configSources() map[string][]byte {
if m.configLoader == nil {
return nil
}
return m.configLoader.Sources()
}
func (m *Meta) modulesDir() string {
return filepath.Join(m.DataDir(), "modules")
}
// initConfigLoader initializes the shared configuration loader if it isn't
// already initialized.
//
// If the loader cannot be created for some reason then an error is returned
// and no loader is created. Subsequent calls will presumably see the same
// error. Loader initialization errors will tend to prevent any further use
// of most Terraform features, so callers should report any error and safely
// terminate.
func (m *Meta) initConfigLoader() (*configload.Loader, error) {
if m.configLoader == nil {
loader, err := configload.NewLoader(&configload.Config{
ModulesDir: m.modulesDir(),
Services: m.Services,
Creds: m.Credentials,
})
if err != nil {
return nil, err
}
m.configLoader = loader
}
return m.configLoader, nil
}

View File

@ -31,7 +31,8 @@ func (m *Meta) Input() bool {
return true
}
// Module loads the module tree for the given root path.
// Module loads the module tree for the given root path using the legacy
// configuration loader.
//
// It expects the modules to already be downloaded. This will never
// download any modules.
@ -65,8 +66,11 @@ func (m *Meta) Module(path string) (*module.Tree, tfdiags.Diagnostics) {
return mod, diags
}
// Config loads the root config for the path specified. Path may be a directory
// or file. The absence of configuration is not an error and returns a nil Config.
// Config loads the root config for the path specified, using the legacy
// configuration loader.
//
// Path may be a directory or file. The absence of configuration is not an
// error and returns a nil Config.
func (m *Meta) Config(path string) (*config.Config, error) {
// If no explicit path was given then it is okay for there to be
// no backend configuration found.