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