diff --git a/builder/googlecompute/account.go b/builder/googlecompute/account.go index c8cb3fc13..e2d630732 100644 --- a/builder/googlecompute/account.go +++ b/builder/googlecompute/account.go @@ -9,9 +9,10 @@ import ( "golang.org/x/oauth2/jwt" ) -func ProcessAccountFile(text string) (*jwt.Config, error) { +func ProcessAccountFile(text string, iap bool) (*jwt.Config, error) { + driverScopes := getDriverScopes(iap) // Assume text is a JSON string - conf, err := google.JWTConfigFromJSON([]byte(text), DriverScopes...) + conf, err := google.JWTConfigFromJSON([]byte(text), driverScopes...) if err != nil { // If text was not JSON, assume it is a file path instead if _, err := os.Stat(text); os.IsNotExist(err) { @@ -25,7 +26,7 @@ func ProcessAccountFile(text string) (*jwt.Config, error) { "Error reading account_file from path '%s': %s", text, err) } - conf, err = google.JWTConfigFromJSON(data, DriverScopes...) + conf, err = google.JWTConfigFromJSON(data, driverScopes...) if err != nil { return nil, fmt.Errorf("Error parsing account_file: %s", err) } diff --git a/builder/googlecompute/builder.go b/builder/googlecompute/builder.go index 47167a43f..1257e59a4 100644 --- a/builder/googlecompute/builder.go +++ b/builder/googlecompute/builder.go @@ -37,7 +37,8 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) { // representing a GCE machine image. func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { driver, err := NewDriverGCE( - ui, b.config.ProjectId, b.config.account, b.config.VaultGCPOauthEngine) + ui, b.config.ProjectId, b.config.account, b.config.VaultGCPOauthEngine, + b.config.IAP) if err != nil { return nil, err } @@ -66,6 +67,11 @@ 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, + }, &communicator.StepConnect{ Config: &b.config.Comm, Host: communicator.CommHost(b.config.Comm.Host(), "instance_ip"), diff --git a/builder/googlecompute/config.go b/builder/googlecompute/config.go index abe8e4531..195d487c1 100644 --- a/builder/googlecompute/config.go +++ b/builder/googlecompute/config.go @@ -72,6 +72,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 +322,28 @@ 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 == "" { + c.IAPConfig.IAPHashBang = "/bin/sh" + } + if c.IAPConfig.IAPExt == "" { + c.IAPConfig.IAPExt = ".sh" + } + + // Configure IAP: Update SSH config to use localhost proxy instead + 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( @@ -359,7 +379,7 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { errs = packer.MultiErrorAppend(errs, fmt.Errorf("You cannot "+ "specify both account_file and vault_gcp_oauth_engine.")) } - cfg, err := ProcessAccountFile(c.AccountFile) + cfg, err := ProcessAccountFile(c.AccountFile, c.IAP) if err != nil { errs = packer.MultiErrorAppend(errs, err) } diff --git a/builder/googlecompute/config.hcl2spec.go b/builder/googlecompute/config.hcl2spec.go index 124435d7d..e000224f4 100644 --- a/builder/googlecompute/config.hcl2spec.go +++ b/builder/googlecompute/config.hcl2spec.go @@ -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())}, diff --git a/builder/googlecompute/driver_gce.go b/builder/googlecompute/driver_gce.go index f98f964c4..9209fbd17 100644 --- a/builder/googlecompute/driver_gce.go +++ b/builder/googlecompute/driver_gce.go @@ -34,7 +34,13 @@ type driverGCE struct { ui packer.Ui } -var DriverScopes = []string{"https://www.googleapis.com/auth/compute", "https://www.googleapis.com/auth/devstorage.full_control"} +func getDriverScopes(iap bool) []string { + ds := []string{"https://www.googleapis.com/auth/compute", "https://www.googleapis.com/auth/devstorage.full_control"} + // if iap { + // ds = append(ds, "https://www.googleapis.com/auth/iap.tunnelResourceAccessor") + // } + return ds +} // Define a TokenSource that gets tokens from Vault type OauthTokenSource struct { @@ -69,7 +75,7 @@ func (ots OauthTokenSource) Token() (*oauth2.Token, error) { } -func NewClientGCE(conf *jwt.Config, vaultOauth string) (*http.Client, error) { +func NewClientGCE(conf *jwt.Config, vaultOauth string, iap bool) (*http.Client, error) { var err error var client *http.Client @@ -84,7 +90,7 @@ func NewClientGCE(conf *jwt.Config, vaultOauth string) (*http.Client, error) { // Auth with AccountFile if provided log.Printf("[INFO] Requesting Google token via account_file...") log.Printf("[INFO] -- Email: %s", conf.Email) - log.Printf("[INFO] -- Scopes: %s", DriverScopes) + log.Printf("[INFO] -- Scopes: %s", getDriverScopes(iap)) log.Printf("[INFO] -- Private Key Length: %d", len(conf.PrivateKey)) // Initiate an http.Client. The following GET request will be @@ -93,7 +99,7 @@ func NewClientGCE(conf *jwt.Config, vaultOauth string) (*http.Client, error) { client = conf.Client(context.TODO()) } else { log.Printf("[INFO] Requesting Google token via GCE API Default Client Token Source...") - client, err = google.DefaultClient(context.TODO(), DriverScopes...) + client, err = google.DefaultClient(context.TODO(), getDriverScopes(iap)...) // The DefaultClient uses the DefaultTokenSource of the google lib. // The DefaultTokenSource uses the "Application Default Credentials" // It looks for credentials in the following places, preferring the first location found: @@ -115,8 +121,8 @@ func NewClientGCE(conf *jwt.Config, vaultOauth string) (*http.Client, error) { return client, nil } -func NewDriverGCE(ui packer.Ui, p string, conf *jwt.Config, vaultOauth string) (Driver, error) { - client, err := NewClientGCE(conf, vaultOauth) +func NewDriverGCE(ui packer.Ui, p string, conf *jwt.Config, vaultOauth string, iap bool) (Driver, error) { + client, err := NewClientGCE(conf, vaultOauth, iap) if err != nil { return nil, err } diff --git a/builder/googlecompute/step_start_tunnel.go b/builder/googlecompute/step_start_tunnel.go new file mode 100644 index 000000000..4be633a48 --- /dev/null +++ b/builder/googlecompute/step_start_tunnel.go @@ -0,0 +1,260 @@ +//go:generate struct-markdown +//go:generate mapstructure-to-hcl2 -type IAPConfig + +package googlecompute + +import ( + "bufio" + "bytes" + "context" + "fmt" + "log" + "os" + "os/exec" + "strconv" + "strings" + "syscall" + "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 StepStartTunnel struct { + IAPConf *IAPConfig + CommConf *communicator.Config + AccountFile string + + ctxCancel context.CancelFunc + cmd *exec.Cmd +} + +func (s *StepStartTunnel) ConfigureLocalHostPort(ctx context.Context) error { + if s.IAPConf.IAPLocalhostPort == 0 { + log.Printf("Finding an available TCP port for IAP proxy") + l, err := net.ListenRangeConfig{ + Min: 8000, + Max: 9000, + 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) + + s.IAPConf.IAPHashBang = fmt.Sprintf("#!%s\n", s.IAPConf.IAPHashBang) + log.Printf("[INFO] (google): Prepending inline gcloud setup script with %s", + s.IAPConf.IAPHashBang) + writer.WriteString(s.IAPConf.IAPHashBang) + + // authenticate to gcloud + _, err = writer.WriteString( + fmt.Sprintf("gcloud auth activate-service-account --key-file='%s'\n", + s.AccountFile)) + if err != nil { + return "", fmt.Errorf("Error preparing gcloud shell script: %s", err) + } + // call command + args = append([]string{"gcloud"}, args...) + argString := strings.Join(args, " ") + if _, err := writer.WriteString(argString + "\n"); 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) + } + + 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 != "" { + os.Rename(tempScriptFileName, fmt.Sprintf("%s%s", tempScriptFileName, s.IAPConf.IAPExt)) + 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("Calling tunnel launch 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) + + // Shell out to gcloud. + cancelCtx, cancel := context.WithCancel(ctx) + s.ctxCancel = cancel + + err = retry.Config{ + Tries: 11, + ShouldRetry: func(err error) bool { + // Example of error you get as the tunnel is still getting users + // configured in the cloud: + //"ERROR: (gcloud.compute.start-iap-tunnel) Error while connecting + // [4033: u'not authorized']." + return true + // if strings.Contains(err.Error(), "[4033: u'not authorized']") { + // log.Printf("Waiting for tunnel permissions to update. Retrying...") + // return true + // } + // return false + }, + RetryDelay: (&retry.Backoff{InitialBackoff: 200 * time.Millisecond, MaxBackoff: 30 * time.Second, Multiplier: 2}).Linear, + }.Run(ctx, func(ctx context.Context) error { + // set stdout and stderr so we can read what's going on. + var stdout, stderr bytes.Buffer + + cmd := exec.CommandContext(cancelCtx, tempScriptFileName) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + + err := cmd.Start() + log.Printf("Waiting 30s for tunnel to create...") + if err != nil { + err := fmt.Errorf("Error calling gcloud sdk to launch IAP tunnel: %s", + err) + cmd.Process.Kill() + return err + } + // Wait for tunnel to launch and gather response. TODO: do this without + // a sleep. + time.Sleep(30 * time.Second) + + // Track stdout. + sout := stdout.String() + if sout != "" { + log.Printf("[start-iap-tunnel] stdout is:") + } + + log.Printf("[start-iap-tunnel] stderr is:") + serr := stderr.String() + log.Println(serr) + if strings.Contains(serr, "ERROR") { + cmd.Process.Kill() + errIdx := strings.Index(serr, "ERROR:") + return fmt.Errorf("ERROR: %s", serr[errIdx+7:len(serr)]) + } + // Store successful command on step so we can access it to cancel it + // later. + s.cmd = cmd + return nil + }) + + return multistep.ActionContinue +} + +// Cleanup destroys the GCE instance created during the image creation process. +func (s *StepStartTunnel) Cleanup(state multistep.StateBag) { + if s.cmd != nil && s.cmd.Process != nil { + log.Printf("Cleaning up the IAP tunnel...") + // Why not just s.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 kill 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 destroy it here. + syscall.Kill(-s.cmd.Process.Pid, syscall.SIGKILL) + } else { + log.Printf("Couldn't find IAP tunnel process to kill. Continuing.") + } + return +} diff --git a/builder/googlecompute/step_start_tunnel.hcl2spec.go b/builder/googlecompute/step_start_tunnel.hcl2spec.go new file mode 100644 index 000000000..ac617884a --- /dev/null +++ b/builder/googlecompute/step_start_tunnel.hcl2spec.go @@ -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 +} diff --git a/post-processor/googlecompute-export/post-processor.go b/post-processor/googlecompute-export/post-processor.go index 4f21e4fde..f02c99974 100644 --- a/post-processor/googlecompute-export/post-processor.go +++ b/post-processor/googlecompute-export/post-processor.go @@ -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"` @@ -111,14 +112,14 @@ func (p *PostProcessor) PostProcess(ctx context.Context, ui packer.Ui, artifact // Set up credentials for GCE driver. if builderAccountFile != "" { - cfg, err := googlecompute.ProcessAccountFile(builderAccountFile) + cfg, err := googlecompute.ProcessAccountFile(builderAccountFile, p.config.IAP) if err != nil { return nil, false, false, err } p.config.account = cfg } if p.config.AccountFile != "" { - cfg, err := googlecompute.ProcessAccountFile(p.config.AccountFile) + cfg, err := googlecompute.ProcessAccountFile(p.config.AccountFile, p.config.IAP) if err != nil { return nil, false, false, err } @@ -159,7 +160,7 @@ func (p *PostProcessor) PostProcess(ctx context.Context, ui packer.Ui, artifact } driver, err := googlecompute.NewDriverGCE(ui, builderProjectId, - p.config.account, p.config.VaultGCPOauthEngine) + p.config.account, p.config.VaultGCPOauthEngine, p.config.IAP) if err != nil { return nil, false, false, err } diff --git a/post-processor/googlecompute-export/post-processor.hcl2spec.go b/post-processor/googlecompute-export/post-processor.hcl2spec.go index ed91bcccf..156c852b8 100644 --- a/post-processor/googlecompute-export/post-processor.hcl2spec.go +++ b/post-processor/googlecompute-export/post-processor.hcl2spec.go @@ -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}, diff --git a/post-processor/googlecompute-import/post-processor.go b/post-processor/googlecompute-import/post-processor.go index bca834c98..a37d7f8fc 100644 --- a/post-processor/googlecompute-import/post-processor.go +++ b/post-processor/googlecompute-import/post-processor.go @@ -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"` @@ -77,7 +78,7 @@ func (p *PostProcessor) Configure(raws ...interface{}) error { } if p.config.AccountFile != "" { - cfg, err := googlecompute.ProcessAccountFile(p.config.AccountFile) + cfg, err := googlecompute.ProcessAccountFile(p.config.AccountFile, p.config.IAP) if err != nil { errs = packer.MultiErrorAppend(errs, err) } @@ -117,7 +118,7 @@ func (p *PostProcessor) PostProcess(ctx context.Context, ui packer.Ui, artifact } p.config.ctx.Data = generatedData - client, err := googlecompute.NewClientGCE(p.config.account, p.config.VaultGCPOauthEngine) + client, err := googlecompute.NewClientGCE(p.config.account, p.config.VaultGCPOauthEngine, p.config.IAP) if err != nil { return nil, false, false, err } diff --git a/post-processor/googlecompute-import/post-processor.hcl2spec.go b/post-processor/googlecompute-import/post-processor.hcl2spec.go index 96e597813..c773769f9 100644 --- a/post-processor/googlecompute-import/post-processor.hcl2spec.go +++ b/post-processor/googlecompute-import/post-processor.hcl2spec.go @@ -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}, diff --git a/website/pages/docs/builders/googlecompute.mdx b/website/pages/docs/builders/googlecompute.mdx index 0bb5f8c25..5ed73746b 100644 --- a/website/pages/docs/builders/googlecompute.mdx +++ b/website/pages/docs/builders/googlecompute.mdx @@ -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 diff --git a/website/pages/partials/builder/googlecompute/IAPConfig-not-required.mdx b/website/pages/partials/builder/googlecompute/IAPConfig-not-required.mdx new file mode 100644 index 000000000..28bdeb1d7 --- /dev/null +++ b/website/pages/partials/builder/googlecompute/IAPConfig-not-required.mdx @@ -0,0 +1,25 @@ + + +- `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" + \ No newline at end of file diff --git a/website/pages/partials/builder/googlecompute/IAPConfig.mdx b/website/pages/partials/builder/googlecompute/IAPConfig.mdx new file mode 100644 index 000000000..b43f7ce04 --- /dev/null +++ b/website/pages/partials/builder/googlecompute/IAPConfig.mdx @@ -0,0 +1,2 @@ + +StepStartTunnel represents a Packer build step that launches an IAP tunnel