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