mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-10 08:03:08 -06:00
Merge pull request #1868 from svanharmelen/f-chef-client-provisioner
New provisioner for Chef-Client
This commit is contained in:
commit
4e0aa334fa
15
builtin/bins/provisioner-chef/main.go
Normal file
15
builtin/bins/provisioner-chef/main.go
Normal file
@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform/builtin/provisioners/chef"
|
||||
"github.com/hashicorp/terraform/plugin"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func main() {
|
||||
plugin.Serve(&plugin.ServeOpts{
|
||||
ProvisionerFunc: func() terraform.ResourceProvisioner {
|
||||
return new(chef.ResourceProvisioner)
|
||||
},
|
||||
})
|
||||
}
|
1
builtin/bins/provisioner-chef/main_test.go
Normal file
1
builtin/bins/provisioner-chef/main_test.go
Normal file
@ -0,0 +1 @@
|
||||
package main
|
414
builtin/provisioners/chef/resource_provisioner.go
Normal file
414
builtin/provisioners/chef/resource_provisioner.go
Normal file
@ -0,0 +1,414 @@
|
||||
package chef
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform/communicator"
|
||||
"github.com/hashicorp/terraform/communicator/remote"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"github.com/mitchellh/go-linereader"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
const (
|
||||
clienrb = "client.rb"
|
||||
defaultEnv = "_default"
|
||||
firstBoot = "first-boot.json"
|
||||
logfileDir = "logfiles"
|
||||
linuxConfDir = "/etc/chef"
|
||||
validationKey = "validation.pem"
|
||||
windowsConfDir = "C:/chef"
|
||||
)
|
||||
|
||||
const clientConf = `
|
||||
log_location STDOUT
|
||||
chef_server_url "{{ .ServerURL }}"
|
||||
validation_client_name "{{ .ValidationClientName }}"
|
||||
node_name "{{ .NodeName }}"
|
||||
|
||||
{{ 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 "," }}"{{ end }}
|
||||
{{ if .SSLVerifyMode }}ssl_verify_mode {{ .SSLVerifyMode }}{{ end }}
|
||||
`
|
||||
|
||||
// Provisioner represents a specificly configured chef provisioner
|
||||
type Provisioner struct {
|
||||
Attributes interface{} `mapstructure:"attributes"`
|
||||
Environment string `mapstructure:"environment"`
|
||||
LogToFile bool `mapstructure:"log_to_file"`
|
||||
HTTPProxy string `mapstructure:"http_proxy"`
|
||||
HTTPSProxy string `mapstructure:"https_proxy"`
|
||||
NOProxy []string `mapstructure:"no_proxy"`
|
||||
NodeName string `mapstructure:"node_name"`
|
||||
PreventSudo bool `mapstructure:"prevent_sudo"`
|
||||
RunList []string `mapstructure:"run_list"`
|
||||
ServerURL string `mapstructure:"server_url"`
|
||||
SkipInstall bool `mapstructure:"skip_install"`
|
||||
SSLVerifyMode string `mapstructure:"ssl_verify_mode"`
|
||||
ValidationClientName string `mapstructure:"validation_client_name"`
|
||||
ValidationKeyPath string `mapstructure:"validation_key_path"`
|
||||
Version string `mapstructure:"version"`
|
||||
|
||||
installChefClient func(terraform.UIOutput, communicator.Communicator) error
|
||||
createConfigFiles func(terraform.UIOutput, communicator.Communicator) error
|
||||
runChefClient func(terraform.UIOutput, communicator.Communicator) error
|
||||
useSudo bool
|
||||
}
|
||||
|
||||
// ResourceProvisioner represents a generic chef provisioner
|
||||
type ResourceProvisioner struct{}
|
||||
|
||||
// Apply executes the file provisioner
|
||||
func (r *ResourceProvisioner) Apply(
|
||||
o terraform.UIOutput,
|
||||
s *terraform.InstanceState,
|
||||
c *terraform.ResourceConfig) error {
|
||||
// Decode the raw config for this provisioner
|
||||
p, err := r.decodeConfig(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set some values based on the targeted OS
|
||||
switch s.Ephemeral.ConnInfo["type"] {
|
||||
case "ssh", "": // The default connection type is ssh, so if the type is empty use ssh
|
||||
p.installChefClient = p.sshInstallChefClient
|
||||
p.createConfigFiles = p.sshCreateConfigFiles
|
||||
p.runChefClient = p.runChefClientFunc(linuxConfDir)
|
||||
p.useSudo = !p.PreventSudo && s.Ephemeral.ConnInfo["user"] != "root"
|
||||
case "winrm":
|
||||
p.installChefClient = p.winrmInstallChefClient
|
||||
p.createConfigFiles = p.winrmCreateConfigFiles
|
||||
p.runChefClient = p.runChefClientFunc(windowsConfDir)
|
||||
p.useSudo = false
|
||||
default:
|
||||
return fmt.Errorf("Unsupported connection type: %s", s.Ephemeral.ConnInfo["type"])
|
||||
}
|
||||
|
||||
// Get a new communicator
|
||||
comm, err := communicator.New(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait and retry until we establish the connection
|
||||
err = retryFunc(comm.Timeout(), func() error {
|
||||
err := comm.Connect(o)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer comm.Disconnect()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
o.Output("Starting initial Chef-Client run...")
|
||||
if err := p.runChefClient(o, comm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks if the required arguments are configured
|
||||
func (r *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) {
|
||||
p, err := r.decodeConfig(c)
|
||||
if err != nil {
|
||||
es = append(es, err)
|
||||
return ws, es
|
||||
}
|
||||
|
||||
if p.NodeName == "" {
|
||||
es = append(es, fmt.Errorf("Key not found: node_name"))
|
||||
}
|
||||
if p.RunList == nil {
|
||||
es = append(es, fmt.Errorf("Key not found: run_list"))
|
||||
}
|
||||
if p.ServerURL == "" {
|
||||
es = append(es, fmt.Errorf("Key not found: server_url"))
|
||||
}
|
||||
if p.ValidationClientName == "" {
|
||||
es = append(es, fmt.Errorf("Key not found: validation_client_name"))
|
||||
}
|
||||
if p.ValidationKeyPath == "" {
|
||||
es = append(es, fmt.Errorf("Key not found: validation_key_path"))
|
||||
}
|
||||
|
||||
return ws, es
|
||||
}
|
||||
|
||||
func (r *ResourceProvisioner) decodeConfig(c *terraform.ResourceConfig) (*Provisioner, error) {
|
||||
p := new(Provisioner)
|
||||
|
||||
decConf := &mapstructure.DecoderConfig{
|
||||
ErrorUnused: true,
|
||||
WeaklyTypedInput: true,
|
||||
Result: p,
|
||||
}
|
||||
dec, err := mapstructure.NewDecoder(decConf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := dec.Decode(c.Raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if p.Environment == "" {
|
||||
p.Environment = defaultEnv
|
||||
}
|
||||
|
||||
if attrs, ok := c.Raw["attributes"]; ok {
|
||||
p.Attributes, err = rawToJSON(attrs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error parsing the attributes: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func rawToJSON(raw interface{}) (interface{}, error) {
|
||||
switch s := raw.(type) {
|
||||
case []map[string]interface{}:
|
||||
if len(s) != 1 {
|
||||
return nil, errors.New("unexpected input while parsing raw config to JSON")
|
||||
}
|
||||
|
||||
var err error
|
||||
for k, v := range s[0] {
|
||||
s[0][k], err = rawToJSON(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return s[0], nil
|
||||
default:
|
||||
return raw, nil
|
||||
}
|
||||
}
|
||||
|
||||
// retryFunc is used to retry a function for a given duration
|
||||
func retryFunc(timeout time.Duration, f func() error) error {
|
||||
finish := time.After(timeout)
|
||||
for {
|
||||
err := f()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
log.Printf("Retryable error: %v", err)
|
||||
|
||||
select {
|
||||
case <-finish:
|
||||
return err
|
||||
case <-time.After(3 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provisioner) runChefClientFunc(
|
||||
confDir string) func(terraform.UIOutput, communicator.Communicator) error {
|
||||
return func(o terraform.UIOutput, comm communicator.Communicator) error {
|
||||
fb := path.Join(confDir, firstBoot)
|
||||
cmd := fmt.Sprintf("chef-client -j %q -E %q", 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)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provisioner) deployConfigFiles(
|
||||
o terraform.UIOutput,
|
||||
comm communicator.Communicator,
|
||||
confDir string) error {
|
||||
// Open the validation key file
|
||||
f, err := os.Open(p.ValidationKeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Copy the validation key to the new instance
|
||||
if err := comm.Upload(path.Join(confDir, validationKey), f); err != nil {
|
||||
return fmt.Errorf("Uploading %s failed: %v", validationKey, err)
|
||||
}
|
||||
|
||||
// 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.(map[string]interface{})
|
||||
}
|
||||
|
||||
// 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("[WARNING] 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
|
||||
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
|
||||
}
|
||||
|
||||
// runCommand is used to run already prepared commands
|
||||
func (p *Provisioner) runCommand(
|
||||
o terraform.UIOutput,
|
||||
comm communicator.Communicator,
|
||||
command string) error {
|
||||
var err error
|
||||
|
||||
// Unless prevented, prefix the command with sudo
|
||||
if p.useSudo {
|
||||
command = "sudo " + command
|
||||
}
|
||||
|
||||
outR, outW := io.Pipe()
|
||||
errR, errW := io.Pipe()
|
||||
outDoneCh := make(chan struct{})
|
||||
errDoneCh := make(chan struct{})
|
||||
go p.copyOutput(o, outR, outDoneCh)
|
||||
go p.copyOutput(o, errR, errDoneCh)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
cmd.Wait()
|
||||
if cmd.ExitStatus != 0 {
|
||||
err = fmt.Errorf(
|
||||
"Command %q exited with non-zero exit status: %d", cmd.Command, cmd.ExitStatus)
|
||||
}
|
||||
|
||||
// Wait for output to clean up
|
||||
outW.Close()
|
||||
errW.Close()
|
||||
<-outDoneCh
|
||||
<-errDoneCh
|
||||
|
||||
// If we have an error, return it out now that we've cleaned up
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provisioner) copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) {
|
||||
defer close(doneCh)
|
||||
lr := linereader.New(r)
|
||||
for line := range lr.Ch {
|
||||
o.Output(line)
|
||||
}
|
||||
}
|
136
builtin/provisioners/chef/resource_provisioner_test.go
Normal file
136
builtin/provisioners/chef/resource_provisioner_test.go
Normal file
@ -0,0 +1,136 @@
|
||||
package chef
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/communicator"
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func TestResourceProvisioner_impl(t *testing.T) {
|
||||
var _ terraform.ResourceProvisioner = new(ResourceProvisioner)
|
||||
}
|
||||
|
||||
func TestResourceProvider_Validate_good(t *testing.T) {
|
||||
c := testConfig(t, map[string]interface{}{
|
||||
"attributes": []interface{}{"key1 { subkey1 = value1 }"},
|
||||
"environment": "_default",
|
||||
"node_name": "nodename1",
|
||||
"run_list": []interface{}{"cookbook::recipe"},
|
||||
"server_url": "https://chef.local",
|
||||
"validation_client_name": "validator",
|
||||
"validation_key_path": "validator.pem",
|
||||
})
|
||||
r := new(ResourceProvisioner)
|
||||
warn, errs := r.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",
|
||||
})
|
||||
p := new(ResourceProvisioner)
|
||||
warn, errs := p.Validate(c)
|
||||
if len(warn) > 0 {
|
||||
t.Fatalf("Warnings: %v", warn)
|
||||
}
|
||||
if len(errs) == 0 {
|
||||
t.Fatalf("Should have errors")
|
||||
}
|
||||
}
|
||||
|
||||
func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig {
|
||||
r, err := config.NewRawConfig(c)
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
||||
return terraform.NewResourceConfig(r)
|
||||
}
|
||||
|
||||
func TestResourceProvider_runChefClient(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
Config *terraform.ResourceConfig
|
||||
ConfDir string
|
||||
Commands map[string]bool
|
||||
}{
|
||||
"Sudo": {
|
||||
Config: testConfig(t, map[string]interface{}{
|
||||
"node_name": "nodename1",
|
||||
"run_list": []interface{}{"cookbook::recipe"},
|
||||
"server_url": "https://chef.local",
|
||||
"validation_client_name": "validator",
|
||||
"validation_key_path": "test-fixtures/validator.pem",
|
||||
}),
|
||||
|
||||
ConfDir: linuxConfDir,
|
||||
|
||||
Commands: map[string]bool{
|
||||
`sudo chef-client -j "/etc/chef/first-boot.json" -E "_default"`: true,
|
||||
},
|
||||
},
|
||||
|
||||
"NoSudo": {
|
||||
Config: testConfig(t, map[string]interface{}{
|
||||
"node_name": "nodename1",
|
||||
"prevent_sudo": true,
|
||||
"run_list": []interface{}{"cookbook::recipe"},
|
||||
"server_url": "https://chef.local",
|
||||
"validation_client_name": "validator",
|
||||
"validation_key_path": "test-fixtures/validator.pem",
|
||||
}),
|
||||
|
||||
ConfDir: linuxConfDir,
|
||||
|
||||
Commands: map[string]bool{
|
||||
`chef-client -j "/etc/chef/first-boot.json" -E "_default"`: true,
|
||||
},
|
||||
},
|
||||
|
||||
"Environment": {
|
||||
Config: testConfig(t, map[string]interface{}{
|
||||
"environment": "production",
|
||||
"node_name": "nodename1",
|
||||
"prevent_sudo": true, // Needs to be set for ALL WinRM tests!
|
||||
"run_list": []interface{}{"cookbook::recipe"},
|
||||
"server_url": "https://chef.local",
|
||||
"validation_client_name": "validator",
|
||||
"validation_key_path": "test-fixtures/validator.pem",
|
||||
}),
|
||||
|
||||
ConfDir: windowsConfDir,
|
||||
|
||||
Commands: map[string]bool{
|
||||
`chef-client -j "C:/chef/first-boot.json" -E "production"`: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r := new(ResourceProvisioner)
|
||||
o := new(terraform.MockUIOutput)
|
||||
c := new(communicator.MockCommunicator)
|
||||
|
||||
for k, tc := range cases {
|
||||
c.Commands = tc.Commands
|
||||
|
||||
p, err := r.decodeConfig(tc.Config)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %v", err)
|
||||
}
|
||||
|
||||
p.runChefClient = p.runChefClientFunc(tc.ConfDir)
|
||||
p.useSudo = !p.PreventSudo
|
||||
|
||||
err = p.runChefClient(o, c)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %q failed: %v", k, err)
|
||||
}
|
||||
}
|
||||
}
|
74
builtin/provisioners/chef/ssh_provisioner.go
Normal file
74
builtin/provisioners/chef/ssh_provisioner.go
Normal file
@ -0,0 +1,74 @@
|
||||
package chef
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/communicator"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
const (
|
||||
installURL = "https://www.chef.io/chef/install.sh"
|
||||
)
|
||||
|
||||
func (p *Provisioner) sshInstallChefClient(
|
||||
o terraform.UIOutput,
|
||||
comm communicator.Communicator) error {
|
||||
|
||||
// Build up the command prefix
|
||||
prefix := ""
|
||||
if p.HTTPProxy != "" {
|
||||
prefix += fmt.Sprintf("proxy_http='%s' ", p.HTTPProxy)
|
||||
}
|
||||
if p.NOProxy != nil {
|
||||
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 %s", prefix, p.Version))
|
||||
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) sshCreateConfigFiles(
|
||||
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.deployConfigFiles(o, comm, linuxConfDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// When done copying the 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, "chown -R root.root "+linuxConfDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
290
builtin/provisioners/chef/ssh_provisioner_test.go
Normal file
290
builtin/provisioners/chef/ssh_provisioner_test.go
Normal file
@ -0,0 +1,290 @@
|
||||
package chef
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/communicator"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func TestResourceProvider_sshInstallChefClient(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
Config *terraform.ResourceConfig
|
||||
Commands map[string]bool
|
||||
}{
|
||||
"Sudo": {
|
||||
Config: testConfig(t, map[string]interface{}{
|
||||
"node_name": "nodename1",
|
||||
"run_list": []interface{}{"cookbook::recipe"},
|
||||
"server_url": "https://chef.local",
|
||||
"validation_client_name": "validator",
|
||||
"validation_key_path": "validator.pem",
|
||||
}),
|
||||
|
||||
Commands: map[string]bool{
|
||||
"sudo curl -LO https://www.chef.io/chef/install.sh": true,
|
||||
"sudo bash ./install.sh -v ": true,
|
||||
"sudo rm -f install.sh": true,
|
||||
},
|
||||
},
|
||||
|
||||
"NoSudo": {
|
||||
Config: testConfig(t, map[string]interface{}{
|
||||
"node_name": "nodename1",
|
||||
"prevent_sudo": true,
|
||||
"run_list": []interface{}{"cookbook::recipe"},
|
||||
"server_url": "https://chef.local",
|
||||
"validation_client_name": "validator",
|
||||
"validation_key_path": "validator.pem",
|
||||
}),
|
||||
|
||||
Commands: map[string]bool{
|
||||
"curl -LO https://www.chef.io/chef/install.sh": true,
|
||||
"bash ./install.sh -v ": true,
|
||||
"rm -f install.sh": true,
|
||||
},
|
||||
},
|
||||
|
||||
"HTTPProxy": {
|
||||
Config: testConfig(t, map[string]interface{}{
|
||||
"http_proxy": "http://proxy.local",
|
||||
"node_name": "nodename1",
|
||||
"prevent_sudo": true,
|
||||
"run_list": []interface{}{"cookbook::recipe"},
|
||||
"server_url": "https://chef.local",
|
||||
"validation_client_name": "validator",
|
||||
"validation_key_path": "validator.pem",
|
||||
}),
|
||||
|
||||
Commands: map[string]bool{
|
||||
"proxy_http='http://proxy.local' curl -LO https://www.chef.io/chef/install.sh": true,
|
||||
"proxy_http='http://proxy.local' bash ./install.sh -v ": true,
|
||||
"proxy_http='http://proxy.local' rm -f install.sh": true,
|
||||
},
|
||||
},
|
||||
|
||||
"NOProxy": {
|
||||
Config: testConfig(t, 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",
|
||||
"validation_client_name": "validator",
|
||||
"validation_key_path": "validator.pem",
|
||||
}),
|
||||
|
||||
Commands: map[string]bool{
|
||||
"proxy_http='http://proxy.local' no_proxy='http://local.local,http://local.org' " +
|
||||
"curl -LO https://www.chef.io/chef/install.sh": true,
|
||||
"proxy_http='http://proxy.local' no_proxy='http://local.local,http://local.org' " +
|
||||
"bash ./install.sh -v ": true,
|
||||
"proxy_http='http://proxy.local' no_proxy='http://local.local,http://local.org' " +
|
||||
"rm -f install.sh": true,
|
||||
},
|
||||
},
|
||||
|
||||
"Version": {
|
||||
Config: testConfig(t, map[string]interface{}{
|
||||
"node_name": "nodename1",
|
||||
"prevent_sudo": true,
|
||||
"run_list": []interface{}{"cookbook::recipe"},
|
||||
"server_url": "https://chef.local",
|
||||
"validation_client_name": "validator",
|
||||
"validation_key_path": "validator.pem",
|
||||
"version": "11.18.6",
|
||||
}),
|
||||
|
||||
Commands: map[string]bool{
|
||||
"curl -LO https://www.chef.io/chef/install.sh": true,
|
||||
"bash ./install.sh -v 11.18.6": true,
|
||||
"rm -f install.sh": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r := new(ResourceProvisioner)
|
||||
o := new(terraform.MockUIOutput)
|
||||
c := new(communicator.MockCommunicator)
|
||||
|
||||
for k, tc := range cases {
|
||||
c.Commands = tc.Commands
|
||||
|
||||
p, err := r.decodeConfig(tc.Config)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %v", err)
|
||||
}
|
||||
|
||||
p.useSudo = !p.PreventSudo
|
||||
|
||||
err = p.sshInstallChefClient(o, c)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %q failed: %v", k, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceProvider_sshCreateConfigFiles(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
Config *terraform.ResourceConfig
|
||||
Commands map[string]bool
|
||||
Uploads map[string]string
|
||||
}{
|
||||
"Sudo": {
|
||||
Config: testConfig(t, map[string]interface{}{
|
||||
"node_name": "nodename1",
|
||||
"run_list": []interface{}{"cookbook::recipe"},
|
||||
"server_url": "https://chef.local",
|
||||
"validation_client_name": "validator",
|
||||
"validation_key_path": "test-fixtures/validator.pem",
|
||||
}),
|
||||
|
||||
Commands: map[string]bool{
|
||||
"sudo mkdir -p " + linuxConfDir: true,
|
||||
"sudo chmod 777 " + linuxConfDir: true,
|
||||
"sudo chmod 755 " + linuxConfDir: true,
|
||||
"sudo chown -R root.root " + linuxConfDir: true,
|
||||
},
|
||||
|
||||
Uploads: map[string]string{
|
||||
"/etc/chef/validation.pem": "VALIDATOR-PEM-FILE",
|
||||
"/etc/chef/client.rb": defaultSSHClientConf,
|
||||
"/etc/chef/first-boot.json": `{"run_list":["cookbook::recipe"]}`,
|
||||
},
|
||||
},
|
||||
|
||||
"NoSudo": {
|
||||
Config: testConfig(t, map[string]interface{}{
|
||||
"node_name": "nodename1",
|
||||
"prevent_sudo": true,
|
||||
"run_list": []interface{}{"cookbook::recipe"},
|
||||
"server_url": "https://chef.local",
|
||||
"validation_client_name": "validator",
|
||||
"validation_key_path": "test-fixtures/validator.pem",
|
||||
}),
|
||||
|
||||
Commands: map[string]bool{
|
||||
"mkdir -p " + linuxConfDir: true,
|
||||
},
|
||||
|
||||
Uploads: map[string]string{
|
||||
"/etc/chef/validation.pem": "VALIDATOR-PEM-FILE",
|
||||
"/etc/chef/client.rb": defaultSSHClientConf,
|
||||
"/etc/chef/first-boot.json": `{"run_list":["cookbook::recipe"]}`,
|
||||
},
|
||||
},
|
||||
|
||||
"Proxy": {
|
||||
Config: testConfig(t, 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"},
|
||||
"server_url": "https://chef.local",
|
||||
"validation_client_name": "validator",
|
||||
"validation_key_path": "test-fixtures/validator.pem",
|
||||
}),
|
||||
|
||||
Commands: map[string]bool{
|
||||
"mkdir -p " + linuxConfDir: true,
|
||||
},
|
||||
|
||||
Uploads: map[string]string{
|
||||
"/etc/chef/validation.pem": "VALIDATOR-PEM-FILE",
|
||||
"/etc/chef/client.rb": proxySSHClientConf,
|
||||
"/etc/chef/first-boot.json": `{"run_list":["cookbook::recipe"]}`,
|
||||
},
|
||||
},
|
||||
|
||||
"Attributes": {
|
||||
Config: testConfig(t, map[string]interface{}{
|
||||
"attributes": []map[string]interface{}{
|
||||
map[string]interface{}{
|
||||
"key1": []map[string]interface{}{
|
||||
map[string]interface{}{
|
||||
"subkey1": []map[string]interface{}{
|
||||
map[string]interface{}{
|
||||
"subkey2a": []interface{}{
|
||||
"val1", "val2", "val3",
|
||||
},
|
||||
"subkey2b": []map[string]interface{}{
|
||||
map[string]interface{}{
|
||||
"subkey3": "value3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"key2": "value2",
|
||||
},
|
||||
},
|
||||
"node_name": "nodename1",
|
||||
"prevent_sudo": true,
|
||||
"run_list": []interface{}{"cookbook::recipe"},
|
||||
"server_url": "https://chef.local",
|
||||
"validation_client_name": "validator",
|
||||
"validation_key_path": "test-fixtures/validator.pem",
|
||||
}),
|
||||
|
||||
Commands: map[string]bool{
|
||||
"mkdir -p " + linuxConfDir: true,
|
||||
},
|
||||
|
||||
Uploads: map[string]string{
|
||||
"/etc/chef/validation.pem": "VALIDATOR-PEM-FILE",
|
||||
"/etc/chef/client.rb": defaultSSHClientConf,
|
||||
"/etc/chef/first-boot.json": `{"key1":{"subkey1":{"subkey2a":["val1","val2","val3"],` +
|
||||
`"subkey2b":{"subkey3":"value3"}}},"key2":"value2","run_list":["cookbook::recipe"]}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r := new(ResourceProvisioner)
|
||||
o := new(terraform.MockUIOutput)
|
||||
c := new(communicator.MockCommunicator)
|
||||
|
||||
for k, tc := range cases {
|
||||
c.Commands = tc.Commands
|
||||
c.Uploads = tc.Uploads
|
||||
|
||||
p, err := r.decodeConfig(tc.Config)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %v", err)
|
||||
}
|
||||
|
||||
p.useSudo = !p.PreventSudo
|
||||
|
||||
err = p.sshCreateConfigFiles(o, c)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %q failed: %v", k, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const defaultSSHClientConf = `log_location STDOUT
|
||||
chef_server_url "https://chef.local"
|
||||
validation_client_name "validator"
|
||||
node_name "nodename1"`
|
||||
|
||||
const proxySSHClientConf = `log_location STDOUT
|
||||
chef_server_url "https://chef.local"
|
||||
validation_client_name "validator"
|
||||
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"`
|
1
builtin/provisioners/chef/test-fixtures/validator.pem
Normal file
1
builtin/provisioners/chef/test-fixtures/validator.pem
Normal file
@ -0,0 +1 @@
|
||||
VALIDATOR-PEM-FILE
|
75
builtin/provisioners/chef/winrm_provisioner.go
Normal file
75
builtin/provisioners/chef/winrm_provisioner.go
Normal file
@ -0,0 +1,75 @@
|
||||
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://www.chef.io/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) winrmInstallChefClient(
|
||||
o terraform.UIOutput,
|
||||
comm communicator.Communicator) error {
|
||||
script := path.Join(path.Dir(comm.ScriptPath()), "ChefClient.ps1")
|
||||
content := fmt.Sprintf(installScript, 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) winrmCreateConfigFiles(
|
||||
o terraform.UIOutput,
|
||||
comm communicator.Communicator) error {
|
||||
// Make sure the config directory exists
|
||||
cmd := fmt.Sprintf("if not exist %q mkdir %q", windowsConfDir, windowsConfDir)
|
||||
if err := p.runCommand(o, comm, cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.deployConfigFiles(o, comm, windowsConfDir)
|
||||
}
|
344
builtin/provisioners/chef/winrm_provisioner_test.go
Normal file
344
builtin/provisioners/chef/winrm_provisioner_test.go
Normal file
@ -0,0 +1,344 @@
|
||||
package chef
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/communicator"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func TestResourceProvider_winrmInstallChefClient(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
Config *terraform.ResourceConfig
|
||||
Commands map[string]bool
|
||||
UploadScripts map[string]string
|
||||
}{
|
||||
"Default": {
|
||||
Config: testConfig(t, map[string]interface{}{
|
||||
"node_name": "nodename1",
|
||||
"run_list": []interface{}{"cookbook::recipe"},
|
||||
"server_url": "https://chef.local",
|
||||
"validation_client_name": "validator",
|
||||
"validation_key_path": "validator.pem",
|
||||
}),
|
||||
|
||||
Commands: map[string]bool{
|
||||
"powershell -NoProfile -ExecutionPolicy Bypass -File ChefClient.ps1": true,
|
||||
},
|
||||
|
||||
UploadScripts: map[string]string{
|
||||
"ChefClient.ps1": defaultWinRMInstallScript,
|
||||
},
|
||||
},
|
||||
|
||||
"Proxy": {
|
||||
Config: testConfig(t, 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",
|
||||
"validation_client_name": "validator",
|
||||
"validation_key_path": "validator.pem",
|
||||
}),
|
||||
|
||||
Commands: map[string]bool{
|
||||
"powershell -NoProfile -ExecutionPolicy Bypass -File ChefClient.ps1": true,
|
||||
},
|
||||
|
||||
UploadScripts: map[string]string{
|
||||
"ChefClient.ps1": proxyWinRMInstallScript,
|
||||
},
|
||||
},
|
||||
|
||||
"Version": {
|
||||
Config: testConfig(t, map[string]interface{}{
|
||||
"node_name": "nodename1",
|
||||
"run_list": []interface{}{"cookbook::recipe"},
|
||||
"server_url": "https://chef.local",
|
||||
"validation_client_name": "validator",
|
||||
"validation_key_path": "validator.pem",
|
||||
"version": "11.18.6",
|
||||
}),
|
||||
|
||||
Commands: map[string]bool{
|
||||
"powershell -NoProfile -ExecutionPolicy Bypass -File ChefClient.ps1": true,
|
||||
},
|
||||
|
||||
UploadScripts: map[string]string{
|
||||
"ChefClient.ps1": versionWinRMInstallScript,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r := new(ResourceProvisioner)
|
||||
o := new(terraform.MockUIOutput)
|
||||
c := new(communicator.MockCommunicator)
|
||||
|
||||
for k, tc := range cases {
|
||||
c.Commands = tc.Commands
|
||||
c.UploadScripts = tc.UploadScripts
|
||||
|
||||
p, err := r.decodeConfig(tc.Config)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %v", err)
|
||||
}
|
||||
|
||||
p.useSudo = false
|
||||
|
||||
err = p.winrmInstallChefClient(o, c)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %q failed: %v", k, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceProvider_winrmCreateConfigFiles(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
Config *terraform.ResourceConfig
|
||||
Commands map[string]bool
|
||||
Uploads map[string]string
|
||||
}{
|
||||
"Default": {
|
||||
Config: testConfig(t, map[string]interface{}{
|
||||
"node_name": "nodename1",
|
||||
"run_list": []interface{}{"cookbook::recipe"},
|
||||
"server_url": "https://chef.local",
|
||||
"validation_client_name": "validator",
|
||||
"validation_key_path": "test-fixtures/validator.pem",
|
||||
}),
|
||||
|
||||
Commands: map[string]bool{
|
||||
fmt.Sprintf("if not exist %q mkdir %q", windowsConfDir, windowsConfDir): true,
|
||||
},
|
||||
|
||||
Uploads: map[string]string{
|
||||
"C:/chef/validation.pem": "VALIDATOR-PEM-FILE",
|
||||
"C:/chef/client.rb": defaultWinRMClientConf,
|
||||
"C:/chef/first-boot.json": `{"run_list":["cookbook::recipe"]}`,
|
||||
},
|
||||
},
|
||||
|
||||
"Proxy": {
|
||||
Config: testConfig(t, 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"},
|
||||
"server_url": "https://chef.local",
|
||||
"validation_client_name": "validator",
|
||||
"validation_key_path": "test-fixtures/validator.pem",
|
||||
}),
|
||||
|
||||
Commands: map[string]bool{
|
||||
fmt.Sprintf("if not exist %q mkdir %q", windowsConfDir, windowsConfDir): true,
|
||||
},
|
||||
|
||||
Uploads: map[string]string{
|
||||
"C:/chef/validation.pem": "VALIDATOR-PEM-FILE",
|
||||
"C:/chef/client.rb": proxyWinRMClientConf,
|
||||
"C:/chef/first-boot.json": `{"run_list":["cookbook::recipe"]}`,
|
||||
},
|
||||
},
|
||||
|
||||
"Attributes": {
|
||||
Config: testConfig(t, map[string]interface{}{
|
||||
"attributes": []map[string]interface{}{
|
||||
map[string]interface{}{
|
||||
"key1": []map[string]interface{}{
|
||||
map[string]interface{}{
|
||||
"subkey1": []map[string]interface{}{
|
||||
map[string]interface{}{
|
||||
"subkey2a": []interface{}{
|
||||
"val1", "val2", "val3",
|
||||
},
|
||||
"subkey2b": []map[string]interface{}{
|
||||
map[string]interface{}{
|
||||
"subkey3": "value3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"key2": "value2",
|
||||
},
|
||||
},
|
||||
"node_name": "nodename1",
|
||||
"run_list": []interface{}{"cookbook::recipe"},
|
||||
"server_url": "https://chef.local",
|
||||
"validation_client_name": "validator",
|
||||
"validation_key_path": "test-fixtures/validator.pem",
|
||||
}),
|
||||
|
||||
Commands: map[string]bool{
|
||||
fmt.Sprintf("if not exist %q mkdir %q", windowsConfDir, windowsConfDir): true,
|
||||
},
|
||||
|
||||
Uploads: map[string]string{
|
||||
"C:/chef/validation.pem": "VALIDATOR-PEM-FILE",
|
||||
"C:/chef/client.rb": defaultWinRMClientConf,
|
||||
"C:/chef/first-boot.json": `{"key1":{"subkey1":{"subkey2a":["val1","val2","val3"],` +
|
||||
`"subkey2b":{"subkey3":"value3"}}},"key2":"value2","run_list":["cookbook::recipe"]}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r := new(ResourceProvisioner)
|
||||
o := new(terraform.MockUIOutput)
|
||||
c := new(communicator.MockCommunicator)
|
||||
|
||||
for k, tc := range cases {
|
||||
c.Commands = tc.Commands
|
||||
c.Uploads = tc.Uploads
|
||||
|
||||
p, err := r.decodeConfig(tc.Config)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %v", err)
|
||||
}
|
||||
|
||||
p.useSudo = false
|
||||
|
||||
err = p.winrmCreateConfigFiles(o, c)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %q failed: %v", k, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const defaultWinRMInstallScript = `
|
||||
$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://www.chef.io/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 proxyWinRMInstallScript = `
|
||||
$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://www.chef.io/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 versionWinRMInstallScript = `
|
||||
$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://www.chef.io/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 defaultWinRMClientConf = `log_location STDOUT
|
||||
chef_server_url "https://chef.local"
|
||||
validation_client_name "validator"
|
||||
node_name "nodename1"`
|
||||
|
||||
const proxyWinRMClientConf = `log_location STDOUT
|
||||
chef_server_url "https://chef.local"
|
||||
validation_client_name "validator"
|
||||
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"`
|
91
communicator/communicator_mock.go
Normal file
91
communicator/communicator_mock.go
Normal file
@ -0,0 +1,91 @@
|
||||
package communicator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform/communicator/remote"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
// MockCommunicator is an implementation of Communicator that can be used for tests.
|
||||
type MockCommunicator struct {
|
||||
RemoteScriptPath string
|
||||
Commands map[string]bool
|
||||
Uploads map[string]string
|
||||
UploadScripts map[string]string
|
||||
UploadDirs map[string]string
|
||||
}
|
||||
|
||||
// Connect implementation of communicator.Communicator interface
|
||||
func (c *MockCommunicator) Connect(o terraform.UIOutput) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect implementation of communicator.Communicator interface
|
||||
func (c *MockCommunicator) Disconnect() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Timeout implementation of communicator.Communicator interface
|
||||
func (c *MockCommunicator) Timeout() time.Duration {
|
||||
return time.Duration(5 * time.Second)
|
||||
}
|
||||
|
||||
// ScriptPath implementation of communicator.Communicator interface
|
||||
func (c *MockCommunicator) ScriptPath() string {
|
||||
return c.RemoteScriptPath
|
||||
}
|
||||
|
||||
// Start implementation of communicator.Communicator interface
|
||||
func (c *MockCommunicator) Start(r *remote.Cmd) error {
|
||||
if !c.Commands[r.Command] {
|
||||
return fmt.Errorf("Command not found!")
|
||||
}
|
||||
|
||||
r.SetExited(0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Upload implementation of communicator.Communicator interface
|
||||
func (c *MockCommunicator) Upload(path string, input io.Reader) error {
|
||||
f, ok := c.Uploads[path]
|
||||
if !ok {
|
||||
return fmt.Errorf("Path %q not found!", path)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(input)
|
||||
content := strings.TrimSpace(buf.String())
|
||||
|
||||
f = strings.TrimSpace(f)
|
||||
if f != content {
|
||||
return fmt.Errorf("expected: %q\n\ngot: %q\n", f, content)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UploadScript implementation of communicator.Communicator interface
|
||||
func (c *MockCommunicator) UploadScript(path string, input io.Reader) error {
|
||||
c.Uploads = c.UploadScripts
|
||||
return c.Upload(path, input)
|
||||
}
|
||||
|
||||
// UploadDir implementation of communicator.Communicator interface
|
||||
func (c *MockCommunicator) UploadDir(dst string, src string) error {
|
||||
v, ok := c.UploadDirs[src]
|
||||
if !ok {
|
||||
return fmt.Errorf("Directory not found!")
|
||||
}
|
||||
|
||||
if v != dst {
|
||||
return fmt.Errorf("expected: %q\n\ngot: %q\n", v, dst)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -252,7 +252,7 @@ func (c *Communicator) UploadScript(path string, input io.Reader) error {
|
||||
|
||||
// UploadDir implementation of communicator.Communicator interface
|
||||
func (c *Communicator) UploadDir(dst string, src string) error {
|
||||
log.Printf("Upload dir '%s' to '%s'", src, dst)
|
||||
log.Printf("Uploading dir '%s' to '%s'", src, dst)
|
||||
scpFunc := func(w io.Writer, r *bufio.Reader) error {
|
||||
uploadEntries := func() error {
|
||||
f, err := os.Open(src)
|
||||
|
@ -125,8 +125,6 @@ func (c *Communicator) ScriptPath() string {
|
||||
|
||||
// Start implementation of communicator.Communicator interface
|
||||
func (c *Communicator) Start(rc *remote.Cmd) error {
|
||||
log.Printf("starting remote command: %s", rc.Command)
|
||||
|
||||
err := c.Connect(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -137,6 +135,7 @@ func (c *Communicator) Start(rc *remote.Cmd) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("starting remote command: %s", rc.Command)
|
||||
cmd, err := shell.Execute(rc.Command)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -162,6 +161,7 @@ func (c *Communicator) Upload(path string, input io.Reader) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Uploading file to '%s'", path)
|
||||
return wcp.Write(path, input)
|
||||
}
|
||||
|
||||
@ -172,7 +172,7 @@ func (c *Communicator) UploadScript(path string, input io.Reader) error {
|
||||
|
||||
// UploadDir implementation of communicator.Communicator interface
|
||||
func (c *Communicator) UploadDir(dst string, src string) error {
|
||||
log.Printf("Upload dir '%s' to '%s'", src, dst)
|
||||
log.Printf("Uploading dir '%s' to '%s'", src, dst)
|
||||
wcp, err := c.newCopyClient()
|
||||
if err != nil {
|
||||
return err
|
||||
|
96
website/source/docs/provisioners/chef.html.markdown
Normal file
96
website/source/docs/provisioners/chef.html.markdown
Normal file
@ -0,0 +1,96 @@
|
||||
---
|
||||
layout: "docs"
|
||||
page_title: "Provisioner: chef"
|
||||
sidebar_current: "docs-provisioners-chef"
|
||||
description: |-
|
||||
The `chef` provisioner invokes a Chef Client run on a remote resource after first installing and configuring Chef Client on the remote resource. The `chef` provisioner supports both `ssh` and `winrm` type connections.
|
||||
---
|
||||
|
||||
# Chef Provisioner
|
||||
|
||||
The `chef` provisioner invokes a Chef Client run on a remote resource after first installing
|
||||
and configuring Chef Client on the remote resource. The `chef` provisioner supports both `ssh`
|
||||
and `winrm` type [connections](/docs/provisioners/connection.html).
|
||||
|
||||
## Requirements
|
||||
|
||||
In order for the `chef` provisioner to work properly, you need either `cURL` (when using
|
||||
a `ssh` type connection) or `PowerShell 2.0` (when using a `winrm` type connection) to be
|
||||
available on the target machine.
|
||||
|
||||
## Example usage
|
||||
|
||||
```
|
||||
# Start a initial chef run on a resource
|
||||
resource "aws_instance" "web" {
|
||||
...
|
||||
provisioner "chef" {
|
||||
attributes {
|
||||
"key" = "value"
|
||||
"app" {
|
||||
"cluster1" {
|
||||
"nodes" = ["webserver1", webserver2]
|
||||
}
|
||||
}
|
||||
}
|
||||
environment = "_default"
|
||||
run_list = ["cookbook::recipe"]
|
||||
node_name = "webserver1"
|
||||
server_url = "https://chef.company.com/organizations/org1"
|
||||
validation_client_name = "chef-validator"
|
||||
validation_key_path = "../chef-validator.pem"
|
||||
version = "11.18.6"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Argument Reference
|
||||
|
||||
The following arguments are supported:
|
||||
|
||||
* `attributes (map)` - (Optional) A map with initial node attributes for the new node.
|
||||
See example.
|
||||
|
||||
* `environment (string)` - (Optional) The Chef environment the new node will be joining
|
||||
(defaults `_default`).
|
||||
|
||||
* `log_to_file (boolean)` - (Optional) If true, the output of the initial Chef Client run
|
||||
will be logged to a local file instead of the console. The file will be created in a
|
||||
subdirectory called `logfiles` created in your current directory. The filename will be
|
||||
the `node_name` of the new node.
|
||||
|
||||
* `http_proxy (string)` - (Optional) The proxy server for Chef Client HTTP connections.
|
||||
|
||||
* `https_proxy (string)` - (Optional) The proxy server for Chef Client HTTPS connections.
|
||||
|
||||
* `no_proxy (array)` - (Optional) A list of URLs that should bypass the proxy.
|
||||
|
||||
* `node_name (string)` - (Required) The name of the node to register with the Chef Server.
|
||||
|
||||
* `prevent_sudo (boolean)` - (Optional) Prevent the use of sudo while installing, configuring
|
||||
and running the initial Chef Client run. This option is only used with `ssh` type
|
||||
[connections](/docs/provisioners/connection.html).
|
||||
|
||||
* `run_list (array)` - (Required) A list with recipes that will be invoked during the initial
|
||||
Chef Client run. The run-list will also be saved to the Chef Server after a successful
|
||||
initial run.
|
||||
|
||||
* `server_url (string)` - (Required) The URL to the Chef server. This includes the path to
|
||||
the organization. See the example.
|
||||
|
||||
* `skip_install (boolean)` - (Optional) Skip the installation of Chef Client on the remote
|
||||
machine. This assumes Chef Client is already installed when you run the `chef`
|
||||
provisioner.
|
||||
|
||||
* `ssl_verify_mode (string)` - (Optional) Use to set the verify mode for Chef Client HTTPS
|
||||
requests.
|
||||
|
||||
* `validation_client_name (string)` - (Required) The name of the validation client to use
|
||||
for the initial communication with the Chef Server.
|
||||
|
||||
* `validation_key_path (string)` - (Required) The path to the validation key that is needed
|
||||
by the node to register itself with the Chef Server. The key will be uploaded to the remote
|
||||
machine.
|
||||
|
||||
* `version (string)` - (Optional) The Chef Client version to install on the remote machine.
|
||||
If not set the latest available version will be installed.
|
@ -3,7 +3,7 @@ layout: "docs"
|
||||
page_title: "Provisioner: file"
|
||||
sidebar_current: "docs-provisioners-file"
|
||||
description: |-
|
||||
The `file` provisioner is used to copy files or directories from the machine executing Terraform to the newly created resource. The `file` provisioner only supports `ssh` type connections.
|
||||
The `file` provisioner is used to copy files or directories from the machine executing Terraform to the newly created resource. The `file` provisioner supports both `ssh` and `winrm` type connections.
|
||||
---
|
||||
|
||||
# File Provisioner
|
||||
|
@ -3,7 +3,7 @@ layout: "docs"
|
||||
page_title: "Provisioner: remote-exec"
|
||||
sidebar_current: "docs-provisioners-remote"
|
||||
description: |-
|
||||
The `remote-exec` provisioner invokes a script on a remote resource after it is created. This can be used to run a configuration management tool, bootstrap into a cluster, etc. To invoke a local process, see the `local-exec` provisioner instead. The `remote-exec` provisioner only supports `ssh` type connections.
|
||||
The `remote-exec` provisioner invokes a script on a remote resource after it is created. This can be used to run a configuration management tool, bootstrap into a cluster, etc. To invoke a local process, see the `local-exec` provisioner instead. The `remote-exec` provisioner supports both `ssh` and `winrm` type connections.
|
||||
---
|
||||
|
||||
# remote-exec Provisioner
|
||||
|
@ -178,6 +178,10 @@
|
||||
<li<%= sidebar_current(/^docs-provisioners/) %>>
|
||||
<a href="/docs/provisioners/index.html">Provisioners</a>
|
||||
<ul class="nav">
|
||||
<li<%= sidebar_current("docs-provisioners-chef") %>>
|
||||
<a href="/docs/provisioners/chef.html">chef</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-provisioners-connection") %>>
|
||||
<a href="/docs/provisioners/connection.html">connection</a>
|
||||
</li>
|
||||
|
Loading…
Reference in New Issue
Block a user