Merge pull request #9105 from hashicorp/google_iap

Implement iap proxy for googlecompute
This commit is contained in:
Megan Marsh 2020-05-08 12:40:42 -07:00 committed by GitHub
commit 9476aa03de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 735 additions and 0 deletions

View File

@ -66,6 +66,12 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
&StepInstanceInfo{
Debug: b.config.PackerDebug,
},
&StepStartTunnel{
IAPConf: &b.config.IAPConfig,
CommConf: &b.config.Comm,
AccountFile: b.config.AccountFile,
ProjectId: b.config.ProjectId,
},
&communicator.StepConnect{
Config: &b.config.Comm,
Host: communicator.CommHost(b.config.Comm.Host(), "instance_ip"),

View File

@ -8,6 +8,7 @@ import (
"fmt"
"os"
"regexp"
"runtime"
"time"
"github.com/hashicorp/packer/common"
@ -72,6 +73,8 @@ type Config struct {
// state of your VM instances. Note: integrity monitoring relies on having
// vTPM enabled. [Details](https://cloud.google.com/security/shielded-cloud/shielded-vm)
EnableIntegrityMonitoring bool `mapstructure:"enable_integrity_monitoring" required:"false"`
// Whether to use an IAP proxy.
IAPConfig `mapstructure:",squash"`
// The unique name of the resulting image. Defaults to
// `packer-{{timestamp}}`.
ImageName string `mapstructure:"image_name" required:"false"`
@ -320,10 +323,36 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
c.StateTimeout = 5 * time.Minute
}
// Set up communicator
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
errs = packer.MultiErrorAppend(errs, es...)
}
// set defaults for IAP
if c.IAPConfig.IAPHashBang == "" {
if runtime.GOOS == "windows" {
c.IAPConfig.IAPHashBang = ""
} else {
c.IAPConfig.IAPHashBang = "/bin/sh"
}
}
if c.IAPConfig.IAPExt == "" {
if runtime.GOOS == "windows" {
c.IAPConfig.IAPExt = ".cmd"
}
}
// 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")
errs = packer.MultiErrorAppend(errs, err)
}
}
// Process required parameters.
if c.ProjectId == "" {
errs = packer.MultiErrorAppend(

View File

@ -70,6 +70,10 @@ type FlatConfig struct {
EnableSecureBoot *bool `mapstructure:"enable_secure_boot" required:"false" cty:"enable_secure_boot"`
EnableVtpm *bool `mapstructure:"enable_vtpm" required:"false" cty:"enable_vtpm"`
EnableIntegrityMonitoring *bool `mapstructure:"enable_integrity_monitoring" required:"false" cty:"enable_integrity_monitoring"`
IAP *bool `mapstructure:"use_iap" required:"false" cty:"use_iap"`
IAPLocalhostPort *int `mapstructure:"iap_localhost_port" cty:"iap_localhost_port"`
IAPHashBang *string `mapstructure:"iap_hashbang" required:"false" cty:"iap_hashbang"`
IAPExt *string `mapstructure:"iap_ext" required:"false" cty:"iap_ext"`
ImageName *string `mapstructure:"image_name" required:"false" cty:"image_name"`
ImageDescription *string `mapstructure:"image_description" required:"false" cty:"image_description"`
ImageEncryptionKey *FlatCustomerEncryptionKey `mapstructure:"image_encryption_key" required:"false" cty:"image_encryption_key"`
@ -175,6 +179,10 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"enable_secure_boot": &hcldec.AttrSpec{Name: "enable_secure_boot", Type: cty.Bool, Required: false},
"enable_vtpm": &hcldec.AttrSpec{Name: "enable_vtpm", Type: cty.Bool, Required: false},
"enable_integrity_monitoring": &hcldec.AttrSpec{Name: "enable_integrity_monitoring", Type: cty.Bool, Required: false},
"use_iap": &hcldec.AttrSpec{Name: "use_iap", Type: cty.Bool, Required: false},
"iap_localhost_port": &hcldec.AttrSpec{Name: "iap_localhost_port", Type: cty.Number, Required: false},
"iap_hashbang": &hcldec.AttrSpec{Name: "iap_hashbang", Type: cty.String, Required: false},
"iap_ext": &hcldec.AttrSpec{Name: "iap_ext", Type: cty.String, Required: false},
"image_name": &hcldec.AttrSpec{Name: "image_name", Type: cty.String, Required: false},
"image_description": &hcldec.AttrSpec{Name: "image_description", Type: cty.String, Required: false},
"image_encryption_key": &hcldec.BlockSpec{TypeName: "image_encryption_key", Nested: hcldec.ObjectSpec((*FlatCustomerEncryptionKey)(nil).HCL2Spec())},

View File

@ -4,6 +4,7 @@ import (
"fmt"
"io/ioutil"
"os"
"runtime"
"strings"
"testing"
)
@ -382,6 +383,67 @@ func TestConfigPrepareStartupScriptFile(t *testing.T) {
}
}
func TestConfigPrepareIAP(t *testing.T) {
config := map[string]interface{}{
"project_id": "project",
"source_image": "foo",
"ssh_username": "packer",
"zone": "us-central1-a",
"communicator": "ssh",
"use_iap": true,
}
var c Config
_, err := c.Prepare(config)
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.")
}
}
func TestConfigPrepareIAP_failures(t *testing.T) {
config := map[string]interface{}{
"project_id": "project",
"source_image": "foo",
"winrm_username": "packer",
"zone": "us-central1-a",
"communicator": "winrm",
"iap_hashbang": "/bin/bash",
"iap_ext": ".ps1",
"use_iap": true,
}
var c Config
_, errs := c.Prepare(config)
if errs == nil {
t.Fatalf("Should have errored because we're using winrm.")
}
if c.IAPHashBang != "/bin/bash" {
t.Fatalf("IAP hashbang defaulted even though set.")
}
if c.IAPExt != ".ps1" {
t.Fatalf("IAP tempfile defaulted even though set.")
}
}
func TestConfigDefaults(t *testing.T) {
cases := []struct {
Read func(c *Config) interface{}

View File

@ -0,0 +1,331 @@
//go:generate struct-markdown
//go:generate mapstructure-to-hcl2 -type IAPConfig
package googlecompute
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"log"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"text/template"
"time"
"github.com/hashicorp/packer/common/net"
"github.com/hashicorp/packer/common/retry"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/packer/tmp"
)
// StepStartTunnel represents a Packer build step that launches an IAP tunnel
type IAPConfig struct {
// Whether to use an IAP proxy.
// Prerequisites and limitations for using IAP:
// - You must manually enable the IAP API in the Google Cloud console.
// - 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" >
// "Add Member". Then add your service account and choose the role
// "IAP-secured Tunnel User" and add any conditions you may care about.
IAP bool `mapstructure:"use_iap" required:"false"`
// Which port to connect the local end of the IAM localhost proxy to. If
// left blank, Packer will choose a port for you from available ports.
IAPLocalhostPort int `mapstructure:"iap_localhost_port"`
// What "hashbang" to use to invoke script that sets up gcloud.
// Default: "/bin/sh"
IAPHashBang string `mapstructure:"iap_hashbang" required:"false"`
// What file extension to use for script that sets up gcloud.
// Default: ".sh"
IAPExt string `mapstructure:"iap_ext" required:"false"`
}
type TunnelDriver interface {
StartTunnel(context.Context, string) error
StopTunnel()
}
func RunTunnelCommand(cmd *exec.Cmd) error {
// set stdout and stderr so we can read what's going on.
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Start()
if err != nil {
err := fmt.Errorf("Error calling gcloud sdk to launch IAP tunnel: %s",
err)
return err
}
// Give tunnel 30 seconds to either launch, or return an error.
// Unfortunately, the SDK doesn't provide any official acknowledgment that
// the tunnel is launched when it's not being run through a TTY so we
// are just trusting here that 30s is enough to know whether the tunnel
// launch was going to fail. Yep, feels icky to me too. But I spent an
// afternoon trying to figure out how to get the SDK to actually send
// the "Listening on port [n]" line I see when I run it manually, and I
// can't justify spending more time than that on aesthetics.
for i := 0; i < 30; i++ {
time.Sleep(1 * time.Second)
lineStderr, err := stderr.ReadString('\n')
if err != nil && err != io.EOF {
log.Printf("Err from scanning stderr is %s", err)
return fmt.Errorf("Error reading stderr from tunnel launch: %s", err)
}
if lineStderr != "" {
log.Printf("stderr: %s", lineStderr)
}
lineStdout, err := stdout.ReadString('\n')
if err != nil && err != io.EOF {
log.Printf("Err from scanning stdout is %s", err)
return fmt.Errorf("Error reading stdout from tunnel launch: %s", err)
}
if lineStdout != "" {
log.Printf("stdout: %s", lineStdout)
}
if strings.Contains(lineStderr, "ERROR") {
// 4033: Either you don't have permission to access the instance,
// the instance doesn't exist, or the instance is stopped.
// The two sub-errors we may see while the permissions settle are
// "not authorized" and "failed to connect to backend," but after
// about a minute of retries this goes away and we're able to
// connect.
// 4003: "failed to connect to backend". Network blip.
if strings.Contains(lineStderr, "4033") || strings.Contains(lineStderr, "4003") {
return RetryableTunnelError{lineStderr}
} else {
log.Printf("NOT RETRYABLE: %s", lineStderr)
return fmt.Errorf("Non-retryable tunnel error: %s", lineStderr)
}
}
}
log.Printf("No error detected after tunnel launch; continuing...")
return nil
}
type RetryableTunnelError struct {
s string
}
func (e RetryableTunnelError) Error() string {
return "Tunnel start: " + e.s
}
type StepStartTunnel struct {
IAPConf *IAPConfig
CommConf *communicator.Config
AccountFile string
ProjectId string
tunnelDriver TunnelDriver
}
func (s *StepStartTunnel) ConfigureLocalHostPort(ctx context.Context) error {
minPortNumber, maxPortNumber := 8000, 9000
if s.IAPConf.IAPLocalhostPort != 0 {
minPortNumber = s.IAPConf.IAPLocalhostPort
maxPortNumber = minPortNumber
log.Printf("Using TCP port for %d IAP proxy", s.IAPConf.IAPLocalhostPort)
} else {
log.Printf("Finding an available TCP port for IAP proxy")
}
l, err := net.ListenRangeConfig{
Min: minPortNumber,
Max: maxPortNumber,
Addr: "0.0.0.0",
Network: "tcp",
}.Listen(ctx)
if err != nil {
err := fmt.Errorf("error finding an available port to initiate a session tunnel: %s", err)
return err
}
s.IAPConf.IAPLocalhostPort = l.Port
l.Close()
log.Printf("Setting up proxy to listen on localhost at %d",
s.IAPConf.IAPLocalhostPort)
return nil
}
func (s *StepStartTunnel) createTempGcloudScript(args []string) (string, error) {
// Generate temp script that contains both gcloud auth and gcloud compute
// iap launch call.
// Create temp file.
tf, err := tmp.File("gcloud-setup")
if err != nil {
return "", fmt.Errorf("Error preparing gcloud setup script: %s", err)
}
defer tf.Close()
// Write our contents to it
writer := bufio.NewWriter(tf)
if s.IAPConf.IAPHashBang != "" {
s.IAPConf.IAPHashBang = fmt.Sprintf("#!%s\n", s.IAPConf.IAPHashBang)
log.Printf("[INFO] (google): Prepending inline gcloud setup script with %s",
s.IAPConf.IAPHashBang)
_, err = writer.WriteString(s.IAPConf.IAPHashBang)
if err != nil {
return "", fmt.Errorf("Error preparing inline hashbang: %s", err)
}
}
launchTemplate := `
gcloud auth activate-service-account --key-file='{{.AccountFile}}'
gcloud config set project {{.ProjectID}}
{{.Args}}
`
if runtime.GOOS == "windows" {
launchTemplate = `
call gcloud auth activate-service-account --key-file "{{.AccountFile}}"
call gcloud config set project {{.ProjectID}}
call {{.Args}}
`
}
// call command
args = append([]string{"gcloud"}, args...)
argString := strings.Join(args, " ")
var tpl = template.Must(template.New("createTunnel").Parse(launchTemplate))
var b bytes.Buffer
opts := map[string]string{
"AccountFile": s.AccountFile,
"ProjectID": s.ProjectId,
"Args": argString,
}
err = tpl.Execute(&b, opts)
if err != nil {
fmt.Println(err)
}
if _, err := writer.WriteString(b.String()); err != nil {
return "", fmt.Errorf("Error preparing gcloud shell script: %s", err)
}
if err := writer.Flush(); err != nil {
return "", fmt.Errorf("Error preparing shell script: %s", err)
}
// Have to close temp file before renaming it or Windows will complain.
tf.Close()
err = os.Chmod(tf.Name(), 0700)
if err != nil {
log.Printf("[ERROR] (google): error modifying permissions of temp script file: %s", err.Error())
}
// figure out what extension the file should have, and rename it.
tempScriptFileName := tf.Name()
if s.IAPConf.IAPExt != "" {
err := os.Rename(tempScriptFileName, fmt.Sprintf("%s%s", tempScriptFileName, s.IAPConf.IAPExt))
if err != nil {
return "", fmt.Errorf("Error setting the correct temp file extension: %s", err)
}
tempScriptFileName = fmt.Sprintf("%s%s", tempScriptFileName, s.IAPConf.IAPExt)
}
return tempScriptFileName, nil
}
// Run executes the Packer build step that creates an IAP tunnel.
func (s *StepStartTunnel) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
if !s.IAPConf.IAP {
log.Printf("Skipping step launch IAP tunnel; \"iap\" is false.")
return multistep.ActionContinue
}
// shell out to create the tunnel.
ui := state.Get("ui").(packer.Ui)
instanceName := state.Get("instance_name").(string)
c := state.Get("config").(*Config)
ui.Say("Step Launch IAP Tunnel...")
err := s.ConfigureLocalHostPort(ctx)
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Generate list of args to use to call gcloud cli.
args := []string{"compute", "start-iap-tunnel", instanceName,
strconv.Itoa(s.CommConf.Port()),
fmt.Sprintf("--local-host-port=localhost:%d", s.IAPConf.IAPLocalhostPort),
"--zone", c.Zone,
}
// This is the port the IAP tunnel listens on, on localhost.
// TODO make setting LocalHostPort optional
s.CommConf.SSHPort = s.IAPConf.IAPLocalhostPort
log.Printf("Creating tunnel launch script with args %#v", args)
// Create temp file that contains both gcloud authentication, and gcloud
// proxy setup call.
tempScriptFileName, err := s.createTempGcloudScript(args)
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
defer os.Remove(tempScriptFileName)
s.tunnelDriver = NewTunnelDriver()
err = retry.Config{
Tries: 11,
ShouldRetry: func(err error) bool {
switch err.(type) {
case RetryableTunnelError:
return true
default:
return false
}
},
RetryDelay: (&retry.Backoff{InitialBackoff: 200 * time.Millisecond, MaxBackoff: 30 * time.Second, Multiplier: 2}).Linear,
}.Run(ctx, func(ctx context.Context) error {
// tunnel launcher/destroyer has to be different on windows vs. unix.
err := s.tunnelDriver.StartTunnel(ctx, tempScriptFileName)
return err
})
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
// Cleanup stops the IAP tunnel and cleans up processes.
func (s *StepStartTunnel) Cleanup(state multistep.StateBag) {
if !s.IAPConf.IAP {
log.Printf("Skipping cleanup of IAP tunnel; \"iap\" is false.")
return
}
if s.tunnelDriver != nil {
s.tunnelDriver.StopTunnel()
}
}

View File

@ -0,0 +1,36 @@
// Code generated by "mapstructure-to-hcl2 -type IAPConfig"; DO NOT EDIT.
package googlecompute
import (
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/zclconf/go-cty/cty"
)
// FlatIAPConfig is an auto-generated flat version of IAPConfig.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatIAPConfig struct {
IAP *bool `mapstructure:"use_iap" required:"false" cty:"use_iap"`
IAPLocalhostPort *int `mapstructure:"iap_localhost_port" cty:"iap_localhost_port"`
IAPHashBang *string `mapstructure:"iap_hashbang" required:"false" cty:"iap_hashbang"`
IAPExt *string `mapstructure:"iap_ext" required:"false" cty:"iap_ext"`
}
// FlatMapstructure returns a new FlatIAPConfig.
// FlatIAPConfig is an auto-generated flat version of IAPConfig.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*IAPConfig) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatIAPConfig)
}
// HCL2Spec returns the hcl spec of a IAPConfig.
// This spec is used by HCL to read the fields of IAPConfig.
// The decoded values from this spec will then be applied to a FlatIAPConfig.
func (*FlatIAPConfig) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"use_iap": &hcldec.AttrSpec{Name: "use_iap", Type: cty.Bool, Required: false},
"iap_localhost_port": &hcldec.AttrSpec{Name: "iap_localhost_port", Type: cty.Number, Required: false},
"iap_hashbang": &hcldec.AttrSpec{Name: "iap_hashbang", Type: cty.String, Required: false},
"iap_ext": &hcldec.AttrSpec{Name: "iap_ext", Type: cty.String, Required: false},
}
return s
}

View File

@ -0,0 +1,136 @@
package googlecompute
import (
"context"
"fmt"
"io/ioutil"
"os"
"runtime"
"testing"
"github.com/hashicorp/packer/helper/communicator"
)
type MockTunnelDriver struct {
StopTunnelCalled bool
StartTunnelCalled bool
}
func (m *MockTunnelDriver) StopTunnel() {
m.StopTunnelCalled = true
}
func (m *MockTunnelDriver) StartTunnel(context.Context, string) error {
m.StartTunnelCalled = true
return nil
}
func getTestStepStartTunnel() *StepStartTunnel {
return &StepStartTunnel{
IAPConf: &IAPConfig{
IAP: true,
IAPLocalhostPort: 0,
IAPHashBang: "/bin/bash",
IAPExt: "",
},
CommConf: &communicator.Config{
SSH: communicator.SSH{
SSHPort: 1234,
},
},
AccountFile: "/path/to/account_file.json",
ProjectId: "fake-project-123",
}
}
func TestStepStartTunnel_CreateTempScript(t *testing.T) {
s := getTestStepStartTunnel()
args := []string{"compute", "start-iap-tunnel", "fakeinstance-12345",
"1234", "--local-host-port=localhost:8774", "--zone", "us-central-b"}
scriptPath, err := s.createTempGcloudScript(args)
if err != nil {
t.Fatalf("Shouldn't have error building script file.")
}
defer os.Remove(scriptPath)
f, err := ioutil.ReadFile(scriptPath)
if err != nil {
t.Fatalf("couldn't read created inventoryfile: %s", err)
}
expected := `#!/bin/bash
gcloud auth activate-service-account --key-file='/path/to/account_file.json'
gcloud config set project fake-project-123
gcloud compute start-iap-tunnel fakeinstance-12345 1234 --local-host-port=localhost:8774 --zone us-central-b
`
if runtime.GOOS == "windows" {
// in real life you'd not be passing a HashBang here, but GIGO.
expected = `#!/bin/bash
call gcloud auth activate-service-account --key-file "/path/to/account_file.json"
call gcloud config set project fake-project-123
call gcloud compute start-iap-tunnel fakeinstance-12345 1234 --local-host-port=localhost:8774 --zone us-central-b
`
}
if fmt.Sprintf("%s", f) != expected {
t.Fatalf("script didn't match expected:\n\n expected: \n%s\n; recieved: \n%s\n", expected, f)
}
}
func TestStepStartTunnel_Cleanup(t *testing.T) {
// Check IAP true
s := getTestStepStartTunnel()
td := &MockTunnelDriver{}
s.tunnelDriver = td
state := testState(t)
s.Cleanup(state)
if !td.StopTunnelCalled {
t.Fatalf("Should have called StopTunnel, since IAP is true")
}
// Check IAP false
s = getTestStepStartTunnel()
td = &MockTunnelDriver{}
s.tunnelDriver = td
s.IAPConf.IAP = false
s.Cleanup(state)
if td.StopTunnelCalled {
t.Fatalf("Should not have called StopTunnel, since IAP is false")
}
}
func TestStepStartTunnel_ConfigurePort_port_set_by_user(t *testing.T) {
s := getTestStepStartTunnel()
s.IAPConf.IAPLocalhostPort = 8447
ctx := context.TODO()
err := s.ConfigureLocalHostPort(ctx)
if err != nil {
t.Fatalf("Shouldn't have error detecting port")
}
if s.IAPConf.IAPLocalhostPort != 8447 {
t.Fatalf("Shouldn't have found new port; one was configured.")
}
}
func TestStepStartTunnel_ConfigurePort_port_not_set_by_user(t *testing.T) {
s := getTestStepStartTunnel()
s.IAPConf.IAPLocalhostPort = 0
ctx := context.TODO()
err := s.ConfigureLocalHostPort(ctx)
if err != nil {
t.Fatalf("Shouldn't have error detecting port")
}
if s.IAPConf.IAPLocalhostPort == 0 {
t.Fatalf("Should have found new port; none was configured.")
}
}

View File

@ -0,0 +1,51 @@
// +build !windows
package googlecompute
import (
"context"
"log"
"os/exec"
"syscall"
)
func NewTunnelDriver() TunnelDriver {
return &TunnelDriverLinux{}
}
type TunnelDriverLinux struct {
cmd *exec.Cmd
}
func (t *TunnelDriverLinux) StartTunnel(cancelCtx context.Context, tempScriptFileName string) error {
cmd := exec.CommandContext(cancelCtx, tempScriptFileName)
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
err := RunTunnelCommand(cmd)
if err != nil {
return err
}
// Store successful command on step so we can access it to cancel it
// later.
t.cmd = cmd
return nil
}
func (t *TunnelDriverLinux) StopTunnel() {
if t.cmd != nil && t.cmd.Process != nil {
log.Printf("Cleaning up the IAP tunnel...")
// Why not just cmd.Process.Kill()? I'm glad you asked. The gcloud
// call spawns a python subprocess that listens on the port, and you
// need to use the process _group_ id to halt this process and its
// daemon child. We create the group ID with the syscall.SysProcAttr
// call inside the retry loop above, and then store that ID on the
// command so we can halt it here.
err := syscall.Kill(-t.cmd.Process.Pid, syscall.SIGINT)
if err != nil {
log.Printf("Issue stopping IAP tunnel: %s", err)
}
} else {
log.Printf("Couldn't find IAP tunnel process to kill. Continuing.")
}
}

View File

@ -0,0 +1,41 @@
// +build windows
package googlecompute
import (
"context"
"log"
"os/exec"
)
func NewTunnelDriver() TunnelDriver {
return &TunnelDriverWindows{}
}
type TunnelDriverWindows struct {
cmd *exec.Cmd
}
func (t *TunnelDriverWindows) StartTunnel(cancelCtx context.Context, tempScriptFileName string) error {
args := []string{"/C", "call", tempScriptFileName}
cmd := exec.CommandContext(cancelCtx, "cmd", args...)
err := RunTunnelCommand(cmd)
if err != nil {
return err
}
// Store successful command on step so we can access it to cancel it
// later.
t.cmd = cmd
return nil
}
func (t *TunnelDriverWindows) StopTunnel() {
if t.cmd != nil && t.cmd.Process != nil {
err := t.cmd.Process.Kill()
if err != nil {
log.Printf("Issue stopping IAP tunnel: %s", err)
}
} else {
log.Printf("Couldn't find IAP tunnel process to kill. Continuing.")
}
}

View File

@ -22,6 +22,7 @@ type Config struct {
common.PackerConfig `mapstructure:",squash"`
AccountFile string `mapstructure:"account_file"`
IAP bool `mapstructure:"iap"`
DiskSizeGb int64 `mapstructure:"disk_size"`
DiskType string `mapstructure:"disk_type"`

View File

@ -17,6 +17,7 @@ type FlatConfig struct {
PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables"`
PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables"`
AccountFile *string `mapstructure:"account_file" cty:"account_file"`
IAP *bool `mapstructure:"iap" cty:"iap"`
DiskSizeGb *int64 `mapstructure:"disk_size" cty:"disk_size"`
DiskType *string `mapstructure:"disk_type" cty:"disk_type"`
MachineType *string `mapstructure:"machine_type" cty:"machine_type"`
@ -48,6 +49,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false},
"packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false},
"account_file": &hcldec.AttrSpec{Name: "account_file", Type: cty.String, Required: false},
"iap": &hcldec.AttrSpec{Name: "iap", Type: cty.Bool, Required: false},
"disk_size": &hcldec.AttrSpec{Name: "disk_size", Type: cty.Number, Required: false},
"disk_type": &hcldec.AttrSpec{Name: "disk_type", Type: cty.String, Required: false},
"machine_type": &hcldec.AttrSpec{Name: "machine_type", Type: cty.String, Required: false},

View File

@ -28,6 +28,7 @@ type Config struct {
AccountFile string `mapstructure:"account_file"`
ProjectId string `mapstructure:"project_id"`
IAP bool `mapstructure:"iap"`
Bucket string `mapstructure:"bucket"`
GCSObjectName string `mapstructure:"gcs_object_name"`

View File

@ -18,6 +18,7 @@ type FlatConfig struct {
PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables"`
AccountFile *string `mapstructure:"account_file" cty:"account_file"`
ProjectId *string `mapstructure:"project_id" cty:"project_id"`
IAP *bool `mapstructure:"iap" cty:"iap"`
Bucket *string `mapstructure:"bucket" cty:"bucket"`
GCSObjectName *string `mapstructure:"gcs_object_name" cty:"gcs_object_name"`
ImageDescription *string `mapstructure:"image_description" cty:"image_description"`
@ -50,6 +51,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false},
"account_file": &hcldec.AttrSpec{Name: "account_file", Type: cty.String, Required: false},
"project_id": &hcldec.AttrSpec{Name: "project_id", Type: cty.String, Required: false},
"iap": &hcldec.AttrSpec{Name: "iap", Type: cty.Bool, Required: false},
"bucket": &hcldec.AttrSpec{Name: "bucket", Type: cty.String, Required: false},
"gcs_object_name": &hcldec.AttrSpec{Name: "gcs_object_name", Type: cty.String, Required: false},
"image_description": &hcldec.AttrSpec{Name: "image_description", Type: cty.String, Required: false},

View File

@ -217,6 +217,8 @@ builder.
@include 'builder/googlecompute/Config-not-required.mdx'
@include 'builder/googlecompute/IAPConfig-not-required.mdx'
## Startup Scripts
Startup scripts can be a powerful tool for configuring the instance from which

View File

@ -0,0 +1,25 @@
<!-- Code generated from the comments of the IAPConfig struct in builder/googlecompute/step_start_tunnel.go; DO NOT EDIT MANUALLY -->
- `use_iap` (bool) - Whether to use an IAP proxy.
Prerequisites and limitations for using IAP:
- You must manually enable the IAP API in the Google Cloud console.
- 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" >
"Add Member". Then add your service account and choose the role
"IAP-secured Tunnel User" and add any conditions you may care about.
- `iap_localhost_port` (int) - Which port to connect the local end of the IAM localhost proxy to. If
left blank, Packer will choose a port for you from available ports.
- `iap_hashbang` (string) - What "hashbang" to use to invoke script that sets up gcloud.
Default: "/bin/sh"
- `iap_ext` (string) - What file extension to use for script that sets up gcloud.
Default: ".sh"

View File

@ -0,0 +1,2 @@
<!-- Code generated from the comments of the IAPConfig struct in builder/googlecompute/step_start_tunnel.go; DO NOT EDIT MANUALLY -->
StepStartTunnel represents a Packer build step that launches an IAP tunnel