mirror of https://github.com/hashicorp/terraform
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
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…
Reference in new issue