diff --git a/terraform/graph.go b/terraform/graph.go index 1e1fb25be..0560a1919 100644 --- a/terraform/graph.go +++ b/terraform/graph.go @@ -2,6 +2,7 @@ package terraform import ( "fmt" + "strings" "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/depgraph" @@ -23,9 +24,9 @@ type GraphNodeResource struct { // GraphNodeResourceProvider is a node type in the graph that represents // the configuration for a resource provider. type GraphNodeResourceProvider struct { - ID string - Provider ResourceProvider - Config *config.ProviderConfig + ID string + Providers []ResourceProvider + Config *config.ProviderConfig } // 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{ Name: r.Id(), Meta: &GraphNodeResource{ - Type: r.Type, - Config: r, + Type: r.Type, + Config: r, + Resource: new(Resource), }, } nouns[noun.Name] = noun @@ -196,12 +198,9 @@ func graphAddVariableDeps(g *depgraph.Graph) { } // Find the target - var target *depgraph.Noun - for _, n := range g.Nouns { - if n.Name == rv.ResourceId() { - target = n - break - } + target := g.Noun(rv.ResourceId()) + if target == nil { + continue } // 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 +} diff --git a/terraform/graph_test.go b/terraform/graph_test.go index e1528ec8c..dd721dc07 100644 --- a/terraform/graph_test.go +++ b/terraform/graph_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func TestTerraformGraph(t *testing.T) { +func TestGraph(t *testing.T) { config := testConfig(t, "graph-basic") 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") 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") state := &State{ Resources: map[string]*ResourceState{ diff --git a/terraform/plan_test.go b/terraform/plan_test.go index 09fe9fe28..8a9a28810 100644 --- a/terraform/plan_test.go +++ b/terraform/plan_test.go @@ -8,9 +8,8 @@ import ( ) func TestReadWritePlan(t *testing.T) { - tf := testTerraform(t, "new-good") plan := &Plan{ - Config: tf.config, + Config: testConfig(t, "new-good"), Diff: &Diff{ Resources: map[string]*ResourceDiff{ "nodeA": &ResourceDiff{ diff --git a/terraform/semantics.go b/terraform/semantics.go index 43a8aed16..9f7732a88 100644 --- a/terraform/semantics.go +++ b/terraform/semantics.go @@ -2,7 +2,6 @@ package terraform import ( "fmt" - "strings" "github.com/hashicorp/terraform/config" ) @@ -154,23 +153,3 @@ func smcVariables(c *Config) []error { 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 -} diff --git a/terraform/terraform.go b/terraform/terraform.go index 0cb58e309..949edf3c4 100644 --- a/terraform/terraform.go +++ b/terraform/terraform.go @@ -12,8 +12,7 @@ import ( // Terraform from code, and can perform operations such as returning // all resources, a resource tree, a specific resource, etc. type Terraform struct { - config *config.Config - mapping map[*config.Resource]*terraformProvider + providers map[string]ResourceProviderFactory variables map[string]string } @@ -46,7 +45,6 @@ type Config struct { // can be properly initialized, can be configured, etc. func New(c *Config) (*Terraform, error) { var errs []error - var mapping map[*config.Resource]*terraformProvider if c.Config != nil { // Validate that all required variables have values @@ -86,8 +84,7 @@ func New(c *Config) (*Terraform, error) { } return &Terraform{ - config: c.Config, - mapping: mapping, + providers: c.Providers, variables: c.Variables, }, nil } @@ -115,8 +112,15 @@ func (t *Terraform) Graph(c *config.Config, s *State) (*depgraph.Graph, error) { return nil, err } - // Next, we want to go through the graph and make sure that we - // map a provider to each of the resources. + // Initialize all the providers + 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 } @@ -221,7 +225,7 @@ func (t *Terraform) planWalkFn( result.init() // Write our configuration out - result.Config = t.config + //result.Config = t.config // Copy the variables result.Vars = make(map[string]string) @@ -280,7 +284,7 @@ func (t *Terraform) genericWalkFn( diff *Diff, invars map[string]string, cb genericWalkFunc) depgraph.WalkFunc { - var l sync.Mutex + //var l sync.Mutex // Initialize the variables for application vars := make(map[string]string) @@ -289,79 +293,81 @@ func (t *Terraform) genericWalkFn( } return func(n *depgraph.Noun) error { - // If it is the root node, ignore - 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 it is the root node, ignore + if n.Meta == nil { + return nil } - } - // 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 + 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 + // 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 } diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 630837b68..2d958d2b1 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -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) { tf := testTerraform(t, "plan-good") @@ -328,12 +368,20 @@ func testProviderFunc(n string, rs []string) ResourceProviderFactory { } } -func testProvider(tf *Terraform, n string) ResourceProvider { - for r, tp := range tf.mapping { - if r.Id() == n { - return tp.Provider - } +func testProviderFuncFixed(rp ResourceProvider) ResourceProviderFactory { + return func() (ResourceProvider, error) { + return rp, nil } +} + +func testProvider(tf *Terraform, n string) ResourceProvider { + /* + for r, tp := range tf.mapping { + if r.Id() == n { + return tp.Provider + } + } + */ return nil } @@ -343,23 +391,27 @@ func testProviderMock(p ResourceProvider) *MockResourceProvider { } func testProviderConfig(tf *Terraform, n string) *config.ProviderConfig { - for r, tp := range tf.mapping { - if r.Id() == n { - return tp.Config + /* + for r, tp := range tf.mapping { + if r.Id() == n { + return tp.Config + } } - } + */ return nil } func testProviderName(t *testing.T, tf *Terraform, n string) string { var p ResourceProvider - for r, tp := range tf.mapping { - if r.Id() == n { - p = tp.Provider - break + /* + for r, tp := range tf.mapping { + if r.Id() == n { + p = tp.Provider + break + } } - } + */ if p == nil { 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 { - for r, tp := range tf.mapping { - if r.Id() == n { - return tp + /* + for r, tp := range tf.mapping { + if r.Id() == n { + return tp + } } - } + */ return nil } diff --git a/terraform/test-fixtures/graph-basic/main.tf b/terraform/test-fixtures/graph-basic/main.tf index cd84eb51a..469d9a314 100644 --- a/terraform/test-fixtures/graph-basic/main.tf +++ b/terraform/test-fixtures/graph-basic/main.tf @@ -7,7 +7,7 @@ provider "aws" { foo = "${openstack_floating_ip.random.value}" } -resource "openstack_floating_ip" "random" {} +#resource "openstack_floating_ip" "random" {} resource "aws_security_group" "firewall" {}