From 3f37ce317f0d500095453938438b6ff7c686612b Mon Sep 17 00:00:00 2001 From: Mars Hall Date: Fri, 17 Dec 2021 08:28:44 -0800 Subject: [PATCH] heroku_addon "config_var_values" (#325) * New heroku_addon "config_var_values" attribute * Disable tests failing due to SSL Endpoint shutdown * Await all add-on config vars appearing, before completing creation; refactor selection of config vars; improve docs * Move add-on config var retry/await into create; clean-up debug logs --- docs/resources/addon.md | 1 + heroku/import_heroku_cert_test.go | 2 + heroku/resource_heroku_addon.go | 66 +++++++++++++++++++++++--- heroku/resource_heroku_addon_test.go | 69 ++++++++++++++++++++++++++-- heroku/resource_heroku_cert_test.go | 4 ++ 5 files changed, 130 insertions(+), 12 deletions(-) diff --git a/docs/resources/addon.md b/docs/resources/addon.md index 3d77dd06..18927ef1 100644 --- a/docs/resources/addon.md +++ b/docs/resources/addon.md @@ -54,6 +54,7 @@ The following attributes are exported: * `plan` - The plan name * `provider_id` - The ID of the plan provider * `config_vars` - The Configuration variables of the add-on +* `config_var_values` - A sensitive map of the add-on's configuration variables. Upon add-on creation, these values will be up-to-date, while the app's own `config_vars` require another Terraform refresh cycle to be updated. Useful when an output contains an add-on config var value, or when a configuration needs to operate on a new add-on during an apply. ## Import diff --git a/heroku/import_heroku_cert_test.go b/heroku/import_heroku_cert_test.go index 0e5fd0f7..842ca06f 100644 --- a/heroku/import_heroku_cert_test.go +++ b/heroku/import_heroku_cert_test.go @@ -10,6 +10,8 @@ import ( ) func TestAccHerokuCert_importBasic(t *testing.T) { + t.Skip("SSL Endpoint shutdown: https://devcenter.heroku.com/changelog-items/2280") + appName := fmt.Sprintf("tftest-%s", acctest.RandString(10)) wd, _ := os.Getwd() diff --git a/heroku/resource_heroku_addon.go b/heroku/resource_heroku_addon.go index ac8c6638..1fa15faf 100644 --- a/heroku/resource_heroku_addon.go +++ b/heroku/resource_heroku_addon.go @@ -3,7 +3,6 @@ package heroku import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "log" "net/url" "regexp" @@ -11,6 +10,8 @@ import ( "sync" "time" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" heroku "github.com/heroku/heroku-go/v5" @@ -73,6 +74,17 @@ func resourceHerokuAddon() *schema.Resource { Type: schema.TypeString, }, }, + + "config_var_values": { + Type: schema.TypeMap, + Optional: true, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + Sensitive: true, + }, + Sensitive: true, + }, }, } } @@ -121,29 +133,44 @@ func resourceHerokuAddonCreate(d *schema.ResourceData, meta interface{}) error { } log.Printf("[DEBUG] Addon create configuration: %#v, %#v", app, opts) - a, err := client.AddOnCreate(context.TODO(), app, opts) + addon, err := client.AddOnCreate(context.TODO(), app, opts) if err != nil { return err } // Wait for the Addon to be provisioned - log.Printf("[DEBUG] Waiting for Addon (%s) to be provisioned", a.ID) + log.Printf("[DEBUG] Waiting for Addon (%s) to be provisioned", addon.ID) stateConf := &resource.StateChangeConf{ Pending: []string{"provisioning"}, Target: []string{"provisioned"}, - Refresh: AddOnStateRefreshFunc(client, app, a.ID), + Refresh: AddOnStateRefreshFunc(client, app, addon.ID), Timeout: time.Duration(config.AddonCreateTimeout) * time.Minute, } if _, err := stateConf.WaitForState(); err != nil { - return fmt.Errorf("Error waiting for Addon (%s) to be provisioned: %s", d.Id(), err) + return fmt.Errorf("Error waiting for Addon (%s) to be provisioned: %s", addon.ID, err) } - log.Printf("[INFO] Addon provisioned: %s", d.Id()) + log.Printf("[INFO] Addon provisioned: %s", addon.ID) // This should be only set after the addon provisioning has been fully completed. - d.SetId(a.ID) + d.SetId(addon.ID) log.Printf("[INFO] Addon ID: %s", d.Id()) + err = resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError { + configVarValues, err := retrieveSpecificConfigVars(client, addon.App.Name, addon.ConfigVars) + if err != nil { + return resource.NonRetryableError(err) + } + if len(configVarValues) != len(addon.ConfigVars) { + return resource.RetryableError(fmt.Errorf("Got %d add-on config vars from the app, but expected %d", len(configVarValues), len(addon.ConfigVars))) + } + log.Printf("[INFO] Addon config vars are set: %v", addon.ConfigVars) + return nil + }) + if err != nil { + return err + } + return resourceHerokuAddonRead(d, meta) } @@ -176,6 +203,15 @@ func resourceHerokuAddonRead(d *schema.ResourceData, meta interface{}) error { return err } + configVarValues, err := retrieveSpecificConfigVars(client, addon.App.Name, addon.ConfigVars) + if err != nil { + return err + } + err = d.Set("config_var_values", configVarValues) + if err != nil { + return err + } + return nil } @@ -269,3 +305,19 @@ func AddOnStateRefreshFunc(client *heroku.Service, appID, addOnID string) resour return (*heroku.AddOn)(addon), addon.State, nil } } + +func retrieveSpecificConfigVars(client *heroku.Service, appID string, varNames []string) (map[string]string, error) { + vars, err := client.ConfigVarInfoForApp(context.TODO(), appID) + if err != nil { + return nil, err + } + + nonNullVars := map[string]string{} + for k, v := range vars { + if SliceContainsString(varNames, k) && v != nil { + nonNullVars[k] = *v + } + } + + return nonNullVars, nil +} diff --git a/heroku/resource_heroku_addon_test.go b/heroku/resource_heroku_addon_test.go index 946c463f..4d7556a0 100644 --- a/heroku/resource_heroku_addon_test.go +++ b/heroku/resource_heroku_addon_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "regexp" + "strings" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" @@ -25,7 +26,7 @@ func TestAccHerokuAddon_Basic(t *testing.T) { Config: testAccCheckHerokuAddonConfig_basic(appName), Check: resource.ComposeTestCheckFunc( testAccCheckHerokuAddonExists("heroku_addon.foobar", &addon), - testAccCheckHerokuAddonAttributes(&addon, "deployhooks:http"), + testAccCheckHerokuAddonPlan(&addon, "deployhooks:http"), resource.TestCheckResourceAttr( "heroku_addon.foobar", "config.url", "http://google.com"), resource.TestCheckResourceAttr( @@ -51,7 +52,7 @@ func TestAccHerokuAddon_noPlan(t *testing.T) { Config: testAccCheckHerokuAddonConfig_no_plan(appName), Check: resource.ComposeTestCheckFunc( testAccCheckHerokuAddonExists("heroku_addon.foobar", &addon), - testAccCheckHerokuAddonAttributes(&addon, "memcachier:dev"), + testAccCheckHerokuAddonPlan(&addon, "memcachier:dev"), resource.TestCheckResourceAttr( "heroku_addon.foobar", "app", appName), resource.TestCheckResourceAttr( @@ -62,7 +63,7 @@ func TestAccHerokuAddon_noPlan(t *testing.T) { Config: testAccCheckHerokuAddonConfig_no_plan(appName), Check: resource.ComposeTestCheckFunc( testAccCheckHerokuAddonExists("heroku_addon.foobar", &addon), - testAccCheckHerokuAddonAttributes(&addon, "memcachier:dev"), + testAccCheckHerokuAddonPlan(&addon, "memcachier:dev"), resource.TestCheckResourceAttr( "heroku_addon.foobar", "app", appName), resource.TestCheckResourceAttr( @@ -73,6 +74,27 @@ func TestAccHerokuAddon_noPlan(t *testing.T) { }) } +func TestAccHerokuAddon_ConfigVarValues(t *testing.T) { + var addon heroku.AddOn + appName := fmt.Sprintf("tftest-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckHerokuAddonDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckHerokuAddonConfig_configVarValues(appName), + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAddonExists("heroku_addon.pg", &addon), + testAccCheckHerokuAddonPlan(&addon, "heroku-postgresql:hobby-dev"), + testAccCheckHerokuAddonConfigVarValueHasDatabaseURL("heroku_addon.pg", &addon), + ), + }, + }, + }) +} + func TestAccHerokuAddon_CustomName(t *testing.T) { var addon heroku.AddOn appName := fmt.Sprintf("tftest-%s", acctest.RandString(10)) @@ -87,7 +109,7 @@ func TestAccHerokuAddon_CustomName(t *testing.T) { Config: testAccCheckHerokuAddonConfig_CustomName(appName, customName), Check: resource.ComposeTestCheckFunc( testAccCheckHerokuAddonExists("heroku_addon.foobar", &addon), - testAccCheckHerokuAddonAttributes(&addon, "memcachier:dev"), + testAccCheckHerokuAddonPlan(&addon, "memcachier:dev"), resource.TestCheckResourceAttr( "heroku_addon.foobar", "app", appName), resource.TestCheckResourceAttr( @@ -190,7 +212,7 @@ func testAccCheckHerokuAddonDestroy(s *terraform.State) error { return nil } -func testAccCheckHerokuAddonAttributes(addon *heroku.AddOn, n string) resource.TestCheckFunc { +func testAccCheckHerokuAddonPlan(addon *heroku.AddOn, n string) resource.TestCheckFunc { return func(s *terraform.State) error { if addon.Plan.Name != n { @@ -201,6 +223,30 @@ func testAccCheckHerokuAddonAttributes(addon *heroku.AddOn, n string) resource.T } } +func testAccCheckHerokuAddonConfigVarValueHasDatabaseURL(n string, addon *heroku.AddOn) 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 Addon ID is set") + } + + dbURL := rs.Primary.Attributes["config_var_values.DATABASE_URL"] + if dbURL == "" { + return fmt.Errorf(`Expected "config_var_values" to contain the key "DATABASE_URL"`) + } + if !strings.HasPrefix(dbURL, "postgres://") { + return fmt.Errorf(`Expected "DATABASE_URL" to start with "postgres://", got %s`, dbURL) + } + + return nil + } +} + func testAccCheckHerokuAddonExists(n string, addon *heroku.AddOn) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -256,6 +302,19 @@ resource "heroku_addon" "foobar" { }`, appName) } +func testAccCheckHerokuAddonConfig_configVarValues(appName string) string { + return fmt.Sprintf(` +resource "heroku_app" "foobar" { + name = "%s" + region = "us" +} + +resource "heroku_addon" "pg" { + app = "${heroku_app.foobar.name}" + plan = "heroku-postgresql:hobby-dev" +}`, appName) +} + func testAccCheckHerokuAddonConfig_no_plan(appName string) string { return fmt.Sprintf(` resource "heroku_app" "foobar" { diff --git a/heroku/resource_heroku_cert_test.go b/heroku/resource_heroku_cert_test.go index 7109feee..bee32cbf 100644 --- a/heroku/resource_heroku_cert_test.go +++ b/heroku/resource_heroku_cert_test.go @@ -29,6 +29,8 @@ import ( // on update seems to allow the test to run smoothly; in real life, this test // case is definitely an extreme edge case. func TestAccHerokuCert_EU(t *testing.T) { + t.Skip("SSL Endpoint shutdown: https://devcenter.heroku.com/changelog-items/2280") + var endpoint heroku.SSLEndpoint appName := fmt.Sprintf("tftest-%s", acctest.RandString(10)) @@ -74,6 +76,8 @@ func TestAccHerokuCert_EU(t *testing.T) { } func TestAccHerokuCert_US(t *testing.T) { + t.Skip("SSL Endpoint shutdown: https://devcenter.heroku.com/changelog-items/2280") + var endpoint heroku.SSLEndpoint appName := fmt.Sprintf("tftest-%s", acctest.RandString(10))