terraform: provider config inheritance in modules

This commit is contained in:
Mitchell Hashimoto 2015-02-09 23:32:28 -08:00
parent 0a68576746
commit d847b2b672
17 changed files with 199 additions and 91 deletions

View File

@ -160,6 +160,29 @@ func TestContext2Validate_moduleBadResource(t *testing.T) {
}
}
func TestContext2Validate_moduleProviderInherit(t *testing.T) {
m := testModule(t, "validate-module-pc-inherit")
p := testProvider("aws")
c := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
})
p.ValidateFn = func(c *ResourceConfig) ([]string, []error) {
return nil, c.CheckSet([]string{"set"})
}
w, e := c.Validate()
if len(w) > 0 {
t.Fatalf("bad: %#v", w)
}
if len(e) > 0 {
t.Fatalf("bad: %s", e)
}
}
func TestContext2Validate_orphans(t *testing.T) {
p := testProvider("aws")
m := testModule(t, "validate-good")
@ -543,29 +566,6 @@ func TestContext2Validate_varRefFilled(t *testing.T) {
}
/*
func TestContextValidate_moduleProviderInherit(t *testing.T) {
m := testModule(t, "validate-module-pc-inherit")
p := testProvider("aws")
c := testContext(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
})
p.ValidateFn = func(c *ResourceConfig) ([]string, []error) {
return nil, c.CheckSet([]string{"set"})
}
w, e := c.Validate()
if len(w) > 0 {
t.Fatalf("bad: %#v", w)
}
if len(e) > 0 {
t.Fatalf("bad: %#v", e)
}
}
func TestContextInput(t *testing.T) {
input := new(MockUIInput)
m := testModule(t, "input-vars")

View File

@ -19,6 +19,13 @@ type EvalContext interface {
// initialized) or returns nil if the provider isn't initialized.
Provider(string) ResourceProvider
// ConfigureProvider configures the provider with the given
// configuration. This is a separate context call because this call
// is used to store the provider configuration for inheritance lookups
// with ParentProviderConfig().
ConfigureProvider(string, *ResourceConfig) error
ParentProviderConfig(string) *ResourceConfig
// InitProvisioner initializes the provisioner with the given name and
// returns the implementation of the resource provisioner or an error.
//
@ -49,6 +56,15 @@ type MockEvalContext struct {
ProviderName string
ProviderProvider ResourceProvider
ConfigureProviderCalled bool
ConfigureProviderName string
ConfigureProviderConfig *ResourceConfig
ConfigureProviderError error
ParentProviderConfigCalled bool
ParentProviderConfigName string
ParentProviderConfigConfig *ResourceConfig
InitProvisionerCalled bool
InitProvisionerName string
InitProvisionerProvisioner ResourceProvisioner
@ -80,6 +96,19 @@ func (c *MockEvalContext) Provider(n string) ResourceProvider {
return c.ProviderProvider
}
func (c *MockEvalContext) ConfigureProvider(n string, cfg *ResourceConfig) error {
c.ConfigureProviderCalled = true
c.ConfigureProviderName = n
c.ConfigureProviderConfig = cfg
return c.ConfigureProviderError
}
func (c *MockEvalContext) ParentProviderConfig(n string) *ResourceConfig {
c.ParentProviderConfigCalled = true
c.ParentProviderConfigName = n
return c.ParentProviderConfigConfig
}
func (c *MockEvalContext) InitProvisioner(n string) (ResourceProvisioner, error) {
c.InitProvisionerCalled = true
c.InitProvisionerName = n

View File

@ -12,14 +12,15 @@ import (
// BuiltinEvalContext is an EvalContext implementation that is used by
// Terraform by default.
type BuiltinEvalContext struct {
PathValue []string
Interpolater *Interpolater
Providers map[string]ResourceProviderFactory
ProviderCache map[string]ResourceProvider
ProviderLock *sync.Mutex
Provisioners map[string]ResourceProvisionerFactory
ProvisionerCache map[string]ResourceProvisioner
ProvisionerLock *sync.Mutex
PathValue []string
Interpolater *Interpolater
Providers map[string]ResourceProviderFactory
ProviderCache map[string]ResourceProvider
ProviderConfigCache map[string]*ResourceConfig
ProviderLock *sync.Mutex
Provisioners map[string]ResourceProvisionerFactory
ProvisionerCache map[string]ResourceProvisioner
ProvisionerLock *sync.Mutex
once sync.Once
}
@ -47,7 +48,7 @@ func (ctx *BuiltinEvalContext) InitProvider(n string) (ResourceProvider, error)
return nil, err
}
ctx.ProviderCache[ctx.pathCacheKey()] = p
ctx.ProviderCache[ctx.pathCacheKey(ctx.Path())] = p
return p, nil
}
@ -57,7 +58,37 @@ func (ctx *BuiltinEvalContext) Provider(n string) ResourceProvider {
ctx.ProviderLock.Lock()
defer ctx.ProviderLock.Unlock()
return ctx.ProviderCache[ctx.pathCacheKey()]
return ctx.ProviderCache[ctx.pathCacheKey(ctx.Path())]
}
func (ctx *BuiltinEvalContext) ConfigureProvider(
n string, cfg *ResourceConfig) error {
p := ctx.Provider(n)
if p == nil {
return fmt.Errorf("Provider '%s' not initialized", n)
}
// Save the configuration
ctx.ProviderLock.Lock()
ctx.ProviderConfigCache[ctx.pathCacheKey(ctx.Path())] = cfg
ctx.ProviderLock.Unlock()
return p.Configure(cfg)
}
func (ctx *BuiltinEvalContext) ParentProviderConfig(n string) *ResourceConfig {
ctx.ProviderLock.Lock()
defer ctx.ProviderLock.Unlock()
path := ctx.Path()
for i := len(path) - 1; i >= 1; i-- {
k := ctx.pathCacheKey(path[:i])
if v, ok := ctx.ProviderConfigCache[k]; ok {
return v
}
}
return nil
}
func (ctx *BuiltinEvalContext) InitProvisioner(
@ -84,7 +115,7 @@ func (ctx *BuiltinEvalContext) InitProvisioner(
return nil, err
}
ctx.ProvisionerCache[ctx.pathCacheKey()] = p
ctx.ProvisionerCache[ctx.pathCacheKey(ctx.Path())] = p
return p, nil
}
@ -94,7 +125,7 @@ func (ctx *BuiltinEvalContext) Provisioner(n string) ResourceProvisioner {
ctx.ProvisionerLock.Lock()
defer ctx.ProvisionerLock.Unlock()
return ctx.ProvisionerCache[ctx.pathCacheKey()]
return ctx.ProvisionerCache[ctx.pathCacheKey(ctx.Path())]
}
func (ctx *BuiltinEvalContext) Interpolate(
@ -137,12 +168,12 @@ func (ctx *BuiltinEvalContext) init() {
//
// This is used because there is a variety of information that needs to be
// cached per-path, rather than per-context.
func (ctx *BuiltinEvalContext) pathCacheKey() string {
func (ctx *BuiltinEvalContext) pathCacheKey(path []string) string {
// There is probably a better way to do this, but this is working for now.
// We just create an MD5 hash of all the MD5 hashes of all the path
// elements. This gets us the property that it is unique per ordering.
hash := md5.New()
for _, p := range ctx.Path() {
for _, p := range path {
single := md5.Sum([]byte(p))
if _, err := hash.Write(single[:]); err != nil {
panic(err)

View File

@ -7,20 +7,25 @@ import (
// EvalConfigProvider is an EvalNode implementation that configures
// a provider that is already initialized and retrieved.
type EvalConfigProvider struct {
Provider EvalNode
Provider string
Config EvalNode
}
func (n *EvalConfigProvider) Args() ([]EvalNode, []EvalType) {
return []EvalNode{n.Provider, n.Config},
[]EvalType{EvalTypeResourceProvider, EvalTypeConfig}
return []EvalNode{n.Config}, []EvalType{EvalTypeConfig}
}
func (n *EvalConfigProvider) Eval(
ctx EvalContext, args []interface{}) (interface{}, error) {
provider := args[0].(ResourceProvider)
config := args[1].(*ResourceConfig)
return nil, provider.Configure(config)
config := args[0].(*ResourceConfig)
// Get the parent configuration if there is one
if parent := ctx.ParentProviderConfig(n.Provider); parent != nil {
merged := parent.raw.Merge(config.raw)
config = NewResourceConfig(merged)
}
return nil, ctx.ConfigureProvider(n.Provider, config)
}
func (n *EvalConfigProvider) Type() EvalType {

View File

@ -15,7 +15,7 @@ func TestEvalConfigProvider(t *testing.T) {
n := &EvalConfigProvider{}
ctx := &MockEvalContext{ProviderProvider: provider}
args := []interface{}{provider, config}
args := []interface{}{config}
if actual, err := n.Eval(ctx, args); err != nil {
t.Fatalf("err: %s", err)
} else if actual != nil {
@ -32,14 +32,12 @@ func TestEvalConfigProvider(t *testing.T) {
func TestEvalConfigProvider_args(t *testing.T) {
config := testResourceConfig(t, map[string]interface{}{})
provider := &MockResourceProvider{}
providerNode := &EvalLiteral{Value: provider}
configNode := &EvalLiteral{Value: config}
n := &EvalConfigProvider{Provider: providerNode, Config: configNode}
n := &EvalConfigProvider{Provider: "foo", Config: configNode}
args, types := n.Args()
expectedArgs := []EvalNode{providerNode, configNode}
expectedTypes := []EvalType{EvalTypeResourceProvider, EvalTypeConfig}
expectedArgs := []EvalNode{configNode}
expectedTypes := []EvalType{EvalTypeConfig}
if !reflect.DeepEqual(args, expectedArgs) {
t.Fatalf("bad: %#v", args)
}

View File

@ -14,7 +14,7 @@ type EvalValidateError struct {
}
func (e *EvalValidateError) Error() string {
return ""
return fmt.Sprintf("Warnings: %s. Errors: %s", e.Warnings, e.Errors)
}
// EvalValidateCount is an EvalNode implementation that validates
@ -63,8 +63,9 @@ func (n *EvalValidateCount) Type() EvalType {
// EvalValidateProvider is an EvalNode implementation that validates
// the configuration of a resource.
type EvalValidateProvider struct {
Provider EvalNode
Config EvalNode
ProviderName string
Provider EvalNode
Config EvalNode
}
func (n *EvalValidateProvider) Args() ([]EvalNode, []EvalType) {
@ -76,6 +77,13 @@ func (n *EvalValidateProvider) Eval(
ctx EvalContext, args []interface{}) (interface{}, error) {
provider := args[0].(ResourceProvider)
config := args[1].(*ResourceConfig)
// Get the parent configuration if there is one
if parent := ctx.ParentProviderConfig(n.ProviderName); parent != nil {
merged := parent.raw.Merge(config.raw)
config = NewResourceConfig(merged)
}
warns, errs := provider.Validate(config)
if len(warns) == 0 && len(errs) == 0 {
return nil, nil

View File

@ -11,11 +11,12 @@ func ProviderEvalTree(n string, config *config.RawConfig) EvalNode {
Nodes: []EvalNode{
&EvalInitProvider{Name: n},
&EvalValidateProvider{
Provider: &EvalGetProvider{Name: n},
Config: &EvalInterpolate{Config: config},
ProviderName: n,
Provider: &EvalGetProvider{Name: n},
Config: &EvalInterpolate{Config: config},
},
&EvalConfigProvider{
Provider: &EvalGetProvider{Name: n},
Provider: n,
Config: &EvalInterpolate{Config: config},
},
},

View File

@ -3,6 +3,7 @@ package terraform
import (
"fmt"
"log"
"strings"
"sync"
"github.com/hashicorp/terraform/dag"
@ -132,10 +133,13 @@ func (g *Graph) walk(walker GraphWalker) error {
ctx := walker.EnterGraph(g)
defer walker.ExitGraph(g)
// Get the path for logs
path := strings.Join(ctx.Path(), ".")
// Walk the graph.
var walkFn dag.WalkFunc
walkFn = func(v dag.Vertex) (rerr error) {
log.Printf("[DEBUG] vertex %s: walking", dag.VertexName(v))
log.Printf("[DEBUG] vertex %s.%s: walking", path, dag.VertexName(v))
walker.EnterVertex(v)
defer func() { walker.ExitVertex(v, rerr) }()
@ -145,12 +149,12 @@ func (g *Graph) walk(walker GraphWalker) error {
tree := ev.EvalTree()
if tree == nil {
panic(fmt.Sprintf(
"%s (%T): nil eval tree", dag.VertexName(v), v))
"%s.%s (%T): nil eval tree", path, dag.VertexName(v), v))
}
// Allow the walker to change our tree if needed. Eval,
// then callback with the output.
log.Printf("[DEBUG] vertex %s: evaluating", dag.VertexName(v))
log.Printf("[DEBUG] vertex %s.%s: evaluating", path, dag.VertexName(v))
tree = walker.EnterEvalTree(v, tree)
output, err := Eval(tree, ctx)
walker.ExitEvalTree(v, output, err)
@ -159,7 +163,8 @@ func (g *Graph) walk(walker GraphWalker) error {
// If the node is dynamically expanded, then expand it
if ev, ok := v.(GraphNodeDynamicExpandable); ok {
log.Printf(
"[DEBUG] vertex %s: expanding dynamic subgraph",
"[DEBUG] vertex %s.%s: expanding/walking dynamic subgraph",
path,
dag.VertexName(v))
g, err := ev.DynamicExpand(ctx)
if err != nil {
@ -176,7 +181,8 @@ func (g *Graph) walk(walker GraphWalker) error {
// If the node has a subgraph, then walk the subgraph
if sn, ok := v.(GraphNodeSubgraph); ok {
log.Printf(
"[DEBUG] vertex %s: walking subgraph",
"[DEBUG] vertex %s.%s: walking subgraph",
path,
dag.VertexName(v))
if rerr = sn.Subgraph().walk(walker); rerr != nil {

View File

@ -122,5 +122,6 @@ aws_security_group.firewall
provider.aws
module.consul (expanded)
aws_security_group.firewall
provider.aws
provider.aws
`

View File

@ -52,6 +52,30 @@ func (n *GraphNodeConfigModule) Expand(b GraphBuilder) (*Graph, error) {
return b.Build(n.Path)
}
// GraphNodeExpandable
func (n *GraphNodeConfigModule) ProvidedBy() []string {
// Build up the list of providers by simply going over our configuration
// to find the providers that are configured there as well as the
// providers that the resources use.
config := n.Tree.Config()
providers := make(map[string]struct{})
for _, p := range config.ProviderConfigs {
providers[p.Name] = struct{}{}
}
for _, r := range config.Resources {
providers[resourceProvider(r.Type)] = struct{}{}
}
// Turn the map into a string. This makes sure that the list is
// de-dupped since we could be going over potentially many resources.
result := make([]string, 0, len(providers))
for p, _ := range providers {
result = append(result, p)
}
return result
}
// GraphNodeConfigProvider represents a configured provider within the
// configuration graph. These are only immediately in the graph when an
// explicit `provider` configuration block is in the configuration.
@ -144,8 +168,8 @@ func (n *GraphNodeConfigResource) EvalTree() EvalNode {
}
// GraphNodeProviderConsumer
func (n *GraphNodeConfigResource) ProvidedBy() string {
return resourceProvider(n.Resource.Type)
func (n *GraphNodeConfigResource) ProvidedBy() []string {
return []string{resourceProvider(n.Resource.Type)}
}
// GraphNodeProvisionerConsumer

View File

@ -71,7 +71,7 @@ func TestGraphNodeConfigResource_ProvidedBy(t *testing.T) {
Resource: &config.Resource{Type: "aws_instance"},
}
if v := n.ProvidedBy(); v != "aws" {
if v := n.ProvidedBy(); v[0] != "aws" {
t.Fatalf("bad: %#v", v)
}
}

View File

@ -22,25 +22,27 @@ type ContextGraphWalker struct {
ValidationWarnings []string
ValidationErrors []error
errorLock sync.Mutex
once sync.Once
providerCache map[string]ResourceProvider
providerLock sync.Mutex
provisionerCache map[string]ResourceProvisioner
provisionerLock sync.Mutex
errorLock sync.Mutex
once sync.Once
providerCache map[string]ResourceProvider
providerConfigCache map[string]*ResourceConfig
providerLock sync.Mutex
provisionerCache map[string]ResourceProvisioner
provisionerLock sync.Mutex
}
func (w *ContextGraphWalker) EnterGraph(g *Graph) EvalContext {
w.once.Do(w.init)
return &BuiltinEvalContext{
PathValue: g.Path,
Providers: w.Context.providers,
ProviderCache: w.providerCache,
ProviderLock: &w.providerLock,
Provisioners: w.Context.provisioners,
ProvisionerCache: w.provisionerCache,
ProvisionerLock: &w.provisionerLock,
PathValue: g.Path,
Providers: w.Context.providers,
ProviderCache: w.providerCache,
ProviderConfigCache: w.providerConfigCache,
ProviderLock: &w.providerLock,
Provisioners: w.Context.provisioners,
ProvisionerCache: w.provisionerCache,
ProvisionerLock: &w.provisionerLock,
Interpolater: &Interpolater{
Operation: w.Operation,
Module: w.Context.module,
@ -77,5 +79,6 @@ func (w *ContextGraphWalker) ExitEvalTree(
func (w *ContextGraphWalker) init() {
w.providerCache = make(map[string]ResourceProvider, 5)
w.providerConfigCache = make(map[string]*ResourceConfig, 5)
w.provisionerCache = make(map[string]ResourceProvisioner, 5)
}

View File

@ -114,8 +114,8 @@ func (n *graphNodeOrphanResource) Name() string {
return fmt.Sprintf("%s (orphan)", n.ResourceName)
}
func (n *graphNodeOrphanResource) ProvidedBy() string {
return resourceProvider(n.ResourceName)
func (n *graphNodeOrphanResource) ProvidedBy() []string {
return []string{resourceProvider(n.ResourceName)}
}
// GraphNodeEvalable impl.

View File

@ -269,7 +269,7 @@ func TestGraphNodeOrphanResource_impl(t *testing.T) {
func TestGraphNodeOrphanResource_ProvidedBy(t *testing.T) {
n := &graphNodeOrphanResource{ResourceName: "aws_instance.foo"}
if v := n.ProvidedBy(); v != "aws" {
if v := n.ProvidedBy(); v[0] != "aws" {
t.Fatalf("bad: %#v", v)
}
}

View File

@ -18,7 +18,7 @@ type GraphNodeProvider interface {
// a provider must implement. ProvidedBy must return the name of the provider
// to use.
type GraphNodeProviderConsumer interface {
ProvidedBy() string
ProvidedBy() []string
}
// ProviderTransformer is a GraphTransformer that maps resources to
@ -32,15 +32,17 @@ func (t *ProviderTransformer) Transform(g *Graph) error {
m := providerVertexMap(g)
for _, v := range g.Vertices() {
if pv, ok := v.(GraphNodeProviderConsumer); ok {
target := m[pv.ProvidedBy()]
if target == nil {
err = multierror.Append(err, fmt.Errorf(
"%s: provider %s couldn't be found",
dag.VertexName(v), pv.ProvidedBy()))
continue
}
for _, p := range pv.ProvidedBy() {
target := m[p]
if target == nil {
err = multierror.Append(err, fmt.Errorf(
"%s: provider %s couldn't be found",
dag.VertexName(v), p))
continue
}
g.Connect(dag.BasicEdge(v, target))
g.Connect(dag.BasicEdge(v, target))
}
}
}

View File

@ -57,6 +57,6 @@ func (n *graphNodeTaintedResource) Name() string {
return fmt.Sprintf("%s (tainted #%d)", n.ResourceName, n.Index+1)
}
func (n *graphNodeTaintedResource) ProvidedBy() string {
return resourceProvider(n.ResourceName)
func (n *graphNodeTaintedResource) ProvidedBy() []string {
return []string{resourceProvider(n.ResourceName)}
}

View File

@ -53,7 +53,7 @@ func TestGraphNodeTaintedResource_impl(t *testing.T) {
func TestGraphNodeTaintedResource_ProvidedBy(t *testing.T) {
n := &graphNodeTaintedResource{ResourceName: "aws_instance.foo"}
if v := n.ProvidedBy(); v != "aws" {
if v := n.ProvidedBy(); v[0] != "aws" {
t.Fatalf("bad: %#v", v)
}
}