Merge pull request #22705 from kmott/habitat-provisioner-updates

Habitat provisioner updates
This commit is contained in:
Pam Selle 2019-10-01 14:27:52 -04:00 committed by GitHub
commit b40385772e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 904 additions and 425 deletions

View File

@ -0,0 +1,376 @@
package habitat
import (
"bytes"
"errors"
"fmt"
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/terraform"
"path"
"path/filepath"
"strings"
"text/template"
)
const installURL = "https://raw.githubusercontent.com/habitat-sh/habitat/master/components/hab/install.sh"
const systemdUnit = `[Unit]
Description=Habitat Supervisor
[Service]
ExecStart=/bin/hab sup run{{ .SupOptions }}
Restart=on-failure
{{ if .GatewayAuthToken -}}
Environment="HAB_SUP_GATEWAY_AUTH_TOKEN={{ .GatewayAuthToken }}"
{{ end -}}
{{ if .BuilderAuthToken -}}
Environment="HAB_AUTH_TOKEN={{ .BuilderAuthToken }}"
{{ end -}}
[Install]
WantedBy=default.target
`
func (p *provisioner) linuxInstallHabitat(o terraform.UIOutput, comm communicator.Communicator) error {
// Download the hab installer
if err := p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("curl --silent -L0 %s > install.sh", installURL))); err != nil {
return err
}
// Run the install script
var command string
if p.Version == "" {
command = fmt.Sprintf("bash ./install.sh ")
} else {
command = fmt.Sprintf("bash ./install.sh -v %s", p.Version)
}
if err := p.runCommand(o, comm, p.linuxGetCommand(command)); err != nil {
return err
}
// Accept the license
if p.AcceptLicense {
var cmd string
if p.UseSudo == true {
cmd = "env HAB_LICENSE=accept sudo -E /bin/bash -c 'hab -V'"
} else {
cmd = "env HAB_LICENSE=accept /bin/bash -c 'hab -V'"
}
if err := p.runCommand(o, comm, cmd); err != nil {
return err
}
}
// Create the hab user
if err := p.createHabUser(o, comm); err != nil {
return err
}
// Cleanup the installer
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("rm -f install.sh")))
}
func (p *provisioner) createHabUser(o terraform.UIOutput, comm communicator.Communicator) error {
var addUser bool
// Install busybox to get us the user tools we need
if err := p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("hab install core/busybox"))); err != nil {
return err
}
// Check for existing hab user
if err := p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("hab pkg exec core/busybox id hab"))); err != nil {
o.Output("No existing hab user detected, creating...")
addUser = true
}
if addUser {
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("hab pkg exec core/busybox adduser -D -g \"\" hab")))
}
return nil
}
func (p *provisioner) linuxStartHabitat(o terraform.UIOutput, comm communicator.Communicator) error {
// Install the supervisor first
var command string
if p.Version == "" {
command += p.linuxGetCommand(fmt.Sprintf("hab install core/hab-sup"))
} else {
command += p.linuxGetCommand(fmt.Sprintf("hab install core/hab-sup/%s", p.Version))
}
if err := p.runCommand(o, comm, command); err != nil {
return err
}
// Build up supervisor options
options := ""
if p.PermanentPeer {
options += " --permanent-peer"
}
if p.ListenCtl != "" {
options += fmt.Sprintf(" --listen-ctl %s", p.ListenCtl)
}
if p.ListenGossip != "" {
options += fmt.Sprintf(" --listen-gossip %s", p.ListenGossip)
}
if p.ListenHTTP != "" {
options += fmt.Sprintf(" --listen-http %s", p.ListenHTTP)
}
if p.Peer != "" {
options += fmt.Sprintf(" %s", p.Peer)
}
if len(p.Peers) > 0 {
if len(p.Peers) == 1 {
options += fmt.Sprintf(" --peer %s", p.Peers[0])
} else {
options += fmt.Sprintf(" --peer %s", strings.Join(p.Peers, " --peer "))
}
}
if p.RingKey != "" {
options += fmt.Sprintf(" --ring %s", p.RingKey)
}
if p.URL != "" {
options += fmt.Sprintf(" --url %s", p.URL)
}
if p.Channel != "" {
options += fmt.Sprintf(" --channel %s", p.Channel)
}
if p.Events != "" {
options += fmt.Sprintf(" --events %s", p.Events)
}
if p.Organization != "" {
options += fmt.Sprintf(" --org %s", p.Organization)
}
if p.HttpDisable == true {
options += fmt.Sprintf(" --http-disable")
}
if p.AutoUpdate == true {
options += fmt.Sprintf(" --auto-update")
}
p.SupOptions = options
// Start hab depending on service type
switch p.ServiceType {
case "unmanaged":
return p.linuxStartHabitatUnmanaged(o, comm, options)
case "systemd":
return p.linuxStartHabitatSystemd(o, comm, options)
default:
return errors.New("unsupported service type")
}
}
// This func is a little different than the others since we need to expose HAB_AUTH_TOKEN to a shell
// sub-process that's actually running the supervisor.
func (p *provisioner) linuxStartHabitatUnmanaged(o terraform.UIOutput, comm communicator.Communicator, options string) error {
var token string
// Create the sup directory for the log file
if err := p.runCommand(o, comm, p.linuxGetCommand("mkdir -p /hab/sup/default && chmod o+w /hab/sup/default")); err != nil {
return err
}
// Set HAB_AUTH_TOKEN if provided
if p.BuilderAuthToken != "" {
token = fmt.Sprintf("env HAB_AUTH_TOKEN=%s ", p.BuilderAuthToken)
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("(%ssetsid hab sup run%s > /hab/sup/default/sup.log 2>&1 <&1 &) ; sleep 1", token, options)))
}
func (p *provisioner) linuxStartHabitatSystemd(o terraform.UIOutput, comm communicator.Communicator, options string) error {
// Create a new template and parse the client config into it
unitString := template.Must(template.New("hab-supervisor.service").Parse(systemdUnit))
var buf bytes.Buffer
err := unitString.Execute(&buf, p)
if err != nil {
return fmt.Errorf("error executing %s.service template: %s", p.ServiceName, err)
}
if err := p.linuxUploadSystemdUnit(o, comm, &buf); err != nil {
return err
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("systemctl enable %s && systemctl start %s", p.ServiceName, p.ServiceName)))
}
func (p *provisioner) linuxUploadSystemdUnit(o terraform.UIOutput, comm communicator.Communicator, contents *bytes.Buffer) error {
destination := fmt.Sprintf("/etc/systemd/system/%s.service", p.ServiceName)
if p.UseSudo {
tempPath := fmt.Sprintf("/tmp/%s.service", p.ServiceName)
if err := comm.Upload(tempPath, contents); err != nil {
return err
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("mv %s %s", tempPath, destination)))
}
return comm.Upload(destination, contents)
}
func (p *provisioner) linuxUploadRingKey(o terraform.UIOutput, comm communicator.Communicator) error {
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf(`echo -e "%s" | hab ring key import`, p.RingKeyContent)))
}
func (p *provisioner) linuxUploadCtlSecret(o terraform.UIOutput, comm communicator.Communicator) error {
destination := fmt.Sprintf("/hab/sup/default/CTL_SECRET")
// Create the destination directory
err := p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("mkdir -p %s", filepath.Dir(destination))))
if err != nil {
return err
}
keyContent := strings.NewReader(p.CtlSecret)
if p.UseSudo {
tempPath := fmt.Sprintf("/tmp/CTL_SECRET")
if err := comm.Upload(tempPath, keyContent); err != nil {
return err
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("mv %s %s && chown root:root %s && chmod 0600 %s", tempPath, destination, destination, destination)))
}
return comm.Upload(destination, keyContent)
}
//
// Habitat Services
//
func (p *provisioner) linuxStartHabitatService(o terraform.UIOutput, comm communicator.Communicator, service Service) error {
var options string
if err := p.linuxInstallHabitatPackage(o, comm, service); err != nil {
return err
}
if err := p.uploadUserTOML(o, comm, service); err != nil {
return err
}
// Upload service group key
if service.ServiceGroupKey != "" {
err := p.uploadServiceGroupKey(o, comm, service.ServiceGroupKey)
if err != nil {
return err
}
}
if service.Topology != "" {
options += fmt.Sprintf(" --topology %s", service.Topology)
}
if service.Strategy != "" {
options += fmt.Sprintf(" --strategy %s", service.Strategy)
}
if service.Channel != "" {
options += fmt.Sprintf(" --channel %s", service.Channel)
}
if service.URL != "" {
options += fmt.Sprintf(" --url %s", service.URL)
}
if service.Group != "" {
options += fmt.Sprintf(" --group %s", service.Group)
}
for _, bind := range service.Binds {
options += fmt.Sprintf(" --bind %s", bind.toBindString())
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("hab svc load %s %s", service.Name, options)))
}
// In the future we'll remove the dedicated install once the synchronous load feature in hab-sup is
// available. Until then we install here to provide output and a noisy failure mechanism because
// if you install with the pkg load, it occurs asynchronously and fails quietly.
func (p *provisioner) linuxInstallHabitatPackage(o terraform.UIOutput, comm communicator.Communicator, service Service) error {
var options string
if service.Channel != "" {
options += fmt.Sprintf(" --channel %s", service.Channel)
}
if service.URL != "" {
options += fmt.Sprintf(" --url %s", service.URL)
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("hab pkg install %s %s", service.Name, options)))
}
func (p *provisioner) uploadServiceGroupKey(o terraform.UIOutput, comm communicator.Communicator, key string) error {
keyName := strings.Split(key, "\n")[1]
o.Output("Uploading service group key: " + keyName)
keyFileName := fmt.Sprintf("%s.box.key", keyName)
destPath := path.Join("/hab/cache/keys", keyFileName)
keyContent := strings.NewReader(key)
if p.UseSudo {
tempPath := path.Join("/tmp", keyFileName)
if err := comm.Upload(tempPath, keyContent); err != nil {
return err
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("mv %s %s", tempPath, destPath)))
}
return comm.Upload(destPath, keyContent)
}
func (p *provisioner) uploadUserTOML(o terraform.UIOutput, comm communicator.Communicator, service Service) error {
// Create the hab svc directory to lay down the user.toml before loading the service
o.Output("Uploading user.toml for service: " + service.Name)
destDir := fmt.Sprintf("/hab/user/%s/config", service.getPackageName(service.Name))
command := p.linuxGetCommand(fmt.Sprintf("mkdir -p %s", destDir))
if err := p.runCommand(o, comm, command); err != nil {
return err
}
userToml := strings.NewReader(service.UserTOML)
if p.UseSudo {
if err := comm.Upload(fmt.Sprintf("/tmp/user-%s.toml", service.getServiceNameChecksum()), userToml); err != nil {
return err
}
command = p.linuxGetCommand(fmt.Sprintf("mv /tmp/user-%s.toml %s/user.toml", service.getServiceNameChecksum(), destDir))
return p.runCommand(o, comm, command)
}
return comm.Upload(path.Join(destDir, "user.toml"), userToml)
}
func (p *provisioner) linuxGetCommand(command string) string {
// Always set HAB_NONINTERACTIVE & HAB_NOCOLORING
env := fmt.Sprintf("env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true")
// Set builder auth token
if p.BuilderAuthToken != "" {
env += fmt.Sprintf(" HAB_AUTH_TOKEN=%s", p.BuilderAuthToken)
}
if p.UseSudo {
command = fmt.Sprintf("%s sudo -E /bin/bash -c '%s'", env, command)
} else {
command = fmt.Sprintf("%s /bin/bash -c '%s'", env, command)
}
return command
}

View File

@ -0,0 +1,348 @@
package habitat
import (
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
"testing"
)
const linuxDefaultSystemdUnitFileContents = `[Unit]
Description=Habitat Supervisor
[Service]
ExecStart=/bin/hab sup run --peer host1 --peer 1.2.3.4 --auto-update
Restart=on-failure
[Install]
WantedBy=default.target`
const linuxCustomSystemdUnitFileContents = `[Unit]
Description=Habitat Supervisor
[Service]
ExecStart=/bin/hab sup run --listen-ctl 192.168.0.1:8443 --listen-gossip 192.168.10.1:9443 --listen-http 192.168.20.1:8080 --peer host1 --peer host2 --peer 1.2.3.4 --peer 5.6.7.8 --peer foo.example.com
Restart=on-failure
Environment="HAB_SUP_GATEWAY_AUTH_TOKEN=ea7-beef"
Environment="HAB_AUTH_TOKEN=dead-beef"
[Install]
WantedBy=default.target`
func TestLinuxProvisioner_linuxInstallHabitat(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
Commands map[string]bool
}{
"Installation with sudo": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": true,
"use_sudo": true,
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'curl --silent -L0 https://raw.githubusercontent.com/habitat-sh/habitat/master/components/hab/install.sh > install.sh'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'bash ./install.sh -v 0.79.1'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab install core/busybox'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab pkg exec core/busybox adduser -D -g \"\" hab'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'rm -f install.sh'": true,
},
},
"Installation without sudo": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": true,
"use_sudo": false,
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'curl --silent -L0 https://raw.githubusercontent.com/habitat-sh/habitat/master/components/hab/install.sh > install.sh'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'bash ./install.sh -v 0.79.1'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'hab install core/busybox'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'hab pkg exec core/busybox adduser -D -g \"\" hab'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'rm -f install.sh'": true,
},
},
"Installation with Habitat license acceptance": {
Config: map[string]interface{}{
"version": "0.81.0",
"accept_license": true,
"auto_update": true,
"use_sudo": true,
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'curl --silent -L0 https://raw.githubusercontent.com/habitat-sh/habitat/master/components/hab/install.sh > install.sh'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'bash ./install.sh -v 0.81.0'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab install core/busybox'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab pkg exec core/busybox adduser -D -g \"\" hab'": true,
"env HAB_LICENSE=accept sudo -E /bin/bash -c 'hab -V'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'rm -f install.sh'": true,
},
},
}
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
for k, tc := range cases {
c.Commands = tc.Commands
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
err = p.linuxInstallHabitat(o, c)
if err != nil {
t.Fatalf("Test %q failed: %v", k, err)
}
}
}
func TestLinuxProvisioner_linuxStartHabitat(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
Commands map[string]bool
Uploads map[string]string
}{
"Start systemd Habitat with sudo": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": true,
"use_sudo": true,
"service_name": "hab-sup",
"peer": "--peer host1",
"peers": []interface{}{"1.2.3.4"},
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab install core/hab-sup/0.79.1'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'systemctl enable hab-sup && systemctl start hab-sup'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'mv /tmp/hab-sup.service /etc/systemd/system/hab-sup.service'": true,
},
Uploads: map[string]string{
"/tmp/hab-sup.service": linuxDefaultSystemdUnitFileContents,
},
},
"Start systemd Habitat without sudo": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": true,
"use_sudo": false,
"service_name": "hab-sup",
"peer": "--peer host1",
"peers": []interface{}{"1.2.3.4"},
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'hab install core/hab-sup/0.79.1'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'systemctl enable hab-sup && systemctl start hab-sup'": true,
},
Uploads: map[string]string{
"/etc/systemd/system/hab-sup.service": linuxDefaultSystemdUnitFileContents,
},
},
"Start unmanaged Habitat with sudo": {
Config: map[string]interface{}{
"version": "0.81.0",
"license": "accept-no-persist",
"auto_update": true,
"use_sudo": true,
"service_type": "unmanaged",
"peer": "--peer host1",
"peers": []interface{}{"1.2.3.4"},
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab install core/hab-sup/0.81.0'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'mkdir -p /hab/sup/default && chmod o+w /hab/sup/default'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c '(setsid hab sup run --peer host1 --peer 1.2.3.4 --auto-update > /hab/sup/default/sup.log 2>&1 <&1 &) ; sleep 1'": true,
},
Uploads: map[string]string{
"/etc/systemd/system/hab-sup.service": linuxDefaultSystemdUnitFileContents,
},
},
"Start Habitat with custom config": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": false,
"use_sudo": true,
"service_name": "hab-sup",
"peer": "--peer host1 --peer host2",
"peers": []interface{}{"1.2.3.4", "5.6.7.8", "foo.example.com"},
"listen_ctl": "192.168.0.1:8443",
"listen_gossip": "192.168.10.1:9443",
"listen_http": "192.168.20.1:8080",
"builder_auth_token": "dead-beef",
"gateway_auth_token": "ea7-beef",
"ctl_secret": "bad-beef",
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true HAB_AUTH_TOKEN=dead-beef sudo -E /bin/bash -c 'hab install core/hab-sup/0.79.1'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true HAB_AUTH_TOKEN=dead-beef sudo -E /bin/bash -c 'systemctl enable hab-sup && systemctl start hab-sup'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true HAB_AUTH_TOKEN=dead-beef sudo -E /bin/bash -c 'mv /tmp/hab-sup.service /etc/systemd/system/hab-sup.service'": true,
},
Uploads: map[string]string{
"/tmp/hab-sup.service": linuxCustomSystemdUnitFileContents,
},
},
}
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
for k, tc := range cases {
c.Commands = tc.Commands
c.Uploads = tc.Uploads
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
err = p.linuxStartHabitat(o, c)
if err != nil {
t.Fatalf("Test %q failed: %v", k, err)
}
}
}
func TestLinuxProvisioner_linuxUploadRingKey(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
Commands map[string]bool
}{
"Upload ring key": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": true,
"use_sudo": true,
"service_name": "hab-sup",
"peers": []interface{}{"1.2.3.4"},
"ring_key": "test-ring",
"ring_key_content": "dead-beef",
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'echo -e \"dead-beef\" | hab ring key import'": true,
},
},
}
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
for k, tc := range cases {
c.Commands = tc.Commands
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
err = p.linuxUploadRingKey(o, c)
if err != nil {
t.Fatalf("Test %q failed: %v", k, err)
}
}
}
func TestLinuxProvisioner_linuxStartHabitatService(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
Commands map[string]bool
Uploads map[string]string
}{
"Start Habitat service with sudo": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": false,
"use_sudo": true,
"service_name": "hab-sup",
"peers": []interface{}{"1.2.3.4"},
"ring_key": "test-ring",
"ring_key_content": "dead-beef",
"service": []interface{}{
map[string]interface{}{
"name": "core/foo",
"topology": "standalone",
"strategy": "none",
"channel": "stable",
"user_toml": "[config]\nlisten = 0.0.0.0:8080",
"bind": []interface{}{
map[string]interface{}{
"alias": "backend",
"service": "bar",
"group": "default",
},
},
},
map[string]interface{}{
"name": "core/bar",
"topology": "standalone",
"strategy": "rolling",
"channel": "staging",
"user_toml": "[config]\nlisten = 0.0.0.0:443",
},
},
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab pkg install core/foo --channel stable'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'mkdir -p /hab/user/foo/config'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'mv /tmp/user-a5b83ec1b302d109f41852ae17379f75c36dff9bc598aae76b6f7c9cd425fd76.toml /hab/user/foo/config/user.toml'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab svc load core/foo --topology standalone --strategy none --channel stable --bind backend:bar.default'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab pkg install core/bar --channel staging'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'mkdir -p /hab/user/bar/config'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'mv /tmp/user-6466ae3283ae1bd4737b00367bc676c6465b25682169ea5f7da222f3f078a5bf.toml /hab/user/bar/config/user.toml'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab svc load core/bar --topology standalone --strategy rolling --channel staging'": true,
},
Uploads: map[string]string{
"/tmp/user-a5b83ec1b302d109f41852ae17379f75c36dff9bc598aae76b6f7c9cd425fd76.toml": "[config]\nlisten = 0.0.0.0:8080",
"/tmp/user-6466ae3283ae1bd4737b00367bc676c6465b25682169ea5f7da222f3f078a5bf.toml": "[config]\nlisten = 0.0.0.0:443",
},
},
}
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
for k, tc := range cases {
c.Commands = tc.Commands
c.Uploads = tc.Uploads
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
var errs []error
for _, s := range p.Services {
err = p.linuxStartHabitatService(o, c, s)
if err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
for _, e := range errs {
t.Logf("Test %q failed: %v", k, e)
t.Fail()
}
}
}
}

View File

@ -1,55 +1,38 @@
package habitat package habitat
import ( import (
"bytes"
"context" "context"
"crypto/sha256"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"net/url" "net/url"
"path"
"strings" "strings"
"text/template"
version "github.com/hashicorp/go-version" version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/communicator" "github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/communicator/remote" "github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/configs/hcl2shim"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
linereader "github.com/mitchellh/go-linereader" "github.com/mitchellh/go-linereader"
) )
const installURL = "https://raw.githubusercontent.com/habitat-sh/habitat/master/components/hab/install.sh"
const systemdUnit = `
[Unit]
Description=Habitat Supervisor
[Service]
ExecStart=/bin/hab sup run {{ .SupOptions }}
Restart=on-failure
{{ if .BuilderAuthToken -}}
Environment="HAB_AUTH_TOKEN={{ .BuilderAuthToken }}"
{{ end -}}
[Install]
WantedBy=default.target
`
var serviceTypes = map[string]bool{"unmanaged": true, "systemd": true}
var updateStrategies = map[string]bool{"at-once": true, "rolling": true, "none": true}
var topologies = map[string]bool{"leader": true, "standalone": true}
type provisionFn func(terraform.UIOutput, communicator.Communicator) error
type provisioner struct { type provisioner struct {
Version string Version string
AutoUpdate bool
HttpDisable bool
Services []Service Services []Service
PermanentPeer bool PermanentPeer bool
ListenCtl string
ListenGossip string ListenGossip string
ListenHTTP string ListenHTTP string
Peer string Peer string
Peers []string
RingKey string RingKey string
RingKeyContent string RingKeyContent string
CtlSecret string
SkipInstall bool SkipInstall bool
UseSudo bool UseSudo bool
ServiceType string ServiceType string
@ -57,13 +40,24 @@ type provisioner struct {
URL string URL string
Channel string Channel string
Events string Events string
OverrideName string
Organization string Organization string
GatewayAuthToken string
BuilderAuthToken string BuilderAuthToken string
SupOptions string SupOptions string
AcceptLicense bool 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 { func Provisioner() terraform.ResourceProvisioner {
return &schema.Provisioner{ return &schema.Provisioner{
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
@ -71,14 +65,30 @@ func Provisioner() terraform.ResourceProvisioner {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, 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{ "peer": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
}, },
"peers": &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
},
"service_type": &schema.Schema{ "service_type": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Default: "systemd", Default: "systemd",
ValidateFunc: validation.StringInSlice([]string{"systemd", "unmanaged"}, false),
}, },
"service_name": &schema.Schema{ "service_name": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
@ -99,6 +109,10 @@ func Provisioner() terraform.ResourceProvisioner {
Optional: true, Optional: true,
Default: false, Default: false,
}, },
"listen_ctl": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"listen_gossip": &schema.Schema{ "listen_gossip": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
@ -115,9 +129,25 @@ func Provisioner() terraform.ResourceProvisioner {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
}, },
"ctl_secret": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"url": &schema.Schema{ "url": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, 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{ "channel": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
@ -127,11 +157,11 @@ func Provisioner() terraform.ResourceProvisioner {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
}, },
"override_name": &schema.Schema{ "organization": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
}, },
"organization": &schema.Schema{ "gateway_auth_token": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
}, },
@ -175,6 +205,7 @@ func Provisioner() terraform.ResourceProvisioner {
"topology": &schema.Schema{ "topology": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
ValidateFunc: validation.StringInSlice([]string{"leader", "standalone"}, false),
}, },
"user_toml": &schema.Schema{ "user_toml": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
@ -183,9 +214,9 @@ func Provisioner() terraform.ResourceProvisioner {
"strategy": &schema.Schema{ "strategy": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
ValidateFunc: validation.StringInSlice([]string{"none", "rolling", "at-once"}, false),
}, },
"channel": &schema.Schema{ "channel": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
}, },
@ -196,6 +227,18 @@ func Provisioner() terraform.ResourceProvisioner {
"url": &schema.Schema{ "url": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, 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{ "application": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
@ -205,10 +248,6 @@ func Provisioner() terraform.ResourceProvisioner {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
}, },
"override_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"service_key": &schema.Schema{ "service_key": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
@ -233,6 +272,30 @@ func applyFn(ctx context.Context) error {
return err 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) comm, err := communicator.New(s)
if err != nil { if err != nil {
return err return err
@ -241,6 +304,7 @@ func applyFn(ctx context.Context) error {
retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout()) retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
defer cancel() defer cancel()
// Wait and retry until we establish the connection
err = communicator.Retry(retryCtx, func() error { err = communicator.Retry(retryCtx, func() error {
return comm.Connect(o) return comm.Connect(o)
}) })
@ -252,7 +316,7 @@ func applyFn(ctx context.Context) error {
if !p.SkipInstall { if !p.SkipInstall {
o.Output("Installing habitat...") o.Output("Installing habitat...")
if err := p.installHab(o, comm); err != nil { if err := p.installHabitat(o, comm); err != nil {
return err return err
} }
} }
@ -264,15 +328,22 @@ func applyFn(ctx context.Context) error {
} }
} }
if p.CtlSecret != "" {
o.Output("Uploading ctl secret...")
if err := p.uploadCtlSecret(o, comm); err != nil {
return err
}
}
o.Output("Starting the habitat supervisor...") o.Output("Starting the habitat supervisor...")
if err := p.startHab(o, comm); err != nil { if err := p.startHabitat(o, comm); err != nil {
return err return err
} }
if p.Services != nil { if p.Services != nil {
for _, service := range p.Services { for _, service := range p.Services {
o.Output("Starting service: " + service.Name) o.Output("Starting service: " + service.Name)
if err := p.startHabService(o, comm, service); err != nil { if err := p.startHabitatService(o, comm, service); err != nil {
return err return err
} }
} }
@ -282,17 +353,11 @@ func applyFn(ctx context.Context) error {
} }
func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) { func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) {
serviceType, ok := c.Get("service_type") ringKeyContent, ok := c.Get("ring_key_content")
if ok { if ok && ringKeyContent != "" && ringKeyContent != hcl2shim.UnknownVariableValue {
if !serviceTypes[serviceType.(string)] { ringKey, ringOk := c.Get("ring_key")
es = append(es, errors.New(serviceType.(string)+" is not a valid service_type.")) if ringOk && ringKey == "" {
} es = append(es, errors.New("if ring_key_content is specified, ring_key must be specified as well"))
}
builderURL, ok := c.Get("url")
if ok {
if _, err := url.ParseRequestURI(builderURL.(string)); err != nil {
es = append(es, errors.New(builderURL.(string)+" is not a valid URL."))
} }
} }
@ -319,30 +384,12 @@ func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) {
// Validate service level configs // Validate service level configs
services, ok := c.Get("service") services, ok := c.Get("service")
if ok { if ok {
for i, svc := range services.([]interface{}) { data, dataOk := services.(string)
service, ok := svc.(map[string]interface{}) if dataOk {
if !ok { es = append(es, fmt.Errorf("service '%v': must be a block", data))
es = append(es, fmt.Errorf("service %d: must be a block", i))
continue
} }
strategy, ok := service["strategy"].(string)
if ok && !updateStrategies[strategy] {
es = append(es, errors.New(strategy+" is not a valid update strategy."))
} }
topology, ok := service["topology"].(string)
if ok && !topologies[topology] {
es = append(es, errors.New(topology+" is not a valid topology"))
}
builderURL, ok := service["url"].(string)
if ok {
if _, err := url.ParseRequestURI(builderURL); err != nil {
es = append(es, errors.New(builderURL+" is not a valid URL."))
}
}
}
}
return ws, es return ws, es
} }
@ -358,20 +405,23 @@ type Service struct {
UserTOML string UserTOML string
AppName string AppName string
Environment string Environment string
OverrideName string
ServiceGroupKey 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 { type Bind struct {
Alias string Alias string
Service string Service string
Group string Group string
} }
func (s *Service) getPackageName(fullName string) string {
return strings.Split(fullName, "/")[1]
}
func (b *Bind) toBindString() string { func (b *Bind) toBindString() string {
return fmt.Sprintf("%s:%s.%s", b.Alias, b.Service, b.Group) return fmt.Sprintf("%s:%s.%s", b.Alias, b.Service, b.Group)
} }
@ -379,7 +429,10 @@ func (b *Bind) toBindString() string {
func decodeConfig(d *schema.ResourceData) (*provisioner, error) { func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
p := &provisioner{ p := &provisioner{
Version: d.Get("version").(string), Version: d.Get("version").(string),
AutoUpdate: d.Get("auto_update").(bool),
HttpDisable: d.Get("http_disable").(bool),
Peer: d.Get("peer").(string), Peer: d.Get("peer").(string),
Peers: getPeers(d.Get("peers").([]interface{})),
Services: getServices(d.Get("service").(*schema.Set).List()), Services: getServices(d.Get("service").(*schema.Set).List()),
UseSudo: d.Get("use_sudo").(bool), UseSudo: d.Get("use_sudo").(bool),
AcceptLicense: d.Get("accept_license").(bool), AcceptLicense: d.Get("accept_license").(bool),
@ -387,20 +440,30 @@ func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
ServiceName: d.Get("service_name").(string), ServiceName: d.Get("service_name").(string),
RingKey: d.Get("ring_key").(string), RingKey: d.Get("ring_key").(string),
RingKeyContent: d.Get("ring_key_content").(string), RingKeyContent: d.Get("ring_key_content").(string),
CtlSecret: d.Get("ctl_secret").(string),
PermanentPeer: d.Get("permanent_peer").(bool), PermanentPeer: d.Get("permanent_peer").(bool),
ListenCtl: d.Get("listen_ctl").(string),
ListenGossip: d.Get("listen_gossip").(string), ListenGossip: d.Get("listen_gossip").(string),
ListenHTTP: d.Get("listen_http").(string), ListenHTTP: d.Get("listen_http").(string),
URL: d.Get("url").(string), URL: d.Get("url").(string),
Channel: d.Get("channel").(string), Channel: d.Get("channel").(string),
Events: d.Get("events").(string), Events: d.Get("events").(string),
OverrideName: d.Get("override_name").(string),
Organization: d.Get("organization").(string), Organization: d.Get("organization").(string),
BuilderAuthToken: d.Get("builder_auth_token").(string), BuilderAuthToken: d.Get("builder_auth_token").(string),
GatewayAuthToken: d.Get("gateway_auth_token").(string),
} }
return p, nil 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 { func getServices(v []interface{}) []Service {
services := make([]Service, 0, len(v)) services := make([]Service, 0, len(v))
for _, rawServiceData := range v { for _, rawServiceData := range v {
@ -413,7 +476,6 @@ func getServices(v []interface{}) []Service {
url := (serviceData["url"].(string)) url := (serviceData["url"].(string))
app := (serviceData["application"].(string)) app := (serviceData["application"].(string))
env := (serviceData["environment"].(string)) env := (serviceData["environment"].(string))
override := (serviceData["override_name"].(string))
userToml := (serviceData["user_toml"].(string)) userToml := (serviceData["user_toml"].(string))
serviceGroupKey := (serviceData["service_key"].(string)) serviceGroupKey := (serviceData["service_key"].(string))
var bindStrings []string var bindStrings []string
@ -438,7 +500,6 @@ func getServices(v []interface{}) []Service {
Binds: binds, Binds: binds,
AppName: app, AppName: app,
Environment: env, Environment: env,
OverrideName: override,
ServiceGroupKey: serviceGroupKey, ServiceGroupKey: serviceGroupKey,
} }
services = append(services, service) services = append(services, service)
@ -463,331 +524,6 @@ func getBinds(v []interface{}) []Bind {
return binds return binds
} }
func (p *provisioner) uploadRingKey(o terraform.UIOutput, comm communicator.Communicator) error {
command := fmt.Sprintf("echo '%s' | hab ring key import", p.RingKeyContent)
if p.UseSudo {
command = fmt.Sprintf("echo '%s' | sudo hab ring key import", p.RingKeyContent)
}
return p.runCommand(o, comm, command)
}
func (p *provisioner) installHab(o terraform.UIOutput, comm communicator.Communicator) error {
// Build the install command
command := fmt.Sprintf("curl -L0 %s > install.sh", installURL)
if err := p.runCommand(o, comm, command); err != nil {
return err
}
// Run the install script
if p.Version == "" {
command = fmt.Sprintf("env HAB_NONINTERACTIVE=true bash ./install.sh ")
} else {
command = fmt.Sprintf("env HAB_NONINTERACTIVE=true bash ./install.sh -v %s", p.Version)
}
if p.UseSudo {
command = fmt.Sprintf("sudo %s", command)
}
if err := p.runCommand(o, comm, command); err != nil {
return err
}
// Accept the license
if p.AcceptLicense {
command = fmt.Sprintf("export HAB_LICENSE=accept; hab -V")
if p.UseSudo {
command = fmt.Sprintf("sudo HAB_LICENSE=accept hab -V")
}
if err := p.runCommand(o, comm, command); err != nil {
return err
}
}
if err := p.createHabUser(o, comm); err != nil {
return err
}
return p.runCommand(o, comm, fmt.Sprintf("rm -f install.sh"))
}
func (p *provisioner) startHab(o terraform.UIOutput, comm communicator.Communicator) error {
// Install the supervisor first
var command string
if p.Version == "" {
command += fmt.Sprintf("hab install core/hab-sup")
} else {
command += fmt.Sprintf("hab install core/hab-sup/%s", p.Version)
}
if p.UseSudo {
command = fmt.Sprintf("sudo -E %s", command)
}
command = fmt.Sprintf("env HAB_NONINTERACTIVE=true %s", command)
if err := p.runCommand(o, comm, command); err != nil {
return err
}
// Build up sup options
options := ""
if p.PermanentPeer {
options += " -I"
}
if p.ListenGossip != "" {
options += fmt.Sprintf(" --listen-gossip %s", p.ListenGossip)
}
if p.ListenHTTP != "" {
options += fmt.Sprintf(" --listen-http %s", p.ListenHTTP)
}
if p.Peer != "" {
options += fmt.Sprintf(" --peer %s", p.Peer)
}
if p.RingKey != "" {
options += fmt.Sprintf(" --ring %s", p.RingKey)
}
if p.URL != "" {
options += fmt.Sprintf(" --url %s", p.URL)
}
if p.Channel != "" {
options += fmt.Sprintf(" --channel %s", p.Channel)
}
if p.Events != "" {
options += fmt.Sprintf(" --events %s", p.Events)
}
if p.OverrideName != "" {
options += fmt.Sprintf(" --override-name %s", p.OverrideName)
}
if p.Organization != "" {
options += fmt.Sprintf(" --org %s", p.Organization)
}
p.SupOptions = options
switch p.ServiceType {
case "unmanaged":
return p.startHabUnmanaged(o, comm, options)
case "systemd":
return p.startHabSystemd(o, comm, options)
default:
return errors.New("Unsupported service type")
}
}
func (p *provisioner) startHabUnmanaged(o terraform.UIOutput, comm communicator.Communicator, options string) error {
// Create the sup directory for the log file
var command string
var token string
if p.UseSudo {
command = "sudo mkdir -p /hab/sup/default && sudo chmod o+w /hab/sup/default"
} else {
command = "mkdir -p /hab/sup/default && chmod o+w /hab/sup/default"
}
if err := p.runCommand(o, comm, command); err != nil {
return err
}
if p.BuilderAuthToken != "" {
token = fmt.Sprintf("env HAB_AUTH_TOKEN=%s", p.BuilderAuthToken)
}
if p.UseSudo {
command = fmt.Sprintf("(%s setsid sudo -E hab sup run %s > /hab/sup/default/sup.log 2>&1 &) ; sleep 1", token, options)
} else {
command = fmt.Sprintf("(%s setsid hab sup run %s > /hab/sup/default/sup.log 2>&1 <&1 &) ; sleep 1", token, options)
}
return p.runCommand(o, comm, command)
}
func (p *provisioner) startHabSystemd(o terraform.UIOutput, comm communicator.Communicator, options string) error {
// Create a new template and parse the client config into it
unitString := template.Must(template.New("hab-supervisor.service").Parse(systemdUnit))
var buf bytes.Buffer
err := unitString.Execute(&buf, p)
if err != nil {
return fmt.Errorf("Error executing %s template: %s", "hab-supervisor.service", err)
}
var command string
if p.UseSudo {
command = fmt.Sprintf("sudo echo '%s' | sudo tee /etc/systemd/system/%s.service > /dev/null", &buf, p.ServiceName)
} else {
command = fmt.Sprintf("echo '%s' | tee /etc/systemd/system/%s.service > /dev/null", &buf, p.ServiceName)
}
if err := p.runCommand(o, comm, command); err != nil {
return err
}
if p.UseSudo {
command = fmt.Sprintf("sudo systemctl enable hab-supervisor && sudo systemctl start hab-supervisor")
} else {
command = fmt.Sprintf("systemctl enable hab-supervisor && systemctl start hab-supervisor")
}
return p.runCommand(o, comm, command)
}
func (p *provisioner) createHabUser(o terraform.UIOutput, comm communicator.Communicator) error {
addUser := false
// Install busybox to get us the user tools we need
command := fmt.Sprintf("env HAB_NONINTERACTIVE=true hab install core/busybox")
if p.UseSudo {
command = fmt.Sprintf("sudo %s", command)
}
if err := p.runCommand(o, comm, command); err != nil {
return err
}
// Check for existing hab user
command = fmt.Sprintf("hab pkg exec core/busybox id hab")
if p.UseSudo {
command = fmt.Sprintf("sudo %s", command)
}
if err := p.runCommand(o, comm, command); err != nil {
o.Output("No existing hab user detected, creating...")
addUser = true
}
if addUser {
command = fmt.Sprintf("hab pkg exec core/busybox adduser -D -g \"\" hab")
if p.UseSudo {
command = fmt.Sprintf("sudo %s", command)
}
return p.runCommand(o, comm, command)
}
return nil
}
// In the future we'll remove the dedicated install once the synchronous load feature in hab-sup is
// available. Until then we install here to provide output and a noisy failure mechanism because
// if you install with the pkg load, it occurs asynchronously and fails quietly.
func (p *provisioner) installHabPackage(o terraform.UIOutput, comm communicator.Communicator, service Service) error {
var command string
options := ""
if service.Channel != "" {
options += fmt.Sprintf(" --channel %s", service.Channel)
}
if service.URL != "" {
options += fmt.Sprintf(" --url %s", service.URL)
}
if p.UseSudo {
command = fmt.Sprintf("env HAB_NONINTERACTIVE=true sudo -E hab pkg install %s %s", service.Name, options)
} else {
command = fmt.Sprintf("env HAB_NONINTERACTIVE=true hab pkg install %s %s", service.Name, options)
}
if p.BuilderAuthToken != "" {
command = fmt.Sprintf("env HAB_AUTH_TOKEN=%s %s", p.BuilderAuthToken, command)
}
return p.runCommand(o, comm, command)
}
func (p *provisioner) startHabService(o terraform.UIOutput, comm communicator.Communicator, service Service) error {
var command string
if err := p.installHabPackage(o, comm, service); err != nil {
return err
}
if err := p.uploadUserTOML(o, comm, service); err != nil {
return err
}
// Upload service group key
if service.ServiceGroupKey != "" {
p.uploadServiceGroupKey(o, comm, service.ServiceGroupKey)
}
options := ""
if service.Topology != "" {
options += fmt.Sprintf(" --topology %s", service.Topology)
}
if service.Strategy != "" {
options += fmt.Sprintf(" --strategy %s", service.Strategy)
}
if service.Channel != "" {
options += fmt.Sprintf(" --channel %s", service.Channel)
}
if service.URL != "" {
options += fmt.Sprintf(" --url %s", service.URL)
}
if service.Group != "" {
options += fmt.Sprintf(" --group %s", service.Group)
}
for _, bind := range service.Binds {
options += fmt.Sprintf(" --bind %s", bind.toBindString())
}
command = fmt.Sprintf("hab svc load %s %s", service.Name, options)
if p.UseSudo {
command = fmt.Sprintf("sudo -E %s", command)
}
if p.BuilderAuthToken != "" {
command = fmt.Sprintf("env HAB_AUTH_TOKEN=%s %s", p.BuilderAuthToken, command)
}
return p.runCommand(o, comm, command)
}
func (p *provisioner) uploadServiceGroupKey(o terraform.UIOutput, comm communicator.Communicator, key string) error {
keyName := strings.Split(key, "\n")[1]
o.Output("Uploading service group key: " + keyName)
keyFileName := fmt.Sprintf("%s.box.key", keyName)
destPath := path.Join("/hab/cache/keys", keyFileName)
keyContent := strings.NewReader(key)
if p.UseSudo {
tempPath := path.Join("/tmp", keyFileName)
if err := comm.Upload(tempPath, keyContent); err != nil {
return err
}
command := fmt.Sprintf("sudo mv %s %s", tempPath, destPath)
return p.runCommand(o, comm, command)
}
return comm.Upload(destPath, keyContent)
}
func (p *provisioner) uploadUserTOML(o terraform.UIOutput, comm communicator.Communicator, service Service) error {
// Create the hab svc directory to lay down the user.toml before loading the service
o.Output("Uploading user.toml for service: " + service.Name)
destDir := fmt.Sprintf("/hab/svc/%s", service.getPackageName(service.Name))
command := fmt.Sprintf("mkdir -p %s", destDir)
if p.UseSudo {
command = fmt.Sprintf("sudo %s", command)
}
if err := p.runCommand(o, comm, command); err != nil {
return err
}
userToml := strings.NewReader(service.UserTOML)
if p.UseSudo {
if err := comm.Upload("/tmp/user.toml", userToml); err != nil {
return err
}
command = fmt.Sprintf("sudo mv /tmp/user.toml %s", destDir)
return p.runCommand(o, comm, command)
}
return comm.Upload(path.Join(destDir, "user.toml"), userToml)
}
func (p *provisioner) copyOutput(o terraform.UIOutput, r io.Reader) { func (p *provisioner) copyOutput(o terraform.UIOutput, r io.Reader) {
lr := linereader.New(r) lr := linereader.New(r)
for line := range lr.Ch { for line := range lr.Ch {
@ -811,7 +547,7 @@ func (p *provisioner) runCommand(o terraform.UIOutput, comm communicator.Communi
} }
if err := comm.Start(cmd); err != nil { if err := comm.Start(cmd); err != nil {
return fmt.Errorf("Error executing command %q: %v", cmd.Command, err) return fmt.Errorf("error executing command %q: %v", cmd.Command, err)
} }
if err := cmd.Wait(); err != nil { if err := cmd.Wait(); err != nil {
@ -830,7 +566,7 @@ func getBindFromString(bind string) (Bind, error) {
return false return false
}) })
if len(t) != 3 { if len(t) != 3 {
return Bind{}, errors.New("Invalid bind specification: " + bind) return Bind{}, errors.New("invalid bind specification: " + bind)
} }
return Bind{Alias: t[0], Service: t[1], Group: t[2]}, nil return Bind{Alias: t[0], Service: t[1], Group: t[2]}, nil
} }

View File

@ -19,7 +19,7 @@ func TestProvisioner(t *testing.T) {
func TestResourceProvisioner_Validate_good(t *testing.T) { func TestResourceProvisioner_Validate_good(t *testing.T) {
c := testConfig(t, map[string]interface{}{ c := testConfig(t, map[string]interface{}{
"peer": "1.2.3.4", "peers": []interface{}{"1.2.3.4"},
"version": "0.32.0", "version": "0.32.0",
"service_type": "systemd", "service_type": "systemd",
"accept_license": false, "accept_license": false,
@ -37,15 +37,16 @@ func TestResourceProvisioner_Validate_good(t *testing.T) {
func TestResourceProvisioner_Validate_bad(t *testing.T) { func TestResourceProvisioner_Validate_bad(t *testing.T) {
c := testConfig(t, map[string]interface{}{ c := testConfig(t, map[string]interface{}{
"service_type": "invalidtype", "service_type": "invalidtype",
"url": "badurl",
}) })
warn, errs := Provisioner().Validate(c) warn, errs := Provisioner().Validate(c)
if len(warn) > 0 { if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn) t.Fatalf("Warnings: %v", warn)
} }
//Two errors, one for service_type, other for missing required accept_license argument // 3 errors, bad service_type, bad url, missing accept_license
if len(errs) != 2 { if len(errs) != 3 {
t.Fatalf("Should have one errors, got %d", len(errs)) t.Fatalf("Should have three errors, got %d", len(errs))
} }
} }
@ -67,7 +68,21 @@ func TestResourceProvisioner_Validate_bad_service_config(t *testing.T) {
t.Fatalf("Warnings: %v", warn) t.Fatalf("Warnings: %v", warn)
} }
if len(errs) != 3 { if len(errs) != 3 {
t.Fatalf("Should have three errors") t.Fatalf("Should have three errors, got %d", len(errs))
}
}
func TestResourceProvisioner_Validate_bad_service_definition(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"service": "core/vault",
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) != 3 {
t.Fatalf("Should have three errors, got %d", len(errs))
} }
} }

View File

@ -32,7 +32,7 @@ resource "aws_instance" "redis" {
count = 3 count = 3
provisioner "habitat" { provisioner "habitat" {
peer = "${aws_instance.redis.0.private_ip}" peers = [aws_instance.redis[0].private_ip]
use_sudo = true use_sudo = true
service_type = "systemd" service_type = "systemd"
accept_license = true accept_license = true
@ -40,7 +40,7 @@ resource "aws_instance" "redis" {
service { service {
name = "core/redis" name = "core/redis"
topology = "leader" topology = "leader"
user_toml = "${file("conf/redis.toml")}" user_toml = file("conf/redis.toml")
} }
} }
} }
@ -54,20 +54,25 @@ There are 2 configuration levels, `supervisor` and `service`. Configuration pla
### Supervisor Arguments ### Supervisor Arguments
* `accept_license (bool)` - (Required) Set to true to accept [Habitat end user license agreement](https://www.chef.io/end-user-license-agreement/) * `accept_license (bool)` - (Required) Set to true to accept [Habitat end user license agreement](https://www.chef.io/end-user-license-agreement/)
* `version (string)` - (Optional) The Habitat version to install on the remote machine. If not specified, the latest available version is used. * `version (string)` - (Optional) The Habitat version to install on the remote machine. If not specified, the latest available version is used.
* `use_sudo (bool)` - (Optional) Use `sudo` when executing remote commands. Required when the user specified in the `connection` block is not `root`. (Defaults to `true`) * `auto_update (bool)` - (Optional) If set to `true`, the supervisor will auto-update itself as soon as new releases are available on the specified `channel`.
* `http_disable (bool)` - (Optional) If set to `true`, disables the supervisor HTTP listener entirely.
* `peer (string)` - (Optional, deprecated) IP addresses or FQDN's for other Habitat supervisors to peer with, like: `--peer 1.2.3.4 --peer 5.6.7.8`. (Defaults to none)
* `peers (array)` - (Optional) A list of IP or FQDN's of other supervisor instance(s) to peer with. (Defaults to none)
* `service_type (string)` - (Optional) Method used to run the Habitat supervisor. Valid options are `unmanaged` and `systemd`. (Defaults to `systemd`) * `service_type (string)` - (Optional) Method used to run the Habitat supervisor. Valid options are `unmanaged` and `systemd`. (Defaults to `systemd`)
* `service_name (string)` - (Optional) The name of the Habitat supervisor service, if using an init system such as `systemd`. (Defaults to `hab-supervisor`) * `service_name (string)` - (Optional) The name of the Habitat supervisor service, if using an init system such as `systemd`. (Defaults to `hab-supervisor`)
* `peer (string)` - (Optional) IP or FQDN of a supervisor instance to peer with. (Defaults to none) * `use_sudo (bool)` - (Optional) Use `sudo` when executing remote commands. Required when the user specified in the `connection` block is not `root`. (Defaults to `true`)
* `permanent_peer (bool)` - (Optional) Marks this supervisor as a permanent peer. (Defaults to false) * `permanent_peer (bool)` - (Optional) Marks this supervisor as a permanent peer. (Defaults to false)
* `listen_ctl (string)` - (Optional) The listen address for the countrol gateway system (Defaults to 127.0.0.1:9632)
* `listen_gossip (string)` - (Optional) The listen address for the gossip system (Defaults to 0.0.0.0:9638) * `listen_gossip (string)` - (Optional) The listen address for the gossip system (Defaults to 0.0.0.0:9638)
* `listen_http (string)` - (Optional) The listen address for the HTTP gateway (Defaults to 0.0.0.0:9631) * `listen_http (string)` - (Optional) The listen address for the HTTP gateway (Defaults to 0.0.0.0:9631)
* `ring_key (string)` - (Optional) The name of the ring key for encrypting gossip ring communication (Defaults to no encryption) * `ring_key (string)` - (Optional) The name of the ring key for encrypting gossip ring communication (Defaults to no encryption)
* `ring_key_content (string)` - (Optional) The key content. Only needed if using ring encryption and want the provisioner to take care of uploading and importing it. Easiest to source from a file (eg `ring_key_content = "${file("conf/foo-123456789.sym.key")}"`) (Defaults to none) * `ring_key_content (string)` - (Optional) The key content. Only needed if using ring encryption and want the provisioner to take care of uploading and importing it. Easiest to source from a file (eg `ring_key_content = "${file("conf/foo-123456789.sym.key")}"`) (Defaults to none)
* `ctl_secret (string)` - (Optional) Specify a secret to use (from `hab sup secret generate`) for control gateway communication between hab client(s) and the supervisor. (Defaults to none)
* `url (string)` - (Optional) The URL of a Builder service to download packages and receive updates from. (Defaults to https://bldr.habitat.sh) * `url (string)` - (Optional) The URL of a Builder service to download packages and receive updates from. (Defaults to https://bldr.habitat.sh)
* `channel (string)` - (Optional) The release channel in the Builder service to use. (Defaults to `stable`) * `channel (string)` - (Optional) The release channel in the Builder service to use. (Defaults to `stable`)
* `events (string)` - (Optional) Name of the service group running a Habitat EventSrv to forward Supervisor and service event data to. (Defaults to none) * `events (string)` - (Optional) Name of the service group running a Habitat EventSrv to forward Supervisor and service event data to. (Defaults to none)
* `override_name (string)` - (Optional) The name of the Supervisor (Defaults to `default`)
* `organization (string)` - (Optional) The organization that the Supervisor and it's subsequent services are part of. (Defaults to `default`) * `organization (string)` - (Optional) The organization that the Supervisor and it's subsequent services are part of. (Defaults to `default`)
* `gateway_auth_token (string)` - (Optional) The http gateway authorization token (Defaults to none)
* `builder_auth_token (string)` - (Optional) The builder authorization token when using a private origin. (Defaults to none) * `builder_auth_token (string)` - (Optional) The builder authorization token when using a private origin. (Defaults to none)
### Service Arguments ### Service Arguments
@ -84,11 +89,10 @@ bind {
``` ```
* `topology (string)` - (Optional) Topology to start service in. Possible values `standalone` or `leader`. (Defaults to `standalone`) * `topology (string)` - (Optional) Topology to start service in. Possible values `standalone` or `leader`. (Defaults to `standalone`)
* `strategy (string)` - (Optional) Update strategy to use. Possible values `at-once`, `rolling` or `none`. (Defaults to `none`) * `strategy (string)` - (Optional) Update strategy to use. Possible values `at-once`, `rolling` or `none`. (Defaults to `none`)
* `user_toml (string)` - (Optional) TOML formatted user configuration for the service. Easiest to source from a file (eg `user_toml = "${file("conf/redis.toml")}")`. (Defaults to none) * `user_toml (string)` - (Optional) TOML formatted user configuration for the service. Easiest to source from a file (eg `user_toml = "${file("conf/redis.toml")}"`). (Defaults to none)
* `channel (string)` - (Optional) The release channel in the Builder service to use. (Defaults to `stable`) * `channel (string)` - (Optional) The release channel in the Builder service to use. (Defaults to `stable`)
* `group (string)` - (Optional) The service group to join. (Defaults to `default`) * `group (string)` - (Optional) The service group to join. (Defaults to `default`)
* `url (string)` - (Optional) The URL of a Builder service to download packages and receive updates from. (Defaults to https://bldr.habitat.sh) * `url (string)` - (Optional) The URL of a Builder service to download packages and receive updates from. (Defaults to https://bldr.habitat.sh)
* `application (string)` - (Optional) The application name. (Defaults to none) * `application (string)` - (Optional) The application name. (Defaults to none)
* `environment (string)` - (Optional) The environment name. (Defaults to none) * `environment (string)` - (Optional) The environment name. (Defaults to none)
* `override_name (string)` - (Optional) The name for the state directory if there is more than one Supervisor running. (Defaults to `default`)
* `service_key (string)` - (Optional) The key content of a service private key, if using service group encryption. Easiest to source from a file (eg `service_key = "${file("conf/redis.default@org-123456789.box.key")}"`) (Defaults to none) * `service_key (string)` - (Optional) The key content of a service private key, if using service group encryption. Easiest to source from a file (eg `service_key = "${file("conf/redis.default@org-123456789.box.key")}"`) (Defaults to none)