diff --git a/builtin/providers/heroku2/config.go b/builtin/providers/heroku2/config.go new file mode 100644 index 000000000..614c3831f --- /dev/null +++ b/builtin/providers/heroku2/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/heroku2/resource_heroku_addon.go b/builtin/providers/heroku2/resource_heroku_addon.go new file mode 100644 index 000000000..967e8a02f --- /dev/null +++ b/builtin/providers/heroku2/resource_heroku_addon.go @@ -0,0 +1,199 @@ +package heroku + +import ( + "fmt" + "log" + "sync" + + "github.com/bgentry/heroku-go" + "github.com/hashicorp/terraform/flatmap" + "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/helper/diff" + "github.com/hashicorp/terraform/terraform" +) + +// Global lock to prevent parallelism for heroku_addon since +// the Heroku API cannot handle a single application requesting +// multiple addons simultaneously. +var addonLock sync.Mutex + +func resource_heroku_addon_create( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + addonLock.Lock() + defer addonLock.Unlock() + + p := meta.(*ResourceProvider) + client := p.client + + // Merge the diff into the state so that we have all the attributes + // properly. + rs := s.MergeDiff(d) + + app := rs.Attributes["app"] + plan := rs.Attributes["plan"] + opts := heroku.AddonCreateOpts{} + + if attr, ok := rs.Attributes["config.#"]; ok && attr == "1" { + vs := flatmap.Expand( + rs.Attributes, "config").([]interface{}) + + config := make(map[string]string) + for k, v := range vs[0].(map[string]interface{}) { + config[k] = v.(string) + } + + opts.Config = &config + } + + log.Printf("[DEBUG] Addon create configuration: %#v, %#v, %#v", app, plan, opts) + + a, err := client.AddonCreate(app, plan, &opts) + + if err != nil { + return s, err + } + + rs.ID = a.Id + log.Printf("[INFO] Addon ID: %s", rs.ID) + + addon, err := resource_heroku_addon_retrieve(app, rs.ID, client) + if err != nil { + return rs, err + } + + return resource_heroku_addon_update_state(rs, addon) +} + +func resource_heroku_addon_update( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + rs := s.MergeDiff(d) + + app := rs.Attributes["app"] + + if attr, ok := d.Attributes["plan"]; ok { + ad, err := client.AddonUpdate( + app, rs.ID, + attr.New) + + if err != nil { + return s, err + } + + // Store the new ID + rs.ID = ad.Id + } + + addon, err := resource_heroku_addon_retrieve(app, rs.ID, client) + + if err != nil { + return rs, err + } + + return resource_heroku_addon_update_state(rs, addon) +} + +func resource_heroku_addon_destroy( + s *terraform.ResourceState, + meta interface{}) error { + p := meta.(*ResourceProvider) + client := p.client + + log.Printf("[INFO] Deleting Addon: %s", s.ID) + + // Destroy the app + err := client.AddonDelete(s.Attributes["app"], s.ID) + + if err != nil { + return fmt.Errorf("Error deleting addon: %s", err) + } + + return nil +} + +func resource_heroku_addon_refresh( + s *terraform.ResourceState, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + + app, err := resource_heroku_addon_retrieve(s.Attributes["app"], s.ID, client) + if err != nil { + return nil, err + } + + return resource_heroku_addon_update_state(s, app) +} + +func resource_heroku_addon_diff( + s *terraform.ResourceState, + c *terraform.ResourceConfig, + meta interface{}) (*terraform.ResourceDiff, error) { + + b := &diff.ResourceBuilder{ + Attrs: map[string]diff.AttrType{ + "app": diff.AttrTypeCreate, + "plan": diff.AttrTypeUpdate, + "config": diff.AttrTypeCreate, + }, + + ComputedAttrs: []string{ + "provider_id", + "config_vars", + }, + } + + return b.Diff(s, c) +} + +func resource_heroku_addon_update_state( + s *terraform.ResourceState, + addon *heroku.Addon) (*terraform.ResourceState, error) { + + s.Attributes["name"] = addon.Name + s.Attributes["plan"] = addon.Plan.Name + s.Attributes["provider_id"] = addon.ProviderId + + toFlatten := make(map[string]interface{}) + + if len(addon.ConfigVars) > 0 { + toFlatten["config_vars"] = addon.ConfigVars + } + + for k, v := range flatmap.Flatten(toFlatten) { + s.Attributes[k] = v + } + + s.Dependencies = []terraform.ResourceDependency{ + terraform.ResourceDependency{ID: s.Attributes["app"]}, + } + + return s, nil +} + +func resource_heroku_addon_retrieve(app string, id string, client *heroku.Client) (*heroku.Addon, error) { + addon, err := client.AddonInfo(app, id) + + if err != nil { + return nil, fmt.Errorf("Error retrieving addon: %s", err) + } + + return addon, nil +} + +func resource_heroku_addon_validation() *config.Validator { + return &config.Validator{ + Required: []string{ + "app", + "plan", + }, + Optional: []string{ + "config.*", + }, + } +} diff --git a/builtin/providers/heroku2/resource_heroku_addon_test.go b/builtin/providers/heroku2/resource_heroku_addon_test.go new file mode 100644 index 000000000..1c099b683 --- /dev/null +++ b/builtin/providers/heroku2/resource_heroku_addon_test.go @@ -0,0 +1,107 @@ +package heroku + +import ( + "fmt" + "testing" + + "github.com/bgentry/heroku-go" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccHerokuAddon_Basic(t *testing.T) { + var addon heroku.Addon + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckHerokuAddonDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckHerokuAddonConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAddonExists("heroku_addon.foobar", &addon), + testAccCheckHerokuAddonAttributes(&addon), + resource.TestCheckResourceAttr( + "heroku_addon.foobar", "config.0.url", "http://google.com"), + resource.TestCheckResourceAttr( + "heroku_addon.foobar", "app", "terraform-test-app"), + resource.TestCheckResourceAttr( + "heroku_addon.foobar", "plan", "deployhooks:http"), + ), + }, + }, + }) +} + +func testAccCheckHerokuAddonDestroy(s *terraform.State) error { + client := testAccProvider.client + + for _, rs := range s.Resources { + if rs.Type != "heroku_addon" { + continue + } + + _, err := client.AddonInfo(rs.Attributes["app"], rs.ID) + + if err == nil { + return fmt.Errorf("Addon still exists") + } + } + + return nil +} + +func testAccCheckHerokuAddonAttributes(addon *heroku.Addon) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if addon.Plan.Name != "deployhooks:http" { + return fmt.Errorf("Bad plan: %s", addon.Plan) + } + + return nil + } +} + +func testAccCheckHerokuAddonExists(n string, addon *heroku.Addon) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.ID == "" { + return fmt.Errorf("No Addon ID is set") + } + + client := testAccProvider.client + + foundAddon, err := client.AddonInfo(rs.Attributes["app"], rs.ID) + + if err != nil { + return err + } + + if foundAddon.Id != rs.ID { + return fmt.Errorf("Addon not found") + } + + *addon = *foundAddon + + return nil + } +} + +const testAccCheckHerokuAddonConfig_basic = ` +resource "heroku_app" "foobar" { + name = "terraform-test-app" +} + +resource "heroku_addon" "foobar" { + app = "${heroku_app.foobar.name}" + plan = "deployhooks:http" + config { + url = "http://google.com" + } +}` diff --git a/builtin/providers/heroku2/resource_heroku_app.go b/builtin/providers/heroku2/resource_heroku_app.go new file mode 100644 index 000000000..0655daf3a --- /dev/null +++ b/builtin/providers/heroku2/resource_heroku_app.go @@ -0,0 +1,295 @@ +package heroku + +import ( + "fmt" + "log" + + "github.com/bgentry/heroku-go" + "github.com/hashicorp/terraform/flatmap" + "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/helper/diff" + "github.com/hashicorp/terraform/helper/multierror" + "github.com/hashicorp/terraform/terraform" +) + +// type application is used to store all the details of a heroku app +type application struct { + Id string // Id of the resource + + App *heroku.App // The heroku application + Client *heroku.Client // Client to interact with the heroku API + Vars map[string]string // The vars on the application +} + +// Updates the application to have the latest from remote +func (a *application) Update() error { + var errs []error + var err error + + a.App, err = a.Client.AppInfo(a.Id) + if err != nil { + errs = append(errs, err) + } + + a.Vars, err = retrieve_config_vars(a.Id, a.Client) + if err != nil { + errs = append(errs, err) + } + + if len(errs) > 0 { + return &multierror.Error{Errors: errs} + } + + return nil +} + +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) + + a, err := client.AppCreate(&opts) + if err != nil { + return s, err + } + + rs.ID = a.Name + log.Printf("[INFO] App ID: %s", rs.ID) + + if attr, ok := rs.Attributes["config_vars.#"]; ok && attr == "1" { + vs := flatmap.Expand( + rs.Attributes, "config_vars").([]interface{}) + + err = update_config_vars(rs.ID, vs, client) + if err != nil { + return rs, err + } + } + + app, err := resource_heroku_app_retrieve(rs.ID, client) + if err != nil { + return rs, err + } + + return resource_heroku_app_update_state(rs, app) +} + +func resource_heroku_app_update( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + rs := s.MergeDiff(d) + + if attr, ok := d.Attributes["name"]; ok { + opts := heroku.AppUpdateOpts{ + Name: &attr.New, + } + + renamedApp, err := client.AppUpdate(rs.ID, &opts) + + if err != nil { + return s, err + } + + // Store the new ID + rs.ID = renamedApp.Name + } + + attr, ok := s.Attributes["config_vars.#"] + + // If the config var block was removed, nuke all config vars + if ok && attr == "1" { + vs := flatmap.Expand( + rs.Attributes, "config_vars").([]interface{}) + + err := update_config_vars(rs.ID, vs, client) + if err != nil { + return rs, err + } + } else if ok && attr == "0" { + log.Println("[INFO] Config vars removed, removing all vars") + + err := update_config_vars(rs.ID, make([]interface{}, 0), client) + + if err != nil { + return rs, err + } + } + + app, err := resource_heroku_app_retrieve(rs.ID, client) + if err != nil { + return rs, err + } + + return resource_heroku_app_update_state(rs, app) +} + +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.AttrTypeUpdate, + "region": diff.AttrTypeUpdate, + "stack": diff.AttrTypeCreate, + "config_vars": diff.AttrTypeUpdate, + }, + + ComputedAttrs: []string{ + "name", + "region", + "stack", + "git_url", + "web_url", + "id", + "config_vars", + }, + + ComputedAttrsUpdate: []string{ + "heroku_hostname", + }, + } + + return b.Diff(s, c) +} + +func resource_heroku_app_update_state( + s *terraform.ResourceState, + app *application) (*terraform.ResourceState, error) { + + s.Attributes["name"] = app.App.Name + s.Attributes["stack"] = app.App.Stack.Name + s.Attributes["region"] = app.App.Region.Name + s.Attributes["git_url"] = app.App.GitURL + s.Attributes["web_url"] = app.App.WebURL + + // We know that the hostname on heroku will be the name+herokuapp.com + // You need this to do things like create DNS CNAME records + s.Attributes["heroku_hostname"] = fmt.Sprintf("%s.herokuapp.com", app.App.Name) + + toFlatten := make(map[string]interface{}) + + if len(app.Vars) > 0 { + toFlatten["config_vars"] = []map[string]string{app.Vars} + } + + for k, v := range flatmap.Flatten(toFlatten) { + s.Attributes[k] = v + } + + return s, nil +} + +func resource_heroku_app_retrieve(id string, client *heroku.Client) (*application, error) { + app := application{Id: id, Client: client} + + err := app.Update() + + 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", + "config_vars.*", + }, + } +} + +func retrieve_config_vars(id string, client *heroku.Client) (map[string]string, error) { + vars, err := client.ConfigVarInfo(id) + + if err != nil { + return nil, err + } + + return vars, nil +} + +// Updates the config vars for from an expanded (prior to assertion) +// []map[string]string config +func update_config_vars(id string, vs []interface{}, client *heroku.Client) error { + vars := make(map[string]*string) + + for k, v := range vs[0].(map[string]interface{}) { + val := v.(string) + vars[k] = &val + } + + log.Printf("[INFO] Updating config vars: *%#v", vars) + + _, err := client.ConfigVarUpdate(id, vars) + + if err != nil { + return fmt.Errorf("Error updating config vars: %s", err) + } + + return nil +} diff --git a/builtin/providers/heroku2/resource_heroku_app_test.go b/builtin/providers/heroku2/resource_heroku_app_test.go new file mode 100644 index 000000000..ff7c6f110 --- /dev/null +++ b/builtin/providers/heroku2/resource_heroku_app_test.go @@ -0,0 +1,252 @@ +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"), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "config_vars.0.FOO", "bar"), + ), + }, + }, + }) +} + +func TestAccHerokuApp_NameChange(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"), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "config_vars.0.FOO", "bar"), + ), + }, + resource.TestStep{ + Config: testAccCheckHerokuAppConfig_updated, + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAppExists("heroku_app.foobar", &app), + testAccCheckHerokuAppAttributesUpdated(&app), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "name", "terraform-test-renamed"), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "config_vars.0.FOO", "bing"), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "config_vars.0.BAZ", "bar"), + ), + }, + }, + }) +} + +func TestAccHerokuApp_NukeVars(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"), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "config_vars.0.FOO", "bar"), + ), + }, + resource.TestStep{ + Config: testAccCheckHerokuAppConfig_no_vars, + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAppExists("heroku_app.foobar", &app), + testAccCheckHerokuAppAttributesNoVars(&app), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "name", "terraform-test-app"), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "config_vars.0.FOO", ""), + ), + }, + }, + }) +} + +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 { + client := testAccProvider.client + + if app.Region.Name != "us" { + return fmt.Errorf("Bad region: %s", app.Region.Name) + } + + if app.Stack.Name != "cedar" { + return fmt.Errorf("Bad stack: %s", app.Stack.Name) + } + + if app.Name != "terraform-test-app" { + return fmt.Errorf("Bad name: %s", app.Name) + } + + vars, err := client.ConfigVarInfo(app.Name) + if err != nil { + return err + } + + if vars["FOO"] != "bar" { + return fmt.Errorf("Bad config vars: %v", vars) + } + + return nil + } +} + +func testAccCheckHerokuAppAttributesUpdated(app *heroku.App) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.client + + if app.Name != "terraform-test-renamed" { + return fmt.Errorf("Bad name: %s", app.Name) + } + + vars, err := client.ConfigVarInfo(app.Name) + if err != nil { + return err + } + + // Make sure we kept the old one + if vars["FOO"] != "bing" { + return fmt.Errorf("Bad config vars: %v", vars) + } + + if vars["BAZ"] != "bar" { + return fmt.Errorf("Bad config vars: %v", vars) + } + + return nil + + } +} + +func testAccCheckHerokuAppAttributesNoVars(app *heroku.App) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.client + + if app.Name != "terraform-test-app" { + return fmt.Errorf("Bad name: %s", app.Name) + } + + vars, err := client.ConfigVarInfo(app.Name) + if err != nil { + return err + } + + if len(vars) != 0 { + return fmt.Errorf("vars exist: %v", vars) + } + + return nil + } +} + +func testAccCheckHerokuAppExists(n string, app *heroku.App) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.Resources[n] + + 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" + + config_vars { + FOO = "bar" + } +}` + +const testAccCheckHerokuAppConfig_updated = ` +resource "heroku_app" "foobar" { + name = "terraform-test-renamed" + + config_vars { + FOO = "bing" + BAZ = "bar" + } +}` + +const testAccCheckHerokuAppConfig_no_vars = ` +resource "heroku_app" "foobar" { + name = "terraform-test-app" +}` diff --git a/builtin/providers/heroku2/resource_heroku_domain.go b/builtin/providers/heroku2/resource_heroku_domain.go new file mode 100644 index 000000000..6f00fdf55 --- /dev/null +++ b/builtin/providers/heroku2/resource_heroku_domain.go @@ -0,0 +1,126 @@ +package heroku + +import ( + "fmt" + "log" + + "github.com/bgentry/heroku-go" + "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/helper/diff" + "github.com/hashicorp/terraform/terraform" +) + +func resource_heroku_domain_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) + + app := rs.Attributes["app"] + hostname := rs.Attributes["hostname"] + + log.Printf("[DEBUG] Domain create configuration: %#v, %#v", app, hostname) + + do, err := client.DomainCreate(app, hostname) + + if err != nil { + return s, err + } + + rs.ID = do.Id + rs.Attributes["hostname"] = do.Hostname + rs.Attributes["cname"] = fmt.Sprintf("%s.herokuapp.com", app) + + log.Printf("[INFO] Domain ID: %s", rs.ID) + + return rs, nil +} + +func resource_heroku_domain_update( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + + panic("Cannot update domain") + + return nil, nil +} + +func resource_heroku_domain_destroy( + s *terraform.ResourceState, + meta interface{}) error { + p := meta.(*ResourceProvider) + client := p.client + + log.Printf("[INFO] Deleting Domain: %s", s.ID) + + // Destroy the app + err := client.DomainDelete(s.Attributes["app"], s.ID) + + if err != nil { + return fmt.Errorf("Error deleting domain: %s", err) + } + + return nil +} + +func resource_heroku_domain_refresh( + s *terraform.ResourceState, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + + domain, err := resource_heroku_domain_retrieve(s.Attributes["app"], s.ID, client) + if err != nil { + return nil, err + } + + s.Attributes["hostname"] = domain.Hostname + s.Attributes["cname"] = fmt.Sprintf("%s.herokuapp.com", s.Attributes["app"]) + + return s, nil +} + +func resource_heroku_domain_diff( + s *terraform.ResourceState, + c *terraform.ResourceConfig, + meta interface{}) (*terraform.ResourceDiff, error) { + + b := &diff.ResourceBuilder{ + Attrs: map[string]diff.AttrType{ + "hostname": diff.AttrTypeCreate, + "app": diff.AttrTypeCreate, + }, + + ComputedAttrs: []string{ + "cname", + }, + } + + return b.Diff(s, c) +} + +func resource_heroku_domain_retrieve(app string, id string, client *heroku.Client) (*heroku.Domain, error) { + domain, err := client.DomainInfo(app, id) + + if err != nil { + return nil, fmt.Errorf("Error retrieving domain: %s", err) + } + + return domain, nil +} + +func resource_heroku_domain_validation() *config.Validator { + return &config.Validator{ + Required: []string{ + "hostname", + "app", + }, + Optional: []string{}, + } +} diff --git a/builtin/providers/heroku2/resource_heroku_domain_test.go b/builtin/providers/heroku2/resource_heroku_domain_test.go new file mode 100644 index 000000000..315881690 --- /dev/null +++ b/builtin/providers/heroku2/resource_heroku_domain_test.go @@ -0,0 +1,104 @@ +package heroku + +import ( + "fmt" + "testing" + + "github.com/bgentry/heroku-go" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccHerokuDomain_Basic(t *testing.T) { + var domain heroku.Domain + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckHerokuDomainDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckHerokuDomainConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuDomainExists("heroku_domain.foobar", &domain), + testAccCheckHerokuDomainAttributes(&domain), + resource.TestCheckResourceAttr( + "heroku_domain.foobar", "hostname", "terraform.example.com"), + resource.TestCheckResourceAttr( + "heroku_domain.foobar", "app", "terraform-test-app"), + resource.TestCheckResourceAttr( + "heroku_domain.foobar", "cname", "terraform-test-app.herokuapp.com"), + ), + }, + }, + }) +} + +func testAccCheckHerokuDomainDestroy(s *terraform.State) error { + client := testAccProvider.client + + for _, rs := range s.Resources { + if rs.Type != "heroku_domain" { + continue + } + + _, err := client.DomainInfo(rs.Attributes["app"], rs.ID) + + if err == nil { + return fmt.Errorf("Domain still exists") + } + } + + return nil +} + +func testAccCheckHerokuDomainAttributes(Domain *heroku.Domain) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if Domain.Hostname != "terraform.example.com" { + return fmt.Errorf("Bad hostname: %s", Domain.Hostname) + } + + return nil + } +} + +func testAccCheckHerokuDomainExists(n string, Domain *heroku.Domain) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.ID == "" { + return fmt.Errorf("No Domain ID is set") + } + + client := testAccProvider.client + + foundDomain, err := client.DomainInfo(rs.Attributes["app"], rs.ID) + + if err != nil { + return err + } + + if foundDomain.Id != rs.ID { + return fmt.Errorf("Domain not found") + } + + *Domain = *foundDomain + + return nil + } +} + +const testAccCheckHerokuDomainConfig_basic = ` +resource "heroku_app" "foobar" { + name = "terraform-test-app" +} + +resource "heroku_domain" "foobar" { + app = "${heroku_app.foobar.name}" + hostname = "terraform.example.com" +}` diff --git a/builtin/providers/heroku2/resource_heroku_drain.go b/builtin/providers/heroku2/resource_heroku_drain.go new file mode 100644 index 000000000..908125eea --- /dev/null +++ b/builtin/providers/heroku2/resource_heroku_drain.go @@ -0,0 +1,126 @@ +package heroku + +import ( + "fmt" + "log" + + "github.com/bgentry/heroku-go" + "github.com/hashicorp/terraform/helper/config" + "github.com/hashicorp/terraform/helper/diff" + "github.com/hashicorp/terraform/terraform" +) + +func resource_heroku_drain_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) + + app := rs.Attributes["app"] + url := rs.Attributes["url"] + + log.Printf("[DEBUG] Drain create configuration: %#v, %#v", app, url) + + dr, err := client.LogDrainCreate(app, url) + + if err != nil { + return s, err + } + + rs.ID = dr.Id + rs.Attributes["url"] = dr.URL + rs.Attributes["token"] = dr.Token + + log.Printf("[INFO] Drain ID: %s", rs.ID) + + return rs, nil +} + +func resource_heroku_drain_update( + s *terraform.ResourceState, + d *terraform.ResourceDiff, + meta interface{}) (*terraform.ResourceState, error) { + + panic("Cannot update drain") + + return nil, nil +} + +func resource_heroku_drain_destroy( + s *terraform.ResourceState, + meta interface{}) error { + p := meta.(*ResourceProvider) + client := p.client + + log.Printf("[INFO] Deleting drain: %s", s.ID) + + // Destroy the app + err := client.LogDrainDelete(s.Attributes["app"], s.ID) + + if err != nil { + return fmt.Errorf("Error deleting drain: %s", err) + } + + return nil +} + +func resource_heroku_drain_refresh( + s *terraform.ResourceState, + meta interface{}) (*terraform.ResourceState, error) { + p := meta.(*ResourceProvider) + client := p.client + + drain, err := resource_heroku_drain_retrieve(s.Attributes["app"], s.ID, client) + if err != nil { + return nil, err + } + + s.Attributes["url"] = drain.URL + s.Attributes["token"] = drain.Token + + return s, nil +} + +func resource_heroku_drain_diff( + s *terraform.ResourceState, + c *terraform.ResourceConfig, + meta interface{}) (*terraform.ResourceDiff, error) { + + b := &diff.ResourceBuilder{ + Attrs: map[string]diff.AttrType{ + "url": diff.AttrTypeCreate, + "app": diff.AttrTypeCreate, + }, + + ComputedAttrs: []string{ + "token", + }, + } + + return b.Diff(s, c) +} + +func resource_heroku_drain_retrieve(app string, id string, client *heroku.Client) (*heroku.LogDrain, error) { + drain, err := client.LogDrainInfo(app, id) + + if err != nil { + return nil, fmt.Errorf("Error retrieving drain: %s", err) + } + + return drain, nil +} + +func resource_heroku_drain_validation() *config.Validator { + return &config.Validator{ + Required: []string{ + "url", + "app", + }, + Optional: []string{}, + } +} diff --git a/builtin/providers/heroku2/resource_heroku_drain_test.go b/builtin/providers/heroku2/resource_heroku_drain_test.go new file mode 100644 index 000000000..9a848ac98 --- /dev/null +++ b/builtin/providers/heroku2/resource_heroku_drain_test.go @@ -0,0 +1,106 @@ +package heroku + +import ( + "fmt" + "testing" + + "github.com/bgentry/heroku-go" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccHerokuDrain_Basic(t *testing.T) { + var drain heroku.LogDrain + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckHerokuDrainDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckHerokuDrainConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuDrainExists("heroku_drain.foobar", &drain), + testAccCheckHerokuDrainAttributes(&drain), + resource.TestCheckResourceAttr( + "heroku_drain.foobar", "url", "syslog://terraform.example.com:1234"), + resource.TestCheckResourceAttr( + "heroku_drain.foobar", "app", "terraform-test-app"), + ), + }, + }, + }) +} + +func testAccCheckHerokuDrainDestroy(s *terraform.State) error { + client := testAccProvider.client + + for _, rs := range s.Resources { + if rs.Type != "heroku_drain" { + continue + } + + _, err := client.LogDrainInfo(rs.Attributes["app"], rs.ID) + + if err == nil { + return fmt.Errorf("Drain still exists") + } + } + + return nil +} + +func testAccCheckHerokuDrainAttributes(Drain *heroku.LogDrain) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if Drain.URL != "syslog://terraform.example.com:1234" { + return fmt.Errorf("Bad URL: %s", Drain.URL) + } + + if Drain.Token == "" { + return fmt.Errorf("No token: %#v", Drain) + } + + return nil + } +} + +func testAccCheckHerokuDrainExists(n string, Drain *heroku.LogDrain) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.ID == "" { + return fmt.Errorf("No Drain ID is set") + } + + client := testAccProvider.client + + foundDrain, err := client.LogDrainInfo(rs.Attributes["app"], rs.ID) + + if err != nil { + return err + } + + if foundDrain.Id != rs.ID { + return fmt.Errorf("Drain not found") + } + + *Drain = *foundDrain + + return nil + } +} + +const testAccCheckHerokuDrainConfig_basic = ` +resource "heroku_app" "foobar" { + name = "terraform-test-app" +} + +resource "heroku_drain" "foobar" { + app = "${heroku_app.foobar.name}" + url = "syslog://terraform.example.com:1234" +}` diff --git a/builtin/providers/heroku2/resource_provider.go b/builtin/providers/heroku2/resource_provider.go new file mode 100644 index 000000000..d15067cc2 --- /dev/null +++ b/builtin/providers/heroku2/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/heroku2/resource_provider_test.go b/builtin/providers/heroku2/resource_provider_test.go new file mode 100644 index 000000000..8585e53cd --- /dev/null +++ b/builtin/providers/heroku2/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/heroku2/resources.go b/builtin/providers/heroku2/resources.go new file mode 100644 index 000000000..4c47632f8 --- /dev/null +++ b/builtin/providers/heroku2/resources.go @@ -0,0 +1,49 @@ +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_addon": resource.Resource{ + ConfigValidator: resource_heroku_addon_validation(), + Create: resource_heroku_addon_create, + Destroy: resource_heroku_addon_destroy, + Diff: resource_heroku_addon_diff, + Refresh: resource_heroku_addon_refresh, + Update: resource_heroku_addon_update, + }, + + "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, + }, + + "heroku_domain": resource.Resource{ + ConfigValidator: resource_heroku_domain_validation(), + Create: resource_heroku_domain_create, + Destroy: resource_heroku_domain_destroy, + Diff: resource_heroku_domain_diff, + Refresh: resource_heroku_domain_refresh, + }, + + "heroku_drain": resource.Resource{ + ConfigValidator: resource_heroku_drain_validation(), + Create: resource_heroku_drain_create, + Destroy: resource_heroku_drain_destroy, + Diff: resource_heroku_drain_diff, + Refresh: resource_heroku_drain_refresh, + }, + }, + } +}