github.com/andrewrynhard/terraform@v0.9.5-0.20170502003928-8d286b83eae4/builtin/providers/heroku/resource_heroku_app.go (about)

     1  package heroku
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  
     8  	"github.com/cyberdelia/heroku-go/v3"
     9  	multierror "github.com/hashicorp/go-multierror"
    10  	"github.com/hashicorp/terraform/helper/schema"
    11  )
    12  
    13  // herokuApplication is a value type used to hold the details of an
    14  // application. We use this for common storage of values needed for the
    15  // heroku.App and heroku.OrganizationApp types
    16  type herokuApplication struct {
    17  	Name             string
    18  	Region           string
    19  	Space            string
    20  	Stack            string
    21  	GitURL           string
    22  	WebURL           string
    23  	OrganizationName string
    24  	Locked           bool
    25  }
    26  
    27  // type application is used to store all the details of a heroku app
    28  type application struct {
    29  	Id string // Id of the resource
    30  
    31  	App          *herokuApplication // The heroku application
    32  	Client       *heroku.Service    // Client to interact with the heroku API
    33  	Vars         map[string]string  // The vars on the application
    34  	Buildpacks   []string           // The application's buildpack names or URLs
    35  	Organization bool               // is the application organization app
    36  }
    37  
    38  // Updates the application to have the latest from remote
    39  func (a *application) Update() error {
    40  	var errs []error
    41  	var err error
    42  
    43  	if !a.Organization {
    44  		app, err := a.Client.AppInfo(context.TODO(), a.Id)
    45  		if err != nil {
    46  			errs = append(errs, err)
    47  		} else {
    48  			a.App = &herokuApplication{}
    49  			a.App.Name = app.Name
    50  			a.App.Region = app.Region.Name
    51  			a.App.Stack = app.Stack.Name
    52  			a.App.GitURL = app.GitURL
    53  			a.App.WebURL = app.WebURL
    54  		}
    55  	} else {
    56  		app, err := a.Client.OrganizationAppInfo(context.TODO(), a.Id)
    57  		if err != nil {
    58  			errs = append(errs, err)
    59  		} else {
    60  			// No inheritance between OrganizationApp and App is killing it :/
    61  			a.App = &herokuApplication{}
    62  			a.App.Name = app.Name
    63  			a.App.Region = app.Region.Name
    64  			a.App.Stack = app.Stack.Name
    65  			a.App.GitURL = app.GitURL
    66  			a.App.WebURL = app.WebURL
    67  			if app.Space != nil {
    68  				a.App.Space = app.Space.Name
    69  			}
    70  			if app.Organization != nil {
    71  				a.App.OrganizationName = app.Organization.Name
    72  			} else {
    73  				log.Println("[DEBUG] Something is wrong - didn't get information about organization name, while the app is marked as being so")
    74  			}
    75  			a.App.Locked = app.Locked
    76  		}
    77  	}
    78  
    79  	a.Buildpacks, err = retrieveBuildpacks(a.Id, a.Client)
    80  	if err != nil {
    81  		errs = append(errs, err)
    82  	}
    83  
    84  	a.Vars, err = retrieveConfigVars(a.Id, a.Client)
    85  	if err != nil {
    86  		errs = append(errs, err)
    87  	}
    88  
    89  	if len(errs) > 0 {
    90  		return &multierror.Error{Errors: errs}
    91  	}
    92  
    93  	return nil
    94  }
    95  
    96  func resourceHerokuApp() *schema.Resource {
    97  	return &schema.Resource{
    98  		Create: switchHerokuAppCreate,
    99  		Read:   resourceHerokuAppRead,
   100  		Update: resourceHerokuAppUpdate,
   101  		Delete: resourceHerokuAppDelete,
   102  
   103  		Schema: map[string]*schema.Schema{
   104  			"name": {
   105  				Type:     schema.TypeString,
   106  				Required: true,
   107  			},
   108  
   109  			"space": {
   110  				Type:     schema.TypeString,
   111  				Optional: true,
   112  				ForceNew: true,
   113  			},
   114  
   115  			"region": {
   116  				Type:     schema.TypeString,
   117  				Required: true,
   118  				ForceNew: true,
   119  			},
   120  
   121  			"stack": {
   122  				Type:     schema.TypeString,
   123  				Optional: true,
   124  				Computed: true,
   125  				ForceNew: true,
   126  			},
   127  
   128  			"buildpacks": {
   129  				Type:     schema.TypeList,
   130  				Optional: true,
   131  				Elem: &schema.Schema{
   132  					Type: schema.TypeString,
   133  				},
   134  			},
   135  
   136  			"config_vars": {
   137  				Type:     schema.TypeList,
   138  				Optional: true,
   139  				Elem: &schema.Schema{
   140  					Type: schema.TypeMap,
   141  				},
   142  			},
   143  
   144  			"all_config_vars": {
   145  				Type:     schema.TypeMap,
   146  				Computed: true,
   147  			},
   148  
   149  			"git_url": {
   150  				Type:     schema.TypeString,
   151  				Computed: true,
   152  			},
   153  
   154  			"web_url": {
   155  				Type:     schema.TypeString,
   156  				Computed: true,
   157  			},
   158  
   159  			"heroku_hostname": {
   160  				Type:     schema.TypeString,
   161  				Computed: true,
   162  			},
   163  
   164  			"organization": {
   165  				Type:     schema.TypeList,
   166  				Optional: true,
   167  				ForceNew: true,
   168  				Elem: &schema.Resource{
   169  					Schema: map[string]*schema.Schema{
   170  						"name": {
   171  							Type:     schema.TypeString,
   172  							Required: true,
   173  						},
   174  
   175  						"locked": {
   176  							Type:     schema.TypeBool,
   177  							Optional: true,
   178  						},
   179  
   180  						"personal": {
   181  							Type:     schema.TypeBool,
   182  							Optional: true,
   183  						},
   184  					},
   185  				},
   186  			},
   187  		},
   188  	}
   189  }
   190  
   191  func isOrganizationApp(d *schema.ResourceData) bool {
   192  	v := d.Get("organization").([]interface{})
   193  	return len(v) > 0 && v[0] != nil
   194  }
   195  
   196  func switchHerokuAppCreate(d *schema.ResourceData, meta interface{}) error {
   197  	if isOrganizationApp(d) {
   198  		return resourceHerokuOrgAppCreate(d, meta)
   199  	}
   200  
   201  	return resourceHerokuAppCreate(d, meta)
   202  }
   203  
   204  func resourceHerokuAppCreate(d *schema.ResourceData, meta interface{}) error {
   205  	client := meta.(*heroku.Service)
   206  
   207  	// Build up our creation options
   208  	opts := heroku.AppCreateOpts{}
   209  
   210  	if v, ok := d.GetOk("name"); ok {
   211  		vs := v.(string)
   212  		log.Printf("[DEBUG] App name: %s", vs)
   213  		opts.Name = &vs
   214  	}
   215  	if v, ok := d.GetOk("region"); ok {
   216  		vs := v.(string)
   217  		log.Printf("[DEBUG] App region: %s", vs)
   218  		opts.Region = &vs
   219  	}
   220  	if v, ok := d.GetOk("stack"); ok {
   221  		vs := v.(string)
   222  		log.Printf("[DEBUG] App stack: %s", vs)
   223  		opts.Stack = &vs
   224  	}
   225  
   226  	log.Printf("[DEBUG] Creating Heroku app...")
   227  	a, err := client.AppCreate(context.TODO(), opts)
   228  	if err != nil {
   229  		return err
   230  	}
   231  
   232  	d.SetId(a.Name)
   233  	log.Printf("[INFO] App ID: %s", d.Id())
   234  
   235  	if err := performAppPostCreateTasks(d, client); err != nil {
   236  		return err
   237  	}
   238  
   239  	return resourceHerokuAppRead(d, meta)
   240  }
   241  
   242  func resourceHerokuOrgAppCreate(d *schema.ResourceData, meta interface{}) error {
   243  	client := meta.(*heroku.Service)
   244  	// Build up our creation options
   245  	opts := heroku.OrganizationAppCreateOpts{}
   246  
   247  	v := d.Get("organization").([]interface{})
   248  	if len(v) > 1 {
   249  		return fmt.Errorf("Error Creating Heroku App: Only 1 Heroku Organization is permitted")
   250  	}
   251  	orgDetails := v[0].(map[string]interface{})
   252  
   253  	if v := orgDetails["name"]; v != nil {
   254  		vs := v.(string)
   255  		log.Printf("[DEBUG] Organization name: %s", vs)
   256  		opts.Organization = &vs
   257  	}
   258  
   259  	if v := orgDetails["personal"]; v != nil {
   260  		vs := v.(bool)
   261  		log.Printf("[DEBUG] Organization Personal: %t", vs)
   262  		opts.Personal = &vs
   263  	}
   264  
   265  	if v := orgDetails["locked"]; v != nil {
   266  		vs := v.(bool)
   267  		log.Printf("[DEBUG] Organization locked: %t", vs)
   268  		opts.Locked = &vs
   269  	}
   270  
   271  	if v := d.Get("name"); v != nil {
   272  		vs := v.(string)
   273  		log.Printf("[DEBUG] App name: %s", vs)
   274  		opts.Name = &vs
   275  	}
   276  	if v, ok := d.GetOk("region"); ok {
   277  		vs := v.(string)
   278  		log.Printf("[DEBUG] App region: %s", vs)
   279  		opts.Region = &vs
   280  	}
   281  	if v, ok := d.GetOk("space"); ok {
   282  		vs := v.(string)
   283  		log.Printf("[DEBUG] App space: %s", vs)
   284  		opts.Space = &vs
   285  	}
   286  	if v, ok := d.GetOk("stack"); ok {
   287  		vs := v.(string)
   288  		log.Printf("[DEBUG] App stack: %s", vs)
   289  		opts.Stack = &vs
   290  	}
   291  
   292  	log.Printf("[DEBUG] Creating Heroku app...")
   293  	a, err := client.OrganizationAppCreate(context.TODO(), opts)
   294  	if err != nil {
   295  		return err
   296  	}
   297  
   298  	d.SetId(a.Name)
   299  	log.Printf("[INFO] App ID: %s", d.Id())
   300  
   301  	if err := performAppPostCreateTasks(d, client); err != nil {
   302  		return err
   303  	}
   304  
   305  	return resourceHerokuAppRead(d, meta)
   306  }
   307  
   308  func resourceHerokuAppRead(d *schema.ResourceData, meta interface{}) error {
   309  	client := meta.(*heroku.Service)
   310  
   311  	configVars := make(map[string]string)
   312  	care := make(map[string]struct{})
   313  	for _, v := range d.Get("config_vars").([]interface{}) {
   314  		for k := range v.(map[string]interface{}) {
   315  			care[k] = struct{}{}
   316  		}
   317  	}
   318  
   319  	// Only track buildpacks when set in the configuration.
   320  	_, buildpacksConfigured := d.GetOk("buildpacks")
   321  
   322  	organizationApp := isOrganizationApp(d)
   323  
   324  	// Only set the config_vars that we have set in the configuration.
   325  	// The "all_config_vars" field has all of them.
   326  	app, err := resourceHerokuAppRetrieve(d.Id(), organizationApp, client)
   327  	if err != nil {
   328  		return err
   329  	}
   330  
   331  	for k, v := range app.Vars {
   332  		if _, ok := care[k]; ok {
   333  			configVars[k] = v
   334  		}
   335  	}
   336  	var configVarsValue []map[string]string
   337  	if len(configVars) > 0 {
   338  		configVarsValue = []map[string]string{configVars}
   339  	}
   340  
   341  	d.Set("name", app.App.Name)
   342  	d.Set("stack", app.App.Stack)
   343  	d.Set("region", app.App.Region)
   344  	d.Set("git_url", app.App.GitURL)
   345  	d.Set("web_url", app.App.WebURL)
   346  	if buildpacksConfigured {
   347  		d.Set("buildpacks", app.Buildpacks)
   348  	}
   349  	d.Set("config_vars", configVarsValue)
   350  	d.Set("all_config_vars", app.Vars)
   351  	if organizationApp {
   352  		d.Set("space", app.App.Space)
   353  
   354  		orgDetails := map[string]interface{}{
   355  			"name":     app.App.OrganizationName,
   356  			"locked":   app.App.Locked,
   357  			"personal": false,
   358  		}
   359  		err := d.Set("organization", []interface{}{orgDetails})
   360  		if err != nil {
   361  			return err
   362  		}
   363  	}
   364  
   365  	// We know that the hostname on heroku will be the name+herokuapp.com
   366  	// You need this to do things like create DNS CNAME records
   367  	d.Set("heroku_hostname", fmt.Sprintf("%s.herokuapp.com", app.App.Name))
   368  
   369  	return nil
   370  }
   371  
   372  func resourceHerokuAppUpdate(d *schema.ResourceData, meta interface{}) error {
   373  	client := meta.(*heroku.Service)
   374  
   375  	// If name changed, update it
   376  	if d.HasChange("name") {
   377  		v := d.Get("name").(string)
   378  		opts := heroku.AppUpdateOpts{
   379  			Name: &v,
   380  		}
   381  
   382  		renamedApp, err := client.AppUpdate(context.TODO(), d.Id(), opts)
   383  		if err != nil {
   384  			return err
   385  		}
   386  
   387  		// Store the new ID
   388  		d.SetId(renamedApp.Name)
   389  	}
   390  
   391  	// If the config vars changed, then recalculate those
   392  	if d.HasChange("config_vars") {
   393  		o, n := d.GetChange("config_vars")
   394  		if o == nil {
   395  			o = []interface{}{}
   396  		}
   397  		if n == nil {
   398  			n = []interface{}{}
   399  		}
   400  
   401  		err := updateConfigVars(
   402  			d.Id(), client, o.([]interface{}), n.([]interface{}))
   403  		if err != nil {
   404  			return err
   405  		}
   406  	}
   407  
   408  	if d.HasChange("buildpacks") {
   409  		err := updateBuildpacks(d.Id(), client, d.Get("buildpacks").([]interface{}))
   410  		if err != nil {
   411  			return err
   412  		}
   413  	}
   414  
   415  	return resourceHerokuAppRead(d, meta)
   416  }
   417  
   418  func resourceHerokuAppDelete(d *schema.ResourceData, meta interface{}) error {
   419  	client := meta.(*heroku.Service)
   420  
   421  	log.Printf("[INFO] Deleting App: %s", d.Id())
   422  	_, err := client.AppDelete(context.TODO(), d.Id())
   423  	if err != nil {
   424  		return fmt.Errorf("Error deleting App: %s", err)
   425  	}
   426  
   427  	d.SetId("")
   428  	return nil
   429  }
   430  
   431  func resourceHerokuAppRetrieve(id string, organization bool, client *heroku.Service) (*application, error) {
   432  	app := application{Id: id, Client: client, Organization: organization}
   433  
   434  	err := app.Update()
   435  
   436  	if err != nil {
   437  		return nil, fmt.Errorf("Error retrieving app: %s", err)
   438  	}
   439  
   440  	return &app, nil
   441  }
   442  
   443  func retrieveBuildpacks(id string, client *heroku.Service) ([]string, error) {
   444  	results, err := client.BuildpackInstallationList(context.TODO(), id, nil)
   445  
   446  	if err != nil {
   447  		return nil, err
   448  	}
   449  
   450  	buildpacks := []string{}
   451  	for _, installation := range results {
   452  		buildpacks = append(buildpacks, installation.Buildpack.Name)
   453  	}
   454  
   455  	return buildpacks, nil
   456  }
   457  
   458  func retrieveConfigVars(id string, client *heroku.Service) (map[string]string, error) {
   459  	vars, err := client.ConfigVarInfoForApp(context.TODO(), id)
   460  
   461  	if err != nil {
   462  		return nil, err
   463  	}
   464  
   465  	nonNullVars := map[string]string{}
   466  	for k, v := range vars {
   467  		if v != nil {
   468  			nonNullVars[k] = *v
   469  		}
   470  	}
   471  
   472  	return nonNullVars, nil
   473  }
   474  
   475  // Updates the config vars for from an expanded configuration.
   476  func updateConfigVars(
   477  	id string,
   478  	client *heroku.Service,
   479  	o []interface{},
   480  	n []interface{}) error {
   481  	vars := make(map[string]*string)
   482  
   483  	for _, v := range o {
   484  		if v != nil {
   485  			for k := range v.(map[string]interface{}) {
   486  				vars[k] = nil
   487  			}
   488  		}
   489  	}
   490  	for _, v := range n {
   491  		if v != nil {
   492  			for k, v := range v.(map[string]interface{}) {
   493  				val := v.(string)
   494  				vars[k] = &val
   495  			}
   496  		}
   497  	}
   498  
   499  	log.Printf("[INFO] Updating config vars: *%#v", vars)
   500  	if _, err := client.ConfigVarUpdate(context.TODO(), id, vars); err != nil {
   501  		return fmt.Errorf("Error updating config vars: %s", err)
   502  	}
   503  
   504  	return nil
   505  }
   506  
   507  func updateBuildpacks(id string, client *heroku.Service, v []interface{}) error {
   508  	opts := heroku.BuildpackInstallationUpdateOpts{
   509  		Updates: []struct {
   510  			Buildpack string `json:"buildpack" url:"buildpack,key"`
   511  		}{}}
   512  
   513  	for _, buildpack := range v {
   514  		opts.Updates = append(opts.Updates, struct {
   515  			Buildpack string `json:"buildpack" url:"buildpack,key"`
   516  		}{
   517  			Buildpack: buildpack.(string),
   518  		})
   519  	}
   520  
   521  	if _, err := client.BuildpackInstallationUpdate(context.TODO(), id, opts); err != nil {
   522  		return fmt.Errorf("Error updating buildpacks: %s", err)
   523  	}
   524  
   525  	return nil
   526  }
   527  
   528  // performAppPostCreateTasks performs post-create tasks common to both org and non-org apps.
   529  func performAppPostCreateTasks(d *schema.ResourceData, client *heroku.Service) error {
   530  	if v, ok := d.GetOk("config_vars"); ok {
   531  		if err := updateConfigVars(d.Id(), client, nil, v.([]interface{})); err != nil {
   532  			return err
   533  		}
   534  	}
   535  
   536  	if v, ok := d.GetOk("buildpacks"); ok {
   537  		if err := updateBuildpacks(d.Id(), client, v.([]interface{})); err != nil {
   538  			return err
   539  		}
   540  	}
   541  
   542  	return nil
   543  }