Remove vendor provisioners and add fmt Make target
Remove chef, habitat, puppet, and salt-masterless provsioners, which follows their deprecation. Update the documentatin for these provisioners to clarify that they have been removed from later versions of Terraform. Adds the fmt Make target back and updates fmtcheck script for correctness.
This commit is contained in:
parent
c1d30401c5
commit
e39e0e3d04
|
@ -1,12 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/terraform/builtin/provisioners/chef"
|
|
||||||
"github.com/hashicorp/terraform/plugin"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
plugin.Serve(&plugin.ServeOpts{
|
|
||||||
ProvisionerFunc: chef.Provisioner,
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/terraform/builtin/provisioners/habitat"
|
|
||||||
"github.com/hashicorp/terraform/plugin"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
plugin.Serve(&plugin.ServeOpts{
|
|
||||||
ProvisionerFunc: habitat.Provisioner,
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/terraform/builtin/provisioners/puppet"
|
|
||||||
"github.com/hashicorp/terraform/plugin"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
plugin.Serve(&plugin.ServeOpts{
|
|
||||||
ProvisionerFunc: puppet.Provisioner,
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/terraform/builtin/provisioners/salt-masterless"
|
|
||||||
"github.com/hashicorp/terraform/plugin"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
plugin.Serve(&plugin.ServeOpts{
|
|
||||||
ProvisionerFunc: saltmasterless.Provisioner,
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,115 +0,0 @@
|
||||||
package chef
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/communicator"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
chmod = "find %s -maxdepth 1 -type f -exec /bin/chmod %d {} +"
|
|
||||||
installURL = "https://omnitruck.chef.io/install.sh"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *provisioner) linuxInstallChefClient(o terraform.UIOutput, comm communicator.Communicator) error {
|
|
||||||
// Build up the command prefix
|
|
||||||
prefix := ""
|
|
||||||
if p.HTTPProxy != "" {
|
|
||||||
prefix += fmt.Sprintf("http_proxy='%s' ", p.HTTPProxy)
|
|
||||||
}
|
|
||||||
if p.HTTPSProxy != "" {
|
|
||||||
prefix += fmt.Sprintf("https_proxy='%s' ", p.HTTPSProxy)
|
|
||||||
}
|
|
||||||
if len(p.NOProxy) > 0 {
|
|
||||||
prefix += fmt.Sprintf("no_proxy='%s' ", strings.Join(p.NOProxy, ","))
|
|
||||||
}
|
|
||||||
|
|
||||||
// First download the install.sh script from Chef
|
|
||||||
err := p.runCommand(o, comm, fmt.Sprintf("%scurl -LO %s", prefix, installURL))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then execute the install.sh scrip to download and install Chef Client
|
|
||||||
err = p.runCommand(o, comm, fmt.Sprintf("%sbash ./install.sh -v %q -c %s", prefix, p.Version, p.Channel))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// And finally cleanup the install.sh script again
|
|
||||||
return p.runCommand(o, comm, fmt.Sprintf("%srm -f install.sh", prefix))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) linuxCreateConfigFiles(o terraform.UIOutput, comm communicator.Communicator) error {
|
|
||||||
// Make sure the config directory exists
|
|
||||||
if err := p.runCommand(o, comm, "mkdir -p "+linuxConfDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure we have enough rights to upload the files if using sudo
|
|
||||||
if p.useSudo {
|
|
||||||
if err := p.runCommand(o, comm, "chmod 777 "+linuxConfDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := p.runCommand(o, comm, fmt.Sprintf(chmod, linuxConfDir, 666)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.deployConfigFiles(o, comm, linuxConfDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(p.OhaiHints) > 0 {
|
|
||||||
// Make sure the hits directory exists
|
|
||||||
hintsDir := path.Join(linuxConfDir, "ohai/hints")
|
|
||||||
if err := p.runCommand(o, comm, "mkdir -p "+hintsDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure we have enough rights to upload the hints if using sudo
|
|
||||||
if p.useSudo {
|
|
||||||
if err := p.runCommand(o, comm, "chmod 777 "+hintsDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := p.runCommand(o, comm, fmt.Sprintf(chmod, hintsDir, 666)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.deployOhaiHints(o, comm, hintsDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// When done copying the hints restore the rights and make sure root is owner
|
|
||||||
if p.useSudo {
|
|
||||||
if err := p.runCommand(o, comm, "chmod 755 "+hintsDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := p.runCommand(o, comm, fmt.Sprintf(chmod, hintsDir, 600)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := p.runCommand(o, comm, "chown -R root:root "+hintsDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// When done copying all files restore the rights and make sure root is owner
|
|
||||||
if p.useSudo {
|
|
||||||
if err := p.runCommand(o, comm, "chmod 755 "+linuxConfDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := p.runCommand(o, comm, fmt.Sprintf(chmod, linuxConfDir, 600)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := p.runCommand(o, comm, "chown -R root:root "+linuxConfDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,330 +0,0 @@
|
||||||
package chef
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/communicator"
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestResourceProvider_linuxInstallChefClient(t *testing.T) {
|
|
||||||
cases := map[string]struct {
|
|
||||||
Config map[string]interface{}
|
|
||||||
Commands map[string]bool
|
|
||||||
}{
|
|
||||||
"Sudo": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
},
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"sudo curl -LO https://omnitruck.chef.io/install.sh": true,
|
|
||||||
"sudo bash ./install.sh -v \"\" -c stable": true,
|
|
||||||
"sudo rm -f install.sh": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"NoSudo": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"prevent_sudo": true,
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"secret_key": "SECRET-KEY",
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
},
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"curl -LO https://omnitruck.chef.io/install.sh": true,
|
|
||||||
"bash ./install.sh -v \"\" -c stable": true,
|
|
||||||
"rm -f install.sh": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"HTTPProxy": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"http_proxy": "http://proxy.local",
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"prevent_sudo": true,
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
},
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"http_proxy='http://proxy.local' curl -LO https://omnitruck.chef.io/install.sh": true,
|
|
||||||
"http_proxy='http://proxy.local' bash ./install.sh -v \"\" -c stable": true,
|
|
||||||
"http_proxy='http://proxy.local' rm -f install.sh": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"HTTPSProxy": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"https_proxy": "https://proxy.local",
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"prevent_sudo": true,
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
},
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"https_proxy='https://proxy.local' curl -LO https://omnitruck.chef.io/install.sh": true,
|
|
||||||
"https_proxy='https://proxy.local' bash ./install.sh -v \"\" -c stable": true,
|
|
||||||
"https_proxy='https://proxy.local' rm -f install.sh": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"NoProxy": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"http_proxy": "http://proxy.local",
|
|
||||||
"no_proxy": []interface{}{"http://local.local", "http://local.org"},
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"prevent_sudo": true,
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
},
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"http_proxy='http://proxy.local' no_proxy='http://local.local,http://local.org' " +
|
|
||||||
"curl -LO https://omnitruck.chef.io/install.sh": true,
|
|
||||||
"http_proxy='http://proxy.local' no_proxy='http://local.local,http://local.org' " +
|
|
||||||
"bash ./install.sh -v \"\" -c stable": true,
|
|
||||||
"http_proxy='http://proxy.local' no_proxy='http://local.local,http://local.org' " +
|
|
||||||
"rm -f install.sh": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"Version": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"prevent_sudo": true,
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
"version": "11.18.6",
|
|
||||||
},
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"curl -LO https://omnitruck.chef.io/install.sh": true,
|
|
||||||
"bash ./install.sh -v \"11.18.6\" -c stable": true,
|
|
||||||
"rm -f install.sh": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"Channel": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"channel": "current",
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"prevent_sudo": true,
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
"version": "11.18.6",
|
|
||||||
},
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"curl -LO https://omnitruck.chef.io/install.sh": true,
|
|
||||||
"bash ./install.sh -v \"11.18.6\" -c current": true,
|
|
||||||
"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)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.useSudo = !p.PreventSudo
|
|
||||||
|
|
||||||
err = p.linuxInstallChefClient(o, c)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %q failed: %v", k, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvider_linuxCreateConfigFiles(t *testing.T) {
|
|
||||||
cases := map[string]struct {
|
|
||||||
Config map[string]interface{}
|
|
||||||
Commands map[string]bool
|
|
||||||
Uploads map[string]string
|
|
||||||
}{
|
|
||||||
"Sudo": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"ohai_hints": []interface{}{"testdata/ohaihint.json"},
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"secret_key": "SECRET-KEY",
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
},
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"sudo mkdir -p " + linuxConfDir: true,
|
|
||||||
"sudo chmod 777 " + linuxConfDir: true,
|
|
||||||
"sudo " + fmt.Sprintf(chmod, linuxConfDir, 666): true,
|
|
||||||
"sudo mkdir -p " + path.Join(linuxConfDir, "ohai/hints"): true,
|
|
||||||
"sudo chmod 777 " + path.Join(linuxConfDir, "ohai/hints"): true,
|
|
||||||
"sudo " + fmt.Sprintf(chmod, path.Join(linuxConfDir, "ohai/hints"), 666): true,
|
|
||||||
"sudo chmod 755 " + path.Join(linuxConfDir, "ohai/hints"): true,
|
|
||||||
"sudo " + fmt.Sprintf(chmod, path.Join(linuxConfDir, "ohai/hints"), 600): true,
|
|
||||||
"sudo chown -R root:root " + path.Join(linuxConfDir, "ohai/hints"): true,
|
|
||||||
"sudo chmod 755 " + linuxConfDir: true,
|
|
||||||
"sudo " + fmt.Sprintf(chmod, linuxConfDir, 600): true,
|
|
||||||
"sudo chown -R root:root " + linuxConfDir: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
Uploads: map[string]string{
|
|
||||||
linuxConfDir + "/client.rb": defaultLinuxClientConf,
|
|
||||||
linuxConfDir + "/encrypted_data_bag_secret": "SECRET-KEY",
|
|
||||||
linuxConfDir + "/first-boot.json": `{"run_list":["cookbook::recipe"]}`,
|
|
||||||
linuxConfDir + "/ohai/hints/ohaihint.json": "OHAI-HINT-FILE",
|
|
||||||
linuxConfDir + "/bob.pem": "USER-KEY",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"NoSudo": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"prevent_sudo": true,
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"secret_key": "SECRET-KEY",
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
},
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"mkdir -p " + linuxConfDir: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
Uploads: map[string]string{
|
|
||||||
linuxConfDir + "/client.rb": defaultLinuxClientConf,
|
|
||||||
linuxConfDir + "/encrypted_data_bag_secret": "SECRET-KEY",
|
|
||||||
linuxConfDir + "/first-boot.json": `{"run_list":["cookbook::recipe"]}`,
|
|
||||||
linuxConfDir + "/bob.pem": "USER-KEY",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"Proxy": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"http_proxy": "http://proxy.local",
|
|
||||||
"https_proxy": "https://proxy.local",
|
|
||||||
"no_proxy": []interface{}{"http://local.local", "https://local.local"},
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"prevent_sudo": true,
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"secret_key": "SECRET-KEY",
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"ssl_verify_mode": "verify_none",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
},
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"mkdir -p " + linuxConfDir: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
Uploads: map[string]string{
|
|
||||||
linuxConfDir + "/client.rb": proxyLinuxClientConf,
|
|
||||||
linuxConfDir + "/encrypted_data_bag_secret": "SECRET-KEY",
|
|
||||||
linuxConfDir + "/first-boot.json": `{"run_list":["cookbook::recipe"]}`,
|
|
||||||
linuxConfDir + "/bob.pem": "USER-KEY",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"Attributes JSON": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"attributes_json": `{"key1":{"subkey1":{"subkey2a":["val1","val2","val3"],` +
|
|
||||||
`"subkey2b":{"subkey3":"value3"}}},"key2":"value2"}`,
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"prevent_sudo": true,
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"secret_key": "SECRET-KEY",
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
},
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"mkdir -p " + linuxConfDir: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
Uploads: map[string]string{
|
|
||||||
linuxConfDir + "/client.rb": defaultLinuxClientConf,
|
|
||||||
linuxConfDir + "/encrypted_data_bag_secret": "SECRET-KEY",
|
|
||||||
linuxConfDir + "/bob.pem": "USER-KEY",
|
|
||||||
linuxConfDir + "/first-boot.json": `{"key1":{"subkey1":{"subkey2a":["val1","val2","val3"],` +
|
|
||||||
`"subkey2b":{"subkey3":"value3"}}},"key2":"value2","run_list":["cookbook::recipe"]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.useSudo = !p.PreventSudo
|
|
||||||
|
|
||||||
err = p.linuxCreateConfigFiles(o, c)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %q failed: %v", k, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultLinuxClientConf = `log_location STDOUT
|
|
||||||
chef_server_url "https://chef.local/"
|
|
||||||
node_name "nodename1"`
|
|
||||||
|
|
||||||
const proxyLinuxClientConf = `log_location STDOUT
|
|
||||||
chef_server_url "https://chef.local/"
|
|
||||||
node_name "nodename1"
|
|
||||||
|
|
||||||
http_proxy "http://proxy.local"
|
|
||||||
ENV['http_proxy'] = "http://proxy.local"
|
|
||||||
ENV['HTTP_PROXY'] = "http://proxy.local"
|
|
||||||
|
|
||||||
https_proxy "https://proxy.local"
|
|
||||||
ENV['https_proxy'] = "https://proxy.local"
|
|
||||||
ENV['HTTPS_PROXY'] = "https://proxy.local"
|
|
||||||
|
|
||||||
no_proxy "http://local.local,https://local.local"
|
|
||||||
ENV['no_proxy'] = "http://local.local,https://local.local"
|
|
||||||
|
|
||||||
ssl_verify_mode :verify_none`
|
|
|
@ -1,904 +0,0 @@
|
||||||
package chef
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"text/template"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/communicator"
|
|
||||||
"github.com/hashicorp/terraform/communicator/remote"
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
"github.com/mitchellh/go-homedir"
|
|
||||||
"github.com/mitchellh/go-linereader"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
clienrb = "client.rb"
|
|
||||||
defaultEnv = "_default"
|
|
||||||
firstBoot = "first-boot.json"
|
|
||||||
logfileDir = "logfiles"
|
|
||||||
linuxChefCmd = "chef-client"
|
|
||||||
linuxConfDir = "/etc/chef"
|
|
||||||
linuxNoOutput = "> /dev/null 2>&1"
|
|
||||||
linuxGemCmd = "/opt/chef/embedded/bin/gem"
|
|
||||||
linuxKnifeCmd = "knife"
|
|
||||||
secretKey = "encrypted_data_bag_secret"
|
|
||||||
windowsChefCmd = "cmd /c chef-client"
|
|
||||||
windowsConfDir = "C:/chef"
|
|
||||||
windowsNoOutput = "> nul 2>&1"
|
|
||||||
windowsGemCmd = "C:/opscode/chef/embedded/bin/gem"
|
|
||||||
windowsKnifeCmd = "cmd /c knife"
|
|
||||||
)
|
|
||||||
|
|
||||||
const clientConf = `
|
|
||||||
log_location STDOUT
|
|
||||||
chef_server_url "{{ .ServerURL }}"
|
|
||||||
node_name "{{ .NodeName }}"
|
|
||||||
{{ if .UsePolicyfile }}
|
|
||||||
use_policyfile true
|
|
||||||
policy_group "{{ .PolicyGroup }}"
|
|
||||||
policy_name "{{ .PolicyName }}"
|
|
||||||
{{ end -}}
|
|
||||||
|
|
||||||
{{ if .HTTPProxy }}
|
|
||||||
http_proxy "{{ .HTTPProxy }}"
|
|
||||||
ENV['http_proxy'] = "{{ .HTTPProxy }}"
|
|
||||||
ENV['HTTP_PROXY'] = "{{ .HTTPProxy }}"
|
|
||||||
{{ end -}}
|
|
||||||
|
|
||||||
{{ if .HTTPSProxy }}
|
|
||||||
https_proxy "{{ .HTTPSProxy }}"
|
|
||||||
ENV['https_proxy'] = "{{ .HTTPSProxy }}"
|
|
||||||
ENV['HTTPS_PROXY'] = "{{ .HTTPSProxy }}"
|
|
||||||
{{ end -}}
|
|
||||||
|
|
||||||
{{ if .NOProxy }}
|
|
||||||
no_proxy "{{ join .NOProxy "," }}"
|
|
||||||
ENV['no_proxy'] = "{{ join .NOProxy "," }}"
|
|
||||||
{{ end -}}
|
|
||||||
|
|
||||||
{{ if .SSLVerifyMode }}
|
|
||||||
ssl_verify_mode {{ .SSLVerifyMode }}
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{ if .DisableReporting }}
|
|
||||||
enable_reporting false
|
|
||||||
{{ end -}}
|
|
||||||
|
|
||||||
{{ if .ClientOptions }}
|
|
||||||
{{ join .ClientOptions "\n" }}
|
|
||||||
{{ end }}
|
|
||||||
`
|
|
||||||
|
|
||||||
type provisionFn func(terraform.UIOutput, communicator.Communicator) error
|
|
||||||
|
|
||||||
type provisioner struct {
|
|
||||||
Attributes map[string]interface{}
|
|
||||||
Channel string
|
|
||||||
ClientOptions []string
|
|
||||||
DisableReporting bool
|
|
||||||
Environment string
|
|
||||||
FetchChefCertificates bool
|
|
||||||
LogToFile bool
|
|
||||||
UsePolicyfile bool
|
|
||||||
PolicyGroup string
|
|
||||||
PolicyName string
|
|
||||||
HTTPProxy string
|
|
||||||
HTTPSProxy string
|
|
||||||
MaxRetries int
|
|
||||||
NamedRunList string
|
|
||||||
NOProxy []string
|
|
||||||
NodeName string
|
|
||||||
OhaiHints []string
|
|
||||||
OSType string
|
|
||||||
RecreateClient bool
|
|
||||||
PreventSudo bool
|
|
||||||
RetryOnExitCode map[int]bool
|
|
||||||
RunList []string
|
|
||||||
SecretKey string
|
|
||||||
ServerURL string
|
|
||||||
SkipInstall bool
|
|
||||||
SkipRegister bool
|
|
||||||
SSLVerifyMode string
|
|
||||||
UserName string
|
|
||||||
UserKey string
|
|
||||||
Vaults map[string][]string
|
|
||||||
Version string
|
|
||||||
WaitForRetry time.Duration
|
|
||||||
|
|
||||||
cleanupUserKeyCmd string
|
|
||||||
createConfigFiles provisionFn
|
|
||||||
installChefClient provisionFn
|
|
||||||
fetchChefCertificates provisionFn
|
|
||||||
generateClientKey provisionFn
|
|
||||||
configureVaults provisionFn
|
|
||||||
runChefClient provisionFn
|
|
||||||
useSudo bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provisioner returns a Chef provisioner
|
|
||||||
func Provisioner() terraform.ResourceProvisioner {
|
|
||||||
return &schema.Provisioner{
|
|
||||||
Schema: map[string]*schema.Schema{
|
|
||||||
"node_name": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"server_url": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"user_name": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"user_key": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
"attributes_json": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"channel": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
Default: "stable",
|
|
||||||
},
|
|
||||||
"client_options": &schema.Schema{
|
|
||||||
Type: schema.TypeList,
|
|
||||||
Elem: &schema.Schema{Type: schema.TypeString},
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"disable_reporting": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"environment": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
Default: defaultEnv,
|
|
||||||
},
|
|
||||||
"fetch_chef_certificates": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"log_to_file": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"use_policyfile": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"policy_group": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"policy_name": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"http_proxy": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"https_proxy": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"max_retries": &schema.Schema{
|
|
||||||
Type: schema.TypeInt,
|
|
||||||
Optional: true,
|
|
||||||
Default: 0,
|
|
||||||
},
|
|
||||||
"no_proxy": &schema.Schema{
|
|
||||||
Type: schema.TypeList,
|
|
||||||
Elem: &schema.Schema{Type: schema.TypeString},
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"named_run_list": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"ohai_hints": &schema.Schema{
|
|
||||||
Type: schema.TypeList,
|
|
||||||
Elem: &schema.Schema{Type: schema.TypeString},
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"os_type": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"prevent_sudo": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"recreate_client": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"retry_on_exit_code": &schema.Schema{
|
|
||||||
Type: schema.TypeList,
|
|
||||||
Elem: &schema.Schema{Type: schema.TypeInt},
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"run_list": &schema.Schema{
|
|
||||||
Type: schema.TypeList,
|
|
||||||
Elem: &schema.Schema{Type: schema.TypeString},
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"secret_key": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"skip_install": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"skip_register": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"ssl_verify_mode": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"vault_json": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"version": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"wait_for_retry": &schema.Schema{
|
|
||||||
Type: schema.TypeInt,
|
|
||||||
Optional: true,
|
|
||||||
Default: 30,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
ApplyFunc: applyFn,
|
|
||||||
ValidateFunc: validateFn,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Support context cancelling (Provisioner Stop)
|
|
||||||
func applyFn(ctx context.Context) error {
|
|
||||||
o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
|
|
||||||
s := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
|
|
||||||
d := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
|
|
||||||
|
|
||||||
// Decode the provisioner config
|
|
||||||
p, err := decodeConfig(d)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.OSType == "" {
|
|
||||||
switch t := s.Ephemeral.ConnInfo["type"]; t {
|
|
||||||
case "ssh", "": // The default connection type is ssh, so if the type is empty assume ssh
|
|
||||||
p.OSType = "linux"
|
|
||||||
case "winrm":
|
|
||||||
p.OSType = "windows"
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Unsupported connection type: %s", t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set some values based on the targeted OS
|
|
||||||
switch p.OSType {
|
|
||||||
case "linux":
|
|
||||||
p.cleanupUserKeyCmd = fmt.Sprintf("rm -f %s", path.Join(linuxConfDir, p.UserName+".pem"))
|
|
||||||
p.createConfigFiles = p.linuxCreateConfigFiles
|
|
||||||
p.installChefClient = p.linuxInstallChefClient
|
|
||||||
p.fetchChefCertificates = p.fetchChefCertificatesFunc(linuxKnifeCmd, linuxConfDir)
|
|
||||||
p.generateClientKey = p.generateClientKeyFunc(linuxKnifeCmd, linuxConfDir, linuxNoOutput)
|
|
||||||
p.configureVaults = p.configureVaultsFunc(linuxGemCmd, linuxKnifeCmd, linuxConfDir)
|
|
||||||
p.runChefClient = p.runChefClientFunc(linuxChefCmd, linuxConfDir)
|
|
||||||
p.useSudo = !p.PreventSudo && s.Ephemeral.ConnInfo["user"] != "root"
|
|
||||||
case "windows":
|
|
||||||
p.cleanupUserKeyCmd = fmt.Sprintf("cd %s && del /F /Q %s", windowsConfDir, p.UserName+".pem")
|
|
||||||
p.createConfigFiles = p.windowsCreateConfigFiles
|
|
||||||
p.installChefClient = p.windowsInstallChefClient
|
|
||||||
p.fetchChefCertificates = p.fetchChefCertificatesFunc(windowsKnifeCmd, windowsConfDir)
|
|
||||||
p.generateClientKey = p.generateClientKeyFunc(windowsKnifeCmd, windowsConfDir, windowsNoOutput)
|
|
||||||
p.configureVaults = p.configureVaultsFunc(windowsGemCmd, windowsKnifeCmd, windowsConfDir)
|
|
||||||
p.runChefClient = p.runChefClientFunc(windowsChefCmd, windowsConfDir)
|
|
||||||
p.useSudo = false
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Unsupported os type: %s", p.OSType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a new communicator
|
|
||||||
comm, err := communicator.New(s)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Wait and retry until we establish the connection
|
|
||||||
err = communicator.Retry(retryCtx, func() error {
|
|
||||||
return comm.Connect(o)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer comm.Disconnect()
|
|
||||||
|
|
||||||
// Make sure we always delete the user key from the new node!
|
|
||||||
var once sync.Once
|
|
||||||
cleanupUserKey := func() {
|
|
||||||
o.Output("Cleanup user key...")
|
|
||||||
if err := p.runCommand(o, comm, p.cleanupUserKeyCmd); err != nil {
|
|
||||||
o.Output("WARNING: Failed to cleanup user key on new node: " + err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer once.Do(cleanupUserKey)
|
|
||||||
|
|
||||||
if !p.SkipInstall {
|
|
||||||
if err := p.installChefClient(o, comm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
o.Output("Creating configuration files...")
|
|
||||||
if err := p.createConfigFiles(o, comm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.SkipRegister {
|
|
||||||
if p.FetchChefCertificates {
|
|
||||||
o.Output("Fetch Chef certificates...")
|
|
||||||
if err := p.fetchChefCertificates(o, comm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
o.Output("Generate the private key...")
|
|
||||||
if err := p.generateClientKey(o, comm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.Vaults != nil {
|
|
||||||
o.Output("Configure Chef vaults...")
|
|
||||||
if err := p.configureVaults(o, comm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup the user key before we run Chef-Client to prevent issues
|
|
||||||
// with rights caused by changing settings during the run.
|
|
||||||
once.Do(cleanupUserKey)
|
|
||||||
|
|
||||||
o.Output("Starting initial Chef-Client run...")
|
|
||||||
|
|
||||||
for attempt := 0; attempt <= p.MaxRetries; attempt++ {
|
|
||||||
// We need a new retry context for each attempt, to make sure
|
|
||||||
// they all get the correct timeout.
|
|
||||||
retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Make sure to (re)connect before trying to run Chef-Client.
|
|
||||||
if err := communicator.Retry(retryCtx, func() error {
|
|
||||||
return comm.Connect(o)
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = p.runChefClient(o, comm)
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow RFC062 Exit Codes:
|
|
||||||
// https://github.com/chef/chef-rfc/blob/master/rfc062-exit-status.md
|
|
||||||
exitError, ok := err.(*remote.ExitError)
|
|
||||||
if !ok {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch exitError.ExitStatus {
|
|
||||||
case 35:
|
|
||||||
o.Output("Reboot has been scheduled in the run state")
|
|
||||||
err = nil
|
|
||||||
case 37:
|
|
||||||
o.Output("Reboot needs to be completed")
|
|
||||||
err = nil
|
|
||||||
case 213:
|
|
||||||
o.Output("Chef has exited during a client upgrade")
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.RetryOnExitCode[exitError.ExitStatus] {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if attempt < p.MaxRetries {
|
|
||||||
o.Output(fmt.Sprintf("Waiting %s before retrying Chef-Client run...", p.WaitForRetry))
|
|
||||||
time.Sleep(p.WaitForRetry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) {
|
|
||||||
usePolicyFile := false
|
|
||||||
if usePolicyFileRaw, ok := c.Get("use_policyfile"); ok {
|
|
||||||
switch usePolicyFileRaw := usePolicyFileRaw.(type) {
|
|
||||||
case bool:
|
|
||||||
usePolicyFile = usePolicyFileRaw
|
|
||||||
case string:
|
|
||||||
usePolicyFileBool, err := strconv.ParseBool(usePolicyFileRaw)
|
|
||||||
if err != nil {
|
|
||||||
return ws, append(es, errors.New("\"use_policyfile\" must be a boolean"))
|
|
||||||
}
|
|
||||||
usePolicyFile = usePolicyFileBool
|
|
||||||
default:
|
|
||||||
return ws, append(es, errors.New("\"use_policyfile\" must be a boolean"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !usePolicyFile && !c.IsSet("run_list") {
|
|
||||||
es = append(es, errors.New("\"run_list\": required field is not set"))
|
|
||||||
}
|
|
||||||
if usePolicyFile && !c.IsSet("policy_name") {
|
|
||||||
es = append(es, errors.New("using policyfile, but \"policy_name\" not set"))
|
|
||||||
}
|
|
||||||
if usePolicyFile && !c.IsSet("policy_group") {
|
|
||||||
es = append(es, errors.New("using policyfile, but \"policy_group\" not set"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return ws, es
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) deployConfigFiles(o terraform.UIOutput, comm communicator.Communicator, confDir string) error {
|
|
||||||
// Copy the user key to the new instance
|
|
||||||
pk := strings.NewReader(p.UserKey)
|
|
||||||
if err := comm.Upload(path.Join(confDir, p.UserName+".pem"), pk); err != nil {
|
|
||||||
return fmt.Errorf("Uploading user key failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.SecretKey != "" {
|
|
||||||
// Copy the secret key to the new instance
|
|
||||||
s := strings.NewReader(p.SecretKey)
|
|
||||||
if err := comm.Upload(path.Join(confDir, secretKey), s); err != nil {
|
|
||||||
return fmt.Errorf("Uploading %s failed: %v", secretKey, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the SSLVerifyMode value is written as a symbol
|
|
||||||
if p.SSLVerifyMode != "" && !strings.HasPrefix(p.SSLVerifyMode, ":") {
|
|
||||||
p.SSLVerifyMode = fmt.Sprintf(":%s", p.SSLVerifyMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make strings.Join available for use within the template
|
|
||||||
funcMap := template.FuncMap{
|
|
||||||
"join": strings.Join,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new template and parse the client config into it
|
|
||||||
t := template.Must(template.New(clienrb).Funcs(funcMap).Parse(clientConf))
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
err := t.Execute(&buf, p)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Error executing %s template: %s", clienrb, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the client config to the new instance
|
|
||||||
if err = comm.Upload(path.Join(confDir, clienrb), &buf); err != nil {
|
|
||||||
return fmt.Errorf("Uploading %s failed: %v", clienrb, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a map with first boot settings
|
|
||||||
fb := make(map[string]interface{})
|
|
||||||
if p.Attributes != nil {
|
|
||||||
fb = p.Attributes
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the run_list was also in the attributes and if so log a warning
|
|
||||||
// that it will be overwritten with the value of the run_list argument.
|
|
||||||
if _, found := fb["run_list"]; found {
|
|
||||||
log.Printf("[WARN] Found a 'run_list' specified in the configured attributes! " +
|
|
||||||
"This value will be overwritten by the value of the `run_list` argument!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the initial runlist to the first boot settings
|
|
||||||
if !p.UsePolicyfile {
|
|
||||||
fb["run_list"] = p.RunList
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal the first boot settings to JSON
|
|
||||||
d, err := json.Marshal(fb)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to create %s data: %s", firstBoot, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the first-boot.json to the new instance
|
|
||||||
if err := comm.Upload(path.Join(confDir, firstBoot), bytes.NewReader(d)); err != nil {
|
|
||||||
return fmt.Errorf("Uploading %s failed: %v", firstBoot, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) deployOhaiHints(o terraform.UIOutput, comm communicator.Communicator, hintDir string) error {
|
|
||||||
for _, hint := range p.OhaiHints {
|
|
||||||
// Open the hint file
|
|
||||||
f, err := os.Open(hint)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
// Copy the hint to the new instance
|
|
||||||
if err := comm.Upload(path.Join(hintDir, path.Base(hint)), f); err != nil {
|
|
||||||
return fmt.Errorf("Uploading %s failed: %v", path.Base(hint), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) fetchChefCertificatesFunc(
|
|
||||||
knifeCmd string,
|
|
||||||
confDir string) func(terraform.UIOutput, communicator.Communicator) error {
|
|
||||||
return func(o terraform.UIOutput, comm communicator.Communicator) error {
|
|
||||||
clientrb := path.Join(confDir, clienrb)
|
|
||||||
cmd := fmt.Sprintf("%s ssl fetch -c %s", knifeCmd, clientrb)
|
|
||||||
|
|
||||||
return p.runCommand(o, comm, cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) generateClientKeyFunc(knifeCmd string, confDir string, noOutput string) provisionFn {
|
|
||||||
return func(o terraform.UIOutput, comm communicator.Communicator) error {
|
|
||||||
options := fmt.Sprintf("-c %s -u %s --key %s",
|
|
||||||
path.Join(confDir, clienrb),
|
|
||||||
p.UserName,
|
|
||||||
path.Join(confDir, p.UserName+".pem"),
|
|
||||||
)
|
|
||||||
|
|
||||||
// See if we already have a node object
|
|
||||||
getNodeCmd := fmt.Sprintf("%s node show %s %s %s", knifeCmd, p.NodeName, options, noOutput)
|
|
||||||
node := p.runCommand(o, comm, getNodeCmd) == nil
|
|
||||||
|
|
||||||
// See if we already have a client object
|
|
||||||
getClientCmd := fmt.Sprintf("%s client show %s %s %s", knifeCmd, p.NodeName, options, noOutput)
|
|
||||||
client := p.runCommand(o, comm, getClientCmd) == nil
|
|
||||||
|
|
||||||
// If we have a client, we can only continue if we are to recreate the client
|
|
||||||
if client && !p.RecreateClient {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"Chef client %q already exists, set recreate_client=true to automatically recreate the client", p.NodeName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the node exists, try to delete it
|
|
||||||
if node {
|
|
||||||
deleteNodeCmd := fmt.Sprintf("%s node delete %s -y %s",
|
|
||||||
knifeCmd,
|
|
||||||
p.NodeName,
|
|
||||||
options,
|
|
||||||
)
|
|
||||||
if err := p.runCommand(o, comm, deleteNodeCmd); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the client exists, try to delete it
|
|
||||||
if client {
|
|
||||||
deleteClientCmd := fmt.Sprintf("%s client delete %s -y %s",
|
|
||||||
knifeCmd,
|
|
||||||
p.NodeName,
|
|
||||||
options,
|
|
||||||
)
|
|
||||||
if err := p.runCommand(o, comm, deleteClientCmd); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the new client object
|
|
||||||
createClientCmd := fmt.Sprintf("%s client create %s -d -f %s %s",
|
|
||||||
knifeCmd,
|
|
||||||
p.NodeName,
|
|
||||||
path.Join(confDir, "client.pem"),
|
|
||||||
options,
|
|
||||||
)
|
|
||||||
|
|
||||||
return p.runCommand(o, comm, createClientCmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) configureVaultsFunc(gemCmd string, knifeCmd string, confDir string) provisionFn {
|
|
||||||
return func(o terraform.UIOutput, comm communicator.Communicator) error {
|
|
||||||
if err := p.runCommand(o, comm, fmt.Sprintf("%s install chef-vault", gemCmd)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
options := fmt.Sprintf("-c %s -u %s --key %s",
|
|
||||||
path.Join(confDir, clienrb),
|
|
||||||
p.UserName,
|
|
||||||
path.Join(confDir, p.UserName+".pem"),
|
|
||||||
)
|
|
||||||
|
|
||||||
// if client gets recreated, remove (old) client (with old keys) from vaults/items
|
|
||||||
// otherwise, the (new) client (with new keys) will not be able to decrypt the vault
|
|
||||||
if p.RecreateClient {
|
|
||||||
for vault, items := range p.Vaults {
|
|
||||||
for _, item := range items {
|
|
||||||
deleteCmd := fmt.Sprintf("%s vault remove %s %s -C \"%s\" -M client %s",
|
|
||||||
knifeCmd,
|
|
||||||
vault,
|
|
||||||
item,
|
|
||||||
p.NodeName,
|
|
||||||
options,
|
|
||||||
)
|
|
||||||
if err := p.runCommand(o, comm, deleteCmd); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for vault, items := range p.Vaults {
|
|
||||||
for _, item := range items {
|
|
||||||
updateCmd := fmt.Sprintf("%s vault update %s %s -C %s -M client %s",
|
|
||||||
knifeCmd,
|
|
||||||
vault,
|
|
||||||
item,
|
|
||||||
p.NodeName,
|
|
||||||
options,
|
|
||||||
)
|
|
||||||
if err := p.runCommand(o, comm, updateCmd); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) runChefClientFunc(chefCmd string, confDir string) provisionFn {
|
|
||||||
return func(o terraform.UIOutput, comm communicator.Communicator) error {
|
|
||||||
fb := path.Join(confDir, firstBoot)
|
|
||||||
var cmd string
|
|
||||||
|
|
||||||
// Policyfiles do not support chef environments, so don't pass the `-E` flag.
|
|
||||||
switch {
|
|
||||||
case p.UsePolicyfile && p.NamedRunList == "":
|
|
||||||
cmd = fmt.Sprintf("%s -j %q", chefCmd, fb)
|
|
||||||
case p.UsePolicyfile && p.NamedRunList != "":
|
|
||||||
cmd = fmt.Sprintf("%s -j %q -n %q", chefCmd, fb, p.NamedRunList)
|
|
||||||
default:
|
|
||||||
cmd = fmt.Sprintf("%s -j %q -E %q", chefCmd, fb, p.Environment)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.LogToFile {
|
|
||||||
if err := os.MkdirAll(logfileDir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("Error creating logfile directory %s: %v", logfileDir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logFile := path.Join(logfileDir, p.NodeName)
|
|
||||||
f, err := os.Create(path.Join(logFile))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Error creating logfile %s: %v", logFile, err)
|
|
||||||
}
|
|
||||||
f.Close()
|
|
||||||
|
|
||||||
o.Output("Writing Chef Client output to " + logFile)
|
|
||||||
o = p
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.runCommand(o, comm, cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output implementation of terraform.UIOutput interface
|
|
||||||
func (p *provisioner) Output(output string) {
|
|
||||||
logFile := path.Join(logfileDir, p.NodeName)
|
|
||||||
f, err := os.OpenFile(logFile, os.O_APPEND|os.O_WRONLY, 0666)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error creating logfile %s: %v", logFile, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
// These steps are needed to remove any ANSI escape codes used to colorize
|
|
||||||
// the output and to make sure we have proper line endings before writing
|
|
||||||
// the string to the logfile.
|
|
||||||
re := regexp.MustCompile(`\x1b\[[0-9;]+m`)
|
|
||||||
output = re.ReplaceAllString(output, "")
|
|
||||||
output = strings.Replace(output, "\r", "\n", -1)
|
|
||||||
|
|
||||||
if _, err := f.WriteString(output); err != nil {
|
|
||||||
log.Printf("Error writing output to logfile %s: %v", logFile, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := f.Sync(); err != nil {
|
|
||||||
log.Printf("Error saving logfile %s to disk: %v", logFile, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// runCommand is used to run already prepared commands
|
|
||||||
func (p *provisioner) runCommand(o terraform.UIOutput, comm communicator.Communicator, command string) error {
|
|
||||||
// Unless prevented, prefix the command with sudo
|
|
||||||
if p.useSudo {
|
|
||||||
command = "sudo " + command
|
|
||||||
}
|
|
||||||
|
|
||||||
outR, outW := io.Pipe()
|
|
||||||
errR, errW := io.Pipe()
|
|
||||||
go p.copyOutput(o, outR)
|
|
||||||
go p.copyOutput(o, errR)
|
|
||||||
defer outW.Close()
|
|
||||||
defer errW.Close()
|
|
||||||
|
|
||||||
cmd := &remote.Cmd{
|
|
||||||
Command: command,
|
|
||||||
Stdout: outW,
|
|
||||||
Stderr: errW,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := comm.Start(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Error executing command %q: %v", cmd.Command, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) copyOutput(o terraform.UIOutput, r io.Reader) {
|
|
||||||
lr := linereader.New(r)
|
|
||||||
for line := range lr.Ch {
|
|
||||||
o.Output(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
|
|
||||||
p := &provisioner{
|
|
||||||
Channel: d.Get("channel").(string),
|
|
||||||
ClientOptions: getStringList(d.Get("client_options")),
|
|
||||||
DisableReporting: d.Get("disable_reporting").(bool),
|
|
||||||
Environment: d.Get("environment").(string),
|
|
||||||
FetchChefCertificates: d.Get("fetch_chef_certificates").(bool),
|
|
||||||
LogToFile: d.Get("log_to_file").(bool),
|
|
||||||
UsePolicyfile: d.Get("use_policyfile").(bool),
|
|
||||||
PolicyGroup: d.Get("policy_group").(string),
|
|
||||||
PolicyName: d.Get("policy_name").(string),
|
|
||||||
HTTPProxy: d.Get("http_proxy").(string),
|
|
||||||
HTTPSProxy: d.Get("https_proxy").(string),
|
|
||||||
NOProxy: getStringList(d.Get("no_proxy")),
|
|
||||||
MaxRetries: d.Get("max_retries").(int),
|
|
||||||
NamedRunList: d.Get("named_run_list").(string),
|
|
||||||
NodeName: d.Get("node_name").(string),
|
|
||||||
OhaiHints: getStringList(d.Get("ohai_hints")),
|
|
||||||
OSType: d.Get("os_type").(string),
|
|
||||||
RecreateClient: d.Get("recreate_client").(bool),
|
|
||||||
PreventSudo: d.Get("prevent_sudo").(bool),
|
|
||||||
RetryOnExitCode: getRetryOnExitCodes(d),
|
|
||||||
RunList: getStringList(d.Get("run_list")),
|
|
||||||
SecretKey: d.Get("secret_key").(string),
|
|
||||||
ServerURL: d.Get("server_url").(string),
|
|
||||||
SkipInstall: d.Get("skip_install").(bool),
|
|
||||||
SkipRegister: d.Get("skip_register").(bool),
|
|
||||||
SSLVerifyMode: d.Get("ssl_verify_mode").(string),
|
|
||||||
UserName: d.Get("user_name").(string),
|
|
||||||
UserKey: d.Get("user_key").(string),
|
|
||||||
Version: d.Get("version").(string),
|
|
||||||
WaitForRetry: time.Duration(d.Get("wait_for_retry").(int)) * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the supplied URL has a trailing slash
|
|
||||||
p.ServerURL = strings.TrimSuffix(p.ServerURL, "/") + "/"
|
|
||||||
|
|
||||||
for i, hint := range p.OhaiHints {
|
|
||||||
hintPath, err := homedir.Expand(hint)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Error expanding the path %s: %v", hint, err)
|
|
||||||
}
|
|
||||||
p.OhaiHints[i] = hintPath
|
|
||||||
}
|
|
||||||
|
|
||||||
if attrs, ok := d.GetOk("attributes_json"); ok {
|
|
||||||
var m map[string]interface{}
|
|
||||||
if err := json.Unmarshal([]byte(attrs.(string)), &m); err != nil {
|
|
||||||
return nil, fmt.Errorf("Error parsing attributes_json: %v", err)
|
|
||||||
}
|
|
||||||
p.Attributes = m
|
|
||||||
}
|
|
||||||
|
|
||||||
if vaults, ok := d.GetOk("vault_json"); ok {
|
|
||||||
var m map[string]interface{}
|
|
||||||
if err := json.Unmarshal([]byte(vaults.(string)), &m); err != nil {
|
|
||||||
return nil, fmt.Errorf("Error parsing vault_json: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
v := make(map[string][]string)
|
|
||||||
for vault, items := range m {
|
|
||||||
switch items := items.(type) {
|
|
||||||
case []interface{}:
|
|
||||||
for _, item := range items {
|
|
||||||
if item, ok := item.(string); ok {
|
|
||||||
v[vault] = append(v[vault], item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case interface{}:
|
|
||||||
if item, ok := items.(string); ok {
|
|
||||||
v[vault] = append(v[vault], item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Vaults = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRetryOnExitCodes(d *schema.ResourceData) map[int]bool {
|
|
||||||
result := make(map[int]bool)
|
|
||||||
|
|
||||||
v, ok := d.GetOk("retry_on_exit_code")
|
|
||||||
if !ok || v == nil {
|
|
||||||
// Use default exit codes
|
|
||||||
result[35] = true
|
|
||||||
result[37] = true
|
|
||||||
result[213] = true
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := v.(type) {
|
|
||||||
case []interface{}:
|
|
||||||
for _, vv := range v {
|
|
||||||
if vv, ok := vv.(int); ok {
|
|
||||||
result[vv] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("Unsupported type: %T", v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getStringList(v interface{}) []string {
|
|
||||||
var result []string
|
|
||||||
|
|
||||||
switch v := v.(type) {
|
|
||||||
case nil:
|
|
||||||
return result
|
|
||||||
case []interface{}:
|
|
||||||
for _, vv := range v {
|
|
||||||
if vv, ok := vv.(string); ok {
|
|
||||||
result = append(result, vv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("Unsupported type: %T", v))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,435 +0,0 @@
|
||||||
package chef
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/communicator"
|
|
||||||
"github.com/hashicorp/terraform/configs/hcl2shim"
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestResourceProvisioner_impl(t *testing.T) {
|
|
||||||
var _ terraform.ResourceProvisioner = Provisioner()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisioner(t *testing.T) {
|
|
||||||
if err := Provisioner().(*schema.Provisioner).InternalValidate(); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvider_Validate_good(t *testing.T) {
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"environment": "_default",
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
})
|
|
||||||
|
|
||||||
warn, errs := Provisioner().Validate(c)
|
|
||||||
if len(warn) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warn)
|
|
||||||
}
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Fatalf("Errors: %v", errs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvider_Validate_bad(t *testing.T) {
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"invalid": "nope",
|
|
||||||
})
|
|
||||||
|
|
||||||
warn, errs := Provisioner().Validate(c)
|
|
||||||
if len(warn) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warn)
|
|
||||||
}
|
|
||||||
if len(errs) == 0 {
|
|
||||||
t.Fatalf("Should have errors")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that the JSON attributes with an unknown value don't
|
|
||||||
// validate.
|
|
||||||
func TestResourceProvider_Validate_computedValues(t *testing.T) {
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"environment": "_default",
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
"attributes_json": hcl2shim.UnknownVariableValue,
|
|
||||||
})
|
|
||||||
|
|
||||||
warn, errs := Provisioner().Validate(c)
|
|
||||||
if len(warn) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warn)
|
|
||||||
}
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Fatalf("Errors: %v", errs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvider_runChefClient(t *testing.T) {
|
|
||||||
cases := map[string]struct {
|
|
||||||
Config map[string]interface{}
|
|
||||||
ChefCmd string
|
|
||||||
ConfDir string
|
|
||||||
Commands map[string]bool
|
|
||||||
}{
|
|
||||||
"Sudo": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
},
|
|
||||||
|
|
||||||
ChefCmd: linuxChefCmd,
|
|
||||||
|
|
||||||
ConfDir: linuxConfDir,
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
fmt.Sprintf(`sudo %s -j %q -E "_default"`,
|
|
||||||
linuxChefCmd,
|
|
||||||
path.Join(linuxConfDir, "first-boot.json")): true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"NoSudo": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"prevent_sudo": true,
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
},
|
|
||||||
|
|
||||||
ChefCmd: linuxChefCmd,
|
|
||||||
|
|
||||||
ConfDir: linuxConfDir,
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
fmt.Sprintf(`%s -j %q -E "_default"`,
|
|
||||||
linuxChefCmd,
|
|
||||||
path.Join(linuxConfDir, "first-boot.json")): true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"Environment": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"environment": "production",
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"prevent_sudo": true,
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
},
|
|
||||||
|
|
||||||
ChefCmd: windowsChefCmd,
|
|
||||||
|
|
||||||
ConfDir: windowsConfDir,
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
fmt.Sprintf(`%s -j %q -E "production"`,
|
|
||||||
windowsChefCmd,
|
|
||||||
path.Join(windowsConfDir, "first-boot.json")): 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.runChefClient = p.runChefClientFunc(tc.ChefCmd, tc.ConfDir)
|
|
||||||
p.useSudo = !p.PreventSudo
|
|
||||||
|
|
||||||
err = p.runChefClient(o, c)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %q failed: %v", k, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvider_fetchChefCertificates(t *testing.T) {
|
|
||||||
cases := map[string]struct {
|
|
||||||
Config map[string]interface{}
|
|
||||||
KnifeCmd string
|
|
||||||
ConfDir string
|
|
||||||
Commands map[string]bool
|
|
||||||
}{
|
|
||||||
"Sudo": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"fetch_chef_certificates": true,
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
},
|
|
||||||
|
|
||||||
KnifeCmd: linuxKnifeCmd,
|
|
||||||
|
|
||||||
ConfDir: linuxConfDir,
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
fmt.Sprintf(`sudo %s ssl fetch -c %s`,
|
|
||||||
linuxKnifeCmd,
|
|
||||||
path.Join(linuxConfDir, "client.rb")): true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"NoSudo": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"fetch_chef_certificates": true,
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"prevent_sudo": true,
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
},
|
|
||||||
|
|
||||||
KnifeCmd: windowsKnifeCmd,
|
|
||||||
|
|
||||||
ConfDir: windowsConfDir,
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
fmt.Sprintf(`%s ssl fetch -c %s`,
|
|
||||||
windowsKnifeCmd,
|
|
||||||
path.Join(windowsConfDir, "client.rb")): 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.fetchChefCertificates = p.fetchChefCertificatesFunc(tc.KnifeCmd, tc.ConfDir)
|
|
||||||
p.useSudo = !p.PreventSudo
|
|
||||||
|
|
||||||
err = p.fetchChefCertificates(o, c)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %q failed: %v", k, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvider_configureVaults(t *testing.T) {
|
|
||||||
cases := map[string]struct {
|
|
||||||
Config map[string]interface{}
|
|
||||||
GemCmd string
|
|
||||||
KnifeCmd string
|
|
||||||
ConfDir string
|
|
||||||
Commands map[string]bool
|
|
||||||
}{
|
|
||||||
"Linux Vault string": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"prevent_sudo": true,
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
"vault_json": `{"vault1": "item1"}`,
|
|
||||||
},
|
|
||||||
|
|
||||||
GemCmd: linuxGemCmd,
|
|
||||||
KnifeCmd: linuxKnifeCmd,
|
|
||||||
ConfDir: linuxConfDir,
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
fmt.Sprintf("%s install chef-vault", linuxGemCmd): true,
|
|
||||||
fmt.Sprintf("%s vault update vault1 item1 -C nodename1 -M client -c %s/client.rb "+
|
|
||||||
"-u bob --key %s/bob.pem", linuxKnifeCmd, linuxConfDir, linuxConfDir): true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"Linux Vault []string": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"fetch_chef_certificates": true,
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"prevent_sudo": true,
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
"vault_json": `{"vault1": ["item1", "item2"]}`,
|
|
||||||
},
|
|
||||||
|
|
||||||
GemCmd: linuxGemCmd,
|
|
||||||
KnifeCmd: linuxKnifeCmd,
|
|
||||||
ConfDir: linuxConfDir,
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
fmt.Sprintf("%s install chef-vault", linuxGemCmd): true,
|
|
||||||
fmt.Sprintf("%s vault update vault1 item1 -C nodename1 -M client -c %s/client.rb "+
|
|
||||||
"-u bob --key %s/bob.pem", linuxKnifeCmd, linuxConfDir, linuxConfDir): true,
|
|
||||||
fmt.Sprintf("%s vault update vault1 item2 -C nodename1 -M client -c %s/client.rb "+
|
|
||||||
"-u bob --key %s/bob.pem", linuxKnifeCmd, linuxConfDir, linuxConfDir): true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"Linux Vault []string (recreate-client for vault)": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"fetch_chef_certificates": true,
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"prevent_sudo": true,
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
"vault_json": `{"vault1": ["item1", "item2"]}`,
|
|
||||||
"recreate_client": true,
|
|
||||||
},
|
|
||||||
|
|
||||||
GemCmd: linuxGemCmd,
|
|
||||||
KnifeCmd: linuxKnifeCmd,
|
|
||||||
ConfDir: linuxConfDir,
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
fmt.Sprintf("%s install chef-vault", linuxGemCmd): true,
|
|
||||||
fmt.Sprintf("%s vault remove vault1 item1 -C \"nodename1\" -M client -c %s/client.rb "+
|
|
||||||
"-u bob --key %s/bob.pem", linuxKnifeCmd, linuxConfDir, linuxConfDir): true,
|
|
||||||
fmt.Sprintf("%s vault remove vault1 item2 -C \"nodename1\" -M client -c %s/client.rb "+
|
|
||||||
"-u bob --key %s/bob.pem", linuxKnifeCmd, linuxConfDir, linuxConfDir): true,
|
|
||||||
fmt.Sprintf("%s vault update vault1 item1 -C nodename1 -M client -c %s/client.rb "+
|
|
||||||
"-u bob --key %s/bob.pem", linuxKnifeCmd, linuxConfDir, linuxConfDir): true,
|
|
||||||
fmt.Sprintf("%s vault update vault1 item2 -C nodename1 -M client -c %s/client.rb "+
|
|
||||||
"-u bob --key %s/bob.pem", linuxKnifeCmd, linuxConfDir, linuxConfDir): true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"Windows Vault string": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"prevent_sudo": true,
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
"vault_json": `{"vault1": "item1"}`,
|
|
||||||
},
|
|
||||||
|
|
||||||
GemCmd: windowsGemCmd,
|
|
||||||
KnifeCmd: windowsKnifeCmd,
|
|
||||||
ConfDir: windowsConfDir,
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
fmt.Sprintf("%s install chef-vault", windowsGemCmd): true,
|
|
||||||
fmt.Sprintf("%s vault update vault1 item1 -C nodename1 -M client -c %s/client.rb "+
|
|
||||||
"-u bob --key %s/bob.pem", windowsKnifeCmd, windowsConfDir, windowsConfDir): true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"Windows Vault []string": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"fetch_chef_certificates": true,
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"prevent_sudo": true,
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
"vault_json": `{"vault1": ["item1", "item2"]}`,
|
|
||||||
},
|
|
||||||
|
|
||||||
GemCmd: windowsGemCmd,
|
|
||||||
KnifeCmd: windowsKnifeCmd,
|
|
||||||
ConfDir: windowsConfDir,
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
fmt.Sprintf("%s install chef-vault", windowsGemCmd): true,
|
|
||||||
fmt.Sprintf("%s vault update vault1 item1 -C nodename1 -M client -c %s/client.rb "+
|
|
||||||
"-u bob --key %s/bob.pem", windowsKnifeCmd, windowsConfDir, windowsConfDir): true,
|
|
||||||
fmt.Sprintf("%s vault update vault1 item2 -C nodename1 -M client -c %s/client.rb "+
|
|
||||||
"-u bob --key %s/bob.pem", windowsKnifeCmd, windowsConfDir, windowsConfDir): true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"Windows Vault [] string (recreate-client for vault)": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"fetch_chef_certificates": true,
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"prevent_sudo": true,
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
"vault_json": `{"vault1": ["item1", "item2"]}`,
|
|
||||||
"recreate_client": true,
|
|
||||||
},
|
|
||||||
|
|
||||||
GemCmd: windowsGemCmd,
|
|
||||||
KnifeCmd: windowsKnifeCmd,
|
|
||||||
ConfDir: windowsConfDir,
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
fmt.Sprintf("%s install chef-vault", windowsGemCmd): true,
|
|
||||||
fmt.Sprintf("%s vault remove vault1 item1 -C \"nodename1\" -M client -c %s/client.rb "+
|
|
||||||
"-u bob --key %s/bob.pem", windowsKnifeCmd, windowsConfDir, windowsConfDir): true,
|
|
||||||
fmt.Sprintf("%s vault remove vault1 item2 -C \"nodename1\" -M client -c %s/client.rb "+
|
|
||||||
"-u bob --key %s/bob.pem", windowsKnifeCmd, windowsConfDir, windowsConfDir): true,
|
|
||||||
fmt.Sprintf("%s vault update vault1 item1 -C nodename1 -M client -c %s/client.rb "+
|
|
||||||
"-u bob --key %s/bob.pem", windowsKnifeCmd, windowsConfDir, windowsConfDir): true,
|
|
||||||
fmt.Sprintf("%s vault update vault1 item2 -C nodename1 -M client -c %s/client.rb "+
|
|
||||||
"-u bob --key %s/bob.pem", windowsKnifeCmd, windowsConfDir, windowsConfDir): 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.configureVaults = p.configureVaultsFunc(tc.GemCmd, tc.KnifeCmd, tc.ConfDir)
|
|
||||||
p.useSudo = !p.PreventSudo
|
|
||||||
|
|
||||||
err = p.configureVaults(o, c)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %q failed: %v", k, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig {
|
|
||||||
return terraform.NewResourceConfigRaw(c)
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
OHAI-HINT-FILE
|
|
|
@ -1,84 +0,0 @@
|
||||||
package chef
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/communicator"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
)
|
|
||||||
|
|
||||||
const installScript = `
|
|
||||||
$winver = [System.Environment]::OSVersion.Version | %% {"{0}.{1}" -f $_.Major,$_.Minor}
|
|
||||||
|
|
||||||
switch ($winver)
|
|
||||||
{
|
|
||||||
"6.0" {$machine_os = "2008"}
|
|
||||||
"6.1" {$machine_os = "2008r2"}
|
|
||||||
"6.2" {$machine_os = "2012"}
|
|
||||||
"6.3" {$machine_os = "2012"}
|
|
||||||
default {$machine_os = "2008r2"}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([System.IntPtr]::Size -eq 4) {$machine_arch = "i686"} else {$machine_arch = "x86_64"}
|
|
||||||
|
|
||||||
$url = "http://omnitruck.chef.io/%s/chef/download?p=windows&pv=$machine_os&m=$machine_arch&v=%s"
|
|
||||||
$dest = [System.IO.Path]::GetTempFileName()
|
|
||||||
$dest = [System.IO.Path]::ChangeExtension($dest, ".msi")
|
|
||||||
$downloader = New-Object System.Net.WebClient
|
|
||||||
|
|
||||||
$http_proxy = '%s'
|
|
||||||
if ($http_proxy -ne '') {
|
|
||||||
$no_proxy = '%s'
|
|
||||||
if ($no_proxy -eq ''){
|
|
||||||
$no_proxy = "127.0.0.1"
|
|
||||||
}
|
|
||||||
|
|
||||||
$proxy = New-Object System.Net.WebProxy($http_proxy, $true, ,$no_proxy.Split(','))
|
|
||||||
$downloader.proxy = $proxy
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host 'Downloading Chef Client...'
|
|
||||||
$downloader.DownloadFile($url, $dest)
|
|
||||||
|
|
||||||
Write-Host 'Installing Chef Client...'
|
|
||||||
Start-Process -FilePath msiexec -ArgumentList /qn, /i, $dest -Wait
|
|
||||||
`
|
|
||||||
|
|
||||||
func (p *provisioner) windowsInstallChefClient(o terraform.UIOutput, comm communicator.Communicator) error {
|
|
||||||
script := path.Join(path.Dir(comm.ScriptPath()), "ChefClient.ps1")
|
|
||||||
content := fmt.Sprintf(installScript, p.Channel, p.Version, p.HTTPProxy, strings.Join(p.NOProxy, ","))
|
|
||||||
|
|
||||||
// Copy the script to the new instance
|
|
||||||
if err := comm.UploadScript(script, strings.NewReader(content)); err != nil {
|
|
||||||
return fmt.Errorf("Uploading client.rb failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the script to install Chef Client
|
|
||||||
installCmd := fmt.Sprintf("powershell -NoProfile -ExecutionPolicy Bypass -File %s", script)
|
|
||||||
return p.runCommand(o, comm, installCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) windowsCreateConfigFiles(o terraform.UIOutput, comm communicator.Communicator) error {
|
|
||||||
// Make sure the config directory exists
|
|
||||||
cmd := fmt.Sprintf("cmd /c if not exist %q mkdir %q", windowsConfDir, windowsConfDir)
|
|
||||||
if err := p.runCommand(o, comm, cmd); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(p.OhaiHints) > 0 {
|
|
||||||
// Make sure the hits directory exists
|
|
||||||
hintsDir := path.Join(windowsConfDir, "ohai/hints")
|
|
||||||
cmd := fmt.Sprintf("cmd /c if not exist %q mkdir %q", hintsDir, hintsDir)
|
|
||||||
if err := p.runCommand(o, comm, cmd); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.deployOhaiHints(o, comm, hintsDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.deployConfigFiles(o, comm, windowsConfDir)
|
|
||||||
}
|
|
|
@ -1,394 +0,0 @@
|
||||||
package chef
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/communicator"
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestResourceProvider_windowsInstallChefClient(t *testing.T) {
|
|
||||||
cases := map[string]struct {
|
|
||||||
Config map[string]interface{}
|
|
||||||
Commands map[string]bool
|
|
||||||
UploadScripts map[string]string
|
|
||||||
}{
|
|
||||||
"Default": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
},
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"powershell -NoProfile -ExecutionPolicy Bypass -File ChefClient.ps1": true,
|
|
||||||
},
|
|
||||||
|
|
||||||
UploadScripts: map[string]string{
|
|
||||||
"ChefClient.ps1": defaultWindowsInstallScript,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"Proxy": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"http_proxy": "http://proxy.local",
|
|
||||||
"no_proxy": []interface{}{"http://local.local", "http://local.org"},
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
},
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"powershell -NoProfile -ExecutionPolicy Bypass -File ChefClient.ps1": true,
|
|
||||||
},
|
|
||||||
|
|
||||||
UploadScripts: map[string]string{
|
|
||||||
"ChefClient.ps1": proxyWindowsInstallScript,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"Version": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
"version": "11.18.6",
|
|
||||||
},
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"powershell -NoProfile -ExecutionPolicy Bypass -File ChefClient.ps1": true,
|
|
||||||
},
|
|
||||||
|
|
||||||
UploadScripts: map[string]string{
|
|
||||||
"ChefClient.ps1": versionWindowsInstallScript,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"Channel": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"channel": "current",
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
"version": "11.18.6",
|
|
||||||
},
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"powershell -NoProfile -ExecutionPolicy Bypass -File ChefClient.ps1": true,
|
|
||||||
},
|
|
||||||
|
|
||||||
UploadScripts: map[string]string{
|
|
||||||
"ChefClient.ps1": channelWindowsInstallScript,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
o := new(terraform.MockUIOutput)
|
|
||||||
c := new(communicator.MockCommunicator)
|
|
||||||
|
|
||||||
for k, tc := range cases {
|
|
||||||
c.Commands = tc.Commands
|
|
||||||
c.UploadScripts = tc.UploadScripts
|
|
||||||
|
|
||||||
p, err := decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.useSudo = false
|
|
||||||
|
|
||||||
err = p.windowsInstallChefClient(o, c)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %q failed: %v", k, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvider_windowsCreateConfigFiles(t *testing.T) {
|
|
||||||
cases := map[string]struct {
|
|
||||||
Config map[string]interface{}
|
|
||||||
Commands map[string]bool
|
|
||||||
Uploads map[string]string
|
|
||||||
}{
|
|
||||||
"Default": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"ohai_hints": []interface{}{"testdata/ohaihint.json"},
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"secret_key": "SECRET-KEY",
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
},
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
fmt.Sprintf("cmd /c if not exist %q mkdir %q", windowsConfDir, windowsConfDir): true,
|
|
||||||
fmt.Sprintf("cmd /c if not exist %q mkdir %q",
|
|
||||||
path.Join(windowsConfDir, "ohai/hints"),
|
|
||||||
path.Join(windowsConfDir, "ohai/hints")): true,
|
|
||||||
},
|
|
||||||
|
|
||||||
Uploads: map[string]string{
|
|
||||||
windowsConfDir + "/client.rb": defaultWindowsClientConf,
|
|
||||||
windowsConfDir + "/encrypted_data_bag_secret": "SECRET-KEY",
|
|
||||||
windowsConfDir + "/first-boot.json": `{"run_list":["cookbook::recipe"]}`,
|
|
||||||
windowsConfDir + "/ohai/hints/ohaihint.json": "OHAI-HINT-FILE",
|
|
||||||
windowsConfDir + "/bob.pem": "USER-KEY",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"Proxy": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"http_proxy": "http://proxy.local",
|
|
||||||
"https_proxy": "https://proxy.local",
|
|
||||||
"no_proxy": []interface{}{"http://local.local", "https://local.local"},
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"secret_key": "SECRET-KEY",
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"ssl_verify_mode": "verify_none",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
},
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
fmt.Sprintf("cmd /c if not exist %q mkdir %q", windowsConfDir, windowsConfDir): true,
|
|
||||||
},
|
|
||||||
|
|
||||||
Uploads: map[string]string{
|
|
||||||
windowsConfDir + "/client.rb": proxyWindowsClientConf,
|
|
||||||
windowsConfDir + "/first-boot.json": `{"run_list":["cookbook::recipe"]}`,
|
|
||||||
windowsConfDir + "/encrypted_data_bag_secret": "SECRET-KEY",
|
|
||||||
windowsConfDir + "/bob.pem": "USER-KEY",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"Attributes JSON": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"attributes_json": `{"key1":{"subkey1":{"subkey2a":["val1","val2","val3"],` +
|
|
||||||
`"subkey2b":{"subkey3":"value3"}}},"key2":"value2"}`,
|
|
||||||
"node_name": "nodename1",
|
|
||||||
"run_list": []interface{}{"cookbook::recipe"},
|
|
||||||
"secret_key": "SECRET-KEY",
|
|
||||||
"server_url": "https://chef.local",
|
|
||||||
"user_name": "bob",
|
|
||||||
"user_key": "USER-KEY",
|
|
||||||
},
|
|
||||||
|
|
||||||
Commands: map[string]bool{
|
|
||||||
fmt.Sprintf("cmd /c if not exist %q mkdir %q", windowsConfDir, windowsConfDir): true,
|
|
||||||
},
|
|
||||||
|
|
||||||
Uploads: map[string]string{
|
|
||||||
windowsConfDir + "/client.rb": defaultWindowsClientConf,
|
|
||||||
windowsConfDir + "/encrypted_data_bag_secret": "SECRET-KEY",
|
|
||||||
windowsConfDir + "/bob.pem": "USER-KEY",
|
|
||||||
windowsConfDir + "/first-boot.json": `{"key1":{"subkey1":{"subkey2a":["val1","val2","val3"],` +
|
|
||||||
`"subkey2b":{"subkey3":"value3"}}},"key2":"value2","run_list":["cookbook::recipe"]}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.useSudo = false
|
|
||||||
|
|
||||||
err = p.windowsCreateConfigFiles(o, c)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %q failed: %v", k, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultWindowsInstallScript = `
|
|
||||||
$winver = [System.Environment]::OSVersion.Version | % {"{0}.{1}" -f $_.Major,$_.Minor}
|
|
||||||
|
|
||||||
switch ($winver)
|
|
||||||
{
|
|
||||||
"6.0" {$machine_os = "2008"}
|
|
||||||
"6.1" {$machine_os = "2008r2"}
|
|
||||||
"6.2" {$machine_os = "2012"}
|
|
||||||
"6.3" {$machine_os = "2012"}
|
|
||||||
default {$machine_os = "2008r2"}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([System.IntPtr]::Size -eq 4) {$machine_arch = "i686"} else {$machine_arch = "x86_64"}
|
|
||||||
|
|
||||||
$url = "http://omnitruck.chef.io/stable/chef/download?p=windows&pv=$machine_os&m=$machine_arch&v="
|
|
||||||
$dest = [System.IO.Path]::GetTempFileName()
|
|
||||||
$dest = [System.IO.Path]::ChangeExtension($dest, ".msi")
|
|
||||||
$downloader = New-Object System.Net.WebClient
|
|
||||||
|
|
||||||
$http_proxy = ''
|
|
||||||
if ($http_proxy -ne '') {
|
|
||||||
$no_proxy = ''
|
|
||||||
if ($no_proxy -eq ''){
|
|
||||||
$no_proxy = "127.0.0.1"
|
|
||||||
}
|
|
||||||
|
|
||||||
$proxy = New-Object System.Net.WebProxy($http_proxy, $true, ,$no_proxy.Split(','))
|
|
||||||
$downloader.proxy = $proxy
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host 'Downloading Chef Client...'
|
|
||||||
$downloader.DownloadFile($url, $dest)
|
|
||||||
|
|
||||||
Write-Host 'Installing Chef Client...'
|
|
||||||
Start-Process -FilePath msiexec -ArgumentList /qn, /i, $dest -Wait
|
|
||||||
`
|
|
||||||
|
|
||||||
const proxyWindowsInstallScript = `
|
|
||||||
$winver = [System.Environment]::OSVersion.Version | % {"{0}.{1}" -f $_.Major,$_.Minor}
|
|
||||||
|
|
||||||
switch ($winver)
|
|
||||||
{
|
|
||||||
"6.0" {$machine_os = "2008"}
|
|
||||||
"6.1" {$machine_os = "2008r2"}
|
|
||||||
"6.2" {$machine_os = "2012"}
|
|
||||||
"6.3" {$machine_os = "2012"}
|
|
||||||
default {$machine_os = "2008r2"}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([System.IntPtr]::Size -eq 4) {$machine_arch = "i686"} else {$machine_arch = "x86_64"}
|
|
||||||
|
|
||||||
$url = "http://omnitruck.chef.io/stable/chef/download?p=windows&pv=$machine_os&m=$machine_arch&v="
|
|
||||||
$dest = [System.IO.Path]::GetTempFileName()
|
|
||||||
$dest = [System.IO.Path]::ChangeExtension($dest, ".msi")
|
|
||||||
$downloader = New-Object System.Net.WebClient
|
|
||||||
|
|
||||||
$http_proxy = 'http://proxy.local'
|
|
||||||
if ($http_proxy -ne '') {
|
|
||||||
$no_proxy = 'http://local.local,http://local.org'
|
|
||||||
if ($no_proxy -eq ''){
|
|
||||||
$no_proxy = "127.0.0.1"
|
|
||||||
}
|
|
||||||
|
|
||||||
$proxy = New-Object System.Net.WebProxy($http_proxy, $true, ,$no_proxy.Split(','))
|
|
||||||
$downloader.proxy = $proxy
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host 'Downloading Chef Client...'
|
|
||||||
$downloader.DownloadFile($url, $dest)
|
|
||||||
|
|
||||||
Write-Host 'Installing Chef Client...'
|
|
||||||
Start-Process -FilePath msiexec -ArgumentList /qn, /i, $dest -Wait
|
|
||||||
`
|
|
||||||
|
|
||||||
const versionWindowsInstallScript = `
|
|
||||||
$winver = [System.Environment]::OSVersion.Version | % {"{0}.{1}" -f $_.Major,$_.Minor}
|
|
||||||
|
|
||||||
switch ($winver)
|
|
||||||
{
|
|
||||||
"6.0" {$machine_os = "2008"}
|
|
||||||
"6.1" {$machine_os = "2008r2"}
|
|
||||||
"6.2" {$machine_os = "2012"}
|
|
||||||
"6.3" {$machine_os = "2012"}
|
|
||||||
default {$machine_os = "2008r2"}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([System.IntPtr]::Size -eq 4) {$machine_arch = "i686"} else {$machine_arch = "x86_64"}
|
|
||||||
|
|
||||||
$url = "http://omnitruck.chef.io/stable/chef/download?p=windows&pv=$machine_os&m=$machine_arch&v=11.18.6"
|
|
||||||
$dest = [System.IO.Path]::GetTempFileName()
|
|
||||||
$dest = [System.IO.Path]::ChangeExtension($dest, ".msi")
|
|
||||||
$downloader = New-Object System.Net.WebClient
|
|
||||||
|
|
||||||
$http_proxy = ''
|
|
||||||
if ($http_proxy -ne '') {
|
|
||||||
$no_proxy = ''
|
|
||||||
if ($no_proxy -eq ''){
|
|
||||||
$no_proxy = "127.0.0.1"
|
|
||||||
}
|
|
||||||
|
|
||||||
$proxy = New-Object System.Net.WebProxy($http_proxy, $true, ,$no_proxy.Split(','))
|
|
||||||
$downloader.proxy = $proxy
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host 'Downloading Chef Client...'
|
|
||||||
$downloader.DownloadFile($url, $dest)
|
|
||||||
|
|
||||||
Write-Host 'Installing Chef Client...'
|
|
||||||
Start-Process -FilePath msiexec -ArgumentList /qn, /i, $dest -Wait
|
|
||||||
`
|
|
||||||
const channelWindowsInstallScript = `
|
|
||||||
$winver = [System.Environment]::OSVersion.Version | % {"{0}.{1}" -f $_.Major,$_.Minor}
|
|
||||||
|
|
||||||
switch ($winver)
|
|
||||||
{
|
|
||||||
"6.0" {$machine_os = "2008"}
|
|
||||||
"6.1" {$machine_os = "2008r2"}
|
|
||||||
"6.2" {$machine_os = "2012"}
|
|
||||||
"6.3" {$machine_os = "2012"}
|
|
||||||
default {$machine_os = "2008r2"}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([System.IntPtr]::Size -eq 4) {$machine_arch = "i686"} else {$machine_arch = "x86_64"}
|
|
||||||
|
|
||||||
$url = "http://omnitruck.chef.io/current/chef/download?p=windows&pv=$machine_os&m=$machine_arch&v=11.18.6"
|
|
||||||
$dest = [System.IO.Path]::GetTempFileName()
|
|
||||||
$dest = [System.IO.Path]::ChangeExtension($dest, ".msi")
|
|
||||||
$downloader = New-Object System.Net.WebClient
|
|
||||||
|
|
||||||
$http_proxy = ''
|
|
||||||
if ($http_proxy -ne '') {
|
|
||||||
$no_proxy = ''
|
|
||||||
if ($no_proxy -eq ''){
|
|
||||||
$no_proxy = "127.0.0.1"
|
|
||||||
}
|
|
||||||
|
|
||||||
$proxy = New-Object System.Net.WebProxy($http_proxy, $true, ,$no_proxy.Split(','))
|
|
||||||
$downloader.proxy = $proxy
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host 'Downloading Chef Client...'
|
|
||||||
$downloader.DownloadFile($url, $dest)
|
|
||||||
|
|
||||||
Write-Host 'Installing Chef Client...'
|
|
||||||
Start-Process -FilePath msiexec -ArgumentList /qn, /i, $dest -Wait
|
|
||||||
`
|
|
||||||
|
|
||||||
const defaultWindowsClientConf = `log_location STDOUT
|
|
||||||
chef_server_url "https://chef.local/"
|
|
||||||
node_name "nodename1"`
|
|
||||||
|
|
||||||
const proxyWindowsClientConf = `log_location STDOUT
|
|
||||||
chef_server_url "https://chef.local/"
|
|
||||||
node_name "nodename1"
|
|
||||||
|
|
||||||
http_proxy "http://proxy.local"
|
|
||||||
ENV['http_proxy'] = "http://proxy.local"
|
|
||||||
ENV['HTTP_PROXY'] = "http://proxy.local"
|
|
||||||
|
|
||||||
https_proxy "https://proxy.local"
|
|
||||||
ENV['https_proxy'] = "https://proxy.local"
|
|
||||||
ENV['HTTPS_PROXY'] = "https://proxy.local"
|
|
||||||
|
|
||||||
no_proxy "http://local.local,https://local.local"
|
|
||||||
ENV['no_proxy'] = "http://local.local,https://local.local"
|
|
||||||
|
|
||||||
ssl_verify_mode :verify_none`
|
|
|
@ -1,377 +0,0 @@
|
||||||
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("chown root:root %s && chmod 0600 %s && mv %s %s", tempPath, tempPath, tempPath, 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 {
|
|
||||||
checksum := service.getServiceNameChecksum()
|
|
||||||
if err := comm.Upload(fmt.Sprintf("/tmp/user-%s.toml", checksum), userToml); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
command = p.linuxGetCommand(fmt.Sprintf("chmod o-r /tmp/user-%s.toml && mv /tmp/user-%s.toml %s/user.toml", checksum, checksum, 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
|
|
||||||
}
|
|
|
@ -1,348 +0,0 @@
|
||||||
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 'chmod o-r /tmp/user-a5b83ec1b302d109f41852ae17379f75c36dff9bc598aae76b6f7c9cd425fd76.toml && 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 'chmod o-r /tmp/user-6466ae3283ae1bd4737b00367bc676c6465b25682169ea5f7da222f3f078a5bf.toml && 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,572 +0,0 @@
|
||||||
package habitat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/sha256"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
version "github.com/hashicorp/go-version"
|
|
||||||
"github.com/hashicorp/terraform/communicator"
|
|
||||||
"github.com/hashicorp/terraform/communicator/remote"
|
|
||||||
"github.com/hashicorp/terraform/configs/hcl2shim"
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
|
||||||
"github.com/hashicorp/terraform/helper/validation"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
"github.com/mitchellh/go-linereader"
|
|
||||||
)
|
|
||||||
|
|
||||||
type provisioner struct {
|
|
||||||
Version string
|
|
||||||
AutoUpdate bool
|
|
||||||
HttpDisable bool
|
|
||||||
Services []Service
|
|
||||||
PermanentPeer bool
|
|
||||||
ListenCtl string
|
|
||||||
ListenGossip string
|
|
||||||
ListenHTTP string
|
|
||||||
Peer string
|
|
||||||
Peers []string
|
|
||||||
RingKey string
|
|
||||||
RingKeyContent string
|
|
||||||
CtlSecret string
|
|
||||||
SkipInstall bool
|
|
||||||
UseSudo bool
|
|
||||||
ServiceType string
|
|
||||||
ServiceName string
|
|
||||||
URL string
|
|
||||||
Channel string
|
|
||||||
Events string
|
|
||||||
Organization string
|
|
||||||
GatewayAuthToken string
|
|
||||||
BuilderAuthToken string
|
|
||||||
SupOptions string
|
|
||||||
AcceptLicense bool
|
|
||||||
|
|
||||||
installHabitat provisionFn
|
|
||||||
startHabitat provisionFn
|
|
||||||
uploadRingKey provisionFn
|
|
||||||
uploadCtlSecret provisionFn
|
|
||||||
startHabitatService provisionServiceFn
|
|
||||||
|
|
||||||
osType string
|
|
||||||
}
|
|
||||||
|
|
||||||
type provisionFn func(terraform.UIOutput, communicator.Communicator) error
|
|
||||||
type provisionServiceFn func(terraform.UIOutput, communicator.Communicator, Service) error
|
|
||||||
|
|
||||||
func Provisioner() terraform.ResourceProvisioner {
|
|
||||||
return &schema.Provisioner{
|
|
||||||
Schema: map[string]*schema.Schema{
|
|
||||||
"version": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"auto_update": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
Default: false,
|
|
||||||
},
|
|
||||||
"http_disable": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
Default: false,
|
|
||||||
},
|
|
||||||
"peer": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"peers": &schema.Schema{
|
|
||||||
Type: schema.TypeList,
|
|
||||||
Elem: &schema.Schema{Type: schema.TypeString},
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"service_type": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
Default: "systemd",
|
|
||||||
ValidateFunc: validation.StringInSlice([]string{"systemd", "unmanaged"}, false),
|
|
||||||
},
|
|
||||||
"service_name": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
Default: "hab-supervisor",
|
|
||||||
},
|
|
||||||
"use_sudo": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
Default: true,
|
|
||||||
},
|
|
||||||
"accept_license": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"permanent_peer": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
Default: false,
|
|
||||||
},
|
|
||||||
"listen_ctl": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"listen_gossip": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"listen_http": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"ring_key": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"ring_key_content": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"ctl_secret": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"url": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) {
|
|
||||||
u, err := url.Parse(val.(string))
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("invalid URL specified for %q: %v", key, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.Scheme == "" {
|
|
||||||
errs = append(errs, fmt.Errorf("invalid URL specified for %q (scheme must be specified)", key))
|
|
||||||
}
|
|
||||||
|
|
||||||
return warns, errs
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"channel": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"events": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"organization": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"gateway_auth_token": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"builder_auth_token": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"service": &schema.Schema{
|
|
||||||
Type: schema.TypeSet,
|
|
||||||
Elem: &schema.Resource{
|
|
||||||
Schema: map[string]*schema.Schema{
|
|
||||||
"name": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"binds": &schema.Schema{
|
|
||||||
Type: schema.TypeList,
|
|
||||||
Elem: &schema.Schema{Type: schema.TypeString},
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"bind": &schema.Schema{
|
|
||||||
Type: schema.TypeSet,
|
|
||||||
Elem: &schema.Resource{
|
|
||||||
Schema: map[string]*schema.Schema{
|
|
||||||
"alias": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"service": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"group": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"topology": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
ValidateFunc: validation.StringInSlice([]string{"leader", "standalone"}, false),
|
|
||||||
},
|
|
||||||
"user_toml": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"strategy": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
ValidateFunc: validation.StringInSlice([]string{"none", "rolling", "at-once"}, false),
|
|
||||||
},
|
|
||||||
"channel": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"group": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"url": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) {
|
|
||||||
u, err := url.Parse(val.(string))
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("invalid URL specified for %q: %v", key, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.Scheme == "" {
|
|
||||||
errs = append(errs, fmt.Errorf("invalid URL specified for %q (scheme must be specified)", key))
|
|
||||||
}
|
|
||||||
|
|
||||||
return warns, errs
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"application": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"environment": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"service_key": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ApplyFunc: applyFn,
|
|
||||||
ValidateFunc: validateFn,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyFn(ctx context.Context) error {
|
|
||||||
o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
|
|
||||||
s := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
|
|
||||||
d := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
|
|
||||||
|
|
||||||
p, err := decodeConfig(d)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Automatically determine the OS type
|
|
||||||
switch t := s.Ephemeral.ConnInfo["type"]; t {
|
|
||||||
case "ssh", "":
|
|
||||||
p.osType = "linux"
|
|
||||||
case "winrm":
|
|
||||||
p.osType = "windows"
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported connection type: %s", t)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch p.osType {
|
|
||||||
case "linux":
|
|
||||||
p.installHabitat = p.linuxInstallHabitat
|
|
||||||
p.uploadRingKey = p.linuxUploadRingKey
|
|
||||||
p.uploadCtlSecret = p.linuxUploadCtlSecret
|
|
||||||
p.startHabitat = p.linuxStartHabitat
|
|
||||||
p.startHabitatService = p.linuxStartHabitatService
|
|
||||||
case "windows":
|
|
||||||
return fmt.Errorf("windows is not supported yet for the habitat provisioner")
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported os type: %s", p.osType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a new communicator
|
|
||||||
comm, err := communicator.New(s)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Wait and retry until we establish the connection
|
|
||||||
err = communicator.Retry(retryCtx, func() error {
|
|
||||||
return comm.Connect(o)
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer comm.Disconnect()
|
|
||||||
|
|
||||||
if !p.SkipInstall {
|
|
||||||
o.Output("Installing habitat...")
|
|
||||||
if err := p.installHabitat(o, comm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.RingKeyContent != "" {
|
|
||||||
o.Output("Uploading supervisor ring key...")
|
|
||||||
if err := p.uploadRingKey(o, comm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.CtlSecret != "" {
|
|
||||||
o.Output("Uploading ctl secret...")
|
|
||||||
if err := p.uploadCtlSecret(o, comm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
o.Output("Starting the habitat supervisor...")
|
|
||||||
if err := p.startHabitat(o, comm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.Services != nil {
|
|
||||||
for _, service := range p.Services {
|
|
||||||
o.Output("Starting service: " + service.Name)
|
|
||||||
if err := p.startHabitatService(o, comm, service); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) {
|
|
||||||
ringKeyContent, ok := c.Get("ring_key_content")
|
|
||||||
if ok && ringKeyContent != "" && ringKeyContent != hcl2shim.UnknownVariableValue {
|
|
||||||
ringKey, ringOk := c.Get("ring_key")
|
|
||||||
if ringOk && ringKey == "" {
|
|
||||||
es = append(es, errors.New("if ring_key_content is specified, ring_key must be specified as well"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok := c.Get("version")
|
|
||||||
if ok && v != nil && strings.TrimSpace(v.(string)) != "" {
|
|
||||||
if _, err := version.NewVersion(v.(string)); err != nil {
|
|
||||||
es = append(es, errors.New(v.(string)+" is not a valid version."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
acceptLicense, ok := c.Get("accept_license")
|
|
||||||
if ok && !acceptLicense.(bool) {
|
|
||||||
if v != nil && strings.TrimSpace(v.(string)) != "" {
|
|
||||||
versionOld, _ := version.NewVersion("0.79.0")
|
|
||||||
versionRequired, _ := version.NewVersion(v.(string))
|
|
||||||
if versionRequired.GreaterThan(versionOld) {
|
|
||||||
es = append(es, errors.New("Habitat end user license agreement needs to be accepted, set the accept_license argument to true to accept"))
|
|
||||||
}
|
|
||||||
} else { // blank means latest version
|
|
||||||
es = append(es, errors.New("Habitat end user license agreement needs to be accepted, set the accept_license argument to true to accept"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate service level configs
|
|
||||||
services, ok := c.Get("service")
|
|
||||||
if ok {
|
|
||||||
data, dataOk := services.(string)
|
|
||||||
if dataOk {
|
|
||||||
es = append(es, fmt.Errorf("service '%v': must be a block", data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ws, es
|
|
||||||
}
|
|
||||||
|
|
||||||
type Service struct {
|
|
||||||
Name string
|
|
||||||
Strategy string
|
|
||||||
Topology string
|
|
||||||
Channel string
|
|
||||||
Group string
|
|
||||||
URL string
|
|
||||||
Binds []Bind
|
|
||||||
BindStrings []string
|
|
||||||
UserTOML string
|
|
||||||
AppName string
|
|
||||||
Environment string
|
|
||||||
ServiceGroupKey string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) getPackageName(fullName string) string {
|
|
||||||
return strings.Split(fullName, "/")[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) getServiceNameChecksum() string {
|
|
||||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(s.Name)))
|
|
||||||
}
|
|
||||||
|
|
||||||
type Bind struct {
|
|
||||||
Alias string
|
|
||||||
Service string
|
|
||||||
Group string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bind) toBindString() string {
|
|
||||||
return fmt.Sprintf("%s:%s.%s", b.Alias, b.Service, b.Group)
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
|
|
||||||
p := &provisioner{
|
|
||||||
Version: d.Get("version").(string),
|
|
||||||
AutoUpdate: d.Get("auto_update").(bool),
|
|
||||||
HttpDisable: d.Get("http_disable").(bool),
|
|
||||||
Peer: d.Get("peer").(string),
|
|
||||||
Peers: getPeers(d.Get("peers").([]interface{})),
|
|
||||||
Services: getServices(d.Get("service").(*schema.Set).List()),
|
|
||||||
UseSudo: d.Get("use_sudo").(bool),
|
|
||||||
AcceptLicense: d.Get("accept_license").(bool),
|
|
||||||
ServiceType: d.Get("service_type").(string),
|
|
||||||
ServiceName: d.Get("service_name").(string),
|
|
||||||
RingKey: d.Get("ring_key").(string),
|
|
||||||
RingKeyContent: d.Get("ring_key_content").(string),
|
|
||||||
CtlSecret: d.Get("ctl_secret").(string),
|
|
||||||
PermanentPeer: d.Get("permanent_peer").(bool),
|
|
||||||
ListenCtl: d.Get("listen_ctl").(string),
|
|
||||||
ListenGossip: d.Get("listen_gossip").(string),
|
|
||||||
ListenHTTP: d.Get("listen_http").(string),
|
|
||||||
URL: d.Get("url").(string),
|
|
||||||
Channel: d.Get("channel").(string),
|
|
||||||
Events: d.Get("events").(string),
|
|
||||||
Organization: d.Get("organization").(string),
|
|
||||||
BuilderAuthToken: d.Get("builder_auth_token").(string),
|
|
||||||
GatewayAuthToken: d.Get("gateway_auth_token").(string),
|
|
||||||
}
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPeers(v []interface{}) []string {
|
|
||||||
peers := make([]string, 0, len(v))
|
|
||||||
for _, rawPeerData := range v {
|
|
||||||
peers = append(peers, rawPeerData.(string))
|
|
||||||
}
|
|
||||||
return peers
|
|
||||||
}
|
|
||||||
|
|
||||||
func getServices(v []interface{}) []Service {
|
|
||||||
services := make([]Service, 0, len(v))
|
|
||||||
for _, rawServiceData := range v {
|
|
||||||
serviceData := rawServiceData.(map[string]interface{})
|
|
||||||
name := (serviceData["name"].(string))
|
|
||||||
strategy := (serviceData["strategy"].(string))
|
|
||||||
topology := (serviceData["topology"].(string))
|
|
||||||
channel := (serviceData["channel"].(string))
|
|
||||||
group := (serviceData["group"].(string))
|
|
||||||
url := (serviceData["url"].(string))
|
|
||||||
app := (serviceData["application"].(string))
|
|
||||||
env := (serviceData["environment"].(string))
|
|
||||||
userToml := (serviceData["user_toml"].(string))
|
|
||||||
serviceGroupKey := (serviceData["service_key"].(string))
|
|
||||||
var bindStrings []string
|
|
||||||
binds := getBinds(serviceData["bind"].(*schema.Set).List())
|
|
||||||
for _, b := range serviceData["binds"].([]interface{}) {
|
|
||||||
bind, err := getBindFromString(b.(string))
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
binds = append(binds, bind)
|
|
||||||
}
|
|
||||||
|
|
||||||
service := Service{
|
|
||||||
Name: name,
|
|
||||||
Strategy: strategy,
|
|
||||||
Topology: topology,
|
|
||||||
Channel: channel,
|
|
||||||
Group: group,
|
|
||||||
URL: url,
|
|
||||||
UserTOML: userToml,
|
|
||||||
BindStrings: bindStrings,
|
|
||||||
Binds: binds,
|
|
||||||
AppName: app,
|
|
||||||
Environment: env,
|
|
||||||
ServiceGroupKey: serviceGroupKey,
|
|
||||||
}
|
|
||||||
services = append(services, service)
|
|
||||||
}
|
|
||||||
return services
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBinds(v []interface{}) []Bind {
|
|
||||||
binds := make([]Bind, 0, len(v))
|
|
||||||
for _, rawBindData := range v {
|
|
||||||
bindData := rawBindData.(map[string]interface{})
|
|
||||||
alias := bindData["alias"].(string)
|
|
||||||
service := bindData["service"].(string)
|
|
||||||
group := bindData["group"].(string)
|
|
||||||
bind := Bind{
|
|
||||||
Alias: alias,
|
|
||||||
Service: service,
|
|
||||||
Group: group,
|
|
||||||
}
|
|
||||||
binds = append(binds, bind)
|
|
||||||
}
|
|
||||||
return binds
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) copyOutput(o terraform.UIOutput, r io.Reader) {
|
|
||||||
lr := linereader.New(r)
|
|
||||||
for line := range lr.Ch {
|
|
||||||
o.Output(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) runCommand(o terraform.UIOutput, comm communicator.Communicator, command string) error {
|
|
||||||
outR, outW := io.Pipe()
|
|
||||||
errR, errW := io.Pipe()
|
|
||||||
|
|
||||||
go p.copyOutput(o, outR)
|
|
||||||
go p.copyOutput(o, errR)
|
|
||||||
defer outW.Close()
|
|
||||||
defer errW.Close()
|
|
||||||
|
|
||||||
cmd := &remote.Cmd{
|
|
||||||
Command: command,
|
|
||||||
Stdout: outW,
|
|
||||||
Stderr: errW,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := comm.Start(cmd); err != nil {
|
|
||||||
return fmt.Errorf("error executing command %q: %v", cmd.Command, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBindFromString(bind string) (Bind, error) {
|
|
||||||
t := strings.FieldsFunc(bind, func(d rune) bool {
|
|
||||||
switch d {
|
|
||||||
case ':', '.':
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
if len(t) != 3 {
|
|
||||||
return Bind{}, errors.New("invalid bind specification: " + bind)
|
|
||||||
}
|
|
||||||
return Bind{Alias: t[0], Service: t[1], Group: t[2]}, nil
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
package habitat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestResourceProvisioner_impl(t *testing.T) {
|
|
||||||
var _ terraform.ResourceProvisioner = Provisioner()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisioner(t *testing.T) {
|
|
||||||
if err := Provisioner().(*schema.Provisioner).InternalValidate(); err != nil {
|
|
||||||
t.Fatalf("error: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvisioner_Validate_good(t *testing.T) {
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"peers": []interface{}{"1.2.3.4"},
|
|
||||||
"version": "0.32.0",
|
|
||||||
"service_type": "systemd",
|
|
||||||
"accept_license": false,
|
|
||||||
})
|
|
||||||
|
|
||||||
warn, errs := Provisioner().Validate(c)
|
|
||||||
if len(warn) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warn)
|
|
||||||
}
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Fatalf("Errors: %v", errs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
// 3 errors, bad service_type, bad url, missing accept_license
|
|
||||||
if len(errs) != 3 {
|
|
||||||
t.Fatalf("Should have three errors, got %d", len(errs))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvisioner_Validate_bad_service_config(t *testing.T) {
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"accept_license": true,
|
|
||||||
"service": []interface{}{
|
|
||||||
map[string]interface{}{
|
|
||||||
"name": "core/foo",
|
|
||||||
"strategy": "bar",
|
|
||||||
"topology": "baz",
|
|
||||||
"url": "badurl",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig {
|
|
||||||
return terraform.NewResourceConfigRaw(c)
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
package bolt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Result struct {
|
|
||||||
Items []struct {
|
|
||||||
Node string `json:"node"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Result map[string]string `json:"result"`
|
|
||||||
} `json:"items"`
|
|
||||||
NodeCount int `json:"node_count"`
|
|
||||||
ElapsedTime int `json:"elapsed_time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func runCommand(command string, timeout time.Duration) ([]byte, error) {
|
|
||||||
var cmdargs []string
|
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
cmdargs = []string{"cmd", "/C"}
|
|
||||||
} else {
|
|
||||||
cmdargs = []string{"/bin/sh", "-c"}
|
|
||||||
}
|
|
||||||
cmdargs = append(cmdargs, command)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, cmdargs[0], cmdargs[1:]...)
|
|
||||||
return cmd.Output()
|
|
||||||
}
|
|
||||||
|
|
||||||
func Task(connInfo map[string]string, timeout time.Duration, sudo bool, task string, args map[string]string) (*Result, error) {
|
|
||||||
cmdargs := []string{
|
|
||||||
"bolt", "task", "run", "--nodes", connInfo["type"] + "://" + connInfo["host"], "-u", connInfo["user"],
|
|
||||||
}
|
|
||||||
|
|
||||||
if connInfo["type"] == "winrm" {
|
|
||||||
cmdargs = append(cmdargs, "-p", "\""+connInfo["password"]+"\"", "--no-ssl")
|
|
||||||
} else {
|
|
||||||
if sudo {
|
|
||||||
cmdargs = append(cmdargs, "--run-as", "root")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmdargs = append(cmdargs, "--no-host-key-check")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmdargs = append(cmdargs, "--format", "json", "--connect-timeout", "120", task)
|
|
||||||
|
|
||||||
if args != nil {
|
|
||||||
for key, value := range args {
|
|
||||||
cmdargs = append(cmdargs, strings.Join([]string{key, value}, "="))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out, err := runCommand(strings.Join(cmdargs, " "), timeout)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Bolt: \"%s\": %s: %s", strings.Join(cmdargs, " "), out, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := new(Result)
|
|
||||||
if err = json.Unmarshal(out, result); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
package puppet
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/communicator/remote"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *provisioner) linuxUploadFile(f io.Reader, dir string, filename string) error {
|
|
||||||
_, err := p.runCommand("mkdir -p " + dir)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to make directory %s: %s", dir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = p.comm.Upload("/tmp/"+filename, f)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to upload %s to /tmp: %s", filename, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = p.runCommand(fmt.Sprintf("mv /tmp/%s %s/%s", filename, dir, filename))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) linuxDefaultCertname() (string, error) {
|
|
||||||
certname, err := p.runCommand("hostname -f")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return certname, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) linuxInstallPuppetAgent() error {
|
|
||||||
_, err := p.runCommand(fmt.Sprintf("curl -kO https://%s:8140/packages/current/install.bash", p.Server))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = p.runCommand("bash -- ./install.bash --puppet-service-ensure stopped")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = p.runCommand("rm -f install.bash")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) linuxRunPuppetAgent() error {
|
|
||||||
_, err := p.runCommand(fmt.Sprintf(
|
|
||||||
"/opt/puppetlabs/puppet/bin/puppet agent --test --server %s --environment %s",
|
|
||||||
p.Server,
|
|
||||||
p.Environment,
|
|
||||||
))
|
|
||||||
|
|
||||||
// Puppet exits 2 if changes have been successfully made.
|
|
||||||
if err != nil {
|
|
||||||
errStruct, _ := err.(*remote.ExitError)
|
|
||||||
if errStruct.ExitStatus == 2 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -1,379 +0,0 @@
|
||||||
package puppet
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/communicator"
|
|
||||||
"github.com/hashicorp/terraform/communicator/remote"
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestResourceProvisioner_linuxUploadFile(t *testing.T) {
|
|
||||||
cases := map[string]struct {
|
|
||||||
Config map[string]interface{}
|
|
||||||
Commands map[string]bool
|
|
||||||
CommandFunc func(*remote.Cmd) error
|
|
||||||
ExpectedError bool
|
|
||||||
Uploads map[string]string
|
|
||||||
File io.Reader
|
|
||||||
Dir string
|
|
||||||
Filename string
|
|
||||||
}{
|
|
||||||
"Successful upload": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"mkdir -p /etc/puppetlabs/puppet": true,
|
|
||||||
"mv /tmp/csr_attributes.yaml /etc/puppetlabs/puppet/csr_attributes.yaml": true,
|
|
||||||
},
|
|
||||||
Uploads: map[string]string{
|
|
||||||
"/tmp/csr_attributes.yaml": "",
|
|
||||||
},
|
|
||||||
Dir: "/etc/puppetlabs/puppet",
|
|
||||||
Filename: "csr_attributes.yaml",
|
|
||||||
File: strings.NewReader(""),
|
|
||||||
},
|
|
||||||
"Failure when creating the directory": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"mkdir -p /etc/puppetlabs/puppet": true,
|
|
||||||
},
|
|
||||||
Dir: "/etc/puppetlabs/puppet",
|
|
||||||
Filename: "csr_attributes.yaml",
|
|
||||||
File: strings.NewReader(""),
|
|
||||||
CommandFunc: func(r *remote.Cmd) error {
|
|
||||||
r.SetExitStatus(1, &remote.ExitError{
|
|
||||||
Command: "mkdir -p /etc/puppetlabs/puppet",
|
|
||||||
ExitStatus: 1,
|
|
||||||
Err: nil,
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExpectedError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, tc := range cases {
|
|
||||||
p, err := decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := new(communicator.MockCommunicator)
|
|
||||||
c.Commands = tc.Commands
|
|
||||||
c.Uploads = tc.Uploads
|
|
||||||
if tc.CommandFunc != nil {
|
|
||||||
c.CommandFunc = tc.CommandFunc
|
|
||||||
}
|
|
||||||
p.comm = c
|
|
||||||
p.output = new(terraform.MockUIOutput)
|
|
||||||
|
|
||||||
err = p.linuxUploadFile(tc.File, tc.Dir, tc.Filename)
|
|
||||||
if tc.ExpectedError {
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Expected error, but no error returned")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %q failed: %v", k, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvisioner_linuxDefaultCertname(t *testing.T) {
|
|
||||||
cases := map[string]struct {
|
|
||||||
Config map[string]interface{}
|
|
||||||
Commands map[string]bool
|
|
||||||
CommandFunc func(*remote.Cmd) error
|
|
||||||
ExpectedError bool
|
|
||||||
}{
|
|
||||||
"No sudo": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"hostname -f": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"With sudo": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": true,
|
|
||||||
},
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"sudo hostname -f": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"Failed execution": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"hostname -f": true,
|
|
||||||
},
|
|
||||||
CommandFunc: func(r *remote.Cmd) error {
|
|
||||||
if r.Command == "hostname -f" {
|
|
||||||
r.SetExitStatus(1, &remote.ExitError{
|
|
||||||
Command: "hostname -f",
|
|
||||||
ExitStatus: 1,
|
|
||||||
Err: nil,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
r.SetExitStatus(0, nil)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExpectedError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, tc := range cases {
|
|
||||||
p, err := decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := new(communicator.MockCommunicator)
|
|
||||||
c.Commands = tc.Commands
|
|
||||||
if tc.CommandFunc != nil {
|
|
||||||
c.CommandFunc = tc.CommandFunc
|
|
||||||
}
|
|
||||||
p.comm = c
|
|
||||||
p.output = new(terraform.MockUIOutput)
|
|
||||||
|
|
||||||
_, err = p.linuxDefaultCertname()
|
|
||||||
if tc.ExpectedError {
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Expected error, but no error returned")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %q failed: %v", k, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvisioner_linuxInstallPuppetAgent(t *testing.T) {
|
|
||||||
cases := map[string]struct {
|
|
||||||
Config map[string]interface{}
|
|
||||||
Commands map[string]bool
|
|
||||||
CommandFunc func(*remote.Cmd) error
|
|
||||||
ExpectedError bool
|
|
||||||
}{
|
|
||||||
"Everything runs succcessfully": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"curl -kO https://puppet.test.com:8140/packages/current/install.bash": true,
|
|
||||||
"bash -- ./install.bash --puppet-service-ensure stopped": true,
|
|
||||||
"rm -f install.bash": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"Respects the use_sudo config flag": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": true,
|
|
||||||
},
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"sudo curl -kO https://puppet.test.com:8140/packages/current/install.bash": true,
|
|
||||||
"sudo bash -- ./install.bash --puppet-service-ensure stopped": true,
|
|
||||||
"sudo rm -f install.bash": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"When the curl command fails": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"curl -kO https://puppet.test.com:8140/packages/current/install.bash": true,
|
|
||||||
"bash -- ./install.bash --puppet-service-ensure stopped": false,
|
|
||||||
"rm -f install.bash": false,
|
|
||||||
},
|
|
||||||
CommandFunc: func(r *remote.Cmd) error {
|
|
||||||
if r.Command == "curl -kO https://puppet.test.com:8140/packages/current/install.bash" {
|
|
||||||
r.SetExitStatus(1, &remote.ExitError{
|
|
||||||
Command: "curl -kO https://puppet.test.com:8140/packages/current/install.bash",
|
|
||||||
ExitStatus: 1,
|
|
||||||
Err: nil,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
r.SetExitStatus(0, nil)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExpectedError: true,
|
|
||||||
},
|
|
||||||
"When the install script fails": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"curl -kO https://puppet.test.com:8140/packages/current/install.bash": true,
|
|
||||||
"bash -- ./install.bash --puppet-service-ensure stopped": true,
|
|
||||||
"rm -f install.bash": false,
|
|
||||||
},
|
|
||||||
CommandFunc: func(r *remote.Cmd) error {
|
|
||||||
if r.Command == "bash -- ./install.bash --puppet-service-ensure stopped" {
|
|
||||||
r.SetExitStatus(1, &remote.ExitError{
|
|
||||||
Command: "bash -- ./install.bash --puppet-service-ensure stopped",
|
|
||||||
ExitStatus: 1,
|
|
||||||
Err: nil,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
r.SetExitStatus(0, nil)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExpectedError: true,
|
|
||||||
},
|
|
||||||
"When the cleanup rm fails": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"curl -kO https://puppet.test.com:8140/packages/current/install.bash": true,
|
|
||||||
"bash -- ./install.bash --puppet-service-ensure stopped": true,
|
|
||||||
"rm -f install.bash": true,
|
|
||||||
},
|
|
||||||
CommandFunc: func(r *remote.Cmd) error {
|
|
||||||
if r.Command == "rm -f install.bash" {
|
|
||||||
r.SetExitStatus(1, &remote.ExitError{
|
|
||||||
Command: "rm -f install.bash",
|
|
||||||
ExitStatus: 1,
|
|
||||||
Err: nil,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
r.SetExitStatus(0, nil)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExpectedError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, tc := range cases {
|
|
||||||
p, err := decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := new(communicator.MockCommunicator)
|
|
||||||
c.Commands = tc.Commands
|
|
||||||
if tc.CommandFunc != nil {
|
|
||||||
c.CommandFunc = tc.CommandFunc
|
|
||||||
}
|
|
||||||
p.comm = c
|
|
||||||
p.output = new(terraform.MockUIOutput)
|
|
||||||
|
|
||||||
err = p.linuxInstallPuppetAgent()
|
|
||||||
if tc.ExpectedError {
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Expected error, but no error returned")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %q failed: %v", k, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvisioner_linuxRunPuppetAgent(t *testing.T) {
|
|
||||||
cases := map[string]struct {
|
|
||||||
Config map[string]interface{}
|
|
||||||
Commands map[string]bool
|
|
||||||
CommandFunc func(*remote.Cmd) error
|
|
||||||
ExpectedError bool
|
|
||||||
}{
|
|
||||||
"When puppet returns 0": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
Commands: map[string]bool{
|
|
||||||
"/opt/puppetlabs/puppet/bin/puppet agent --test --server puppet.test.com --environment production": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"When puppet returns 2 (changes applied without error)": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
CommandFunc: func(r *remote.Cmd) error {
|
|
||||||
r.SetExitStatus(2, &remote.ExitError{
|
|
||||||
Command: "/opt/puppetlabs/puppet/bin/puppet agent --test --server puppet.test.com",
|
|
||||||
ExitStatus: 2,
|
|
||||||
Err: nil,
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExpectedError: false,
|
|
||||||
},
|
|
||||||
"When puppet returns something not 0 or 2": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
CommandFunc: func(r *remote.Cmd) error {
|
|
||||||
r.SetExitStatus(1, &remote.ExitError{
|
|
||||||
Command: "/opt/puppetlabs/puppet/bin/puppet agent --test --server puppet.test.com",
|
|
||||||
ExitStatus: 1,
|
|
||||||
Err: nil,
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExpectedError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, tc := range cases {
|
|
||||||
p, err := decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := new(communicator.MockCommunicator)
|
|
||||||
c.Commands = tc.Commands
|
|
||||||
if tc.CommandFunc != nil {
|
|
||||||
c.CommandFunc = tc.CommandFunc
|
|
||||||
}
|
|
||||||
p.comm = c
|
|
||||||
p.output = new(terraform.MockUIOutput)
|
|
||||||
|
|
||||||
err = p.linuxRunPuppetAgent()
|
|
||||||
if tc.ExpectedError {
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Expected error, but no error returned")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %q failed: %v", k, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,359 +0,0 @@
|
||||||
package puppet
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/builtin/provisioners/puppet/bolt"
|
|
||||||
"github.com/hashicorp/terraform/communicator"
|
|
||||||
"github.com/hashicorp/terraform/communicator/remote"
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
|
||||||
"github.com/hashicorp/terraform/helper/validation"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
"github.com/mitchellh/go-linereader"
|
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type provisioner struct {
|
|
||||||
Server string
|
|
||||||
ServerUser string
|
|
||||||
OSType string
|
|
||||||
Certname string
|
|
||||||
Environment string
|
|
||||||
Autosign bool
|
|
||||||
OpenSource bool
|
|
||||||
UseSudo bool
|
|
||||||
BoltTimeout time.Duration
|
|
||||||
CustomAttributes map[string]interface{}
|
|
||||||
ExtensionRequests map[string]interface{}
|
|
||||||
|
|
||||||
runPuppetAgent func() error
|
|
||||||
installPuppetAgent func() error
|
|
||||||
uploadFile func(f io.Reader, dir string, filename string) error
|
|
||||||
defaultCertname func() (string, error)
|
|
||||||
|
|
||||||
instanceState *terraform.InstanceState
|
|
||||||
output terraform.UIOutput
|
|
||||||
comm communicator.Communicator
|
|
||||||
|
|
||||||
outputWG sync.WaitGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
type csrAttributes struct {
|
|
||||||
CustomAttributes map[string]string `yaml:"custom_attributes"`
|
|
||||||
ExtensionRequests map[string]string `yaml:"extension_requests"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provisioner returns a Puppet resource provisioner.
|
|
||||||
func Provisioner() terraform.ResourceProvisioner {
|
|
||||||
return &schema.Provisioner{
|
|
||||||
Schema: map[string]*schema.Schema{
|
|
||||||
"server": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"server_user": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
Default: "root",
|
|
||||||
},
|
|
||||||
"os_type": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
ValidateFunc: validation.StringInSlice([]string{"linux", "windows"}, false),
|
|
||||||
},
|
|
||||||
"use_sudo": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
Default: true,
|
|
||||||
},
|
|
||||||
"autosign": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
Default: true,
|
|
||||||
},
|
|
||||||
"open_source": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
Default: true,
|
|
||||||
},
|
|
||||||
"certname": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"extension_requests": &schema.Schema{
|
|
||||||
Type: schema.TypeMap,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"custom_attributes": &schema.Schema{
|
|
||||||
Type: schema.TypeMap,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"environment": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Default: "production",
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"bolt_timeout": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Default: "5m",
|
|
||||||
Optional: true,
|
|
||||||
ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) {
|
|
||||||
_, err := time.ParseDuration(val.(string))
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
return warns, errs
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ApplyFunc: applyFn,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyFn(ctx context.Context) error {
|
|
||||||
output := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
|
|
||||||
state := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
|
|
||||||
configData := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
|
|
||||||
|
|
||||||
p, err := decodeConfig(configData)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
p.instanceState = state
|
|
||||||
p.output = output
|
|
||||||
|
|
||||||
if p.OSType == "" {
|
|
||||||
switch connType := state.Ephemeral.ConnInfo["type"]; connType {
|
|
||||||
case "ssh", "": // The default connection type is ssh, so if the type is empty assume ssh
|
|
||||||
p.OSType = "linux"
|
|
||||||
case "winrm":
|
|
||||||
p.OSType = "windows"
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Unsupported connection type: %s", connType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch p.OSType {
|
|
||||||
case "linux":
|
|
||||||
p.runPuppetAgent = p.linuxRunPuppetAgent
|
|
||||||
p.installPuppetAgent = p.linuxInstallPuppetAgent
|
|
||||||
p.uploadFile = p.linuxUploadFile
|
|
||||||
p.defaultCertname = p.linuxDefaultCertname
|
|
||||||
case "windows":
|
|
||||||
p.runPuppetAgent = p.windowsRunPuppetAgent
|
|
||||||
p.installPuppetAgent = p.windowsInstallPuppetAgent
|
|
||||||
p.uploadFile = p.windowsUploadFile
|
|
||||||
p.UseSudo = false
|
|
||||||
p.defaultCertname = p.windowsDefaultCertname
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Unsupported OS type: %s", p.OSType)
|
|
||||||
}
|
|
||||||
|
|
||||||
comm, err := communicator.New(state)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err = communicator.Retry(retryCtx, func() error {
|
|
||||||
return comm.Connect(output)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer comm.Disconnect()
|
|
||||||
|
|
||||||
p.comm = comm
|
|
||||||
|
|
||||||
if p.OpenSource {
|
|
||||||
p.installPuppetAgent = p.installPuppetAgentOpenSource
|
|
||||||
}
|
|
||||||
|
|
||||||
csrAttrs := new(csrAttributes)
|
|
||||||
csrAttrs.CustomAttributes = make(map[string]string)
|
|
||||||
for k, v := range p.CustomAttributes {
|
|
||||||
csrAttrs.CustomAttributes[k] = v.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
csrAttrs.ExtensionRequests = make(map[string]string)
|
|
||||||
for k, v := range p.ExtensionRequests {
|
|
||||||
csrAttrs.ExtensionRequests[k] = v.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.Autosign {
|
|
||||||
if p.Certname == "" {
|
|
||||||
p.Certname, _ = p.defaultCertname()
|
|
||||||
}
|
|
||||||
|
|
||||||
autosignToken, err := p.generateAutosignToken(p.Certname)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to generate an autosign token: %s", err)
|
|
||||||
}
|
|
||||||
csrAttrs.CustomAttributes["challengePassword"] = autosignToken
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = p.writeCSRAttributes(csrAttrs); err != nil {
|
|
||||||
return fmt.Errorf("Failed to write csr_attributes.yaml: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = p.installPuppetAgent(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = p.runPuppetAgent(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) writeCSRAttributes(attrs *csrAttributes) (rerr error) {
|
|
||||||
content, err := yaml.Marshal(attrs)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to marshal CSR attributes to YAML: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
configDir := map[string]string{
|
|
||||||
"linux": "/etc/puppetlabs/puppet",
|
|
||||||
"windows": "C:\\ProgramData\\PuppetLabs\\Puppet\\etc",
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.uploadFile(bytes.NewBuffer(content), configDir[p.OSType], "csr_attributes.yaml")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) generateAutosignToken(certname string) (string, error) {
|
|
||||||
task := "autosign::generate_token"
|
|
||||||
|
|
||||||
masterConnInfo := map[string]string{
|
|
||||||
"type": "ssh",
|
|
||||||
"host": p.Server,
|
|
||||||
"user": p.ServerUser,
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := bolt.Task(
|
|
||||||
masterConnInfo,
|
|
||||||
p.BoltTimeout,
|
|
||||||
p.ServerUser != "root",
|
|
||||||
task,
|
|
||||||
map[string]string{"certname": certname},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.Items[0].Status != "success" {
|
|
||||||
return "", fmt.Errorf("Bolt %s failed on %s: %v",
|
|
||||||
task,
|
|
||||||
result.Items[0].Node,
|
|
||||||
result.Items[0].Result["_error"],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.Items[0].Result["_output"], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) installPuppetAgentOpenSource() error {
|
|
||||||
task := "puppet_agent::install"
|
|
||||||
|
|
||||||
connType := p.instanceState.Ephemeral.ConnInfo["type"]
|
|
||||||
if connType == "" {
|
|
||||||
connType = "ssh"
|
|
||||||
}
|
|
||||||
|
|
||||||
agentConnInfo := map[string]string{
|
|
||||||
"type": connType,
|
|
||||||
"host": p.instanceState.Ephemeral.ConnInfo["host"],
|
|
||||||
"user": p.instanceState.Ephemeral.ConnInfo["user"],
|
|
||||||
"password": p.instanceState.Ephemeral.ConnInfo["password"], // Required on Windows only
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := bolt.Task(
|
|
||||||
agentConnInfo,
|
|
||||||
p.BoltTimeout,
|
|
||||||
p.UseSudo,
|
|
||||||
task,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil || result.Items[0].Status != "success" {
|
|
||||||
return fmt.Errorf("%s failed: %s\n%+v", task, err, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) runCommand(command string) (stdout string, err error) {
|
|
||||||
if p.UseSudo {
|
|
||||||
command = "sudo " + command
|
|
||||||
}
|
|
||||||
|
|
||||||
var stdoutBuffer bytes.Buffer
|
|
||||||
outR, outW := io.Pipe()
|
|
||||||
errR, errW := io.Pipe()
|
|
||||||
outTee := io.TeeReader(outR, &stdoutBuffer)
|
|
||||||
|
|
||||||
p.outputWG.Add(2)
|
|
||||||
go p.copyToOutput(outTee)
|
|
||||||
go p.copyToOutput(errR)
|
|
||||||
|
|
||||||
defer outW.Close()
|
|
||||||
defer errW.Close()
|
|
||||||
|
|
||||||
cmd := &remote.Cmd{
|
|
||||||
Command: command,
|
|
||||||
Stdout: outW,
|
|
||||||
Stderr: errW,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = p.comm.Start(cmd)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("Error executing command %q: %v", cmd.Command, err)
|
|
||||||
return stdout, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = cmd.Wait()
|
|
||||||
|
|
||||||
outW.Close()
|
|
||||||
errW.Close()
|
|
||||||
p.outputWG.Wait()
|
|
||||||
|
|
||||||
stdout = strings.TrimSpace(stdoutBuffer.String())
|
|
||||||
|
|
||||||
return stdout, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) copyToOutput(reader io.Reader) {
|
|
||||||
defer p.outputWG.Done()
|
|
||||||
|
|
||||||
lr := linereader.New(reader)
|
|
||||||
for line := range lr.Ch {
|
|
||||||
p.output.Output(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
|
|
||||||
p := &provisioner{
|
|
||||||
UseSudo: d.Get("use_sudo").(bool),
|
|
||||||
Server: d.Get("server").(string),
|
|
||||||
ServerUser: d.Get("server_user").(string),
|
|
||||||
OSType: strings.ToLower(d.Get("os_type").(string)),
|
|
||||||
Autosign: d.Get("autosign").(bool),
|
|
||||||
OpenSource: d.Get("open_source").(bool),
|
|
||||||
Certname: strings.ToLower(d.Get("certname").(string)),
|
|
||||||
ExtensionRequests: d.Get("extension_requests").(map[string]interface{}),
|
|
||||||
CustomAttributes: d.Get("custom_attributes").(map[string]interface{}),
|
|
||||||
Environment: d.Get("environment").(string),
|
|
||||||
}
|
|
||||||
p.BoltTimeout, _ = time.ParseDuration(d.Get("bolt_timeout").(string))
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
package puppet
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestResourceProvisioner_impl(t *testing.T) {
|
|
||||||
var _ terraform.ResourceProvisioner = Provisioner()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisioner(t *testing.T) {
|
|
||||||
if err := Provisioner().(*schema.Provisioner).InternalValidate(); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisioner_Validate_good_server(t *testing.T) {
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
})
|
|
||||||
|
|
||||||
warn, errs := Provisioner().Validate(c)
|
|
||||||
if len(warn) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warn)
|
|
||||||
}
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Fatalf("Errors: %v", errs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisioner_Validate_bad_no_server(t *testing.T) {
|
|
||||||
c := testConfig(t, map[string]interface{}{})
|
|
||||||
|
|
||||||
warn, errs := Provisioner().Validate(c)
|
|
||||||
if len(warn) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warn)
|
|
||||||
}
|
|
||||||
if len(errs) == 0 {
|
|
||||||
t.Fatalf("Should have errors")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisioner_Validate_bad_os_type(t *testing.T) {
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"os_type": "OS/2",
|
|
||||||
})
|
|
||||||
|
|
||||||
warn, errs := Provisioner().Validate(c)
|
|
||||||
if len(warn) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warn)
|
|
||||||
}
|
|
||||||
if len(errs) == 0 {
|
|
||||||
t.Fatalf("Should have errors")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisioner_Validate_good_os_type_linux(t *testing.T) {
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"os_type": "linux",
|
|
||||||
})
|
|
||||||
|
|
||||||
warn, errs := Provisioner().Validate(c)
|
|
||||||
if len(warn) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warn)
|
|
||||||
}
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Fatalf("Errors: %v", errs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisioner_Validate_good_os_type_windows(t *testing.T) {
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"os_type": "windows",
|
|
||||||
})
|
|
||||||
|
|
||||||
warn, errs := Provisioner().Validate(c)
|
|
||||||
if len(warn) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warn)
|
|
||||||
}
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Fatalf("Errors: %v", errs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisioner_Validate_bad_bolt_timeout(t *testing.T) {
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"bolt_timeout": "123oeau",
|
|
||||||
})
|
|
||||||
|
|
||||||
warn, errs := Provisioner().Validate(c)
|
|
||||||
if len(warn) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warn)
|
|
||||||
}
|
|
||||||
if len(errs) == 0 {
|
|
||||||
t.Fatalf("Should have errors")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisioner_Validate_good_bolt_timeout(t *testing.T) {
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"bolt_timeout": "123m",
|
|
||||||
})
|
|
||||||
|
|
||||||
warn, errs := Provisioner().Validate(c)
|
|
||||||
if len(warn) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warn)
|
|
||||||
}
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Fatalf("Errors: %v", warn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig {
|
|
||||||
return terraform.NewResourceConfigRaw(c)
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
package puppet
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/communicator/remote"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
getHostByName = "([System.Net.Dns]::GetHostByName(($env:computerName))).Hostname"
|
|
||||||
domainQuery = "(Get-WmiObject -Query 'select DNSDomain from Win32_NetworkAdapterConfiguration where IPEnabled = True').DNSDomain"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *provisioner) windowsUploadFile(f io.Reader, dir string, filename string) error {
|
|
||||||
_, err := p.runCommand("powershell.exe new-item -itemtype directory -force -path " + dir)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to make directory %s: %s", dir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.comm.Upload(dir+"\\"+filename, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) windowsDefaultCertname() (string, error) {
|
|
||||||
certname, err := p.runCommand(fmt.Sprintf(`powershell -Command "& {%s}"`, getHostByName))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sometimes System.Net.Dns::GetHostByName does not return a full FQDN, so
|
|
||||||
// we have to look up the domain separately.
|
|
||||||
if strings.Contains(certname, ".") {
|
|
||||||
return certname, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
domain, err := p.runCommand(fmt.Sprintf(`powershell -Command "& {%s}"`, domainQuery))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.ToLower(certname + "." + domain), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) windowsInstallPuppetAgent() error {
|
|
||||||
_, err := p.runCommand(fmt.Sprintf(
|
|
||||||
`powershell -Command "& {[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}; `+
|
|
||||||
`(New-Object System.Net.WebClient).DownloadFile('https://%s:8140/packages/current/install.ps1', `+
|
|
||||||
`'install.ps1')}"`,
|
|
||||||
p.Server,
|
|
||||||
))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = p.runCommand(`powershell -Command "& .\install.ps1 -PuppetServiceEnsure stopped"`)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) windowsRunPuppetAgent() error {
|
|
||||||
_, err := p.runCommand(fmt.Sprintf("puppet agent --test --server %s --environment %s", p.Server, p.Environment))
|
|
||||||
if err != nil {
|
|
||||||
errStruct, _ := err.(*remote.ExitError)
|
|
||||||
if errStruct.ExitStatus == 2 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -1,393 +0,0 @@
|
||||||
package puppet
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/communicator"
|
|
||||||
"github.com/hashicorp/terraform/communicator/remote"
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
getHostByNameCmd = `powershell -Command "& {([System.Net.Dns]::GetHostByName(($env:computerName))).Hostname}"`
|
|
||||||
domainQueryCmd = `powershell -Command "& {(Get-WmiObject -Query 'select DNSDomain from Win32_NetworkAdapterConfiguration where IPEnabled = True').DNSDomain}"`
|
|
||||||
downloadInstallerCmd = `powershell -Command "& {[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}; (New-Object System.Net.WebClient).DownloadFile('https://puppet.test.com:8140/packages/current/install.ps1', 'install.ps1')}"`
|
|
||||||
runInstallerCmd = `powershell -Command "& .\install.ps1 -PuppetServiceEnsure stopped"`
|
|
||||||
runPuppetCmd = "puppet agent --test --server puppet.test.com --environment production"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestResourceProvisioner_windowsUploadFile(t *testing.T) {
|
|
||||||
cases := map[string]struct {
|
|
||||||
Config map[string]interface{}
|
|
||||||
Commands map[string]bool
|
|
||||||
CommandFunc func(*remote.Cmd) error
|
|
||||||
ExpectedError bool
|
|
||||||
Uploads map[string]string
|
|
||||||
File io.Reader
|
|
||||||
Dir string
|
|
||||||
Filename string
|
|
||||||
}{
|
|
||||||
"Successful upload": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
Commands: map[string]bool{
|
|
||||||
`powershell.exe new-item -itemtype directory -force -path C:\ProgramData\PuppetLabs\puppet\etc`: true,
|
|
||||||
},
|
|
||||||
Uploads: map[string]string{
|
|
||||||
`C:\ProgramData\PuppetLabs\puppet\etc\csr_attributes.yaml`: "",
|
|
||||||
},
|
|
||||||
Dir: `C:\ProgramData\PuppetLabs\puppet\etc`,
|
|
||||||
Filename: "csr_attributes.yaml",
|
|
||||||
File: strings.NewReader(""),
|
|
||||||
},
|
|
||||||
"Failure when creating the directory": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
Commands: map[string]bool{
|
|
||||||
`powershell.exe new-item -itemtype directory -force -path C:\ProgramData\PuppetLabs\puppet\etc`: true,
|
|
||||||
},
|
|
||||||
Dir: `C:\ProgramData\PuppetLabs\puppet\etc`,
|
|
||||||
Filename: "csr_attributes.yaml",
|
|
||||||
File: strings.NewReader(""),
|
|
||||||
CommandFunc: func(r *remote.Cmd) error {
|
|
||||||
r.SetExitStatus(1, &remote.ExitError{
|
|
||||||
Command: `powershell.exe new-item -itemtype directory -force -path C:\ProgramData\PuppetLabs\puppet\etc`,
|
|
||||||
ExitStatus: 1,
|
|
||||||
Err: nil,
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExpectedError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, tc := range cases {
|
|
||||||
p, err := decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := new(communicator.MockCommunicator)
|
|
||||||
c.Commands = tc.Commands
|
|
||||||
c.Uploads = tc.Uploads
|
|
||||||
if tc.CommandFunc != nil {
|
|
||||||
c.CommandFunc = tc.CommandFunc
|
|
||||||
}
|
|
||||||
p.comm = c
|
|
||||||
p.output = new(terraform.MockUIOutput)
|
|
||||||
|
|
||||||
err = p.windowsUploadFile(tc.File, tc.Dir, tc.Filename)
|
|
||||||
if tc.ExpectedError {
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Expected error, but no error returned")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %q failed: %v", k, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvisioner_windowsDefaultCertname(t *testing.T) {
|
|
||||||
cases := map[string]struct {
|
|
||||||
Config map[string]interface{}
|
|
||||||
Commands map[string]bool
|
|
||||||
CommandFunc func(*remote.Cmd) error
|
|
||||||
ExpectedError bool
|
|
||||||
}{
|
|
||||||
"GetHostByName failure": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
CommandFunc: func(r *remote.Cmd) error {
|
|
||||||
switch r.Command {
|
|
||||||
case getHostByNameCmd:
|
|
||||||
r.SetExitStatus(1, &remote.ExitError{
|
|
||||||
Command: getHostByNameCmd,
|
|
||||||
ExitStatus: 1,
|
|
||||||
Err: nil,
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Command not found!")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExpectedError: true,
|
|
||||||
},
|
|
||||||
"GetHostByName returns FQDN": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
CommandFunc: func(r *remote.Cmd) error {
|
|
||||||
switch r.Command {
|
|
||||||
case getHostByNameCmd:
|
|
||||||
r.Stdout.Write([]byte("example.test.com\n"))
|
|
||||||
time.Sleep(200 * time.Millisecond)
|
|
||||||
r.SetExitStatus(0, nil)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Command not found!")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"GetHostByName returns hostname, DNSDomain query succeeds": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
CommandFunc: func(r *remote.Cmd) error {
|
|
||||||
switch r.Command {
|
|
||||||
case getHostByNameCmd:
|
|
||||||
r.Stdout.Write([]byte("example\n"))
|
|
||||||
time.Sleep(200 * time.Millisecond)
|
|
||||||
r.SetExitStatus(0, nil)
|
|
||||||
case domainQueryCmd:
|
|
||||||
r.Stdout.Write([]byte("test.com\n"))
|
|
||||||
time.Sleep(200 * time.Millisecond)
|
|
||||||
r.SetExitStatus(0, nil)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Command not found!")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"GetHostByName returns hostname, DNSDomain query fails": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
CommandFunc: func(r *remote.Cmd) error {
|
|
||||||
switch r.Command {
|
|
||||||
case getHostByNameCmd:
|
|
||||||
r.Stdout.Write([]byte("example\n"))
|
|
||||||
time.Sleep(200 * time.Millisecond)
|
|
||||||
r.SetExitStatus(0, nil)
|
|
||||||
case domainQueryCmd:
|
|
||||||
r.SetExitStatus(1, &remote.ExitError{
|
|
||||||
Command: domainQueryCmd,
|
|
||||||
ExitStatus: 1,
|
|
||||||
Err: nil,
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Command not found!")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExpectedError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, tc := range cases {
|
|
||||||
p, err := decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := new(communicator.MockCommunicator)
|
|
||||||
c.Commands = tc.Commands
|
|
||||||
if tc.CommandFunc != nil {
|
|
||||||
c.CommandFunc = tc.CommandFunc
|
|
||||||
}
|
|
||||||
p.comm = c
|
|
||||||
p.output = new(terraform.MockUIOutput)
|
|
||||||
|
|
||||||
_, err = p.windowsDefaultCertname()
|
|
||||||
if tc.ExpectedError {
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Expected error, but no error returned")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %q failed: %v", k, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvisioner_windowsInstallPuppetAgent(t *testing.T) {
|
|
||||||
cases := map[string]struct {
|
|
||||||
Config map[string]interface{}
|
|
||||||
Commands map[string]bool
|
|
||||||
CommandFunc func(*remote.Cmd) error
|
|
||||||
ExpectedError bool
|
|
||||||
}{
|
|
||||||
"Everything runs succcessfully": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
Commands: map[string]bool{
|
|
||||||
downloadInstallerCmd: true,
|
|
||||||
runInstallerCmd: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"Installer download fails": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": true,
|
|
||||||
},
|
|
||||||
CommandFunc: func(r *remote.Cmd) error {
|
|
||||||
switch r.Command {
|
|
||||||
case downloadInstallerCmd:
|
|
||||||
r.SetExitStatus(1, &remote.ExitError{
|
|
||||||
Command: downloadInstallerCmd,
|
|
||||||
ExitStatus: 1,
|
|
||||||
Err: nil,
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Command not found!")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExpectedError: true,
|
|
||||||
},
|
|
||||||
"Install script fails": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
CommandFunc: func(r *remote.Cmd) error {
|
|
||||||
switch r.Command {
|
|
||||||
case downloadInstallerCmd:
|
|
||||||
r.SetExitStatus(0, nil)
|
|
||||||
case runInstallerCmd:
|
|
||||||
r.SetExitStatus(1, &remote.ExitError{
|
|
||||||
Command: runInstallerCmd,
|
|
||||||
ExitStatus: 1,
|
|
||||||
Err: nil,
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Command not found!")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExpectedError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, tc := range cases {
|
|
||||||
p, err := decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := new(communicator.MockCommunicator)
|
|
||||||
c.Commands = tc.Commands
|
|
||||||
if tc.CommandFunc != nil {
|
|
||||||
c.CommandFunc = tc.CommandFunc
|
|
||||||
}
|
|
||||||
p.comm = c
|
|
||||||
p.output = new(terraform.MockUIOutput)
|
|
||||||
|
|
||||||
err = p.windowsInstallPuppetAgent()
|
|
||||||
if tc.ExpectedError {
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Expected error, but no error returned")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %q failed: %v", k, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvisioner_windowsRunPuppetAgent(t *testing.T) {
|
|
||||||
cases := map[string]struct {
|
|
||||||
Config map[string]interface{}
|
|
||||||
Commands map[string]bool
|
|
||||||
CommandFunc func(*remote.Cmd) error
|
|
||||||
ExpectedError bool
|
|
||||||
}{
|
|
||||||
"When puppet returns 0": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
Commands: map[string]bool{
|
|
||||||
runPuppetCmd: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"When puppet returns 2 (changes applied without error)": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
CommandFunc: func(r *remote.Cmd) error {
|
|
||||||
r.SetExitStatus(2, &remote.ExitError{
|
|
||||||
Command: runPuppetCmd,
|
|
||||||
ExitStatus: 2,
|
|
||||||
Err: nil,
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"When puppet returns something not 0 or 2": {
|
|
||||||
Config: map[string]interface{}{
|
|
||||||
"server": "puppet.test.com",
|
|
||||||
"use_sudo": false,
|
|
||||||
},
|
|
||||||
CommandFunc: func(r *remote.Cmd) error {
|
|
||||||
r.SetExitStatus(1, &remote.ExitError{
|
|
||||||
Command: runPuppetCmd,
|
|
||||||
ExitStatus: 1,
|
|
||||||
Err: nil,
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
ExpectedError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, tc := range cases {
|
|
||||||
p, err := decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := new(communicator.MockCommunicator)
|
|
||||||
c.Commands = tc.Commands
|
|
||||||
if tc.CommandFunc != nil {
|
|
||||||
c.CommandFunc = tc.CommandFunc
|
|
||||||
}
|
|
||||||
p.comm = c
|
|
||||||
p.output = new(terraform.MockUIOutput)
|
|
||||||
|
|
||||||
err = p.windowsRunPuppetAgent()
|
|
||||||
if tc.ExpectedError {
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Expected error, but no error returned")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %q failed: %v", k, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,525 +0,0 @@
|
||||||
// This package implements a provisioner for Terraform that executes a
|
|
||||||
// saltstack state within the remote machine
|
|
||||||
//
|
|
||||||
// Adapted from gitub.com/hashicorp/packer/provisioner/salt-masterless
|
|
||||||
|
|
||||||
package saltmasterless
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/communicator"
|
|
||||||
"github.com/hashicorp/terraform/communicator/remote"
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
linereader "github.com/mitchellh/go-linereader"
|
|
||||||
)
|
|
||||||
|
|
||||||
type provisionFn func(terraform.UIOutput, communicator.Communicator) error
|
|
||||||
|
|
||||||
type provisioner struct {
|
|
||||||
SkipBootstrap bool
|
|
||||||
BootstrapArgs string
|
|
||||||
LocalStateTree string
|
|
||||||
DisableSudo bool
|
|
||||||
CustomState string
|
|
||||||
MinionConfig string
|
|
||||||
LocalPillarRoots string
|
|
||||||
RemoteStateTree string
|
|
||||||
RemotePillarRoots string
|
|
||||||
TempConfigDir string
|
|
||||||
NoExitOnFailure bool
|
|
||||||
LogLevel string
|
|
||||||
SaltCallArgs string
|
|
||||||
CmdArgs string
|
|
||||||
}
|
|
||||||
|
|
||||||
const DefaultStateTreeDir = "/srv/salt"
|
|
||||||
const DefaultPillarRootDir = "/srv/pillar"
|
|
||||||
|
|
||||||
// Provisioner returns a salt-masterless provisioner
|
|
||||||
func Provisioner() terraform.ResourceProvisioner {
|
|
||||||
return &schema.Provisioner{
|
|
||||||
Schema: map[string]*schema.Schema{
|
|
||||||
"local_state_tree": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"local_pillar_roots": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"remote_state_tree": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
Default: DefaultStateTreeDir,
|
|
||||||
},
|
|
||||||
"remote_pillar_roots": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
Default: DefaultPillarRootDir,
|
|
||||||
},
|
|
||||||
"temp_config_dir": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
Default: "/tmp/salt",
|
|
||||||
},
|
|
||||||
"skip_bootstrap": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"no_exit_on_failure": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"bootstrap_args": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"disable_sudo": &schema.Schema{
|
|
||||||
Type: schema.TypeBool,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"custom_state": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"minion_config_file": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"cmd_args": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"salt_call_args": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
"log_level": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
|
||||||
Optional: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
ApplyFunc: applyFn,
|
|
||||||
ValidateFunc: validateFn,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply executes the file provisioner
|
|
||||||
func applyFn(ctx context.Context) error {
|
|
||||||
// Decode the raw config for this provisioner
|
|
||||||
o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
|
|
||||||
d := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
|
|
||||||
connState := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
|
|
||||||
|
|
||||||
p, err := decodeConfig(d)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a new communicator
|
|
||||||
comm, err := communicator.New(connState)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Wait and retry until we establish the connection
|
|
||||||
err = communicator.Retry(retryCtx, func() error {
|
|
||||||
return comm.Connect(o)
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for the context to end and then disconnect
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
comm.Disconnect()
|
|
||||||
}()
|
|
||||||
|
|
||||||
var src, dst string
|
|
||||||
|
|
||||||
o.Output("Provisioning with Salt...")
|
|
||||||
if !p.SkipBootstrap {
|
|
||||||
cmd := &remote.Cmd{
|
|
||||||
// Fallback on wget if curl failed for any reason (such as not being installed)
|
|
||||||
Command: fmt.Sprintf("curl -L https://bootstrap.saltstack.com -o /tmp/install_salt.sh || wget -O /tmp/install_salt.sh https://bootstrap.saltstack.com"),
|
|
||||||
}
|
|
||||||
o.Output(fmt.Sprintf("Downloading saltstack bootstrap to /tmp/install_salt.sh"))
|
|
||||||
if err = comm.Start(cmd); err != nil {
|
|
||||||
err = fmt.Errorf("Unable to download Salt: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
outR, outW := io.Pipe()
|
|
||||||
errR, errW := io.Pipe()
|
|
||||||
go copyOutput(o, outR)
|
|
||||||
go copyOutput(o, errR)
|
|
||||||
defer outW.Close()
|
|
||||||
defer errW.Close()
|
|
||||||
|
|
||||||
cmd = &remote.Cmd{
|
|
||||||
Command: fmt.Sprintf("%s /tmp/install_salt.sh %s", p.sudo("sh"), p.BootstrapArgs),
|
|
||||||
Stdout: outW,
|
|
||||||
Stderr: errW,
|
|
||||||
}
|
|
||||||
|
|
||||||
o.Output(fmt.Sprintf("Installing Salt with command %s", cmd.Command))
|
|
||||||
if err := comm.Start(cmd); err != nil {
|
|
||||||
return fmt.Errorf("Unable to install Salt: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
o.Output(fmt.Sprintf("Creating remote temporary directory: %s", p.TempConfigDir))
|
|
||||||
if err := p.createDir(o, comm, p.TempConfigDir); err != nil {
|
|
||||||
return fmt.Errorf("Error creating remote temporary directory: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.MinionConfig != "" {
|
|
||||||
o.Output(fmt.Sprintf("Uploading minion config: %s", p.MinionConfig))
|
|
||||||
src = p.MinionConfig
|
|
||||||
dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion"))
|
|
||||||
if err = p.uploadFile(o, comm, dst, src); err != nil {
|
|
||||||
return fmt.Errorf("Error uploading local minion config file to remote: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// move minion config into /etc/salt
|
|
||||||
o.Output(fmt.Sprintf("Make sure directory %s exists", "/etc/salt"))
|
|
||||||
if err := p.createDir(o, comm, "/etc/salt"); err != nil {
|
|
||||||
return fmt.Errorf("Error creating remote salt configuration directory: %s", err)
|
|
||||||
}
|
|
||||||
src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion"))
|
|
||||||
dst = "/etc/salt/minion"
|
|
||||||
if err = p.moveFile(o, comm, dst, src); err != nil {
|
|
||||||
return fmt.Errorf("Unable to move %s/minion to /etc/salt/minion: %s", p.TempConfigDir, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
o.Output(fmt.Sprintf("Uploading local state tree: %s", p.LocalStateTree))
|
|
||||||
src = p.LocalStateTree
|
|
||||||
dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states"))
|
|
||||||
if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil {
|
|
||||||
return fmt.Errorf("Error uploading local state tree to remote: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// move state tree from temporary directory
|
|
||||||
src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states"))
|
|
||||||
dst = p.RemoteStateTree
|
|
||||||
if err = p.removeDir(o, comm, dst); err != nil {
|
|
||||||
return fmt.Errorf("Unable to clear salt tree: %s", err)
|
|
||||||
}
|
|
||||||
if err = p.moveFile(o, comm, dst, src); err != nil {
|
|
||||||
return fmt.Errorf("Unable to move %s/states to %s: %s", p.TempConfigDir, dst, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.LocalPillarRoots != "" {
|
|
||||||
o.Output(fmt.Sprintf("Uploading local pillar roots: %s", p.LocalPillarRoots))
|
|
||||||
src = p.LocalPillarRoots
|
|
||||||
dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar"))
|
|
||||||
if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil {
|
|
||||||
return fmt.Errorf("Error uploading local pillar roots to remote: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// move pillar root from temporary directory
|
|
||||||
src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar"))
|
|
||||||
dst = p.RemotePillarRoots
|
|
||||||
|
|
||||||
if err = p.removeDir(o, comm, dst); err != nil {
|
|
||||||
return fmt.Errorf("Unable to clear pillar root: %s", err)
|
|
||||||
}
|
|
||||||
if err = p.moveFile(o, comm, dst, src); err != nil {
|
|
||||||
return fmt.Errorf("Unable to move %s/pillar to %s: %s", p.TempConfigDir, dst, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
outR, outW := io.Pipe()
|
|
||||||
errR, errW := io.Pipe()
|
|
||||||
go copyOutput(o, outR)
|
|
||||||
go copyOutput(o, errR)
|
|
||||||
defer outW.Close()
|
|
||||||
defer errW.Close()
|
|
||||||
|
|
||||||
o.Output(fmt.Sprintf("Running: salt-call --local %s", p.CmdArgs))
|
|
||||||
cmd := &remote.Cmd{
|
|
||||||
Command: p.sudo(fmt.Sprintf("salt-call --local %s", p.CmdArgs)),
|
|
||||||
Stdout: outW,
|
|
||||||
Stderr: errW,
|
|
||||||
}
|
|
||||||
if err = comm.Start(cmd); err != nil {
|
|
||||||
err = fmt.Errorf("Error executing salt-call: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepends sudo to supplied command if config says to
|
|
||||||
func (p *provisioner) sudo(cmd string) string {
|
|
||||||
if p.DisableSudo {
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
return "sudo " + cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateDirConfig(path string, name string, required bool) error {
|
|
||||||
if required == true && path == "" {
|
|
||||||
return fmt.Errorf("%s cannot be empty", name)
|
|
||||||
} else if required == false && path == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
info, err := os.Stat(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err)
|
|
||||||
} else if !info.IsDir() {
|
|
||||||
return fmt.Errorf("%s: path '%s' must point to a directory", name, path)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateFileConfig(path string, name string, required bool) error {
|
|
||||||
if required == true && path == "" {
|
|
||||||
return fmt.Errorf("%s cannot be empty", name)
|
|
||||||
} else if required == false && path == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
info, err := os.Stat(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err)
|
|
||||||
} else if info.IsDir() {
|
|
||||||
return fmt.Errorf("%s: path '%s' must point to a file", name, path)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) uploadFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error {
|
|
||||||
f, err := os.Open(src)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Error opening: %s", err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
if err = comm.Upload(dst, f); err != nil {
|
|
||||||
return fmt.Errorf("Error uploading %s: %s", src, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) moveFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error {
|
|
||||||
o.Output(fmt.Sprintf("Moving %s to %s", src, dst))
|
|
||||||
cmd := &remote.Cmd{Command: fmt.Sprintf(p.sudo("mv %s %s"), src, dst)}
|
|
||||||
if err := comm.Start(cmd); err != nil {
|
|
||||||
return fmt.Errorf("Unable to move %s to %s: %s", src, dst, err)
|
|
||||||
}
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) createDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error {
|
|
||||||
o.Output(fmt.Sprintf("Creating directory: %s", dir))
|
|
||||||
cmd := &remote.Cmd{
|
|
||||||
Command: fmt.Sprintf("mkdir -p '%s'", dir),
|
|
||||||
}
|
|
||||||
if err := comm.Start(cmd); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) removeDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error {
|
|
||||||
o.Output(fmt.Sprintf("Removing directory: %s", dir))
|
|
||||||
cmd := &remote.Cmd{
|
|
||||||
Command: fmt.Sprintf("rm -rf '%s'", dir),
|
|
||||||
}
|
|
||||||
if err := comm.Start(cmd); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *provisioner) uploadDir(o terraform.UIOutput, comm communicator.Communicator, dst, src string, ignore []string) error {
|
|
||||||
if err := p.createDir(o, comm, dst); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure there is a trailing "/" so that the directory isn't
|
|
||||||
// created on the other side.
|
|
||||||
if src[len(src)-1] != '/' {
|
|
||||||
src = src + "/"
|
|
||||||
}
|
|
||||||
return comm.UploadDir(dst, src)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate checks if the required arguments are configured
|
|
||||||
func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) {
|
|
||||||
// require a salt state tree
|
|
||||||
localStateTreeTmp, ok := c.Get("local_state_tree")
|
|
||||||
var localStateTree string
|
|
||||||
if !ok {
|
|
||||||
es = append(es,
|
|
||||||
errors.New("Required local_state_tree is not set"))
|
|
||||||
} else {
|
|
||||||
localStateTree = localStateTreeTmp.(string)
|
|
||||||
}
|
|
||||||
err := validateDirConfig(localStateTree, "local_state_tree", true)
|
|
||||||
if err != nil {
|
|
||||||
es = append(es, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var localPillarRoots string
|
|
||||||
localPillarRootsTmp, ok := c.Get("local_pillar_roots")
|
|
||||||
if !ok {
|
|
||||||
localPillarRoots = ""
|
|
||||||
} else {
|
|
||||||
localPillarRoots = localPillarRootsTmp.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = validateDirConfig(localPillarRoots, "local_pillar_roots", false)
|
|
||||||
if err != nil {
|
|
||||||
es = append(es, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var minionConfig string
|
|
||||||
minionConfigTmp, ok := c.Get("minion_config_file")
|
|
||||||
if !ok {
|
|
||||||
minionConfig = ""
|
|
||||||
} else {
|
|
||||||
minionConfig = minionConfigTmp.(string)
|
|
||||||
}
|
|
||||||
err = validateFileConfig(minionConfig, "minion_config_file", false)
|
|
||||||
if err != nil {
|
|
||||||
es = append(es, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var remoteStateTree string
|
|
||||||
remoteStateTreeTmp, ok := c.Get("remote_state_tree")
|
|
||||||
if !ok {
|
|
||||||
remoteStateTree = DefaultStateTreeDir
|
|
||||||
} else {
|
|
||||||
remoteStateTree = remoteStateTreeTmp.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
var remotePillarRoots string
|
|
||||||
remotePillarRootsTmp, ok := c.Get("remote_pillar_roots")
|
|
||||||
if !ok {
|
|
||||||
remotePillarRoots = DefaultPillarRootDir
|
|
||||||
} else {
|
|
||||||
remotePillarRoots = remotePillarRootsTmp.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if minionConfig != "" && (remoteStateTree != DefaultStateTreeDir || remotePillarRoots != DefaultPillarRootDir) {
|
|
||||||
es = append(es,
|
|
||||||
errors.New("remote_state_tree and remote_pillar_roots only apply when minion_config_file is not used"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(es) > 0 {
|
|
||||||
return ws, es
|
|
||||||
}
|
|
||||||
|
|
||||||
return ws, es
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
|
|
||||||
p := &provisioner{
|
|
||||||
LocalStateTree: d.Get("local_state_tree").(string),
|
|
||||||
LogLevel: d.Get("log_level").(string),
|
|
||||||
SaltCallArgs: d.Get("salt_call_args").(string),
|
|
||||||
CmdArgs: d.Get("cmd_args").(string),
|
|
||||||
MinionConfig: d.Get("minion_config_file").(string),
|
|
||||||
CustomState: d.Get("custom_state").(string),
|
|
||||||
DisableSudo: d.Get("disable_sudo").(bool),
|
|
||||||
BootstrapArgs: d.Get("bootstrap_args").(string),
|
|
||||||
NoExitOnFailure: d.Get("no_exit_on_failure").(bool),
|
|
||||||
SkipBootstrap: d.Get("skip_bootstrap").(bool),
|
|
||||||
TempConfigDir: d.Get("temp_config_dir").(string),
|
|
||||||
RemotePillarRoots: d.Get("remote_pillar_roots").(string),
|
|
||||||
RemoteStateTree: d.Get("remote_state_tree").(string),
|
|
||||||
LocalPillarRoots: d.Get("local_pillar_roots").(string),
|
|
||||||
}
|
|
||||||
|
|
||||||
// build the command line args to pass onto salt
|
|
||||||
var cmdArgs bytes.Buffer
|
|
||||||
|
|
||||||
if p.CustomState == "" {
|
|
||||||
cmdArgs.WriteString(" state.highstate")
|
|
||||||
} else {
|
|
||||||
cmdArgs.WriteString(" state.sls ")
|
|
||||||
cmdArgs.WriteString(p.CustomState)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.MinionConfig == "" {
|
|
||||||
// pass --file-root and --pillar-root if no minion_config_file is supplied
|
|
||||||
if p.RemoteStateTree != "" {
|
|
||||||
cmdArgs.WriteString(" --file-root=")
|
|
||||||
cmdArgs.WriteString(p.RemoteStateTree)
|
|
||||||
} else {
|
|
||||||
cmdArgs.WriteString(" --file-root=")
|
|
||||||
cmdArgs.WriteString(DefaultStateTreeDir)
|
|
||||||
}
|
|
||||||
if p.RemotePillarRoots != "" {
|
|
||||||
cmdArgs.WriteString(" --pillar-root=")
|
|
||||||
cmdArgs.WriteString(p.RemotePillarRoots)
|
|
||||||
} else {
|
|
||||||
cmdArgs.WriteString(" --pillar-root=")
|
|
||||||
cmdArgs.WriteString(DefaultPillarRootDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.NoExitOnFailure {
|
|
||||||
cmdArgs.WriteString(" --retcode-passthrough")
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.LogLevel == "" {
|
|
||||||
cmdArgs.WriteString(" -l info")
|
|
||||||
} else {
|
|
||||||
cmdArgs.WriteString(" -l ")
|
|
||||||
cmdArgs.WriteString(p.LogLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.SaltCallArgs != "" {
|
|
||||||
cmdArgs.WriteString(" ")
|
|
||||||
cmdArgs.WriteString(p.SaltCallArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.CmdArgs = cmdArgs.String()
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyOutput(
|
|
||||||
o terraform.UIOutput, r io.Reader) {
|
|
||||||
lr := linereader.New(r)
|
|
||||||
for line := range lr.Ch {
|
|
||||||
o.Output(line)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,452 +0,0 @@
|
||||||
package saltmasterless
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
)
|
|
||||||
|
|
||||||
func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig {
|
|
||||||
return terraform.NewResourceConfigRaw(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvisioner_impl(t *testing.T) {
|
|
||||||
var _ terraform.ResourceProvisioner = Provisioner()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisioner(t *testing.T) {
|
|
||||||
if err := Provisioner().(*schema.Provisioner).InternalValidate(); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvisioner_Validate_good(t *testing.T) {
|
|
||||||
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error when creating temp dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer os.RemoveAll(dir) // clean up
|
|
||||||
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"local_state_tree": dir,
|
|
||||||
})
|
|
||||||
warn, errs := Provisioner().Validate(c)
|
|
||||||
if len(warn) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warn)
|
|
||||||
}
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Fatalf("Errors: %v", errs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvider_Validate_missing_required(t *testing.T) {
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"remote_state_tree": "_default",
|
|
||||||
})
|
|
||||||
warn, errs := Provisioner().Validate(c)
|
|
||||||
if len(warn) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warn)
|
|
||||||
}
|
|
||||||
if len(errs) == 0 {
|
|
||||||
t.Fatalf("Should have errors")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvider_Validate_LocalStateTree_doesnt_exist(t *testing.T) {
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"local_state_tree": "/i/dont/exist",
|
|
||||||
})
|
|
||||||
warn, errs := Provisioner().Validate(c)
|
|
||||||
if len(warn) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warn)
|
|
||||||
}
|
|
||||||
if len(errs) == 0 {
|
|
||||||
t.Fatalf("Should have errors")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvisioner_Validate_invalid(t *testing.T) {
|
|
||||||
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error when creating temp dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer os.RemoveAll(dir) // clean up
|
|
||||||
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"local_state_tree": dir,
|
|
||||||
"i_am_not_valid": "_invalid",
|
|
||||||
})
|
|
||||||
|
|
||||||
warn, errs := Provisioner().Validate(c)
|
|
||||||
if len(warn) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warn)
|
|
||||||
}
|
|
||||||
if len(errs) == 0 {
|
|
||||||
t.Fatalf("Should have errors")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisionerPrepare_CustomState(t *testing.T) {
|
|
||||||
c := map[string]interface{}{
|
|
||||||
"local_state_tree": "/tmp/local_state_tree",
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(p.CmdArgs, "state.highstate") {
|
|
||||||
t.Fatal("CmdArgs should contain state.highstate")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c = map[string]interface{}{
|
|
||||||
"local_state_tree": "/tmp/local_state_tree",
|
|
||||||
"custom_state": "custom",
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err = decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(p.CmdArgs, "state.sls custom") {
|
|
||||||
t.Fatal("CmdArgs should contain state.sls custom")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisionerPrepare_MinionConfig(t *testing.T) {
|
|
||||||
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error when creating temp dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer os.RemoveAll(dir) // clean up
|
|
||||||
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"local_state_tree": dir,
|
|
||||||
"minion_config_file": "i/dont/exist",
|
|
||||||
})
|
|
||||||
|
|
||||||
warns, errs := Provisioner().Validate(c)
|
|
||||||
|
|
||||||
if len(warns) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warns)
|
|
||||||
}
|
|
||||||
if len(errs) == 0 {
|
|
||||||
t.Fatalf("Should have error")
|
|
||||||
}
|
|
||||||
|
|
||||||
tf, err := ioutil.TempFile("", "minion")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error tempfile: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer os.Remove(tf.Name())
|
|
||||||
|
|
||||||
c = testConfig(t, map[string]interface{}{
|
|
||||||
"local_state_tree": dir,
|
|
||||||
"minion_config_file": tf.Name(),
|
|
||||||
})
|
|
||||||
|
|
||||||
warns, errs = Provisioner().Validate(c)
|
|
||||||
|
|
||||||
if len(warns) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warns)
|
|
||||||
}
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Fatalf("errs: %s", errs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisionerPrepare_MinionConfig_RemoteStateTree(t *testing.T) {
|
|
||||||
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error when creating temp dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"local_state_tree": dir,
|
|
||||||
"minion_config_file": "i/dont/exist",
|
|
||||||
"remote_state_tree": "i/dont/exist/remote_state_tree",
|
|
||||||
})
|
|
||||||
|
|
||||||
warns, errs := Provisioner().Validate(c)
|
|
||||||
if len(warns) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warns)
|
|
||||||
}
|
|
||||||
if len(errs) == 0 {
|
|
||||||
t.Fatalf("Should be error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisionerPrepare_MinionConfig_RemotePillarRoots(t *testing.T) {
|
|
||||||
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error when creating temp dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"local_state_tree": dir,
|
|
||||||
"minion_config_file": "i/dont/exist",
|
|
||||||
"remote_pillar_roots": "i/dont/exist/remote_pillar_roots",
|
|
||||||
})
|
|
||||||
|
|
||||||
warns, errs := Provisioner().Validate(c)
|
|
||||||
if len(warns) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warns)
|
|
||||||
}
|
|
||||||
if len(errs) == 0 {
|
|
||||||
t.Fatalf("Should be error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisionerPrepare_LocalPillarRoots(t *testing.T) {
|
|
||||||
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error when creating temp dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"local_state_tree": dir,
|
|
||||||
"minion_config_file": "i/dont/exist",
|
|
||||||
"local_pillar_roots": "i/dont/exist/local_pillar_roots",
|
|
||||||
})
|
|
||||||
|
|
||||||
warns, errs := Provisioner().Validate(c)
|
|
||||||
if len(warns) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warns)
|
|
||||||
}
|
|
||||||
if len(errs) == 0 {
|
|
||||||
t.Fatalf("Should be error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisionerSudo(t *testing.T) {
|
|
||||||
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error when creating temp dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := map[string]interface{}{
|
|
||||||
"local_state_tree": dir,
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
|
|
||||||
)
|
|
||||||
|
|
||||||
withSudo := p.sudo("echo hello")
|
|
||||||
if withSudo != "sudo echo hello" {
|
|
||||||
t.Fatalf("sudo command not generated correctly")
|
|
||||||
}
|
|
||||||
|
|
||||||
c = map[string]interface{}{
|
|
||||||
"local_state_tree": dir,
|
|
||||||
"disable_sudo": "true",
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err = decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
withoutSudo := p.sudo("echo hello")
|
|
||||||
if withoutSudo != "echo hello" {
|
|
||||||
t.Fatalf("sudo-less command not generated correctly")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisionerPrepare_RemoteStateTree(t *testing.T) {
|
|
||||||
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error when creating temp dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := map[string]interface{}{
|
|
||||||
"local_state_tree": dir,
|
|
||||||
"remote_state_tree": "/remote_state_tree",
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(p.CmdArgs, "--file-root=/remote_state_tree") {
|
|
||||||
t.Fatal("--file-root should be set in CmdArgs")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisionerPrepare_RemotePillarRoots(t *testing.T) {
|
|
||||||
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error when creating temp dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := map[string]interface{}{
|
|
||||||
"local_state_tree": dir,
|
|
||||||
"remote_pillar_roots": "/remote_pillar_roots",
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(p.CmdArgs, "--pillar-root=/remote_pillar_roots") {
|
|
||||||
t.Fatal("--pillar-root should be set in CmdArgs")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisionerPrepare_RemoteStateTree_Default(t *testing.T) {
|
|
||||||
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error when creating temp dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := map[string]interface{}{
|
|
||||||
"local_state_tree": dir,
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(p.CmdArgs, "--file-root=/srv/salt") {
|
|
||||||
t.Fatal("--file-root should be set in CmdArgs")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisionerPrepare_RemotePillarRoots_Default(t *testing.T) {
|
|
||||||
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error when creating temp dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := map[string]interface{}{
|
|
||||||
"local_state_tree": dir,
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(p.CmdArgs, "--pillar-root=/srv/pillar") {
|
|
||||||
t.Fatal("--pillar-root should be set in CmdArgs")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisionerPrepare_NoExitOnFailure(t *testing.T) {
|
|
||||||
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error when creating temp dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := map[string]interface{}{
|
|
||||||
"local_state_tree": dir,
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(p.CmdArgs, "--retcode-passthrough") {
|
|
||||||
t.Fatal("--retcode-passthrough should be set in CmdArgs")
|
|
||||||
}
|
|
||||||
|
|
||||||
c = map[string]interface{}{
|
|
||||||
"no_exit_on_failure": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err = decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(p.CmdArgs, "--retcode-passthrough") {
|
|
||||||
t.Fatal("--retcode-passthrough should not be set in CmdArgs")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisionerPrepare_LogLevel(t *testing.T) {
|
|
||||||
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error when creating temp dir: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := map[string]interface{}{
|
|
||||||
"local_state_tree": dir,
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(p.CmdArgs, "-l info") {
|
|
||||||
t.Fatal("-l info should be set in CmdArgs")
|
|
||||||
}
|
|
||||||
|
|
||||||
c = map[string]interface{}{
|
|
||||||
"log_level": "debug",
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err = decodeConfig(
|
|
||||||
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(p.CmdArgs, "-l debug") {
|
|
||||||
t.Fatal("-l debug should be set in CmdArgs")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,13 +4,9 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
chefprovisioner "github.com/hashicorp/terraform/builtin/provisioners/chef"
|
|
||||||
fileprovisioner "github.com/hashicorp/terraform/builtin/provisioners/file"
|
fileprovisioner "github.com/hashicorp/terraform/builtin/provisioners/file"
|
||||||
habitatprovisioner "github.com/hashicorp/terraform/builtin/provisioners/habitat"
|
|
||||||
localexecprovisioner "github.com/hashicorp/terraform/builtin/provisioners/local-exec"
|
localexecprovisioner "github.com/hashicorp/terraform/builtin/provisioners/local-exec"
|
||||||
puppetprovisioner "github.com/hashicorp/terraform/builtin/provisioners/puppet"
|
|
||||||
remoteexecprovisioner "github.com/hashicorp/terraform/builtin/provisioners/remote-exec"
|
remoteexecprovisioner "github.com/hashicorp/terraform/builtin/provisioners/remote-exec"
|
||||||
saltmasterlessprovisioner "github.com/hashicorp/terraform/builtin/provisioners/salt-masterless"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/plugin"
|
"github.com/hashicorp/terraform/plugin"
|
||||||
)
|
)
|
||||||
|
@ -18,11 +14,7 @@ import (
|
||||||
var InternalProviders = map[string]plugin.ProviderFunc{}
|
var InternalProviders = map[string]plugin.ProviderFunc{}
|
||||||
|
|
||||||
var InternalProvisioners = map[string]plugin.ProvisionerFunc{
|
var InternalProvisioners = map[string]plugin.ProvisionerFunc{
|
||||||
"chef": chefprovisioner.Provisioner,
|
|
||||||
"file": fileprovisioner.Provisioner,
|
"file": fileprovisioner.Provisioner,
|
||||||
"habitat": habitatprovisioner.Provisioner,
|
|
||||||
"local-exec": localexecprovisioner.Provisioner,
|
"local-exec": localexecprovisioner.Provisioner,
|
||||||
"puppet": puppetprovisioner.Provisioner,
|
|
||||||
"remote-exec": remoteexecprovisioner.Provisioner,
|
"remote-exec": remoteexecprovisioner.Provisioner,
|
||||||
"salt-masterless": saltmasterlessprovisioner.Provisioner,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ func TestInternalPlugin_InternalProviders(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInternalPlugin_InternalProvisioners(t *testing.T) {
|
func TestInternalPlugin_InternalProvisioners(t *testing.T) {
|
||||||
for _, name := range []string{"chef", "file", "local-exec", "remote-exec", "salt-masterless"} {
|
for _, name := range []string{"file", "local-exec", "remote-exec"} {
|
||||||
if _, ok := InternalProvisioners[name]; !ok {
|
if _, ok := InternalProvisioners[name]; !ok {
|
||||||
t.Errorf("Expected to find %s in InternalProvisioners", name)
|
t.Errorf("Expected to find %s in InternalProvisioners", name)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,11 +34,12 @@ func decodeProvisionerBlock(block *hcl.Block) (*Provisioner, hcl.Diagnostics) {
|
||||||
switch pv.Type {
|
switch pv.Type {
|
||||||
case "chef", "habitat", "puppet", "salt-masterless":
|
case "chef", "habitat", "puppet", "salt-masterless":
|
||||||
diags = append(diags, &hcl.Diagnostic{
|
diags = append(diags, &hcl.Diagnostic{
|
||||||
Severity: hcl.DiagWarning,
|
Severity: hcl.DiagError,
|
||||||
Summary: fmt.Sprintf("The \"%s\" provisioner is deprecated", pv.Type),
|
Summary: fmt.Sprintf("The \"%s\" provisioner has been removed", pv.Type),
|
||||||
Detail: fmt.Sprintf("The \"%s\" provisioner is deprecated and will be removed from future versions of Terraform. Visit https://learn.hashicorp.com/collections/terraform/provision for alternatives to using provisioners that are a better fit for the Terraform workflow.", pv.Type),
|
Detail: fmt.Sprintf("The \"%s\" provisioner was deprecated in Terraform 0.13.4 has been removed from Terraform. Visit https://learn.hashicorp.com/collections/terraform/provision for alternatives to using provisioners that are a better fit for the Terraform workflow.", pv.Type),
|
||||||
Subject: &pv.TypeRange,
|
Subject: &pv.TypeRange,
|
||||||
})
|
})
|
||||||
|
return nil, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
if attr, exists := content.Attributes["when"]; exists {
|
if attr, exists := content.Attributes["when"]; exists {
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
resource "null_resource" "test" {
|
||||||
|
provisioner "habitat" {} # ERROR: The "habitat" provisioner has been removed
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
resource "null_resource" "test" {
|
|
||||||
provisioner "habitat" {} # WARNING: The "habitat" provisioner is deprecated
|
|
||||||
}
|
|
|
@ -6,7 +6,7 @@ gofmt_files=$(gofmt -l `find . -name '*.go' | grep -v vendor`)
|
||||||
if [[ -n ${gofmt_files} ]]; then
|
if [[ -n ${gofmt_files} ]]; then
|
||||||
echo 'gofmt needs running on the following files:'
|
echo 'gofmt needs running on the following files:'
|
||||||
echo "${gofmt_files}"
|
echo "${gofmt_files}"
|
||||||
echo "You can use the command: \`make fmtcheck\` to reformat code."
|
echo "You can use the command: \`gofmt -w .\` to reformat code."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,7 @@ The `chef` provisioner installs, configures and runs the Chef Client on a remote
|
||||||
resource. The `chef` provisioner supports both `ssh` and `winrm` type
|
resource. The `chef` provisioner supports both `ssh` and `winrm` type
|
||||||
[connections](/docs/provisioners/connection.html).
|
[connections](/docs/provisioners/connection.html).
|
||||||
|
|
||||||
!> **Note:** This provisioner has been deprecated as of Terraform 0.13.4 and will be
|
!> **Note:** This provisioner was removed in the 0.14.0 version of Terraform after being deprecated as of Terraform 0.13.4. For most common situations there are better alternatives to using provisioners. For more information, see [the main Provisioners page](./).
|
||||||
removed in a future version of Terraform. For most common situations there are better
|
|
||||||
alternatives to using provisioners. For more information, see
|
|
||||||
[the main Provisioners page](./).
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|
|
@ -10,10 +10,7 @@ description: |-
|
||||||
|
|
||||||
The `habitat` provisioner installs the [Habitat](https://habitat.sh) supervisor and loads configured services. This provisioner only supports Linux targets using the `ssh` connection type at this time.
|
The `habitat` provisioner installs the [Habitat](https://habitat.sh) supervisor and loads configured services. This provisioner only supports Linux targets using the `ssh` connection type at this time.
|
||||||
|
|
||||||
!> **Note:** This provisioner has been deprecated as of Terraform 0.13.4 and will be
|
!> **Note:** This provisioner was removed in the 0.14.0 version of Terraform after being deprecated as of Terraform 0.13.4. For most common situations there are better alternatives to using provisioners. For more information, see [the main Provisioners page](./).
|
||||||
removed in a future version of Terraform. For most common situations there are better
|
|
||||||
alternatives to using provisioners. For more information, see
|
|
||||||
[the main Provisioners page](./).
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,7 @@ The `puppet` provisioner installs, configures and runs the Puppet agent on a
|
||||||
remote resource. The `puppet` provisioner supports both `ssh` and `winrm` type
|
remote resource. The `puppet` provisioner supports both `ssh` and `winrm` type
|
||||||
[connections](/docs/provisioners/connection.html).
|
[connections](/docs/provisioners/connection.html).
|
||||||
|
|
||||||
!> **Note:** This provisioner has been deprecated as of Terraform 0.13.4 and will be
|
!> **Note:** This provisioner was removed in the 0.14.0 version of Terraform after being deprecated as of Terraform 0.13.4. For most common situations there are better alternatives to using provisioners. For more information, see [the main Provisioners page](./).
|
||||||
removed in a future version of Terraform. For most common situations there are better
|
|
||||||
alternatives to using provisioners. For more information, see
|
|
||||||
[the main Provisioners page](./).
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,7 @@ Type: `salt-masterless`
|
||||||
The `salt-masterless` Terraform provisioner provisions machines built by Terraform
|
The `salt-masterless` Terraform provisioner provisions machines built by Terraform
|
||||||
using [Salt](http://saltstack.com/) states, without connecting to a Salt master. The `salt-masterless` provisioner supports `ssh` [connections](/docs/provisioners/connection.html).
|
using [Salt](http://saltstack.com/) states, without connecting to a Salt master. The `salt-masterless` provisioner supports `ssh` [connections](/docs/provisioners/connection.html).
|
||||||
|
|
||||||
!> **Note:** This provisioner has been deprecated as of Terraform 0.13.4 and will be
|
!> **Note:** This provisioner was removed in the 0.14.0 version of Terraform after being deprecated as of Terraform 0.13.4. For most common situations there are better alternatives to using provisioners. For more information, see [the main Provisioners page](./).
|
||||||
removed in a future version of Terraform. For most common situations there are better
|
|
||||||
alternatives to using provisioners. For more information, see
|
|
||||||
[the main Provisioners page](./).
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue