diff --git a/builtin/providers/heroku/provider.go b/builtin/providers/heroku/provider.go index 6a8c9b986..08432ac98 100644 --- a/builtin/providers/heroku/provider.go +++ b/builtin/providers/heroku/provider.go @@ -1,7 +1,9 @@ package heroku import ( + "fmt" "log" + "strings" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" @@ -25,12 +27,13 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "heroku_addon": resourceHerokuAddon(), - "heroku_app": resourceHerokuApp(), - "heroku_cert": resourceHerokuCert(), - "heroku_domain": resourceHerokuDomain(), - "heroku_drain": resourceHerokuDrain(), - "heroku_space": resourceHerokuSpace(), + "heroku_addon": resourceHerokuAddon(), + "heroku_app": resourceHerokuApp(), + "heroku_app_feature": resourceHerokuAppFeature(), + "heroku_cert": resourceHerokuCert(), + "heroku_domain": resourceHerokuDomain(), + "heroku_drain": resourceHerokuDrain(), + "heroku_space": resourceHerokuSpace(), }, ConfigureFunc: providerConfigure, @@ -46,3 +49,12 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { log.Println("[INFO] Initializing Heroku client") return config.Client() } + +func buildCompositeID(a, b string) string { + return fmt.Sprintf("%s:%s", a, b) +} + +func parseCompositeID(id string) (string, string) { + parts := strings.SplitN(id, ":", 2) + return parts[0], parts[1] +} diff --git a/builtin/providers/heroku/resource_heroku_app_feature.go b/builtin/providers/heroku/resource_heroku_app_feature.go new file mode 100644 index 000000000..c93ef968c --- /dev/null +++ b/builtin/providers/heroku/resource_heroku_app_feature.go @@ -0,0 +1,101 @@ +package heroku + +import ( + "context" + "log" + + heroku "github.com/cyberdelia/heroku-go/v3" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceHerokuAppFeature() *schema.Resource { + return &schema.Resource{ + Create: resourceAppFeatureCreate, + Update: resourceAppFeatureUpdate, + Read: resourceAppFeatureRead, + Delete: resourceAppFeatureDelete, + + Schema: map[string]*schema.Schema{ + "app": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + }, + } +} + +func resourceAppFeatureRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*heroku.Service) + + app, id := parseCompositeID(d.Id()) + + feature, err := client.AppFeatureInfo(context.TODO(), app, id) + if err != nil { + return err + } + + d.Set("app", app) + d.Set("name", feature.Name) + d.Set("enabled", feature.Enabled) + + return nil +} + +func resourceAppFeatureCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*heroku.Service) + + app := d.Get("app").(string) + featureName := d.Get("name").(string) + enabled := d.Get("enabled").(bool) + + opts := heroku.AppFeatureUpdateOpts{Enabled: enabled} + + log.Printf("[DEBUG] Feature set configuration: %#v, %#v", featureName, opts) + + feature, err := client.AppFeatureUpdate(context.TODO(), app, featureName, opts) + if err != nil { + return err + } + + d.SetId(buildCompositeID(app, feature.ID)) + + return resourceAppFeatureRead(d, meta) +} + +func resourceAppFeatureUpdate(d *schema.ResourceData, meta interface{}) error { + if d.HasChange("enabled") { + return resourceAppFeatureCreate(d, meta) + } + + return resourceAppFeatureRead(d, meta) +} + +func resourceAppFeatureDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*heroku.Service) + + app, id := parseCompositeID(d.Id()) + featureName := d.Get("name").(string) + + log.Printf("[INFO] Deleting app feature %s (%s) for app %s", featureName, id, app) + opts := heroku.AppFeatureUpdateOpts{Enabled: false} + _, err := client.AppFeatureUpdate(context.TODO(), app, id, opts) + if err != nil { + return err + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/heroku/resource_heroku_app_feature_test.go b/builtin/providers/heroku/resource_heroku_app_feature_test.go new file mode 100644 index 000000000..870216ae9 --- /dev/null +++ b/builtin/providers/heroku/resource_heroku_app_feature_test.go @@ -0,0 +1,135 @@ +package heroku + +import ( + "context" + "fmt" + "testing" + + heroku "github.com/cyberdelia/heroku-go/v3" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccHerokuAppFeature(t *testing.T) { + var feature heroku.AppFeatureInfoResult + appName := fmt.Sprintf("tftest-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckHerokuFeatureDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckHerokuFeature_basic(appName), + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuFeatureExists("heroku_app_feature.runtime_metrics", &feature), + testAccCheckHerokuFeatureEnabled(&feature, true), + resource.TestCheckResourceAttr( + "heroku_app_feature.runtime_metrics", "enabled", "true", + ), + ), + }, + { + Config: testAccCheckHerokuFeature_disabled(appName), + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuFeatureExists("heroku_app_feature.runtime_metrics", &feature), + testAccCheckHerokuFeatureEnabled(&feature, false), + resource.TestCheckResourceAttr( + "heroku_app_feature.runtime_metrics", "enabled", "false", + ), + ), + }, + }, + }) +} + +func testAccCheckHerokuFeatureDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*heroku.Service) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "heroku_app_feature" { + continue + } + + _, err := client.AppFeatureInfo(context.TODO(), rs.Primary.Attributes["app"], rs.Primary.ID) + + if err == nil { + return fmt.Errorf("Feature still exists") + } + } + + return nil +} + +func testAccCheckHerokuFeatureExists(n string, feature *heroku.AppFeatureInfoResult) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No feature ID is set") + } + + app, id := parseCompositeID(rs.Primary.ID) + if app != rs.Primary.Attributes["app"] { + return fmt.Errorf("Bad app: %s", app) + } + + client := testAccProvider.Meta().(*heroku.Service) + + foundFeature, err := client.AppFeatureInfo(context.TODO(), app, id) + if err != nil { + return err + } + + if foundFeature.ID != id { + return fmt.Errorf("Feature not found") + } + + *feature = *foundFeature + return nil + } +} + +func testAccCheckHerokuFeatureEnabled(feature *heroku.AppFeatureInfoResult, enabled bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + if feature.Enabled != enabled { + return fmt.Errorf("Bad enabled: %v", feature.Enabled) + } + + return nil + } +} + +func testAccCheckHerokuFeature_basic(appName string) string { + return fmt.Sprintf(` +resource "heroku_app" "example" { + name = "%s" + region = "us" +} + +resource "heroku_app_feature" "runtime_metrics" { + app = "${heroku_app.example.name}" + name = "log-runtime-metrics" +} +`, appName) +} + +func testAccCheckHerokuFeature_disabled(appName string) string { + return fmt.Sprintf(` +resource "heroku_app" "example" { + name = "%s" + region = "us" +} + +resource "heroku_app_feature" "runtime_metrics" { + app = "${heroku_app.example.name}" + name = "log-runtime-metrics" + enabled = false +} +`, appName) +} diff --git a/website/source/docs/providers/heroku/r/app_feature.html.markdown b/website/source/docs/providers/heroku/r/app_feature.html.markdown new file mode 100644 index 000000000..c532b47d7 --- /dev/null +++ b/website/source/docs/providers/heroku/r/app_feature.html.markdown @@ -0,0 +1,28 @@ +--- +layout: "heroku" +page_title: "Heroku: heroku_app_feature" +sidebar_current: "docs-heroku-resource-app-feature" +description: |- + Provides a Heroku App Feature resource. This can be used to create and manage App Features on Heroku. +--- + +# heroku\_app\_feature + +Provides a Heroku App Feature resource. This can be used to create and manage App Features on Heroku. + +## Example Usage + +```hcl +resource "heroku_app_feature" "log_runtime_metrics" { + app = "test-app" + name = "log-runtime-metrics" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `app` - (Required) The Heroku app to link to. +* `name` - (Required) The name of the App Feature to manage. +* `enabled` - (Optional) Whether to enable or disable the App Feature. The default value is true.