From 5b99a56fde893b6d029d7f1d2a780dabed30388c Mon Sep 17 00:00:00 2001 From: hhofs <35528009+hhofs@users.noreply.github.com> Date: Thu, 12 Nov 2020 16:00:48 +0100 Subject: [PATCH] communicator/ssh: Add support for Windows targets (#26865) --- communicator/ssh/communicator.go | 46 ++++++++-------- communicator/ssh/provisioner.go | 52 +++++++++++++------ communicator/ssh/provisioner_test.go | 5 +- terraform/eval_validate.go | 5 +- .../provisioners/connection.html.markdown | 5 +- 5 files changed, 72 insertions(+), 41 deletions(-) diff --git a/communicator/ssh/communicator.go b/communicator/ssh/communicator.go index f39d70898..2427194bd 100644 --- a/communicator/ssh/communicator.go +++ b/communicator/ssh/communicator.go @@ -139,13 +139,15 @@ func (c *Communicator) Connect(o terraform.UIOutput) (err error) { " Private key: %t\n"+ " Certificate: %t\n"+ " SSH Agent: %t\n"+ - " Checking Host Key: %t", + " Checking Host Key: %t\n"+ + " Target Platform: %s\n", c.connInfo.Host, c.connInfo.User, c.connInfo.Password != "", c.connInfo.PrivateKey != "", c.connInfo.Certificate != "", c.connInfo.Agent, c.connInfo.HostKey != "", + c.connInfo.TargetPlatform, )) if c.connInfo.BastionHost != "" { @@ -343,7 +345,7 @@ func (c *Communicator) Start(cmd *remote.Cmd) error { session.Stdout = cmd.Stdout session.Stderr = cmd.Stderr - if !c.config.noPty { + if !c.config.noPty && c.connInfo.TargetPlatform != TargetPlatformWindows { // Request a PTY termModes := ssh.TerminalModes{ ssh.ECHO: 0, // do not echo @@ -425,35 +427,35 @@ func (c *Communicator) UploadScript(path string, input io.Reader) error { if err != nil { return fmt.Errorf("Error reading script: %s", err) } - var script bytes.Buffer - if string(prefix) != "#!" { + + if string(prefix) != "#!" && c.connInfo.TargetPlatform != TargetPlatformWindows { script.WriteString(DefaultShebang) } - script.ReadFrom(reader) + if err := c.Upload(path, &script); err != nil { return err } + if c.connInfo.TargetPlatform != TargetPlatformWindows { + var stdout, stderr bytes.Buffer + cmd := &remote.Cmd{ + Command: fmt.Sprintf("chmod 0777 %s", path), + Stdout: &stdout, + Stderr: &stderr, + } + if err := c.Start(cmd); err != nil { + return fmt.Errorf( + "Error chmodding script file to 0777 in remote "+ + "machine: %s", err) + } - var stdout, stderr bytes.Buffer - cmd := &remote.Cmd{ - Command: fmt.Sprintf("chmod 0777 %s", path), - Stdout: &stdout, - Stderr: &stderr, + if err := cmd.Wait(); err != nil { + return fmt.Errorf( + "Error chmodding script file to 0777 in remote "+ + "machine %v: %s %s", err, stdout.String(), stderr.String()) + } } - if err := c.Start(cmd); err != nil { - return fmt.Errorf( - "Error chmodding script file to 0777 in remote "+ - "machine: %s", err) - } - - if err := cmd.Wait(); err != nil { - return fmt.Errorf( - "Error chmodding script file to 0777 in remote "+ - "machine %v: %s %s", err, stdout.String(), stderr.String()) - } - return nil } diff --git a/communicator/ssh/provisioner.go b/communicator/ssh/provisioner.go index b6fe80a4a..63a753c1a 100644 --- a/communicator/ssh/provisioner.go +++ b/communicator/ssh/provisioner.go @@ -29,29 +29,38 @@ const ( // DefaultPort is used if there is no port given DefaultPort = 22 - // DefaultScriptPath is used as the path to copy the file to - // for remote execution if not provided otherwise. - DefaultScriptPath = "/tmp/terraform_%RAND%.sh" + // DefaultUnixScriptPath is used as the path to copy the file to + // for remote execution on unix if not provided otherwise. + DefaultUnixScriptPath = "/tmp/terraform_%RAND%.sh" + // DefaultWindowsScriptPath is used as the path to copy the file to + // for remote execution on windows if not provided otherwise. + DefaultWindowsScriptPath = "C:/windows/temp/terraform_%RAND%.cmd" // DefaultTimeout is used if there is no timeout given DefaultTimeout = 5 * time.Minute + + // TargetPlatformUnix used for cleaner code, and is used if no target platform has been specified + TargetPlatformUnix = "unix" + //TargetPlatformWindows used for cleaner code + TargetPlatformWindows = "windows" ) // connectionInfo is decoded from the ConnInfo of the resource. These are the // only keys we look at. If a PrivateKey is given, that is used instead // of a password. type connectionInfo struct { - User string - Password string - PrivateKey string `mapstructure:"private_key"` - Certificate string `mapstructure:"certificate"` - Host string - HostKey string `mapstructure:"host_key"` - Port int - Agent bool - Timeout string - ScriptPath string `mapstructure:"script_path"` - TimeoutVal time.Duration `mapstructure:"-"` + User string + Password string + PrivateKey string `mapstructure:"private_key"` + Certificate string `mapstructure:"certificate"` + Host string + HostKey string `mapstructure:"host_key"` + Port int + Agent bool + Timeout string + ScriptPath string `mapstructure:"script_path"` + TimeoutVal time.Duration `mapstructure:"-"` + TargetPlatform string `mapstructure:"target_platform"` BastionUser string `mapstructure:"bastion_user"` BastionPassword string `mapstructure:"bastion_password"` @@ -106,8 +115,19 @@ func parseConnectionInfo(s *terraform.InstanceState) (*connectionInfo, error) { if connInfo.Port == 0 { connInfo.Port = DefaultPort } - if connInfo.ScriptPath == "" { - connInfo.ScriptPath = DefaultScriptPath + // Set default targetPlatform to unix if it's empty + if connInfo.TargetPlatform == "" { + connInfo.TargetPlatform = TargetPlatformUnix + } else if connInfo.TargetPlatform != TargetPlatformUnix && connInfo.TargetPlatform != TargetPlatformWindows { + return nil, fmt.Errorf("target_platform for provisioner has to be either %s or %s", TargetPlatformUnix, TargetPlatformWindows) + } + // Choose an appropriate default script path based on the target platform. There is no single + // suitable default script path which works on both UNIX and Windows targets. + if connInfo.ScriptPath == "" && connInfo.TargetPlatform == TargetPlatformUnix { + connInfo.ScriptPath = DefaultUnixScriptPath + } + if connInfo.ScriptPath == "" && connInfo.TargetPlatform == TargetPlatformWindows { + connInfo.ScriptPath = DefaultWindowsScriptPath } if connInfo.Timeout != "" { connInfo.TimeoutVal = safeDuration(connInfo.Timeout, DefaultTimeout) diff --git a/communicator/ssh/provisioner_test.go b/communicator/ssh/provisioner_test.go index f8e0f77d8..959831544 100644 --- a/communicator/ssh/provisioner_test.go +++ b/communicator/ssh/provisioner_test.go @@ -50,7 +50,10 @@ func TestProvisioner_connInfo(t *testing.T) { if conf.Timeout != "30s" { t.Fatalf("bad: %v", conf) } - if conf.ScriptPath != DefaultScriptPath { + if conf.ScriptPath != DefaultUnixScriptPath { + t.Fatalf("bad: %v", conf) + } + if conf.TargetPlatform != TargetPlatformUnix { t.Fatalf("bad: %v", conf) } if conf.BastionHost != "127.0.1.1" { diff --git a/terraform/eval_validate.go b/terraform/eval_validate.go index a84aa3ac9..8f1fd7e08 100644 --- a/terraform/eval_validate.go +++ b/terraform/eval_validate.go @@ -161,8 +161,11 @@ var connectionBlockSupersetSchema = &configschema.Block{ Type: cty.String, Optional: true, }, - // For type=ssh only (enforced in ssh communicator) + "target_platform": { + Type: cty.String, + Optional: true, + }, "private_key": { Type: cty.String, Optional: true, diff --git a/website/docs/provisioners/connection.html.markdown b/website/docs/provisioners/connection.html.markdown index ffa771ed7..52005876c 100644 --- a/website/docs/provisioners/connection.html.markdown +++ b/website/docs/provisioners/connection.html.markdown @@ -123,6 +123,9 @@ block would create a dependency cycle. * `host_key` - The public key from the remote host or the signing CA, used to verify the connection. +* `target_platform` - The target platform to connect to. Valid values are `windows` and `unix`. Defaults to `unix` if not set. + + If the platform is set to `windows`, the default `script_path` is `c:\windows\temp\terraform_%RAND%.cmd`, assuming [the SSH default shell](https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_server_configuration#configuring-the-default-shell-for-openssh-in-windows) is `cmd.exe`. If the SSH default shell is PowerShell, set `script_path` to `"c:/windows/temp/terraform_%RAND%.ps1"` **Additional arguments only supported by the `winrm` connection type:** @@ -163,4 +166,4 @@ The `ssh` connection also supports the following fields to facilitate connnectio * `bastion_certificate` - The contents of a signed CA Certificate. The certificate argument must be used in conjunction with a `bastion_private_key`. These can be loaded from - a file on disk using the [the `file` function](/docs/configuration/functions/file.html). \ No newline at end of file + a file on disk using the [the `file` function](/docs/configuration/functions/file.html).