internal/earlyconfig: Liberal config parsing for init

This is an alternative to the full config loader in the "configs" package
that is good for "early" use-cases like "terraform init", where we want
to find out what our dependencies are without getting tripped up on any
other errors that might be present.
pluginsdk-v0.12-early2
Martin Atkins 7 years ago
parent 21d65cfa9a
commit 8ca1fcec51

@ -0,0 +1,123 @@
package earlyconfig
import (
"fmt"
"sort"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform-config-inspect/tfconfig"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/moduledeps"
"github.com/hashicorp/terraform/plugin/discovery"
"github.com/hashicorp/terraform/tfdiags"
)
// A Config is a node in the tree of modules within a configuration.
//
// The module tree is constructed by following ModuleCall instances recursively
// through the root module transitively into descendent modules.
type Config struct {
// RootModule points to the Config for the root module within the same
// module tree as this module. If this module _is_ the root module then
// this is self-referential.
Root *Config
// ParentModule points to the Config for the module that directly calls
// this module. If this is the root module then this field is nil.
Parent *Config
// Path is a sequence of module logical names that traverse from the root
// module to this config. Path is empty for the root module.
//
// This should only be used to display paths to the end-user in rare cases
// where we are talking about the static module tree, before module calls
// have been resolved. In most cases, a addrs.ModuleInstance describing
// a node in the dynamic module tree is better, since it will then include
// any keys resulting from evaluating "count" and "for_each" arguments.
Path addrs.Module
// ChildModules points to the Config for each of the direct child modules
// called from this module. The keys in this map match the keys in
// Module.ModuleCalls.
Children map[string]*Config
// Module points to the object describing the configuration for the
// various elements (variables, resources, etc) defined by this module.
Module *tfconfig.Module
// CallPos is the source position for the header of the module block that
// requested this module.
//
// This field is meaningless for the root module, where its contents are undefined.
CallPos tfconfig.SourcePos
// SourceAddr is the source address that the referenced module was requested
// from, as specified in configuration.
//
// This field is meaningless for the root module, where its contents are undefined.
SourceAddr string
// Version is the specific version that was selected for this module,
// based on version constraints given in configuration.
//
// This field is nil if the module was loaded from a non-registry source,
// since versions are not supported for other sources.
//
// This field is meaningless for the root module, where it will always
// be nil.
Version *version.Version
}
// ProviderDependencies returns the provider dependencies for the recieving
// config, including all of its descendent modules.
func (c *Config) ProviderDependencies() (*moduledeps.Module, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
var name string
if len(c.Path) > 0 {
name = c.Path[len(c.Path)-1]
}
ret := &moduledeps.Module{
Name: name,
}
providers := make(moduledeps.Providers)
for name, reqs := range c.Module.RequiredProviders {
inst := moduledeps.ProviderInstance(name)
var constraints version.Constraints
for _, reqStr := range reqs {
if reqStr == "" {
constraint, err := version.NewConstraint(reqStr)
if err != nil {
diags = diags.Append(wrapDiagnostic(tfconfig.Diagnostic{
Severity: tfconfig.DiagError,
Summary: "Invalid provider version constraint",
Detail: fmt.Sprintf("Invalid version constraint %q for provider %s.", reqStr, name),
}))
continue
}
constraints = append(constraints, constraint...)
}
}
providers[inst] = moduledeps.ProviderDependency{
Constraints: discovery.NewConstraints(constraints),
Reason: moduledeps.ProviderDependencyExplicit,
}
}
ret.Providers = providers
childNames := make([]string, 0, len(c.Children))
for name := range c.Children {
childNames = append(childNames, name)
}
sort.Strings(childNames)
for _, name := range childNames {
child, childDiags := c.Children[name].ProviderDependencies()
ret.Children = append(ret.Children, child)
diags = diags.Append(childDiags)
}
return ret, diags
}

@ -0,0 +1,139 @@
package earlyconfig
import (
"fmt"
"sort"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform-config-inspect/tfconfig"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/tfdiags"
)
// BuildConfig constructs a Config from a root module by loading all of its
// descendent modules via the given ModuleWalker.
func BuildConfig(root *tfconfig.Module, walker ModuleWalker) (*Config, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
cfg := &Config{
Module: root,
}
cfg.Root = cfg // Root module is self-referential.
cfg.Children, diags = buildChildModules(cfg, walker)
return cfg, diags
}
func buildChildModules(parent *Config, walker ModuleWalker) (map[string]*Config, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
ret := map[string]*Config{}
calls := parent.Module.ModuleCalls
// We'll sort the calls by their local names so that they'll appear in a
// predictable order in any logging that's produced during the walk.
callNames := make([]string, 0, len(calls))
for k := range calls {
callNames = append(callNames, k)
}
sort.Strings(callNames)
for _, callName := range callNames {
call := calls[callName]
path := make([]string, len(parent.Path)+1)
copy(path, parent.Path)
path[len(path)-1] = call.Name
vc, err := version.NewConstraint(call.Version)
if err != nil {
diags = diags.Append(wrapDiagnostic(tfconfig.Diagnostic{
Severity: tfconfig.DiagError,
Summary: "Invalid version constraint",
Detail: fmt.Sprintf("Module %q (declared at %s line %d) has invalid version constraint: %s.", callName, call.Pos.Filename, call.Pos.Line, err),
}))
continue
}
req := ModuleRequest{
Name: call.Name,
Path: path,
SourceAddr: call.Source,
VersionConstraints: vc,
Parent: parent,
CallPos: call.Pos,
}
mod, ver, modDiags := walker.LoadModule(&req)
diags = append(diags, modDiags...)
if mod == nil {
// nil can be returned if the source address was invalid and so
// nothing could be loaded whatsoever. LoadModule should've
// returned at least one error diagnostic in that case.
continue
}
child := &Config{
Parent: parent,
Root: parent.Root,
Path: path,
Module: mod,
CallPos: call.Pos,
SourceAddr: call.Source,
Version: ver,
}
child.Children, modDiags = buildChildModules(child, walker)
diags = diags.Append(modDiags)
ret[call.Name] = child
}
return ret, diags
}
// ModuleRequest is used as part of the ModuleWalker interface used with
// function BuildConfig.
type ModuleRequest struct {
// Name is the "logical name" of the module call within configuration.
// This is provided in case the name is used as part of a storage key
// for the module, but implementations must otherwise treat it as an
// opaque string. It is guaranteed to have already been validated as an
// HCL identifier and UTF-8 encoded.
Name string
// Path is a list of logical names that traverse from the root module to
// this module. This can be used, for example, to form a lookup key for
// each distinct module call in a configuration, allowing for multiple
// calls with the same name at different points in the tree.
Path addrs.Module
// SourceAddr is the source address string provided by the user in
// configuration.
SourceAddr string
// VersionConstraint is the version constraint applied to the module in
// configuration.
VersionConstraints version.Constraints
// Parent is the partially-constructed module tree node that the loaded
// module will be added to. Callers may refer to any field of this
// structure except Children, which is still under construction when
// ModuleRequest objects are created and thus has undefined content.
// The main reason this is provided is so that full module paths can
// be constructed for uniqueness.
Parent *Config
// CallRange is the source position for the header of the "module" block
// in configuration that prompted this request.
CallPos tfconfig.SourcePos
}
// ModuleWalker is an interface used with BuildConfig.
type ModuleWalker interface {
LoadModule(req *ModuleRequest) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics)
}
// ModuleWalkerFunc is an implementation of ModuleWalker that directly wraps
// a callback function, for more convenient use of that interface.
type ModuleWalkerFunc func(req *ModuleRequest) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics)
func (f ModuleWalkerFunc) LoadModule(req *ModuleRequest) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) {
return f(req)
}

@ -0,0 +1,78 @@
package earlyconfig
import (
"fmt"
"github.com/hashicorp/terraform-config-inspect/tfconfig"
"github.com/hashicorp/terraform/tfdiags"
)
func wrapDiagnostics(diags tfconfig.Diagnostics) tfdiags.Diagnostics {
ret := make(tfdiags.Diagnostics, len(diags))
for i, diag := range diags {
ret[i] = wrapDiagnostic(diag)
}
return ret
}
func wrapDiagnostic(diag tfconfig.Diagnostic) tfdiags.Diagnostic {
return wrappedDiagnostic{
d: diag,
}
}
type wrappedDiagnostic struct {
d tfconfig.Diagnostic
}
func (d wrappedDiagnostic) Severity() tfdiags.Severity {
switch d.d.Severity {
case tfconfig.DiagError:
return tfdiags.Error
case tfconfig.DiagWarning:
return tfdiags.Warning
default:
// Should never happen since there are no other severities
return 0
}
}
func (d wrappedDiagnostic) Description() tfdiags.Description {
// Since the inspect library doesn't produce precise source locations,
// we include the position information as part of the error message text.
// See the comment inside method "Source" for more information.
switch {
case d.d.Pos == nil:
return tfdiags.Description{
Summary: d.d.Summary,
Detail: d.d.Detail,
}
case d.d.Detail != "":
return tfdiags.Description{
Summary: d.d.Summary,
Detail: fmt.Sprintf("On %s line %d: %s", d.d.Pos.Filename, d.d.Pos.Line, d.d.Detail),
}
default:
return tfdiags.Description{
Summary: fmt.Sprintf("%s (on %s line %d)", d.d.Summary, d.d.Pos.Filename, d.d.Pos.Line),
}
}
}
func (d wrappedDiagnostic) Source() tfdiags.Source {
// Since the inspect library is constrained by the lowest common denominator
// between legacy HCL and modern HCL, it only returns ranges at whole-line
// granularity, and that isn't sufficient to populate a tfdiags.Source
// and so we'll just omit ranges altogether and include the line number in
// the Description text.
//
// Callers that want to return nicer errors should consider reacting to
// earlyconfig errors by attempting a follow-up parse with the normal
// config loader, which can produce more precise source location
// information.
return tfdiags.Source{}
}
func (d wrappedDiagnostic) FromExpr() *tfdiags.FromExpr {
return nil
}

@ -0,0 +1,20 @@
// Package earlyconfig is a specialized alternative to the top-level "configs"
// package that does only shallow processing of configuration and is therefore
// able to be much more liberal than the full config loader in what it accepts.
//
// In particular, it can accept both current and legacy HCL syntax, and it
// ignores top-level blocks that it doesn't recognize. These two characteristics
// make this package ideal for dependency-checking use-cases so that we are
// more likely to be able to return an error message about an explicit
// incompatibility than to return a less-actionable message about a construct
// not being supported.
//
// However, its liberal approach also means it should be used sparingly. It
// exists primarily for "terraform init", so that it is able to detect
// incompatibilities more robustly when installing dependencies. For most
// other use-cases, use the "configs" and "configs/configload" packages.
//
// Package earlyconfig is a wrapper around the terraform-config-inspect
// codebase, adding to it just some helper functionality for Terraform's own
// use-cases.
package earlyconfig

@ -0,0 +1,13 @@
package earlyconfig
import (
"github.com/hashicorp/terraform-config-inspect/tfconfig"
"github.com/hashicorp/terraform/tfdiags"
)
// LoadModule loads some top-level metadata for the module in the given
// directory.
func LoadModule(dir string) (*tfconfig.Module, tfdiags.Diagnostics) {
mod, diags := tfconfig.LoadModule(dir)
return mod, wrapDiagnostics(diags)
}
Loading…
Cancel
Save