diff --git a/builder/googlecompute/api.go b/builder/googlecompute/api.go index e178c8aa8..6657fdc28 100644 --- a/builder/googlecompute/api.go +++ b/builder/googlecompute/api.go @@ -66,18 +66,6 @@ func New(projectId string, zone string, c *clientSecrets, pemKey []byte) (*Googl return googleComputeClient, nil } -// InstanceStatus returns a string representing the status of the named instance. -// Status will be one of: "PROVISIONING", "STAGING", "RUNNING", "STOPPING", -// "STOPPED", "TERMINATED". -func (g *GoogleComputeClient) InstanceStatus(zone, name string) (string, error) { - instanceGetCall := g.Service.Instances.Get(g.ProjectId, zone, name) - instance, err := instanceGetCall.Do() - if err != nil { - return "", err - } - return instance.Status, nil -} - // CreateImage registers a GCE Image with a project. func (g *GoogleComputeClient) CreateImage(name, description, sourceURL string) (*compute.Operation, error) { imageRawDisk := &compute.ImageRawDisk{ @@ -98,26 +86,6 @@ func (g *GoogleComputeClient) CreateImage(name, description, sourceURL string) ( return operation, nil } -// GetNatIp returns the public IPv4 address for named GCE instance. -func (g *GoogleComputeClient) GetNatIP(zone, name string) (string, error) { - instanceGetCall := g.Service.Instances.Get(g.ProjectId, zone, name) - instance, err := instanceGetCall.Do() - if err != nil { - return "", err - } - for _, ni := range instance.NetworkInterfaces { - if ni.AccessConfigs == nil { - continue - } - for _, ac := range ni.AccessConfigs { - if ac.NatIP != "" { - return ac.NatIP, nil - } - } - } - return "", nil -} - // ZoneOperationStatus returns the status for the named zone operation. func (g *GoogleComputeClient) ZoneOperationStatus(zone, name string) (string, error) { zoneOperationsGetCall := g.Service.ZoneOperations.Get(g.ProjectId, zone, name) diff --git a/builder/googlecompute/builder.go b/builder/googlecompute/builder.go index 749b3cdce..1fe154635 100644 --- a/builder/googlecompute/builder.go +++ b/builder/googlecompute/builder.go @@ -50,9 +50,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe steps := []multistep.Step{ new(StepCreateSSHKey), new(StepCreateInstance), + new(StepInstanceInfo), } /* - new(stepInstanceInfo), &common.StepConnectSSH{ SSHAddress: sshAddress, SSHConfig: sshConfig, diff --git a/builder/googlecompute/driver.go b/builder/googlecompute/driver.go index 4a0f20448..1f7030e67 100644 --- a/builder/googlecompute/driver.go +++ b/builder/googlecompute/driver.go @@ -7,8 +7,14 @@ type Driver interface { // DeleteInstance deletes the given instance. DeleteInstance(zone, name string) (<-chan error, error) + // GetNatIP gets the NAT IP address for the instance. + GetNatIP(zone, name string) (string, error) + // RunInstance takes the given config and launches an instance. RunInstance(*InstanceConfig) (<-chan error, error) + + // WaitForInstance waits for an instance to reach the given state. + WaitForInstance(state, zone, name string) <-chan error } type InstanceConfig struct { diff --git a/builder/googlecompute/driver_gce.go b/builder/googlecompute/driver_gce.go index 9cdfb45f5..bc6363aba 100644 --- a/builder/googlecompute/driver_gce.go +++ b/builder/googlecompute/driver_gce.go @@ -70,6 +70,27 @@ func (d *driverGCE) DeleteInstance(zone, name string) (<-chan error, error) { return errCh, nil } +func (d *driverGCE) GetNatIP(zone, name string) (string, error) { + instance, err := d.service.Instances.Get(d.projectId, zone, name).Do() + if err != nil { + return "", err + } + + for _, ni := range instance.NetworkInterfaces { + if ni.AccessConfigs == nil { + continue + } + + for _, ac := range ni.AccessConfigs { + if ac.NatIP != "" { + return ac.NatIP, nil + } + } + } + + return "", nil +} + func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) { // Get the zone d.ui.Message(fmt.Sprintf("Loading zone: %s", c.Zone)) @@ -156,6 +177,12 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) { return errCh, nil } +func (d *driverGCE) WaitForInstance(state, zone, name string) <-chan error { + errCh := make(chan error, 1) + go waitForState(errCh, state, d.refreshInstanceState(zone, name)) + return errCh +} + func (d *driverGCE) getImage(name string) (image *compute.Image, err error) { projects := []string{d.projectId, "debian-cloud", "centos-cloud"} for _, project := range projects { @@ -173,6 +200,16 @@ func (d *driverGCE) getImage(name string) (image *compute.Image, err error) { return } +func (d *driverGCE) refreshInstanceState(zone, name string) stateRefreshFunc { + return func() (string, error) { + instance, err := d.service.Instances.Get(d.projectId, zone, name).Do() + if err != nil { + return "", err + } + return instance.Status, nil + } +} + func (d *driverGCE) refreshZoneOp(zone string, op *compute.Operation) stateRefreshFunc { return func() (string, error) { newOp, err := d.service.ZoneOperations.Get(d.projectId, zone, op.Name).Do() diff --git a/builder/googlecompute/driver_mock.go b/builder/googlecompute/driver_mock.go index 6a0e6821a..13dfa0712 100644 --- a/builder/googlecompute/driver_mock.go +++ b/builder/googlecompute/driver_mock.go @@ -8,9 +8,19 @@ type DriverMock struct { DeleteInstanceErrCh <-chan error DeleteInstanceErr error + GetNatIPZone string + GetNatIPName string + GetNatIPResult string + GetNatIPErr error + RunInstanceConfig *InstanceConfig RunInstanceErrCh <-chan error RunInstanceErr error + + WaitForInstanceState string + WaitForInstanceZone string + WaitForInstanceName string + WaitForInstanceErrCh <-chan error } func (d *DriverMock) DeleteInstance(zone, name string) (<-chan error, error) { @@ -27,6 +37,12 @@ func (d *DriverMock) DeleteInstance(zone, name string) (<-chan error, error) { return resultCh, d.DeleteInstanceErr } +func (d *DriverMock) GetNatIP(zone, name string) (string, error) { + d.GetNatIPZone = zone + d.GetNatIPName = name + return d.GetNatIPResult, d.GetNatIPErr +} + func (d *DriverMock) RunInstance(c *InstanceConfig) (<-chan error, error) { d.RunInstanceConfig = c @@ -39,3 +55,18 @@ func (d *DriverMock) RunInstance(c *InstanceConfig) (<-chan error, error) { return resultCh, d.RunInstanceErr } + +func (d *DriverMock) WaitForInstance(state, zone, name string) <-chan error { + d.WaitForInstanceState = state + d.WaitForInstanceZone = zone + d.WaitForInstanceName = name + + resultCh := d.WaitForInstanceErrCh + if resultCh == nil { + ch := make(chan error) + close(ch) + resultCh = ch + } + + return resultCh +} diff --git a/builder/googlecompute/step_instance_info.go b/builder/googlecompute/step_instance_info.go index b68b011b2..0f87077fd 100644 --- a/builder/googlecompute/step_instance_info.go +++ b/builder/googlecompute/step_instance_info.go @@ -1,40 +1,53 @@ package googlecompute import ( + "errors" "fmt" + "time" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" ) // stepInstanceInfo represents a Packer build step that gathers GCE instance info. -type stepInstanceInfo int +type StepInstanceInfo int // Run executes the Packer build step that gathers GCE instance info. -func (s *stepInstanceInfo) Run(state multistep.StateBag) multistep.StepAction { - var ( - client = state.Get("client").(*GoogleComputeClient) - config = state.Get("config").(*Config) - ui = state.Get("ui").(packer.Ui) - ) +func (s *StepInstanceInfo) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + instanceName := state.Get("instance_name").(string) - err := waitForInstanceState("RUNNING", config.Zone, instanceName, client, config.stateTimeout) + + ui.Say("Waiting for the instance to become running...") + errCh := driver.WaitForInstance("RUNNING", config.Zone, instanceName) + var err error + select { + case err = <-errCh: + case <-time.After(config.stateTimeout): + err = errors.New("time out while waiting for instance to become running") + } + if err != nil { - err := fmt.Errorf("Error creating instance: %s", err) + err := fmt.Errorf("Error waiting for instance: %s", err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } - ip, err := client.GetNatIP(config.Zone, instanceName) + + ip, err := driver.GetNatIP(config.Zone, instanceName) if err != nil { err := fmt.Errorf("Error retrieving instance nat ip address: %s", err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } + + ui.Message(fmt.Sprintf("IP: %s", ip)) state.Put("instance_ip", ip) return multistep.ActionContinue } // Cleanup. -func (s *stepInstanceInfo) Cleanup(state multistep.StateBag) {} +func (s *StepInstanceInfo) Cleanup(state multistep.StateBag) {} diff --git a/builder/googlecompute/step_instance_info_test.go b/builder/googlecompute/step_instance_info_test.go new file mode 100644 index 000000000..8566ce722 --- /dev/null +++ b/builder/googlecompute/step_instance_info_test.go @@ -0,0 +1,134 @@ +package googlecompute + +import ( + "errors" + "github.com/mitchellh/multistep" + "testing" + "time" +) + +func TestStepInstanceInfo_impl(t *testing.T) { + var _ multistep.Step = new(StepInstanceInfo) +} + +func TestStepInstanceInfo(t *testing.T) { + state := testState(t) + step := new(StepInstanceInfo) + defer step.Cleanup(state) + + state.Put("instance_name", "foo") + + config := state.Get("config").(*Config) + driver := state.Get("driver").(*DriverMock) + driver.GetNatIPResult = "1.2.3.4" + + // run the step + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + + // Verify state + if driver.WaitForInstanceState != "RUNNING" { + t.Fatalf("bad: %#v", driver.WaitForInstanceState) + } + if driver.WaitForInstanceZone != config.Zone { + t.Fatalf("bad: %#v", driver.WaitForInstanceZone) + } + if driver.WaitForInstanceName != "foo" { + t.Fatalf("bad: %#v", driver.WaitForInstanceName) + } + + ipRaw, ok := state.GetOk("instance_ip") + if !ok { + t.Fatal("should have ip") + } + if ip, ok := ipRaw.(string); !ok { + t.Fatal("ip is not a string") + } else if ip != "1.2.3.4" { + t.Fatalf("bad ip: %s", ip) + } +} + +func TestStepInstanceInfo_getNatIPError(t *testing.T) { + state := testState(t) + step := new(StepInstanceInfo) + defer step.Cleanup(state) + + state.Put("instance_name", "foo") + + driver := state.Get("driver").(*DriverMock) + driver.GetNatIPErr = errors.New("error") + + // 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("instance_ip"); ok { + t.Fatal("should NOT have instance IP") + } +} + +func TestStepInstanceInfo_waitError(t *testing.T) { + state := testState(t) + step := new(StepInstanceInfo) + defer step.Cleanup(state) + + state.Put("instance_name", "foo") + + errCh := make(chan error, 1) + errCh <- errors.New("error") + + driver := state.Get("driver").(*DriverMock) + driver.WaitForInstanceErrCh = 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("instance_ip"); ok { + t.Fatal("should NOT have instance IP") + } +} + +func TestStepInstanceInfo_errorTimeout(t *testing.T) { + state := testState(t) + step := new(StepInstanceInfo) + defer step.Cleanup(state) + + errCh := make(chan error, 1) + go func() { + <-time.After(10 * time.Millisecond) + errCh <- nil + }() + + state.Put("instance_name", "foo") + + config := state.Get("config").(*Config) + config.stateTimeout = 1 * time.Microsecond + + driver := state.Get("driver").(*DriverMock) + driver.WaitForInstanceErrCh = 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("instance_ip"); ok { + t.Fatal("should NOT have instance IP") + } +}