From c9d84134311618a21514a4caca97855f6ed21700 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 3 Jun 2014 15:08:00 -0700 Subject: [PATCH] terraform: New happy path works decently well --- terraform/resource_provider.go | 10 +++ terraform/resource_provider_mock.go | 3 + terraform/terraform.go | 78 ++++++++++++++++++++++- terraform/terraform_test.go | 81 ++++++++++++++++++++++++ terraform/test-fixtures/new-good/main.tf | 2 + 5 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 terraform/terraform_test.go create mode 100644 terraform/test-fixtures/new-good/main.tf diff --git a/terraform/resource_provider.go b/terraform/resource_provider.go index 365764794..3518aa419 100644 --- a/terraform/resource_provider.go +++ b/terraform/resource_provider.go @@ -23,3 +23,13 @@ type ResourceType struct { // ResourceProviderFactory is a function type that creates a new instance // of a resource provider. type ResourceProviderFactory func() (ResourceProvider, error) + +func ProviderSatisfies(p ResourceProvider, n string) bool { + for _, rt := range p.Resources() { + if rt.Name == n { + return true + } + } + + return false +} diff --git a/terraform/resource_provider_mock.go b/terraform/resource_provider_mock.go index cf85cfe8c..ac5d48a7c 100644 --- a/terraform/resource_provider_mock.go +++ b/terraform/resource_provider_mock.go @@ -3,6 +3,9 @@ package terraform // MockResourceProvider implements ResourceProvider but mocks out all the // calls for testing purposes. type MockResourceProvider struct { + // Anything you want, in case you need to store extra data with the mock. + Meta interface{} + ConfigureCalled bool ConfigureConfig map[string]interface{} ConfigureReturnWarnings []string diff --git a/terraform/terraform.go b/terraform/terraform.go index f1cd66b2c..8de2d2557 100644 --- a/terraform/terraform.go +++ b/terraform/terraform.go @@ -1,6 +1,9 @@ package terraform import ( + "fmt" + "strings" + "github.com/hashicorp/terraform/config" ) @@ -8,8 +11,8 @@ 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 - providers []ResourceProvider + config *config.Config + mapping map[*config.Resource]ResourceProvider } // Config is the configuration that must be given to instantiate @@ -27,7 +30,56 @@ type Config struct { // time, as well as richer checks such as verifying that the resource providers // can be properly initialized, can be configured, etc. func New(c *Config) (*Terraform, error) { - return nil, nil + // Go through each resource and match it up to a provider + mapping := make(map[*config.Resource]ResourceProvider) + providers := make(map[string]ResourceProvider) + for _, r := range c.Config.Resources { + // Find the prefixes that match this in the order of + // longest matching first (most specific) + prefixes := matchingPrefixes(r.Type, c.Providers) + + // Go through each prefix and instantiate if necessary, then + // verify if this provider is of use to us or not. + var provider ResourceProvider = nil + for _, prefix := range prefixes { + p, ok := providers[prefix] + if !ok { + var err error + p, err = c.Providers[prefix]() + if err != nil { + err = fmt.Errorf( + "Error instantiating resource provider for "+ + "prefix %s: %s", prefix, err) + return nil, err + } + + providers[prefix] = p + } + + // Test if this provider matches what we need + if !ProviderSatisfies(p, r.Type) { + continue + } + + // A match! Set it and break + provider = p + break + } + + if provider == nil { + // We never found a matching provider. + return nil, fmt.Errorf( + "Provider for resource %s not found.", + r.Id()) + } + + mapping[r] = provider + } + + return &Terraform{ + config: c.Config, + mapping: mapping, + }, nil } func (t *Terraform) Apply(*State, *Diff) (*State, error) { @@ -41,3 +93,23 @@ func (t *Terraform) Diff(*State) (*Diff, error) { func (t *Terraform) Refresh(*State) (*State, error) { return nil, 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/terraform_test.go b/terraform/terraform_test.go new file mode 100644 index 000000000..585d36866 --- /dev/null +++ b/terraform/terraform_test.go @@ -0,0 +1,81 @@ +package terraform + +import ( + "path/filepath" + "testing" + + "github.com/hashicorp/terraform/config" +) + +// This is the directory where our test fixtures are. +const fixtureDir = "./test-fixtures" + +func testConfig(t *testing.T, name string) *config.Config { + c, err := config.Load(filepath.Join(fixtureDir, name, "main.tf")) + if err != nil { + t.Fatalf("err: %s", err) + } + + return c +} + +func testProviderFunc(n string, rs []string) ResourceProviderFactory { + resources := make([]ResourceType, len(rs)) + for i, v := range rs { + resources[i] = ResourceType{ + Name: v, + } + } + + return func() (ResourceProvider, error) { + result := &MockResourceProvider{ + Meta: n, + ResourcesReturn: resources, + } + + return result, nil + } +} + +func testProviderName(p ResourceProvider) string { + return p.(*MockResourceProvider).Meta.(string) +} + +func testResourceMapping(tf *Terraform) map[string]ResourceProvider { + result := make(map[string]ResourceProvider) + for resource, provider := range tf.mapping { + result[resource.Id()] = provider + } + + return result +} + +func TestNew(t *testing.T) { + config := testConfig(t, "new-good") + tfConfig := &Config{ + Config: config, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFunc("aws", []string{"aws_instance"}), + "do": testProviderFunc("do", []string{"do_droplet"}), + }, + } + + tf, err := New(tfConfig) + if err != nil { + t.Fatalf("err: %s", err) + } + if tf == nil { + t.Fatal("tf should not be nil") + } + + mapping := testResourceMapping(tf) + if len(mapping) != 2 { + t.Fatalf("bad: %#v", mapping) + } + if testProviderName(mapping["aws_instance.foo"]) != "aws" { + t.Fatalf("bad: %#v", mapping) + } + if testProviderName(mapping["do_droplet.bar"]) != "do" { + t.Fatalf("bad: %#v", mapping) + } +} diff --git a/terraform/test-fixtures/new-good/main.tf b/terraform/test-fixtures/new-good/main.tf new file mode 100644 index 000000000..7970e8f49 --- /dev/null +++ b/terraform/test-fixtures/new-good/main.tf @@ -0,0 +1,2 @@ +resource "aws_instance" "foo" {} +resource "do_droplet" "bar" {}