From 33a84c09381e30d795a2b99e794101872e2def3f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Dec 2013 19:03:10 -0800 Subject: [PATCH] builder/googlecompute: StepRegisterImage --- builder/googlecompute/api.go | 128 ------------------ builder/googlecompute/builder.go | 4 +- builder/googlecompute/driver.go | 3 + builder/googlecompute/driver_gce.go | 43 ++++++ builder/googlecompute/driver_mock.go | 20 +++ builder/googlecompute/step_register_image.go | 46 ++++--- .../googlecompute/step_register_image_test.go | 100 ++++++++++++++ 7 files changed, 192 insertions(+), 152 deletions(-) create mode 100644 builder/googlecompute/step_register_image_test.go diff --git a/builder/googlecompute/api.go b/builder/googlecompute/api.go index 6657fdc28..46611f02e 100644 --- a/builder/googlecompute/api.go +++ b/builder/googlecompute/api.go @@ -1,12 +1,6 @@ package googlecompute import ( - "errors" - "net/http" - "strings" - - "code.google.com/p/goauth2/oauth" - "code.google.com/p/goauth2/oauth/jwt" "code.google.com/p/google-api-go-client/compute/v1beta16" ) @@ -18,118 +12,6 @@ type GoogleComputeClient struct { clientSecrets *clientSecrets } -// InstanceConfig represents a GCE instance configuration. -// Used for creating machine instances. -/* -type InstanceConfig struct { - Description string - Image string - MachineType string - Metadata *compute.Metadata - Name string - NetworkInterfaces []*compute.NetworkInterface - ServiceAccounts []*compute.ServiceAccount - Tags *compute.Tags -} -*/ - -// New initializes and returns a *GoogleComputeClient. -// -// The projectId must be the project name, i.e. myproject, not the project -// number. -func New(projectId string, zone string, c *clientSecrets, pemKey []byte) (*GoogleComputeClient, error) { - googleComputeClient := &GoogleComputeClient{ - ProjectId: projectId, - Zone: zone, - } - // Get the access token. - t := jwt.NewToken(c.Web.ClientEmail, "", pemKey) - t.ClaimSet.Aud = c.Web.TokenURI - httpClient := &http.Client{} - token, err := t.Assert(httpClient) - if err != nil { - return nil, err - } - config := &oauth.Config{ - ClientId: c.Web.ClientId, - Scope: "", - TokenURL: c.Web.TokenURI, - AuthURL: c.Web.AuthURI, - } - transport := &oauth.Transport{Config: config} - transport.Token = token - s, err := compute.New(transport.Client()) - if err != nil { - return nil, err - } - googleComputeClient.Service = s - return googleComputeClient, nil -} - -// CreateImage registers a GCE Image with a project. -func (g *GoogleComputeClient) CreateImage(name, description, sourceURL string) (*compute.Operation, error) { - imageRawDisk := &compute.ImageRawDisk{ - ContainerType: "TAR", - Source: sourceURL, - } - image := &compute.Image{ - Description: description, - Name: name, - RawDisk: imageRawDisk, - SourceType: "RAW", - } - imageInsertCall := g.Service.Images.Insert(g.ProjectId, image) - operation, err := imageInsertCall.Do() - if err != nil { - return nil, err - } - return operation, 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) - operation, err := zoneOperationsGetCall.Do() - if err != nil { - return "", err - } - if operation.Status == "DONE" { - err = processOperationStatus(operation) - if err != nil { - return operation.Status, err - } - } - return operation.Status, nil -} - -// GlobalOperationStatus returns the status for the named global operation. -func (g *GoogleComputeClient) GlobalOperationStatus(name string) (string, error) { - globalOperationsGetCall := g.Service.GlobalOperations.Get(g.ProjectId, name) - operation, err := globalOperationsGetCall.Do() - if err != nil { - return "", err - } - if operation.Status == "DONE" { - err = processOperationStatus(operation) - if err != nil { - return operation.Status, err - } - } - return operation.Status, nil -} - -// processOperationStatus extracts errors from the specified operation. -func processOperationStatus(o *compute.Operation) error { - if o.Error != nil { - messages := make([]string, len(o.Error.Errors)) - for _, e := range o.Error.Errors { - messages = append(messages, e.Message) - } - return errors.New(strings.Join(messages, "\n")) - } - return nil -} - // DeleteImage deletes the named image. Returns a Global Operation. func (g *GoogleComputeClient) DeleteImage(name string) (*compute.Operation, error) { imagesDeleteCall := g.Service.Images.Delete(g.ProjectId, name) @@ -139,13 +21,3 @@ func (g *GoogleComputeClient) DeleteImage(name string) (*compute.Operation, erro } return operation, nil } - -// DeleteInstance deletes the named instance. Returns a Zone Operation. -func (g *GoogleComputeClient) DeleteInstance(zone, name string) (*compute.Operation, error) { - instanceDeleteCall := g.Service.Instances.Delete(g.ProjectId, zone, name) - operation, err := instanceDeleteCall.Do() - if err != nil { - return nil, err - } - return operation, nil -} diff --git a/builder/googlecompute/builder.go b/builder/googlecompute/builder.go index 3cbab24a5..6ac294d6a 100644 --- a/builder/googlecompute/builder.go +++ b/builder/googlecompute/builder.go @@ -61,10 +61,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe new(StepUpdateGsutil), new(StepCreateImage), new(StepUploadImage), + new(StepRegisterImage), } - /* - new(stepRegisterImage), - }*/ // Run the steps. if b.config.PackerDebug { diff --git a/builder/googlecompute/driver.go b/builder/googlecompute/driver.go index 1f7030e67..fc8f010c5 100644 --- a/builder/googlecompute/driver.go +++ b/builder/googlecompute/driver.go @@ -4,6 +4,9 @@ package googlecompute // with GCE. The Driver interface exists mostly to allow a mock implementation // to be used to test the steps. type Driver interface { + // CreateImage creates an image with the given URL in Google Storage. + CreateImage(name, description, url string) <-chan error + // DeleteInstance deletes the given instance. DeleteInstance(zone, name string) (<-chan error, error) diff --git a/builder/googlecompute/driver_gce.go b/builder/googlecompute/driver_gce.go index bc6363aba..c464ee352 100644 --- a/builder/googlecompute/driver_gce.go +++ b/builder/googlecompute/driver_gce.go @@ -59,6 +59,28 @@ func NewDriverGCE(ui packer.Ui, projectId string, c *clientSecrets, key []byte) }, nil } +func (d *driverGCE) CreateImage(name, description, url string) <-chan error { + image := &compute.Image{ + Description: description, + Name: name, + RawDisk: &compute.ImageRawDisk{ + ContainerType: "TAR", + Source: url, + }, + SourceType: "RAW", + } + + errCh := make(chan error, 1) + op, err := d.service.Images.Insert(d.projectId, image).Do() + if err != nil { + errCh <- err + } else { + go waitForState(errCh, "DONE", d.refreshGlobalOp(op)) + } + + return errCh +} + func (d *driverGCE) DeleteInstance(zone, name string) (<-chan error, error) { op, err := d.service.Instances.Delete(d.projectId, zone, name).Do() if err != nil { @@ -210,6 +232,27 @@ func (d *driverGCE) refreshInstanceState(zone, name string) stateRefreshFunc { } } +func (d *driverGCE) refreshGlobalOp(op *compute.Operation) stateRefreshFunc { + return func() (string, error) { + newOp, err := d.service.GlobalOperations.Get(d.projectId, op.Name).Do() + if err != nil { + return "", err + } + + // If the op is done, check for errors + err = nil + if newOp.Status == "DONE" { + if newOp.Error != nil { + for _, e := range newOp.Error.Errors { + err = packer.MultiErrorAppend(err, fmt.Errorf(e.Message)) + } + } + } + + return newOp.Status, err + } +} + 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 13dfa0712..87d2528b7 100644 --- a/builder/googlecompute/driver_mock.go +++ b/builder/googlecompute/driver_mock.go @@ -3,6 +3,11 @@ package googlecompute // DriverMock is a Driver implementation that is a mocked out so that // it can be used for tests. type DriverMock struct { + CreateImageName string + CreateImageDesc string + CreateImageURL string + CreateImageErrCh <-chan error + DeleteInstanceZone string DeleteInstanceName string DeleteInstanceErrCh <-chan error @@ -23,6 +28,21 @@ type DriverMock struct { WaitForInstanceErrCh <-chan error } +func (d *DriverMock) CreateImage(name, description, url string) <-chan error { + d.CreateImageName = name + d.CreateImageDesc = description + d.CreateImageURL = url + + resultCh := d.CreateImageErrCh + if resultCh == nil { + ch := make(chan error) + close(ch) + resultCh = ch + } + + return resultCh +} + func (d *DriverMock) DeleteInstance(zone, name string) (<-chan error, error) { d.DeleteInstanceZone = zone d.DeleteInstanceName = name diff --git a/builder/googlecompute/step_register_image.go b/builder/googlecompute/step_register_image.go index 5e8087921..84b2a5894 100644 --- a/builder/googlecompute/step_register_image.go +++ b/builder/googlecompute/step_register_image.go @@ -1,42 +1,46 @@ 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 +// 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 { - var ( - client = state.Get("client").(*GoogleComputeClient) - config = state.Get("config").(*Config) - ui = state.Get("ui").(packer.Ui) - ) - ui.Say("Adding image to the project...") - imageURL := fmt.Sprintf("https://storage.cloud.google.com/%s/%s.tar.gz", config.BucketName, config.ImageName) - operation, err := client.CreateImage(config.ImageName, config.ImageDescription, imageURL) +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 creating image: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - ui.Say("Waiting for image to become available...") - err = waitForGlobalOperationState("DONE", operation.Name, client, config.stateTimeout) - if err != nil { - err := fmt.Errorf("Error creating image: %s", err) + 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) {} +func (s *StepRegisterImage) Cleanup(state multistep.StateBag) {} diff --git a/builder/googlecompute/step_register_image_test.go b/builder/googlecompute/step_register_image_test.go new file mode 100644 index 000000000..5faa89b2c --- /dev/null +++ b/builder/googlecompute/step_register_image_test.go @@ -0,0 +1,100 @@ +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") + } +}