Commit 23c947ac authored by Dustin Carlino's avatar Dustin Carlino

Create GCE image from persistent disk instead of from a tarball.

The new flow:
1) Provision the instance
2) Tear down the instance, but keep the boot disk
3) Create an image from the disk
4) Tear down the disk

The step to update gcloud is no longer needed, since gceimagebundle isn't used anymore.
Fixes #1507 and addresses https://github.com/mitchellh/packer/issues/1447#issuecomment-61610235.
parent 87001dba
...@@ -65,10 +65,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -65,10 +65,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
SSHWaitTimeout: 5 * time.Minute, SSHWaitTimeout: 5 * time.Minute,
}, },
new(common.StepProvision), new(common.StepProvision),
new(StepUpdateGcloud), new(StepTeardownInstance),
new(StepCreateImage), new(StepCreateImage),
new(StepUploadImage),
new(StepRegisterImage),
} }
// Run the steps. // Run the steps.
......
...@@ -16,10 +16,11 @@ import ( ...@@ -16,10 +16,11 @@ import (
type Config struct { type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
AccountFile string `mapstructure:"account_file"` AccountFile string `mapstructure:"account_file"`
ProjectId string `mapstructure:"project_id"` ProjectId string `mapstructure:"project_id"`
BucketName string `mapstructure:"bucket_name"` BucketName string `mapstructure:"bucket_name"`
DiskName string `mapstructure:"disk_name"`
DiskSizeGb int64 `mapstructure:"disk_size"` DiskSizeGb int64 `mapstructure:"disk_size"`
ImageName string `mapstructure:"image_name"` ImageName string `mapstructure:"image_name"`
ImageDescription string `mapstructure:"image_description"` ImageDescription string `mapstructure:"image_description"`
...@@ -37,7 +38,6 @@ type Config struct { ...@@ -37,7 +38,6 @@ type Config struct {
Zone string `mapstructure:"zone"` Zone string `mapstructure:"zone"`
account accountFile account accountFile
instanceName string
privateKeyBytes []byte privateKeyBytes []byte
sshTimeout time.Duration sshTimeout time.Duration
stateTimeout time.Duration stateTimeout time.Duration
...@@ -81,6 +81,10 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -81,6 +81,10 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
c.InstanceName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) c.InstanceName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
} }
if c.DiskName == "" {
c.DiskName = c.InstanceName
}
if c.MachineType == "" { if c.MachineType == "" {
c.MachineType = "n1-standard-1" c.MachineType = "n1-standard-1"
} }
...@@ -103,9 +107,10 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -103,9 +107,10 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
// Process Templates // Process Templates
templates := map[string]*string{ templates := map[string]*string{
"account_file": &c.AccountFile, "account_file": &c.AccountFile,
"bucket_name": &c.BucketName, "bucket_name": &c.BucketName,
"disk_name": &c.DiskName,
"image_name": &c.ImageName, "image_name": &c.ImageName,
"image_description": &c.ImageDescription, "image_description": &c.ImageDescription,
"instance_name": &c.InstanceName, "instance_name": &c.InstanceName,
......
...@@ -7,11 +7,11 @@ import ( ...@@ -7,11 +7,11 @@ import (
func testConfig(t *testing.T) map[string]interface{} { func testConfig(t *testing.T) map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"account_file": testAccountFile(t), "account_file": testAccountFile(t),
"bucket_name": "foo", "bucket_name": "foo",
"project_id": "hashicorp", "project_id": "hashicorp",
"source_image": "foo", "source_image": "foo",
"zone": "us-east-1a", "zone": "us-east-1a",
} }
} }
......
...@@ -4,15 +4,19 @@ package googlecompute ...@@ -4,15 +4,19 @@ package googlecompute
// with GCE. The Driver interface exists mostly to allow a mock implementation // with GCE. The Driver interface exists mostly to allow a mock implementation
// to be used to test the steps. // to be used to test the steps.
type Driver interface { type Driver interface {
// CreateImage creates an image with the given URL in Google Storage. // CreateImage creates an image from the given disk in Google Compute
CreateImage(name, description, url string) <-chan error // Engine.
CreateImage(name, description, zone, disk string) <-chan error
// DeleteImage deletes the image with the given name. // DeleteImage deletes the image with the given name.
DeleteImage(name string) <-chan error DeleteImage(name string) <-chan error
// DeleteInstance deletes the given instance. // DeleteInstance deletes the given instance, keeping the boot disk.
DeleteInstance(zone, name string) (<-chan error, error) DeleteInstance(zone, name string) (<-chan error, error)
// DeleteDisk deletes the disk with the given name.
DeleteDisk(zone, name string) (<-chan error, error)
// GetNatIP gets the NAT IP address for the instance. // GetNatIP gets the NAT IP address for the instance.
GetNatIP(zone, name string) (string, error) GetNatIP(zone, name string) (string, error)
......
...@@ -23,7 +23,7 @@ type driverGCE struct { ...@@ -23,7 +23,7 @@ type driverGCE struct {
var DriverScopes = []string{"https://www.googleapis.com/auth/compute", "https://www.googleapis.com/auth/devstorage.full_control"} var DriverScopes = []string{"https://www.googleapis.com/auth/compute", "https://www.googleapis.com/auth/devstorage.full_control"}
func NewDriverGCE(ui packer.Ui, p string, a *accountFile) (Driver, error) { func NewDriverGCE(ui packer.Ui, p string, a *accountFile) (Driver, error) {
var f *oauth2.Flow var f *oauth2.Options
var err error var err error
// Auth with AccountFile first if provided // Auth with AccountFile first if provided
...@@ -60,15 +60,12 @@ func NewDriverGCE(ui packer.Ui, p string, a *accountFile) (Driver, error) { ...@@ -60,15 +60,12 @@ func NewDriverGCE(ui packer.Ui, p string, a *accountFile) (Driver, error) {
}, nil }, nil
} }
func (d *driverGCE) CreateImage(name, description, url string) <-chan error { func (d *driverGCE) CreateImage(name, description, zone, disk string) <-chan error {
image := &compute.Image{ image := &compute.Image{
Description: description, Description: description,
Name: name, Name: name,
RawDisk: &compute.ImageRawDisk{ SourceDisk: fmt.Sprintf("%s%s/zones/%s/disks/%s", d.service.BasePath, d.projectId, zone, disk),
ContainerType: "TAR", SourceType: "RAW",
Source: url,
},
SourceType: "RAW",
} }
errCh := make(chan error, 1) errCh := make(chan error, 1)
...@@ -105,6 +102,17 @@ func (d *driverGCE) DeleteInstance(zone, name string) (<-chan error, error) { ...@@ -105,6 +102,17 @@ func (d *driverGCE) DeleteInstance(zone, name string) (<-chan error, error) {
return errCh, nil return errCh, nil
} }
func (d *driverGCE) DeleteDisk(zone, name string) (<-chan error, error) {
op, err := d.service.Disks.Delete(d.projectId, zone, name).Do()
if err != nil {
return nil, err
}
errCh := make(chan error, 1)
go waitForState(errCh, "DONE", d.refreshZoneOp(zone, op))
return errCh, nil
}
func (d *driverGCE) GetNatIP(zone, name string) (string, error) { func (d *driverGCE) GetNatIP(zone, name string) (string, error) {
instance, err := d.service.Instances.Get(d.projectId, zone, name).Do() instance, err := d.service.Instances.Get(d.projectId, zone, name).Do()
if err != nil { if err != nil {
...@@ -175,7 +183,7 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) { ...@@ -175,7 +183,7 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
Mode: "READ_WRITE", Mode: "READ_WRITE",
Kind: "compute#attachedDisk", Kind: "compute#attachedDisk",
Boot: true, Boot: true,
AutoDelete: true, AutoDelete: false,
InitializeParams: &compute.AttachedDiskInitializeParams{ InitializeParams: &compute.AttachedDiskInitializeParams{
SourceImage: image.SelfLink, SourceImage: image.SelfLink,
DiskSizeGb: c.DiskSizeGb, DiskSizeGb: c.DiskSizeGb,
......
...@@ -5,7 +5,8 @@ package googlecompute ...@@ -5,7 +5,8 @@ package googlecompute
type DriverMock struct { type DriverMock struct {
CreateImageName string CreateImageName string
CreateImageDesc string CreateImageDesc string
CreateImageURL string CreateImageZone string
CreateImageDisk string
CreateImageErrCh <-chan error CreateImageErrCh <-chan error
DeleteImageName string DeleteImageName string
...@@ -16,6 +17,11 @@ type DriverMock struct { ...@@ -16,6 +17,11 @@ type DriverMock struct {
DeleteInstanceErrCh <-chan error DeleteInstanceErrCh <-chan error
DeleteInstanceErr error DeleteInstanceErr error
DeleteDiskZone string
DeleteDiskName string
DeleteDiskErrCh <-chan error
DeleteDiskErr error
GetNatIPZone string GetNatIPZone string
GetNatIPName string GetNatIPName string
GetNatIPResult string GetNatIPResult string
...@@ -31,10 +37,11 @@ type DriverMock struct { ...@@ -31,10 +37,11 @@ type DriverMock struct {
WaitForInstanceErrCh <-chan error WaitForInstanceErrCh <-chan error
} }
func (d *DriverMock) CreateImage(name, description, url string) <-chan error { func (d *DriverMock) CreateImage(name, description, zone, disk string) <-chan error {
d.CreateImageName = name d.CreateImageName = name
d.CreateImageDesc = description d.CreateImageDesc = description
d.CreateImageURL = url d.CreateImageZone = zone
d.CreateImageDisk = disk
resultCh := d.CreateImageErrCh resultCh := d.CreateImageErrCh
if resultCh == nil { if resultCh == nil {
...@@ -73,6 +80,20 @@ func (d *DriverMock) DeleteInstance(zone, name string) (<-chan error, error) { ...@@ -73,6 +80,20 @@ func (d *DriverMock) DeleteInstance(zone, name string) (<-chan error, error) {
return resultCh, d.DeleteInstanceErr return resultCh, d.DeleteInstanceErr
} }
func (d *DriverMock) DeleteDisk(zone, name string) (<-chan error, error) {
d.DeleteDiskZone = zone
d.DeleteDiskName = name
resultCh := d.DeleteDiskErrCh
if resultCh == nil {
ch := make(chan error)
close(ch)
resultCh = ch
}
return resultCh, d.DeleteDiskErr
}
func (d *DriverMock) GetNatIP(zone, name string) (string, error) { func (d *DriverMock) GetNatIP(zone, name string) (string, error) {
d.GetNatIPZone = zone d.GetNatIPZone = zone
d.GetNatIPName = name d.GetNatIPName = name
......
package googlecompute package googlecompute
import ( import (
"errors"
"fmt" "fmt"
"path/filepath" "time"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
...@@ -14,39 +15,32 @@ type StepCreateImage int ...@@ -14,39 +15,32 @@ type StepCreateImage int
// Run executes the Packer build step that creates a GCE machine image. // Run executes the Packer build step that creates a GCE machine image.
// //
// Currently the only way to create a GCE image is to run the gcimagebundle // The image is created from the persistent disk used by the instance. The
// command on the running GCE instance. // instance must be deleted and the disk retained before doing this step.
func (s *StepCreateImage) Run(state multistep.StateBag) multistep.StepAction { func (s *StepCreateImage) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config) config := state.Get("config").(*Config)
comm := state.Get("communicator").(packer.Communicator) driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
sudoPrefix := ""
if config.SSHUsername != "root" {
sudoPrefix = "sudo "
}
imageFilename := fmt.Sprintf("%s.tar.gz", config.ImageName)
imageBundleCmd := "/usr/bin/gcimagebundle -d /dev/sda -o /tmp/"
ui.Say("Creating image...") ui.Say("Creating image...")
cmd := new(packer.RemoteCmd) errCh := driver.CreateImage(config.ImageName, config.ImageDescription, config.Zone, config.DiskName)
cmd.Command = fmt.Sprintf("%s%s --output_file_name %s --fssize %d", var err error
sudoPrefix, imageBundleCmd, imageFilename, config.DiskSizeGb*1024*1024*1024) select {
err := cmd.StartWithUi(comm, ui) case err = <-errCh:
if err == nil && cmd.ExitStatus != 0 { case <-time.After(config.stateTimeout):
err = fmt.Errorf( err = errors.New("time out while waiting for image to register")
"gcimagebundle exited with non-zero exit status: %d", cmd.ExitStatus)
} }
if err != nil { if err != nil {
err := fmt.Errorf("Error creating image: %s", err) err := fmt.Errorf("Error waiting for image: %s", err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
} }
state.Put("image_file_name", filepath.Join("/tmp", imageFilename)) state.Put("image_name", config.ImageName)
return multistep.ActionContinue return multistep.ActionContinue
} }
// Cleanup.
func (s *StepCreateImage) Cleanup(state multistep.StateBag) {} func (s *StepCreateImage) Cleanup(state multistep.StateBag) {}
package googlecompute package googlecompute
import ( import (
"strings" "errors"
"testing" "testing"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
) )
func TestStepCreateImage_impl(t *testing.T) { func TestStepCreateImage_impl(t *testing.T) {
...@@ -17,38 +16,49 @@ func TestStepCreateImage(t *testing.T) { ...@@ -17,38 +16,49 @@ func TestStepCreateImage(t *testing.T) {
step := new(StepCreateImage) step := new(StepCreateImage)
defer step.Cleanup(state) defer step.Cleanup(state)
comm := new(packer.MockCommunicator) config := state.Get("config").(*Config)
state.Put("communicator", comm) driver := state.Get("driver").(*DriverMock)
// run the step // run the step
if action := step.Run(state); action != multistep.ActionContinue { if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action) t.Fatalf("bad action: %#v", action)
} }
// Verify // Verify state
if !comm.StartCalled { if driver.CreateImageName != config.ImageName {
t.Fatal("start should be called") t.Fatalf("bad: %#v", driver.CreateImageName)
}
if driver.CreateImageDesc != config.ImageDescription {
t.Fatalf("bad: %#v", driver.CreateImageDesc)
} }
if strings.HasPrefix(comm.StartCmd.Command, "sudo") { if driver.CreateImageZone != config.Zone {
t.Fatal("should not sudo") t.Fatalf("bad: %#v", driver.CreateImageZone)
} }
if !strings.Contains(comm.StartCmd.Command, "gcimagebundle") { if driver.CreateImageDisk != config.DiskName {
t.Fatalf("bad command: %#v", comm.StartCmd.Command) t.Fatalf("bad: %#v", driver.CreateImageDisk)
} }
if _, ok := state.GetOk("image_file_name"); !ok { nameRaw, ok := state.GetOk("image_name")
t.Fatal("should have image") if !ok {
t.Fatal("should have name")
}
if name, ok := nameRaw.(string); !ok {
t.Fatal("name is not a string")
} else if name != config.ImageName {
t.Fatalf("bad name: %s", name)
} }
} }
func TestStepCreateImage_badExitStatus(t *testing.T) { func TestStepCreateImage_errorOnChannel(t *testing.T) {
state := testState(t) state := testState(t)
step := new(StepCreateImage) step := new(StepCreateImage)
defer step.Cleanup(state) defer step.Cleanup(state)
comm := new(packer.MockCommunicator) errCh := make(chan error, 1)
comm.StartExitStatus = 12 errCh <- errors.New("error")
state.Put("communicator", comm)
driver := state.Get("driver").(*DriverMock)
driver.CreateImageErrCh = errCh
// run the step // run the step
if action := step.Run(state); action != multistep.ActionHalt { if action := step.Run(state); action != multistep.ActionHalt {
...@@ -58,39 +68,7 @@ func TestStepCreateImage_badExitStatus(t *testing.T) { ...@@ -58,39 +68,7 @@ func TestStepCreateImage_badExitStatus(t *testing.T) {
if _, ok := state.GetOk("error"); !ok { if _, ok := state.GetOk("error"); !ok {
t.Fatal("should have error") t.Fatal("should have error")
} }
if _, ok := state.GetOk("image_file_name"); ok { if _, ok := state.GetOk("image_name"); ok {
t.Fatal("should NOT have image") t.Fatal("should NOT have image")
} }
} }
func TestStepCreateImage_nonRoot(t *testing.T) {
state := testState(t)
step := new(StepCreateImage)
defer step.Cleanup(state)
comm := new(packer.MockCommunicator)
state.Put("communicator", comm)
config := state.Get("config").(*Config)
config.SSHUsername = "bob"
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// Verify
if !comm.StartCalled {
t.Fatal("start should be called")
}
if !strings.HasPrefix(comm.StartCmd.Command, "sudo") {
t.Fatal("should sudo")
}
if !strings.Contains(comm.StartCmd.Command, "gcimagebundle") {
t.Fatalf("bad command: %#v", comm.StartCmd.Command)
}
if _, ok := state.GetOk("image_file_name"); !ok {
t.Fatal("should have image")
}
}
...@@ -12,8 +12,6 @@ import ( ...@@ -12,8 +12,6 @@ import (
// StepCreateInstance represents a Packer build step that creates GCE instances. // StepCreateInstance represents a Packer build step that creates GCE instances.
type StepCreateInstance struct { type StepCreateInstance struct {
Debug bool Debug bool
instanceName string
} }
func (config *Config) getImage() Image { func (config *Config) getImage() Image {
...@@ -91,14 +89,18 @@ func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction ...@@ -91,14 +89,18 @@ func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction
// Things succeeded, store the name so we can remove it later // Things succeeded, store the name so we can remove it later
state.Put("instance_name", name) state.Put("instance_name", name)
s.instanceName = name
return multistep.ActionContinue return multistep.ActionContinue
} }
// Cleanup destroys the GCE instance created during the image creation process. // Cleanup destroys the GCE instance created during the image creation process.
func (s *StepCreateInstance) Cleanup(state multistep.StateBag) { func (s *StepCreateInstance) Cleanup(state multistep.StateBag) {
if s.instanceName == "" { nameRaw, ok := state.GetOk("instance_name")
if !ok {
return
}
name := nameRaw.(string)
if name == "" {
return return
} }
...@@ -107,7 +109,7 @@ func (s *StepCreateInstance) Cleanup(state multistep.StateBag) { ...@@ -107,7 +109,7 @@ func (s *StepCreateInstance) Cleanup(state multistep.StateBag) {
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
ui.Say("Deleting instance...") ui.Say("Deleting instance...")
errCh, err := driver.DeleteInstance(config.Zone, s.instanceName) errCh, err := driver.DeleteInstance(config.Zone, name)
if err == nil { if err == nil {
select { select {
case err = <-errCh: case err = <-errCh:
...@@ -120,9 +122,9 @@ func (s *StepCreateInstance) Cleanup(state multistep.StateBag) { ...@@ -120,9 +122,9 @@ func (s *StepCreateInstance) Cleanup(state multistep.StateBag) {
ui.Error(fmt.Sprintf( ui.Error(fmt.Sprintf(
"Error deleting instance. Please delete it manually.\n\n"+ "Error deleting instance. Please delete it manually.\n\n"+
"Name: %s\n"+ "Name: %s\n"+
"Error: %s", s.instanceName, err)) "Error: %s", name, err))
} }
s.instanceName = "" state.Put("instance_name", "")
return return
} }
package googlecompute
import (
"errors"
"fmt"
"time"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
// StepRegisterImage represents a Packer build step that registers GCE machine images.
type StepRegisterImage int
// Run executes the Packer build step that registers a GCE machine image.
func (s *StepRegisterImage) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
var err error
imageURL := fmt.Sprintf(
"https://storage.cloud.google.com/%s/%s.tar.gz",
config.BucketName, config.ImageName)
ui.Say("Registering image...")
errCh := driver.CreateImage(config.ImageName, config.ImageDescription, imageURL)
select {
case err = <-errCh:
case <-time.After(config.stateTimeout):
err = errors.New("time out while waiting for image to register")
}
if err != nil {
err := fmt.Errorf("Error waiting for image: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("image_name", config.ImageName)
return multistep.ActionContinue
}
// Cleanup.
func (s *StepRegisterImage) Cleanup(state multistep.StateBag) {}
package googlecompute
import (
"errors"
"github.com/mitchellh/multistep"
"testing"
"time"
)
func TestStepRegisterImage_impl(t *testing.T) {
var _ multistep.Step = new(StepRegisterImage)
}
func TestStepRegisterImage(t *testing.T) {
state := testState(t)
step := new(StepRegisterImage)
defer step.Cleanup(state)
config := state.Get("config").(*Config)
driver := state.Get("driver").(*DriverMock)
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// Verify state
if driver.CreateImageName != config.ImageName {
t.Fatalf("bad: %#v", driver.CreateImageName)
}
if driver.CreateImageDesc != config.ImageDescription {
t.Fatalf("bad: %#v", driver.CreateImageDesc)
}
nameRaw, ok := state.GetOk("image_name")
if !ok {
t.Fatal("should have name")
}
if name, ok := nameRaw.(string); !ok {
t.Fatal("name is not a string")
} else if name != config.ImageName {
t.Fatalf("bad name: %s", name)
}
}
func TestStepRegisterImage_waitError(t *testing.T) {
state := testState(t)
step := new(StepRegisterImage)
defer step.Cleanup(state)
errCh := make(chan error, 1)
errCh <- errors.New("error")
driver := state.Get("driver").(*DriverMock)
driver.CreateImageErrCh = errCh
// run the step
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
// Verify state
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should have error")
}
if _, ok := state.GetOk("image_name"); ok {
t.Fatal("should NOT have image_name")
}
}
func TestStepRegisterImage_errorTimeout(t *testing.T) {
state := testState(t)
step := new(StepRegisterImage)
defer step.Cleanup(state)
errCh := make(chan error, 1)
go func() {
<-time.After(10 * time.Millisecond)
errCh <- nil
}()
config := state.Get("config").(*Config)
config.stateTimeout = 1 * time.Microsecond
driver := state.Get("driver").(*DriverMock)
driver.CreateImageErrCh = errCh
// run the step
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
// Verify state
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should have error")
}
if _, ok := state.GetOk("image_name"); ok {
t.Fatal("should NOT have image name")
}
}
package googlecompute
import (
"errors"
"fmt"
"time"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
// StepTeardownInstance represents a Packer build step that tears down GCE
// instances.
type StepTeardownInstance struct {
Debug bool
}
// Run executes the Packer build step that tears down a GCE instance.
func (s *StepTeardownInstance) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
name := config.InstanceName
if name == "" {
return multistep.ActionHalt
}
ui.Say("Deleting instance...")
errCh, err := driver.DeleteInstance(config.Zone, name)
if err == nil {
select {
case err = <-errCh:
case <-time.After(config.stateTimeout):
err = errors.New("time out while waiting for instance to delete")
}
}
if err != nil {
ui.Error(fmt.Sprintf(
"Error deleting instance. Please delete it manually.\n\n"+
"Name: %s\n"+
"Error: %s", name, err))
return multistep.ActionHalt
}
ui.Message("Instance has been deleted!")
state.Put("instance_name", "")
return multistep.ActionContinue
}
// Deleting the instance does not remove the boot disk. This cleanup removes
// the disk.
func (s *StepTeardownInstance) Cleanup(state multistep.StateBag) {
config := state.Get("config").(*Config)
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
ui.Say("Deleting disk...")
errCh, err := driver.DeleteDisk(config.Zone, config.DiskName)
if err == nil {
select {
case err = <-errCh:
case <-time.After(config.stateTimeout):
err = errors.New("time out while waiting for disk to delete")
}
}
if err != nil {
ui.Error(fmt.Sprintf(
"Error deleting disk. Please delete it manually.\n\n"+
"Name: %s\n"+
"Error: %s", config.InstanceName, err))
}
ui.Message("Disk has been deleted!")
return
}
package googlecompute
import (
"github.com/mitchellh/multistep"
"testing"
)
func TestStepTeardownInstance_impl(t *testing.T) {
var _ multistep.Step = new(StepTeardownInstance)
}
func TestStepTeardownInstance(t *testing.T) {
state := testState(t)
step := new(StepTeardownInstance)
defer step.Cleanup(state)
config := state.Get("config").(*Config)
driver := state.Get("driver").(*DriverMock)
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
if driver.DeleteInstanceName != config.InstanceName {
t.Fatal("should've deleted instance")
}
if driver.DeleteInstanceZone != config.Zone {
t.Fatal("bad zone: %#v", driver.DeleteInstanceZone)
}
// cleanup
step.Cleanup(state)
if driver.DeleteDiskName != config.InstanceName {
t.Fatal("should've deleted disk")
}
if driver.DeleteDiskZone != config.Zone {
t.Fatal("bad zone: %#v", driver.DeleteDiskZone)
}
}
package googlecompute
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
// StepUpdateGcloud represents a Packer build step that updates the gsutil
// utility to the latest version available.
type StepUpdateGcloud int
// Run executes the Packer build step that updates the gsutil utility to the
// latest version available.
//
// This step is required to prevent the image creation process from hanging;
// the image creation process utilizes the gcimagebundle cli tool which will
// prompt to update gsutil if a newer version is available.
func (s *StepUpdateGcloud) Run(state multistep.StateBag) multistep.StepAction {
comm := state.Get("communicator").(packer.Communicator)
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
sudoPrefix := ""
if config.SSHUsername != "root" {
sudoPrefix = "sudo "
}
gsutilUpdateCmd := "/usr/local/bin/gcloud -q components update"
cmd := new(packer.RemoteCmd)
cmd.Command = fmt.Sprintf("%s%s", sudoPrefix, gsutilUpdateCmd)
ui.Say("Updating gcloud components...")
err := cmd.StartWithUi(comm, ui)
if err == nil && cmd.ExitStatus != 0 {
err = fmt.Errorf(
"gcloud components update exited with non-zero exit status: %d", cmd.ExitStatus)
}
if err != nil {
err := fmt.Errorf("Error updating gcloud components: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
// Cleanup.
func (s *StepUpdateGcloud) Cleanup(state multistep.StateBag) {}
package googlecompute
import (
"strings"
"testing"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
func TestStepUpdateGcloud_impl(t *testing.T) {
var _ multistep.Step = new(StepUpdateGcloud)
}
func TestStepUpdateGcloud(t *testing.T) {
state := testState(t)
step := new(StepUpdateGcloud)
defer step.Cleanup(state)
comm := new(packer.MockCommunicator)
state.Put("communicator", comm)
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// Verify
if !comm.StartCalled {
t.Fatal("start should be called")
}
if strings.HasPrefix(comm.StartCmd.Command, "sudo") {
t.Fatal("should not sudo")
}
if !strings.Contains(comm.StartCmd.Command, "gcloud -q components update") {
t.Fatalf("bad command: %#v", comm.StartCmd.Command)
}
}
func TestStepUpdateGcloud_badExitStatus(t *testing.T) {
state := testState(t)
step := new(StepUpdateGcloud)
defer step.Cleanup(state)
comm := new(packer.MockCommunicator)
comm.StartExitStatus = 12
state.Put("communicator", comm)
// run the step
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should have error")
}
}
func TestStepUpdateGcloud_nonRoot(t *testing.T) {
state := testState(t)
step := new(StepUpdateGcloud)
defer step.Cleanup(state)
comm := new(packer.MockCommunicator)
state.Put("communicator", comm)
config := state.Get("config").(*Config)
config.SSHUsername = "bob"
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// Verify
if !comm.StartCalled {
t.Fatal("start should be called")
}
if !strings.HasPrefix(comm.StartCmd.Command, "sudo") {
t.Fatal("should sudo")
}
if !strings.Contains(comm.StartCmd.Command, "gcloud -q components update") {
t.Fatalf("bad command: %#v", comm.StartCmd.Command)
}
}
package googlecompute
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
// StepUploadImage represents a Packer build step that uploads GCE machine images.
type StepUploadImage int
// Run executes the Packer build step that uploads a GCE machine image.
func (s *StepUploadImage) Run(state multistep.StateBag) multistep.StepAction {
comm := state.Get("communicator").(packer.Communicator)
config := state.Get("config").(*Config)
imageFilename := state.Get("image_file_name").(string)
ui := state.Get("ui").(packer.Ui)
sudoPrefix := ""
if config.SSHUsername != "root" {
sudoPrefix = "sudo "
}
ui.Say("Uploading image...")
cmd := new(packer.RemoteCmd)
cmd.Command = fmt.Sprintf("%s/usr/local/bin/gsutil cp %s gs://%s",
sudoPrefix, imageFilename, config.BucketName)
err := cmd.StartWithUi(comm, ui)
if err == nil && cmd.ExitStatus != 0 {
err = fmt.Errorf(
"gsutil exited with non-zero exit status: %d", cmd.ExitStatus)
}
if err != nil {
err := fmt.Errorf("Error uploading image: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
// Cleanup.
func (s *StepUploadImage) Cleanup(state multistep.StateBag) {}
package googlecompute
import (
"strings"
"testing"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
func TestStepUploadImage_impl(t *testing.T) {
var _ multistep.Step = new(StepUploadImage)
}
func TestStepUploadImage(t *testing.T) {
state := testState(t)
step := new(StepUploadImage)
defer step.Cleanup(state)
comm := new(packer.MockCommunicator)
state.Put("communicator", comm)
state.Put("image_file_name", "foo")
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// Verify
if !comm.StartCalled {
t.Fatal("start should be called")
}
if strings.HasPrefix(comm.StartCmd.Command, "sudo") {
t.Fatal("should not sudo")
}
if !strings.Contains(comm.StartCmd.Command, "gsutil cp") {
t.Fatalf("bad command: %#v", comm.StartCmd.Command)
}
}
func TestStepUploadImage_badExitStatus(t *testing.T) {
state := testState(t)
step := new(StepUploadImage)
defer step.Cleanup(state)
comm := new(packer.MockCommunicator)
comm.StartExitStatus = 12
state.Put("communicator", comm)
state.Put("image_file_name", "foo")
// run the step
if action := step.Run(state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v", action)
}
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should have error")
}
}
func TestStepUploadImage_nonRoot(t *testing.T) {
state := testState(t)
step := new(StepUploadImage)
defer step.Cleanup(state)
comm := new(packer.MockCommunicator)
state.Put("communicator", comm)
state.Put("image_file_name", "foo")
config := state.Get("config").(*Config)
config.SSHUsername = "bob"
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// Verify
if !comm.StartCalled {
t.Fatal("start should be called")
}
if !strings.HasPrefix(comm.StartCmd.Command, "sudo") {
t.Fatal("should sudo")
}
if !strings.Contains(comm.StartCmd.Command, "gsutil cp") {
t.Fatalf("bad command: %#v", comm.StartCmd.Command)
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment