diff --git a/builtin/bins/provisioner-chef-client/main.go b/builtin/bins/provisioner-chef-client/main.go new file mode 100644 index 000000000..ca483ad76 --- /dev/null +++ b/builtin/bins/provisioner-chef-client/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/provisioners/chef-client" + "github.com/hashicorp/terraform/plugin" + "github.com/hashicorp/terraform/terraform" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProvisionerFunc: func() terraform.ResourceProvisioner { + return new(chefclient.ResourceProvisioner) + }, + }) +} diff --git a/builtin/bins/provisioner-chef-client/main_test.go b/builtin/bins/provisioner-chef-client/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/builtin/bins/provisioner-chef-client/main_test.go @@ -0,0 +1 @@ +package main diff --git a/builtin/provisioners/chef-client/resource_provisioner.go b/builtin/provisioners/chef-client/resource_provisioner.go new file mode 100644 index 000000000..e4c410e20 --- /dev/null +++ b/builtin/provisioners/chef-client/resource_provisioner.go @@ -0,0 +1,395 @@ +package chefclient + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "os" + "path" + "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 ( + firstBoot = "first-boot.json" + logfileDir = "logfiles" + linuxConfDir = "/etc/chef" + 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:"-"` + 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 +} + +// 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.PreventSudo = p.PreventSudo || s.Ephemeral.ConnInfo["user"] == "root" + p.installChefClient = p.sshInstallChefClient + p.createConfigFiles = p.sshCreateConfigFiles + p.runChefClient = p.runChefClientFunc(linuxConfDir) + case "winrm": + p.PreventSudo = true + p.installChefClient = p.winrmInstallChefClient + p.createConfigFiles = p.winrmCreateConfigFiles + p.runChefClient = p.runChefClientFunc(windowsConfDir) + 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{ + 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 = "_default" + } + + 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, 0777); 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() + + 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 .pem file + f, err := os.Open(p.ValidationKeyPath) + if err != nil { + return err + } + defer f.Close() + + // Copy the validation .pem to the new instance + if err := comm.Upload(path.Join(confDir, "validation.pem"), f); err != nil { + return fmt.Errorf("Uploading validation.pem failed: %v", err) + } + + // Make strings.Join available for use within the template + funcMap := template.FuncMap{ + "join": strings.Join, + } + + // Create a new template and parse the client.rb into it + t := template.Must(template.New("client.rb").Funcs(funcMap).Parse(clientConf)) + + var buf bytes.Buffer + err = t.Execute(&buf, p) + if err != nil { + return fmt.Errorf("Error executing client.rb template: %s", err) + } + + // Copy the client.rb to the new instance + if err := comm.Upload(path.Join(confDir, "client.rb"), &buf); err != nil { + return fmt.Errorf("Uploading client.rb failed: %v", err) + } + + // Create a map with first boot settings + fb := make(map[string]interface{}) + if p.Attributes != nil { + fb = p.Attributes.(map[string]interface{}) + } + + // 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 first-boot.json failed: %v", 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.PreventSudo { + 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-client/resource_provisioner_test.go b/builtin/provisioners/chef-client/resource_provisioner_test.go new file mode 100644 index 000000000..7c798c7e3 --- /dev/null +++ b/builtin/provisioners/chef-client/resource_provisioner_test.go @@ -0,0 +1,56 @@ +package chefclient + +import ( + "testing" + + "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{}{ + "package": "https://opscode-omnibus-packages.s3.amazonaws.com/el/6/x86_64/chef-11.18.6-1.el6.x86_64.rpm", + "run_list": []interface{}{"cookbook::recipe"}, + "node_name": "nodename1", + "environment": "_default", + "server_url": "https://chef.local", + "validation_client_name": "validator", + "validation_key_path": "validator.pem", + "attributes": []interface{}{"key1 { subkey1 = value1 }"}, + }) + p := new(ResourceProvisioner) + warn, errs := p.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{}{ + "package": "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) +} diff --git a/builtin/provisioners/chef-client/ssh_provisioner.go b/builtin/provisioners/chef-client/ssh_provisioner.go new file mode 100644 index 000000000..2f95f7cc4 --- /dev/null +++ b/builtin/provisioners/chef-client/ssh_provisioner.go @@ -0,0 +1,70 @@ +package chefclient + +import ( + "bytes" + "fmt" + "strings" + + "github.com/hashicorp/terraform/communicator" + "github.com/hashicorp/terraform/terraform" +) + +func (p *Provisioner) sshInstallChefClient( + o terraform.UIOutput, + comm communicator.Communicator) error { + var installCmd bytes.Buffer + + // Build up a single command based on the given config options + installCmd.WriteString("curl") + if p.HTTPProxy != "" { + installCmd.WriteString(" --proxy " + p.HTTPProxy) + } + if p.NOProxy != nil { + installCmd.WriteString(" --noproxy " + strings.Join(p.NOProxy, ",")) + } + installCmd.WriteString(" -LO https://www.chef.io/chef/install.sh 2>/dev/null &&") + if !p.PreventSudo { + installCmd.WriteString(" sudo") + } + installCmd.WriteString(" bash ./install.sh") + if p.Version != "" { + installCmd.WriteString(" -v " + p.Version) + } + installCmd.WriteString(" && rm -f install.sh") + + // Execute the command to install Chef Client + return p.runCommand(o, comm, installCmd.String()) +} + +func (p *Provisioner) sshCreateConfigFiles( + o terraform.UIOutput, + comm communicator.Communicator) error { + // Make sure the config directory exists + cmd := fmt.Sprintf("mkdir -p %q", linuxConfDir) + if err := p.runCommand(o, comm, cmd); err != nil { + return err + } + + // Make sure we have enough rights to upload the files if using sudo + if !p.PreventSudo { + 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.PreventSudo { + 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-client/ssh_provisioner_test.go b/builtin/provisioners/chef-client/ssh_provisioner_test.go new file mode 100644 index 000000000..9378abfe0 --- /dev/null +++ b/builtin/provisioners/chef-client/ssh_provisioner_test.go @@ -0,0 +1 @@ +package chefclient diff --git a/builtin/provisioners/chef-client/winrm_provisioner.go b/builtin/provisioners/chef-client/winrm_provisioner.go new file mode 100644 index 000000000..6b4edcf5f --- /dev/null +++ b/builtin/provisioners/chef-client/winrm_provisioner.go @@ -0,0 +1,75 @@ +package chefclient + +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-client/winrm_provisioner_test.go b/builtin/provisioners/chef-client/winrm_provisioner_test.go new file mode 100644 index 000000000..9378abfe0 --- /dev/null +++ b/builtin/provisioners/chef-client/winrm_provisioner_test.go @@ -0,0 +1 @@ +package chefclient diff --git a/website/source/docs/provisioners/chef-client.html.markdown b/website/source/docs/provisioners/chef-client.html.markdown new file mode 100644 index 000000000..758b8c31c --- /dev/null +++ b/website/source/docs/provisioners/chef-client.html.markdown @@ -0,0 +1,90 @@ +--- +layout: "docs" +page_title: "Provisioner: chef-client" +sidebar_current: "docs-provisioners-chef-client" +description: |- + The `chef-client` provisioner invokes a Chef Client run on a remote resource after first installing and configuring Chef Client on the remote resource. The `chef-client` provisioner supports both `ssh` and `winrm` type connections. +--- + +# chef Provisioner + +The `chef-client` provisioner invokes a Chef Client run on a remote resource after first +installing and configuring Chef Client on the remote resource. The `chef-client` provisioner +supports both `ssh` and `winrm` type [connections](/docs/provisioners/connection.html). + +## Example usage + +``` +# Start a initial chef run on a resource +resource "aws_instance" "web" { + ... + provisioner "chef-client" { + 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 (hash)` - (Optional) A hash 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-client` + 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 70a266c3b..be50ed374 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 79b7de9c0..d771e5586 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 493dc6816..7ff775168 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -178,6 +178,10 @@