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