moduledeps: new package for representing module dependencies
As we add support for versioned providers, it's getting more complex to track the dependencies of each module and of the configuration as a whole, so this new package is intended to give us some room to model that nicely as a building block for the various aspects of dependency management. This package is not responsible for *building* the dependency data structure, since that requires knowledge of core Terraform and that would create cyclic package dependencies. A later change will add some logic in Terraform to create a Module tree based on the combination of a given configuration and state, returning an instance of this package's Module type. The Module.PluginRequirements method flattens the provider-oriented requirements into a set of plugin-oriented requirements (flattening any provider aliases) giving us what we need to work with the plugin/discovery package to find matching installed plugins. Other later uses of this package will include selecting plugin archives to auto-install from releases.hashicorp.com as part of "terraform init", where the module-oriented level of abstraction here should be useful for giving users good, specific feedback when constraints cannot be met. A "reason" is tracked for each provider dependency with the intent that this would later drive a UI for users to see and understand why a given dependency is present, to aid in debugging sticky issues with dependency resolution.
This commit is contained in:
parent
a1e29ae290
commit
e89b5390ca
|
@ -0,0 +1,43 @@
|
||||||
|
package moduledeps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/terraform/plugin/discovery"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Providers describes a set of provider dependencies for a given module.
|
||||||
|
//
|
||||||
|
// Each named provider instance can have one version constraint.
|
||||||
|
type Providers map[ProviderInstance]ProviderDependency
|
||||||
|
|
||||||
|
// ProviderDependency describes the dependency for a particular provider
|
||||||
|
// instance, including both the set of allowed versions and the reason for
|
||||||
|
// the dependency.
|
||||||
|
type ProviderDependency struct {
|
||||||
|
Versions discovery.VersionSet
|
||||||
|
Reason ProviderDependencyReason
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProviderDependencyReason is an enumeration of reasons why a dependency might be
|
||||||
|
// present.
|
||||||
|
type ProviderDependencyReason int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ProviderDependencyExplicit means that there is an explicit "provider"
|
||||||
|
// block in the configuration for this module.
|
||||||
|
ProviderDependencyExplicit ProviderDependencyReason = iota
|
||||||
|
|
||||||
|
// ProviderDependencyImplicit means that there is no explicit "provider"
|
||||||
|
// block but there is at least one resource that uses this provider.
|
||||||
|
ProviderDependencyImplicit
|
||||||
|
|
||||||
|
// ProviderDependencyInherited is a special case of
|
||||||
|
// ProviderDependencyImplicit where a parent module has defined a
|
||||||
|
// configuration for the provider that has been inherited by at least one
|
||||||
|
// resource in this module.
|
||||||
|
ProviderDependencyInherited
|
||||||
|
|
||||||
|
// ProviderDependencyFromState means that this provider is not currently
|
||||||
|
// referenced by configuration at all, but some existing instances in
|
||||||
|
// the state still depend on it.
|
||||||
|
ProviderDependencyFromState
|
||||||
|
)
|
|
@ -0,0 +1,7 @@
|
||||||
|
// Package moduledeps contains types that can be used to describe the
|
||||||
|
// providers required for all of the modules in a module tree.
|
||||||
|
//
|
||||||
|
// It does not itself contain the functionality for populating such
|
||||||
|
// data structures; that's in Terraform core, since this package intentionally
|
||||||
|
// does not depend on terraform core to avoid package dependency cycles.
|
||||||
|
package moduledeps
|
|
@ -0,0 +1,135 @@
|
||||||
|
package moduledeps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/plugin/discovery"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Module represents the dependencies of a single module, as well being
|
||||||
|
// a node in a tree of such structures representing the dependencies of
|
||||||
|
// an entire configuration.
|
||||||
|
type Module struct {
|
||||||
|
Name string
|
||||||
|
Providers Providers
|
||||||
|
Children []*Module
|
||||||
|
}
|
||||||
|
|
||||||
|
// WalkFunc is a callback type for use with Module.WalkTree
|
||||||
|
type WalkFunc func(path []string, parent *Module, current *Module) error
|
||||||
|
|
||||||
|
// WalkTree calls the given callback once for the receiver and then
|
||||||
|
// once for each descendent, in an order such that parents are called
|
||||||
|
// before their children and siblings are called in the order they
|
||||||
|
// appear in the Children slice.
|
||||||
|
//
|
||||||
|
// When calling the callback, parent will be nil for the first call
|
||||||
|
// for the receiving module, and then set to the direct parent of
|
||||||
|
// each module for the subsequent calls.
|
||||||
|
//
|
||||||
|
// The path given to the callback is valid only until the callback
|
||||||
|
// returns, after which it will be mutated and reused. Callbacks must
|
||||||
|
// therefore copy the path slice if they wish to retain it.
|
||||||
|
//
|
||||||
|
// If the given callback returns an error, the walk will be aborted at
|
||||||
|
// that point and that error returned to the caller.
|
||||||
|
//
|
||||||
|
// This function is not thread-safe for concurrent modifications of the
|
||||||
|
// data structure, so it's the caller's responsibility to arrange for that
|
||||||
|
// should it be needed.
|
||||||
|
//
|
||||||
|
// It is safe for a callback to modify the descendents of the "current"
|
||||||
|
// module, including the ordering of the Children slice itself, but the
|
||||||
|
// callback MUST NOT modify the parent module.
|
||||||
|
func (m *Module) WalkTree(cb WalkFunc) error {
|
||||||
|
return walkModuleTree(make([]string, 0, 1), nil, m, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func walkModuleTree(path []string, parent *Module, current *Module, cb WalkFunc) error {
|
||||||
|
path = append(path, current.Name)
|
||||||
|
err := cb(path, parent, current)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range current.Children {
|
||||||
|
err := walkModuleTree(path, current, child, cb)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortChildren sorts the Children slice into lexicographic order by
|
||||||
|
// name, in-place.
|
||||||
|
//
|
||||||
|
// This is primarily useful prior to calling WalkTree so that the walk
|
||||||
|
// will proceed in a consistent order.
|
||||||
|
func (m *Module) SortChildren() {
|
||||||
|
sort.Sort(sortModules{m.Children})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortDescendents is a convenience wrapper for calling SortChildren on
|
||||||
|
// the receiver and all of its descendent modules.
|
||||||
|
func (m *Module) SortDescendents() {
|
||||||
|
m.WalkTree(func(path []string, parent *Module, current *Module) error {
|
||||||
|
current.SortChildren()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type sortModules struct {
|
||||||
|
modules []*Module
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sortModules) Len() int {
|
||||||
|
return len(s.modules)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sortModules) Less(i, j int) bool {
|
||||||
|
cmp := strings.Compare(s.modules[i].Name, s.modules[j].Name)
|
||||||
|
return cmp < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s sortModules) Swap(i, j int) {
|
||||||
|
s.modules[i], s.modules[j] = s.modules[j], s.modules[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginRequirements produces a PluginRequirements structure that can
|
||||||
|
// be used with discovery.PluginMetaSet.ConstrainVersions to identify
|
||||||
|
// suitable plugins to satisfy the module's provider dependencies.
|
||||||
|
//
|
||||||
|
// This method only considers the direct requirements of the receiver.
|
||||||
|
// Use AllPluginRequirements to flatten the dependencies for the
|
||||||
|
// entire tree of modules.
|
||||||
|
func (m *Module) PluginRequirements() discovery.PluginRequirements {
|
||||||
|
ret := make(discovery.PluginRequirements)
|
||||||
|
for inst, dep := range m.Providers {
|
||||||
|
// m.Providers is keyed on provider names, such as "aws.foo".
|
||||||
|
// a PluginRequirements wants keys to be provider *types*, such
|
||||||
|
// as "aws". If there are multiple aliases for the same
|
||||||
|
// provider then we will flatten them into a single requirement
|
||||||
|
// by using Intersection to merge the version sets.
|
||||||
|
pty := inst.Type()
|
||||||
|
if existing, exists := ret[pty]; exists {
|
||||||
|
ret[pty] = existing.Intersection(dep.Versions)
|
||||||
|
} else {
|
||||||
|
ret[pty] = dep.Versions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllPluginRequirements calls PluginRequirements for the receiver and all
|
||||||
|
// of its descendents, and merges the result into a single PluginRequirements
|
||||||
|
// structure that would satisfy all of the modules together.
|
||||||
|
func (m *Module) AllPluginRequirements() discovery.PluginRequirements {
|
||||||
|
var ret discovery.PluginRequirements
|
||||||
|
m.WalkTree(func(path []string, parent *Module, current *Module) error {
|
||||||
|
ret = ret.Merge(current.PluginRequirements())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return ret
|
||||||
|
}
|
|
@ -0,0 +1,216 @@
|
||||||
|
package moduledeps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/plugin/discovery"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestModuleWalkTree(t *testing.T) {
|
||||||
|
type walkStep struct {
|
||||||
|
Path []string
|
||||||
|
ParentName string
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
Root *Module
|
||||||
|
WalkOrder []walkStep
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
&Module{
|
||||||
|
Name: "root",
|
||||||
|
Children: nil,
|
||||||
|
},
|
||||||
|
[]walkStep{
|
||||||
|
{
|
||||||
|
Path: []string{"root"},
|
||||||
|
ParentName: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&Module{
|
||||||
|
Name: "root",
|
||||||
|
Children: []*Module{
|
||||||
|
{
|
||||||
|
Name: "child",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]walkStep{
|
||||||
|
{
|
||||||
|
Path: []string{"root"},
|
||||||
|
ParentName: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: []string{"root", "child"},
|
||||||
|
ParentName: "root",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&Module{
|
||||||
|
Name: "root",
|
||||||
|
Children: []*Module{
|
||||||
|
{
|
||||||
|
Name: "child",
|
||||||
|
Children: []*Module{
|
||||||
|
{
|
||||||
|
Name: "grandchild",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]walkStep{
|
||||||
|
{
|
||||||
|
Path: []string{"root"},
|
||||||
|
ParentName: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: []string{"root", "child"},
|
||||||
|
ParentName: "root",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: []string{"root", "child", "grandchild"},
|
||||||
|
ParentName: "child",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&Module{
|
||||||
|
Name: "root",
|
||||||
|
Children: []*Module{
|
||||||
|
{
|
||||||
|
Name: "child1",
|
||||||
|
Children: []*Module{
|
||||||
|
{
|
||||||
|
Name: "grandchild1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "child2",
|
||||||
|
Children: []*Module{
|
||||||
|
{
|
||||||
|
Name: "grandchild2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[]walkStep{
|
||||||
|
{
|
||||||
|
Path: []string{"root"},
|
||||||
|
ParentName: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: []string{"root", "child1"},
|
||||||
|
ParentName: "root",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: []string{"root", "child1", "grandchild1"},
|
||||||
|
ParentName: "child1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: []string{"root", "child2"},
|
||||||
|
ParentName: "root",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: []string{"root", "child2", "grandchild2"},
|
||||||
|
ParentName: "child2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
|
||||||
|
wo := test.WalkOrder
|
||||||
|
test.Root.WalkTree(func(path []string, parent *Module, current *Module) error {
|
||||||
|
if len(wo) == 0 {
|
||||||
|
t.Fatalf("ran out of walk steps while expecting one for %#v", path)
|
||||||
|
}
|
||||||
|
step := wo[0]
|
||||||
|
wo = wo[1:]
|
||||||
|
if got, want := path, step.Path; !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("wrong path %#v; want %#v", got, want)
|
||||||
|
}
|
||||||
|
parentName := ""
|
||||||
|
if parent != nil {
|
||||||
|
parentName = parent.Name
|
||||||
|
}
|
||||||
|
if got, want := parentName, step.ParentName; got != want {
|
||||||
|
t.Errorf("wrong parent name %q; want %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := current.Name, path[len(path)-1]; got != want {
|
||||||
|
t.Errorf("mismatching current.Name %q and final path element %q", got, want)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModuleSortChildren(t *testing.T) {
|
||||||
|
m := &Module{
|
||||||
|
Name: "root",
|
||||||
|
Children: []*Module{
|
||||||
|
{
|
||||||
|
Name: "apple",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "zebra",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "xylophone",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "pig",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m.SortChildren()
|
||||||
|
|
||||||
|
want := []string{"apple", "pig", "xylophone", "zebra"}
|
||||||
|
var got []string
|
||||||
|
for _, c := range m.Children {
|
||||||
|
got = append(got, c.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(want, got) {
|
||||||
|
t.Errorf("wrong order %#v; want %#v", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModulePluginRequirements(t *testing.T) {
|
||||||
|
m := &Module{
|
||||||
|
Name: "root",
|
||||||
|
Providers: Providers{
|
||||||
|
"foo": ProviderDependency{
|
||||||
|
Versions: discovery.ConstraintStr(">=1.0.0").MustParse(),
|
||||||
|
},
|
||||||
|
"foo.bar": ProviderDependency{
|
||||||
|
Versions: discovery.ConstraintStr(">=2.0.0").MustParse(),
|
||||||
|
},
|
||||||
|
"baz": ProviderDependency{
|
||||||
|
Versions: discovery.ConstraintStr(">=3.0.0").MustParse(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
reqd := m.PluginRequirements()
|
||||||
|
if len(reqd) != 2 {
|
||||||
|
t.Errorf("wrong number of elements in %#v; want 2", reqd)
|
||||||
|
}
|
||||||
|
if got, want := reqd["foo"].String(), ">=1.0.0,>=2.0.0"; got != want {
|
||||||
|
t.Errorf("wrong combination of versions for 'foo' %q; want %q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := reqd["baz"].String(), ">=3.0.0"; got != want {
|
||||||
|
t.Errorf("wrong combination of versions for 'baz' %q; want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package moduledeps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProviderInstance describes a particular provider instance by its full name,
|
||||||
|
// like "null" or "aws.foo".
|
||||||
|
type ProviderInstance string
|
||||||
|
|
||||||
|
// Type returns the provider type of this instance. For example, for an instance
|
||||||
|
// named "aws.foo" the type is "aws".
|
||||||
|
func (p ProviderInstance) Type() string {
|
||||||
|
t := string(p)
|
||||||
|
if dotPos := strings.Index(t, "."); dotPos != -1 {
|
||||||
|
t = t[:dotPos]
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alias returns the alias of this provider, if any. An instance named "aws.foo"
|
||||||
|
// has the alias "foo", while an instance named just "docker" has no alias,
|
||||||
|
// so the empty string would be returned.
|
||||||
|
func (p ProviderInstance) Alias() string {
|
||||||
|
t := string(p)
|
||||||
|
if dotPos := strings.Index(t, "."); dotPos != -1 {
|
||||||
|
return t[dotPos+1:]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package moduledeps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProviderInstance(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Name string
|
||||||
|
WantType string
|
||||||
|
WantAlias string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "aws",
|
||||||
|
WantType: "aws",
|
||||||
|
WantAlias: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "aws.foo",
|
||||||
|
WantType: "aws",
|
||||||
|
WantAlias: "foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.Name, func(t *testing.T) {
|
||||||
|
inst := ProviderInstance(test.Name)
|
||||||
|
if got, want := inst.Type(), test.WantType; got != want {
|
||||||
|
t.Errorf("got type %q; want %q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := inst.Alias(), test.WantAlias; got != want {
|
||||||
|
t.Errorf("got alias %q; want %q", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue