diff --git a/builder/googlecompute/config.go b/builder/googlecompute/config.go index 3d9194c17..11e8ac47f 100644 --- a/builder/googlecompute/config.go +++ b/builder/googlecompute/config.go @@ -26,39 +26,39 @@ type Config struct { AccountFile string `mapstructure:"account_file"` ProjectId string `mapstructure:"project_id"` - AcceleratorType string `mapstructure:"accelerator_type"` - AcceleratorCount int64 `mapstructure:"accelerator_count"` - Address string `mapstructure:"address"` - DiskName string `mapstructure:"disk_name"` - DiskSizeGb int64 `mapstructure:"disk_size"` - DiskType string `mapstructure:"disk_type"` - ImageName string `mapstructure:"image_name"` - ImageDescription string `mapstructure:"image_description"` - ImageFamily string `mapstructure:"image_family"` - ImageLabels map[string]string `mapstructure:"image_labels"` - ImageLicenses []string `mapstructure:"image_licenses"` - InstanceName string `mapstructure:"instance_name"` - Labels map[string]string `mapstructure:"labels"` - MachineType string `mapstructure:"machine_type"` - Metadata map[string]string `mapstructure:"metadata"` - Network string `mapstructure:"network"` - NetworkProjectId string `mapstructure:"network_project_id"` - OmitExternalIP bool `mapstructure:"omit_external_ip"` - OnHostMaintenance string `mapstructure:"on_host_maintenance"` - Preemptible bool `mapstructure:"preemptible"` - RawStateTimeout string `mapstructure:"state_timeout"` - Region string `mapstructure:"region"` - Scopes []string `mapstructure:"scopes"` - SourceImage string `mapstructure:"source_image"` - SourceImageFamily string `mapstructure:"source_image_family"` - SourceImageProjectId string `mapstructure:"source_image_project_id"` - StartupScriptFile string `mapstructure:"startup_script_file"` - Subnetwork string `mapstructure:"subnetwork"` - Tags []string `mapstructure:"tags"` - UseInternalIP bool `mapstructure:"use_internal_ip"` - Zone string `mapstructure:"zone"` - - ServiceAccountEmail string `mapstructure:"service_account_email"` + AcceleratorType string `mapstructure:"accelerator_type"` + AcceleratorCount int64 `mapstructure:"accelerator_count"` + Address string `mapstructure:"address"` + DisableDefaultServiceAccount bool `mapstructure:"disable_default_service_account"` + DiskName string `mapstructure:"disk_name"` + DiskSizeGb int64 `mapstructure:"disk_size"` + DiskType string `mapstructure:"disk_type"` + ImageName string `mapstructure:"image_name"` + ImageDescription string `mapstructure:"image_description"` + ImageFamily string `mapstructure:"image_family"` + ImageLabels map[string]string `mapstructure:"image_labels"` + ImageLicenses []string `mapstructure:"image_licenses"` + InstanceName string `mapstructure:"instance_name"` + Labels map[string]string `mapstructure:"labels"` + MachineType string `mapstructure:"machine_type"` + Metadata map[string]string `mapstructure:"metadata"` + Network string `mapstructure:"network"` + NetworkProjectId string `mapstructure:"network_project_id"` + OmitExternalIP bool `mapstructure:"omit_external_ip"` + OnHostMaintenance string `mapstructure:"on_host_maintenance"` + Preemptible bool `mapstructure:"preemptible"` + RawStateTimeout string `mapstructure:"state_timeout"` + Region string `mapstructure:"region"` + Scopes []string `mapstructure:"scopes"` + ServiceAccountEmail string `mapstructure:"service_account_email"` + SourceImage string `mapstructure:"source_image"` + SourceImageFamily string `mapstructure:"source_image_family"` + SourceImageProjectId string `mapstructure:"source_image_project_id"` + StartupScriptFile string `mapstructure:"startup_script_file"` + Subnetwork string `mapstructure:"subnetwork"` + Tags []string `mapstructure:"tags"` + UseInternalIP bool `mapstructure:"use_internal_ip"` + Zone string `mapstructure:"zone"` Account AccountFile stateTimeout time.Duration @@ -225,6 +225,11 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { errs = packer.MultiErrorAppend(fmt.Errorf("'on_host_maintenance' must be set to 'TERMINATE' when 'accelerator_count' is more than 0")) } + // If DisableDefaultServiceAccount is provided, don't allow a value for ServiceAccountEmail + if c.DisableDefaultServiceAccount && c.ServiceAccountEmail != "" { + errs = packer.MultiErrorAppend(fmt.Errorf("you may not specify a 'service_account_email' when 'disable_default_service_account' is true")) + } + // Check for any errors. if errs != nil && len(errs.Errors) > 0 { return nil, nil, errs diff --git a/builder/googlecompute/config_test.go b/builder/googlecompute/config_test.go index a4b862a1a..d3ed84506 100644 --- a/builder/googlecompute/config_test.go +++ b/builder/googlecompute/config_test.go @@ -170,6 +170,32 @@ func TestConfigPrepare(t *testing.T) { []string{"https://www.googleapis.com/auth/cloud-platform"}, false, }, + + { + "disable_default_service_account", + "", + false, + }, + { + "disable_default_service_account", + nil, + false, + }, + { + "disable_default_service_account", + false, + false, + }, + { + "disable_default_service_account", + true, + false, + }, + { + "disable_default_service_account", + "NOT A BOOL", + true, + }, } for _, tc := range cases { @@ -250,6 +276,55 @@ func TestConfigPrepareAccelerator(t *testing.T) { } } +func TestConfigPrepareServiceAccount(t *testing.T) { + cases := []struct { + Keys []string + Values []interface{} + Err bool + }{ + { + []string{"disable_default_service_account", "service_account_email"}, + []interface{}{true, "service@account.email.com"}, + true, + }, + { + []string{"disable_default_service_account", "service_account_email"}, + []interface{}{false, "service@account.email.com"}, + false, + }, + { + []string{"disable_default_service_account", "service_account_email"}, + []interface{}{true, ""}, + false, + }, + } + + for _, tc := range cases { + raw := testConfig(t) + + errStr := "" + for k := range tc.Keys { + + // Create the string for error reporting + // convert value to string if it can be converted + errStr += fmt.Sprintf("%s:%v, ", tc.Keys[k], tc.Values[k]) + if tc.Values[k] == nil { + delete(raw, tc.Keys[k]) + } else { + raw[tc.Keys[k]] = tc.Values[k] + } + } + + _, warns, errs := NewConfig(raw) + + if tc.Err { + testConfigErr(t, warns, errs, strings.TrimRight(errStr, ", ")) + } else { + testConfigOk(t, warns, errs) + } + } +} + func TestConfigDefaults(t *testing.T) { cases := []struct { Read func(c *Config) interface{} diff --git a/builder/googlecompute/driver.go b/builder/googlecompute/driver.go index 4c07a06cf..22ec62707 100644 --- a/builder/googlecompute/driver.go +++ b/builder/googlecompute/driver.go @@ -58,28 +58,29 @@ type Driver interface { } type InstanceConfig struct { - AcceleratorType string - AcceleratorCount int64 - Address string - Description string - DiskSizeGb int64 - DiskType string - Image *Image - Labels map[string]string - MachineType string - Metadata map[string]string - Name string - Network string - NetworkProjectId string - OmitExternalIP bool - OnHostMaintenance string - Preemptible bool - Region string - ServiceAccountEmail string - Scopes []string - Subnetwork string - Tags []string - Zone string + AcceleratorType string + AcceleratorCount int64 + Address string + Description string + DisableDefaultServiceAccount bool + DiskSizeGb int64 + DiskType string + Image *Image + Labels map[string]string + MachineType string + Metadata map[string]string + Name string + Network string + NetworkProjectId string + OmitExternalIP bool + OnHostMaintenance string + Preemptible bool + Region string + ServiceAccountEmail string + Scopes []string + Subnetwork string + Tags []string + Zone string } // WindowsPasswordConfig is the data structue that GCE needs to encrypt the created diff --git a/builder/googlecompute/driver_gce.go b/builder/googlecompute/driver_gce.go index 5f286fa71..8adbd833e 100644 --- a/builder/googlecompute/driver_gce.go +++ b/builder/googlecompute/driver_gce.go @@ -343,12 +343,18 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) { guestAccelerators = append(guestAccelerators, ac) } - serviceAccount := &compute.ServiceAccount{ - Email: "default", - Scopes: c.Scopes, + // Configure the instance's service account. If the user has set + // disable_default_service_account, then the default service account + // will not be used. If they also do not set service_account_email, then + // the instance will be created with no service account or scopes. + serviceAccount := &compute.ServiceAccount{} + if !c.DisableDefaultServiceAccount { + serviceAccount.Email = "default" + serviceAccount.Scopes = c.Scopes } if c.ServiceAccountEmail != "" { serviceAccount.Email = c.ServiceAccountEmail + serviceAccount.Scopes = c.Scopes } // Create the instance information diff --git a/builder/googlecompute/step_create_instance.go b/builder/googlecompute/step_create_instance.go index bc37aa7b2..06e5b8eb1 100644 --- a/builder/googlecompute/step_create_instance.go +++ b/builder/googlecompute/step_create_instance.go @@ -100,28 +100,29 @@ func (s *StepCreateInstance) Run(_ context.Context, state multistep.StateBag) mu var metadata map[string]string metadata, err = c.createInstanceMetadata(sourceImage, sshPublicKey) errCh, err = d.RunInstance(&InstanceConfig{ - AcceleratorType: c.AcceleratorType, - AcceleratorCount: c.AcceleratorCount, - Address: c.Address, - Description: "New instance created by Packer", - DiskSizeGb: c.DiskSizeGb, - DiskType: c.DiskType, - Image: sourceImage, - Labels: c.Labels, - MachineType: c.MachineType, - Metadata: metadata, - Name: name, - Network: c.Network, - NetworkProjectId: c.NetworkProjectId, - OmitExternalIP: c.OmitExternalIP, - OnHostMaintenance: c.OnHostMaintenance, - Preemptible: c.Preemptible, - Region: c.Region, - ServiceAccountEmail: c.ServiceAccountEmail, - Scopes: c.Scopes, - Subnetwork: c.Subnetwork, - Tags: c.Tags, - Zone: c.Zone, + AcceleratorType: c.AcceleratorType, + AcceleratorCount: c.AcceleratorCount, + Address: c.Address, + Description: "New instance created by Packer", + DisableDefaultServiceAccount: c.DisableDefaultServiceAccount, + DiskSizeGb: c.DiskSizeGb, + DiskType: c.DiskType, + Image: sourceImage, + Labels: c.Labels, + MachineType: c.MachineType, + Metadata: metadata, + Name: name, + Network: c.Network, + NetworkProjectId: c.NetworkProjectId, + OmitExternalIP: c.OmitExternalIP, + OnHostMaintenance: c.OnHostMaintenance, + Preemptible: c.Preemptible, + Region: c.Region, + ServiceAccountEmail: c.ServiceAccountEmail, + Scopes: c.Scopes, + Subnetwork: c.Subnetwork, + Tags: c.Tags, + Zone: c.Zone, }) if err == nil { diff --git a/builder/googlecompute/step_create_instance_test.go b/builder/googlecompute/step_create_instance_test.go index f8196e3d0..e56159138 100644 --- a/builder/googlecompute/step_create_instance_test.go +++ b/builder/googlecompute/step_create_instance_test.go @@ -248,6 +248,54 @@ func TestStepCreateInstance_errorTimeout(t *testing.T) { assert.False(t, ok, "State should not have an instance name.") } +func TestStepCreateInstance_noServiceAccount(t *testing.T) { + state := testState(t) + step := new(StepCreateInstance) + defer step.Cleanup(state) + + state.Put("ssh_public_key", "key") + + c := state.Get("config").(*Config) + c.DisableDefaultServiceAccount = true + c.ServiceAccountEmail = "" + d := state.Get("driver").(*DriverMock) + d.GetImageResult = StubImage("test-image", "test-project", []string{}, 100) + + // run the step + assert.Equal(t, step.Run(context.Background(), state), multistep.ActionContinue, "Step should have passed and continued.") + + // cleanup + step.Cleanup(state) + + // Check args passed to the driver. + assert.Equal(t, d.RunInstanceConfig.DisableDefaultServiceAccount, c.DisableDefaultServiceAccount, "Incorrect value for DisableDefaultServiceAccount passed to driver.") + assert.Equal(t, d.RunInstanceConfig.ServiceAccountEmail, c.ServiceAccountEmail, "Incorrect value for ServiceAccountEmail passed to driver.") +} + +func TestStepCreateInstance_customServiceAccount(t *testing.T) { + state := testState(t) + step := new(StepCreateInstance) + defer step.Cleanup(state) + + state.Put("ssh_public_key", "key") + + c := state.Get("config").(*Config) + c.DisableDefaultServiceAccount = true + c.ServiceAccountEmail = "custom-service-account" + d := state.Get("driver").(*DriverMock) + d.GetImageResult = StubImage("test-image", "test-project", []string{}, 100) + + // run the step + assert.Equal(t, step.Run(context.Background(), state), multistep.ActionContinue, "Step should have passed and continued.") + + // cleanup + step.Cleanup(state) + + // Check args passed to the driver. + assert.Equal(t, d.RunInstanceConfig.DisableDefaultServiceAccount, c.DisableDefaultServiceAccount, "Incorrect value for DisableDefaultServiceAccount passed to driver.") + assert.Equal(t, d.RunInstanceConfig.ServiceAccountEmail, c.ServiceAccountEmail, "Incorrect value for ServiceAccountEmail passed to driver.") +} + func TestCreateInstanceMetadata(t *testing.T) { state := testState(t) c := state.Get("config").(*Config) diff --git a/website/source/docs/builders/googlecompute.html.md b/website/source/docs/builders/googlecompute.html.md index fb006c7ae..89e57a1f7 100644 --- a/website/source/docs/builders/googlecompute.html.md +++ b/website/source/docs/builders/googlecompute.html.md @@ -210,6 +210,9 @@ builder. - `address` (string) - The name of a pre-allocated static external IP address. Note, must be the name and not the actual IP address. +- `disable_default_service_account` (bool) - If true, the default service account will not be used if `service_account_email` + is not specified. Set this value to true and omit `service_account_email` to provision a VM with no service account. + - `disk_name` (string) - The name of the disk, if unset the instance name will be used. @@ -269,7 +272,7 @@ builder. to the region hosting the specified `zone`. - `service_account_email` (string) - The service account to be used for launched instance. Defaults to - the project's default service account. + the project's default service account unless `disable_default_service_account` is true. - `scopes` (array of strings) - The service account scopes for launched instance. Defaults to: