Skip to content

Commit

Permalink
heroku_addon "config_var_values" (#325)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mars authored Dec 17, 2021
1 parent 4dd933d commit 3f37ce3
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 12 deletions.
1 change: 1 addition & 0 deletions docs/resources/addon.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions heroku/import_heroku_cert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
66 changes: 59 additions & 7 deletions heroku/resource_heroku_addon.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ package heroku
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"log"
"net/url"
"regexp"
"strings"
"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"
Expand Down Expand Up @@ -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,
},
},
}
}
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
69 changes: 64 additions & 5 deletions heroku/resource_heroku_addon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"regexp"
"strings"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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))
Expand All @@ -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(
Expand Down Expand Up @@ -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 {
Expand All @@ -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]
Expand Down Expand Up @@ -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" {
Expand Down
4 changes: 4 additions & 0 deletions heroku/resource_heroku_cert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down Expand Up @@ -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))

Expand Down

0 comments on commit 3f37ce3

Please sign in to comment.