terraform: getting closer to mapping resource providers properly

This commit is contained in:
Mitchell Hashimoto 2014-06-25 12:58:27 -07:00
parent 94a11583c2
commit cdab89d7c1
7 changed files with 318 additions and 135 deletions

View File

@ -2,6 +2,7 @@ package terraform
import ( import (
"fmt" "fmt"
"strings"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/depgraph" "github.com/hashicorp/terraform/depgraph"
@ -23,9 +24,9 @@ type GraphNodeResource struct {
// GraphNodeResourceProvider is a node type in the graph that represents // GraphNodeResourceProvider is a node type in the graph that represents
// the configuration for a resource provider. // the configuration for a resource provider.
type GraphNodeResourceProvider struct { type GraphNodeResourceProvider struct {
ID string ID string
Provider ResourceProvider Providers []ResourceProvider
Config *config.ProviderConfig Config *config.ProviderConfig
} }
// Graph builds a dependency graph for the given configuration and state. // Graph builds a dependency graph for the given configuration and state.
@ -78,8 +79,9 @@ func graphAddConfigResources(g *depgraph.Graph, c *config.Config) {
noun := &depgraph.Noun{ noun := &depgraph.Noun{
Name: r.Id(), Name: r.Id(),
Meta: &GraphNodeResource{ Meta: &GraphNodeResource{
Type: r.Type, Type: r.Type,
Config: r, Config: r,
Resource: new(Resource),
}, },
} }
nouns[noun.Name] = noun nouns[noun.Name] = noun
@ -196,12 +198,9 @@ func graphAddVariableDeps(g *depgraph.Graph) {
} }
// Find the target // Find the target
var target *depgraph.Noun target := g.Noun(rv.ResourceId())
for _, n := range g.Nouns { if target == nil {
if n.Name == rv.ResourceId() { continue
target = n
break
}
} }
// Build the dependency // Build the dependency
@ -215,3 +214,149 @@ func graphAddVariableDeps(g *depgraph.Graph) {
} }
} }
} }
// graphInitResourceProviders maps the resource providers onto the graph
// given a mapping of prefixes to resource providers.
//
// Unlike the graphAdd* functions, this one can return an error if resource
// providers can't be found or can't be instantiated.
func graphInitResourceProviders(
g *depgraph.Graph,
ps map[string]ResourceProviderFactory) error {
var errs []error
// Keep track of providers we know we couldn't instantiate so
// that we don't get a ton of errors about the same provider.
failures := make(map[string]struct{})
for _, n := range g.Nouns {
// We only care about the resource providers first. There is guaranteed
// to be only one node per tuple (providerId, providerConfig), which
// means we don't need to verify we have instantiated it before.
rn, ok := n.Meta.(*GraphNodeResourceProvider)
if !ok {
continue
}
prefixes := matchingPrefixes(rn.ID, ps)
if len(prefixes) > 0 {
if _, ok := failures[prefixes[0]]; ok {
// We already failed this provider, meaning this
// resource will never succeed, so just continue.
continue
}
}
// Go through each prefix and instantiate if necessary, then
// verify if this provider is of use to us or not.
for _, prefix := range prefixes {
p, err := ps[prefix]()
if err != nil {
errs = append(errs, fmt.Errorf(
"Error instantiating resource provider for "+
"prefix %s: %s", prefix, err))
// Record the error so that we don't check it again
failures[prefix] = struct{}{}
// Jump to the next prefix
continue
}
rn.Providers = append(rn.Providers, p)
}
// If we never found a provider, then error and continue
if len(rn.Providers) == 0 {
errs = append(errs, fmt.Errorf(
"Provider for configuration '%s' not found.",
rn.ID))
continue
}
}
if len(errs) > 0 {
return &MultiError{Errors: errs}
}
return nil
}
// graphMapResourceProviders takes a graph that already has initialized
// the resource providers (using graphInitResourceProviders) and maps the
// resource providers to the resources themselves.
func graphMapResourceProviders(g *depgraph.Graph) error {
var errs []error
// First build a mapping of resource provider ID to the node that
// contains those resources.
mapping := make(map[string]*GraphNodeResourceProvider)
for _, n := range g.Nouns {
rn, ok := n.Meta.(*GraphNodeResourceProvider)
if !ok {
continue
}
mapping[rn.ID] = rn
}
// Now go through each of the resources and find a matching provider.
for _, n := range g.Nouns {
rn, ok := n.Meta.(*GraphNodeResource)
if !ok {
continue
}
rpn, ok := mapping[rn.ResourceProviderID]
if !ok {
// This should never happen since when building the graph
// we ensure that everything matches up.
panic(fmt.Sprintf(
"Resource provider ID not found: %s (type: %s)",
rn.ResourceProviderID,
rn.Type))
}
var provider ResourceProvider
for _, rp := range rpn.Providers {
if ProviderSatisfies(rp, rn.Type) {
provider = rp
break
}
}
if provider == nil {
errs = append(errs, fmt.Errorf(
"Resource provider not found for resource type '%s'",
rn.Type))
continue
}
rn.Resource.Provider = provider
}
if len(errs) > 0 {
return &MultiError{Errors: errs}
}
return nil
}
// matchingPrefixes takes a resource type and a set of resource
// providers we know about by prefix and returns a list of prefixes
// that might be valid for that resource.
//
// The list returned is in the order that they should be attempted.
func matchingPrefixes(
t string,
ps map[string]ResourceProviderFactory) []string {
result := make([]string, 0, 1)
for prefix, _ := range ps {
if strings.HasPrefix(t, prefix) {
result = append(result, prefix)
}
}
// TODO(mitchellh): Order by longest prefix first
return result
}

View File

@ -5,7 +5,7 @@ import (
"testing" "testing"
) )
func TestTerraformGraph(t *testing.T) { func TestGraph(t *testing.T) {
config := testConfig(t, "graph-basic") config := testConfig(t, "graph-basic")
g := Graph(config, nil) g := Graph(config, nil)
@ -20,7 +20,7 @@ func TestTerraformGraph(t *testing.T) {
} }
} }
func TestTerraformGraph_cycle(t *testing.T) { func TestGraph_cycle(t *testing.T) {
config := testConfig(t, "graph-cycle") config := testConfig(t, "graph-cycle")
g := Graph(config, nil) g := Graph(config, nil)
@ -29,7 +29,7 @@ func TestTerraformGraph_cycle(t *testing.T) {
} }
} }
func TestTerraformGraph_state(t *testing.T) { func TestGraph_state(t *testing.T) {
config := testConfig(t, "graph-basic") config := testConfig(t, "graph-basic")
state := &State{ state := &State{
Resources: map[string]*ResourceState{ Resources: map[string]*ResourceState{

View File

@ -8,9 +8,8 @@ import (
) )
func TestReadWritePlan(t *testing.T) { func TestReadWritePlan(t *testing.T) {
tf := testTerraform(t, "new-good")
plan := &Plan{ plan := &Plan{
Config: tf.config, Config: testConfig(t, "new-good"),
Diff: &Diff{ Diff: &Diff{
Resources: map[string]*ResourceDiff{ Resources: map[string]*ResourceDiff{
"nodeA": &ResourceDiff{ "nodeA": &ResourceDiff{

View File

@ -2,7 +2,6 @@ package terraform
import ( import (
"fmt" "fmt"
"strings"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
) )
@ -154,23 +153,3 @@ func smcVariables(c *Config) []error {
return errs return errs
} }
// matchingPrefixes takes a resource type and a set of resource
// providers we know about by prefix and returns a list of prefixes
// that might be valid for that resource.
//
// The list returned is in the order that they should be attempted.
func matchingPrefixes(
t string,
ps map[string]ResourceProviderFactory) []string {
result := make([]string, 0, 1)
for prefix, _ := range ps {
if strings.HasPrefix(t, prefix) {
result = append(result, prefix)
}
}
// TODO(mitchellh): Order by longest prefix first
return result
}

View File

@ -12,8 +12,7 @@ import (
// Terraform from code, and can perform operations such as returning // Terraform from code, and can perform operations such as returning
// all resources, a resource tree, a specific resource, etc. // all resources, a resource tree, a specific resource, etc.
type Terraform struct { type Terraform struct {
config *config.Config providers map[string]ResourceProviderFactory
mapping map[*config.Resource]*terraformProvider
variables map[string]string variables map[string]string
} }
@ -46,7 +45,6 @@ type Config struct {
// can be properly initialized, can be configured, etc. // can be properly initialized, can be configured, etc.
func New(c *Config) (*Terraform, error) { func New(c *Config) (*Terraform, error) {
var errs []error var errs []error
var mapping map[*config.Resource]*terraformProvider
if c.Config != nil { if c.Config != nil {
// Validate that all required variables have values // Validate that all required variables have values
@ -86,8 +84,7 @@ func New(c *Config) (*Terraform, error) {
} }
return &Terraform{ return &Terraform{
config: c.Config, providers: c.Providers,
mapping: mapping,
variables: c.Variables, variables: c.Variables,
}, nil }, nil
} }
@ -115,8 +112,15 @@ func (t *Terraform) Graph(c *config.Config, s *State) (*depgraph.Graph, error) {
return nil, err return nil, err
} }
// Next, we want to go through the graph and make sure that we // Initialize all the providers
// map a provider to each of the resources. if err := graphInitResourceProviders(g, t.providers); err != nil {
return nil, err
}
// Map the providers to resources
if err := graphMapResourceProviders(g); err != nil {
return nil, err
}
return g, nil return g, nil
} }
@ -221,7 +225,7 @@ func (t *Terraform) planWalkFn(
result.init() result.init()
// Write our configuration out // Write our configuration out
result.Config = t.config //result.Config = t.config
// Copy the variables // Copy the variables
result.Vars = make(map[string]string) result.Vars = make(map[string]string)
@ -280,7 +284,7 @@ func (t *Terraform) genericWalkFn(
diff *Diff, diff *Diff,
invars map[string]string, invars map[string]string,
cb genericWalkFunc) depgraph.WalkFunc { cb genericWalkFunc) depgraph.WalkFunc {
var l sync.Mutex //var l sync.Mutex
// Initialize the variables for application // Initialize the variables for application
vars := make(map[string]string) vars := make(map[string]string)
@ -289,79 +293,81 @@ func (t *Terraform) genericWalkFn(
} }
return func(n *depgraph.Noun) error { return func(n *depgraph.Noun) error {
// If it is the root node, ignore /*
if n.Meta == nil { // If it is the root node, ignore
return nil if n.Meta == nil {
} return nil
switch n.Meta.(type) {
case *config.ProviderConfig:
// Ignore, we don't treat this any differently since we always
// initialize the provider on first use and use a lock to make
// sure we only do this once.
return nil
case *config.Resource:
// Continue
}
r := n.Meta.(*config.Resource)
p := t.mapping[r]
if p == nil {
panic(fmt.Sprintf("No provider for resource: %s", r.Id()))
}
// Initialize the provider if we haven't already
if err := p.init(vars); err != nil {
return err
}
// Get the resource state
var rs *ResourceState
if state != nil {
rs = state.Resources[r.Id()]
}
// Get the resource diff
var rd *ResourceDiff
if diff != nil {
rd = diff.Resources[r.Id()]
}
if len(vars) > 0 {
if err := r.RawConfig.Interpolate(vars); err != nil {
panic(fmt.Sprintf("Interpolate error: %s", err))
} }
}
// If we have no state, then create an empty state with the switch n.Meta.(type) {
// type fulfilled at the least. case *config.ProviderConfig:
if rs == nil { // Ignore, we don't treat this any differently since we always
rs = new(ResourceState) // initialize the provider on first use and use a lock to make
} // sure we only do this once.
rs.Type = r.Type return nil
case *config.Resource:
// Call the callack // Continue
newVars, err := cb(&Resource{
Id: r.Id(),
Config: NewResourceConfig(r.RawConfig),
Diff: rd,
Provider: p.Provider,
State: rs,
})
if err != nil {
return err
}
if len(newVars) > 0 {
// Acquire a lock since this function is called in parallel
l.Lock()
defer l.Unlock()
// Update variables
for k, v := range newVars {
vars[k] = v
} }
}
r := n.Meta.(*config.Resource)
p := t.mapping[r]
if p == nil {
panic(fmt.Sprintf("No provider for resource: %s", r.Id()))
}
// Initialize the provider if we haven't already
if err := p.init(vars); err != nil {
return err
}
// Get the resource state
var rs *ResourceState
if state != nil {
rs = state.Resources[r.Id()]
}
// Get the resource diff
var rd *ResourceDiff
if diff != nil {
rd = diff.Resources[r.Id()]
}
if len(vars) > 0 {
if err := r.RawConfig.Interpolate(vars); err != nil {
panic(fmt.Sprintf("Interpolate error: %s", err))
}
}
// If we have no state, then create an empty state with the
// type fulfilled at the least.
if rs == nil {
rs = new(ResourceState)
}
rs.Type = r.Type
// Call the callack
newVars, err := cb(&Resource{
Id: r.Id(),
Config: NewResourceConfig(r.RawConfig),
Diff: rd,
Provider: p.Provider,
State: rs,
})
if err != nil {
return err
}
if len(newVars) > 0 {
// Acquire a lock since this function is called in parallel
l.Lock()
defer l.Unlock()
// Update variables
for k, v := range newVars {
vars[k] = v
}
}
*/
return nil return nil
} }

View File

@ -110,6 +110,46 @@ func TestTerraformApply_vars(t *testing.T) {
} }
} }
func TestTerraformGraph(t *testing.T) {
rpAws := new(MockResourceProvider)
rpOS := new(MockResourceProvider)
rpAws.ResourcesReturn = []ResourceType{
ResourceType{Name: "aws_instance"},
ResourceType{Name: "aws_load_balancer"},
ResourceType{Name: "aws_security_group"},
}
rpOS.ResourcesReturn = []ResourceType{
ResourceType{Name: "openstack_floating_ip"},
}
tf := testTerraform2(t, &Config{
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(rpAws),
"open": testProviderFuncFixed(rpOS),
},
})
c := testConfig(t, "graph-basic")
g, err := tf.Graph(c, nil)
if err != nil {
t.Fatalf("err: %s", err)
}
// A helper to help get us the provider for a resource.
graphProvider := func(n string) ResourceProvider {
return g.Noun(n).Meta.(*GraphNodeResource).Resource.Provider
}
if graphProvider("aws_instance.web") != rpAws {
t.Fatalf("bad: %#v", graphProvider("aws_instance.web"))
}
if graphProvider("openstack_floating_ip.random") != rpOS {
t.Fatalf("bad: %#v", graphProvider("openstack_floating_ip.random"))
}
}
func TestTerraformPlan(t *testing.T) { func TestTerraformPlan(t *testing.T) {
tf := testTerraform(t, "plan-good") tf := testTerraform(t, "plan-good")
@ -328,12 +368,20 @@ func testProviderFunc(n string, rs []string) ResourceProviderFactory {
} }
} }
func testProvider(tf *Terraform, n string) ResourceProvider { func testProviderFuncFixed(rp ResourceProvider) ResourceProviderFactory {
for r, tp := range tf.mapping { return func() (ResourceProvider, error) {
if r.Id() == n { return rp, nil
return tp.Provider
}
} }
}
func testProvider(tf *Terraform, n string) ResourceProvider {
/*
for r, tp := range tf.mapping {
if r.Id() == n {
return tp.Provider
}
}
*/
return nil return nil
} }
@ -343,23 +391,27 @@ func testProviderMock(p ResourceProvider) *MockResourceProvider {
} }
func testProviderConfig(tf *Terraform, n string) *config.ProviderConfig { func testProviderConfig(tf *Terraform, n string) *config.ProviderConfig {
for r, tp := range tf.mapping { /*
if r.Id() == n { for r, tp := range tf.mapping {
return tp.Config if r.Id() == n {
return tp.Config
}
} }
} */
return nil return nil
} }
func testProviderName(t *testing.T, tf *Terraform, n string) string { func testProviderName(t *testing.T, tf *Terraform, n string) string {
var p ResourceProvider var p ResourceProvider
for r, tp := range tf.mapping { /*
if r.Id() == n { for r, tp := range tf.mapping {
p = tp.Provider if r.Id() == n {
break p = tp.Provider
break
}
} }
} */
if p == nil { if p == nil {
t.Fatalf("resource not found: %s", n) t.Fatalf("resource not found: %s", n)
@ -406,11 +458,13 @@ func testTerraform2(t *testing.T, c *Config) *Terraform {
} }
func testTerraformProvider(tf *Terraform, n string) *terraformProvider { func testTerraformProvider(tf *Terraform, n string) *terraformProvider {
for r, tp := range tf.mapping { /*
if r.Id() == n { for r, tp := range tf.mapping {
return tp if r.Id() == n {
return tp
}
} }
} */
return nil return nil
} }

View File

@ -7,7 +7,7 @@ provider "aws" {
foo = "${openstack_floating_ip.random.value}" foo = "${openstack_floating_ip.random.value}"
} }
resource "openstack_floating_ip" "random" {} #resource "openstack_floating_ip" "random" {}
resource "aws_security_group" "firewall" {} resource "aws_security_group" "firewall" {}