diff --git a/builder/googlecompute/config.go b/builder/googlecompute/config.go index 8a5f9e653..187dd5345 100644 --- a/builder/googlecompute/config.go +++ b/builder/googlecompute/config.go @@ -43,6 +43,7 @@ type Config struct { 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"` @@ -148,9 +149,9 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { } } - if c.SourceImage == "" { + if c.SourceImage == "" && c.SourceImageFamily == "" { errs = packer.MultiErrorAppend( - errs, errors.New("a source_image must be specified")) + errs, errors.New("a source_image or source_image_family must be specified")) } if c.Zone == "" { diff --git a/builder/googlecompute/config_test.go b/builder/googlecompute/config_test.go index 695da50ff..c2f469c3d 100644 --- a/builder/googlecompute/config_test.go +++ b/builder/googlecompute/config_test.go @@ -46,6 +46,17 @@ func TestConfigPrepare(t *testing.T) { false, }, + { + "source_image_family", + nil, + false, + }, + { + "source_image_family", + "foo", + false, + }, + { "zone", nil, diff --git a/builder/googlecompute/driver.go b/builder/googlecompute/driver.go index b60ad851c..ebaf424f3 100644 --- a/builder/googlecompute/driver.go +++ b/builder/googlecompute/driver.go @@ -22,11 +22,14 @@ type Driver interface { // DeleteDisk deletes the disk with the given name. DeleteDisk(zone, name string) (<-chan error, error) - // GetImage gets an image; tries the default and public projects. - GetImage(name string) (*Image, error) - - // GetImageFromProject gets an image from a specific project. - GetImageFromProject(project, name string) (*Image, error) + // GetImage gets an image; tries the default and public projects. If + // fromFamily is true, name designates an image family instead of a + // particular image. + GetImage(name string, fromFamily bool) (*Image, error) + + // GetImageFromProject gets an image from a specific project. If fromFamily + // is true, name designates an image family instead of a particular image. + GetImageFromProject(project, name string, fromFamily bool) (*Image, error) // GetInstanceMetadata gets a metadata variable for the instance, name. GetInstanceMetadata(zone, name, key string) (string, error) diff --git a/builder/googlecompute/driver_gce.go b/builder/googlecompute/driver_gce.go index 105a3578c..ffa8c4dab 100644 --- a/builder/googlecompute/driver_gce.go +++ b/builder/googlecompute/driver_gce.go @@ -119,7 +119,7 @@ func (d *driverGCE) CreateImage(name, description, family, zone, disk string) (< return } var image *Image - image, err = d.GetImageFromProject(d.projectId, name) + image, err = d.GetImageFromProject(d.projectId, name, false) if err != nil { close(imageCh) errCh <- err @@ -167,11 +167,11 @@ func (d *driverGCE) DeleteDisk(zone, name string) (<-chan error, error) { return errCh, nil } -func (d *driverGCE) GetImage(name string) (*Image, error) { +func (d *driverGCE) GetImage(name string, fromFamily bool) (*Image, error) { projects := []string{d.projectId, "centos-cloud", "coreos-cloud", "debian-cloud", "google-containers", "opensuse-cloud", "rhel-cloud", "suse-cloud", "ubuntu-os-cloud", "windows-cloud", "gce-nvme"} var errs error for _, project := range projects { - image, err := d.GetImageFromProject(project, name) + image, err := d.GetImageFromProject(project, name, fromFamily) if err != nil { errs = packer.MultiErrorAppend(errs, err) } @@ -185,8 +185,17 @@ func (d *driverGCE) GetImage(name string) (*Image, error) { projects, errs) } -func (d *driverGCE) GetImageFromProject(project, name string) (*Image, error) { - image, err := d.service.Images.Get(project, name).Do() +func (d *driverGCE) GetImageFromProject(project, name string, fromFamily bool) (*Image, error) { + var ( + image *compute.Image + err error + ) + + if fromFamily { + image, err = d.service.Images.GetFromFamily(project, name).Do() + } else { + image, err = d.service.Images.Get(project, name).Do() + } if err != nil { return nil, err @@ -264,7 +273,7 @@ func (d *driverGCE) GetSerialPortOutput(zone, name string) (string, error) { } func (d *driverGCE) ImageExists(name string) bool { - _, err := d.GetImageFromProject(d.projectId, name) + _, err := d.GetImageFromProject(d.projectId, name, false) // The API may return an error for reasons other than the image not // existing, but this heuristic is sufficient for now. return err == nil diff --git a/builder/googlecompute/driver_mock.go b/builder/googlecompute/driver_mock.go index 0c3e72a00..f9423b8aa 100644 --- a/builder/googlecompute/driver_mock.go +++ b/builder/googlecompute/driver_mock.go @@ -30,14 +30,16 @@ type DriverMock struct { DeleteDiskErrCh <-chan error DeleteDiskErr error - GetImageName string - GetImageResult *Image - GetImageErr error + GetImageName string + GetImageFromFamily bool + GetImageResult *Image + GetImageErr error - GetImageFromProjectProject string - GetImageFromProjectName string - GetImageFromProjectResult *Image - GetImageFromProjectErr error + GetImageFromProjectProject string + GetImageFromProjectName string + GetImageFromProjectFromFamily bool + GetImageFromProjectResult *Image + GetImageFromProjectErr error GetInstanceMetadataZone string GetInstanceMetadataName string @@ -162,14 +164,16 @@ func (d *DriverMock) DeleteDisk(zone, name string) (<-chan error, error) { return resultCh, d.DeleteDiskErr } -func (d *DriverMock) GetImage(name string) (*Image, error) { +func (d *DriverMock) GetImage(name string, fromFamily bool) (*Image, error) { d.GetImageName = name + d.GetImageFromFamily = fromFamily return d.GetImageResult, d.GetImageErr } -func (d *DriverMock) GetImageFromProject(project, name string) (*Image, error) { +func (d *DriverMock) GetImageFromProject(project, name string, fromFamily bool) (*Image, error) { d.GetImageFromProjectProject = project d.GetImageFromProjectName = name + d.GetImageFromProjectFromFamily = fromFamily return d.GetImageFromProjectResult, d.GetImageFromProjectErr } diff --git a/builder/googlecompute/step_create_instance.go b/builder/googlecompute/step_create_instance.go index 1707ef654..b9d3e55ae 100644 --- a/builder/googlecompute/step_create_instance.go +++ b/builder/googlecompute/step_create_instance.go @@ -58,10 +58,16 @@ func (c *Config) createInstanceMetadata(sourceImage *Image, sshPublicKey string) } func getImage(c *Config, d Driver) (*Image, error) { + name := c.SourceImageFamily + fromFamily := true + if c.SourceImage != "" { + name = c.SourceImage + fromFamily = false + } if c.SourceImageProjectId == "" { - return d.GetImage(c.SourceImage) + return d.GetImage(name, fromFamily) } else { - return d.GetImageFromProject(c.SourceImageProjectId, c.SourceImage) + return d.GetImageFromProject(c.SourceImageProjectId, c.SourceImage, fromFamily) } } @@ -80,6 +86,8 @@ func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction return multistep.ActionHalt } + ui.Say(fmt.Sprintf("Using image: %s", sourceImage.Name)) + if sourceImage.IsWindows() && c.Comm.Type == "winrm" && c.Comm.WinRMPassword == "" { state.Put("create_windows_password", true) } diff --git a/builder/googlecompute/step_create_instance_test.go b/builder/googlecompute/step_create_instance_test.go index c0122584a..00e842963 100644 --- a/builder/googlecompute/step_create_instance_test.go +++ b/builder/googlecompute/step_create_instance_test.go @@ -42,6 +42,45 @@ func TestStepCreateInstance(t *testing.T) { assert.Equal(t, d.DeleteDiskZone, c.Zone, "Incorrect disk zone passed to driver.") } +func TestStepCreateInstance_fromFamily(t *testing.T) { + cases := []struct { + Name string + Family string + Expect bool + }{ + {"test-image", "", false}, + {"test-image", "test-family", false}, // name trumps family + {"", "test-family", true}, + } + + for _, tc := range cases { + state := testState(t) + step := new(StepCreateInstance) + defer step.Cleanup(state) + + state.Put("ssh_public_key", "key") + + c := state.Get("config").(*Config) + c.SourceImage = tc.Name + c.SourceImageFamily = tc.Family + d := state.Get("driver").(*DriverMock) + d.GetImageResult = StubImage("test-image", "test-project", []string{}, 100) + + // run the step + assert.Equal(t, step.Run(state), multistep.ActionContinue, "Step should have passed and continued.") + + // cleanup + step.Cleanup(state) + + // Check args passed to the driver. + if tc.Expect { + assert.True(t, d.GetImageFromFamily, "Driver wasn't instructed to use an image family") + } else { + assert.False(t, d.GetImageFromFamily, "Driver was unexpectedly instructed to use an image family") + } + } +} + func TestStepCreateInstance_windowsNeedsPassword(t *testing.T) { state := testState(t) diff --git a/website/source/docs/builders/googlecompute.html.md b/website/source/docs/builders/googlecompute.html.md index 09f54b9c7..0b49bfe2e 100644 --- a/website/source/docs/builders/googlecompute.html.md +++ b/website/source/docs/builders/googlecompute.html.md @@ -125,8 +125,14 @@ builder. - `project_id` (string) - The project ID that will be used to launch instances and store images. -- `source_image` (string) - The source image to use to create the new - image from. Example: `"debian-7-wheezy-v20150127"` +- `source_image` (string) - The source image to use to create the new image + from. You can also specify `source_image_family` instead. If both + `source_image` and `source_image_family` are specified, `source_image` + takes precedence. Example: `"debian-8-jessie-v20161027"` + +- `source_image_family` (string) - The source image family to use to create + the new image from. The image family always returns its latest image that + is not deprecated. Example: `"debian-8"`. - `zone` (string) - The zone in which to launch the instance used to create the image. Example: `"us-central1-a"` @@ -147,7 +153,10 @@ builder. - `image_description` (string) - The description of the resulting image. -- `image_family` (string) - The name of the image family to which the resulting image belongs. You can create disks by specifying an image family instead of a specific image name. The image family always returns its latest image that is not deprecated. +- `image_family` (string) - The name of the image family to which the + resulting image belongs. You can create disks by specifying an image family + instead of a specific image name. The image family always returns its + latest image that is not deprecated. - `image_name` (string) - The unique name of the resulting image. Defaults to `"packer-{{timestamp}}"`.