Merge pull request #3639 from crunk1/master
Adding support for googlecompute startup scripts.
This commit is contained in:
commit
95cffcae78
|
@ -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{} {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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", "")
|
||||
|
||||
|
|
|
@ -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) {}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue