Merge pull request #3639 from crunk1/master

Adding support for googlecompute startup scripts.
This commit is contained in:
Eric Johnson 2016-07-27 08:48:01 -07:00 committed by GitHub
commit 95cffcae78
18 changed files with 474 additions and 104 deletions

View File

@ -7,7 +7,7 @@ import (
// Artifact represents a GCE image as the result of a Packer build.
type Artifact struct {
imageName string
image Image
driver Driver
}
@ -18,8 +18,8 @@ func (*Artifact) BuilderId() string {
// Destroy destroys the GCE image represented by the artifact.
func (a *Artifact) Destroy() error {
log.Printf("Destroying image: %s", a.imageName)
errCh := a.driver.DeleteImage(a.imageName)
log.Printf("Destroying image: %s", a.image.Name)
errCh := a.driver.DeleteImage(a.image.Name)
return <-errCh
}
@ -30,12 +30,12 @@ func (*Artifact) Files() []string {
// Id returns the GCE image name.
func (a *Artifact) Id() string {
return a.imageName
return a.image.Name
}
// String returns the string representation of the artifact.
func (a *Artifact) String() string {
return fmt.Sprintf("A disk image was created: %v", a.imageName)
return fmt.Sprintf("A disk image was created: %v", a.image.Name)
}
func (a *Artifact) State(name string) interface{} {

View File

@ -67,6 +67,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
SSHConfig: sshConfig,
},
new(common.StepProvision),
new(StepWaitInstanceStartup),
new(StepTeardownInstance),
new(StepCreateImage),
}
@ -86,14 +87,14 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
if rawErr, ok := state.GetOk("error"); ok {
return nil, rawErr.(error)
}
if _, ok := state.GetOk("image_name"); !ok {
log.Println("Failed to find image_name in state. Bug?")
if _, ok := state.GetOk("image"); !ok {
log.Println("Failed to find image in state. Bug?")
return nil, nil
}
artifact := &Artifact{
imageName: state.Get("image_name").(string),
driver: driver,
image: state.Get("image").(Image),
driver: driver,
}
return artifact, nil
}

View File

@ -0,0 +1,44 @@
package googlecompute
import (
"fmt"
"math"
"time"
)
var RetryExhaustedError error = fmt.Errorf("Function never succeeded in Retry")
// Retry retries a function up to numTries times with exponential backoff.
// If numTries == 0, retry indefinitely. If interval == 0, Retry will not delay retrying and there will be
// no exponential backoff. If maxInterval == 0, maxInterval is set to +Infinity.
// Intervals are in seconds.
// Returns an error if initial > max intervals, if retries are exhausted, or if the passed function returns
// an error.
func Retry(initialInterval float64, maxInterval float64, numTries uint, function func() (bool, error)) error {
if maxInterval == 0 {
maxInterval = math.Inf(1)
} else if initialInterval < 0 || initialInterval > maxInterval {
return fmt.Errorf("Invalid retry intervals (negative or initial < max). Initial: %f, Max: %f.", initialInterval, maxInterval)
}
var err error
done := false
interval := initialInterval
for i := uint(0); !done && (numTries == 0 || i < numTries); i++ {
done, err = function()
if err != nil {
return err
}
if !done {
// Retry after delay. Calculate next delay.
time.Sleep(time.Duration(interval) * time.Second)
interval = math.Min(interval * 2, maxInterval)
}
}
if !done {
return RetryExhaustedError
}
return nil
}

View File

@ -0,0 +1,64 @@
package googlecompute
import (
"fmt"
"testing"
)
func TestRetry(t *testing.T) {
numTries := uint(0)
// Test that a passing function only gets called once.
err := Retry(0, 0, 0, func() (bool, error) {
numTries++
return true, nil
})
if numTries != 1 {
t.Fatal("Passing function should not have been retried.")
}
if err != nil {
t.Fatalf("Passing function should not have returned a retry error. Error: %s", err)
}
// Test that a failing function gets retried (once in this example).
numTries = 0
results := []bool{false, true}
err = Retry(0, 0, 0, func() (bool, error) {
result := results[numTries]
numTries++
return result, nil
})
if numTries != 2 {
t.Fatalf("Retried function should have been tried twice. Tried %d times.", numTries)
}
if err != nil {
t.Fatalf("Successful retried function should not have returned a retry error. Error: %s", err)
}
// Test that a function error gets returned, and the function does not get called again.
numTries = 0
funcErr := fmt.Errorf("This function had an error!")
err = Retry(0, 0, 0, func() (bool, error) {
numTries++
return false, funcErr
})
if numTries != 1 {
t.Fatal("Errant function should not have been retried.")
}
if err != funcErr {
t.Fatalf("Errant function did not return the right error %s. Error: %s", funcErr, err)
}
// Test when a function exhausts its retries.
numTries = 0
expectedTries := uint(3)
err = Retry(0, 0, expectedTries, func() (bool, error) {
numTries++
return false, nil
})
if numTries != expectedTries {
t.Fatalf("Unsuccessul retry function should have been called %d times. Only called %d times.", expectedTries, numTries)
}
if err != RetryExhaustedError {
t.Fatalf("Unsuccessful retry function should have returned a retry exhausted error. Actual error: %s", err)
}
}

View File

@ -26,6 +26,7 @@ type Config struct {
AccountFile string `mapstructure:"account_file"`
ProjectId string `mapstructure:"project_id"`
Address string `mapstructure:"address"`
DiskName string `mapstructure:"disk_name"`
DiskSizeGb int64 `mapstructure:"disk_size"`
DiskType string `mapstructure:"disk_type"`
@ -36,15 +37,15 @@ type Config struct {
MachineType string `mapstructure:"machine_type"`
Metadata map[string]string `mapstructure:"metadata"`
Network string `mapstructure:"network"`
Subnetwork string `mapstructure:"subnetwork"`
Address string `mapstructure:"address"`
Preemptible bool `mapstructure:"preemptible"`
RawStateTimeout string `mapstructure:"state_timeout"`
Region string `mapstructure:"region"`
SourceImage string `mapstructure:"source_image"`
SourceImageProjectId string `mapstructure:"source_image_project_id"`
RawStateTimeout string `mapstructure:"state_timeout"`
StartupScriptFile string `mapstructure:"startup_script_file"`
Subnetwork string `mapstructure:"subnetwork"`
Tags []string `mapstructure:"tags"`
UseInternalIP bool `mapstructure:"use_internal_ip"`
Region string `mapstructure:"region"`
Zone string `mapstructure:"zone"`
account accountFile

View File

@ -10,7 +10,7 @@ type Driver interface {
// CreateImage creates an image from the given disk in Google Compute
// Engine.
CreateImage(name, description, family, zone, disk string) <-chan error
CreateImage(name, description, family, zone, disk string) (<-chan Image, <-chan error)
// DeleteImage deletes the image with the given name.
DeleteImage(name string) <-chan error
@ -21,12 +21,15 @@ type Driver interface {
// DeleteDisk deletes the disk with the given name.
DeleteDisk(zone, name string) (<-chan error, error)
// GetNatIP gets the NAT IP address for the instance.
GetNatIP(zone, name string) (string, error)
// GetInternalIP gets the GCE-internal IP address for the instance.
GetInternalIP(zone, name string) (string, error)
// GetNatIP gets the NAT IP address for the instance.
GetNatIP(zone, name string) (string, error)
// GetSerialPortOutput gets the Serial Port contents for the instance.
GetSerialPortOutput(zone, name string) (string, error)
// RunInstance takes the given config and launches an instance.
RunInstance(*InstanceConfig) (<-chan error, error)
@ -37,21 +40,23 @@ type Driver interface {
type Image struct {
Name string
ProjectId string
SizeGb int64
}
type InstanceConfig struct {
Description string
DiskSizeGb int64
DiskType string
Image Image
MachineType string
Metadata map[string]string
Name string
Network string
Subnetwork string
Address string
Preemptible bool
Tags []string
Region string
Zone string
Address string
Description string
DiskSizeGb int64
DiskType string
Image Image
MachineType string
Metadata map[string]string
Name string
Network string
Preemptible bool
Region string
ServiceAccountEmail string
Subnetwork string
Tags []string
Zone string
}

View File

@ -5,7 +5,6 @@ import (
"log"
"net/http"
"runtime"
"time"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/version"
@ -91,7 +90,7 @@ func (d *driverGCE) ImageExists(name string) bool {
return err == nil
}
func (d *driverGCE) CreateImage(name, description, family, zone, disk string) <-chan error {
func (d *driverGCE) CreateImage(name, description, family, zone, disk string) (<-chan Image, <-chan error) {
image := &compute.Image{
Description: description,
Name: name,
@ -100,15 +99,32 @@ func (d *driverGCE) CreateImage(name, description, family, zone, disk string) <-
SourceType: "RAW",
}
imageCh := make(chan Image, 1)
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))
go func() {
err = waitForState(errCh, "DONE", d.refreshGlobalOp(op))
if err != nil {
close(imageCh)
}
image, err = d.getImage(name, d.projectId)
if err != nil {
close(imageCh)
errCh <- err
}
imageCh <- Image{
Name: name,
ProjectId: d.projectId,
SizeGb: image.DiskSizeGb,
}
close(imageCh)
}()
}
return errCh
return imageCh, errCh
}
func (d *driverGCE) DeleteImage(name string) <-chan error {
@ -181,6 +197,15 @@ func (d *driverGCE) GetInternalIP(zone, name string) (string, error) {
return "", nil
}
func (d *driverGCE) GetSerialPortOutput(zone, name string) (string, error) {
output, err := d.service.Instances.GetSerialPortOutput(d.projectId, zone, name).Do()
if err != nil {
return "", err
}
return output.Contents, nil
}
func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
// Get the zone
d.ui.Message(fmt.Sprintf("Loading zone: %s", c.Zone))
@ -191,7 +216,7 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
// 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)
image, err := d.getImage(c.Image.Name, c.Image.ProjectId)
if err != nil {
return nil, err
}
@ -294,7 +319,7 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
},
ServiceAccounts: []*compute.ServiceAccount{
&compute.ServiceAccount{
Email: "default",
Email: c.ServiceAccountEmail,
Scopes: []string{
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/compute",
@ -324,17 +349,17 @@ func (d *driverGCE) WaitForInstance(state, zone, name string) <-chan error {
return errCh
}
func (d *driverGCE) getImage(img Image) (image *compute.Image, err error) {
projects := []string{img.ProjectId, "centos-cloud", "coreos-cloud", "debian-cloud", "google-containers", "opensuse-cloud", "rhel-cloud", "suse-cloud", "ubuntu-os-cloud", "windows-cloud"}
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, img.Name).Do()
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", img.Name, projects)
err = fmt.Errorf("Image %s could not be found in any of these projects: %s", name, projects)
return
}
@ -396,18 +421,16 @@ 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 {
func waitForState(errCh chan<- error, target string, refresh stateRefreshFunc) error {
err := Retry(2, 2, 0, func() (bool, error) {
state, err := refresh()
if err != nil {
errCh <- err
return
return false, err
} else if state == target {
return true, nil
}
if state == target {
errCh <- nil
return
}
time.Sleep(2 * time.Second)
}
return false, nil
})
errCh <- err
return err
}

View File

@ -6,12 +6,15 @@ type DriverMock struct {
ImageExistsName string
ImageExistsResult bool
CreateImageName string
CreateImageDesc string
CreateImageFamily string
CreateImageZone string
CreateImageDisk string
CreateImageErrCh <-chan error
CreateImageName string
CreateImageDesc string
CreateImageFamily string
CreateImageZone string
CreateImageDisk string
CreateImageProjectId string
CreateImageSizeGb int64
CreateImageErrCh <-chan error
CreateImageResultCh <-chan Image
DeleteImageName string
DeleteImageErrCh <-chan error
@ -35,6 +38,11 @@ type DriverMock struct {
GetInternalIPName string
GetInternalIPResult string
GetInternalIPErr error
GetSerialPortOutputZone string
GetSerialPortOutputName string
GetSerialPortOutputResult string
GetSerialPortOutputErr error
RunInstanceConfig *InstanceConfig
RunInstanceErrCh <-chan error
@ -51,21 +59,39 @@ func (d *DriverMock) ImageExists(name string) bool {
return d.ImageExistsResult
}
func (d *DriverMock) CreateImage(name, description, family, zone, disk string) <-chan error {
func (d *DriverMock) CreateImage(name, description, family, zone, disk string) (<-chan Image, <-chan error) {
d.CreateImageName = name
d.CreateImageDesc = description
d.CreateImageFamily = family
d.CreateImageZone = zone
d.CreateImageDisk = disk
if d.CreateImageSizeGb == 0 {
d.CreateImageSizeGb = 10
}
if d.CreateImageProjectId == "" {
d.CreateImageProjectId = "test"
}
resultCh := d.CreateImageErrCh
resultCh := d.CreateImageResultCh
if resultCh == nil {
ch := make(chan error)
ch := make(chan Image, 1)
ch <- Image{
Name: name,
ProjectId: d.CreateImageProjectId,
SizeGb: d.CreateImageSizeGb,
}
close(ch)
resultCh = ch
}
return resultCh
errCh := d.CreateImageErrCh
if errCh == nil {
ch := make(chan error)
close(ch)
errCh = ch
}
return resultCh, errCh
}
func (d *DriverMock) DeleteImage(name string) <-chan error {
@ -121,6 +147,12 @@ func (d *DriverMock) GetInternalIP(zone, name string) (string, error) {
return d.GetInternalIPResult, d.GetInternalIPErr
}
func (d *DriverMock) GetSerialPortOutput(zone, name string) (string, error) {
d.GetSerialPortOutputZone = zone
d.GetSerialPortOutputName = name
return d.GetSerialPortOutputResult, d.GetSerialPortOutputErr
}
func (d *DriverMock) RunInstance(c *InstanceConfig) (<-chan error, error) {
d.RunInstanceConfig = c

View File

@ -0,0 +1,53 @@
package googlecompute
import (
"encoding/base64"
"fmt"
)
const StartupScriptStartLog string = "Packer startup script starting."
const StartupScriptDoneLog string = "Packer startup script done."
const StartupScriptKey string = "startup-script"
const StartupWrappedScriptKey string = "packer-wrapped-startup-script"
// We have to encode StartupScriptDoneLog because we use it as a sentinel value to indicate
// that the user-provided startup script is done. If we pass StartupScriptDoneLog as-is, it
// will be printed early in the instance console log (before the startup script even runs;
// 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
echo %s
RETVAL=0
GetMetadata () {
echo "$(curl -f -H "Metadata-Flavor: Google" http://metadata/computeMetadata/v1/instance/attributes/$1 2> /dev/null)"
}
STARTUPSCRIPT=$(GetMetadata %s)
STARTUPSCRIPTPATH=/packer-wrapped-startup-script
if [ -f "/var/log/startupscript.log" ]; then
STARTUPSCRIPTLOGPATH=/var/log/startupscript.log
else
STARTUPSCRIPTLOGPATH=/var/log/daemon.log
fi
STARTUPSCRIPTLOGDEST=$(GetMetadata startup-script-log-dest)
if [[ ! -z $STARTUPSCRIPT ]]; then
echo "Executing user-provided startup script..."
echo "${STARTUPSCRIPT}" > ${STARTUPSCRIPTPATH}
chmod +x ${STARTUPSCRIPTPATH}
${STARTUPSCRIPTPATH}
RETVAL=$?
if [[ ! -z $STARTUPSCRIPTLOGDEST ]]; then
echo "Uploading user-provided startup script log to ${STARTUPSCRIPTLOGDEST}..."
gsutil -h "Content-Type:text/plain" cp ${STARTUPSCRIPTLOGPATH} ${STARTUPSCRIPTLOGDEST}
fi
rm ${STARTUPSCRIPTPATH}
fi
echo $(echo %s | base64 --decode)
exit $RETVAL
`, StartupScriptStartLog, StartupWrappedScriptKey, StartupScriptDoneLogBase64)

View File

@ -23,7 +23,8 @@ func (s *StepCreateImage) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
ui.Say("Creating image...")
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
select {
case err = <-errCh:
@ -38,7 +39,7 @@ func (s *StepCreateImage) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionHalt
}
state.Put("image_name", config.ImageName)
state.Put("image", <-imageCh)
return multistep.ActionContinue
}

View File

@ -18,13 +18,35 @@ func TestStepCreateImage(t *testing.T) {
config := state.Get("config").(*Config)
driver := state.Get("driver").(*DriverMock)
driver.CreateImageProjectId = "createimage-project"
driver.CreateImageSizeGb = 100
// run the step
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
uncastImage, ok := state.GetOk("image")
if !ok {
t.Fatal("should have image")
}
image, ok := uncastImage.(Image)
if !ok {
t.Fatal("image is not an Image")
}
// Verify created Image results.
if image.Name != config.ImageName {
t.Fatalf("Created image name, %s, does not match config name, %s.", image.Name, config.ImageName)
}
if driver.CreateImageProjectId != image.ProjectId {
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 state
// Verify proper args passed to driver.CreateImage.
if driver.CreateImageName != config.ImageName {
t.Fatalf("bad: %#v", driver.CreateImageName)
}
@ -40,16 +62,6 @@ func TestStepCreateImage(t *testing.T) {
if driver.CreateImageDisk != config.DiskName {
t.Fatalf("bad: %#v", driver.CreateImageDisk)
}
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 TestStepCreateImage_errorOnChannel(t *testing.T) {

View File

@ -3,6 +3,7 @@ package googlecompute
import (
"errors"
"fmt"
"io/ioutil"
"time"
"github.com/mitchellh/multistep"
@ -22,23 +23,34 @@ func (config *Config) getImage() Image {
return Image{Name: config.SourceImage, ProjectId: project}
}
func (config *Config) getInstanceMetadata(sshPublicKey string) map[string]string {
func (config *Config) getInstanceMetadata(sshPublicKey string) (map[string]string, error) {
instanceMetadata := make(map[string]string)
var err error
// Copy metadata from config
// Copy metadata from config.
for k, v := range config.Metadata {
instanceMetadata[k] = v
}
// Merge any existing ssh keys with our public key
// Merge any existing ssh keys with our public key.
sshMetaKey := "sshKeys"
sshKeys := fmt.Sprintf("%s:%s", config.Comm.SSHUsername, sshPublicKey)
if confSshKeys, exists := instanceMetadata[sshMetaKey]; exists {
sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSshKeys)
}
instanceMetadata[sshMetaKey] = sshKeys
// Wrap any startup script with our own startup script.
if config.StartupScriptFile != "" {
var content []byte
content, err = ioutil.ReadFile(config.StartupScriptFile)
instanceMetadata[StartupWrappedScriptKey] = string(content)
} else if wrappedStartupScript, exists := instanceMetadata[StartupScriptKey]; exists {
instanceMetadata[StartupWrappedScriptKey] = wrappedStartupScript
}
instanceMetadata[StartupScriptKey] = StartupScript
return instanceMetadata
return instanceMetadata, err
}
// Run executes the Packer build step that creates a GCE instance.
@ -51,21 +63,26 @@ func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction
ui.Say("Creating instance...")
name := config.InstanceName
errCh, err := driver.RunInstance(&InstanceConfig{
Description: "New instance created by Packer",
DiskSizeGb: config.DiskSizeGb,
DiskType: config.DiskType,
Image: config.getImage(),
MachineType: config.MachineType,
Metadata: config.getInstanceMetadata(sshPublicKey),
Name: name,
Network: config.Network,
Subnetwork: config.Subnetwork,
Address: config.Address,
Preemptible: config.Preemptible,
Tags: config.Tags,
Region: config.Region,
Zone: config.Zone,
var errCh <-chan error
var err error
var metadata map[string]string
metadata, err = config.getInstanceMetadata(sshPublicKey)
errCh, err = driver.RunInstance(&InstanceConfig{
Address: config.Address,
Description: "New instance created by Packer",
DiskSizeGb: config.DiskSizeGb,
DiskType: config.DiskType,
Image: config.getImage(),
MachineType: config.MachineType,
Metadata: metadata,
Name: name,
Network: config.Network,
Preemptible: config.Preemptible,
Region: config.Region,
ServiceAccountEmail: config.account.ClientEmail,
Subnetwork: config.Subnetwork,
Tags: config.Tags,
Zone: config.Zone,
})
if err == nil {

View File

@ -20,6 +20,8 @@ type StepCreateSSHKey struct {
}
// Run executes the Packer build step that generates SSH key pairs.
// The key pairs are added to the multistep state as "ssh_private_key" and
// "ssh_public_key".
func (s *StepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)

View File

@ -17,6 +17,7 @@ type StepInstanceInfo struct {
}
// Run executes the Packer build step that gathers GCE instance info.
// This adds "instance_ip" to the multistep state.
func (s *StepInstanceInfo) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
driver := state.Get("driver").(Driver)

View File

@ -27,6 +27,8 @@ func (s *StepTeardownInstance) Run(state multistep.StateBag) multistep.StepActio
}
ui.Say("Deleting instance...")
instanceLog, _ := driver.GetSerialPortOutput(config.Zone, name)
state.Put("instance_log", instanceLog)
errCh, err := driver.DeleteInstance(config.Zone, name)
if err == nil {
select {
@ -43,7 +45,6 @@ func (s *StepTeardownInstance) Run(state multistep.StateBag) multistep.StepActio
"Error: %s", name, err))
return multistep.ActionHalt
}
ui.Message("Instance has been deleted!")
state.Put("instance_name", "")

View File

@ -0,0 +1,50 @@
package googlecompute
import(
"fmt"
"strings"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type StepWaitInstanceStartup int
// Run reads the instance serial port output and looks for the log entry indicating the startup script finished.
func (s *StepWaitInstanceStartup) 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)
ui.Say("Waiting for any running startup script to finish...")
// Keep checking the serial port output to see if the startup script is done.
err := Retry(10, 60, 0, func() (bool, error) {
output, err := driver.GetSerialPortOutput(config.Zone, instanceName)
if err != nil {
err := fmt.Errorf("Error getting serial port output: %s", err)
return false, err
}
done := strings.Contains(output, StartupScriptDoneLog)
if !done {
ui.Say("Startup script not finished yet. Waiting...")
}
return done, nil
})
if err != nil {
err := fmt.Errorf("Error waiting for startup script to finish: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Startup script, if any, has finished running.")
return multistep.ActionContinue
}
// Cleanup.
func (s *StepWaitInstanceStartup) Cleanup(state multistep.StateBag) {}

View File

@ -0,0 +1,38 @@
package googlecompute
import (
"github.com/mitchellh/multistep"
"testing"
)
func TestStepWaitInstanceStartup(t *testing.T) {
state := testState(t)
step := new(StepWaitInstanceStartup)
config := state.Get("config").(*Config)
driver := state.Get("driver").(*DriverMock)
testZone := "test-zone"
testInstanceName := "test-instance-name"
config.Zone = testZone
state.Put("instance_name", testInstanceName)
// The done log triggers step completion.
driver.GetSerialPortOutputResult = StartupScriptDoneLog
// Run the step.
if action := step.Run(state); action != multistep.ActionContinue {
t.Fatalf("StepWaitInstanceStartup did not return a Continue action: %#v", action)
}
// Check that GetSerialPortOutput was called properly.
if driver.GetSerialPortOutputZone != testZone {
t.Fatalf(
"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)
}
}

View File

@ -1,8 +1,9 @@
---
description: |
The `googlecompute` Packer builder is able to create images for use with Google
Compute Engine (GCE) based on existing images. Google Compute Engine doesn't
allow the creation of images from scratch.
Compute Engine (GCE) based on existing images. Building GCE images from scratch
is not possible from Packer at this time. For building images from scratch, please see
[Building GCE Images from Scratch](https://cloud.google.com/compute/docs/tutorials/building-images).
layout: docs
page_title: Google Compute Builder
...
@ -14,9 +15,9 @@ Type: `googlecompute`
The `googlecompute` Packer builder is able to create
[images](https://developers.google.com/compute/docs/images) for use with [Google
Compute Engine](https://cloud.google.com/products/compute-engine)(GCE) based on
existing images. Google Compute Engine doesn't allow the creation of images from
scratch.
existing images. Building GCE images from scratch is not possible from Packer at
this time. For building images from scratch, please see
[Building GCE Images from Scratch](https://cloud.google.com/compute/docs/tutorials/building-images).
## Authentication
Authenticating with Google Cloud services requires at most one JSON file, called
@ -76,10 +77,10 @@ straightforwarded, it is documented here.
## Basic Example
Below is a fully functioning example. It doesn't do anything useful, since no
provisioners are defined, but it will effectively repackage an existing GCE
image. The account_file is obtained in the previous section. If it parses as
JSON it is assumed to be the file itself, otherwise it is assumed to be
the path to the file containing the JSON.
provisioners or startup-script metadata are defined, but it will effectively
repackage an existing GCE image. The account_file is obtained in the previous
section. If it parses as JSON it is assumed to be the file itself, otherwise it
is assumed to be the path to the file containing the JSON.
``` {.javascript}
{
@ -150,6 +151,9 @@ builder.
- `region` (string) - The region in which to launch the instance. Defaults to
to the region hosting the specified `zone`.
- `startup_script_file` (string) - The filepath to a startup script to run on
the VM from which the image will be made.
- `state_timeout` (string) - The time to wait for instance state changes.
Defaults to `"5m"`.
@ -163,6 +167,27 @@ builder.
- `use_internal_ip` (boolean) - If true, use the instance's internal IP
instead of its external IP during building.
## Startup Scripts
Startup scripts can be a powerful tool for configuring the instance from which the image is made.
The builder will wait for a startup script to terminate. A startup script can be provided via the
`startup_script_file` or 'startup-script' instance creation `metadata` field. Therefore, the build
time will vary depending on the duration of the startup script. If `startup_script_file` is set,
the 'startup-script' `metadata` field will be overwritten. In other words,`startup_script_file`
takes precedence.
The builder does not check for a pass/fail/error signal from the startup script, at this time. Until
such support is implemented, startup scripts should be robust, as an image will still be built even
when a startup script fails.
### Windows
Startup scripts do not work on Windows builds, at this time.
### Logging
Startup script logs can be copied to a Google Cloud Storage (GCS) location specified via the
'startup-script-log-dest' instance creation `metadata` field. The GCS location must be writeable by
the credentials provided in the builder config's `account_file`.
## Gotchas