diff --git a/builtin/provisioners/habitat/linux_provisioner.go b/builtin/provisioners/habitat/linux_provisioner.go new file mode 100644 index 000000000..e5e82f629 --- /dev/null +++ b/builtin/provisioners/habitat/linux_provisioner.go @@ -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 +} diff --git a/builtin/provisioners/habitat/linux_provisioner_test.go b/builtin/provisioners/habitat/linux_provisioner_test.go new file mode 100644 index 000000000..5ef5322d0 --- /dev/null +++ b/builtin/provisioners/habitat/linux_provisioner_test.go @@ -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() + } + } + } +} diff --git a/builtin/provisioners/habitat/resource_provisioner.go b/builtin/provisioners/habitat/resource_provisioner.go index fcb8ddb18..87534a6f6 100644 --- a/builtin/provisioners/habitat/resource_provisioner.go +++ b/builtin/provisioners/habitat/resource_provisioner.go @@ -1,55 +1,38 @@ package habitat import ( - "bytes" "context" + "crypto/sha256" "errors" "fmt" "io" "net/url" - "path" "strings" - "text/template" version "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/communicator" "github.com/hashicorp/terraform/communicator/remote" + "github.com/hashicorp/terraform/configs/hcl2shim" "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" "github.com/hashicorp/terraform/terraform" - 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 { Version string + AutoUpdate bool + HttpDisable bool Services []Service PermanentPeer bool + ListenCtl string ListenGossip string ListenHTTP string Peer string + Peers []string RingKey string RingKeyContent string + CtlSecret string SkipInstall bool UseSudo bool ServiceType string @@ -57,13 +40,24 @@ type provisioner struct { URL string Channel string Events string - OverrideName string Organization string + GatewayAuthToken string BuilderAuthToken string SupOptions string AcceptLicense bool + + installHabitat provisionFn + startHabitat provisionFn + uploadRingKey provisionFn + uploadCtlSecret provisionFn + startHabitatService provisionServiceFn + + osType string } +type provisionFn func(terraform.UIOutput, communicator.Communicator) error +type provisionServiceFn func(terraform.UIOutput, communicator.Communicator, Service) error + func Provisioner() terraform.ResourceProvisioner { return &schema.Provisioner{ Schema: map[string]*schema.Schema{ @@ -71,14 +65,30 @@ func Provisioner() terraform.ResourceProvisioner { Type: schema.TypeString, Optional: true, }, + "auto_update": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "http_disable": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, "peer": &schema.Schema{ Type: schema.TypeString, Optional: true, }, - "service_type": &schema.Schema{ - Type: schema.TypeString, + "peers": &schema.Schema{ + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, 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{ Type: schema.TypeString, @@ -99,6 +109,10 @@ func Provisioner() terraform.ResourceProvisioner { Optional: true, Default: false, }, + "listen_ctl": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, "listen_gossip": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -115,9 +129,25 @@ func Provisioner() terraform.ResourceProvisioner { Type: schema.TypeString, Optional: true, }, + "ctl_secret": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, "url": &schema.Schema{ Type: schema.TypeString, Optional: true, + ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { + u, err := url.Parse(val.(string)) + if err != nil { + errs = append(errs, fmt.Errorf("invalid URL specified for %q: %v", key, err)) + } + + if u.Scheme == "" { + errs = append(errs, fmt.Errorf("invalid URL specified for %q (scheme must be specified)", key)) + } + + return warns, errs + }, }, "channel": &schema.Schema{ Type: schema.TypeString, @@ -127,11 +157,11 @@ func Provisioner() terraform.ResourceProvisioner { Type: schema.TypeString, Optional: true, }, - "override_name": &schema.Schema{ + "organization": &schema.Schema{ Type: schema.TypeString, Optional: true, }, - "organization": &schema.Schema{ + "gateway_auth_token": &schema.Schema{ Type: schema.TypeString, Optional: true, }, @@ -173,19 +203,20 @@ func Provisioner() terraform.ResourceProvisioner { Optional: true, }, "topology": &schema.Schema{ - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"leader", "standalone"}, false), }, "user_toml": &schema.Schema{ Type: schema.TypeString, Optional: true, }, "strategy": &schema.Schema{ - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"none", "rolling", "at-once"}, false), }, "channel": &schema.Schema{ - Type: schema.TypeString, Optional: true, }, @@ -196,6 +227,18 @@ func Provisioner() terraform.ResourceProvisioner { "url": &schema.Schema{ Type: schema.TypeString, Optional: true, + ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { + u, err := url.Parse(val.(string)) + if err != nil { + errs = append(errs, fmt.Errorf("invalid URL specified for %q: %v", key, err)) + } + + if u.Scheme == "" { + errs = append(errs, fmt.Errorf("invalid URL specified for %q (scheme must be specified)", key)) + } + + return warns, errs + }, }, "application": &schema.Schema{ Type: schema.TypeString, @@ -205,10 +248,6 @@ func Provisioner() terraform.ResourceProvisioner { Type: schema.TypeString, Optional: true, }, - "override_name": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, "service_key": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -233,6 +272,30 @@ func applyFn(ctx context.Context) error { return err } + // Automatically determine the OS type + switch t := s.Ephemeral.ConnInfo["type"]; t { + case "ssh", "": + p.osType = "linux" + case "winrm": + p.osType = "windows" + default: + return fmt.Errorf("unsupported connection type: %s", t) + } + + switch p.osType { + case "linux": + p.installHabitat = p.linuxInstallHabitat + p.uploadRingKey = p.linuxUploadRingKey + p.uploadCtlSecret = p.linuxUploadCtlSecret + p.startHabitat = p.linuxStartHabitat + p.startHabitatService = p.linuxStartHabitatService + case "windows": + return fmt.Errorf("windows is not supported yet for the habitat provisioner") + default: + return fmt.Errorf("unsupported os type: %s", p.osType) + } + + // Get a new communicator comm, err := communicator.New(s) if err != nil { return err @@ -241,6 +304,7 @@ func applyFn(ctx context.Context) error { retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout()) defer cancel() + // Wait and retry until we establish the connection err = communicator.Retry(retryCtx, func() error { return comm.Connect(o) }) @@ -252,7 +316,7 @@ func applyFn(ctx context.Context) error { if !p.SkipInstall { o.Output("Installing habitat...") - if err := p.installHab(o, comm); err != nil { + if err := p.installHabitat(o, comm); err != nil { 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...") - if err := p.startHab(o, comm); err != nil { + if err := p.startHabitat(o, comm); err != nil { return err } if p.Services != nil { for _, service := range p.Services { o.Output("Starting service: " + service.Name) - if err := p.startHabService(o, comm, service); err != nil { + if err := p.startHabitatService(o, comm, service); err != nil { return err } } @@ -282,17 +353,11 @@ func applyFn(ctx context.Context) error { } func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) { - serviceType, ok := c.Get("service_type") - if ok { - if !serviceTypes[serviceType.(string)] { - es = append(es, errors.New(serviceType.(string)+" is not a valid service_type.")) - } - } - - 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.")) + ringKeyContent, ok := c.Get("ring_key_content") + if ok && ringKeyContent != "" && ringKeyContent != hcl2shim.UnknownVariableValue { + ringKey, ringOk := c.Get("ring_key") + if ringOk && ringKey == "" { + es = append(es, errors.New("if ring_key_content is specified, ring_key must be specified as well")) } } @@ -319,30 +384,12 @@ func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) { // Validate service level configs services, ok := c.Get("service") if ok { - for i, svc := range services.([]interface{}) { - service, ok := svc.(map[string]interface{}) - if !ok { - 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.")) - } - } + data, dataOk := services.(string) + if dataOk { + es = append(es, fmt.Errorf("service '%v': must be a block", data)) } } + return ws, es } @@ -358,20 +405,23 @@ type Service struct { UserTOML string AppName string Environment string - OverrideName string ServiceGroupKey string } +func (s *Service) getPackageName(fullName string) string { + return strings.Split(fullName, "/")[1] +} + +func (s *Service) getServiceNameChecksum() string { + return fmt.Sprintf("%x", sha256.Sum256([]byte(s.Name))) +} + type Bind struct { Alias string Service string Group string } -func (s *Service) getPackageName(fullName string) string { - return strings.Split(fullName, "/")[1] -} - func (b *Bind) toBindString() string { 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) { p := &provisioner{ Version: d.Get("version").(string), + AutoUpdate: d.Get("auto_update").(bool), + HttpDisable: d.Get("http_disable").(bool), Peer: d.Get("peer").(string), + Peers: getPeers(d.Get("peers").([]interface{})), Services: getServices(d.Get("service").(*schema.Set).List()), UseSudo: d.Get("use_sudo").(bool), AcceptLicense: d.Get("accept_license").(bool), @@ -387,20 +440,30 @@ func decodeConfig(d *schema.ResourceData) (*provisioner, error) { ServiceName: d.Get("service_name").(string), RingKey: d.Get("ring_key").(string), RingKeyContent: d.Get("ring_key_content").(string), + CtlSecret: d.Get("ctl_secret").(string), PermanentPeer: d.Get("permanent_peer").(bool), + ListenCtl: d.Get("listen_ctl").(string), ListenGossip: d.Get("listen_gossip").(string), ListenHTTP: d.Get("listen_http").(string), URL: d.Get("url").(string), Channel: d.Get("channel").(string), Events: d.Get("events").(string), - OverrideName: d.Get("override_name").(string), Organization: d.Get("organization").(string), BuilderAuthToken: d.Get("builder_auth_token").(string), + GatewayAuthToken: d.Get("gateway_auth_token").(string), } return p, nil } +func getPeers(v []interface{}) []string { + peers := make([]string, 0, len(v)) + for _, rawPeerData := range v { + peers = append(peers, rawPeerData.(string)) + } + return peers +} + func getServices(v []interface{}) []Service { services := make([]Service, 0, len(v)) for _, rawServiceData := range v { @@ -413,7 +476,6 @@ func getServices(v []interface{}) []Service { url := (serviceData["url"].(string)) app := (serviceData["application"].(string)) env := (serviceData["environment"].(string)) - override := (serviceData["override_name"].(string)) userToml := (serviceData["user_toml"].(string)) serviceGroupKey := (serviceData["service_key"].(string)) var bindStrings []string @@ -438,7 +500,6 @@ func getServices(v []interface{}) []Service { Binds: binds, AppName: app, Environment: env, - OverrideName: override, ServiceGroupKey: serviceGroupKey, } services = append(services, service) @@ -463,331 +524,6 @@ func getBinds(v []interface{}) []Bind { 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) { lr := linereader.New(r) 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 { - 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 { @@ -830,7 +566,7 @@ func getBindFromString(bind string) (Bind, error) { return false }) 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 } diff --git a/builtin/provisioners/habitat/resource_provisioner_test.go b/builtin/provisioners/habitat/resource_provisioner_test.go index d18a39b5c..054aa9c19 100644 --- a/builtin/provisioners/habitat/resource_provisioner_test.go +++ b/builtin/provisioners/habitat/resource_provisioner_test.go @@ -19,7 +19,7 @@ func TestProvisioner(t *testing.T) { func TestResourceProvisioner_Validate_good(t *testing.T) { c := testConfig(t, map[string]interface{}{ - "peer": "1.2.3.4", + "peers": []interface{}{"1.2.3.4"}, "version": "0.32.0", "service_type": "systemd", "accept_license": false, @@ -37,15 +37,16 @@ func TestResourceProvisioner_Validate_good(t *testing.T) { func TestResourceProvisioner_Validate_bad(t *testing.T) { c := testConfig(t, map[string]interface{}{ "service_type": "invalidtype", + "url": "badurl", }) warn, errs := Provisioner().Validate(c) if len(warn) > 0 { t.Fatalf("Warnings: %v", warn) } - //Two errors, one for service_type, other for missing required accept_license argument - if len(errs) != 2 { - t.Fatalf("Should have one errors, got %d", len(errs)) + // 3 errors, bad service_type, bad url, missing accept_license + if len(errs) != 3 { + 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) } 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)) } } diff --git a/website/docs/provisioners/habitat.html.markdown b/website/docs/provisioners/habitat.html.markdown index 968f8b6fa..2c2da6146 100644 --- a/website/docs/provisioners/habitat.html.markdown +++ b/website/docs/provisioners/habitat.html.markdown @@ -32,7 +32,7 @@ resource "aws_instance" "redis" { count = 3 provisioner "habitat" { - peer = "${aws_instance.redis.0.private_ip}" + peers = [aws_instance.redis[0].private_ip] use_sudo = true service_type = "systemd" accept_license = true @@ -40,7 +40,7 @@ resource "aws_instance" "redis" { service { name = "core/redis" 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 * `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. -* `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_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) +* `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_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_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) * `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) -* `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`) +* `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) ### Service Arguments @@ -84,11 +89,10 @@ bind { ``` * `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`) -* `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`) * `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) * `application (string)` - (Optional) The application 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) \ No newline at end of file +* `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)