github.com/chentex/terraform@v0.11.2-0.20171208003256-252e8145842e/builtin/provisioners/habitat/resource_provisioner.go (about) 1 package habitat 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "log" 9 "net/url" 10 "path" 11 "strings" 12 "time" 13 14 "github.com/hashicorp/terraform/communicator" 15 "github.com/hashicorp/terraform/communicator/remote" 16 "github.com/hashicorp/terraform/helper/schema" 17 "github.com/hashicorp/terraform/terraform" 18 linereader "github.com/mitchellh/go-linereader" 19 ) 20 21 const installURL = "https://raw.githubusercontent.com/habitat-sh/habitat/master/components/hab/install.sh" 22 const systemdUnit = `[Unit] 23 Description=Habitat Supervisor 24 25 [Service] 26 ExecStart=/bin/hab sup run %s 27 Restart=on-failure 28 29 [Install] 30 WantedBy=default.target` 31 32 var serviceTypes = map[string]bool{"unmanaged": true, "systemd": true} 33 var updateStrategies = map[string]bool{"at-once": true, "rolling": true, "none": true} 34 var topologies = map[string]bool{"leader": true, "standalone": true} 35 36 type provisionFn func(terraform.UIOutput, communicator.Communicator) error 37 38 type provisioner struct { 39 Version string 40 Services []Service 41 PermanentPeer bool 42 ListenGossip string 43 ListenHTTP string 44 Peer string 45 RingKey string 46 RingKeyContent string 47 SkipInstall bool 48 UseSudo bool 49 ServiceType string 50 URL string 51 Channel string 52 Events string 53 OverrideName string 54 Organization string 55 } 56 57 func Provisioner() terraform.ResourceProvisioner { 58 return &schema.Provisioner{ 59 Schema: map[string]*schema.Schema{ 60 "version": &schema.Schema{ 61 Type: schema.TypeString, 62 Optional: true, 63 }, 64 "peer": &schema.Schema{ 65 Type: schema.TypeString, 66 Optional: true, 67 }, 68 "service_type": &schema.Schema{ 69 Type: schema.TypeString, 70 Optional: true, 71 Default: "systemd", 72 }, 73 "use_sudo": &schema.Schema{ 74 Type: schema.TypeBool, 75 Optional: true, 76 Default: true, 77 }, 78 "permanent_peer": &schema.Schema{ 79 Type: schema.TypeBool, 80 Optional: true, 81 Default: false, 82 }, 83 "listen_gossip": &schema.Schema{ 84 Type: schema.TypeString, 85 Optional: true, 86 }, 87 "listen_http": &schema.Schema{ 88 Type: schema.TypeString, 89 Optional: true, 90 }, 91 "ring_key": &schema.Schema{ 92 Type: schema.TypeString, 93 Optional: true, 94 }, 95 "ring_key_content": &schema.Schema{ 96 Type: schema.TypeString, 97 Optional: true, 98 }, 99 "url": &schema.Schema{ 100 Type: schema.TypeString, 101 Optional: true, 102 }, 103 "channel": &schema.Schema{ 104 Type: schema.TypeString, 105 Optional: true, 106 }, 107 "events": &schema.Schema{ 108 Type: schema.TypeString, 109 Optional: true, 110 }, 111 "override_name": &schema.Schema{ 112 Type: schema.TypeString, 113 Optional: true, 114 }, 115 "organization": &schema.Schema{ 116 Type: schema.TypeString, 117 Optional: true, 118 }, 119 "service": &schema.Schema{ 120 Type: schema.TypeSet, 121 Elem: &schema.Resource{ 122 Schema: map[string]*schema.Schema{ 123 "name": &schema.Schema{ 124 Type: schema.TypeString, 125 Required: true, 126 }, 127 "binds": &schema.Schema{ 128 Type: schema.TypeList, 129 Elem: &schema.Schema{Type: schema.TypeString}, 130 Optional: true, 131 }, 132 "bind": &schema.Schema{ 133 Type: schema.TypeSet, 134 Elem: &schema.Resource{ 135 Schema: map[string]*schema.Schema{ 136 "alias": &schema.Schema{ 137 Type: schema.TypeString, 138 Required: true, 139 }, 140 "service": &schema.Schema{ 141 Type: schema.TypeString, 142 Required: true, 143 }, 144 "group": &schema.Schema{ 145 Type: schema.TypeString, 146 Required: true, 147 }, 148 }, 149 }, 150 Optional: true, 151 }, 152 "topology": &schema.Schema{ 153 Type: schema.TypeString, 154 Optional: true, 155 }, 156 "user_toml": &schema.Schema{ 157 Type: schema.TypeString, 158 Optional: true, 159 }, 160 "strategy": &schema.Schema{ 161 Type: schema.TypeString, 162 Optional: true, 163 }, 164 "channel": &schema.Schema{ 165 Type: schema.TypeString, 166 Optional: true, 167 }, 168 "group": &schema.Schema{ 169 Type: schema.TypeString, 170 Optional: true, 171 }, 172 "url": &schema.Schema{ 173 Type: schema.TypeString, 174 Optional: true, 175 }, 176 "application": &schema.Schema{ 177 Type: schema.TypeString, 178 Optional: true, 179 }, 180 "environment": &schema.Schema{ 181 Type: schema.TypeString, 182 Optional: true, 183 }, 184 "override_name": &schema.Schema{ 185 Type: schema.TypeString, 186 Optional: true, 187 }, 188 "service_key": &schema.Schema{ 189 Type: schema.TypeString, 190 Optional: true, 191 }, 192 }, 193 }, 194 Optional: true, 195 }, 196 }, 197 ApplyFunc: applyFn, 198 ValidateFunc: validateFn, 199 } 200 } 201 202 func applyFn(ctx context.Context) error { 203 o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput) 204 s := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState) 205 d := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData) 206 207 p, err := decodeConfig(d) 208 if err != nil { 209 return err 210 } 211 212 comm, err := communicator.New(s) 213 if err != nil { 214 return err 215 } 216 217 err = retryFunc(comm.Timeout(), func() error { 218 err = comm.Connect(o) 219 return err 220 }) 221 if err != nil { 222 return err 223 } 224 defer comm.Disconnect() 225 226 if !p.SkipInstall { 227 o.Output("Installing habitat...") 228 if err := p.installHab(o, comm); err != nil { 229 return err 230 } 231 } 232 233 if p.RingKeyContent != "" { 234 o.Output("Uploading supervisor ring key...") 235 if err := p.uploadRingKey(o, comm); err != nil { 236 return err 237 } 238 } 239 240 o.Output("Starting the habitat supervisor...") 241 if err := p.startHab(o, comm); err != nil { 242 return err 243 } 244 245 if p.Services != nil { 246 for _, service := range p.Services { 247 o.Output("Starting service: " + service.Name) 248 if err := p.startHabService(o, comm, service); err != nil { 249 return err 250 } 251 } 252 } 253 254 return nil 255 } 256 257 func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) { 258 serviceType, ok := c.Get("service_type") 259 if ok { 260 if !serviceTypes[serviceType.(string)] { 261 es = append(es, errors.New(serviceType.(string)+" is not a valid service_type.")) 262 } 263 } 264 265 builderURL, ok := c.Get("url") 266 if ok { 267 if _, err := url.ParseRequestURI(builderURL.(string)); err != nil { 268 es = append(es, errors.New(builderURL.(string)+" is not a valid URL.")) 269 } 270 } 271 272 // Validate service level configs 273 services, ok := c.Get("service") 274 if ok { 275 for _, service := range services.([]map[string]interface{}) { 276 strategy, ok := service["strategy"].(string) 277 if ok && !updateStrategies[strategy] { 278 es = append(es, errors.New(strategy+" is not a valid update strategy.")) 279 } 280 281 topology, ok := service["topology"].(string) 282 if ok && !topologies[topology] { 283 es = append(es, errors.New(topology+" is not a valid topology")) 284 } 285 286 builderURL, ok := service["url"].(string) 287 if ok { 288 if _, err := url.ParseRequestURI(builderURL); err != nil { 289 es = append(es, errors.New(builderURL+" is not a valid URL.")) 290 } 291 } 292 } 293 } 294 return ws, es 295 } 296 297 type Service struct { 298 Name string 299 Strategy string 300 Topology string 301 Channel string 302 Group string 303 URL string 304 Binds []Bind 305 BindStrings []string 306 UserTOML string 307 AppName string 308 Environment string 309 OverrideName string 310 ServiceGroupKey string 311 } 312 313 type Bind struct { 314 Alias string 315 Service string 316 Group string 317 } 318 319 func (s *Service) getPackageName(fullName string) string { 320 return strings.Split(fullName, "/")[1] 321 } 322 323 func (b *Bind) toBindString() string { 324 return fmt.Sprintf("%s:%s.%s", b.Alias, b.Service, b.Group) 325 } 326 327 func decodeConfig(d *schema.ResourceData) (*provisioner, error) { 328 p := &provisioner{ 329 Version: d.Get("version").(string), 330 Peer: d.Get("peer").(string), 331 Services: getServices(d.Get("service").(*schema.Set).List()), 332 UseSudo: d.Get("use_sudo").(bool), 333 ServiceType: d.Get("service_type").(string), 334 RingKey: d.Get("ring_key").(string), 335 RingKeyContent: d.Get("ring_key_content").(string), 336 PermanentPeer: d.Get("permanent_peer").(bool), 337 ListenGossip: d.Get("listen_gossip").(string), 338 ListenHTTP: d.Get("listen_http").(string), 339 URL: d.Get("url").(string), 340 Channel: d.Get("channel").(string), 341 Events: d.Get("events").(string), 342 OverrideName: d.Get("override_name").(string), 343 Organization: d.Get("organization").(string), 344 } 345 346 return p, nil 347 } 348 349 func getServices(v []interface{}) []Service { 350 services := make([]Service, 0, len(v)) 351 for _, rawServiceData := range v { 352 serviceData := rawServiceData.(map[string]interface{}) 353 name := (serviceData["name"].(string)) 354 strategy := (serviceData["strategy"].(string)) 355 topology := (serviceData["topology"].(string)) 356 channel := (serviceData["channel"].(string)) 357 group := (serviceData["group"].(string)) 358 url := (serviceData["url"].(string)) 359 app := (serviceData["application"].(string)) 360 env := (serviceData["environment"].(string)) 361 override := (serviceData["override_name"].(string)) 362 userToml := (serviceData["user_toml"].(string)) 363 serviceGroupKey := (serviceData["service_key"].(string)) 364 var bindStrings []string 365 binds := getBinds(serviceData["bind"].(*schema.Set).List()) 366 for _, b := range serviceData["binds"].([]interface{}) { 367 bind, err := getBindFromString(b.(string)) 368 if err != nil { 369 return nil 370 } 371 binds = append(binds, bind) 372 } 373 374 service := Service{ 375 Name: name, 376 Strategy: strategy, 377 Topology: topology, 378 Channel: channel, 379 Group: group, 380 URL: url, 381 UserTOML: userToml, 382 BindStrings: bindStrings, 383 Binds: binds, 384 AppName: app, 385 Environment: env, 386 OverrideName: override, 387 ServiceGroupKey: serviceGroupKey, 388 } 389 services = append(services, service) 390 } 391 return services 392 } 393 394 func getBinds(v []interface{}) []Bind { 395 binds := make([]Bind, 0, len(v)) 396 for _, rawBindData := range v { 397 bindData := rawBindData.(map[string]interface{}) 398 alias := bindData["alias"].(string) 399 service := bindData["service"].(string) 400 group := bindData["group"].(string) 401 bind := Bind{ 402 Alias: alias, 403 Service: service, 404 Group: group, 405 } 406 binds = append(binds, bind) 407 } 408 return binds 409 } 410 411 func (p *provisioner) uploadRingKey(o terraform.UIOutput, comm communicator.Communicator) error { 412 command := fmt.Sprintf("echo '%s' | hab ring key import", p.RingKeyContent) 413 if p.UseSudo { 414 command = fmt.Sprintf("echo '%s' | sudo hab ring key import", p.RingKeyContent) 415 } 416 return p.runCommand(o, comm, command) 417 } 418 419 func (p *provisioner) installHab(o terraform.UIOutput, comm communicator.Communicator) error { 420 // Build the install command 421 command := fmt.Sprintf("curl -L0 %s > install.sh", installURL) 422 if err := p.runCommand(o, comm, command); err != nil { 423 return err 424 } 425 426 // Run the install script 427 if p.Version == "" { 428 command = fmt.Sprintf("env HAB_NONINTERACTIVE=true bash ./install.sh ") 429 } else { 430 command = fmt.Sprintf("env HAB_NONINTERACTIVE=true bash ./install.sh -v %s", p.Version) 431 } 432 433 if p.UseSudo { 434 command = fmt.Sprintf("sudo %s", command) 435 } 436 437 if err := p.runCommand(o, comm, command); err != nil { 438 return err 439 } 440 441 if err := p.createHabUser(o, comm); err != nil { 442 return err 443 } 444 445 return p.runCommand(o, comm, fmt.Sprintf("rm -f install.sh")) 446 } 447 448 func (p *provisioner) startHab(o terraform.UIOutput, comm communicator.Communicator) error { 449 // Install the supervisor first 450 var command string 451 if p.Version == "" { 452 command += fmt.Sprintf("hab install core/hab-sup") 453 } else { 454 command += fmt.Sprintf("hab install core/hab-sup/%s", p.Version) 455 } 456 457 if p.UseSudo { 458 command = fmt.Sprintf("sudo -E %s", command) 459 } 460 461 command = fmt.Sprintf("env HAB_NONINTERACTIVE=true %s", command) 462 463 if err := p.runCommand(o, comm, command); err != nil { 464 return err 465 } 466 467 // Build up sup options 468 options := "" 469 if p.PermanentPeer { 470 options += " -I" 471 } 472 473 if p.ListenGossip != "" { 474 options += fmt.Sprintf(" --listen-gossip %s", p.ListenGossip) 475 } 476 477 if p.ListenHTTP != "" { 478 options += fmt.Sprintf(" --listen-http %s", p.ListenHTTP) 479 } 480 481 if p.Peer != "" { 482 options += fmt.Sprintf(" --peer %s", p.Peer) 483 } 484 485 if p.RingKey != "" { 486 options += fmt.Sprintf(" --ring %s", p.RingKey) 487 } 488 489 if p.URL != "" { 490 options += fmt.Sprintf(" --url %s", p.URL) 491 } 492 493 if p.Channel != "" { 494 options += fmt.Sprintf(" --channel %s", p.Channel) 495 } 496 497 if p.Events != "" { 498 options += fmt.Sprintf(" --events %s", p.Events) 499 } 500 501 if p.OverrideName != "" { 502 options += fmt.Sprintf(" --override-name %s", p.OverrideName) 503 } 504 505 if p.Organization != "" { 506 options += fmt.Sprintf(" --org %s", p.Organization) 507 } 508 509 switch p.ServiceType { 510 case "unmanaged": 511 return p.startHabUnmanaged(o, comm, options) 512 case "systemd": 513 return p.startHabSystemd(o, comm, options) 514 default: 515 return errors.New("Unsupported service type") 516 } 517 } 518 519 func (p *provisioner) startHabUnmanaged(o terraform.UIOutput, comm communicator.Communicator, options string) error { 520 // Create the sup directory for the log file 521 var command string 522 if p.UseSudo { 523 command = "sudo mkdir -p /hab/sup/default && sudo chmod o+w /hab/sup/default" 524 } else { 525 command = "mkdir -p /hab/sup/default && chmod o+w /hab/sup/default" 526 } 527 if err := p.runCommand(o, comm, command); err != nil { 528 return err 529 } 530 531 if p.UseSudo { 532 command = fmt.Sprintf("(setsid sudo hab sup run %s > /hab/sup/default/sup.log 2>&1 &) ; sleep 1", options) 533 } else { 534 command = fmt.Sprintf("(setsid hab sup run %s > /hab/sup/default/sup.log 2>&1 <&1 &) ; sleep 1", options) 535 } 536 return p.runCommand(o, comm, command) 537 } 538 539 func (p *provisioner) startHabSystemd(o terraform.UIOutput, comm communicator.Communicator, options string) error { 540 541 unitString := fmt.Sprintf(systemdUnit, options) 542 var command string 543 if p.UseSudo { 544 command = fmt.Sprintf("sudo echo '%s' | sudo tee /etc/systemd/system/hab-supervisor.service > /dev/null", unitString) 545 } else { 546 command = fmt.Sprintf("echo '%s' | tee /etc/systemd/system/hab-supervisor.service > /dev/null", unitString) 547 } 548 549 if err := p.runCommand(o, comm, command); err != nil { 550 return err 551 } 552 553 if p.UseSudo { 554 command = fmt.Sprintf("sudo systemctl start hab-supervisor") 555 } else { 556 command = fmt.Sprintf("systemctl start hab-supervisor") 557 } 558 return p.runCommand(o, comm, command) 559 } 560 561 func (p *provisioner) createHabUser(o terraform.UIOutput, comm communicator.Communicator) error { 562 // Create the hab user 563 command := fmt.Sprintf("env HAB_NONINTERACTIVE=true hab install core/busybox") 564 if p.UseSudo { 565 command = fmt.Sprintf("sudo %s", command) 566 } 567 if err := p.runCommand(o, comm, command); err != nil { 568 return err 569 } 570 571 command = fmt.Sprintf("hab pkg exec core/busybox adduser -D -g \"\" hab") 572 if p.UseSudo { 573 command = fmt.Sprintf("sudo %s", command) 574 } 575 return p.runCommand(o, comm, command) 576 } 577 578 func (p *provisioner) startHabService(o terraform.UIOutput, comm communicator.Communicator, service Service) error { 579 var command string 580 if p.UseSudo { 581 command = fmt.Sprintf("env HAB_NONINTERACTIVE=true sudo -E hab pkg install %s", service.Name) 582 } else { 583 command = fmt.Sprintf("env HAB_NONINTERACTIVE=true hab pkg install %s", service.Name) 584 } 585 if err := p.runCommand(o, comm, command); err != nil { 586 return err 587 } 588 589 if err := p.uploadUserTOML(o, comm, service); err != nil { 590 return err 591 } 592 593 // Upload service group key 594 if service.ServiceGroupKey != "" { 595 p.uploadServiceGroupKey(o, comm, service.ServiceGroupKey) 596 } 597 598 options := "" 599 if service.Topology != "" { 600 options += fmt.Sprintf(" --topology %s", service.Topology) 601 } 602 603 if service.Strategy != "" { 604 options += fmt.Sprintf(" --strategy %s", service.Strategy) 605 } 606 607 if service.Channel != "" { 608 options += fmt.Sprintf(" --channel %s", service.Channel) 609 } 610 611 if service.URL != "" { 612 options += fmt.Sprintf("--url %s", service.URL) 613 } 614 615 if service.Group != "" { 616 options += fmt.Sprintf(" --group %s", service.Group) 617 } 618 619 for _, bind := range service.Binds { 620 options += fmt.Sprintf(" --bind %s", bind.toBindString()) 621 } 622 command = fmt.Sprintf("hab svc load %s %s", service.Name, options) 623 if p.UseSudo { 624 command = fmt.Sprintf("sudo %s", command) 625 } 626 return p.runCommand(o, comm, command) 627 } 628 629 func (p *provisioner) uploadServiceGroupKey(o terraform.UIOutput, comm communicator.Communicator, key string) error { 630 keyName := strings.Split(key, "\n")[1] 631 o.Output("Uploading service group key: " + keyName) 632 keyFileName := fmt.Sprintf("%s.box.key", keyName) 633 destPath := path.Join("/hab/cache/keys", keyFileName) 634 keyContent := strings.NewReader(key) 635 if p.UseSudo { 636 tempPath := path.Join("/tmp", keyFileName) 637 if err := comm.Upload(tempPath, keyContent); err != nil { 638 return err 639 } 640 command := fmt.Sprintf("sudo mv %s %s", tempPath, destPath) 641 return p.runCommand(o, comm, command) 642 } 643 644 return comm.Upload(destPath, keyContent) 645 } 646 647 func (p *provisioner) uploadUserTOML(o terraform.UIOutput, comm communicator.Communicator, service Service) error { 648 // Create the hab svc directory to lay down the user.toml before loading the service 649 o.Output("Uploading user.toml for service: " + service.Name) 650 destDir := fmt.Sprintf("/hab/svc/%s", service.getPackageName(service.Name)) 651 command := fmt.Sprintf("mkdir -p %s", destDir) 652 if p.UseSudo { 653 command = fmt.Sprintf("sudo %s", command) 654 } 655 if err := p.runCommand(o, comm, command); err != nil { 656 return err 657 } 658 659 userToml := strings.NewReader(service.UserTOML) 660 661 if p.UseSudo { 662 if err := comm.Upload("/tmp/user.toml", userToml); err != nil { 663 return err 664 } 665 command = fmt.Sprintf("sudo mv /tmp/user.toml %s", destDir) 666 return p.runCommand(o, comm, command) 667 } 668 669 return comm.Upload(path.Join(destDir, "user.toml"), userToml) 670 671 } 672 673 func retryFunc(timeout time.Duration, f func() error) error { 674 finish := time.After(timeout) 675 676 for { 677 err := f() 678 if err == nil { 679 return nil 680 } 681 log.Printf("Retryable error: %v", err) 682 683 select { 684 case <-finish: 685 return err 686 case <-time.After(3 * time.Second): 687 } 688 } 689 } 690 691 func (p *provisioner) copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) { 692 defer close(doneCh) 693 lr := linereader.New(r) 694 for line := range lr.Ch { 695 o.Output(line) 696 } 697 } 698 699 func (p *provisioner) runCommand(o terraform.UIOutput, comm communicator.Communicator, command string) error { 700 outR, outW := io.Pipe() 701 errR, errW := io.Pipe() 702 var err error 703 704 outDoneCh := make(chan struct{}) 705 errDoneCh := make(chan struct{}) 706 go p.copyOutput(o, outR, outDoneCh) 707 go p.copyOutput(o, errR, errDoneCh) 708 709 cmd := &remote.Cmd{ 710 Command: command, 711 Stdout: outW, 712 Stderr: errW, 713 } 714 715 if err = comm.Start(cmd); err != nil { 716 return fmt.Errorf("Error executing command %q: %v", cmd.Command, err) 717 } 718 719 cmd.Wait() 720 if cmd.ExitStatus != 0 { 721 err = fmt.Errorf( 722 "Command %q exited with non-zero exit status: %d", cmd.Command, cmd.ExitStatus) 723 } 724 725 outW.Close() 726 errW.Close() 727 <-outDoneCh 728 <-errDoneCh 729 730 return err 731 } 732 733 func getBindFromString(bind string) (Bind, error) { 734 t := strings.FieldsFunc(bind, func(d rune) bool { 735 switch d { 736 case ':', '.': 737 return true 738 } 739 return false 740 }) 741 if len(t) != 3 { 742 return Bind{}, errors.New("Invalid bind specification: " + bind) 743 } 744 return Bind{Alias: t[0], Service: t[1], Group: t[2]}, nil 745 }