providers/heroku/app.go (331 lines of code) (raw):
// Copyright 2019 The Terraformer Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package heroku
import (
"context"
"fmt"
"log"
"strings"
"github.com/GoogleCloudPlatform/terraformer/terraformutils"
heroku "github.com/heroku/heroku-go/v5"
)
type AppGenerator struct {
HerokuService
}
func (g AppGenerator) createResources(appList []heroku.App) ([]terraformutils.Resource, error) {
var resources []terraformutils.Resource
var resourcesEmpty []terraformutils.Resource
for _, app := range appList {
configVars, err := g.getSettableConfigVars(app.ID)
if err != nil {
return resourcesEmpty, fmt.Errorf("Error in getSettableConfigVars for '%s': %w", app.ID, err)
}
resources = append(resources, terraformutils.NewResource(
app.ID,
app.Name,
"heroku_app",
"heroku",
map[string]string{},
[]string{},
map[string]interface{}{
"config_vars": configVars,
}))
}
return resources, nil
}
func (g *AppGenerator) InitResources() error {
svc := g.generateService()
ctx := context.Background()
team := g.GetArgs()["team"].(string)
var output []heroku.App
var hasRequiredFilter bool
if len(g.Filter) > 0 {
for _, filter := range g.Filter {
if filter.IsApplicable("app") {
hasRequiredFilter = true
for _, appID := range filter.AcceptableValues {
app, err := svc.AppInfo(ctx, appID)
if err != nil {
return fmt.Errorf("Error filtering apps by app '%s': %w", appID, err)
}
output = append(output, *app)
}
}
}
}
if team != "" {
hasRequiredFilter = true
teamApps, err := svc.TeamAppListByTeam(ctx, team, &heroku.ListRange{Field: "id", Max: 1000})
if err != nil {
return fmt.Errorf("Error querying apps by team '%s': %w", team, err)
}
for _, app := range teamApps {
output = append(output, heroku.App{ID: app.ID, Name: app.Name})
}
}
if !hasRequiredFilter {
return fmt.Errorf("Heroku Apps must be scoped by team or filtered by app: --team=<name> or --filter=app=<ID>")
}
resources, err := g.createResources(output)
if err != nil {
return fmt.Errorf("Error creating app resources: %w", err)
}
g.Resources = resources
for _, app := range output {
appFeatures, err := g.createAppFeatureResources(ctx, svc, app)
if err != nil {
return fmt.Errorf("Error creating app feature resources: %w", err)
}
g.Resources = append(g.Resources, appFeatures...)
addons, err := g.createAddonResources(ctx, svc, app)
if err != nil {
return fmt.Errorf("Error creating app addon resources: %w", err)
}
g.Resources = append(g.Resources, addons...)
addonAttachments, err := g.createAddonAttachmentResources(ctx, svc, app)
if err != nil {
return fmt.Errorf("Error creating app addon attachment resources: %w", err)
}
g.Resources = append(g.Resources, addonAttachments...)
appWebooks, err := g.createAppWebhookResources(ctx, svc, app)
if err != nil {
return fmt.Errorf("Error creating app webhook resources: %w", err)
}
g.Resources = append(g.Resources, appWebooks...)
ssls, err := g.createSslResources(ctx, svc, app)
if err != nil {
return fmt.Errorf("Error creating SSL resources: %w", err)
}
g.Resources = append(g.Resources, ssls...)
domains, err := g.createDomainResources(ctx, svc, app)
if err != nil {
return fmt.Errorf("Error creating domain resources: %w", err)
}
g.Resources = append(g.Resources, domains...)
drains := g.createDrainResources(ctx, svc, app)
g.Resources = append(g.Resources, drains...)
formations, err := g.createFormationResources(ctx, svc, app)
if err != nil {
return fmt.Errorf("Error creating formation resources: %w", err)
}
g.Resources = append(g.Resources, formations...)
}
return nil
}
func (g AppGenerator) getSettableConfigVars(appID string) (map[string]string, error) {
svc := g.generateService()
ctx := context.Background()
output := map[string]string{}
emptyOutput := map[string]string{}
vars, err := svc.ConfigVarInfoForApp(ctx, appID)
if err != nil {
return emptyOutput, fmt.Errorf("Error querying ConfigVarInfoForApp '%s': %w", appID, err)
}
for k, v := range vars {
if v != nil {
output[k] = *v
}
}
appAddons, err := svc.AddOnListByApp(ctx, appID, &heroku.ListRange{Field: "id", Max: 1000})
if err != nil {
return emptyOutput, fmt.Errorf("Error querying AddOnListByApp '%s': %w", appID, err)
}
for _, addOn := range appAddons {
for _, addOnConfigVar := range addOn.ConfigVars {
delete(output, addOnConfigVar)
}
}
return output, nil
}
func (g AppGenerator) createAppFeatureResources(ctx context.Context, svc *heroku.Service, app heroku.App) ([]terraformutils.Resource, error) {
list := []heroku.AppFeature{}
appFeatures, err := svc.AppFeatureList(ctx, app.ID, &heroku.ListRange{Field: "id", Max: 1000})
if err != nil {
return []terraformutils.Resource{}, fmt.Errorf("Error listing for features for app '%s': %w", app.ID, err)
}
for _, appFeature := range appFeatures {
if appFeature.Enabled {
list = append(list, appFeature)
}
}
var resources []terraformutils.Resource
for _, appFeature := range list {
resources = append(resources, terraformutils.NewResource(
fmt.Sprintf("%s:%s", app.ID, appFeature.Name),
fmt.Sprintf("%s-%s", app.Name, appFeature.Name),
"heroku_app_feature",
"heroku",
map[string]string{"app_id": app.ID},
[]string{},
map[string]interface{}{
"app_id": fmt.Sprintf("${heroku_app.tfer--%s.id}", app.Name),
}))
}
return resources, nil
}
func (g AppGenerator) createAddonResources(ctx context.Context, svc *heroku.Service, app heroku.App) ([]terraformutils.Resource, error) {
list := []heroku.AddOn{}
appAddons, err := svc.AddOnListByApp(ctx, app.ID, &heroku.ListRange{Field: "id", Max: 1000})
if err != nil {
return []terraformutils.Resource{}, fmt.Errorf("Error listing addons by app '%s': %w", app.ID, err)
}
for _, addOn := range appAddons {
list = append(list, addOn)
}
var resources []terraformutils.Resource
for _, addOn := range list {
resources = append(resources, terraformutils.NewResource(
addOn.ID,
addOn.Name,
"heroku_addon",
"heroku",
map[string]string{"app_id": app.ID},
[]string{},
map[string]interface{}{
"app_id": fmt.Sprintf("${heroku_app.tfer--%s.id}", app.Name),
}))
}
return resources, nil
}
func (g AppGenerator) createAddonAttachmentResources(ctx context.Context, svc *heroku.Service, app heroku.App) ([]terraformutils.Resource, error) {
list := []heroku.AddOnAttachment{}
appAddons, err := svc.AddOnListByApp(ctx, app.ID, &heroku.ListRange{Field: "id", Max: 1000})
if err != nil {
return []terraformutils.Resource{}, fmt.Errorf("Error listing addons by app '%s': %w", app.ID, err)
}
for _, addOn := range appAddons {
addonAttachments, err := svc.AddOnAttachmentListByAddOn(ctx, addOn.ID, &heroku.ListRange{Field: "id", Max: 1000})
if err != nil {
return []terraformutils.Resource{}, fmt.Errorf("Error listing addon attachments by addon '%s': %w", addOn.Name, err)
}
for _, attachment := range addonAttachments {
list = append(list, attachment)
}
}
var resources []terraformutils.Resource
for _, addOnAttachment := range list {
resources = append(resources, terraformutils.NewResource(
addOnAttachment.ID,
fmt.Sprintf("%s-%s", addOnAttachment.App.Name, addOnAttachment.Name),
"heroku_addon_attachment",
"heroku",
map[string]string{
"app_id": addOnAttachment.App.ID,
"addon_id": addOnAttachment.Addon.ID,
},
[]string{},
map[string]interface{}{
"app_id": fmt.Sprintf("${heroku_app.tfer--%s.id}", addOnAttachment.App.Name),
"addon_id": fmt.Sprintf("${heroku_addon.tfer--%s.id}", addOnAttachment.Addon.Name),
}))
}
return resources, nil
}
func (g AppGenerator) createAppWebhookResources(ctx context.Context, svc *heroku.Service, app heroku.App) ([]terraformutils.Resource, error) {
appWebhooks, err := svc.AppWebhookList(ctx, app.ID, &heroku.ListRange{Field: "id", Max: 1000})
if err != nil {
return []terraformutils.Resource{}, fmt.Errorf("Error listing webhooks for app '%s': %w", app.ID, err)
}
var resources []terraformutils.Resource
for _, appWebhook := range appWebhooks {
resources = append(resources, terraformutils.NewResource(
appWebhook.ID,
appWebhook.ID,
"heroku_app_webhook",
"heroku",
map[string]string{"app_id": app.ID},
[]string{},
map[string]interface{}{
"app_id": fmt.Sprintf("${heroku_app.tfer--%s.id}", app.Name),
}))
}
return resources, nil
}
func (g AppGenerator) createSslResources(ctx context.Context, svc *heroku.Service, app heroku.App) ([]terraformutils.Resource, error) {
// When app is using automated certificate management, do not import this resource.
if app.Acm {
return []terraformutils.Resource{}, nil
}
sniEnpoints, err := svc.SniEndpointList(ctx, app.ID, &heroku.ListRange{Field: "id", Max: 1000})
if err != nil {
return []terraformutils.Resource{}, fmt.Errorf("Error listing SNI endpoints (SSL) for app '%s': %w", app.ID, err)
}
var resources []terraformutils.Resource
for _, sniEndpoint := range sniEnpoints {
// Empty domains indicates inactive endpoint, such as expired/past ACM cert
if len(sniEndpoint.Domains) < 1 {
continue
}
resources = append(resources, terraformutils.NewResource(
sniEndpoint.ID,
sniEndpoint.Name,
"heroku_ssl",
"heroku",
map[string]string{"app_id": app.ID},
[]string{},
map[string]interface{}{
"app_id": fmt.Sprintf("${heroku_app.tfer--%s.id}", app.Name),
}))
}
return resources, nil
}
func (g AppGenerator) createDomainResources(ctx context.Context, svc *heroku.Service, app heroku.App) ([]terraformutils.Resource, error) {
domains, err := svc.DomainList(ctx, app.ID, &heroku.ListRange{Field: "id", Max: 1000})
if err != nil {
return []terraformutils.Resource{}, fmt.Errorf("Error listing domains for app '%s': %w", app.ID, err)
}
var resources []terraformutils.Resource
for _, domain := range domains {
if strings.HasSuffix(domain.Hostname, "herokuapp.com") {
continue
}
resources = append(resources, terraformutils.NewResource(
domain.ID,
strings.ReplaceAll(domain.Hostname, ".", "-"),
"heroku_domain",
"heroku",
map[string]string{"app_id": app.ID},
[]string{},
map[string]interface{}{
"app_id": fmt.Sprintf("${heroku_app.tfer--%s.id}", app.Name),
}))
}
return resources, nil
}
func (g AppGenerator) createDrainResources(ctx context.Context, svc *heroku.Service, app heroku.App) []terraformutils.Resource {
drains, err := svc.LogDrainList(ctx, app.ID, &heroku.ListRange{Field: "id", Max: 1000})
if err != nil {
log.Printf("skipping App Drains due to error: '%s': %s", app.ID, err)
return []terraformutils.Resource{}
}
var resources []terraformutils.Resource
for _, drain := range drains {
resources = append(resources, terraformutils.NewResource(
fmt.Sprintf("%s:%s", app.ID, drain.ID),
fmt.Sprintf("%s-%s", app.Name, drain.ID),
"heroku_drain",
"heroku",
map[string]string{"app_id": app.ID},
[]string{},
map[string]interface{}{
"app_id": fmt.Sprintf("${heroku_app.tfer--%s.id}", app.Name),
}))
}
return resources
}
func (g AppGenerator) createFormationResources(ctx context.Context, svc *heroku.Service, app heroku.App) ([]terraformutils.Resource, error) {
formations, err := svc.FormationList(ctx, app.ID, &heroku.ListRange{Field: "id", Max: 1000})
if err != nil {
return []terraformutils.Resource{}, fmt.Errorf("Error listing formations for app '%s': %w", app.ID, err)
}
var resources []terraformutils.Resource
for _, formation := range formations {
resources = append(resources, terraformutils.NewResource(
formation.ID,
fmt.Sprintf("%s-%s", app.Name, formation.Type),
"heroku_formation",
"heroku",
map[string]string{"app_id": app.ID},
[]string{},
map[string]interface{}{
"app_id": fmt.Sprintf("${heroku_app.tfer--%s.id}", app.Name),
}))
}
return resources, nil
}