573 lines
15 KiB
Go
573 lines
15 KiB
Go
package habitat
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"strings"
|
|
|
|
version "github.com/hashicorp/go-version"
|
|
"github.com/hashicorp/terraform/communicator"
|
|
"github.com/hashicorp/terraform/communicator/remote"
|
|
"github.com/hashicorp/terraform/configs/hcl2shim"
|
|
"github.com/hashicorp/terraform/helper/schema"
|
|
"github.com/hashicorp/terraform/helper/validation"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
"github.com/mitchellh/go-linereader"
|
|
)
|
|
|
|
type provisioner struct {
|
|
Version string
|
|
AutoUpdate bool
|
|
HttpDisable bool
|
|
Services []Service
|
|
PermanentPeer bool
|
|
ListenCtl string
|
|
ListenGossip string
|
|
ListenHTTP string
|
|
Peer string
|
|
Peers []string
|
|
RingKey string
|
|
RingKeyContent string
|
|
CtlSecret string
|
|
SkipInstall bool
|
|
UseSudo bool
|
|
ServiceType string
|
|
ServiceName string
|
|
URL string
|
|
Channel string
|
|
Events string
|
|
Organization string
|
|
GatewayAuthToken string
|
|
BuilderAuthToken string
|
|
SupOptions string
|
|
AcceptLicense bool
|
|
|
|
installHabitat provisionFn
|
|
startHabitat provisionFn
|
|
uploadRingKey provisionFn
|
|
uploadCtlSecret provisionFn
|
|
startHabitatService provisionServiceFn
|
|
|
|
osType string
|
|
}
|
|
|
|
type provisionFn func(terraform.UIOutput, communicator.Communicator) error
|
|
type provisionServiceFn func(terraform.UIOutput, communicator.Communicator, Service) error
|
|
|
|
func Provisioner() terraform.ResourceProvisioner {
|
|
return &schema.Provisioner{
|
|
Schema: map[string]*schema.Schema{
|
|
"version": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"auto_update": &schema.Schema{
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Default: false,
|
|
},
|
|
"http_disable": &schema.Schema{
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Default: false,
|
|
},
|
|
"peer": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"peers": &schema.Schema{
|
|
Type: schema.TypeList,
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
Optional: true,
|
|
},
|
|
"service_type": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Default: "systemd",
|
|
ValidateFunc: validation.StringInSlice([]string{"systemd", "unmanaged"}, false),
|
|
},
|
|
"service_name": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Default: "hab-supervisor",
|
|
},
|
|
"use_sudo": &schema.Schema{
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Default: true,
|
|
},
|
|
"accept_license": &schema.Schema{
|
|
Type: schema.TypeBool,
|
|
Required: true,
|
|
},
|
|
"permanent_peer": &schema.Schema{
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Default: false,
|
|
},
|
|
"listen_ctl": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"listen_gossip": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"listen_http": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"ring_key": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"ring_key_content": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"ctl_secret": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"url": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) {
|
|
u, err := url.Parse(val.(string))
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("invalid URL specified for %q: %v", key, err))
|
|
}
|
|
|
|
if u.Scheme == "" {
|
|
errs = append(errs, fmt.Errorf("invalid URL specified for %q (scheme must be specified)", key))
|
|
}
|
|
|
|
return warns, errs
|
|
},
|
|
},
|
|
"channel": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"events": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"organization": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"gateway_auth_token": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"builder_auth_token": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"service": &schema.Schema{
|
|
Type: schema.TypeSet,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"name": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
},
|
|
"binds": &schema.Schema{
|
|
Type: schema.TypeList,
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
Optional: true,
|
|
},
|
|
"bind": &schema.Schema{
|
|
Type: schema.TypeSet,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"alias": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
},
|
|
"service": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
},
|
|
"group": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
"topology": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ValidateFunc: validation.StringInSlice([]string{"leader", "standalone"}, false),
|
|
},
|
|
"user_toml": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"strategy": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ValidateFunc: validation.StringInSlice([]string{"none", "rolling", "at-once"}, false),
|
|
},
|
|
"channel": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"group": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"url": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) {
|
|
u, err := url.Parse(val.(string))
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("invalid URL specified for %q: %v", key, err))
|
|
}
|
|
|
|
if u.Scheme == "" {
|
|
errs = append(errs, fmt.Errorf("invalid URL specified for %q (scheme must be specified)", key))
|
|
}
|
|
|
|
return warns, errs
|
|
},
|
|
},
|
|
"application": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"environment": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"service_key": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
Optional: true,
|
|
},
|
|
},
|
|
ApplyFunc: applyFn,
|
|
ValidateFunc: validateFn,
|
|
}
|
|
}
|
|
|
|
func applyFn(ctx context.Context) error {
|
|
o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
|
|
s := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
|
|
d := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
|
|
|
|
p, err := decodeConfig(d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Automatically determine the OS type
|
|
switch t := s.Ephemeral.ConnInfo["type"]; t {
|
|
case "ssh", "":
|
|
p.osType = "linux"
|
|
case "winrm":
|
|
p.osType = "windows"
|
|
default:
|
|
return fmt.Errorf("unsupported connection type: %s", t)
|
|
}
|
|
|
|
switch p.osType {
|
|
case "linux":
|
|
p.installHabitat = p.linuxInstallHabitat
|
|
p.uploadRingKey = p.linuxUploadRingKey
|
|
p.uploadCtlSecret = p.linuxUploadCtlSecret
|
|
p.startHabitat = p.linuxStartHabitat
|
|
p.startHabitatService = p.linuxStartHabitatService
|
|
case "windows":
|
|
return fmt.Errorf("windows is not supported yet for the habitat provisioner")
|
|
default:
|
|
return fmt.Errorf("unsupported os type: %s", p.osType)
|
|
}
|
|
|
|
// Get a new communicator
|
|
comm, err := communicator.New(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
|
|
defer cancel()
|
|
|
|
// Wait and retry until we establish the connection
|
|
err = communicator.Retry(retryCtx, func() error {
|
|
return comm.Connect(o)
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer comm.Disconnect()
|
|
|
|
if !p.SkipInstall {
|
|
o.Output("Installing habitat...")
|
|
if err := p.installHabitat(o, comm); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if p.RingKeyContent != "" {
|
|
o.Output("Uploading supervisor ring key...")
|
|
if err := p.uploadRingKey(o, comm); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if p.CtlSecret != "" {
|
|
o.Output("Uploading ctl secret...")
|
|
if err := p.uploadCtlSecret(o, comm); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
o.Output("Starting the habitat supervisor...")
|
|
if err := p.startHabitat(o, comm); err != nil {
|
|
return err
|
|
}
|
|
|
|
if p.Services != nil {
|
|
for _, service := range p.Services {
|
|
o.Output("Starting service: " + service.Name)
|
|
if err := p.startHabitatService(o, comm, service); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) {
|
|
ringKeyContent, ok := c.Get("ring_key_content")
|
|
if ok && ringKeyContent != "" && ringKeyContent != hcl2shim.UnknownVariableValue {
|
|
ringKey, ringOk := c.Get("ring_key")
|
|
if ringOk && ringKey == "" {
|
|
es = append(es, errors.New("if ring_key_content is specified, ring_key must be specified as well"))
|
|
}
|
|
}
|
|
|
|
v, ok := c.Get("version")
|
|
if ok && v != nil && strings.TrimSpace(v.(string)) != "" {
|
|
if _, err := version.NewVersion(v.(string)); err != nil {
|
|
es = append(es, errors.New(v.(string)+" is not a valid version."))
|
|
}
|
|
}
|
|
|
|
acceptLicense, ok := c.Get("accept_license")
|
|
if ok && !acceptLicense.(bool) {
|
|
if v != nil && strings.TrimSpace(v.(string)) != "" {
|
|
versionOld, _ := version.NewVersion("0.79.0")
|
|
versionRequired, _ := version.NewVersion(v.(string))
|
|
if versionRequired.GreaterThan(versionOld) {
|
|
es = append(es, errors.New("Habitat end user license agreement needs to be accepted, set the accept_license argument to true to accept"))
|
|
}
|
|
} else { // blank means latest version
|
|
es = append(es, errors.New("Habitat end user license agreement needs to be accepted, set the accept_license argument to true to accept"))
|
|
}
|
|
}
|
|
|
|
// Validate service level configs
|
|
services, ok := c.Get("service")
|
|
if ok {
|
|
data, dataOk := services.(string)
|
|
if dataOk {
|
|
es = append(es, fmt.Errorf("service '%v': must be a block", data))
|
|
}
|
|
}
|
|
|
|
return ws, es
|
|
}
|
|
|
|
type Service struct {
|
|
Name string
|
|
Strategy string
|
|
Topology string
|
|
Channel string
|
|
Group string
|
|
URL string
|
|
Binds []Bind
|
|
BindStrings []string
|
|
UserTOML string
|
|
AppName string
|
|
Environment string
|
|
ServiceGroupKey string
|
|
}
|
|
|
|
func (s *Service) getPackageName(fullName string) string {
|
|
return strings.Split(fullName, "/")[1]
|
|
}
|
|
|
|
func (s *Service) getServiceNameChecksum() string {
|
|
return fmt.Sprintf("%x", sha256.Sum256([]byte(s.Name)))
|
|
}
|
|
|
|
type Bind struct {
|
|
Alias string
|
|
Service string
|
|
Group string
|
|
}
|
|
|
|
func (b *Bind) toBindString() string {
|
|
return fmt.Sprintf("%s:%s.%s", b.Alias, b.Service, b.Group)
|
|
}
|
|
|
|
func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
|
|
p := &provisioner{
|
|
Version: d.Get("version").(string),
|
|
AutoUpdate: d.Get("auto_update").(bool),
|
|
HttpDisable: d.Get("http_disable").(bool),
|
|
Peer: d.Get("peer").(string),
|
|
Peers: getPeers(d.Get("peers").([]interface{})),
|
|
Services: getServices(d.Get("service").(*schema.Set).List()),
|
|
UseSudo: d.Get("use_sudo").(bool),
|
|
AcceptLicense: d.Get("accept_license").(bool),
|
|
ServiceType: d.Get("service_type").(string),
|
|
ServiceName: d.Get("service_name").(string),
|
|
RingKey: d.Get("ring_key").(string),
|
|
RingKeyContent: d.Get("ring_key_content").(string),
|
|
CtlSecret: d.Get("ctl_secret").(string),
|
|
PermanentPeer: d.Get("permanent_peer").(bool),
|
|
ListenCtl: d.Get("listen_ctl").(string),
|
|
ListenGossip: d.Get("listen_gossip").(string),
|
|
ListenHTTP: d.Get("listen_http").(string),
|
|
URL: d.Get("url").(string),
|
|
Channel: d.Get("channel").(string),
|
|
Events: d.Get("events").(string),
|
|
Organization: d.Get("organization").(string),
|
|
BuilderAuthToken: d.Get("builder_auth_token").(string),
|
|
GatewayAuthToken: d.Get("gateway_auth_token").(string),
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
func getPeers(v []interface{}) []string {
|
|
peers := make([]string, 0, len(v))
|
|
for _, rawPeerData := range v {
|
|
peers = append(peers, rawPeerData.(string))
|
|
}
|
|
return peers
|
|
}
|
|
|
|
func getServices(v []interface{}) []Service {
|
|
services := make([]Service, 0, len(v))
|
|
for _, rawServiceData := range v {
|
|
serviceData := rawServiceData.(map[string]interface{})
|
|
name := (serviceData["name"].(string))
|
|
strategy := (serviceData["strategy"].(string))
|
|
topology := (serviceData["topology"].(string))
|
|
channel := (serviceData["channel"].(string))
|
|
group := (serviceData["group"].(string))
|
|
url := (serviceData["url"].(string))
|
|
app := (serviceData["application"].(string))
|
|
env := (serviceData["environment"].(string))
|
|
userToml := (serviceData["user_toml"].(string))
|
|
serviceGroupKey := (serviceData["service_key"].(string))
|
|
var bindStrings []string
|
|
binds := getBinds(serviceData["bind"].(*schema.Set).List())
|
|
for _, b := range serviceData["binds"].([]interface{}) {
|
|
bind, err := getBindFromString(b.(string))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
binds = append(binds, bind)
|
|
}
|
|
|
|
service := Service{
|
|
Name: name,
|
|
Strategy: strategy,
|
|
Topology: topology,
|
|
Channel: channel,
|
|
Group: group,
|
|
URL: url,
|
|
UserTOML: userToml,
|
|
BindStrings: bindStrings,
|
|
Binds: binds,
|
|
AppName: app,
|
|
Environment: env,
|
|
ServiceGroupKey: serviceGroupKey,
|
|
}
|
|
services = append(services, service)
|
|
}
|
|
return services
|
|
}
|
|
|
|
func getBinds(v []interface{}) []Bind {
|
|
binds := make([]Bind, 0, len(v))
|
|
for _, rawBindData := range v {
|
|
bindData := rawBindData.(map[string]interface{})
|
|
alias := bindData["alias"].(string)
|
|
service := bindData["service"].(string)
|
|
group := bindData["group"].(string)
|
|
bind := Bind{
|
|
Alias: alias,
|
|
Service: service,
|
|
Group: group,
|
|
}
|
|
binds = append(binds, bind)
|
|
}
|
|
return binds
|
|
}
|
|
|
|
func (p *provisioner) copyOutput(o terraform.UIOutput, r io.Reader) {
|
|
lr := linereader.New(r)
|
|
for line := range lr.Ch {
|
|
o.Output(line)
|
|
}
|
|
}
|
|
|
|
func (p *provisioner) runCommand(o terraform.UIOutput, comm communicator.Communicator, command string) error {
|
|
outR, outW := io.Pipe()
|
|
errR, errW := io.Pipe()
|
|
|
|
go p.copyOutput(o, outR)
|
|
go p.copyOutput(o, errR)
|
|
defer outW.Close()
|
|
defer errW.Close()
|
|
|
|
cmd := &remote.Cmd{
|
|
Command: command,
|
|
Stdout: outW,
|
|
Stderr: errW,
|
|
}
|
|
|
|
if err := comm.Start(cmd); err != nil {
|
|
return fmt.Errorf("error executing command %q: %v", cmd.Command, err)
|
|
}
|
|
|
|
if err := cmd.Wait(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getBindFromString(bind string) (Bind, error) {
|
|
t := strings.FieldsFunc(bind, func(d rune) bool {
|
|
switch d {
|
|
case ':', '.':
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
if len(t) != 3 {
|
|
return Bind{}, errors.New("invalid bind specification: " + bind)
|
|
}
|
|
return Bind{Alias: t[0], Service: t[1], Group: t[2]}, nil
|
|
}
|