configs: BuildConfig function
BuildConfig creates a module tree by recursively walking through module calls in the root module and any descendent modules. This is intended to be used both for the simple case of loading already-installed modules and the more complex case of installing modules inside "terraform init", both of which will be dealt with in a separate package.
This commit is contained in:
parent
cc38e91612
commit
8929eca405
|
@ -1,5 +1,12 @@
|
||||||
package configs
|
package configs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
version "github.com/hashicorp/go-version"
|
||||||
|
"github.com/hashicorp/hcl2/hcl"
|
||||||
|
)
|
||||||
|
|
||||||
// A Config is a node in the tree of modules within a configuration.
|
// A Config is a node in the tree of modules within a configuration.
|
||||||
//
|
//
|
||||||
// The module tree is constructed by following ModuleCall instances recursively
|
// The module tree is constructed by following ModuleCall instances recursively
|
||||||
|
@ -24,7 +31,116 @@ type Config struct {
|
||||||
// Module.ModuleCalls.
|
// Module.ModuleCalls.
|
||||||
Children map[string]*Config
|
Children map[string]*Config
|
||||||
|
|
||||||
// Elements points to the object describing the configuration for the
|
// Module points to the object describing the configuration for the
|
||||||
// various elements (variables, resources, etc) defined by this module.
|
// various elements (variables, resources, etc) defined by this module.
|
||||||
Elements *Module
|
Module *Module
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// SourceAddrRange is the location in the configuration source where the
|
||||||
|
// SourceAddr value was set, for use in diagnostic messages.
|
||||||
|
//
|
||||||
|
// This field is meaningless for the root module, where its contents are undefined.
|
||||||
|
SourceAddrRange hcl.Range
|
||||||
|
|
||||||
|
// Version is the specific version that was selected for this module,
|
||||||
|
// based on version constraints given in configuration.
|
||||||
|
//
|
||||||
|
// This field is meaningless for the root module, where its contents are undefined.
|
||||||
|
Version *version.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path returns the path of logical names that lead to this Config from its
|
||||||
|
// root.
|
||||||
|
//
|
||||||
|
// This function should not be used to display a path to the end-user, since
|
||||||
|
// our UI conventions call for us to return a module address string in that
|
||||||
|
// case, and a module address string ought to be built from the dynamic
|
||||||
|
// module tree (resulting from evaluating "count" and "for_each" arguments
|
||||||
|
// on our calls to produce potentially multiple child instances per call)
|
||||||
|
// rather than from our static module tree.
|
||||||
|
//
|
||||||
|
// This function will panic if called on a config that is not part of a
|
||||||
|
// wholesome config tree, e.g. because it has incorrectly-built Children
|
||||||
|
// maps, missing node pointers, etc. However, it should work as expected
|
||||||
|
// for any tree constructed by BuildConfig and not subsequently modified.
|
||||||
|
func (c *Config) Path() []string {
|
||||||
|
// The implementation here is not especially efficient, but we don't
|
||||||
|
// care too much because module trees are shallow and narrow in all
|
||||||
|
// reasonable configurations.
|
||||||
|
|
||||||
|
// We'll build our path in reverse here, since we're starting at the
|
||||||
|
// leafiest node, and then we'll flip it before we return.
|
||||||
|
path := make([]string, 0, c.Depth())
|
||||||
|
|
||||||
|
this := c
|
||||||
|
for this.Parent != nil {
|
||||||
|
parent := this.Parent
|
||||||
|
var name string
|
||||||
|
for candidate, ref := range parent.Children {
|
||||||
|
if ref == this {
|
||||||
|
name = candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
panic(fmt.Errorf(
|
||||||
|
"Config %p does not appear in the child table for its parent %p: %#v",
|
||||||
|
this, parent, parent.Children,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
path = append(path, name)
|
||||||
|
this = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// reverse the items
|
||||||
|
for i := 0; i < len(path)/2; i++ {
|
||||||
|
j := len(path) - i - 1
|
||||||
|
path[i], path[j] = path[j], path[i]
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depth returns the number of "hops" the receiver is from the root of its
|
||||||
|
// module tree, with the root module having a depth of zero.
|
||||||
|
func (c *Config) Depth() int {
|
||||||
|
ret := 0
|
||||||
|
this := c
|
||||||
|
for this.Parent != nil {
|
||||||
|
ret++
|
||||||
|
this = this.Parent
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepEach calls the given function once for each module in the tree, starting
|
||||||
|
// with the receiver.
|
||||||
|
//
|
||||||
|
// A parent is always called before its children and children of a particular
|
||||||
|
// node are visited in lexicographic order by their names.
|
||||||
|
func (c *Config) DeepEach(cb func(c *Config)) {
|
||||||
|
cb(c)
|
||||||
|
|
||||||
|
names := make([]string, 0, len(c.Children))
|
||||||
|
for name := range c.Children {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range names {
|
||||||
|
c.Children[name].DeepEach(cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllModules returns a slice of all the receiver and all of its descendent
|
||||||
|
// nodes in the module tree, in the same order they would be visited by
|
||||||
|
// DeepEach.
|
||||||
|
func (c *Config) AllModules() []*Config {
|
||||||
|
var ret []*Config
|
||||||
|
c.DeepEach(func(c *Config) {
|
||||||
|
ret = append(ret, c)
|
||||||
|
})
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
package configs
|
||||||
|
|
||||||
|
import (
|
||||||
|
version "github.com/hashicorp/go-version"
|
||||||
|
"github.com/hashicorp/hcl2/hcl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BuildConfig constructs a Config from a root module by loading all of its
|
||||||
|
// descendent modules via the given ModuleWalker.
|
||||||
|
//
|
||||||
|
// The result is a module tree that has so far only had basic module- and
|
||||||
|
// file-level invariants validated. If the returned diagnostics contains errors,
|
||||||
|
// the returned module tree may be incomplete but can still be used carefully
|
||||||
|
// for static analysis.
|
||||||
|
func BuildConfig(root *Module, walker ModuleWalker) (*Config, hcl.Diagnostics) {
|
||||||
|
var diags hcl.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, hcl.Diagnostics) {
|
||||||
|
var diags hcl.Diagnostics
|
||||||
|
ret := map[string]*Config{}
|
||||||
|
|
||||||
|
calls := parent.Module.ModuleCalls
|
||||||
|
|
||||||
|
for _, call := range calls {
|
||||||
|
req := ModuleRequest{
|
||||||
|
Name: call.Name,
|
||||||
|
SourceAddr: call.SourceAddr,
|
||||||
|
SourceAddrRange: call.SourceAddrRange,
|
||||||
|
VersionConstraints: []VersionConstraint{call.Version},
|
||||||
|
Parent: parent,
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
Module: mod,
|
||||||
|
SourceAddr: call.SourceAddr,
|
||||||
|
SourceAddrRange: call.SourceAddrRange,
|
||||||
|
Version: ver,
|
||||||
|
}
|
||||||
|
|
||||||
|
child.Children, modDiags = buildChildModules(child, walker)
|
||||||
|
|
||||||
|
ret[call.Name] = child
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// A ModuleWalker knows how to find and load a child module given details about
|
||||||
|
// the module to be loaded and a reference to its partially-loaded parent
|
||||||
|
// Config.
|
||||||
|
type ModuleWalker interface {
|
||||||
|
// LoadModule finds and loads a requested child module.
|
||||||
|
//
|
||||||
|
// If errors are detected during loading, implementations should return them
|
||||||
|
// in the diagnostics object. If the diagnostics object contains any errors
|
||||||
|
// then the caller will tolerate the returned module being nil or incomplete.
|
||||||
|
// If no errors are returned, it should be non-nil and complete.
|
||||||
|
//
|
||||||
|
// Full validation need not have been performed but an implementation should
|
||||||
|
// ensure that the basic file- and module-validations performed by the
|
||||||
|
// LoadConfigDir function (valid syntax, no namespace collisions, etc) have
|
||||||
|
// been performed before returning a module.
|
||||||
|
LoadModule(req *ModuleRequest) (*Module, *version.Version, hcl.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) (*Module, *version.Version, hcl.Diagnostics)
|
||||||
|
|
||||||
|
// LoadModule implements ModuleWalker.
|
||||||
|
func (f ModuleWalkerFunc) LoadModule(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) {
|
||||||
|
return f(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModuleRequest is used with the ModuleWalker interface to describe a child
|
||||||
|
// module that must be loaded.
|
||||||
|
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
|
||||||
|
|
||||||
|
// SourceAddr is the source address string provided by the user in
|
||||||
|
// configuration.
|
||||||
|
SourceAddr string
|
||||||
|
|
||||||
|
// SourceAddrRange is the source range for the SourceAddr value as it
|
||||||
|
// was provided in configuration. This can and should be used to generate
|
||||||
|
// diagnostics about the source address having invalid syntax, referring
|
||||||
|
// to a non-existent object, etc.
|
||||||
|
SourceAddrRange hcl.Range
|
||||||
|
|
||||||
|
// VersionConstraints are the constraints applied to the module in
|
||||||
|
// configuration. This data structure includes the source range for
|
||||||
|
// each constraint, which can and should be used to generate diagnostics
|
||||||
|
// about constraint-related issues, such as constraints that eliminate all
|
||||||
|
// available versions of a module whose source is otherwise valid.
|
||||||
|
VersionConstraints []VersionConstraint
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package configs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
|
||||||
|
version "github.com/hashicorp/go-version"
|
||||||
|
"github.com/hashicorp/hcl2/hcl"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildConfig(t *testing.T) {
|
||||||
|
parser := NewParser(nil)
|
||||||
|
mod, diags := parser.LoadConfigDir("test-fixtures/config-build")
|
||||||
|
assertNoDiagnostics(t, diags)
|
||||||
|
if mod == nil {
|
||||||
|
t.Fatal("got nil root module; want non-nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
versionI := 0
|
||||||
|
cfg, diags := BuildConfig(mod, ModuleWalkerFunc(
|
||||||
|
func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) {
|
||||||
|
// For the sake of this test we're going to just treat our
|
||||||
|
// SourceAddr as a path relative to our fixture directory.
|
||||||
|
// A "real" implementation of ModuleWalker should accept the
|
||||||
|
// various different source address syntaxes Terraform supports.
|
||||||
|
sourcePath := filepath.Join("test-fixtures/config-build", req.SourceAddr)
|
||||||
|
|
||||||
|
mod, diags := parser.LoadConfigDir(sourcePath)
|
||||||
|
version, _ := version.NewVersion(fmt.Sprintf("1.0.%d", versionI))
|
||||||
|
versionI++
|
||||||
|
return mod, version, diags
|
||||||
|
},
|
||||||
|
))
|
||||||
|
assertNoDiagnostics(t, diags)
|
||||||
|
if cfg == nil {
|
||||||
|
t.Fatal("got nil config; want non-nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
var got []string
|
||||||
|
cfg.DeepEach(func(c *Config) {
|
||||||
|
got = append(got, fmt.Sprintf("%s %s", strings.Join(c.Path(), "."), c.Version))
|
||||||
|
})
|
||||||
|
sort.Strings(got)
|
||||||
|
want := []string{
|
||||||
|
" <nil>",
|
||||||
|
"child_a 1.0.0",
|
||||||
|
"child_a.child_c 1.0.1",
|
||||||
|
"child_b 1.0.2",
|
||||||
|
"child_b.child_c 1.0.3",
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("wrong result\ngot: %swant: %s", spew.Sdump(got), spew.Sdump(want))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := cfg.Children["child_a"].Children["child_c"].Module.Outputs["hello"]; !exists {
|
||||||
|
t.Fatalf("missing output 'hello' in child_a.child_c")
|
||||||
|
}
|
||||||
|
if _, exists := cfg.Children["child_b"].Children["child_c"].Module.Outputs["hello"]; !exists {
|
||||||
|
t.Fatalf("missing output 'hello' in child_b.child_c")
|
||||||
|
}
|
||||||
|
if cfg.Children["child_a"].Children["child_c"].Module == cfg.Children["child_b"].Children["child_c"].Module {
|
||||||
|
t.Fatalf("child_a.child_c is same object as child_b.child_c; should not be")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
module "child_c" {
|
||||||
|
source = "child_c"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
module "child_c" {
|
||||||
|
# In the unit test where this fixture is used, we treat the source strings
|
||||||
|
# as absolute paths rather than as source addresses as we would in a real
|
||||||
|
# module walker.
|
||||||
|
source = "child_c"
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
output "hello" {
|
||||||
|
value = "hello"
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
module "child_a" {
|
||||||
|
source = "child_a"
|
||||||
|
}
|
||||||
|
|
||||||
|
module "child_b" {
|
||||||
|
source = "child_b"
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue