Some googlecompute fixes and cleanup. Addresses https://github.com/mitchellh/packer/issues/3829. Changes:

- startup scripts don't run for Windows since it is isn't implemented yet.
- startup scripts use instance metadata instead of serial port output to flag when they are done.
- added licenses to Image data type (to check if an Image is a  Windows Image).
- added GetImage and GetImageFromProject to googlecompute Drivers.
- changed some of the builder/googlecompute tests to use github.com/stretchr/testify/assert.

Tests:
- (in the Packer directory) `go test .`, `go test ./builder/googlecompute`, and `go test ./post-processor/googlecompute-export`
- manual run of `packer build packer_template.json` with the following files

--packer_template.json--
{
  "builders": [
    {
      "type": "googlecompute",
      "account_file": "creds.json",
      "project_id": "google.com:packer-test",
      "source_image": "debian-8-jessie-v20160629",
      "zone": "us-central1-a",
      "startup_script_file": "startup_script.sh",
      "metadata": {
        "startup-script": "#!/bin/sh\necho \"This should be overwritten.\"",
        "startup-script-log-dest": "gs://packer-test.google.com.a.appspot.com/startup-script.log"
      },
      "image_name": "test-packer-modifications",
      "ssh_username": "foo"
    }
  ],
  "post-processors": [
    {
      "type": "googlecompute-export",
      "paths": [
        "gs://packer-test.google.com.a.appspot.com/foo.tar.gz",
        "gs://packer-test.google.com.a.appspot.com/bar.tar.gz"
      ],
      "keep_input_artifact": true
    }
  ]
}

--startup_script.sh--
\#!/bin/sh
echo "Hi, my name is Scott. I'm waiting 60 seconds!" >> /scott
sleep 60
echo "I'm done waiting!" >> /scott
This commit is contained in:
Scott Crunkleton 2016-09-07 19:00:30 -07:00 committed by Scott Crunkleton
parent 9dc7ce52cf
commit b54b82d3ac
16 changed files with 378 additions and 268 deletions

View File

@ -7,7 +7,7 @@ import (
// Artifact represents a GCE image as the result of a Packer build. // Artifact represents a GCE image as the result of a Packer build.
type Artifact struct { type Artifact struct {
image Image image *Image
driver Driver driver Driver
config *Config config *Config
} }

View File

@ -93,7 +93,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
} }
artifact := &Artifact{ artifact := &Artifact{
image: state.Get("image").(Image), image: state.Get("image").(*Image),
driver: driver, driver: driver,
config: b.config, config: b.config,
} }

View File

@ -4,13 +4,9 @@ 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 {
// ImageExists returns true if the specified image exists. If an error
// occurs calling the API, this method returns false.
ImageExists(name string) bool
// CreateImage creates an image from the given disk in Google Compute // CreateImage creates an image from the given disk in Google Compute
// Engine. // Engine.
CreateImage(name, description, family, zone, disk string) (<-chan Image, <-chan error) CreateImage(name, description, family, zone, disk string) (<-chan *Image, <-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
@ -21,6 +17,15 @@ type Driver interface {
// DeleteDisk deletes the disk with the given name. // DeleteDisk deletes the disk with the given name.
DeleteDisk(zone, name string) (<-chan error, error) DeleteDisk(zone, name string) (<-chan error, error)
// GetImage gets an image; tries the default and public projects.
GetImage(name string) (*Image, error)
// GetImageFromProject gets an image from a specific project.
GetImageFromProject(project, name string) (*Image, error)
// GetInstanceMetadata gets a metadata variable for the instance, name.
GetInstanceMetadata(zone, name, key string) (string, error)
// GetInternalIP gets the GCE-internal IP address for the instance. // GetInternalIP gets the GCE-internal IP address for the instance.
GetInternalIP(zone, name string) (string, error) GetInternalIP(zone, name string) (string, error)
@ -30,6 +35,10 @@ type Driver interface {
// GetSerialPortOutput gets the Serial Port contents for the instance. // GetSerialPortOutput gets the Serial Port contents for the instance.
GetSerialPortOutput(zone, name string) (string, error) GetSerialPortOutput(zone, name string) (string, error)
// ImageExists returns true if the specified image exists. If an error
// occurs calling the API, this method returns false.
ImageExists(name string) bool
// RunInstance takes the given config and launches an instance. // RunInstance takes the given config and launches an instance.
RunInstance(*InstanceConfig) (<-chan error, error) RunInstance(*InstanceConfig) (<-chan error, error)
@ -37,18 +46,12 @@ type Driver interface {
WaitForInstance(state, zone, name string) <-chan error WaitForInstance(state, zone, name string) <-chan error
} }
type Image struct {
Name string
ProjectId string
SizeGb int64
}
type InstanceConfig struct { type InstanceConfig struct {
Address string Address string
Description string Description string
DiskSizeGb int64 DiskSizeGb int64
DiskType string DiskType string
Image Image Image *Image
MachineType string MachineType string
Metadata map[string]string Metadata map[string]string
Name string Name string

View File

@ -5,6 +5,7 @@ import (
"log" "log"
"net/http" "net/http"
"runtime" "runtime"
"strings"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/version" "github.com/mitchellh/packer/version"
@ -13,7 +14,6 @@ import (
"golang.org/x/oauth2/google" "golang.org/x/oauth2/google"
"golang.org/x/oauth2/jwt" "golang.org/x/oauth2/jwt"
"google.golang.org/api/compute/v1" "google.golang.org/api/compute/v1"
"strings"
) )
// driverGCE is a Driver implementation that actually talks to GCE. // driverGCE is a Driver implementation that actually talks to GCE.
@ -88,15 +88,8 @@ func NewDriverGCE(ui packer.Ui, p string, a *AccountFile) (Driver, error) {
}, nil }, nil
} }
func (d *driverGCE) ImageExists(name string) bool { func (d *driverGCE) CreateImage(name, description, family, zone, disk string) (<-chan *Image, <-chan error) {
_, err := d.service.Images.Get(d.projectId, name).Do() gce_image := &compute.Image{
// The API may return an error for reasons other than the image not
// existing, but this heuristic is sufficient for now.
return err == nil
}
func (d *driverGCE) CreateImage(name, description, family, zone, disk string) (<-chan Image, <-chan error) {
image := &compute.Image{
Description: description, Description: description,
Name: name, Name: name,
Family: family, Family: family,
@ -104,9 +97,9 @@ func (d *driverGCE) CreateImage(name, description, family, zone, disk string) (<
SourceType: "RAW", SourceType: "RAW",
} }
imageCh := make(chan Image, 1) imageCh := make(chan *Image, 1)
errCh := make(chan error, 1) errCh := make(chan error, 1)
op, err := d.service.Images.Insert(d.projectId, image).Do() op, err := d.service.Images.Insert(d.projectId, gce_image).Do()
if err != nil { if err != nil {
errCh <- err errCh <- err
} else { } else {
@ -114,17 +107,17 @@ func (d *driverGCE) CreateImage(name, description, family, zone, disk string) (<
err = waitForState(errCh, "DONE", d.refreshGlobalOp(op)) err = waitForState(errCh, "DONE", d.refreshGlobalOp(op))
if err != nil { if err != nil {
close(imageCh) close(imageCh)
errCh <- err
return
} }
image, err = d.getImage(name, d.projectId) var image *Image
image, err = d.GetImageFromProject(d.projectId, name)
if err != nil { if err != nil {
close(imageCh) close(imageCh)
errCh <- err errCh <- err
return
} }
imageCh <- Image{ imageCh <- image
Name: name,
ProjectId: d.projectId,
SizeGb: image.DiskSizeGb,
}
close(imageCh) close(imageCh)
}() }()
} }
@ -166,6 +159,57 @@ func (d *driverGCE) DeleteDisk(zone, name string) (<-chan error, error) {
return errCh, nil return errCh, nil
} }
func (d *driverGCE) GetImage(name string) (*Image, error) {
projects := []string{d.projectId, "centos-cloud", "coreos-cloud", "debian-cloud", "google-containers", "opensuse-cloud", "rhel-cloud", "suse-cloud", "ubuntu-os-cloud", "windows-cloud"}
var errs error
for _, project := range projects {
image, err := d.GetImageFromProject(project, name)
if err != nil {
errs = packer.MultiErrorAppend(errs, err)
}
if image != nil {
return image, nil
}
}
return nil, fmt.Errorf(
"Could not find image, %s, in projects, %s: %s", name,
projects, errs)
}
func (d *driverGCE) GetImageFromProject(project, name string) (*Image, error) {
image, err := d.service.Images.Get(project, name).Do()
if err != nil {
return nil, err
} else if image == nil || image.SelfLink == "" {
return nil, fmt.Errorf("Image, %s, could not be found in project: %s", name, project)
} else {
return &Image{
Licenses: image.Licenses,
Name: image.Name,
ProjectId: project,
SelfLink: image.SelfLink,
SizeGb: image.DiskSizeGb,
}, nil
}
}
func (d *driverGCE) GetInstanceMetadata(zone, name, key string) (string, error) {
instance, err := d.service.Instances.Get(d.projectId, zone, name).Do()
if err != nil {
return "", err
}
for _, item := range instance.Metadata.Items {
if item.Key == key {
return *item.Value, nil
}
}
return "", fmt.Errorf("Instance metadata key, %s, not found.", key)
}
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 {
@ -211,6 +255,13 @@ func (d *driverGCE) GetSerialPortOutput(zone, name string) (string, error) {
return output.Contents, nil return output.Contents, nil
} }
func (d *driverGCE) ImageExists(name string) bool {
_, err := d.GetImageFromProject(d.projectId, name)
// The API may return an error for reasons other than the image not
// existing, but this heuristic is sufficient for now.
return err == nil
}
func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) { func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
// Get the zone // Get the zone
d.ui.Message(fmt.Sprintf("Loading zone: %s", c.Zone)) d.ui.Message(fmt.Sprintf("Loading zone: %s", c.Zone))
@ -219,13 +270,6 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
return nil, err return nil, err
} }
// Get the image
d.ui.Message(fmt.Sprintf("Loading image: %s in project %s", c.Image.Name, c.Image.ProjectId))
image, err := d.getImage(c.Image.Name, c.Image.ProjectId)
if err != nil {
return nil, err
}
// Get the machine type // Get the machine type
d.ui.Message(fmt.Sprintf("Loading machine type: %s", c.MachineType)) d.ui.Message(fmt.Sprintf("Loading machine type: %s", c.MachineType))
machineType, err := d.service.MachineTypes.Get( machineType, err := d.service.MachineTypes.Get(
@ -302,7 +346,7 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
Boot: true, Boot: true,
AutoDelete: false, AutoDelete: false,
InitializeParams: &compute.AttachedDiskInitializeParams{ InitializeParams: &compute.AttachedDiskInitializeParams{
SourceImage: image.SelfLink, SourceImage: c.Image.SelfLink,
DiskSizeGb: c.DiskSizeGb, DiskSizeGb: c.DiskSizeGb,
DiskType: fmt.Sprintf("zones/%s/diskTypes/%s", zone.Name, c.DiskType), DiskType: fmt.Sprintf("zones/%s/diskTypes/%s", zone.Name, c.DiskType),
}, },
@ -355,20 +399,6 @@ func (d *driverGCE) WaitForInstance(state, zone, name string) <-chan error {
return errCh return errCh
} }
func (d *driverGCE) getImage(name, projectId string) (image *compute.Image, err error) {
projects := []string{projectId, "centos-cloud", "coreos-cloud", "debian-cloud", "google-containers", "opensuse-cloud", "rhel-cloud", "suse-cloud", "ubuntu-os-cloud", "windows-cloud"}
for _, project := range projects {
image, err = d.service.Images.Get(project, name).Do()
if err == nil && image != nil && image.SelfLink != "" {
return
}
image = nil
}
err = fmt.Errorf("Image %s could not be found in any of these projects: %s", name, projects)
return
}
func (d *driverGCE) refreshInstanceState(zone, name string) stateRefreshFunc { func (d *driverGCE) refreshInstanceState(zone, name string) stateRefreshFunc {
return func() (string, error) { return func() (string, error) {
instance, err := d.service.Instances.Get(d.projectId, zone, name).Do() instance, err := d.service.Instances.Get(d.projectId, zone, name).Do()

View File

@ -1,20 +1,21 @@
package googlecompute package googlecompute
import "fmt"
// DriverMock is a Driver implementation that is a mocked out so that // DriverMock is a Driver implementation that is a mocked out so that
// it can be used for tests. // it can be used for tests.
type DriverMock struct { type DriverMock struct {
ImageExistsName string CreateImageName string
ImageExistsResult bool CreateImageDesc string
CreateImageFamily string
CreateImageName string CreateImageZone string
CreateImageDesc string CreateImageDisk string
CreateImageFamily string CreateImageResultLicenses []string
CreateImageZone string CreateImageResultProjectId string
CreateImageDisk string CreateImageResultSelfLink string
CreateImageProjectId string CreateImageResultSizeGb int64
CreateImageSizeGb int64 CreateImageErrCh <-chan error
CreateImageErrCh <-chan error CreateImageResultCh <-chan *Image
CreateImageResultCh <-chan Image
DeleteImageName string DeleteImageName string
DeleteImageErrCh <-chan error DeleteImageErrCh <-chan error
@ -29,6 +30,21 @@ type DriverMock struct {
DeleteDiskErrCh <-chan error DeleteDiskErrCh <-chan error
DeleteDiskErr error DeleteDiskErr error
GetImageName string
GetImageResult *Image
GetImageErr error
GetImageFromProjectProject string
GetImageFromProjectName string
GetImageFromProjectResult *Image
GetImageFromProjectErr error
GetInstanceMetadataZone string
GetInstanceMetadataName string
GetInstanceMetadataKey string
GetInstanceMetadataResult string
GetInstanceMetadataErr error
GetNatIPZone string GetNatIPZone string
GetNatIPName string GetNatIPName string
GetNatIPResult string GetNatIPResult string
@ -44,6 +60,9 @@ type DriverMock struct {
GetSerialPortOutputResult string GetSerialPortOutputResult string
GetSerialPortOutputErr error GetSerialPortOutputErr error
ImageExistsName string
ImageExistsResult bool
RunInstanceConfig *InstanceConfig RunInstanceConfig *InstanceConfig
RunInstanceErrCh <-chan error RunInstanceErrCh <-chan error
RunInstanceErr error RunInstanceErr error
@ -54,31 +73,33 @@ type DriverMock struct {
WaitForInstanceErrCh <-chan error WaitForInstanceErrCh <-chan error
} }
func (d *DriverMock) ImageExists(name string) bool { func (d *DriverMock) CreateImage(name, description, family, zone, disk string) (<-chan *Image, <-chan error) {
d.ImageExistsName = name
return d.ImageExistsResult
}
func (d *DriverMock) CreateImage(name, description, family, zone, disk string) (<-chan Image, <-chan error) {
d.CreateImageName = name d.CreateImageName = name
d.CreateImageDesc = description d.CreateImageDesc = description
d.CreateImageFamily = family d.CreateImageFamily = family
d.CreateImageZone = zone d.CreateImageZone = zone
d.CreateImageDisk = disk d.CreateImageDisk = disk
if d.CreateImageSizeGb == 0 { if d.CreateImageResultProjectId == "" {
d.CreateImageSizeGb = 10 d.CreateImageResultProjectId = "test"
} }
if d.CreateImageProjectId == "" { if d.CreateImageResultSelfLink == "" {
d.CreateImageProjectId = "test" d.CreateImageResultSelfLink = fmt.Sprintf(
"http://content.googleapis.com/compute/v1/%s/global/licenses/test",
d.CreateImageResultProjectId)
}
if d.CreateImageResultSizeGb == 0 {
d.CreateImageResultSizeGb = 10
} }
resultCh := d.CreateImageResultCh resultCh := d.CreateImageResultCh
if resultCh == nil { if resultCh == nil {
ch := make(chan Image, 1) ch := make(chan *Image, 1)
ch <- Image{ ch <- &Image{
Licenses: d.CreateImageResultLicenses,
Name: name, Name: name,
ProjectId: d.CreateImageProjectId, ProjectId: d.CreateImageResultProjectId,
SizeGb: d.CreateImageSizeGb, SelfLink: d.CreateImageResultSelfLink,
SizeGb: d.CreateImageResultSizeGb,
} }
close(ch) close(ch)
resultCh = ch resultCh = ch
@ -135,6 +156,24 @@ func (d *DriverMock) DeleteDisk(zone, name string) (<-chan error, error) {
return resultCh, d.DeleteDiskErr return resultCh, d.DeleteDiskErr
} }
func (d *DriverMock) GetImage(name string) (*Image, error) {
d.GetImageName = name
return d.GetImageResult, d.GetImageErr
}
func (d *DriverMock) GetImageFromProject(project, name string) (*Image, error) {
d.GetImageFromProjectProject = project
d.GetImageFromProjectName = name
return d.GetImageFromProjectResult, d.GetImageFromProjectErr
}
func (d *DriverMock) GetInstanceMetadata(zone, name, key string) (string, error) {
d.GetInstanceMetadataZone = zone
d.GetInstanceMetadataName = name
d.GetInstanceMetadataKey = key
return d.GetInstanceMetadataResult, d.GetInstanceMetadataErr
}
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
@ -153,6 +192,11 @@ func (d *DriverMock) GetSerialPortOutput(zone, name string) (string, error) {
return d.GetSerialPortOutputResult, d.GetSerialPortOutputErr return d.GetSerialPortOutputResult, d.GetSerialPortOutputErr
} }
func (d *DriverMock) ImageExists(name string) bool {
d.ImageExistsName = name
return d.ImageExistsResult
}
func (d *DriverMock) RunInstance(c *InstanceConfig) (<-chan error, error) { func (d *DriverMock) RunInstance(c *InstanceConfig) (<-chan error, error) {
d.RunInstanceConfig = c d.RunInstanceConfig = c

View File

@ -0,0 +1,22 @@
package googlecompute
import (
"strings"
)
type Image struct {
Licenses []string
Name string
ProjectId string
SelfLink string
SizeGb int64
}
func (i *Image) IsWindows() bool {
for _, license := range i.Licenses {
if strings.Contains(license, "windows") {
return true
}
}
return false
}

View File

@ -0,0 +1,26 @@
package googlecompute
import(
"testing"
"fmt"
"github.com/stretchr/testify/assert"
)
func StubImage(name, project string, licenses []string, sizeGb int64) *Image {
return &Image{
Licenses: licenses,
Name: name,
ProjectId: project,
SelfLink: fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/images/%s", project, name),
SizeGb: sizeGb,
}
}
func TestImage_IsWindows(t *testing.T) {
i := StubImage("foo", "foo-project", []string{"license-foo", "license-bar"}, 100)
assert.False(t, i.IsWindows())
i = StubImage("foo", "foo-project", []string{"license-foo", "windows-license"}, 100)
assert.True(t, i.IsWindows())
}

View File

@ -1,37 +1,40 @@
package googlecompute package googlecompute
import ( import (
"encoding/base64"
"fmt" "fmt"
) )
const StartupScriptStartLog string = "Packer startup script starting."
const StartupScriptDoneLog string = "Packer startup script done."
const StartupScriptKey string = "startup-script" const StartupScriptKey string = "startup-script"
const StartupScriptStatusKey string = "startup-script-status"
const StartupWrappedScriptKey string = "packer-wrapped-startup-script" const StartupWrappedScriptKey string = "packer-wrapped-startup-script"
// We have to encode StartupScriptDoneLog because we use it as a sentinel value to indicate const StartupScriptStatusDone string = "done"
// that the user-provided startup script is done. If we pass StartupScriptDoneLog as-is, it const StartupScriptStatusError string = "error"
// will be printed early in the instance console log (before the startup script even runs; const StartupScriptStatusNotDone string = "notdone"
// we print out instance creation metadata which contains this wrapper script).
var StartupScriptDoneLogBase64 string = base64.StdEncoding.EncodeToString([]byte(StartupScriptDoneLog))
var StartupScript string = fmt.Sprintf(`#!/bin/bash var StartupScriptLinux string = fmt.Sprintf(`#!/bin/bash
echo %s echo "Packer startup script starting."
RETVAL=0 RETVAL=0
BASEMETADATAURL=http://metadata/computeMetadata/v1/instance/
GetMetadata () { GetMetadata () {
echo "$(curl -f -H "Metadata-Flavor: Google" http://metadata/computeMetadata/v1/instance/attributes/$1 2> /dev/null)" echo "$(curl -f -H "Metadata-Flavor: Google" ${BASEMETADATAURL}/${1} 2> /dev/null)"
} }
STARTUPSCRIPT=$(GetMetadata %s) ZONE=$(GetMetadata zone | grep -oP "[^/]*$")
SetMetadata () {
gcloud compute instances add-metadata ${HOSTNAME} --metadata ${1}=${2} --zone ${ZONE}
}
STARTUPSCRIPT=$(GetMetadata attributes/%s)
STARTUPSCRIPTPATH=/packer-wrapped-startup-script STARTUPSCRIPTPATH=/packer-wrapped-startup-script
if [ -f "/var/log/startupscript.log" ]; then if [ -f "/var/log/startupscript.log" ]; then
STARTUPSCRIPTLOGPATH=/var/log/startupscript.log STARTUPSCRIPTLOGPATH=/var/log/startupscript.log
else else
STARTUPSCRIPTLOGPATH=/var/log/daemon.log STARTUPSCRIPTLOGPATH=/var/log/daemon.log
fi fi
STARTUPSCRIPTLOGDEST=$(GetMetadata startup-script-log-dest) STARTUPSCRIPTLOGDEST=$(GetMetadata attributes/startup-script-log-dest)
if [[ ! -z $STARTUPSCRIPT ]]; then if [[ ! -z $STARTUPSCRIPT ]]; then
echo "Executing user-provided startup script..." echo "Executing user-provided startup script..."
@ -48,6 +51,9 @@ if [[ ! -z $STARTUPSCRIPT ]]; then
rm ${STARTUPSCRIPTPATH} rm ${STARTUPSCRIPTPATH}
fi fi
echo $(echo %s | base64 --decode) echo "Packer startup script done."
SetMetadata %s %s
exit $RETVAL exit $RETVAL
`, StartupScriptStartLog, StartupWrappedScriptKey, StartupScriptDoneLogBase64) `, StartupWrappedScriptKey, StartupScriptStatusKey, StartupScriptStatusDone)
var StartupScriptWindows string = ""

View File

@ -13,14 +13,14 @@ type StepCheckExistingImage int
// Run executes the Packer build step that checks if the image already exists. // Run executes the Packer build step that checks if the image already exists.
func (s *StepCheckExistingImage) Run(state multistep.StateBag) multistep.StepAction { func (s *StepCheckExistingImage) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config) c := state.Get("config").(*Config)
driver := state.Get("driver").(Driver) d := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
ui.Say("Checking image does not exist...") ui.Say("Checking image does not exist...")
exists := driver.ImageExists(config.ImageName) exists := d.ImageExists(c.ImageName)
if exists { if exists {
err := fmt.Errorf("Image %s already exists", config.ImageName) err := fmt.Errorf("Image %s already exists", c.ImageName)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt

View File

@ -24,7 +24,9 @@ func (s *StepCreateImage) Run(state multistep.StateBag) multistep.StepAction {
ui.Say("Creating image...") ui.Say("Creating image...")
imageCh, errCh := driver.CreateImage(config.ImageName, config.ImageDescription, config.ImageFamily, config.Zone, config.DiskName) imageCh, errCh := driver.CreateImage(
config.ImageName, config.ImageDescription, config.ImageFamily, config.Zone,
config.DiskName)
var err error var err error
select { select {
case err = <-errCh: case err = <-errCh:

View File

@ -5,6 +5,7 @@ import (
"testing" "testing"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/stretchr/testify/assert"
) )
func TestStepCreateImage_impl(t *testing.T) { func TestStepCreateImage_impl(t *testing.T) {
@ -16,52 +17,35 @@ func TestStepCreateImage(t *testing.T) {
step := new(StepCreateImage) step := new(StepCreateImage)
defer step.Cleanup(state) defer step.Cleanup(state)
config := state.Get("config").(*Config) c := state.Get("config").(*Config)
driver := state.Get("driver").(*DriverMock) d := state.Get("driver").(*DriverMock)
driver.CreateImageProjectId = "createimage-project"
driver.CreateImageSizeGb = 100 // These are the values of the image the driver will return.
d.CreateImageResultLicenses = []string{"test-license"}
d.CreateImageResultProjectId = "test-project"
d.CreateImageResultSizeGb = 100
// run the step // run the step
if action := step.Run(state); action != multistep.ActionContinue { action := step.Run(state)
t.Fatalf("bad action: %#v", action) assert.Equal(t, action, multistep.ActionContinue, "Step did not pass.")
}
uncastImage, ok := state.GetOk("image") uncastImage, ok := state.GetOk("image")
if !ok { assert.True(t, ok, "State does not have resulting image.")
t.Fatal("should have image") image, ok := uncastImage.(*Image)
} assert.True(t, ok, "Image in state is not an Image.")
image, ok := uncastImage.(Image)
if !ok {
t.Fatal("image is not an Image")
}
// Verify created Image results. // Verify created Image results.
if image.Name != config.ImageName { assert.Equal(t, image.Licenses, d.CreateImageResultLicenses, "Created image licenses don't match the licenses returned by the driver.")
t.Fatalf("Created image name, %s, does not match config name, %s.", image.Name, config.ImageName) assert.Equal(t, image.Name, c.ImageName, "Created image does not match config name.")
} assert.Equal(t, image.ProjectId, d.CreateImageResultProjectId, "Created image project does not match driver project.")
if driver.CreateImageProjectId != image.ProjectId { assert.Equal(t, image.SizeGb, d.CreateImageResultSizeGb, "Created image size does not match the size returned by the driver.")
t.Fatalf("Created image project ID, %s, does not match driver project ID, %s.", image.ProjectId, driver.CreateImageProjectId)
}
if driver.CreateImageSizeGb != image.SizeGb {
t.Fatalf("Created image size, %d, does not match the expected test value, %d.", image.SizeGb, driver.CreateImageSizeGb)
}
// Verify proper args passed to driver.CreateImage. // Verify proper args passed to driver.CreateImage.
if driver.CreateImageName != config.ImageName { assert.Equal(t, d.CreateImageName, c.ImageName, "Incorrect image name passed to driver.")
t.Fatalf("bad: %#v", driver.CreateImageName) assert.Equal(t, d.CreateImageDesc, c.ImageDescription, "Incorrect image description passed to driver.")
} assert.Equal(t, d.CreateImageFamily, c.ImageFamily, "Incorrect image family passed to driver.")
if driver.CreateImageDesc != config.ImageDescription { assert.Equal(t, d.CreateImageZone, c.Zone, "Incorrect image zone passed to driver.")
t.Fatalf("bad: %#v", driver.CreateImageDesc) assert.Equal(t, d.CreateImageDisk, c.DiskName, "Incorrect disk passed to driver.")
}
if driver.CreateImageFamily != config.ImageFamily {
t.Fatalf("bad: %#v", driver.CreateImageFamily)
}
if driver.CreateImageZone != config.Zone {
t.Fatalf("bad: %#v", driver.CreateImageZone)
}
if driver.CreateImageDisk != config.DiskName {
t.Fatalf("bad: %#v", driver.CreateImageDisk)
}
} }
func TestStepCreateImage_errorOnChannel(t *testing.T) { func TestStepCreateImage_errorOnChannel(t *testing.T) {
@ -76,14 +60,10 @@ func TestStepCreateImage_errorOnChannel(t *testing.T) {
driver.CreateImageErrCh = errCh driver.CreateImageErrCh = errCh
// run the step // run the step
if action := step.Run(state); action != multistep.ActionHalt { action := step.Run(state)
t.Fatalf("bad action: %#v", action) assert.Equal(t, action, multistep.ActionHalt, "Step should not have passed.")
} _, ok := state.GetOk("error")
assert.True(t, ok, "State should have an error.")
if _, ok := state.GetOk("error"); !ok { _, ok = state.GetOk("image_name")
t.Fatal("should have error") assert.False(t, ok, "State should not have a resulting image.")
}
if _, ok := state.GetOk("image_name"); ok {
t.Fatal("should NOT have image")
}
} }

View File

@ -15,82 +15,97 @@ type StepCreateInstance struct {
Debug bool Debug bool
} }
func (config *Config) getImage() Image { func (c *Config) createInstanceMetadata(sourceImage *Image, sshPublicKey string) (map[string]string, error) {
project := config.ProjectId
if config.SourceImageProjectId != "" {
project = config.SourceImageProjectId
}
return Image{Name: config.SourceImage, ProjectId: project}
}
func (config *Config) getInstanceMetadata(sshPublicKey string) (map[string]string, error) {
instanceMetadata := make(map[string]string) instanceMetadata := make(map[string]string)
var err error var err error
// Copy metadata from config. // Copy metadata from config.
for k, v := range config.Metadata { for k, v := range c.Metadata {
instanceMetadata[k] = v instanceMetadata[k] = v
} }
// Merge any existing ssh keys with our public key. // Merge any existing ssh keys with our public key.
sshMetaKey := "sshKeys" sshMetaKey := "sshKeys"
sshKeys := fmt.Sprintf("%s:%s", config.Comm.SSHUsername, sshPublicKey) sshKeys := fmt.Sprintf("%s:%s", c.Comm.SSHUsername, sshPublicKey)
if confSshKeys, exists := instanceMetadata[sshMetaKey]; exists { if confSshKeys, exists := instanceMetadata[sshMetaKey]; exists {
sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSshKeys) sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSshKeys)
} }
instanceMetadata[sshMetaKey] = sshKeys instanceMetadata[sshMetaKey] = sshKeys
// Wrap any startup script with our own startup script. // Wrap any startup script with our own startup script.
if config.StartupScriptFile != "" { if c.StartupScriptFile != "" {
var content []byte var content []byte
content, err = ioutil.ReadFile(config.StartupScriptFile) content, err = ioutil.ReadFile(c.StartupScriptFile)
instanceMetadata[StartupWrappedScriptKey] = string(content) instanceMetadata[StartupWrappedScriptKey] = string(content)
} else if wrappedStartupScript, exists := instanceMetadata[StartupScriptKey]; exists { } else if wrappedStartupScript, exists := instanceMetadata[StartupScriptKey]; exists {
instanceMetadata[StartupWrappedScriptKey] = wrappedStartupScript instanceMetadata[StartupWrappedScriptKey] = wrappedStartupScript
} }
instanceMetadata[StartupScriptKey] = StartupScript if sourceImage.IsWindows() {
// Windows startup script support is not yet implemented.
// Mark the startup script as done.
instanceMetadata[StartupScriptKey] = StartupScriptWindows
instanceMetadata[StartupScriptStatusKey] = StartupScriptStatusDone
} else {
instanceMetadata[StartupScriptKey] = StartupScriptLinux
instanceMetadata[StartupScriptStatusKey] = StartupScriptStatusNotDone
}
return instanceMetadata, err return instanceMetadata, err
} }
func getImage(c *Config, d Driver) (*Image, error) {
if c.SourceImageProjectId == "" {
return d.GetImage(c.SourceImage)
} else {
return d.GetImageFromProject(c.SourceImageProjectId, c.SourceImage)
}
}
// Run executes the Packer build step that creates a GCE instance. // Run executes the Packer build step that creates a GCE instance.
func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction { func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config) c := state.Get("config").(*Config)
driver := state.Get("driver").(Driver) d := state.Get("driver").(Driver)
sshPublicKey := state.Get("ssh_public_key").(string) sshPublicKey := state.Get("ssh_public_key").(string)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
sourceImage, err := getImage(c, d)
if err != nil {
err := fmt.Errorf("Error getting source image for instance creation: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Creating instance...") ui.Say("Creating instance...")
name := config.InstanceName name := c.InstanceName
var errCh <-chan error var errCh <-chan error
var err error
var metadata map[string]string var metadata map[string]string
metadata, err = config.getInstanceMetadata(sshPublicKey) metadata, err = c.createInstanceMetadata(sourceImage, sshPublicKey)
errCh, err = driver.RunInstance(&InstanceConfig{ errCh, err = d.RunInstance(&InstanceConfig{
Address: config.Address, Address: c.Address,
Description: "New instance created by Packer", Description: "New instance created by Packer",
DiskSizeGb: config.DiskSizeGb, DiskSizeGb: c.DiskSizeGb,
DiskType: config.DiskType, DiskType: c.DiskType,
Image: config.getImage(), Image: sourceImage,
MachineType: config.MachineType, MachineType: c.MachineType,
Metadata: metadata, Metadata: metadata,
Name: name, Name: name,
Network: config.Network, Network: c.Network,
OmitExternalIP: config.OmitExternalIP, OmitExternalIP: c.OmitExternalIP,
Preemptible: config.Preemptible, Preemptible: c.Preemptible,
Region: config.Region, Region: c.Region,
ServiceAccountEmail: config.Account.ClientEmail, ServiceAccountEmail: c.Account.ClientEmail,
Subnetwork: config.Subnetwork, Subnetwork: c.Subnetwork,
Tags: config.Tags, Tags: c.Tags,
Zone: config.Zone, Zone: c.Zone,
}) })
if err == nil { if err == nil {
ui.Message("Waiting for creation operation to complete...") ui.Message("Waiting for creation operation to complete...")
select { select {
case err = <-errCh: case err = <-errCh:
case <-time.After(config.stateTimeout): case <-time.After(c.stateTimeout):
err = errors.New("time out while waiting for instance to create") err = errors.New("time out while waiting for instance to create")
} }
} }
@ -106,7 +121,7 @@ func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction
if s.Debug { if s.Debug {
if name != "" { if name != "" {
ui.Message(fmt.Sprintf("Instance: %s started in %s", name, config.Zone)) ui.Message(fmt.Sprintf("Instance: %s started in %s", name, c.Zone))
} }
} }

View File

@ -2,9 +2,11 @@ package googlecompute
import ( import (
"errors" "errors"
"github.com/mitchellh/multistep"
"testing" "testing"
"time" "time"
"github.com/mitchellh/multistep"
"github.com/stretchr/testify/assert"
) )
func TestStepCreateInstance_impl(t *testing.T) { func TestStepCreateInstance_impl(t *testing.T) {
@ -18,36 +20,25 @@ func TestStepCreateInstance(t *testing.T) {
state.Put("ssh_public_key", "key") state.Put("ssh_public_key", "key")
config := state.Get("config").(*Config) c := state.Get("config").(*Config)
driver := state.Get("driver").(*DriverMock) d := state.Get("driver").(*DriverMock)
d.GetImageResult = StubImage("test-image", "test-project", []string{}, 100)
// run the step // run the step
if action := step.Run(state); action != multistep.ActionContinue { assert.Equal(t, step.Run(state), multistep.ActionContinue, "Step should have passed and continued.")
t.Fatalf("bad action: %#v", action)
}
// Verify state // Verify state
nameRaw, ok := state.GetOk("instance_name") nameRaw, ok := state.GetOk("instance_name")
if !ok { assert.True(t, ok, "State should have an instance name.")
t.Fatal("should have instance name")
}
// cleanup // cleanup
step.Cleanup(state) step.Cleanup(state)
if driver.DeleteInstanceName != nameRaw.(string) { // Check args passed to the driver.
t.Fatal("should've deleted instance") assert.Equal(t, d.DeleteInstanceName, nameRaw.(string), "Incorrect instance name passed to driver.")
} assert.Equal(t, d.DeleteInstanceZone, c.Zone, "Incorrect instance zone passed to driver.")
if driver.DeleteInstanceZone != config.Zone { assert.Equal(t, d.DeleteDiskName, c.InstanceName, "Incorrect disk name passed to driver.")
t.Fatalf("bad instance zone: %#v", driver.DeleteInstanceZone) assert.Equal(t, d.DeleteDiskZone, c.Zone, "Incorrect disk zone passed to driver.")
}
if driver.DeleteDiskName != config.InstanceName {
t.Fatal("should've deleted disk")
}
if driver.DeleteDiskZone != config.Zone {
t.Fatalf("bad disk zone: %#v", driver.DeleteDiskZone)
}
} }
func TestStepCreateInstance_error(t *testing.T) { func TestStepCreateInstance_error(t *testing.T) {
@ -57,21 +48,18 @@ func TestStepCreateInstance_error(t *testing.T) {
state.Put("ssh_public_key", "key") state.Put("ssh_public_key", "key")
driver := state.Get("driver").(*DriverMock) d := state.Get("driver").(*DriverMock)
driver.RunInstanceErr = errors.New("error") d.RunInstanceErr = errors.New("error")
d.GetImageResult = StubImage("test-image", "test-project", []string{}, 100)
// run the step // run the step
if action := step.Run(state); action != multistep.ActionHalt { assert.Equal(t, step.Run(state), multistep.ActionHalt, "Step should have failed and halted.")
t.Fatalf("bad action: %#v", action)
}
// Verify state // Verify state
if _, ok := state.GetOk("error"); !ok { _, ok := state.GetOk("error")
t.Fatal("should have error") assert.True(t, ok, "State should have an error.")
} _, ok = state.GetOk("instance_name")
if _, ok := state.GetOk("instance_name"); ok { assert.False(t, ok, "State should not have an instance name.")
t.Fatal("should NOT have instance name")
}
} }
func TestStepCreateInstance_errorOnChannel(t *testing.T) { func TestStepCreateInstance_errorOnChannel(t *testing.T) {
@ -79,26 +67,23 @@ func TestStepCreateInstance_errorOnChannel(t *testing.T) {
step := new(StepCreateInstance) step := new(StepCreateInstance)
defer step.Cleanup(state) defer step.Cleanup(state)
state.Put("ssh_public_key", "key")
errCh := make(chan error, 1) errCh := make(chan error, 1)
errCh <- errors.New("error") errCh <- errors.New("error")
state.Put("ssh_public_key", "key") d := state.Get("driver").(*DriverMock)
d.RunInstanceErrCh = errCh
driver := state.Get("driver").(*DriverMock) d.GetImageResult = StubImage("test-image", "test-project", []string{}, 100)
driver.RunInstanceErrCh = errCh
// run the step // run the step
if action := step.Run(state); action != multistep.ActionHalt { assert.Equal(t, step.Run(state), multistep.ActionHalt, "Step should have failed and halted.")
t.Fatalf("bad action: %#v", action)
}
// Verify state // Verify state
if _, ok := state.GetOk("error"); !ok { _, ok := state.GetOk("error")
t.Fatal("should have error") assert.True(t, ok, "State should have an error.")
} _, ok = state.GetOk("instance_name")
if _, ok := state.GetOk("instance_name"); ok { assert.False(t, ok, "State should not have an instance name.")
t.Fatal("should NOT have instance name")
}
} }
func TestStepCreateInstance_errorTimeout(t *testing.T) { func TestStepCreateInstance_errorTimeout(t *testing.T) {
@ -106,30 +91,27 @@ func TestStepCreateInstance_errorTimeout(t *testing.T) {
step := new(StepCreateInstance) step := new(StepCreateInstance)
defer step.Cleanup(state) defer step.Cleanup(state)
state.Put("ssh_public_key", "key")
errCh := make(chan error, 1) errCh := make(chan error, 1)
go func() { go func() {
<-time.After(10 * time.Millisecond) <-time.After(10 * time.Millisecond)
errCh <- nil errCh <- nil
}() }()
state.Put("ssh_public_key", "key")
config := state.Get("config").(*Config) config := state.Get("config").(*Config)
config.stateTimeout = 1 * time.Microsecond config.stateTimeout = 1 * time.Microsecond
driver := state.Get("driver").(*DriverMock) d := state.Get("driver").(*DriverMock)
driver.RunInstanceErrCh = errCh d.RunInstanceErrCh = errCh
d.GetImageResult = StubImage("test-image", "test-project", []string{}, 100)
// run the step // run the step
if action := step.Run(state); action != multistep.ActionHalt { assert.Equal(t, step.Run(state), multistep.ActionHalt, "Step should have failed and halted.")
t.Fatalf("bad action: %#v", action)
}
// Verify state // Verify state
if _, ok := state.GetOk("error"); !ok { _, ok := state.GetOk("error")
t.Fatal("should have error") assert.True(t, ok, "State should have an error.")
} _, ok = state.GetOk("instance_name")
if _, ok := state.GetOk("instance_name"); ok { assert.False(t, ok, "State should not have an instance name.")
t.Fatal("should NOT have instance name")
}
} }

View File

@ -2,9 +2,10 @@ package googlecompute
import ( import (
"bytes" "bytes"
"testing"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"testing"
) )
func testState(t *testing.T) multistep.StateBag { func testState(t *testing.T) multistep.StateBag {

View File

@ -1,8 +1,8 @@
package googlecompute package googlecompute
import( import(
"errors"
"fmt" "fmt"
"strings"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
@ -10,7 +10,8 @@ import(
type StepWaitInstanceStartup int type StepWaitInstanceStartup int
// Run reads the instance serial port output and looks for the log entry indicating the startup script finished. // Run reads the instance metadata and looks for the log entry
// indicating the startup script finished.
func (s *StepWaitInstanceStartup) Run(state multistep.StateBag) multistep.StepAction { func (s *StepWaitInstanceStartup) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config) config := state.Get("config").(*Config)
driver := state.Get("driver").(Driver) driver := state.Get("driver").(Driver)
@ -21,14 +22,20 @@ func (s *StepWaitInstanceStartup) Run(state multistep.StateBag) multistep.StepAc
// Keep checking the serial port output to see if the startup script is done. // Keep checking the serial port output to see if the startup script is done.
err := Retry(10, 60, 0, func() (bool, error) { err := Retry(10, 60, 0, func() (bool, error) {
output, err := driver.GetSerialPortOutput(config.Zone, instanceName) status, err := driver.GetInstanceMetadata(config.Zone,
instanceName, StartupScriptStatusKey)
if err != nil { if err != nil {
err := fmt.Errorf("Error getting serial port output: %s", err) err := fmt.Errorf("Error getting startup script status: %s", err)
return false, err return false, err
} }
done := strings.Contains(output, StartupScriptDoneLog) if status == StartupScriptStatusError {
err = errors.New("Startup script error.")
return false, err
}
done := status == StartupScriptStatusDone
if !done { if !done {
ui.Say("Startup script not finished yet. Waiting...") ui.Say("Startup script not finished yet. Waiting...")
} }

View File

@ -3,36 +3,28 @@ package googlecompute
import ( import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestStepWaitInstanceStartup(t *testing.T) { func TestStepWaitInstanceStartup(t *testing.T) {
state := testState(t) state := testState(t)
step := new(StepWaitInstanceStartup) step := new(StepWaitInstanceStartup)
config := state.Get("config").(*Config) c := state.Get("config").(*Config)
driver := state.Get("driver").(*DriverMock) d := state.Get("driver").(*DriverMock)
testZone := "test-zone" testZone := "test-zone"
testInstanceName := "test-instance-name" testInstanceName := "test-instance-name"
config.Zone = testZone c.Zone = testZone
state.Put("instance_name", testInstanceName) state.Put("instance_name", testInstanceName)
// The done log triggers step completion.
driver.GetSerialPortOutputResult = StartupScriptDoneLog // This step stops when it gets Done back from the metadata.
d.GetInstanceMetadataResult = StartupScriptStatusDone
// Run the step. // Run the step.
if action := step.Run(state); action != multistep.ActionContinue { assert.Equal(t, step.Run(state), multistep.ActionContinue, "Step should have passed and continued.")
t.Fatalf("StepWaitInstanceStartup did not return a Continue action: %#v", action)
}
// Check that GetSerialPortOutput was called properly. // Check that GetInstanceMetadata was called properly.
if driver.GetSerialPortOutputZone != testZone { assert.Equal(t, d.GetInstanceMetadataZone, testZone, "Incorrect zone passed to GetInstanceMetadata.")
t.Fatalf( assert.Equal(t, d.GetInstanceMetadataName, testInstanceName, "Incorrect instance name passed to GetInstanceMetadata.")
"GetSerialPortOutput wrong zone. Expected: %s, Actual: %s", driver.GetSerialPortOutputZone,
testZone)
}
if driver.GetSerialPortOutputName != testInstanceName {
t.Fatalf(
"GetSerialPortOutput wrong instance name. Expected: %s, Actual: %s", driver.GetSerialPortOutputName,
testInstanceName)
}
} }