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 }