github.com/nathanielks/terraform@v0.6.1-0.20170509030759-13e1a62319dc/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  		Importer: &schema.ResourceImporter{
   104  			State: resourceHerokuAppImport,
   105  		},
   106  
   107  		Schema: map[string]*schema.Schema{
   108  			"name": {
   109  				Type:     schema.TypeString,
   110  				Required: true,
   111  			},
   112  
   113  			"space": {
   114  				Type:     schema.TypeString,
   115  				Optional: true,
   116  				ForceNew: true,
   117  			},
   118  
   119  			"region": {
   120  				Type:     schema.TypeString,
   121  				Required: true,
   122  				ForceNew: true,
   123  			},
   124  
   125  			"stack": {
   126  				Type:     schema.TypeString,
   127  				Optional: true,
   128  				Computed: true,
   129  				ForceNew: true,
   130  			},
   131  
   132  			"buildpacks": {
   133  				Type:     schema.TypeList,
   134  				Optional: true,
   135  				Elem: &schema.Schema{
   136  					Type: schema.TypeString,
   137  				},
   138  			},
   139  
   140  			"config_vars": {
   141  				Type:     schema.TypeList,
   142  				Optional: true,
   143  				Elem: &schema.Schema{
   144  					Type: schema.TypeMap,
   145  				},
   146  			},
   147  
   148  			"all_config_vars": {
   149  				Type:     schema.TypeMap,
   150  				Computed: true,
   151  			},
   152  
   153  			"git_url": {
   154  				Type:     schema.TypeString,
   155  				Computed: true,
   156  			},
   157  
   158  			"web_url": {
   159  				Type:     schema.TypeString,
   160  				Computed: true,
   161  			},
   162  
   163  			"heroku_hostname": {
   164  				Type:     schema.TypeString,
   165  				Computed: true,
   166  			},
   167  
   168  			"organization": {
   169  				Type:     schema.TypeList,
   170  				Optional: true,
   171  				ForceNew: true,
   172  				Elem: &schema.Resource{
   173  					Schema: map[string]*schema.Schema{
   174  						"name": {
   175  							Type:     schema.TypeString,
   176  							Required: true,
   177  						},
   178  
   179  						"locked": {
   180  							Type:     schema.TypeBool,
   181  							Optional: true,
   182  						},
   183  
   184  						"personal": {
   185  							Type:     schema.TypeBool,
   186  							Optional: true,
   187  						},
   188  					},
   189  				},
   190  			},
   191  		},
   192  	}
   193  }
   194  
   195  func isOrganizationApp(d *schema.ResourceData) bool {
   196  	v := d.Get("organization").([]interface{})
   197  	return len(v) > 0 && v[0] != nil
   198  }
   199  
   200  func resourceHerokuAppImport(d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) {
   201  	client := m.(*heroku.Service)
   202  
   203  	app, err := client.AppInfo(context.TODO(), d.Id())
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  
   208  	// Flag organization apps by setting the organization name
   209  	if app.Organization != nil {
   210  		d.Set("organization", []map[string]interface{}{
   211  			{"name": app.Organization.Name},
   212  		})
   213  	}
   214  
   215  	return []*schema.ResourceData{d}, nil
   216  }
   217  
   218  func switchHerokuAppCreate(d *schema.ResourceData, meta interface{}) error {
   219  	if isOrganizationApp(d) {
   220  		return resourceHerokuOrgAppCreate(d, meta)
   221  	}
   222  
   223  	return resourceHerokuAppCreate(d, meta)
   224  }
   225  
   226  func resourceHerokuAppCreate(d *schema.ResourceData, meta interface{}) error {
   227  	client := meta.(*heroku.Service)
   228  
   229  	// Build up our creation options
   230  	opts := heroku.AppCreateOpts{}
   231  
   232  	if v, ok := d.GetOk("name"); ok {
   233  		vs := v.(string)
   234  		log.Printf("[DEBUG] App name: %s", vs)
   235  		opts.Name = &vs
   236  	}
   237  	if v, ok := d.GetOk("region"); ok {
   238  		vs := v.(string)
   239  		log.Printf("[DEBUG] App region: %s", vs)
   240  		opts.Region = &vs
   241  	}
   242  	if v, ok := d.GetOk("stack"); ok {
   243  		vs := v.(string)
   244  		log.Printf("[DEBUG] App stack: %s", vs)
   245  		opts.Stack = &vs
   246  	}
   247  
   248  	log.Printf("[DEBUG] Creating Heroku app...")
   249  	a, err := client.AppCreate(context.TODO(), opts)
   250  	if err != nil {
   251  		return err
   252  	}
   253  
   254  	d.SetId(a.Name)
   255  	log.Printf("[INFO] App ID: %s", d.Id())
   256  
   257  	if err := performAppPostCreateTasks(d, client); err != nil {
   258  		return err
   259  	}
   260  
   261  	return resourceHerokuAppRead(d, meta)
   262  }
   263  
   264  func resourceHerokuOrgAppCreate(d *schema.ResourceData, meta interface{}) error {
   265  	client := meta.(*heroku.Service)
   266  	// Build up our creation options
   267  	opts := heroku.OrganizationAppCreateOpts{}
   268  
   269  	v := d.Get("organization").([]interface{})
   270  	if len(v) > 1 {
   271  		return fmt.Errorf("Error Creating Heroku App: Only 1 Heroku Organization is permitted")
   272  	}
   273  	orgDetails := v[0].(map[string]interface{})
   274  
   275  	if v := orgDetails["name"]; v != nil {
   276  		vs := v.(string)
   277  		log.Printf("[DEBUG] Organization name: %s", vs)
   278  		opts.Organization = &vs
   279  	}
   280  
   281  	if v := orgDetails["personal"]; v != nil {
   282  		vs := v.(bool)
   283  		log.Printf("[DEBUG] Organization Personal: %t", vs)
   284  		opts.Personal = &vs
   285  	}
   286  
   287  	if v := orgDetails["locked"]; v != nil {
   288  		vs := v.(bool)
   289  		log.Printf("[DEBUG] Organization locked: %t", vs)
   290  		opts.Locked = &vs
   291  	}
   292  
   293  	if v := d.Get("name"); v != nil {
   294  		vs := v.(string)
   295  		log.Printf("[DEBUG] App name: %s", vs)
   296  		opts.Name = &vs
   297  	}
   298  	if v, ok := d.GetOk("region"); ok {
   299  		vs := v.(string)
   300  		log.Printf("[DEBUG] App region: %s", vs)
   301  		opts.Region = &vs
   302  	}
   303  	if v, ok := d.GetOk("space"); ok {
   304  		vs := v.(string)
   305  		log.Printf("[DEBUG] App space: %s", vs)
   306  		opts.Space = &vs
   307  	}
   308  	if v, ok := d.GetOk("stack"); ok {
   309  		vs := v.(string)
   310  		log.Printf("[DEBUG] App stack: %s", vs)
   311  		opts.Stack = &vs
   312  	}
   313  
   314  	log.Printf("[DEBUG] Creating Heroku app...")
   315  	a, err := client.OrganizationAppCreate(context.TODO(), opts)
   316  	if err != nil {
   317  		return err
   318  	}
   319  
   320  	d.SetId(a.Name)
   321  	log.Printf("[INFO] App ID: %s", d.Id())
   322  
   323  	if err := performAppPostCreateTasks(d, client); err != nil {
   324  		return err
   325  	}
   326  
   327  	return resourceHerokuAppRead(d, meta)
   328  }
   329  
   330  func resourceHerokuAppRead(d *schema.ResourceData, meta interface{}) error {
   331  	client := meta.(*heroku.Service)
   332  
   333  	configVars := make(map[string]string)
   334  	care := make(map[string]struct{})
   335  	for _, v := range d.Get("config_vars").([]interface{}) {
   336  		for k := range v.(map[string]interface{}) {
   337  			care[k] = struct{}{}
   338  		}
   339  	}
   340  
   341  	// Only track buildpacks when set in the configuration.
   342  	_, buildpacksConfigured := d.GetOk("buildpacks")
   343  
   344  	organizationApp := isOrganizationApp(d)
   345  
   346  	// Only set the config_vars that we have set in the configuration.
   347  	// The "all_config_vars" field has all of them.
   348  	app, err := resourceHerokuAppRetrieve(d.Id(), organizationApp, client)
   349  	if err != nil {
   350  		return err
   351  	}
   352  
   353  	for k, v := range app.Vars {
   354  		if _, ok := care[k]; ok {
   355  			configVars[k] = v
   356  		}
   357  	}
   358  	var configVarsValue []map[string]string
   359  	if len(configVars) > 0 {
   360  		configVarsValue = []map[string]string{configVars}
   361  	}
   362  
   363  	d.Set("name", app.App.Name)
   364  	d.Set("stack", app.App.Stack)
   365  	d.Set("region", app.App.Region)
   366  	d.Set("git_url", app.App.GitURL)
   367  	d.Set("web_url", app.App.WebURL)
   368  	if buildpacksConfigured {
   369  		d.Set("buildpacks", app.Buildpacks)
   370  	}
   371  	d.Set("config_vars", configVarsValue)
   372  	d.Set("all_config_vars", app.Vars)
   373  	if organizationApp {
   374  		d.Set("space", app.App.Space)
   375  
   376  		orgDetails := map[string]interface{}{
   377  			"name":     app.App.OrganizationName,
   378  			"locked":   app.App.Locked,
   379  			"personal": false,
   380  		}
   381  		err := d.Set("organization", []interface{}{orgDetails})
   382  		if err != nil {
   383  			return err
   384  		}
   385  	}
   386  
   387  	// We know that the hostname on heroku will be the name+herokuapp.com
   388  	// You need this to do things like create DNS CNAME records
   389  	d.Set("heroku_hostname", fmt.Sprintf("%s.herokuapp.com", app.App.Name))
   390  
   391  	return nil
   392  }
   393  
   394  func resourceHerokuAppUpdate(d *schema.ResourceData, meta interface{}) error {
   395  	client := meta.(*heroku.Service)
   396  
   397  	// If name changed, update it
   398  	if d.HasChange("name") {
   399  		v := d.Get("name").(string)
   400  		opts := heroku.AppUpdateOpts{
   401  			Name: &v,
   402  		}
   403  
   404  		renamedApp, err := client.AppUpdate(context.TODO(), d.Id(), opts)
   405  		if err != nil {
   406  			return err
   407  		}
   408  
   409  		// Store the new ID
   410  		d.SetId(renamedApp.Name)
   411  	}
   412  
   413  	// If the config vars changed, then recalculate those
   414  	if d.HasChange("config_vars") {
   415  		o, n := d.GetChange("config_vars")
   416  		if o == nil {
   417  			o = []interface{}{}
   418  		}
   419  		if n == nil {
   420  			n = []interface{}{}
   421  		}
   422  
   423  		err := updateConfigVars(
   424  			d.Id(), client, o.([]interface{}), n.([]interface{}))
   425  		if err != nil {
   426  			return err
   427  		}
   428  	}
   429  
   430  	if d.HasChange("buildpacks") {
   431  		err := updateBuildpacks(d.Id(), client, d.Get("buildpacks").([]interface{}))
   432  		if err != nil {
   433  			return err
   434  		}
   435  	}
   436  
   437  	return resourceHerokuAppRead(d, meta)
   438  }
   439  
   440  func resourceHerokuAppDelete(d *schema.ResourceData, meta interface{}) error {
   441  	client := meta.(*heroku.Service)
   442  
   443  	log.Printf("[INFO] Deleting App: %s", d.Id())
   444  	_, err := client.AppDelete(context.TODO(), d.Id())
   445  	if err != nil {
   446  		return fmt.Errorf("Error deleting App: %s", err)
   447  	}
   448  
   449  	d.SetId("")
   450  	return nil
   451  }
   452  
   453  func resourceHerokuAppRetrieve(id string, organization bool, client *heroku.Service) (*application, error) {
   454  	app := application{Id: id, Client: client, Organization: organization}
   455  
   456  	err := app.Update()
   457  
   458  	if err != nil {
   459  		return nil, fmt.Errorf("Error retrieving app: %s", err)
   460  	}
   461  
   462  	return &app, nil
   463  }
   464  
   465  func retrieveBuildpacks(id string, client *heroku.Service) ([]string, error) {
   466  	results, err := client.BuildpackInstallationList(context.TODO(), id, nil)
   467  
   468  	if err != nil {
   469  		return nil, err
   470  	}
   471  
   472  	buildpacks := []string{}
   473  	for _, installation := range results {
   474  		buildpacks = append(buildpacks, installation.Buildpack.Name)
   475  	}
   476  
   477  	return buildpacks, nil
   478  }
   479  
   480  func retrieveConfigVars(id string, client *heroku.Service) (map[string]string, error) {
   481  	vars, err := client.ConfigVarInfoForApp(context.TODO(), id)
   482  
   483  	if err != nil {
   484  		return nil, err
   485  	}
   486  
   487  	nonNullVars := map[string]string{}
   488  	for k, v := range vars {
   489  		if v != nil {
   490  			nonNullVars[k] = *v
   491  		}
   492  	}
   493  
   494  	return nonNullVars, nil
   495  }
   496  
   497  // Updates the config vars for from an expanded configuration.
   498  func updateConfigVars(
   499  	id string,
   500  	client *heroku.Service,
   501  	o []interface{},
   502  	n []interface{}) error {
   503  	vars := make(map[string]*string)
   504  
   505  	for _, v := range o {
   506  		if v != nil {
   507  			for k := range v.(map[string]interface{}) {
   508  				vars[k] = nil
   509  			}
   510  		}
   511  	}
   512  	for _, v := range n {
   513  		if v != nil {
   514  			for k, v := range v.(map[string]interface{}) {
   515  				val := v.(string)
   516  				vars[k] = &val
   517  			}
   518  		}
   519  	}
   520  
   521  	log.Printf("[INFO] Updating config vars: *%#v", vars)
   522  	if _, err := client.ConfigVarUpdate(context.TODO(), id, vars); err != nil {
   523  		return fmt.Errorf("Error updating config vars: %s", err)
   524  	}
   525  
   526  	return nil
   527  }
   528  
   529  func updateBuildpacks(id string, client *heroku.Service, v []interface{}) error {
   530  	opts := heroku.BuildpackInstallationUpdateOpts{
   531  		Updates: []struct {
   532  			Buildpack string `json:"buildpack" url:"buildpack,key"`
   533  		}{}}
   534  
   535  	for _, buildpack := range v {
   536  		opts.Updates = append(opts.Updates, struct {
   537  			Buildpack string `json:"buildpack" url:"buildpack,key"`
   538  		}{
   539  			Buildpack: buildpack.(string),
   540  		})
   541  	}
   542  
   543  	if _, err := client.BuildpackInstallationUpdate(context.TODO(), id, opts); err != nil {
   544  		return fmt.Errorf("Error updating buildpacks: %s", err)
   545  	}
   546  
   547  	return nil
   548  }
   549  
   550  // performAppPostCreateTasks performs post-create tasks common to both org and non-org apps.
   551  func performAppPostCreateTasks(d *schema.ResourceData, client *heroku.Service) error {
   552  	if v, ok := d.GetOk("config_vars"); ok {
   553  		if err := updateConfigVars(d.Id(), client, nil, v.([]interface{})); err != nil {
   554  			return err
   555  		}
   556  	}
   557  
   558  	if v, ok := d.GetOk("buildpacks"); ok {
   559  		if err := updateBuildpacks(d.Id(), client, v.([]interface{})); err != nil {
   560  			return err
   561  		}
   562  	}
   563  
   564  	return nil
   565  }