github.com/magodo/terraform@v0.11.12-beta1/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 183 Type: schema.TypeString, 184 Optional: true, 185 }, 186 "group": &schema.Schema{ 187 Type: schema.TypeString, 188 Optional: true, 189 }, 190 "url": &schema.Schema{ 191 Type: schema.TypeString, 192 Optional: true, 193 }, 194 "application": &schema.Schema{ 195 Type: schema.TypeString, 196 Optional: true, 197 }, 198 "environment": &schema.Schema{ 199 Type: schema.TypeString, 200 Optional: true, 201 }, 202 "override_name": &schema.Schema{ 203 Type: schema.TypeString, 204 Optional: true, 205 }, 206 "service_key": &schema.Schema{ 207 Type: schema.TypeString, 208 Optional: true, 209 }, 210 }, 211 }, 212 Optional: true, 213 }, 214 }, 215 ApplyFunc: applyFn, 216 ValidateFunc: validateFn, 217 } 218 } 219 220 func applyFn(ctx context.Context) error { 221 o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput) 222 s := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState) 223 d := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData) 224 225 p, err := decodeConfig(d) 226 if err != nil { 227 return err 228 } 229 230 comm, err := communicator.New(s) 231 if err != nil { 232 return err 233 } 234 235 retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout()) 236 defer cancel() 237 238 err = communicator.Retry(retryCtx, func() error { 239 return comm.Connect(o) 240 }) 241 242 if err != nil { 243 return err 244 } 245 defer comm.Disconnect() 246 247 if !p.SkipInstall { 248 o.Output("Installing habitat...") 249 if err := p.installHab(o, comm); err != nil { 250 return err 251 } 252 } 253 254 if p.RingKeyContent != "" { 255 o.Output("Uploading supervisor ring key...") 256 if err := p.uploadRingKey(o, comm); err != nil { 257 return err 258 } 259 } 260 261 o.Output("Starting the habitat supervisor...") 262 if err := p.startHab(o, comm); err != nil { 263 return err 264 } 265 266 if p.Services != nil { 267 for _, service := range p.Services { 268 o.Output("Starting service: " + service.Name) 269 if err := p.startHabService(o, comm, service); err != nil { 270 return err 271 } 272 } 273 } 274 275 return nil 276 } 277 278 func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) { 279 serviceType, ok := c.Get("service_type") 280 if ok { 281 if !serviceTypes[serviceType.(string)] { 282 es = append(es, errors.New(serviceType.(string)+" is not a valid service_type.")) 283 } 284 } 285 286 builderURL, ok := c.Get("url") 287 if ok { 288 if _, err := url.ParseRequestURI(builderURL.(string)); err != nil { 289 es = append(es, errors.New(builderURL.(string)+" is not a valid URL.")) 290 } 291 } 292 293 // Validate service level configs 294 services, ok := c.Get("service") 295 if ok { 296 for _, service := range services.([]map[string]interface{}) { 297 strategy, ok := service["strategy"].(string) 298 if ok && !updateStrategies[strategy] { 299 es = append(es, errors.New(strategy+" is not a valid update strategy.")) 300 } 301 302 topology, ok := service["topology"].(string) 303 if ok && !topologies[topology] { 304 es = append(es, errors.New(topology+" is not a valid topology")) 305 } 306 307 builderURL, ok := service["url"].(string) 308 if ok { 309 if _, err := url.ParseRequestURI(builderURL); err != nil { 310 es = append(es, errors.New(builderURL+" is not a valid URL.")) 311 } 312 } 313 } 314 } 315 return ws, es 316 } 317 318 type Service struct { 319 Name string 320 Strategy string 321 Topology string 322 Channel string 323 Group string 324 URL string 325 Binds []Bind 326 BindStrings []string 327 UserTOML string 328 AppName string 329 Environment string 330 OverrideName string 331 ServiceGroupKey string 332 } 333 334 type Bind struct { 335 Alias string 336 Service string 337 Group string 338 } 339 340 func (s *Service) getPackageName(fullName string) string { 341 return strings.Split(fullName, "/")[1] 342 } 343 344 func (b *Bind) toBindString() string { 345 return fmt.Sprintf("%s:%s.%s", b.Alias, b.Service, b.Group) 346 } 347 348 func decodeConfig(d *schema.ResourceData) (*provisioner, error) { 349 p := &provisioner{ 350 Version: d.Get("version").(string), 351 Peer: d.Get("peer").(string), 352 Services: getServices(d.Get("service").(*schema.Set).List()), 353 UseSudo: d.Get("use_sudo").(bool), 354 ServiceType: d.Get("service_type").(string), 355 ServiceName: d.Get("service_name").(string), 356 RingKey: d.Get("ring_key").(string), 357 RingKeyContent: d.Get("ring_key_content").(string), 358 PermanentPeer: d.Get("permanent_peer").(bool), 359 ListenGossip: d.Get("listen_gossip").(string), 360 ListenHTTP: d.Get("listen_http").(string), 361 URL: d.Get("url").(string), 362 Channel: d.Get("channel").(string), 363 Events: d.Get("events").(string), 364 OverrideName: d.Get("override_name").(string), 365 Organization: d.Get("organization").(string), 366 BuilderAuthToken: d.Get("builder_auth_token").(string), 367 } 368 369 return p, nil 370 } 371 372 func getServices(v []interface{}) []Service { 373 services := make([]Service, 0, len(v)) 374 for _, rawServiceData := range v { 375 serviceData := rawServiceData.(map[string]interface{}) 376 name := (serviceData["name"].(string)) 377 strategy := (serviceData["strategy"].(string)) 378 topology := (serviceData["topology"].(string)) 379 channel := (serviceData["channel"].(string)) 380 group := (serviceData["group"].(string)) 381 url := (serviceData["url"].(string)) 382 app := (serviceData["application"].(string)) 383 env := (serviceData["environment"].(string)) 384 override := (serviceData["override_name"].(string)) 385 userToml := (serviceData["user_toml"].(string)) 386 serviceGroupKey := (serviceData["service_key"].(string)) 387 var bindStrings []string 388 binds := getBinds(serviceData["bind"].(*schema.Set).List()) 389 for _, b := range serviceData["binds"].([]interface{}) { 390 bind, err := getBindFromString(b.(string)) 391 if err != nil { 392 return nil 393 } 394 binds = append(binds, bind) 395 } 396 397 service := Service{ 398 Name: name, 399 Strategy: strategy, 400 Topology: topology, 401 Channel: channel, 402 Group: group, 403 URL: url, 404 UserTOML: userToml, 405 BindStrings: bindStrings, 406 Binds: binds, 407 AppName: app, 408 Environment: env, 409 OverrideName: override, 410 ServiceGroupKey: serviceGroupKey, 411 } 412 services = append(services, service) 413 } 414 return services 415 } 416 417 func getBinds(v []interface{}) []Bind { 418 binds := make([]Bind, 0, len(v)) 419 for _, rawBindData := range v { 420 bindData := rawBindData.(map[string]interface{}) 421 alias := bindData["alias"].(string) 422 service := bindData["service"].(string) 423 group := bindData["group"].(string) 424 bind := Bind{ 425 Alias: alias, 426 Service: service, 427 Group: group, 428 } 429 binds = append(binds, bind) 430 } 431 return binds 432 } 433 434 func (p *provisioner) uploadRingKey(o terraform.UIOutput, comm communicator.Communicator) error { 435 command := fmt.Sprintf("echo '%s' | hab ring key import", p.RingKeyContent) 436 if p.UseSudo { 437 command = fmt.Sprintf("echo '%s' | sudo hab ring key import", p.RingKeyContent) 438 } 439 return p.runCommand(o, comm, command) 440 } 441 442 func (p *provisioner) installHab(o terraform.UIOutput, comm communicator.Communicator) error { 443 // Build the install command 444 command := fmt.Sprintf("curl -L0 %s > install.sh", installURL) 445 if err := p.runCommand(o, comm, command); err != nil { 446 return err 447 } 448 449 // Run the install script 450 if p.Version == "" { 451 command = fmt.Sprintf("env HAB_NONINTERACTIVE=true bash ./install.sh ") 452 } else { 453 command = fmt.Sprintf("env HAB_NONINTERACTIVE=true bash ./install.sh -v %s", p.Version) 454 } 455 456 if p.UseSudo { 457 command = fmt.Sprintf("sudo %s", command) 458 } 459 460 if err := p.runCommand(o, comm, command); err != nil { 461 return err 462 } 463 464 if err := p.createHabUser(o, comm); err != nil { 465 return err 466 } 467 468 return p.runCommand(o, comm, fmt.Sprintf("rm -f install.sh")) 469 } 470 471 func (p *provisioner) startHab(o terraform.UIOutput, comm communicator.Communicator) error { 472 // Install the supervisor first 473 var command string 474 if p.Version == "" { 475 command += fmt.Sprintf("hab install core/hab-sup") 476 } else { 477 command += fmt.Sprintf("hab install core/hab-sup/%s", p.Version) 478 } 479 480 if p.UseSudo { 481 command = fmt.Sprintf("sudo -E %s", command) 482 } 483 484 command = fmt.Sprintf("env HAB_NONINTERACTIVE=true %s", command) 485 486 if err := p.runCommand(o, comm, command); err != nil { 487 return err 488 } 489 490 // Build up sup options 491 options := "" 492 if p.PermanentPeer { 493 options += " -I" 494 } 495 496 if p.ListenGossip != "" { 497 options += fmt.Sprintf(" --listen-gossip %s", p.ListenGossip) 498 } 499 500 if p.ListenHTTP != "" { 501 options += fmt.Sprintf(" --listen-http %s", p.ListenHTTP) 502 } 503 504 if p.Peer != "" { 505 options += fmt.Sprintf(" --peer %s", p.Peer) 506 } 507 508 if p.RingKey != "" { 509 options += fmt.Sprintf(" --ring %s", p.RingKey) 510 } 511 512 if p.URL != "" { 513 options += fmt.Sprintf(" --url %s", p.URL) 514 } 515 516 if p.Channel != "" { 517 options += fmt.Sprintf(" --channel %s", p.Channel) 518 } 519 520 if p.Events != "" { 521 options += fmt.Sprintf(" --events %s", p.Events) 522 } 523 524 if p.OverrideName != "" { 525 options += fmt.Sprintf(" --override-name %s", p.OverrideName) 526 } 527 528 if p.Organization != "" { 529 options += fmt.Sprintf(" --org %s", p.Organization) 530 } 531 532 p.SupOptions = options 533 534 switch p.ServiceType { 535 case "unmanaged": 536 return p.startHabUnmanaged(o, comm, options) 537 case "systemd": 538 return p.startHabSystemd(o, comm, options) 539 default: 540 return errors.New("Unsupported service type") 541 } 542 } 543 544 func (p *provisioner) startHabUnmanaged(o terraform.UIOutput, comm communicator.Communicator, options string) error { 545 // Create the sup directory for the log file 546 var command string 547 var token string 548 if p.UseSudo { 549 command = "sudo mkdir -p /hab/sup/default && sudo chmod o+w /hab/sup/default" 550 } else { 551 command = "mkdir -p /hab/sup/default && chmod o+w /hab/sup/default" 552 } 553 if err := p.runCommand(o, comm, command); err != nil { 554 return err 555 } 556 557 if p.BuilderAuthToken != "" { 558 token = fmt.Sprintf("env HAB_AUTH_TOKEN=%s", p.BuilderAuthToken) 559 } 560 561 if p.UseSudo { 562 command = fmt.Sprintf("(%s setsid sudo -E hab sup run %s > /hab/sup/default/sup.log 2>&1 &) ; sleep 1", token, options) 563 } else { 564 command = fmt.Sprintf("(%s setsid hab sup run %s > /hab/sup/default/sup.log 2>&1 <&1 &) ; sleep 1", token, options) 565 } 566 return p.runCommand(o, comm, command) 567 } 568 569 func (p *provisioner) startHabSystemd(o terraform.UIOutput, comm communicator.Communicator, options string) error { 570 // Create a new template and parse the client config into it 571 unitString := template.Must(template.New("hab-supervisor.service").Parse(systemdUnit)) 572 573 var buf bytes.Buffer 574 err := unitString.Execute(&buf, p) 575 if err != nil { 576 return fmt.Errorf("Error executing %s template: %s", "hab-supervisor.service", err) 577 } 578 579 var command string 580 if p.UseSudo { 581 command = fmt.Sprintf("sudo echo '%s' | sudo tee /etc/systemd/system/%s.service > /dev/null", &buf, p.ServiceName) 582 } else { 583 command = fmt.Sprintf("echo '%s' | tee /etc/systemd/system/%s.service > /dev/null", &buf, p.ServiceName) 584 } 585 586 if err := p.runCommand(o, comm, command); err != nil { 587 return err 588 } 589 590 if p.UseSudo { 591 command = fmt.Sprintf("sudo systemctl enable hab-supervisor && sudo systemctl start hab-supervisor") 592 } else { 593 command = fmt.Sprintf("systemctl enable hab-supervisor && systemctl start hab-supervisor") 594 } 595 return p.runCommand(o, comm, command) 596 } 597 598 func (p *provisioner) createHabUser(o terraform.UIOutput, comm communicator.Communicator) error { 599 addUser := false 600 // Install busybox to get us the user tools we need 601 command := fmt.Sprintf("env HAB_NONINTERACTIVE=true hab install core/busybox") 602 if p.UseSudo { 603 command = fmt.Sprintf("sudo %s", command) 604 } 605 if err := p.runCommand(o, comm, command); err != nil { 606 return err 607 } 608 609 // Check for existing hab user 610 command = fmt.Sprintf("hab pkg exec core/busybox id hab") 611 if p.UseSudo { 612 command = fmt.Sprintf("sudo %s", command) 613 } 614 if err := p.runCommand(o, comm, command); err != nil { 615 o.Output("No existing hab user detected, creating...") 616 addUser = true 617 } 618 619 if addUser { 620 command = fmt.Sprintf("hab pkg exec core/busybox adduser -D -g \"\" hab") 621 if p.UseSudo { 622 command = fmt.Sprintf("sudo %s", command) 623 } 624 return p.runCommand(o, comm, command) 625 } 626 627 return nil 628 } 629 630 // In the future we'll remove the dedicated install once the synchronous load feature in hab-sup is 631 // available. Until then we install here to provide output and a noisy failure mechanism because 632 // if you install with the pkg load, it occurs asynchronously and fails quietly. 633 func (p *provisioner) installHabPackage(o terraform.UIOutput, comm communicator.Communicator, service Service) error { 634 var command string 635 options := "" 636 if service.Channel != "" { 637 options += fmt.Sprintf(" --channel %s", service.Channel) 638 } 639 640 if service.URL != "" { 641 options += fmt.Sprintf(" --url %s", service.URL) 642 } 643 if p.UseSudo { 644 command = fmt.Sprintf("env HAB_NONINTERACTIVE=true sudo -E hab pkg install %s %s", service.Name, options) 645 } else { 646 command = fmt.Sprintf("env HAB_NONINTERACTIVE=true hab pkg install %s %s", service.Name, options) 647 } 648 649 if p.BuilderAuthToken != "" { 650 command = fmt.Sprintf("env HAB_AUTH_TOKEN=%s %s", p.BuilderAuthToken, command) 651 } 652 return p.runCommand(o, comm, command) 653 } 654 655 func (p *provisioner) startHabService(o terraform.UIOutput, comm communicator.Communicator, service Service) error { 656 var command string 657 if err := p.installHabPackage(o, comm, service); err != nil { 658 return err 659 } 660 if err := p.uploadUserTOML(o, comm, service); err != nil { 661 return err 662 } 663 664 // Upload service group key 665 if service.ServiceGroupKey != "" { 666 p.uploadServiceGroupKey(o, comm, service.ServiceGroupKey) 667 } 668 669 options := "" 670 if service.Topology != "" { 671 options += fmt.Sprintf(" --topology %s", service.Topology) 672 } 673 674 if service.Strategy != "" { 675 options += fmt.Sprintf(" --strategy %s", service.Strategy) 676 } 677 678 if service.Channel != "" { 679 options += fmt.Sprintf(" --channel %s", service.Channel) 680 } 681 682 if service.URL != "" { 683 options += fmt.Sprintf(" --url %s", service.URL) 684 } 685 686 if service.Group != "" { 687 options += fmt.Sprintf(" --group %s", service.Group) 688 } 689 690 for _, bind := range service.Binds { 691 options += fmt.Sprintf(" --bind %s", bind.toBindString()) 692 } 693 command = fmt.Sprintf("hab svc load %s %s", service.Name, options) 694 if p.UseSudo { 695 command = fmt.Sprintf("sudo -E %s", command) 696 } 697 if p.BuilderAuthToken != "" { 698 command = fmt.Sprintf("env HAB_AUTH_TOKEN=%s %s", p.BuilderAuthToken, command) 699 } 700 return p.runCommand(o, comm, command) 701 } 702 703 func (p *provisioner) uploadServiceGroupKey(o terraform.UIOutput, comm communicator.Communicator, key string) error { 704 keyName := strings.Split(key, "\n")[1] 705 o.Output("Uploading service group key: " + keyName) 706 keyFileName := fmt.Sprintf("%s.box.key", keyName) 707 destPath := path.Join("/hab/cache/keys", keyFileName) 708 keyContent := strings.NewReader(key) 709 if p.UseSudo { 710 tempPath := path.Join("/tmp", keyFileName) 711 if err := comm.Upload(tempPath, keyContent); err != nil { 712 return err 713 } 714 command := fmt.Sprintf("sudo mv %s %s", tempPath, destPath) 715 return p.runCommand(o, comm, command) 716 } 717 718 return comm.Upload(destPath, keyContent) 719 } 720 721 func (p *provisioner) uploadUserTOML(o terraform.UIOutput, comm communicator.Communicator, service Service) error { 722 // Create the hab svc directory to lay down the user.toml before loading the service 723 o.Output("Uploading user.toml for service: " + service.Name) 724 destDir := fmt.Sprintf("/hab/svc/%s", service.getPackageName(service.Name)) 725 command := fmt.Sprintf("mkdir -p %s", destDir) 726 if p.UseSudo { 727 command = fmt.Sprintf("sudo %s", command) 728 } 729 if err := p.runCommand(o, comm, command); err != nil { 730 return err 731 } 732 733 userToml := strings.NewReader(service.UserTOML) 734 735 if p.UseSudo { 736 if err := comm.Upload("/tmp/user.toml", userToml); err != nil { 737 return err 738 } 739 command = fmt.Sprintf("sudo mv /tmp/user.toml %s", destDir) 740 return p.runCommand(o, comm, command) 741 } 742 743 return comm.Upload(path.Join(destDir, "user.toml"), userToml) 744 745 } 746 747 func (p *provisioner) copyOutput(o terraform.UIOutput, r io.Reader) { 748 lr := linereader.New(r) 749 for line := range lr.Ch { 750 o.Output(line) 751 } 752 } 753 754 func (p *provisioner) runCommand(o terraform.UIOutput, comm communicator.Communicator, command string) error { 755 outR, outW := io.Pipe() 756 errR, errW := io.Pipe() 757 758 go p.copyOutput(o, outR) 759 go p.copyOutput(o, errR) 760 defer outW.Close() 761 defer errW.Close() 762 763 cmd := &remote.Cmd{ 764 Command: command, 765 Stdout: outW, 766 Stderr: errW, 767 } 768 769 if err := comm.Start(cmd); err != nil { 770 return fmt.Errorf("Error executing command %q: %v", cmd.Command, err) 771 } 772 773 if err := cmd.Wait(); err != nil { 774 return err 775 } 776 777 return nil 778 } 779 780 func getBindFromString(bind string) (Bind, error) { 781 t := strings.FieldsFunc(bind, func(d rune) bool { 782 switch d { 783 case ':', '.': 784 return true 785 } 786 return false 787 }) 788 if len(t) != 3 { 789 return Bind{}, errors.New("Invalid bind specification: " + bind) 790 } 791 return Bind{Alias: t[0], Service: t[1], Group: t[2]}, nil 792 }