Update Linux Habitat Provisioner to support new licensing, options cleanup.

This commit is contained in:
Kyle Mott 2019-09-05 09:44:35 -07:00
parent 4ebf9082cd
commit 164332bfa1
4 changed files with 907 additions and 411 deletions

View File

@ -0,0 +1,375 @@
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 -}}
{{ if .License -}}
Environment="HAB_LICENSE={{ .License }}"
{{ 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 hab install script
var command string
if p.Version == "" {
command = p.linuxGetCommand(fmt.Sprintf("bash ./install.sh "))
} else {
command = p.linuxGetCommand(fmt.Sprintf("bash ./install.sh -v %s", p.Version))
}
if err := p.runCommand(o, comm, command); 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 and HAB_LICENSE 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
var license 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("HAB_AUTH_TOKEN=%s ", p.BuilderAuthToken)
}
// Set HAB_LICENSE if provided
if p.License != "" {
license = fmt.Sprintf("HAB_LICENSE=%s ", p.License)
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("(env %s%s setsid hab sup run%s > /hab/sup/default/sup.log 2>&1 <&1 &) ; sleep 1", token, license, 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 license acceptance
if p.License != "" {
env += fmt.Sprintf(" HAB_LICENSE=%s", p.License)
}
// 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,347 @@
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",
"license": "accept-no-persist",
"auto_update": true,
"use_sudo": true,
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true HAB_LICENSE=accept-no-persist 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 HAB_LICENSE=accept-no-persist sudo -E /bin/bash -c 'bash ./install.sh -v 0.81.0'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true HAB_LICENSE=accept-no-persist sudo -E /bin/bash -c 'hab install core/busybox'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true HAB_LICENSE=accept-no-persist sudo -E /bin/bash -c 'hab pkg exec core/busybox adduser -D -g \"\" hab'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true HAB_LICENSE=accept-no-persist 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 HAB_LICENSE=accept-no-persist sudo -E /bin/bash -c 'hab install core/hab-sup/0.81.0'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true HAB_LICENSE=accept-no-persist sudo -E /bin/bash -c 'mkdir -p /hab/sup/default && chmod o+w /hab/sup/default'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true HAB_LICENSE=accept-no-persist sudo -E /bin/bash -c '(env HAB_LICENSE=accept-no-persist 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,54 +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"
"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
License 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
@ -56,12 +40,23 @@ 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
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{
@ -69,14 +64,34 @@ func Provisioner() terraform.ResourceProvisioner {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
}, },
"license": &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{ "peer": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
}, },
"service_type": &schema.Schema{ "peers": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true, Optional: true,
Default: "systemd", },
"service_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "systemd",
ValidateFunc: validation.StringInSlice([]string{"systemd", "unmanaged"}, false),
}, },
"service_name": &schema.Schema{ "service_name": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
@ -93,6 +108,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,
@ -109,9 +128,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,
@ -121,11 +156,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,
}, },
@ -167,19 +202,20 @@ func Provisioner() terraform.ResourceProvisioner {
Optional: true, Optional: true,
}, },
"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,
Optional: true, Optional: true,
}, },
"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,
}, },
@ -190,6 +226,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,
@ -199,10 +247,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,
@ -227,6 +271,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
@ -235,6 +303,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)
}) })
@ -246,7 +315,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
} }
} }
@ -258,15 +327,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
} }
} }
@ -276,48 +352,34 @@ 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."))
} }
} }
// 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."))
}
}
} }
} }
//Validate service level configs
//services, ok := c.Get("service")
//if ok {
// for i, svc := range services.([]interface{}) {
// _, ok := svc.(map[string]interface{})
// if !ok {
// es = append(es, fmt.Errorf("service %d: must be a block", i))
// continue
// }
// }
//}
return ws, es return ws, es
} }
@ -333,20 +395,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)
} }
@ -354,27 +419,41 @@ 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),
License: d.Get("license").(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),
ServiceType: d.Get("service_type").(string), ServiceType: d.Get("service_type").(string),
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 {
@ -387,7 +466,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
@ -412,7 +490,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)
@ -437,319 +514,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
}
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 {
@ -773,7 +537,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 {
@ -792,7 +556,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",
}) })
@ -36,26 +36,22 @@ 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)
} }
if len(errs) != 1 { if len(errs) != 2 {
t.Fatalf("Should have one error") t.Fatalf("Should have two errors")
} }
} }
func TestResourceProvisioner_Validate_bad_service_config(t *testing.T) { func TestResourceProvisioner_Validate_bad_service_config(t *testing.T) {
c := testConfig(t, map[string]interface{}{ c := testConfig(t, map[string]interface{}{
"service": []interface{}{ "service": []interface{}{
map[string]interface{}{ map[string]interface{}{"name": "core/foo", "strategy": "bar", "topology": "baz", "url": "badurl"},
"name": "core/foo",
"strategy": "bar",
"topology": "baz",
"url": "badurl",
},
}, },
}) })
@ -64,7 +60,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: %v", 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) != 2 {
t.Fatalf("Should have three errors: %v", errs)
} }
} }