builder/googlecompute: driver and create instance tests

This commit is contained in:
Mitchell Hashimoto 2013-12-12 21:38:34 -08:00
parent 3657f33a4d
commit 2bd6f1e2d7
9 changed files with 435 additions and 282 deletions

View File

@ -20,6 +20,7 @@ type GoogleComputeClient struct {
// InstanceConfig represents a GCE instance configuration.
// Used for creating machine instances.
/*
type InstanceConfig struct {
Description string
Image string
@ -30,6 +31,7 @@ type InstanceConfig struct {
ServiceAccounts []*compute.ServiceAccount
Tags *compute.Tags
}
*/
// New initializes and returns a *GoogleComputeClient.
//
@ -41,7 +43,7 @@ func New(projectId string, zone string, c *clientSecrets, pemKey []byte) (*Googl
Zone: zone,
}
// Get the access token.
t := jwt.NewToken(c.Web.ClientEmail, scopes(), pemKey)
t := jwt.NewToken(c.Web.ClientEmail, "", pemKey)
t.ClaimSet.Aud = c.Web.TokenURI
httpClient := &http.Client{}
token, err := t.Assert(httpClient)
@ -50,7 +52,7 @@ func New(projectId string, zone string, c *clientSecrets, pemKey []byte) (*Googl
}
config := &oauth.Config{
ClientId: c.Web.ClientId,
Scope: scopes(),
Scope: "",
TokenURL: c.Web.TokenURI,
AuthURL: c.Web.AuthURI,
}
@ -64,83 +66,6 @@ func New(projectId string, zone string, c *clientSecrets, pemKey []byte) (*Googl
return googleComputeClient, nil
}
// GetZone returns a *compute.Zone representing the named zone.
func (g *GoogleComputeClient) GetZone(name string) (*compute.Zone, error) {
zoneGetCall := g.Service.Zones.Get(g.ProjectId, name)
zone, err := zoneGetCall.Do()
if err != nil {
return nil, err
}
return zone, nil
}
// GetMachineType returns a *compute.MachineType representing the named machine type.
func (g *GoogleComputeClient) GetMachineType(name, zone string) (*compute.MachineType, error) {
machineTypesGetCall := g.Service.MachineTypes.Get(g.ProjectId, zone, name)
machineType, err := machineTypesGetCall.Do()
if err != nil {
return nil, err
}
if machineType.Deprecated == nil {
return machineType, nil
}
return nil, errors.New("Machine Type does not exist: " + name)
}
// GetImage returns a *compute.Image representing the named image.
func (g *GoogleComputeClient) GetImage(name string) (*compute.Image, error) {
var err error
var image *compute.Image
projects := []string{g.ProjectId, "debian-cloud", "centos-cloud"}
for _, project := range projects {
imagesGetCall := g.Service.Images.Get(project, name)
image, err = imagesGetCall.Do()
if image != nil {
break
}
}
if err != nil {
return nil, err
}
if image != nil {
if image.SelfLink != "" {
return image, nil
}
}
return nil, errors.New("Image does not exist: " + name)
}
// GetNetwork returns a *compute.Network representing the named network.
func (g *GoogleComputeClient) GetNetwork(name string) (*compute.Network, error) {
networkGetCall := g.Service.Networks.Get(g.ProjectId, name)
network, err := networkGetCall.Do()
if err != nil {
return nil, err
}
return network, nil
}
// CreateInstance creates an instance in Google Compute Engine based on the
// supplied instanceConfig.
func (g *GoogleComputeClient) CreateInstance(zone string, instanceConfig *InstanceConfig) (*compute.Operation, error) {
instance := &compute.Instance{
Description: instanceConfig.Description,
Image: instanceConfig.Image,
MachineType: instanceConfig.MachineType,
Metadata: instanceConfig.Metadata,
Name: instanceConfig.Name,
NetworkInterfaces: instanceConfig.NetworkInterfaces,
ServiceAccounts: instanceConfig.ServiceAccounts,
Tags: instanceConfig.Tags,
}
instanceInsertCall := g.Service.Instances.Insert(g.ProjectId, zone, instance)
operation, err := instanceInsertCall.Do()
if err != nil {
return nil, err
}
return operation, nil
}
// InstanceStatus returns a string representing the status of the named instance.
// Status will be one of: "PROVISIONING", "STAGING", "RUNNING", "STOPPING",
// "STOPPED", "TERMINATED".
@ -256,59 +181,3 @@ func (g *GoogleComputeClient) DeleteInstance(zone, name string) (*compute.Operat
}
return operation, nil
}
// NewNetworkInterface returns a *compute.NetworkInterface based on the data provided.
func NewNetworkInterface(network *compute.Network, public bool) *compute.NetworkInterface {
accessConfigs := make([]*compute.AccessConfig, 0)
if public {
c := &compute.AccessConfig{
Name: "AccessConfig created by Packer",
Type: "ONE_TO_ONE_NAT",
}
accessConfigs = append(accessConfigs, c)
}
return &compute.NetworkInterface{
AccessConfigs: accessConfigs,
Network: network.SelfLink,
}
}
// NewServiceAccount returns a *compute.ServiceAccount with permissions required
// for creating GCE machine images.
func NewServiceAccount(email string) *compute.ServiceAccount {
return &compute.ServiceAccount{
Email: email,
Scopes: []string{
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/compute",
"https://www.googleapis.com/auth/devstorage.full_control",
},
}
}
// MapToMetadata converts a map[string]string to a *compute.Metadata.
func MapToMetadata(metadata map[string]string) *compute.Metadata {
items := make([]*compute.MetadataItems, len(metadata))
for k, v := range metadata {
items = append(items, &compute.MetadataItems{k, v})
}
return &compute.Metadata{
Items: items,
}
}
// SliceToTags converts a []string to a *compute.Tags.
func SliceToTags(tags []string) *compute.Tags {
return &compute.Tags{
Items: tags,
}
}
// scopes return a space separated list of scopes.
func scopes() string {
s := []string{
"https://www.googleapis.com/auth/compute",
"https://www.googleapis.com/auth/devstorage.full_control",
}
return strings.Join(s, " ")
}

View File

@ -51,7 +51,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Build the steps.
steps := []multistep.Step{
new(StepCreateSSHKey),
new(stepCreateInstance),
new(StepCreateInstance),
new(stepInstanceInfo),
&common.StepConnectSSH{
SSHAddress: sshAddress,

View File

@ -0,0 +1,20 @@
package googlecompute
// Driver is the interface that has to be implemented to communicate
// with GCE. The Driver interface exists mostly to allow a mock implementation
// to be used to test the steps.
type Driver interface {
// RunInstance takes the given config and launches an instance.
RunInstance(*InstanceConfig) (<-chan error, error)
}
type InstanceConfig struct {
Description string
Image string
MachineType string
Metadata map[string]string
Name string
Network string
Tags []string
Zone string
}

View File

@ -0,0 +1,199 @@
package googlecompute
import (
"fmt"
"net/http"
"time"
"code.google.com/p/goauth2/oauth"
"code.google.com/p/goauth2/oauth/jwt"
"code.google.com/p/google-api-go-client/compute/v1beta16"
"github.com/mitchellh/packer/packer"
)
// driverGCE is a Driver implementation that actually talks to GCE.
// Create an instance using NewDriverGCE.
type driverGCE struct {
projectId string
service *compute.Service
ui packer.Ui
}
const DriverScopes string = "https://www.googleapis.com/auth/compute " +
"https://www.googleapis.com/auth/devstorage.full_control"
func NewDriverGCE(ui packer.Ui, projectId string, c *clientSecrets, key []byte) (Driver, error) {
jwtTok := jwt.NewToken(c.Web.ClientEmail, DriverScopes, key)
jwtTok.ClaimSet.Aud = c.Web.TokenURI
token, err := jwtTok.Assert(new(http.Client))
if err != nil {
return nil, err
}
transport := &oauth.Transport{
Config: &oauth.Config{
ClientId: c.Web.ClientId,
Scope: DriverScopes,
TokenURL: c.Web.TokenURI,
AuthURL: c.Web.AuthURI,
},
Token: token,
}
service, err := compute.New(transport.Client())
if err != nil {
return nil, err
}
return &driverGCE{
projectId: projectId,
service: service,
ui: ui,
}, nil
}
func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
// Get the zone
d.ui.Message(fmt.Sprintf("Loading zone: %s", c.Zone))
zone, err := d.service.Zones.Get(d.projectId, c.Zone).Do()
if err != nil {
return nil, err
}
// Get the image
d.ui.Message(fmt.Sprintf("Loading image: %s", c.Image))
image, err := d.getImage(c.Image)
if err != nil {
return nil, err
}
// Get the machine type
d.ui.Message(fmt.Sprintf("Loading machine type: %s", c.MachineType))
machineType, err := d.service.MachineTypes.Get(
d.projectId, zone.Name, c.MachineType).Do()
if err != nil {
return nil, err
}
// TODO(mitchellh): deprecation warnings
// Get the network
d.ui.Message(fmt.Sprintf("Loading network: %s", c.Network))
network, err := d.service.Networks.Get(d.projectId, c.Network).Do()
if err != nil {
return nil, err
}
// Build up the metadata
metadata := make([]*compute.MetadataItems, len(c.Metadata))
for k, v := range c.Metadata {
metadata = append(metadata, &compute.MetadataItems{
Key: k,
Value: v,
})
}
// Create the instance information
instance := compute.Instance{
Description: c.Description,
Image: image.SelfLink,
MachineType: machineType.SelfLink,
Metadata: &compute.Metadata{
Items: metadata,
},
Name: c.Name,
NetworkInterfaces: []*compute.NetworkInterface{
&compute.NetworkInterface{
AccessConfigs: []*compute.AccessConfig{
&compute.AccessConfig{
Name: "AccessConfig created by Packer",
Type: "ONE_TO_ONE_NAT",
},
},
Network: network.SelfLink,
},
},
ServiceAccounts: []*compute.ServiceAccount{
&compute.ServiceAccount{
Email: "default",
Scopes: []string{
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/compute",
"https://www.googleapis.com/auth/devstorage.full_control",
},
},
},
Tags: &compute.Tags{
Items: c.Tags,
},
}
d.ui.Message("Requesting instance creation...")
op, err := d.service.Instances.Insert(d.projectId, zone.Name, &instance).Do()
if err != nil {
return nil, err
}
errCh := make(chan error, 1)
go waitForState(errCh, "DONE", d.refreshZoneOp(op))
return errCh, nil
}
func (d *driverGCE) getImage(name string) (image *compute.Image, err error) {
projects := []string{d.projectId, "debian-cloud", "centos-cloud"}
for _, project := range projects {
image, err = d.service.Images.Get(project, name).Do()
if err == nil && image != nil && image.SelfLink != "" {
return
}
image = nil
}
if err == nil {
err = fmt.Errorf("Image could not be found: %s", name)
}
return
}
func (d *driverGCE) refreshZoneOp(op *compute.Operation) stateRefreshFunc {
return func() (string, error) {
newOp, err := d.service.ZoneOperations.Get(d.projectId, op.Zone, 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
}
}
// stateRefreshFunc is used to refresh the state of a thing and is
// used in conjunction with waitForState.
type stateRefreshFunc func() (string, error)
// waitForState will spin in a loop forever waiting for state to
// reach a certain target.
func waitForState(errCh chan<- error, target string, refresh stateRefreshFunc) {
for {
state, err := refresh()
if err != nil {
errCh <- err
return
}
if state == target {
errCh <- nil
return
}
time.Sleep(2 * time.Second)
}
}

View File

@ -0,0 +1,22 @@
package googlecompute
// DriverMock is a Driver implementation that is a mocked out so that
// it can be used for tests.
type DriverMock struct {
RunInstanceConfig *InstanceConfig
RunInstanceErrCh <-chan error
RunInstanceErr error
}
func (d *DriverMock) RunInstance(c *InstanceConfig) (<-chan error, error) {
d.RunInstanceConfig = c
resultCh := d.RunInstanceErrCh
if resultCh == nil {
ch := make(chan error)
close(ch)
resultCh = ch
}
return resultCh, d.RunInstanceErr
}

View File

@ -1,131 +1,93 @@
package googlecompute
import (
"errors"
"fmt"
"time"
"code.google.com/p/google-api-go-client/compute/v1beta16"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common/uuid"
"github.com/mitchellh/packer/packer"
)
// stepCreateInstance represents a Packer build step that creates GCE instances.
type stepCreateInstance struct {
// StepCreateInstance represents a Packer build step that creates GCE instances.
type StepCreateInstance struct {
instanceName string
}
// Run executes the Packer build step that creates a GCE instance.
func (s *stepCreateInstance) 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 *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
driver := state.Get("driver").(Driver)
sshPublicKey := state.Get("ssh_public_key").(string)
ui := state.Get("ui").(packer.Ui)
ui.Say("Creating instance...")
name := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
// Build up the instance config.
instanceConfig := &InstanceConfig{
Description: "New instance created by Packer",
Name: name,
}
// Validate the zone.
zone, err := client.GetZone(config.Zone)
if err != nil {
err := fmt.Errorf("Error creating instance: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Set the source image. Must be a fully-qualified URL.
image, err := client.GetImage(config.SourceImage)
if err != nil {
err := fmt.Errorf("Error creating instance: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
instanceConfig.Image = image.SelfLink
// Set the machineType. Must be a fully-qualified URL.
machineType, err := client.GetMachineType(config.MachineType, zone.Name)
if err != nil {
err := fmt.Errorf("Error creating instance: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
errCh, err := driver.RunInstance(&InstanceConfig{
Description: "New instance created by Packer",
Image: config.SourceImage,
MachineType: config.MachineType,
Metadata: map[string]string{
"sshKeys": fmt.Sprintf("%s:%s", config.SSHUsername, sshPublicKey),
},
Name: name,
Network: config.Network,
Tags: config.Tags,
Zone: config.Zone,
})
if err == nil {
select {
case err = <-errCh:
case <-time.After(config.stateTimeout):
err = errors.New("time out while waiting for instance to create")
}
}
instanceConfig.MachineType = machineType.SelfLink
// Set up the Network Interface.
network, err := client.GetNetwork(config.Network)
if err != nil {
err := fmt.Errorf("Error creating instance: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
networkInterface := NewNetworkInterface(network, true)
networkInterfaces := []*compute.NetworkInterface{
networkInterface,
}
instanceConfig.NetworkInterfaces = networkInterfaces
// Add the metadata, which also setups up the ssh key.
metadata := make(map[string]string)
sshPublicKey := state.Get("ssh_public_key").(string)
metadata["sshKeys"] = fmt.Sprintf("%s:%s", config.SSHUsername, sshPublicKey)
instanceConfig.Metadata = MapToMetadata(metadata)
// Add the default service so we can create an image of the machine and
// upload it to cloud storage.
defaultServiceAccount := NewServiceAccount("default")
serviceAccounts := []*compute.ServiceAccount{
defaultServiceAccount,
}
instanceConfig.ServiceAccounts = serviceAccounts
// Create the instance based on configuration
operation, err := client.CreateInstance(zone.Name, instanceConfig)
if err != nil {
err := fmt.Errorf("Error creating instance: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Waiting for the instance to be created...")
err = waitForZoneOperationState("DONE", config.Zone, operation.Name, client, config.stateTimeout)
if err != nil {
err := fmt.Errorf("Error creating instance: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Update the state.
ui.Message("Instance has been created!")
// Things succeeded, store the name so we can remove it later
state.Put("instance_name", name)
s.instanceName = name
return multistep.ActionContinue
}
// Cleanup destroys the GCE instance created during the image creation process.
func (s *stepCreateInstance) Cleanup(state multistep.StateBag) {
var (
client = state.Get("client").(*GoogleComputeClient)
config = state.Get("config").(*Config)
ui = state.Get("ui").(packer.Ui)
)
func (s *StepCreateInstance) Cleanup(state multistep.StateBag) {
if s.instanceName == "" {
return
}
ui.Say("Destroying instance...")
operation, err := client.DeleteInstance(config.Zone, s.instanceName)
if err != nil {
ui.Error(fmt.Sprintf("Error destroying instance. Please destroy it manually: %v", s.instanceName))
}
ui.Say("Waiting for the instance to be deleted...")
for {
status, err := client.ZoneOperationStatus(config.Zone, operation.Name)
/*
var (
client = state.Get("client").(*GoogleComputeClient)
config = state.Get("config").(*Config)
ui = state.Get("ui").(packer.Ui)
)
ui.Say("Destroying instance...")
operation, err := client.DeleteInstance(config.Zone, s.instanceName)
if err != nil {
ui.Error(fmt.Sprintf("Error destroying instance. Please destroy it manually: %v", s.instanceName))
}
if status == "DONE" {
break
ui.Say("Waiting for the instance to be deleted...")
for {
status, err := client.ZoneOperationStatus(config.Zone, operation.Name)
if err != nil {
ui.Error(fmt.Sprintf("Error destroying instance. Please destroy it manually: %v", s.instanceName))
}
if status == "DONE" {
break
}
}
}
*/
return
}

View File

@ -0,0 +1,114 @@
package googlecompute
import (
"errors"
"github.com/mitchellh/multistep"
"testing"
"time"
)
func TestStepCreateInstance_impl(t *testing.T) {
var _ multistep.Step = new(StepCreateInstance)
}
func TestStepCreateInstance(t *testing.T) {
state := testState(t)
step := new(StepCreateInstance)
defer step.Cleanup(state)
state.Put("ssh_public_key", "key")
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
// Verify state
if _, ok := state.GetOk("instance_name"); !ok {
t.Fatal("should have instance name")
}
}
func TestStepCreateInstance_error(t *testing.T) {
state := testState(t)
step := new(StepCreateInstance)
defer step.Cleanup(state)
state.Put("ssh_public_key", "key")
driver := state.Get("driver").(*DriverMock)
driver.RunInstanceErr = 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_name"); ok {
t.Fatal("should NOT have instance name")
}
}
func TestStepCreateInstance_errorOnChannel(t *testing.T) {
state := testState(t)
step := new(StepCreateInstance)
defer step.Cleanup(state)
errCh := make(chan error, 1)
errCh <- errors.New("error")
state.Put("ssh_public_key", "key")
driver := state.Get("driver").(*DriverMock)
driver.RunInstanceErrCh = 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_name"); ok {
t.Fatal("should NOT have instance name")
}
}
func TestStepCreateInstance_errorTimeout(t *testing.T) {
state := testState(t)
step := new(StepCreateInstance)
defer step.Cleanup(state)
errCh := make(chan error, 1)
go func() {
<-time.After(10 * time.Millisecond)
errCh <- nil
}()
state.Put("ssh_public_key", "key")
config := state.Get("config").(*Config)
config.stateTimeout = 1 * time.Microsecond
driver := state.Get("driver").(*DriverMock)
driver.RunInstanceErrCh = 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_name"); ok {
t.Fatal("should NOT have instance name")
}
}

View File

@ -10,6 +10,7 @@ import (
func testState(t *testing.T) multistep.StateBag {
state := new(multistep.BasicStateBag)
state.Put("config", testConfigStruct(t))
state.Put("driver", &DriverMock{})
state.Put("hook", &packer.MockHook{})
state.Put("ui", &packer.BasicUi{
Reader: new(bytes.Buffer),

View File

@ -1,72 +1,38 @@
package googlecompute
import (
"fmt"
"log"
"time"
)
// statusFunc.
type statusFunc func() (string, error)
// waitForInstanceState.
func waitForInstanceState(desiredState string, zone string, name string, client *GoogleComputeClient, timeout time.Duration) error {
f := func() (string, error) {
return client.InstanceStatus(zone, name)
}
return waitForState("instance", desiredState, f, timeout)
return nil
/*
f := func() (string, error) {
return client.InstanceStatus(zone, name)
}
return waitForState("instance", desiredState, f, timeout)
*/
}
// waitForZoneOperationState.
func waitForZoneOperationState(desiredState string, zone string, name string, client *GoogleComputeClient, timeout time.Duration) error {
f := func() (string, error) {
return client.ZoneOperationStatus(zone, name)
}
return waitForState("operation", desiredState, f, timeout)
return nil
/*
f := func() (string, error) {
return client.ZoneOperationStatus(zone, name)
}
return waitForState("operation", desiredState, f, timeout)
*/
}
// waitForGlobalOperationState.
func waitForGlobalOperationState(desiredState string, name string, client *GoogleComputeClient, timeout time.Duration) error {
f := func() (string, error) {
return client.GlobalOperationStatus(name)
}
return waitForState("operation", desiredState, f, timeout)
}
// waitForState.
func waitForState(kind string, desiredState string, f statusFunc, timeout time.Duration) error {
done := make(chan struct{})
defer close(done)
result := make(chan error, 1)
go func() {
attempts := 0
for {
attempts += 1
log.Printf("Checking %s state... (attempt: %d)", kind, attempts)
status, err := f()
if err != nil {
result <- err
return
}
if status == desiredState {
result <- nil
return
}
time.Sleep(3 * time.Second)
select {
case <-done:
return
default:
continue
}
/*
f := func() (string, error) {
return client.GlobalOperationStatus(name)
}
}()
log.Printf("Waiting for up to %d seconds for %s to become %s", timeout, kind, desiredState)
select {
case err := <-result:
return err
case <-time.After(timeout):
err := fmt.Errorf("Timeout while waiting to for the %s to become '%s'", kind, desiredState)
return err
}
return waitForState("operation", desiredState, f, timeout)
*/
return nil
}