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 }