Merge pull request #9610 from chilversc/winrm-via-iap-tunnel

Support using WinRM over an IAP tunnel
This commit is contained in:
Megan Marsh 2020-07-28 14:27:43 -07:00 committed by GitHub
commit b40490c3c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 143 additions and 33 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) {
@ -383,7 +385,7 @@ func TestConfigPrepareStartupScriptFile(t *testing.T) {
}
}
func TestConfigPrepareIAP(t *testing.T) {
func TestConfigPrepareIAP_SSH(t *testing.T) {
config := map[string]interface{}{
"project_id": "project",
"source_image": "foo",
@ -398,25 +400,33 @@ func TestConfigPrepareIAP(t *testing.T) {
if err != nil {
t.Fatalf("Shouldn't have errors. Err = %s", err)
}
if runtime.GOOS == "windows" {
if c.IAPExt != ".cmd" {
t.Fatalf("IAP tempfile extension didn't default correctly to .cmd")
}
if c.IAPHashBang != "" {
t.Fatalf("IAP hashbang didn't default correctly to nothing.")
}
} else {
if c.IAPExt != "" {
t.Fatalf("IAP tempfile extension should default to empty on unix mahcines")
}
if c.IAPHashBang != "/bin/sh" {
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.")
t.Fatalf("Should have set SSHHost")
}
testIAPScript(t, &c)
}
func TestConfigPrepareIAP_WinRM(t *testing.T) {
config := map[string]interface{}{
"project_id": "project",
"source_image": "foo",
"winrm_username": "packer",
"zone": "us-central1-a",
"communicator": "winrm",
"use_iap": true,
}
var c Config
_, err := c.Prepare(config)
if err != nil {
t.Fatalf("Shouldn't have errors. Err = %s", err)
}
if c.Comm.WinRMHost != "localhost" {
t.Fatalf("Should have set WinRMHost")
}
testIAPScript(t, &c)
}
func TestConfigPrepareIAP_failures(t *testing.T) {
@ -425,7 +435,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 +444,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 +510,53 @@ 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.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.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) {
@ -576,6 +633,24 @@ func testAccountFile(t *testing.T) string {
return tf.Name()
}
func testIAPScript(t *testing.T, c *Config) {
if runtime.GOOS == "windows" {
if c.IAPExt != ".cmd" {
t.Fatalf("IAP tempfile extension didn't default correctly to .cmd")
}
if c.IAPHashBang != "" {
t.Fatalf("IAP hashbang didn't default correctly to nothing.")
}
} else {
if c.IAPExt != "" {
t.Fatalf("IAP tempfile extension should default to empty on unix mahcines")
}
if c.IAPHashBang != "/bin/sh" {
t.Fatalf("IAP hashbang didn't default correctly to /bin/sh.")
}
}
}
const testMetadataFileContent = `testMetadata`
func testMetadataFile(t *testing.T) 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

View File

@ -6,8 +6,6 @@
- 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" >
@ -24,4 +22,4 @@
Default: ".sh"
- `iap_tunnel_launch_wait` (int) - 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.