terraform: getting closer to mapping resource providers properly
This commit is contained in:
parent
94a11583c2
commit
cdab89d7c1
|
@ -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"
|
||||||
|
@ -24,7 +25,7 @@ type GraphNodeResource struct {
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,6 +81,7 @@ func graphAddConfigResources(g *depgraph.Graph, c *config.Config) {
|
||||||
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
|
||||||
|
}
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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,6 +293,7 @@ func (t *Terraform) genericWalkFn(
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(n *depgraph.Noun) error {
|
return func(n *depgraph.Noun) error {
|
||||||
|
/*
|
||||||
// If it is the root node, ignore
|
// If it is the root node, ignore
|
||||||
if n.Meta == nil {
|
if n.Meta == nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -362,6 +367,7 @@ func (t *Terraform) genericWalkFn(
|
||||||
vars[k] = v
|
vars[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 testProviderFuncFixed(rp ResourceProvider) ResourceProviderFactory {
|
||||||
|
return func() (ResourceProvider, error) {
|
||||||
|
return rp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testProvider(tf *Terraform, n string) ResourceProvider {
|
func testProvider(tf *Terraform, n string) ResourceProvider {
|
||||||
|
/*
|
||||||
for r, tp := range tf.mapping {
|
for r, tp := range tf.mapping {
|
||||||
if r.Id() == n {
|
if r.Id() == n {
|
||||||
return tp.Provider
|
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 {
|
for r, tp := range tf.mapping {
|
||||||
if r.Id() == n {
|
if r.Id() == n {
|
||||||
return tp.Config
|
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 {
|
for r, tp := range tf.mapping {
|
||||||
if r.Id() == n {
|
if r.Id() == n {
|
||||||
p = tp.Provider
|
p = tp.Provider
|
||||||
break
|
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 {
|
for r, tp := range tf.mapping {
|
||||||
if r.Id() == n {
|
if r.Id() == n {
|
||||||
return tp
|
return tp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -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" {}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue