diff --git a/builtin/providers/heroku/resource_heroku_app.go b/builtin/providers/heroku/resource_heroku_app.go index 20a6c9c0d..da0a704a5 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 caeade8f2..21d2a379b 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 410f01ead..9e6b42963 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