From acdb5c659a86773ecb32c24c7108aaacd543de08 Mon Sep 17 00:00:00 2001 From: Bernerd Schaefer Date: Mon, 24 Apr 2017 11:29:28 -0700 Subject: [PATCH] provider/heroku: set app buildpacks from config Many apps deployed to Heroku require that multiple buildpacks be configured in a particular order to operate correctly. This updates the builtin Heroku provider's app resource to support configuring buildpacks and the related documentation in the website. Similar to config vars, externally set buildpacks will not be altered if the config is not set. --- .../providers/heroku/resource_heroku_app.go | 67 +++++++ .../heroku/resource_heroku_app_test.go | 164 ++++++++++++++++++ .../docs/providers/heroku/r/app.html.markdown | 6 + 3 files changed, 237 insertions(+) diff --git a/builtin/providers/heroku/resource_heroku_app.go b/builtin/providers/heroku/resource_heroku_app.go index 20a6c9c0d0..da0a704a52 100644 --- a/builtin/providers/heroku/resource_heroku_app.go +++ b/builtin/providers/heroku/resource_heroku_app.go @@ -30,6 +30,7 @@ type application struct { App *herokuApplication // The heroku application Client *heroku.Service // Client to interact with the heroku API Vars map[string]string // The vars on the application + Buildpacks []string // The application's buildpack names or URLs Organization bool // is the application organization app } @@ -71,6 +72,11 @@ func (a *application) Update() error { } } + a.Buildpacks, err = retrieveBuildpacks(a.Id, a.Client) + if err != nil { + errs = append(errs, err) + } + a.Vars, err = retrieveConfigVars(a.Id, a.Client) if err != nil { errs = append(errs, err) @@ -109,6 +115,14 @@ func resourceHerokuApp() *schema.Resource { ForceNew: true, }, + "buildpacks": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "config_vars": { Type: schema.TypeList, Optional: true, @@ -215,6 +229,10 @@ func resourceHerokuAppCreate(d *schema.ResourceData, meta interface{}) error { } } + if v, ok := d.GetOk("buildpacks"); ok { + err = updateBuildpacks(d.Id(), client, v.([]interface{})) + } + return resourceHerokuAppRead(d, meta) } @@ -293,6 +311,9 @@ func resourceHerokuAppRead(d *schema.ResourceData, meta interface{}) error { } } + // Only track buildpacks when set in the configuration. + _, buildpacksConfigured := d.GetOk("buildpacks") + organizationApp := isOrganizationApp(d) // Only set the config_vars that we have set in the configuration. @@ -317,6 +338,9 @@ func resourceHerokuAppRead(d *schema.ResourceData, meta interface{}) error { d.Set("region", app.App.Region) d.Set("git_url", app.App.GitURL) d.Set("web_url", app.App.WebURL) + if buildpacksConfigured { + d.Set("buildpacks", app.Buildpacks) + } d.Set("config_vars", configVarsValue) d.Set("all_config_vars", app.Vars) if organizationApp { @@ -374,6 +398,13 @@ func resourceHerokuAppUpdate(d *schema.ResourceData, meta interface{}) error { } } + if d.HasChange("buildpacks") { + err := updateBuildpacks(d.Id(), client, d.Get("buildpacks").([]interface{})) + if err != nil { + return err + } + } + return resourceHerokuAppRead(d, meta) } @@ -402,6 +433,21 @@ func resourceHerokuAppRetrieve(id string, organization bool, client *heroku.Serv return &app, nil } +func retrieveBuildpacks(id string, client *heroku.Service) ([]string, error) { + results, err := client.BuildpackInstallationList(context.TODO(), id, nil) + + if err != nil { + return nil, err + } + + buildpacks := []string{} + for _, installation := range results { + buildpacks = append(buildpacks, installation.Buildpack.Name) + } + + return buildpacks, nil +} + func retrieveConfigVars(id string, client *heroku.Service) (map[string]string, error) { vars, err := client.ConfigVarInfoForApp(context.TODO(), id) @@ -450,3 +496,24 @@ func updateConfigVars( return nil } + +func updateBuildpacks(id string, client *heroku.Service, v []interface{}) error { + opts := heroku.BuildpackInstallationUpdateOpts{ + Updates: []struct { + Buildpack string `json:"buildpack" url:"buildpack,key"` + }{}} + + for _, buildpack := range v { + opts.Updates = append(opts.Updates, struct { + Buildpack string `json:"buildpack" url:"buildpack,key"` + }{ + Buildpack: buildpack.(string), + }) + } + + if _, err := client.BuildpackInstallationUpdate(context.TODO(), id, opts); err != nil { + return fmt.Errorf("Error updating buildpacks: %s", err) + } + + return nil +} diff --git a/builtin/providers/heroku/resource_heroku_app_test.go b/builtin/providers/heroku/resource_heroku_app_test.go index caeade8f2c..21d2a379b2 100644 --- a/builtin/providers/heroku/resource_heroku_app_test.go +++ b/builtin/providers/heroku/resource_heroku_app_test.go @@ -109,6 +109,75 @@ func TestAccHerokuApp_NukeVars(t *testing.T) { }) } +func TestAccHerokuApp_Buildpacks(t *testing.T) { + var app heroku.AppInfoResult + appName := fmt.Sprintf("tftest-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckHerokuAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckHerokuAppConfig_go(appName), + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAppExists("heroku_app.foobar", &app), + testAccCheckHerokuAppBuildpacks(appName, false), + resource.TestCheckResourceAttr("heroku_app.foobar", "buildpacks.0", "heroku/go"), + ), + }, + { + Config: testAccCheckHerokuAppConfig_multi(appName), + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAppExists("heroku_app.foobar", &app), + testAccCheckHerokuAppBuildpacks(appName, true), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "buildpacks.0", "https://github.com/heroku/heroku-buildpack-multi-procfile"), + resource.TestCheckResourceAttr("heroku_app.foobar", "buildpacks.1", "heroku/go"), + ), + }, + { + Config: testAccCheckHerokuAppConfig_no_vars(appName), + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAppExists("heroku_app.foobar", &app), + testAccCheckHerokuAppNoBuildpacks(appName), + resource.TestCheckNoResourceAttr("heroku_app.foobar", "buildpacks.0"), + ), + }, + }, + }) +} + +func TestAccHerokuApp_ExternallySetBuildpacks(t *testing.T) { + var app heroku.AppInfoResult + appName := fmt.Sprintf("tftest-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckHerokuAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckHerokuAppConfig_no_vars(appName), + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAppExists("heroku_app.foobar", &app), + testAccCheckHerokuAppNoBuildpacks(appName), + resource.TestCheckNoResourceAttr("heroku_app.foobar", "buildpacks.0"), + ), + }, + { + PreConfig: testAccInstallUnconfiguredBuildpack(t, appName), + Config: testAccCheckHerokuAppConfig_no_vars(appName), + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAppExists("heroku_app.foobar", &app), + testAccCheckHerokuAppBuildpacks(appName, false), + resource.TestCheckNoResourceAttr("heroku_app.foobar", "buildpacks.0"), + ), + }, + }, + }) +} + func TestAccHerokuApp_Organization(t *testing.T) { var app heroku.OrganizationApp appName := fmt.Sprintf("tftest-%s", acctest.RandString(10)) @@ -230,6 +299,59 @@ func testAccCheckHerokuAppAttributesNoVars(app *heroku.AppInfoResult, appName st } } +func testAccCheckHerokuAppBuildpacks(appName string, multi bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*heroku.Service) + + results, err := client.BuildpackInstallationList(context.TODO(), appName, nil) + if err != nil { + return err + } + + buildpacks := []string{} + for _, installation := range results { + buildpacks = append(buildpacks, installation.Buildpack.Name) + } + + if multi { + herokuMulti := "https://github.com/heroku/heroku-buildpack-multi-procfile" + if len(buildpacks) != 2 || buildpacks[0] != herokuMulti || buildpacks[1] != "heroku/go" { + return fmt.Errorf("Bad buildpacks: %v", buildpacks) + } + + return nil + } + + if len(buildpacks) != 1 || buildpacks[0] != "heroku/go" { + return fmt.Errorf("Bad buildpacks: %v", buildpacks) + } + + return nil + } +} + +func testAccCheckHerokuAppNoBuildpacks(appName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*heroku.Service) + + results, err := client.BuildpackInstallationList(context.TODO(), appName, nil) + if err != nil { + return err + } + + buildpacks := []string{} + for _, installation := range results { + buildpacks = append(buildpacks, installation.Buildpack.Name) + } + + if len(buildpacks) != 0 { + return fmt.Errorf("Bad buildpacks: %v", buildpacks) + } + + return nil + } +} + func testAccCheckHerokuAppAttributesOrg(app *heroku.OrganizationApp, appName string, org string) resource.TestCheckFunc { return func(s *terraform.State) error { client := testAccProvider.Meta().(*heroku.Service) @@ -323,6 +445,25 @@ func testAccCheckHerokuAppExistsOrg(n string, app *heroku.OrganizationApp) resou } } +func testAccInstallUnconfiguredBuildpack(t *testing.T, appName string) func() { + return func() { + client := testAccProvider.Meta().(*heroku.Service) + + opts := heroku.BuildpackInstallationUpdateOpts{ + Updates: []struct { + Buildpack string `json:"buildpack" url:"buildpack,key"` + }{ + {Buildpack: "heroku/go"}, + }, + } + + _, err := client.BuildpackInstallationUpdate(context.TODO(), appName, opts) + if err != nil { + t.Fatalf("Error updating buildpacks: %s", err) + } + } +} + func testAccCheckHerokuAppConfig_basic(appName string) string { return fmt.Sprintf(` resource "heroku_app" "foobar" { @@ -335,6 +476,29 @@ resource "heroku_app" "foobar" { }`, appName) } +func testAccCheckHerokuAppConfig_go(appName string) string { + return fmt.Sprintf(` +resource "heroku_app" "foobar" { + name = "%s" + region = "us" + + buildpacks = ["heroku/go"] +}`, appName) +} + +func testAccCheckHerokuAppConfig_multi(appName string) string { + return fmt.Sprintf(` +resource "heroku_app" "foobar" { + name = "%s" + region = "us" + + buildpacks = [ + "https://github.com/heroku/heroku-buildpack-multi-procfile", + "heroku/go" + ] +}`, appName) +} + func testAccCheckHerokuAppConfig_updated(appName string) string { return fmt.Sprintf(` resource "heroku_app" "foobar" { diff --git a/website/source/docs/providers/heroku/r/app.html.markdown b/website/source/docs/providers/heroku/r/app.html.markdown index 410f01ead1..9e6b429638 100644 --- a/website/source/docs/providers/heroku/r/app.html.markdown +++ b/website/source/docs/providers/heroku/r/app.html.markdown @@ -22,6 +22,10 @@ resource "heroku_app" "default" { config_vars { FOOBAR = "baz" } + + buildpacks = [ + "heroku/go" + ] } ``` @@ -34,6 +38,8 @@ The following arguments are supported: * `region` - (Required) The region that the app should be deployed in. * `stack` - (Optional) The application stack is what platform to run the application in. +* `buildpacks` - (Optional) Buildpack names or URLs for the application. + Buildpacks configured externally won't be altered if this is not present. * `config_vars` - (Optional) Configuration variables for the application. The config variables in this map are not the final set of configuration variables, but rather variables you want present. That is, other