github.com/danp/terraform@v0.9.5-0.20170426144147-39d740081351/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 v, ok := d.GetOk("config_vars"); ok {
   236  		err = updateConfigVars(d.Id(), client, nil, v.([]interface{}))
   237  		if err != nil {
   238  			return err
   239  		}
   240  	}
   241  
   242  	if v, ok := d.GetOk("buildpacks"); ok {
   243  		err = updateBuildpacks(d.Id(), client, v.([]interface{}))
   244  	}
   245  
   246  	return resourceHerokuAppRead(d, meta)
   247  }
   248  
   249  func resourceHerokuOrgAppCreate(d *schema.ResourceData, meta interface{}) error {
   250  	client := meta.(*heroku.Service)
   251  	// Build up our creation options
   252  	opts := heroku.OrganizationAppCreateOpts{}
   253  
   254  	v := d.Get("organization").([]interface{})
   255  	if len(v) > 1 {
   256  		return fmt.Errorf("Error Creating Heroku App: Only 1 Heroku Organization is permitted")
   257  	}
   258  	orgDetails := v[0].(map[string]interface{})
   259  
   260  	if v := orgDetails["name"]; v != nil {
   261  		vs := v.(string)
   262  		log.Printf("[DEBUG] Organization name: %s", vs)
   263  		opts.Organization = &vs
   264  	}
   265  
   266  	if v := orgDetails["personal"]; v != nil {
   267  		vs := v.(bool)
   268  		log.Printf("[DEBUG] Organization Personal: %t", vs)
   269  		opts.Personal = &vs
   270  	}
   271  
   272  	if v := orgDetails["locked"]; v != nil {
   273  		vs := v.(bool)
   274  		log.Printf("[DEBUG] Organization locked: %t", vs)
   275  		opts.Locked = &vs
   276  	}
   277  
   278  	if v := d.Get("name"); v != nil {
   279  		vs := v.(string)
   280  		log.Printf("[DEBUG] App name: %s", vs)
   281  		opts.Name = &vs
   282  	}
   283  	if v, ok := d.GetOk("region"); ok {
   284  		vs := v.(string)
   285  		log.Printf("[DEBUG] App region: %s", vs)
   286  		opts.Region = &vs
   287  	}
   288  	if v, ok := d.GetOk("space"); ok {
   289  		vs := v.(string)
   290  		log.Printf("[DEBUG] App space: %s", vs)
   291  		opts.Space = &vs
   292  	}
   293  	if v, ok := d.GetOk("stack"); ok {
   294  		vs := v.(string)
   295  		log.Printf("[DEBUG] App stack: %s", vs)
   296  		opts.Stack = &vs
   297  	}
   298  
   299  	log.Printf("[DEBUG] Creating Heroku app...")
   300  	a, err := client.OrganizationAppCreate(context.TODO(), opts)
   301  	if err != nil {
   302  		return err
   303  	}
   304  
   305  	d.SetId(a.Name)
   306  	log.Printf("[INFO] App ID: %s", d.Id())
   307  
   308  	if v, ok := d.GetOk("config_vars"); ok {
   309  		err = updateConfigVars(d.Id(), client, nil, v.([]interface{}))
   310  		if err != nil {
   311  			return err
   312  		}
   313  	}
   314  
   315  	return resourceHerokuAppRead(d, meta)
   316  }
   317  
   318  func resourceHerokuAppRead(d *schema.ResourceData, meta interface{}) error {
   319  	client := meta.(*heroku.Service)
   320  
   321  	configVars := make(map[string]string)
   322  	care := make(map[string]struct{})
   323  	for _, v := range d.Get("config_vars").([]interface{}) {
   324  		for k := range v.(map[string]interface{}) {
   325  			care[k] = struct{}{}
   326  		}
   327  	}
   328  
   329  	// Only track buildpacks when set in the configuration.
   330  	_, buildpacksConfigured := d.GetOk("buildpacks")
   331  
   332  	organizationApp := isOrganizationApp(d)
   333  
   334  	// Only set the config_vars that we have set in the configuration.
   335  	// The "all_config_vars" field has all of them.
   336  	app, err := resourceHerokuAppRetrieve(d.Id(), organizationApp, client)
   337  	if err != nil {
   338  		return err
   339  	}
   340  
   341  	for k, v := range app.Vars {
   342  		if _, ok := care[k]; ok {
   343  			configVars[k] = v
   344  		}
   345  	}
   346  	var configVarsValue []map[string]string
   347  	if len(configVars) > 0 {
   348  		configVarsValue = []map[string]string{configVars}
   349  	}
   350  
   351  	d.Set("name", app.App.Name)
   352  	d.Set("stack", app.App.Stack)
   353  	d.Set("region", app.App.Region)
   354  	d.Set("git_url", app.App.GitURL)
   355  	d.Set("web_url", app.App.WebURL)
   356  	if buildpacksConfigured {
   357  		d.Set("buildpacks", app.Buildpacks)
   358  	}
   359  	d.Set("config_vars", configVarsValue)
   360  	d.Set("all_config_vars", app.Vars)
   361  	if organizationApp {
   362  		d.Set("space", app.App.Space)
   363  
   364  		orgDetails := map[string]interface{}{
   365  			"name":     app.App.OrganizationName,
   366  			"locked":   app.App.Locked,
   367  			"personal": false,
   368  		}
   369  		err := d.Set("organization", []interface{}{orgDetails})
   370  		if err != nil {
   371  			return err
   372  		}
   373  	}
   374  
   375  	// We know that the hostname on heroku will be the name+herokuapp.com
   376  	// You need this to do things like create DNS CNAME records
   377  	d.Set("heroku_hostname", fmt.Sprintf("%s.herokuapp.com", app.App.Name))
   378  
   379  	return nil
   380  }
   381  
   382  func resourceHerokuAppUpdate(d *schema.ResourceData, meta interface{}) error {
   383  	client := meta.(*heroku.Service)
   384  
   385  	// If name changed, update it
   386  	if d.HasChange("name") {
   387  		v := d.Get("name").(string)
   388  		opts := heroku.AppUpdateOpts{
   389  			Name: &v,
   390  		}
   391  
   392  		renamedApp, err := client.AppUpdate(context.TODO(), d.Id(), opts)
   393  		if err != nil {
   394  			return err
   395  		}
   396  
   397  		// Store the new ID
   398  		d.SetId(renamedApp.Name)
   399  	}
   400  
   401  	// If the config vars changed, then recalculate those
   402  	if d.HasChange("config_vars") {
   403  		o, n := d.GetChange("config_vars")
   404  		if o == nil {
   405  			o = []interface{}{}
   406  		}
   407  		if n == nil {
   408  			n = []interface{}{}
   409  		}
   410  
   411  		err := updateConfigVars(
   412  			d.Id(), client, o.([]interface{}), n.([]interface{}))
   413  		if err != nil {
   414  			return err
   415  		}
   416  	}
   417  
   418  	if d.HasChange("buildpacks") {
   419  		err := updateBuildpacks(d.Id(), client, d.Get("buildpacks").([]interface{}))
   420  		if err != nil {
   421  			return err
   422  		}
   423  	}
   424  
   425  	return resourceHerokuAppRead(d, meta)
   426  }
   427  
   428  func resourceHerokuAppDelete(d *schema.ResourceData, meta interface{}) error {
   429  	client := meta.(*heroku.Service)
   430  
   431  	log.Printf("[INFO] Deleting App: %s", d.Id())
   432  	_, err := client.AppDelete(context.TODO(), d.Id())
   433  	if err != nil {
   434  		return fmt.Errorf("Error deleting App: %s", err)
   435  	}
   436  
   437  	d.SetId("")
   438  	return nil
   439  }
   440  
   441  func resourceHerokuAppRetrieve(id string, organization bool, client *heroku.Service) (*application, error) {
   442  	app := application{Id: id, Client: client, Organization: organization}
   443  
   444  	err := app.Update()
   445  
   446  	if err != nil {
   447  		return nil, fmt.Errorf("Error retrieving app: %s", err)
   448  	}
   449  
   450  	return &app, nil
   451  }
   452  
   453  func retrieveBuildpacks(id string, client *heroku.Service) ([]string, error) {
   454  	results, err := client.BuildpackInstallationList(context.TODO(), id, nil)
   455  
   456  	if err != nil {
   457  		return nil, err
   458  	}
   459  
   460  	buildpacks := []string{}
   461  	for _, installation := range results {
   462  		buildpacks = append(buildpacks, installation.Buildpack.Name)
   463  	}
   464  
   465  	return buildpacks, nil
   466  }
   467  
   468  func retrieveConfigVars(id string, client *heroku.Service) (map[string]string, error) {
   469  	vars, err := client.ConfigVarInfoForApp(context.TODO(), id)
   470  
   471  	if err != nil {
   472  		return nil, err
   473  	}
   474  
   475  	nonNullVars := map[string]string{}
   476  	for k, v := range vars {
   477  		if v != nil {
   478  			nonNullVars[k] = *v
   479  		}
   480  	}
   481  
   482  	return nonNullVars, nil
   483  }
   484  
   485  // Updates the config vars for from an expanded configuration.
   486  func updateConfigVars(
   487  	id string,
   488  	client *heroku.Service,
   489  	o []interface{},
   490  	n []interface{}) error {
   491  	vars := make(map[string]*string)
   492  
   493  	for _, v := range o {
   494  		if v != nil {
   495  			for k := range v.(map[string]interface{}) {
   496  				vars[k] = nil
   497  			}
   498  		}
   499  	}
   500  	for _, v := range n {
   501  		if v != nil {
   502  			for k, v := range v.(map[string]interface{}) {
   503  				val := v.(string)
   504  				vars[k] = &val
   505  			}
   506  		}
   507  	}
   508  
   509  	log.Printf("[INFO] Updating config vars: *%#v", vars)
   510  	if _, err := client.ConfigVarUpdate(context.TODO(), id, vars); err != nil {
   511  		return fmt.Errorf("Error updating config vars: %s", err)
   512  	}
   513  
   514  	return nil
   515  }
   516  
   517  func updateBuildpacks(id string, client *heroku.Service, v []interface{}) error {
   518  	opts := heroku.BuildpackInstallationUpdateOpts{
   519  		Updates: []struct {
   520  			Buildpack string `json:"buildpack" url:"buildpack,key"`
   521  		}{}}
   522  
   523  	for _, buildpack := range v {
   524  		opts.Updates = append(opts.Updates, struct {
   525  			Buildpack string `json:"buildpack" url:"buildpack,key"`
   526  		}{
   527  			Buildpack: buildpack.(string),
   528  		})
   529  	}
   530  
   531  	if _, err := client.BuildpackInstallationUpdate(context.TODO(), id, opts); err != nil {
   532  		return fmt.Errorf("Error updating buildpacks: %s", err)
   533  	}
   534  
   535  	return nil
   536  }