diff --git a/builtin/bins/provider-heroku/main.go b/builtin/bins/provider-heroku/main.go new file mode 100644 index 000000000..af17f5326 --- /dev/null +++ b/builtin/bins/provider-heroku/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/heroku" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(new(heroku.ResourceProvider)) +} diff --git a/builtin/bins/provider-heroku/main_test.go b/builtin/bins/provider-heroku/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/builtin/bins/provider-heroku/main_test.go @@ -0,0 +1 @@ +package main diff --git a/builtin/providers/heroku/config.go b/builtin/providers/heroku/config.go new file mode 100644 index 000000000..614c3831f --- /dev/null +++ b/builtin/providers/heroku/config.go @@ -0,0 +1,33 @@ +package heroku + +import ( + "log" + "os" + + "github.com/bgentry/heroku-go" +) + +type Config struct { + APIKey string `mapstructure:"api_key"` + Email string `mapstructure:"email"` +} + +// Client() returns a new client for accessing heroku. +// +func (c *Config) Client() (*heroku.Client, error) { + + // If we have env vars set (like in the acc) tests, + // we need to override the values passed in here. + if v := os.Getenv("HEROKU_EMAIL"); v != "" { + c.Email = v + } + if v := os.Getenv("HEROKU_API_KEY"); v != "" { + c.APIKey = v + } + + client := heroku.Client{Username: c.Email, Password: c.APIKey} + + log.Printf("[INFO] Heroku Client configured for user: %s", c.Email) + + return &client, nil +} diff --git a/builtin/providers/heroku/resource_heroku_app.go b/builtin/providers/heroku/resource_heroku_app.go new file mode 100644 index 000000000..c1f6b728c --- /dev/null +++ b/builtin/providers/heroku/resource_heroku_app.go @@ -0,0 +1,153 @@ +package heroku + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/helper/diff" + "github.com/hashicorp/terraform/terraform" + "github.com/bgentry/heroku-go" +) + +func resource_heroku_app_create( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + + // Merge the diff into the state so that we have all the attributes + // properly. + rs := s.MergeDiff(d) + + // Build up our creation options + opts := heroku.AppCreateOpts{} + + if attr := rs.Attributes["name"]; attr != "" { + opts.Name = &attr + } + + if attr := rs.Attributes["region"]; attr != "" { + opts.Region = &attr + } + + if attr := rs.Attributes["stack"]; attr != "" { + opts.Stack = &attr + } + + log.Printf("[DEBUG] App create configuration: %#v", opts) + + app, err := client.AppCreate(&opts) + if err != nil { + return s, err + } + + rs.ID = app.Name + + log.Printf("[INFO] App ID: %s", rs.ID) + + return resource_heroku_app_update_state(rs, app) +} + +func resource_heroku_app_update( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + + panic("does not update") + + return nil, nil +} + +func resource_heroku_app_destroy( + s *terraform.ResourceState, + meta interface{}) error { + p := meta.(*ResourceProvider) + client := p.client + + log.Printf("[INFO] Deleting App: %s", s.ID) + + // Destroy the app + err := client.AppDelete(s.ID) + + if err != nil { + return fmt.Errorf("Error deleting App: %s", err) + } + + return nil +} + +func resource_heroku_app_refresh( + s *terraform.ResourceState, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + + app, err := resource_heroku_app_retrieve(s.ID, client) + if err != nil { + return nil, err + } + + return resource_heroku_app_update_state(s, app) +} + +func resource_heroku_app_diff( + s *terraform.ResourceState, + c *terraform.ResourceConfig, + meta interface{}) (*terraform.ResourceDiff, error) { + + b := &diff.ResourceBuilder{ + Attrs: map[string]diff.AttrType{ + "name": diff.AttrTypeCreate, + "region": diff.AttrTypeUpdate, + "stack": diff.AttrTypeCreate, + }, + + ComputedAttrs: []string{ + "name", + "region", + "stack", + "git_url", + "web_url", + "id", + }, + } + + return b.Diff(s, c) +} + +func resource_heroku_app_update_state( + s *terraform.ResourceState, + app *heroku.App) (*terraform.ResourceState, error) { + + s.Attributes["name"] = app.Name + s.Attributes["stack"] = app.Stack.Name + s.Attributes["region"] = app.Region.Name + s.Attributes["git_url"] = app.GitURL + s.Attributes["web_url"] = app.WebURL + s.Attributes["id"] = app.Id + + return s, nil +} + +func resource_heroku_app_retrieve(id string, client *heroku.Client) (*heroku.App, error) { + app, err := client.AppInfo(id) + + if err != nil { + return nil, fmt.Errorf("Error retrieving app: %s", err) + } + + return app, nil +} + +func resource_heroku_app_validation() *config.Validator { + return &config.Validator{ + Required: []string{}, + Optional: []string{ + "name", + "region", + "stack", + }, + } +} diff --git a/builtin/providers/heroku/resource_heroku_app_test.go b/builtin/providers/heroku/resource_heroku_app_test.go new file mode 100644 index 000000000..f6f196ce7 --- /dev/null +++ b/builtin/providers/heroku/resource_heroku_app_test.go @@ -0,0 +1,93 @@ +package heroku + +import ( + "fmt" + "testing" + + "github.com/bgentry/heroku-go" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccHerokuApp_Basic(t *testing.T) { + var app heroku.App + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckHerokuAppDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckHerokuAppConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAppExists("heroku_app.foobar", &app), + testAccCheckHerokuAppAttributes(&app), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "name", "terraform-test-app"), + ), + }, + }, + }) +} + +func testAccCheckHerokuAppDestroy(s *terraform.State) error { + client := testAccProvider.client + + for _, rs := range s.Resources { + if rs.Type != "heroku_app" { + continue + } + + _, err := client.AppInfo(rs.ID) + + if err == nil { + return fmt.Errorf("App still exists") + } + } + + return nil +} + +func testAccCheckHerokuAppAttributes(app *heroku.App) resource.TestCheckFunc { + return func(s *terraform.State) error { + + // check attrs + + return nil + } +} + +func testAccCheckHerokuAppExists(n string, app *heroku.App) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.Resources[n] + fmt.Printf("resources %#v", s.Resources) + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.ID == "" { + return fmt.Errorf("No App Name is set") + } + + client := testAccProvider.client + + foundApp, err := client.AppInfo(rs.ID) + + if err != nil { + return err + } + + if foundApp.Name != rs.ID { + return fmt.Errorf("App not found") + } + + app = foundApp + + return nil + } +} + +const testAccCheckHerokuAppConfig_basic = ` +resource "heroku_app" "foobar" { + name = "terraform-test-app" +}` diff --git a/builtin/providers/heroku/resource_provider.go b/builtin/providers/heroku/resource_provider.go new file mode 100644 index 000000000..d15067cc2 --- /dev/null +++ b/builtin/providers/heroku/resource_provider.go @@ -0,0 +1,68 @@ +package heroku + +import ( + "log" + + "github.com/bgentry/heroku-go" + "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/terraform" +) + +type ResourceProvider struct { + Config Config + + client *heroku.Client +} + +func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []error) { + v := &config.Validator{ + Required: []string{ + "email", + "api_key", + }, + } + + return v.Validate(c) +} + +func (p *ResourceProvider) ValidateResource( + t string, c *terraform.ResourceConfig) ([]string, []error) { + return resourceMap.Validate(t, c) +} + +func (p *ResourceProvider) Configure(c *terraform.ResourceConfig) error { + if _, err := config.Decode(&p.Config, c.Config); err != nil { + return err + } + + log.Println("[INFO] Initializing Heroku client") + var err error + p.client, err = p.Config.Client() + + if err != nil { + return err + } + + return nil +} + +func (p *ResourceProvider) Apply( + s *terraform.ResourceState, + d *terraform.ResourceDiff) (*terraform.ResourceState, error) { + return resourceMap.Apply(s, d, p) +} + +func (p *ResourceProvider) Diff( + s *terraform.ResourceState, + c *terraform.ResourceConfig) (*terraform.ResourceDiff, error) { + return resourceMap.Diff(s, c, p) +} + +func (p *ResourceProvider) Refresh( + s *terraform.ResourceState) (*terraform.ResourceState, error) { + return resourceMap.Refresh(s, p) +} + +func (p *ResourceProvider) Resources() []terraform.ResourceType { + return resourceMap.Resources() +} diff --git a/builtin/providers/heroku/resource_provider_test.go b/builtin/providers/heroku/resource_provider_test.go new file mode 100644 index 000000000..8585e53cd --- /dev/null +++ b/builtin/providers/heroku/resource_provider_test.go @@ -0,0 +1,76 @@ +package heroku + +import ( + "os" + "reflect" + "testing" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *ResourceProvider + +func init() { + testAccProvider = new(ResourceProvider) + testAccProviders = map[string]terraform.ResourceProvider{ + "heroku": testAccProvider, + } +} + +func TestResourceProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = new(ResourceProvider) +} + +func TestResourceProvider_Configure(t *testing.T) { + rp := new(ResourceProvider) + var expectedKey string + var expectedEmail string + + if v := os.Getenv("HEROKU_EMAIL"); v != "" { + expectedEmail = v + } else { + expectedEmail = "foo" + } + + if v := os.Getenv("HEROKU_API_KEY"); v != "" { + expectedKey = v + } else { + expectedKey = "foo" + } + + raw := map[string]interface{}{ + "api_key": expectedKey, + "email": expectedEmail, + } + + rawConfig, err := config.NewRawConfig(raw) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = rp.Configure(terraform.NewResourceConfig(rawConfig)) + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := Config{ + APIKey: expectedKey, + Email: expectedEmail, + } + + if !reflect.DeepEqual(rp.Config, expected) { + t.Fatalf("bad: %#v", rp.Config) + } +} + +func testAccPreCheck(t *testing.T) { + if v := os.Getenv("HEROKU_EMAIL"); v == "" { + t.Fatal("HEROKU_EMAIL must be set for acceptance tests") + } + + if v := os.Getenv("HEROKU_API_KEY"); v == "" { + t.Fatal("HEROKU_API_KEY must be set for acceptance tests") + } +} diff --git a/builtin/providers/heroku/resources.go b/builtin/providers/heroku/resources.go new file mode 100644 index 000000000..00140d3f7 --- /dev/null +++ b/builtin/providers/heroku/resources.go @@ -0,0 +1,24 @@ +package heroku + +import ( + "github.com/hashicorp/terraform/helper/resource" +) + +// resourceMap is the mapping of resources we support to their basic +// operations. This makes it easy to implement new resource types. +var resourceMap *resource.Map + +func init() { + resourceMap = &resource.Map{ + Mapping: map[string]resource.Resource{ + "heroku_app": resource.Resource{ + ConfigValidator: resource_heroku_app_validation(), + Create: resource_heroku_app_create, + Destroy: resource_heroku_app_destroy, + Diff: resource_heroku_app_diff, + Refresh: resource_heroku_app_refresh, + Update: resource_heroku_app_update, + }, + }, + } +} diff --git a/config.go b/config.go index 76023875c..049bd38de 100644 --- a/config.go +++ b/config.go @@ -35,6 +35,7 @@ func init() { BuiltinConfig.Providers = map[string]string{ "aws": "terraform-provider-aws", "digitalocean": "terraform-provider-digitalocean", + "heroku": "terraform-provider-heroku", } BuiltinConfig.Provisioners = map[string]string{ "local-exec": "terraform-provisioner-local-exec",