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.
This commit is contained in:
Bernerd Schaefer 2017-04-24 11:29:28 -07:00
parent fe15c68aa9
commit acdb5c659a
3 changed files with 237 additions and 0 deletions

View File

@ -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
}

View File

@ -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" {

View File

@ -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