builder/googlecompute: StepRegisterImage

This commit is contained in:
Mitchell Hashimoto 2013-12-13 19:03:10 -08:00
parent 587f057bf6
commit 33a84c0938
7 changed files with 192 additions and 152 deletions

View File

@ -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
}

View File

@ -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 {

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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) {}

View File

@ -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")
}
}