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