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 (
"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
}

View File

@ -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{

View File

@ -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{

View File

@ -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
}

View File

@ -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
}

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) {
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
}

View File

@ -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" {}