diff --git a/builtin/bins/provisioner-chef/main.go b/builtin/bins/provisioner-chef/main.go new file mode 100644 index 000000000000..a12c65cf7607 --- /dev/null +++ b/builtin/bins/provisioner-chef/main.go @@ -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) + }, + }) +} diff --git a/builtin/bins/provisioner-chef/main_test.go b/builtin/bins/provisioner-chef/main_test.go new file mode 100644 index 000000000000..06ab7d0f9a35 --- /dev/null +++ b/builtin/bins/provisioner-chef/main_test.go @@ -0,0 +1 @@ +package main diff --git a/builtin/provisioners/chef/resource_provisioner.go b/builtin/provisioners/chef/resource_provisioner.go new file mode 100644 index 000000000000..e546588a5ae4 --- /dev/null +++ b/builtin/provisioners/chef/resource_provisioner.go @@ -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) + } +} diff --git a/builtin/provisioners/chef/resource_provisioner_test.go b/builtin/provisioners/chef/resource_provisioner_test.go new file mode 100644 index 000000000000..84a34351398d --- /dev/null +++ b/builtin/provisioners/chef/resource_provisioner_test.go @@ -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) + } + } +} diff --git a/builtin/provisioners/chef/ssh_provisioner.go b/builtin/provisioners/chef/ssh_provisioner.go new file mode 100644 index 000000000000..c6d0fb011420 --- /dev/null +++ b/builtin/provisioners/chef/ssh_provisioner.go @@ -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 +} diff --git a/builtin/provisioners/chef/ssh_provisioner_test.go b/builtin/provisioners/chef/ssh_provisioner_test.go new file mode 100644 index 000000000000..90e7b7d91b16 --- /dev/null +++ b/builtin/provisioners/chef/ssh_provisioner_test.go @@ -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"` diff --git a/builtin/provisioners/chef/test-fixtures/validator.pem b/builtin/provisioners/chef/test-fixtures/validator.pem new file mode 100644 index 000000000000..03e6d6de7668 --- /dev/null +++ b/builtin/provisioners/chef/test-fixtures/validator.pem @@ -0,0 +1 @@ +VALIDATOR-PEM-FILE diff --git a/builtin/provisioners/chef/winrm_provisioner.go b/builtin/provisioners/chef/winrm_provisioner.go new file mode 100644 index 000000000000..0b25c36d00fa --- /dev/null +++ b/builtin/provisioners/chef/winrm_provisioner.go @@ -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) +} diff --git a/builtin/provisioners/chef/winrm_provisioner_test.go b/builtin/provisioners/chef/winrm_provisioner_test.go new file mode 100644 index 000000000000..075e261bd2a9 --- /dev/null +++ b/builtin/provisioners/chef/winrm_provisioner_test.go @@ -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"` diff --git a/communicator/communicator_mock.go b/communicator/communicator_mock.go new file mode 100644 index 000000000000..f1c5ad5e6fdc --- /dev/null +++ b/communicator/communicator_mock.go @@ -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 +} diff --git a/communicator/ssh/communicator.go b/communicator/ssh/communicator.go index ea6aa688701d..7cf03c870a90 100644 --- a/communicator/ssh/communicator.go +++ b/communicator/ssh/communicator.go @@ -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) diff --git a/communicator/winrm/communicator.go b/communicator/winrm/communicator.go index ad1d1d30e820..27ba31a83353 100644 --- a/communicator/winrm/communicator.go +++ b/communicator/winrm/communicator.go @@ -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 diff --git a/website/source/docs/provisioners/chef.html.markdown b/website/source/docs/provisioners/chef.html.markdown new file mode 100644 index 000000000000..fb7bc44163a4 --- /dev/null +++ b/website/source/docs/provisioners/chef.html.markdown @@ -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. diff --git a/website/source/docs/provisioners/file.html.markdown b/website/source/docs/provisioners/file.html.markdown index 70a266c3b8dc..be50ed374d83 100644 --- a/website/source/docs/provisioners/file.html.markdown +++ b/website/source/docs/provisioners/file.html.markdown @@ -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 diff --git a/website/source/docs/provisioners/remote-exec.html.markdown b/website/source/docs/provisioners/remote-exec.html.markdown index 79b7de9c0eb2..d771e558666b 100644 --- a/website/source/docs/provisioners/remote-exec.html.markdown +++ b/website/source/docs/provisioners/remote-exec.html.markdown @@ -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 diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 493dc68161b3..15029bc4cf57 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -178,6 +178,10 @@ > Provisioners