Adding support for googlecompute startup scripts.
- Startup scripts can be provided through the instance creation metadata field 'startup-script'. - Script log can be copied to a GCS location by setting the metadata field 'startup-script-log-dest'. Added Retry method to googlecompute package. Added GetSerialPortOutput to googlecompute Drivers. Added StepWaitInstanceStartup (and associated test) which waits for an instance startup-script to finish. Changed the instance service account to use the same service account as the one provided in the Packer config template. It was the project default service account. Tested googlecompute package with 'go test' and also performed builds with a startup script and without a startup script.
This commit is contained in:
parent
37fe764727
commit
7190fbeed8
|
@ -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 {
|
||||||
imageName string
|
image Image
|
||||||
driver Driver
|
driver Driver
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,8 +18,8 @@ func (*Artifact) BuilderId() string {
|
||||||
|
|
||||||
// Destroy destroys the GCE image represented by the artifact.
|
// Destroy destroys the GCE image represented by the artifact.
|
||||||
func (a *Artifact) Destroy() error {
|
func (a *Artifact) Destroy() error {
|
||||||
log.Printf("Destroying image: %s", a.imageName)
|
log.Printf("Destroying image: %s", a.image.Name)
|
||||||
errCh := a.driver.DeleteImage(a.imageName)
|
errCh := a.driver.DeleteImage(a.image.Name)
|
||||||
return <-errCh
|
return <-errCh
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,12 +30,12 @@ func (*Artifact) Files() []string {
|
||||||
|
|
||||||
// Id returns the GCE image name.
|
// Id returns the GCE image name.
|
||||||
func (a *Artifact) Id() string {
|
func (a *Artifact) Id() string {
|
||||||
return a.imageName
|
return a.image.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the string representation of the artifact.
|
// String returns the string representation of the artifact.
|
||||||
func (a *Artifact) String() string {
|
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{} {
|
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,
|
SSHConfig: sshConfig,
|
||||||
},
|
},
|
||||||
new(common.StepProvision),
|
new(common.StepProvision),
|
||||||
|
new(StepWaitInstanceStartup),
|
||||||
new(StepTeardownInstance),
|
new(StepTeardownInstance),
|
||||||
new(StepCreateImage),
|
new(StepCreateImage),
|
||||||
}
|
}
|
||||||
|
@ -86,13 +87,13 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||||
if rawErr, ok := state.GetOk("error"); ok {
|
if rawErr, ok := state.GetOk("error"); ok {
|
||||||
return nil, rawErr.(error)
|
return nil, rawErr.(error)
|
||||||
}
|
}
|
||||||
if _, ok := state.GetOk("image_name"); !ok {
|
if _, ok := state.GetOk("image"); !ok {
|
||||||
log.Println("Failed to find image_name in state. Bug?")
|
log.Println("Failed to find image in state. Bug?")
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
artifact := &Artifact{
|
artifact := &Artifact{
|
||||||
imageName: state.Get("image_name").(string),
|
image: state.Get("image").(Image),
|
||||||
driver: driver,
|
driver: driver,
|
||||||
}
|
}
|
||||||
return artifact, nil
|
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"`
|
AccountFile string `mapstructure:"account_file"`
|
||||||
ProjectId string `mapstructure:"project_id"`
|
ProjectId string `mapstructure:"project_id"`
|
||||||
|
|
||||||
|
Address string `mapstructure:"address"`
|
||||||
DiskName string `mapstructure:"disk_name"`
|
DiskName string `mapstructure:"disk_name"`
|
||||||
DiskSizeGb int64 `mapstructure:"disk_size"`
|
DiskSizeGb int64 `mapstructure:"disk_size"`
|
||||||
DiskType string `mapstructure:"disk_type"`
|
DiskType string `mapstructure:"disk_type"`
|
||||||
|
@ -36,15 +37,15 @@ type Config struct {
|
||||||
MachineType string `mapstructure:"machine_type"`
|
MachineType string `mapstructure:"machine_type"`
|
||||||
Metadata map[string]string `mapstructure:"metadata"`
|
Metadata map[string]string `mapstructure:"metadata"`
|
||||||
Network string `mapstructure:"network"`
|
Network string `mapstructure:"network"`
|
||||||
Subnetwork string `mapstructure:"subnetwork"`
|
|
||||||
Address string `mapstructure:"address"`
|
|
||||||
Preemptible bool `mapstructure:"preemptible"`
|
Preemptible bool `mapstructure:"preemptible"`
|
||||||
|
RawStateTimeout string `mapstructure:"state_timeout"`
|
||||||
|
Region string `mapstructure:"region"`
|
||||||
SourceImage string `mapstructure:"source_image"`
|
SourceImage string `mapstructure:"source_image"`
|
||||||
SourceImageProjectId string `mapstructure:"source_image_project_id"`
|
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"`
|
Tags []string `mapstructure:"tags"`
|
||||||
UseInternalIP bool `mapstructure:"use_internal_ip"`
|
UseInternalIP bool `mapstructure:"use_internal_ip"`
|
||||||
Region string `mapstructure:"region"`
|
|
||||||
Zone string `mapstructure:"zone"`
|
Zone string `mapstructure:"zone"`
|
||||||
|
|
||||||
account accountFile
|
account accountFile
|
||||||
|
|
|
@ -10,7 +10,7 @@ type Driver interface {
|
||||||
|
|
||||||
// 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 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,11 +21,14 @@ 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)
|
||||||
|
|
||||||
|
// 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 gets the NAT IP address for the instance.
|
||||||
GetNatIP(zone, name string) (string, error)
|
GetNatIP(zone, name string) (string, error)
|
||||||
|
|
||||||
// GetInternalIP gets the GCE-internal IP address for the instance.
|
// GetSerialPortOutput gets the Serial Port contents for the instance.
|
||||||
GetInternalIP(zone, name string) (string, error)
|
GetSerialPortOutput(zone, name string) (string, error)
|
||||||
|
|
||||||
// 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,9 +40,11 @@ type Driver interface {
|
||||||
type Image struct {
|
type Image struct {
|
||||||
Name string
|
Name string
|
||||||
ProjectId string
|
ProjectId string
|
||||||
|
SizeGb int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type InstanceConfig struct {
|
type InstanceConfig struct {
|
||||||
|
Address string
|
||||||
Description string
|
Description string
|
||||||
DiskSizeGb int64
|
DiskSizeGb int64
|
||||||
DiskType string
|
DiskType string
|
||||||
|
@ -48,10 +53,10 @@ type InstanceConfig struct {
|
||||||
Metadata map[string]string
|
Metadata map[string]string
|
||||||
Name string
|
Name string
|
||||||
Network string
|
Network string
|
||||||
Subnetwork string
|
|
||||||
Address string
|
|
||||||
Preemptible bool
|
Preemptible bool
|
||||||
Tags []string
|
|
||||||
Region string
|
Region string
|
||||||
|
ServiceAccountEmail string
|
||||||
|
Subnetwork string
|
||||||
|
Tags []string
|
||||||
Zone string
|
Zone string
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
"github.com/mitchellh/packer/version"
|
"github.com/mitchellh/packer/version"
|
||||||
|
@ -91,7 +90,7 @@ func (d *driverGCE) ImageExists(name string) bool {
|
||||||
return err == nil
|
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{
|
image := &compute.Image{
|
||||||
Description: description,
|
Description: description,
|
||||||
Name: name,
|
Name: name,
|
||||||
|
@ -100,15 +99,32 @@ func (d *driverGCE) CreateImage(name, description, family, zone, disk string) <-
|
||||||
SourceType: "RAW",
|
SourceType: "RAW",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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, image).Do()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errCh <- err
|
errCh <- err
|
||||||
} else {
|
} 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 {
|
func (d *driverGCE) DeleteImage(name string) <-chan error {
|
||||||
|
@ -181,6 +197,15 @@ func (d *driverGCE) GetInternalIP(zone, name string) (string, error) {
|
||||||
return "", nil
|
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) {
|
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))
|
||||||
|
@ -191,7 +216,7 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
|
||||||
|
|
||||||
// Get the image
|
// Get the image
|
||||||
d.ui.Message(fmt.Sprintf("Loading image: %s in project %s", c.Image.Name, c.Image.ProjectId))
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -294,7 +319,7 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
|
||||||
},
|
},
|
||||||
ServiceAccounts: []*compute.ServiceAccount{
|
ServiceAccounts: []*compute.ServiceAccount{
|
||||||
&compute.ServiceAccount{
|
&compute.ServiceAccount{
|
||||||
Email: "default",
|
Email: c.ServiceAccountEmail,
|
||||||
Scopes: []string{
|
Scopes: []string{
|
||||||
"https://www.googleapis.com/auth/userinfo.email",
|
"https://www.googleapis.com/auth/userinfo.email",
|
||||||
"https://www.googleapis.com/auth/compute",
|
"https://www.googleapis.com/auth/compute",
|
||||||
|
@ -324,17 +349,17 @@ func (d *driverGCE) WaitForInstance(state, zone, name string) <-chan error {
|
||||||
return errCh
|
return errCh
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *driverGCE) getImage(img Image) (image *compute.Image, err error) {
|
func (d *driverGCE) getImage(name, projectId string) (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"}
|
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 {
|
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 != "" {
|
if err == nil && image != nil && image.SelfLink != "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
image = nil
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -396,18 +421,16 @@ type stateRefreshFunc func() (string, error)
|
||||||
|
|
||||||
// waitForState will spin in a loop forever waiting for state to
|
// waitForState will spin in a loop forever waiting for state to
|
||||||
// reach a certain target.
|
// reach a certain target.
|
||||||
func waitForState(errCh chan<- error, target string, refresh stateRefreshFunc) {
|
func waitForState(errCh chan<- error, target string, refresh stateRefreshFunc) error {
|
||||||
for {
|
err := Retry(2, 2, 0, func() (bool, error) {
|
||||||
state, err := refresh()
|
state, err := refresh()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
} else if state == target {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
})
|
||||||
errCh <- err
|
errCh <- err
|
||||||
return
|
return err
|
||||||
}
|
|
||||||
if state == target {
|
|
||||||
errCh <- nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,10 @@ type DriverMock struct {
|
||||||
CreateImageFamily string
|
CreateImageFamily string
|
||||||
CreateImageZone string
|
CreateImageZone string
|
||||||
CreateImageDisk string
|
CreateImageDisk string
|
||||||
|
CreateImageProjectId string
|
||||||
|
CreateImageSizeGb int64
|
||||||
CreateImageErrCh <-chan error
|
CreateImageErrCh <-chan error
|
||||||
|
CreateImageResultCh <-chan Image
|
||||||
|
|
||||||
DeleteImageName string
|
DeleteImageName string
|
||||||
DeleteImageErrCh <-chan error
|
DeleteImageErrCh <-chan error
|
||||||
|
@ -36,6 +39,11 @@ type DriverMock struct {
|
||||||
GetInternalIPResult string
|
GetInternalIPResult string
|
||||||
GetInternalIPErr error
|
GetInternalIPErr error
|
||||||
|
|
||||||
|
GetSerialPortOutputZone string
|
||||||
|
GetSerialPortOutputName string
|
||||||
|
GetSerialPortOutputResult string
|
||||||
|
GetSerialPortOutputErr error
|
||||||
|
|
||||||
RunInstanceConfig *InstanceConfig
|
RunInstanceConfig *InstanceConfig
|
||||||
RunInstanceErrCh <-chan error
|
RunInstanceErrCh <-chan error
|
||||||
RunInstanceErr error
|
RunInstanceErr error
|
||||||
|
@ -51,21 +59,39 @@ func (d *DriverMock) ImageExists(name string) bool {
|
||||||
return d.ImageExistsResult
|
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.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 {
|
||||||
|
d.CreateImageSizeGb = 10
|
||||||
|
}
|
||||||
|
if d.CreateImageProjectId == "" {
|
||||||
|
d.CreateImageProjectId = "test"
|
||||||
|
}
|
||||||
|
|
||||||
resultCh := d.CreateImageErrCh
|
resultCh := d.CreateImageResultCh
|
||||||
if resultCh == nil {
|
if resultCh == nil {
|
||||||
ch := make(chan error)
|
ch := make(chan Image, 1)
|
||||||
|
ch <- Image{
|
||||||
|
Name: name,
|
||||||
|
ProjectId: d.CreateImageProjectId,
|
||||||
|
SizeGb: d.CreateImageSizeGb,
|
||||||
|
}
|
||||||
close(ch)
|
close(ch)
|
||||||
resultCh = 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 {
|
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
|
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) {
|
func (d *DriverMock) RunInstance(c *InstanceConfig) (<-chan error, error) {
|
||||||
d.RunInstanceConfig = c
|
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 := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
ui.Say("Creating image...")
|
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
|
var err error
|
||||||
select {
|
select {
|
||||||
case err = <-errCh:
|
case err = <-errCh:
|
||||||
|
@ -38,7 +39,7 @@ func (s *StepCreateImage) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
}
|
}
|
||||||
|
|
||||||
state.Put("image_name", config.ImageName)
|
state.Put("image", <-imageCh)
|
||||||
return multistep.ActionContinue
|
return multistep.ActionContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,13 +18,35 @@ func TestStepCreateImage(t *testing.T) {
|
||||||
|
|
||||||
config := state.Get("config").(*Config)
|
config := state.Get("config").(*Config)
|
||||||
driver := state.Get("driver").(*DriverMock)
|
driver := state.Get("driver").(*DriverMock)
|
||||||
|
driver.CreateImageProjectId = "createimage-project"
|
||||||
|
driver.CreateImageSizeGb = 100
|
||||||
|
|
||||||
// run the step
|
// run the step
|
||||||
if action := step.Run(state); action != multistep.ActionContinue {
|
if action := step.Run(state); action != multistep.ActionContinue {
|
||||||
t.Fatalf("bad action: %#v", action)
|
t.Fatalf("bad action: %#v", action)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify state
|
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 proper args passed to driver.CreateImage.
|
||||||
if driver.CreateImageName != config.ImageName {
|
if driver.CreateImageName != config.ImageName {
|
||||||
t.Fatalf("bad: %#v", driver.CreateImageName)
|
t.Fatalf("bad: %#v", driver.CreateImageName)
|
||||||
}
|
}
|
||||||
|
@ -40,16 +62,6 @@ func TestStepCreateImage(t *testing.T) {
|
||||||
if driver.CreateImageDisk != config.DiskName {
|
if driver.CreateImageDisk != config.DiskName {
|
||||||
t.Fatalf("bad: %#v", driver.CreateImageDisk)
|
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) {
|
func TestStepCreateImage_errorOnChannel(t *testing.T) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package googlecompute
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
|
@ -22,15 +23,16 @@ func (config *Config) getImage() Image {
|
||||||
return Image{Name: config.SourceImage, ProjectId: project}
|
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)
|
instanceMetadata := make(map[string]string)
|
||||||
|
var err error
|
||||||
|
|
||||||
// Copy metadata from config
|
// Copy metadata from config.
|
||||||
for k, v := range config.Metadata {
|
for k, v := range config.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", config.Comm.SSHUsername, sshPublicKey)
|
||||||
if confSshKeys, exists := instanceMetadata[sshMetaKey]; exists {
|
if confSshKeys, exists := instanceMetadata[sshMetaKey]; exists {
|
||||||
|
@ -38,7 +40,17 @@ func (config *Config) getInstanceMetadata(sshPublicKey string) map[string]string
|
||||||
}
|
}
|
||||||
instanceMetadata[sshMetaKey] = sshKeys
|
instanceMetadata[sshMetaKey] = sshKeys
|
||||||
|
|
||||||
return instanceMetadata
|
// 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, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run executes the Packer build step that creates a GCE instance.
|
// Run executes the Packer build step that creates a GCE instance.
|
||||||
|
@ -51,20 +63,25 @@ func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction
|
||||||
ui.Say("Creating instance...")
|
ui.Say("Creating instance...")
|
||||||
name := config.InstanceName
|
name := config.InstanceName
|
||||||
|
|
||||||
errCh, err := driver.RunInstance(&InstanceConfig{
|
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",
|
Description: "New instance created by Packer",
|
||||||
DiskSizeGb: config.DiskSizeGb,
|
DiskSizeGb: config.DiskSizeGb,
|
||||||
DiskType: config.DiskType,
|
DiskType: config.DiskType,
|
||||||
Image: config.getImage(),
|
Image: config.getImage(),
|
||||||
MachineType: config.MachineType,
|
MachineType: config.MachineType,
|
||||||
Metadata: config.getInstanceMetadata(sshPublicKey),
|
Metadata: metadata,
|
||||||
Name: name,
|
Name: name,
|
||||||
Network: config.Network,
|
Network: config.Network,
|
||||||
Subnetwork: config.Subnetwork,
|
|
||||||
Address: config.Address,
|
|
||||||
Preemptible: config.Preemptible,
|
Preemptible: config.Preemptible,
|
||||||
Tags: config.Tags,
|
|
||||||
Region: config.Region,
|
Region: config.Region,
|
||||||
|
ServiceAccountEmail: config.account.ClientEmail,
|
||||||
|
Subnetwork: config.Subnetwork,
|
||||||
|
Tags: config.Tags,
|
||||||
Zone: config.Zone,
|
Zone: config.Zone,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,8 @@ type StepCreateSSHKey struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run executes the Packer build step that generates SSH key pairs.
|
// 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 {
|
func (s *StepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ type StepInstanceInfo struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run executes the Packer build step that gathers GCE instance info.
|
// 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 {
|
func (s *StepInstanceInfo) 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)
|
||||||
|
|
|
@ -27,6 +27,8 @@ func (s *StepTeardownInstance) Run(state multistep.StateBag) multistep.StepActio
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.Say("Deleting instance...")
|
ui.Say("Deleting instance...")
|
||||||
|
instanceLog, _ := driver.GetSerialPortOutput(config.Zone, name)
|
||||||
|
state.Put("instance_log", instanceLog)
|
||||||
errCh, err := driver.DeleteInstance(config.Zone, name)
|
errCh, err := driver.DeleteInstance(config.Zone, name)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
select {
|
select {
|
||||||
|
@ -43,7 +45,6 @@ func (s *StepTeardownInstance) Run(state multistep.StateBag) multistep.StepActio
|
||||||
"Error: %s", name, err))
|
"Error: %s", name, err))
|
||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.Message("Instance has been deleted!")
|
ui.Message("Instance has been deleted!")
|
||||||
state.Put("instance_name", "")
|
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: |
|
description: |
|
||||||
The `googlecompute` Packer builder is able to create images for use with Google
|
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
|
Compute Engine (GCE) based on existing images. Building GCE images from scratch
|
||||||
allow the creation of 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
|
layout: docs
|
||||||
page_title: Google Compute Builder
|
page_title: Google Compute Builder
|
||||||
...
|
...
|
||||||
|
@ -14,9 +15,9 @@ Type: `googlecompute`
|
||||||
The `googlecompute` Packer builder is able to create
|
The `googlecompute` Packer builder is able to create
|
||||||
[images](https://developers.google.com/compute/docs/images) for use with [Google
|
[images](https://developers.google.com/compute/docs/images) for use with [Google
|
||||||
Compute Engine](https://cloud.google.com/products/compute-engine)(GCE) based on
|
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
|
existing images. Building GCE images from scratch is not possible from Packer at
|
||||||
scratch.
|
this time. For building images from scratch, please see
|
||||||
|
[Building GCE Images from Scratch](https://cloud.google.com/compute/docs/tutorials/building-images).
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
Authenticating with Google Cloud services requires at most one JSON file, called
|
Authenticating with Google Cloud services requires at most one JSON file, called
|
||||||
|
@ -76,10 +77,10 @@ straightforwarded, it is documented here.
|
||||||
## Basic Example
|
## Basic Example
|
||||||
|
|
||||||
Below is a fully functioning example. It doesn't do anything useful, since no
|
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
|
provisioners or startup-script metadata are defined, but it will effectively
|
||||||
image. The account_file is obtained in the previous section. If it parses as
|
repackage an existing GCE image. The account_file is obtained in the previous
|
||||||
JSON it is assumed to be the file itself, otherwise it is assumed to be
|
section. If it parses as JSON it is assumed to be the file itself, otherwise it
|
||||||
the path to the file containing the JSON.
|
is assumed to be the path to the file containing the JSON.
|
||||||
|
|
||||||
``` {.javascript}
|
``` {.javascript}
|
||||||
{
|
{
|
||||||
|
@ -150,6 +151,9 @@ builder.
|
||||||
- `region` (string) - The region in which to launch the instance. Defaults to
|
- `region` (string) - The region in which to launch the instance. Defaults to
|
||||||
to the region hosting the specified `zone`.
|
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.
|
- `state_timeout` (string) - The time to wait for instance state changes.
|
||||||
Defaults to `"5m"`.
|
Defaults to `"5m"`.
|
||||||
|
|
||||||
|
@ -164,6 +168,27 @@ builder.
|
||||||
- `use_internal_ip` (boolean) - If true, use the instance's internal IP
|
- `use_internal_ip` (boolean) - If true, use the instance's internal IP
|
||||||
instead of its external IP during building.
|
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
|
## Gotchas
|
||||||
|
|
||||||
Centos and recent Debian images have root ssh access disabled by default. Set `ssh_username` to
|
Centos and recent Debian images have root ssh access disabled by default. Set `ssh_username` to
|
||||||
|
|
Loading…
Reference in New Issue