Support using WinRM over an IAP tunnel

This avoids the need to expose WinRM ports on the internet and allows
using instances with only an internal private IP address.

When using a WinRM tunnel there is a race condition between the tunnel
connection attempt timing out and packer assuming the connection was
successful. To allow for this, when using WinRM the default success
timeout is increased to 40 seconds.
This commit is contained in:
Chris Chilvers 2020-07-18 23:54:17 +01:00
parent 8964367eb5
commit 37544f4d5f
3 changed files with 104 additions and 15 deletions

View File

@ -426,18 +426,28 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
}
}
if c.IAPConfig.IAPTunnelLaunchWait == 0 {
c.IAPConfig.IAPTunnelLaunchWait = 30
if c.Comm.Type == "winrm" {
// when starting up, WinRM can cause the tunnel to take 30 seconds
// before timing out
c.IAPConfig.IAPTunnelLaunchWait = 40
} else {
c.IAPConfig.IAPTunnelLaunchWait = 30
}
}
// Configure IAP: Update SSH config to use localhost proxy instead
if c.IAPConfig.IAP {
if c.Comm.Type == "ssh" {
c.Comm.SSHHost = "localhost"
} else {
err := fmt.Errorf("Error: IAP tunnel currently only implemnted for" +
" SSH communicator")
if !SupportsIAPTunnel(&c.Comm) {
err := fmt.Errorf("Error: IAP tunnel is not implemented for %s communicator", c.Comm.Type)
errs = packer.MultiErrorAppend(errs, err)
}
// These configuration values are copied early to the generic host parameter when configuring
// StepConnect. As such they must be set now. Ideally we would handle this as part of
// ApplyIAPTunnel and set them during StepStartTunnel but that means defering when the
// CommHost function reads the value from the configuration, perhaps pass in b.config.Comm
// instead of b.config.Comm.Host()?
c.Comm.SSHHost = "localhost"
c.Comm.WinRMHost = "localhost"
}
// Process required parameters.
@ -541,3 +551,25 @@ func (k *CustomerEncryptionKey) ComputeType() *compute.CustomerEncryptionKey {
RawKey: k.RawKey,
}
}
func SupportsIAPTunnel(c *communicator.Config) bool {
switch c.Type {
case "ssh", "winrm":
return true
default:
return false
}
}
func ApplyIAPTunnel(c *communicator.Config, port int) error {
switch c.Type {
case "ssh":
c.SSHPort = port
return nil
case "winrm":
c.WinRMPort = port
return nil
default:
return fmt.Errorf("IAP tunnel is not implemented for %s communicator", c.Type)
}
}

View File

@ -7,6 +7,8 @@ import (
"runtime"
"strings"
"testing"
"github.com/hashicorp/packer/helper/communicator"
)
func TestConfigPrepare(t *testing.T) {
@ -414,9 +416,6 @@ func TestConfigPrepareIAP(t *testing.T) {
t.Fatalf("IAP hashbang didn't default correctly to /bin/sh.")
}
}
if c.Comm.SSHHost != "localhost" {
t.Fatalf("Didn't correctly override the ssh host.")
}
}
func TestConfigPrepareIAP_failures(t *testing.T) {
@ -425,7 +424,7 @@ func TestConfigPrepareIAP_failures(t *testing.T) {
"source_image": "foo",
"winrm_username": "packer",
"zone": "us-central1-a",
"communicator": "winrm",
"communicator": "none",
"iap_hashbang": "/bin/bash",
"iap_ext": ".ps1",
"use_iap": true,
@ -434,7 +433,7 @@ func TestConfigPrepareIAP_failures(t *testing.T) {
var c Config
_, errs := c.Prepare(config)
if errs == nil {
t.Fatalf("Should have errored because we're using winrm.")
t.Fatalf("Should have errored because we're using none.")
}
if c.IAPHashBang != "/bin/bash" {
t.Fatalf("IAP hashbang defaulted even though set.")
@ -500,6 +499,59 @@ func TestRegion(t *testing.T) {
}
}
func TestApplyIAPTunnel_SSH(t *testing.T) {
c := &communicator.Config{
Type: "ssh",
SSH: communicator.SSH{
SSHHost: "example",
SSHPort: 1234,
},
}
err := ApplyIAPTunnel(c, 8447)
if err != nil {
t.Fatalf("Shouldn't have errors")
}
if c.SSHHost != "localhost" {
t.Fatalf("Should have set SSHHost")
}
if c.SSHPort != 8447 {
t.Fatalf("Should have set SSHPort")
}
}
func TestApplyIAPTunnel_WinRM(t *testing.T) {
c := &communicator.Config{
Type: "winrm",
WinRM: communicator.WinRM{
WinRMHost: "example",
WinRMPort: 1234,
},
}
err := ApplyIAPTunnel(c, 8447)
if err != nil {
t.Fatalf("Shouldn't have errors")
}
if c.WinRMHost != "localhost" {
t.Fatalf("Should have set WinRMHost")
}
if c.WinRMPort != 8447 {
t.Fatalf("Should have set WinRMPort")
}
}
func TestApplyIAPTunnel_none(t *testing.T) {
c := &communicator.Config{
Type: "none",
}
err := ApplyIAPTunnel(c, 8447)
if err == nil {
t.Fatalf("Should have errors, none is not supported")
}
}
// Helper stuff below
func testConfig(t *testing.T) (config map[string]interface{}, tempAccountFile string) {

View File

@ -34,8 +34,6 @@ type IAPConfig struct {
// - You must have the gcloud sdk installed on the computer running Packer.
// - You must be using a Service Account with a credentials file (using the
// account_file option in the Packer template)
// - This is currently only implemented for the SSH communicator, not the
// WinRM Communicator.
// - You must add the given service account to project level IAP permissions
// in https://console.cloud.google.com/security/iap. To do so, click
// "project" > "SSH and TCP resoures" > "All Tunnel Resources" >
@ -52,7 +50,7 @@ type IAPConfig struct {
// Default: ".sh"
IAPExt string `mapstructure:"iap_ext" required:"false"`
// How long to wait, in seconds, before assuming a tunnel launch was
// successful. Defaults to 30 seconds.
// successful. Defaults to 30 seconds for SSH or 40 seconds for WinRM.
IAPTunnelLaunchWait int `mapstructure:"iap_tunnel_launch_wait" required:"false"`
}
@ -283,7 +281,14 @@ func (s *StepStartTunnel) Run(ctx context.Context, state multistep.StateBag) mul
// This is the port the IAP tunnel listens on, on localhost.
// TODO make setting LocalHostPort optional
s.CommConf.SSHPort = s.IAPConf.IAPLocalhostPort
err = ApplyIAPTunnel(s.CommConf, s.IAPConf.IAPLocalhostPort)
if err != nil {
// this should not occur as the config should validate that the communicator
// supports using an IAP tunnel
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
log.Printf("Creating tunnel launch script with args %#v", args)
// Create temp file that contains both gcloud authentication, and gcloud