Some googlecompute fixes and cleanup. Addresses https://github.com/mitchellh/packer/issues/3829. Changes:
- startup scripts don't run for Windows since it is isn't implemented yet. - startup scripts use instance metadata instead of serial port output to flag when they are done. - added licenses to Image data type (to check if an Image is a Windows Image). - added GetImage and GetImageFromProject to googlecompute Drivers. - changed some of the builder/googlecompute tests to use github.com/stretchr/testify/assert. Tests: - (in the Packer directory) `go test .`, `go test ./builder/googlecompute`, and `go test ./post-processor/googlecompute-export` - manual run of `packer build packer_template.json` with the following files --packer_template.json-- { "builders": [ { "type": "googlecompute", "account_file": "creds.json", "project_id": "google.com:packer-test", "source_image": "debian-8-jessie-v20160629", "zone": "us-central1-a", "startup_script_file": "startup_script.sh", "metadata": { "startup-script": "#!/bin/sh\necho \"This should be overwritten.\"", "startup-script-log-dest": "gs://packer-test.google.com.a.appspot.com/startup-script.log" }, "image_name": "test-packer-modifications", "ssh_username": "foo" } ], "post-processors": [ { "type": "googlecompute-export", "paths": [ "gs://packer-test.google.com.a.appspot.com/foo.tar.gz", "gs://packer-test.google.com.a.appspot.com/bar.tar.gz" ], "keep_input_artifact": true } ] } --startup_script.sh-- \#!/bin/sh echo "Hi, my name is Scott. I'm waiting 60 seconds!" >> /scott sleep 60 echo "I'm done waiting!" >> /scott
This commit is contained in:
parent
9dc7ce52cf
commit
b54b82d3ac
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
// Artifact represents a GCE image as the result of a Packer build.
|
// Artifact represents a GCE image as the result of a Packer build.
|
||||||
type Artifact struct {
|
type Artifact struct {
|
||||||
image Image
|
image *Image
|
||||||
driver Driver
|
driver Driver
|
||||||
config *Config
|
config *Config
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||||
}
|
}
|
||||||
|
|
||||||
artifact := &Artifact{
|
artifact := &Artifact{
|
||||||
image: state.Get("image").(Image),
|
image: state.Get("image").(*Image),
|
||||||
driver: driver,
|
driver: driver,
|
||||||
config: b.config,
|
config: b.config,
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,9 @@ package googlecompute
|
||||||
// with GCE. The Driver interface exists mostly to allow a mock implementation
|
// with GCE. The Driver interface exists mostly to allow a mock implementation
|
||||||
// to be used to test the steps.
|
// to be used to test the steps.
|
||||||
type Driver interface {
|
type Driver interface {
|
||||||
// ImageExists returns true if the specified image exists. If an error
|
|
||||||
// occurs calling the API, this method returns false.
|
|
||||||
ImageExists(name string) bool
|
|
||||||
|
|
||||||
// CreateImage creates an image from the given disk in Google Compute
|
// CreateImage creates an image from the given disk in Google Compute
|
||||||
// Engine.
|
// Engine.
|
||||||
CreateImage(name, description, family, zone, disk string) (<-chan Image, <-chan error)
|
CreateImage(name, description, family, zone, disk string) (<-chan *Image, <-chan error)
|
||||||
|
|
||||||
// DeleteImage deletes the image with the given name.
|
// DeleteImage deletes the image with the given name.
|
||||||
DeleteImage(name string) <-chan error
|
DeleteImage(name string) <-chan error
|
||||||
|
@ -21,6 +17,15 @@ type Driver interface {
|
||||||
// DeleteDisk deletes the disk with the given name.
|
// DeleteDisk deletes the disk with the given name.
|
||||||
DeleteDisk(zone, name string) (<-chan error, error)
|
DeleteDisk(zone, name string) (<-chan error, error)
|
||||||
|
|
||||||
|
// GetImage gets an image; tries the default and public projects.
|
||||||
|
GetImage(name string) (*Image, error)
|
||||||
|
|
||||||
|
// GetImageFromProject gets an image from a specific project.
|
||||||
|
GetImageFromProject(project, name string) (*Image, error)
|
||||||
|
|
||||||
|
// GetInstanceMetadata gets a metadata variable for the instance, name.
|
||||||
|
GetInstanceMetadata(zone, name, key string) (string, error)
|
||||||
|
|
||||||
// GetInternalIP gets the GCE-internal IP address for the instance.
|
// GetInternalIP gets the GCE-internal IP address for the instance.
|
||||||
GetInternalIP(zone, name string) (string, error)
|
GetInternalIP(zone, name string) (string, error)
|
||||||
|
|
||||||
|
@ -30,6 +35,10 @@ type Driver interface {
|
||||||
// GetSerialPortOutput gets the Serial Port contents for the instance.
|
// GetSerialPortOutput gets the Serial Port contents for the instance.
|
||||||
GetSerialPortOutput(zone, name string) (string, error)
|
GetSerialPortOutput(zone, name string) (string, error)
|
||||||
|
|
||||||
|
// ImageExists returns true if the specified image exists. If an error
|
||||||
|
// occurs calling the API, this method returns false.
|
||||||
|
ImageExists(name string) bool
|
||||||
|
|
||||||
// RunInstance takes the given config and launches an instance.
|
// RunInstance takes the given config and launches an instance.
|
||||||
RunInstance(*InstanceConfig) (<-chan error, error)
|
RunInstance(*InstanceConfig) (<-chan error, error)
|
||||||
|
|
||||||
|
@ -37,18 +46,12 @@ type Driver interface {
|
||||||
WaitForInstance(state, zone, name string) <-chan error
|
WaitForInstance(state, zone, name string) <-chan error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Image struct {
|
|
||||||
Name string
|
|
||||||
ProjectId string
|
|
||||||
SizeGb int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type InstanceConfig struct {
|
type InstanceConfig struct {
|
||||||
Address string
|
Address string
|
||||||
Description string
|
Description string
|
||||||
DiskSizeGb int64
|
DiskSizeGb int64
|
||||||
DiskType string
|
DiskType string
|
||||||
Image Image
|
Image *Image
|
||||||
MachineType string
|
MachineType string
|
||||||
Metadata map[string]string
|
Metadata map[string]string
|
||||||
Name string
|
Name string
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
"github.com/mitchellh/packer/version"
|
"github.com/mitchellh/packer/version"
|
||||||
|
@ -13,7 +14,6 @@ import (
|
||||||
"golang.org/x/oauth2/google"
|
"golang.org/x/oauth2/google"
|
||||||
"golang.org/x/oauth2/jwt"
|
"golang.org/x/oauth2/jwt"
|
||||||
"google.golang.org/api/compute/v1"
|
"google.golang.org/api/compute/v1"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// driverGCE is a Driver implementation that actually talks to GCE.
|
// driverGCE is a Driver implementation that actually talks to GCE.
|
||||||
|
@ -88,15 +88,8 @@ func NewDriverGCE(ui packer.Ui, p string, a *AccountFile) (Driver, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *driverGCE) ImageExists(name string) bool {
|
func (d *driverGCE) CreateImage(name, description, family, zone, disk string) (<-chan *Image, <-chan error) {
|
||||||
_, err := d.service.Images.Get(d.projectId, name).Do()
|
gce_image := &compute.Image{
|
||||||
// The API may return an error for reasons other than the image not
|
|
||||||
// existing, but this heuristic is sufficient for now.
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *driverGCE) CreateImage(name, description, family, zone, disk string) (<-chan Image, <-chan error) {
|
|
||||||
image := &compute.Image{
|
|
||||||
Description: description,
|
Description: description,
|
||||||
Name: name,
|
Name: name,
|
||||||
Family: family,
|
Family: family,
|
||||||
|
@ -104,9 +97,9 @@ func (d *driverGCE) CreateImage(name, description, family, zone, disk string) (<
|
||||||
SourceType: "RAW",
|
SourceType: "RAW",
|
||||||
}
|
}
|
||||||
|
|
||||||
imageCh := make(chan Image, 1)
|
imageCh := make(chan *Image, 1)
|
||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
op, err := d.service.Images.Insert(d.projectId, image).Do()
|
op, err := d.service.Images.Insert(d.projectId, gce_image).Do()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errCh <- err
|
errCh <- err
|
||||||
} else {
|
} else {
|
||||||
|
@ -114,17 +107,17 @@ func (d *driverGCE) CreateImage(name, description, family, zone, disk string) (<
|
||||||
err = waitForState(errCh, "DONE", d.refreshGlobalOp(op))
|
err = waitForState(errCh, "DONE", d.refreshGlobalOp(op))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
close(imageCh)
|
close(imageCh)
|
||||||
|
errCh <- err
|
||||||
|
return
|
||||||
}
|
}
|
||||||
image, err = d.getImage(name, d.projectId)
|
var image *Image
|
||||||
|
image, err = d.GetImageFromProject(d.projectId, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
close(imageCh)
|
close(imageCh)
|
||||||
errCh <- err
|
errCh <- err
|
||||||
|
return
|
||||||
}
|
}
|
||||||
imageCh <- Image{
|
imageCh <- image
|
||||||
Name: name,
|
|
||||||
ProjectId: d.projectId,
|
|
||||||
SizeGb: image.DiskSizeGb,
|
|
||||||
}
|
|
||||||
close(imageCh)
|
close(imageCh)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
@ -166,6 +159,57 @@ func (d *driverGCE) DeleteDisk(zone, name string) (<-chan error, error) {
|
||||||
return errCh, nil
|
return errCh, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *driverGCE) GetImage(name string) (*Image, error) {
|
||||||
|
projects := []string{d.projectId, "centos-cloud", "coreos-cloud", "debian-cloud", "google-containers", "opensuse-cloud", "rhel-cloud", "suse-cloud", "ubuntu-os-cloud", "windows-cloud"}
|
||||||
|
var errs error
|
||||||
|
for _, project := range projects {
|
||||||
|
image, err := d.GetImageFromProject(project, name)
|
||||||
|
if err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(errs, err)
|
||||||
|
}
|
||||||
|
if image != nil {
|
||||||
|
return image, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"Could not find image, %s, in projects, %s: %s", name,
|
||||||
|
projects, errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *driverGCE) GetImageFromProject(project, name string) (*Image, error) {
|
||||||
|
image, err := d.service.Images.Get(project, name).Do()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if image == nil || image.SelfLink == "" {
|
||||||
|
return nil, fmt.Errorf("Image, %s, could not be found in project: %s", name, project)
|
||||||
|
} else {
|
||||||
|
return &Image{
|
||||||
|
Licenses: image.Licenses,
|
||||||
|
Name: image.Name,
|
||||||
|
ProjectId: project,
|
||||||
|
SelfLink: image.SelfLink,
|
||||||
|
SizeGb: image.DiskSizeGb,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *driverGCE) GetInstanceMetadata(zone, name, key string) (string, error) {
|
||||||
|
instance, err := d.service.Instances.Get(d.projectId, zone, name).Do()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range instance.Metadata.Items {
|
||||||
|
if item.Key == key {
|
||||||
|
return *item.Value, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("Instance metadata key, %s, not found.", key)
|
||||||
|
}
|
||||||
|
|
||||||
func (d *driverGCE) GetNatIP(zone, name string) (string, error) {
|
func (d *driverGCE) GetNatIP(zone, name string) (string, error) {
|
||||||
instance, err := d.service.Instances.Get(d.projectId, zone, name).Do()
|
instance, err := d.service.Instances.Get(d.projectId, zone, name).Do()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -211,6 +255,13 @@ func (d *driverGCE) GetSerialPortOutput(zone, name string) (string, error) {
|
||||||
return output.Contents, nil
|
return output.Contents, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *driverGCE) ImageExists(name string) bool {
|
||||||
|
_, err := d.GetImageFromProject(d.projectId, name)
|
||||||
|
// The API may return an error for reasons other than the image not
|
||||||
|
// existing, but this heuristic is sufficient for now.
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
|
func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
|
||||||
// Get the zone
|
// Get the zone
|
||||||
d.ui.Message(fmt.Sprintf("Loading zone: %s", c.Zone))
|
d.ui.Message(fmt.Sprintf("Loading zone: %s", c.Zone))
|
||||||
|
@ -219,13 +270,6 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the image
|
|
||||||
d.ui.Message(fmt.Sprintf("Loading image: %s in project %s", c.Image.Name, c.Image.ProjectId))
|
|
||||||
image, err := d.getImage(c.Image.Name, c.Image.ProjectId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the machine type
|
// Get the machine type
|
||||||
d.ui.Message(fmt.Sprintf("Loading machine type: %s", c.MachineType))
|
d.ui.Message(fmt.Sprintf("Loading machine type: %s", c.MachineType))
|
||||||
machineType, err := d.service.MachineTypes.Get(
|
machineType, err := d.service.MachineTypes.Get(
|
||||||
|
@ -302,7 +346,7 @@ func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
|
||||||
Boot: true,
|
Boot: true,
|
||||||
AutoDelete: false,
|
AutoDelete: false,
|
||||||
InitializeParams: &compute.AttachedDiskInitializeParams{
|
InitializeParams: &compute.AttachedDiskInitializeParams{
|
||||||
SourceImage: image.SelfLink,
|
SourceImage: c.Image.SelfLink,
|
||||||
DiskSizeGb: c.DiskSizeGb,
|
DiskSizeGb: c.DiskSizeGb,
|
||||||
DiskType: fmt.Sprintf("zones/%s/diskTypes/%s", zone.Name, c.DiskType),
|
DiskType: fmt.Sprintf("zones/%s/diskTypes/%s", zone.Name, c.DiskType),
|
||||||
},
|
},
|
||||||
|
@ -355,20 +399,6 @@ func (d *driverGCE) WaitForInstance(state, zone, name string) <-chan error {
|
||||||
return errCh
|
return errCh
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *driverGCE) getImage(name, projectId string) (image *compute.Image, err error) {
|
|
||||||
projects := []string{projectId, "centos-cloud", "coreos-cloud", "debian-cloud", "google-containers", "opensuse-cloud", "rhel-cloud", "suse-cloud", "ubuntu-os-cloud", "windows-cloud"}
|
|
||||||
for _, project := range projects {
|
|
||||||
image, err = d.service.Images.Get(project, name).Do()
|
|
||||||
if err == nil && image != nil && image.SelfLink != "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
image = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = fmt.Errorf("Image %s could not be found in any of these projects: %s", name, projects)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *driverGCE) refreshInstanceState(zone, name string) stateRefreshFunc {
|
func (d *driverGCE) refreshInstanceState(zone, name string) stateRefreshFunc {
|
||||||
return func() (string, error) {
|
return func() (string, error) {
|
||||||
instance, err := d.service.Instances.Get(d.projectId, zone, name).Do()
|
instance, err := d.service.Instances.Get(d.projectId, zone, name).Do()
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
package googlecompute
|
package googlecompute
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
// DriverMock is a Driver implementation that is a mocked out so that
|
// DriverMock is a Driver implementation that is a mocked out so that
|
||||||
// it can be used for tests.
|
// it can be used for tests.
|
||||||
type DriverMock struct {
|
type DriverMock struct {
|
||||||
ImageExistsName string
|
CreateImageName string
|
||||||
ImageExistsResult bool
|
CreateImageDesc string
|
||||||
|
CreateImageFamily string
|
||||||
CreateImageName string
|
CreateImageZone string
|
||||||
CreateImageDesc string
|
CreateImageDisk string
|
||||||
CreateImageFamily string
|
CreateImageResultLicenses []string
|
||||||
CreateImageZone string
|
CreateImageResultProjectId string
|
||||||
CreateImageDisk string
|
CreateImageResultSelfLink string
|
||||||
CreateImageProjectId string
|
CreateImageResultSizeGb int64
|
||||||
CreateImageSizeGb int64
|
CreateImageErrCh <-chan error
|
||||||
CreateImageErrCh <-chan error
|
CreateImageResultCh <-chan *Image
|
||||||
CreateImageResultCh <-chan Image
|
|
||||||
|
|
||||||
DeleteImageName string
|
DeleteImageName string
|
||||||
DeleteImageErrCh <-chan error
|
DeleteImageErrCh <-chan error
|
||||||
|
@ -29,6 +30,21 @@ type DriverMock struct {
|
||||||
DeleteDiskErrCh <-chan error
|
DeleteDiskErrCh <-chan error
|
||||||
DeleteDiskErr error
|
DeleteDiskErr error
|
||||||
|
|
||||||
|
GetImageName string
|
||||||
|
GetImageResult *Image
|
||||||
|
GetImageErr error
|
||||||
|
|
||||||
|
GetImageFromProjectProject string
|
||||||
|
GetImageFromProjectName string
|
||||||
|
GetImageFromProjectResult *Image
|
||||||
|
GetImageFromProjectErr error
|
||||||
|
|
||||||
|
GetInstanceMetadataZone string
|
||||||
|
GetInstanceMetadataName string
|
||||||
|
GetInstanceMetadataKey string
|
||||||
|
GetInstanceMetadataResult string
|
||||||
|
GetInstanceMetadataErr error
|
||||||
|
|
||||||
GetNatIPZone string
|
GetNatIPZone string
|
||||||
GetNatIPName string
|
GetNatIPName string
|
||||||
GetNatIPResult string
|
GetNatIPResult string
|
||||||
|
@ -44,6 +60,9 @@ type DriverMock struct {
|
||||||
GetSerialPortOutputResult string
|
GetSerialPortOutputResult string
|
||||||
GetSerialPortOutputErr error
|
GetSerialPortOutputErr error
|
||||||
|
|
||||||
|
ImageExistsName string
|
||||||
|
ImageExistsResult bool
|
||||||
|
|
||||||
RunInstanceConfig *InstanceConfig
|
RunInstanceConfig *InstanceConfig
|
||||||
RunInstanceErrCh <-chan error
|
RunInstanceErrCh <-chan error
|
||||||
RunInstanceErr error
|
RunInstanceErr error
|
||||||
|
@ -54,31 +73,33 @@ type DriverMock struct {
|
||||||
WaitForInstanceErrCh <-chan error
|
WaitForInstanceErrCh <-chan error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DriverMock) ImageExists(name string) bool {
|
func (d *DriverMock) CreateImage(name, description, family, zone, disk string) (<-chan *Image, <-chan error) {
|
||||||
d.ImageExistsName = name
|
|
||||||
return d.ImageExistsResult
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DriverMock) CreateImage(name, description, family, zone, disk string) (<-chan Image, <-chan error) {
|
|
||||||
d.CreateImageName = name
|
d.CreateImageName = name
|
||||||
d.CreateImageDesc = description
|
d.CreateImageDesc = description
|
||||||
d.CreateImageFamily = family
|
d.CreateImageFamily = family
|
||||||
d.CreateImageZone = zone
|
d.CreateImageZone = zone
|
||||||
d.CreateImageDisk = disk
|
d.CreateImageDisk = disk
|
||||||
if d.CreateImageSizeGb == 0 {
|
if d.CreateImageResultProjectId == "" {
|
||||||
d.CreateImageSizeGb = 10
|
d.CreateImageResultProjectId = "test"
|
||||||
}
|
}
|
||||||
if d.CreateImageProjectId == "" {
|
if d.CreateImageResultSelfLink == "" {
|
||||||
d.CreateImageProjectId = "test"
|
d.CreateImageResultSelfLink = fmt.Sprintf(
|
||||||
|
"http://content.googleapis.com/compute/v1/%s/global/licenses/test",
|
||||||
|
d.CreateImageResultProjectId)
|
||||||
|
}
|
||||||
|
if d.CreateImageResultSizeGb == 0 {
|
||||||
|
d.CreateImageResultSizeGb = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
resultCh := d.CreateImageResultCh
|
resultCh := d.CreateImageResultCh
|
||||||
if resultCh == nil {
|
if resultCh == nil {
|
||||||
ch := make(chan Image, 1)
|
ch := make(chan *Image, 1)
|
||||||
ch <- Image{
|
ch <- &Image{
|
||||||
|
Licenses: d.CreateImageResultLicenses,
|
||||||
Name: name,
|
Name: name,
|
||||||
ProjectId: d.CreateImageProjectId,
|
ProjectId: d.CreateImageResultProjectId,
|
||||||
SizeGb: d.CreateImageSizeGb,
|
SelfLink: d.CreateImageResultSelfLink,
|
||||||
|
SizeGb: d.CreateImageResultSizeGb,
|
||||||
}
|
}
|
||||||
close(ch)
|
close(ch)
|
||||||
resultCh = ch
|
resultCh = ch
|
||||||
|
@ -135,6 +156,24 @@ func (d *DriverMock) DeleteDisk(zone, name string) (<-chan error, error) {
|
||||||
return resultCh, d.DeleteDiskErr
|
return resultCh, d.DeleteDiskErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DriverMock) GetImage(name string) (*Image, error) {
|
||||||
|
d.GetImageName = name
|
||||||
|
return d.GetImageResult, d.GetImageErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DriverMock) GetImageFromProject(project, name string) (*Image, error) {
|
||||||
|
d.GetImageFromProjectProject = project
|
||||||
|
d.GetImageFromProjectName = name
|
||||||
|
return d.GetImageFromProjectResult, d.GetImageFromProjectErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DriverMock) GetInstanceMetadata(zone, name, key string) (string, error) {
|
||||||
|
d.GetInstanceMetadataZone = zone
|
||||||
|
d.GetInstanceMetadataName = name
|
||||||
|
d.GetInstanceMetadataKey = key
|
||||||
|
return d.GetInstanceMetadataResult, d.GetInstanceMetadataErr
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DriverMock) GetNatIP(zone, name string) (string, error) {
|
func (d *DriverMock) GetNatIP(zone, name string) (string, error) {
|
||||||
d.GetNatIPZone = zone
|
d.GetNatIPZone = zone
|
||||||
d.GetNatIPName = name
|
d.GetNatIPName = name
|
||||||
|
@ -153,6 +192,11 @@ func (d *DriverMock) GetSerialPortOutput(zone, name string) (string, error) {
|
||||||
return d.GetSerialPortOutputResult, d.GetSerialPortOutputErr
|
return d.GetSerialPortOutputResult, d.GetSerialPortOutputErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DriverMock) ImageExists(name string) bool {
|
||||||
|
d.ImageExistsName = name
|
||||||
|
return d.ImageExistsResult
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DriverMock) RunInstance(c *InstanceConfig) (<-chan error, error) {
|
func (d *DriverMock) RunInstance(c *InstanceConfig) (<-chan error, error) {
|
||||||
d.RunInstanceConfig = c
|
d.RunInstanceConfig = c
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package googlecompute
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Image struct {
|
||||||
|
Licenses []string
|
||||||
|
Name string
|
||||||
|
ProjectId string
|
||||||
|
SelfLink string
|
||||||
|
SizeGb int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Image) IsWindows() bool {
|
||||||
|
for _, license := range i.Licenses {
|
||||||
|
if strings.Contains(license, "windows") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package googlecompute
|
||||||
|
|
||||||
|
import(
|
||||||
|
"testing"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StubImage(name, project string, licenses []string, sizeGb int64) *Image {
|
||||||
|
return &Image{
|
||||||
|
Licenses: licenses,
|
||||||
|
Name: name,
|
||||||
|
ProjectId: project,
|
||||||
|
SelfLink: fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/images/%s", project, name),
|
||||||
|
SizeGb: sizeGb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImage_IsWindows(t *testing.T) {
|
||||||
|
i := StubImage("foo", "foo-project", []string{"license-foo", "license-bar"}, 100)
|
||||||
|
assert.False(t, i.IsWindows())
|
||||||
|
|
||||||
|
i = StubImage("foo", "foo-project", []string{"license-foo", "windows-license"}, 100)
|
||||||
|
assert.True(t, i.IsWindows())
|
||||||
|
}
|
|
@ -1,37 +1,40 @@
|
||||||
package googlecompute
|
package googlecompute
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
const StartupScriptStartLog string = "Packer startup script starting."
|
|
||||||
const StartupScriptDoneLog string = "Packer startup script done."
|
|
||||||
const StartupScriptKey string = "startup-script"
|
const StartupScriptKey string = "startup-script"
|
||||||
|
const StartupScriptStatusKey string = "startup-script-status"
|
||||||
const StartupWrappedScriptKey string = "packer-wrapped-startup-script"
|
const StartupWrappedScriptKey string = "packer-wrapped-startup-script"
|
||||||
|
|
||||||
// We have to encode StartupScriptDoneLog because we use it as a sentinel value to indicate
|
const StartupScriptStatusDone string = "done"
|
||||||
// that the user-provided startup script is done. If we pass StartupScriptDoneLog as-is, it
|
const StartupScriptStatusError string = "error"
|
||||||
// will be printed early in the instance console log (before the startup script even runs;
|
const StartupScriptStatusNotDone string = "notdone"
|
||||||
// we print out instance creation metadata which contains this wrapper script).
|
|
||||||
var StartupScriptDoneLogBase64 string = base64.StdEncoding.EncodeToString([]byte(StartupScriptDoneLog))
|
|
||||||
|
|
||||||
var StartupScript string = fmt.Sprintf(`#!/bin/bash
|
var StartupScriptLinux string = fmt.Sprintf(`#!/bin/bash
|
||||||
echo %s
|
echo "Packer startup script starting."
|
||||||
RETVAL=0
|
RETVAL=0
|
||||||
|
BASEMETADATAURL=http://metadata/computeMetadata/v1/instance/
|
||||||
|
|
||||||
GetMetadata () {
|
GetMetadata () {
|
||||||
echo "$(curl -f -H "Metadata-Flavor: Google" http://metadata/computeMetadata/v1/instance/attributes/$1 2> /dev/null)"
|
echo "$(curl -f -H "Metadata-Flavor: Google" ${BASEMETADATAURL}/${1} 2> /dev/null)"
|
||||||
}
|
}
|
||||||
|
|
||||||
STARTUPSCRIPT=$(GetMetadata %s)
|
ZONE=$(GetMetadata zone | grep -oP "[^/]*$")
|
||||||
|
|
||||||
|
SetMetadata () {
|
||||||
|
gcloud compute instances add-metadata ${HOSTNAME} --metadata ${1}=${2} --zone ${ZONE}
|
||||||
|
}
|
||||||
|
|
||||||
|
STARTUPSCRIPT=$(GetMetadata attributes/%s)
|
||||||
STARTUPSCRIPTPATH=/packer-wrapped-startup-script
|
STARTUPSCRIPTPATH=/packer-wrapped-startup-script
|
||||||
if [ -f "/var/log/startupscript.log" ]; then
|
if [ -f "/var/log/startupscript.log" ]; then
|
||||||
STARTUPSCRIPTLOGPATH=/var/log/startupscript.log
|
STARTUPSCRIPTLOGPATH=/var/log/startupscript.log
|
||||||
else
|
else
|
||||||
STARTUPSCRIPTLOGPATH=/var/log/daemon.log
|
STARTUPSCRIPTLOGPATH=/var/log/daemon.log
|
||||||
fi
|
fi
|
||||||
STARTUPSCRIPTLOGDEST=$(GetMetadata startup-script-log-dest)
|
STARTUPSCRIPTLOGDEST=$(GetMetadata attributes/startup-script-log-dest)
|
||||||
|
|
||||||
if [[ ! -z $STARTUPSCRIPT ]]; then
|
if [[ ! -z $STARTUPSCRIPT ]]; then
|
||||||
echo "Executing user-provided startup script..."
|
echo "Executing user-provided startup script..."
|
||||||
|
@ -48,6 +51,9 @@ if [[ ! -z $STARTUPSCRIPT ]]; then
|
||||||
rm ${STARTUPSCRIPTPATH}
|
rm ${STARTUPSCRIPTPATH}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo $(echo %s | base64 --decode)
|
echo "Packer startup script done."
|
||||||
|
SetMetadata %s %s
|
||||||
exit $RETVAL
|
exit $RETVAL
|
||||||
`, StartupScriptStartLog, StartupWrappedScriptKey, StartupScriptDoneLogBase64)
|
`, StartupWrappedScriptKey, StartupScriptStatusKey, StartupScriptStatusDone)
|
||||||
|
|
||||||
|
var StartupScriptWindows string = ""
|
||||||
|
|
|
@ -13,14 +13,14 @@ type StepCheckExistingImage int
|
||||||
|
|
||||||
// Run executes the Packer build step that checks if the image already exists.
|
// Run executes the Packer build step that checks if the image already exists.
|
||||||
func (s *StepCheckExistingImage) Run(state multistep.StateBag) multistep.StepAction {
|
func (s *StepCheckExistingImage) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
config := state.Get("config").(*Config)
|
c := state.Get("config").(*Config)
|
||||||
driver := state.Get("driver").(Driver)
|
d := state.Get("driver").(Driver)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
ui.Say("Checking image does not exist...")
|
ui.Say("Checking image does not exist...")
|
||||||
exists := driver.ImageExists(config.ImageName)
|
exists := d.ImageExists(c.ImageName)
|
||||||
if exists {
|
if exists {
|
||||||
err := fmt.Errorf("Image %s already exists", config.ImageName)
|
err := fmt.Errorf("Image %s already exists", c.ImageName)
|
||||||
state.Put("error", err)
|
state.Put("error", err)
|
||||||
ui.Error(err.Error())
|
ui.Error(err.Error())
|
||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
|
|
|
@ -24,7 +24,9 @@ func (s *StepCreateImage) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
|
||||||
ui.Say("Creating image...")
|
ui.Say("Creating image...")
|
||||||
|
|
||||||
imageCh, errCh := driver.CreateImage(config.ImageName, config.ImageDescription, config.ImageFamily, config.Zone, config.DiskName)
|
imageCh, errCh := driver.CreateImage(
|
||||||
|
config.ImageName, config.ImageDescription, config.ImageFamily, config.Zone,
|
||||||
|
config.DiskName)
|
||||||
var err error
|
var err error
|
||||||
select {
|
select {
|
||||||
case err = <-errCh:
|
case err = <-errCh:
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStepCreateImage_impl(t *testing.T) {
|
func TestStepCreateImage_impl(t *testing.T) {
|
||||||
|
@ -16,52 +17,35 @@ func TestStepCreateImage(t *testing.T) {
|
||||||
step := new(StepCreateImage)
|
step := new(StepCreateImage)
|
||||||
defer step.Cleanup(state)
|
defer step.Cleanup(state)
|
||||||
|
|
||||||
config := state.Get("config").(*Config)
|
c := state.Get("config").(*Config)
|
||||||
driver := state.Get("driver").(*DriverMock)
|
d := state.Get("driver").(*DriverMock)
|
||||||
driver.CreateImageProjectId = "createimage-project"
|
|
||||||
driver.CreateImageSizeGb = 100
|
// These are the values of the image the driver will return.
|
||||||
|
d.CreateImageResultLicenses = []string{"test-license"}
|
||||||
|
d.CreateImageResultProjectId = "test-project"
|
||||||
|
d.CreateImageResultSizeGb = 100
|
||||||
|
|
||||||
// run the step
|
// run the step
|
||||||
if action := step.Run(state); action != multistep.ActionContinue {
|
action := step.Run(state)
|
||||||
t.Fatalf("bad action: %#v", action)
|
assert.Equal(t, action, multistep.ActionContinue, "Step did not pass.")
|
||||||
}
|
|
||||||
|
|
||||||
uncastImage, ok := state.GetOk("image")
|
uncastImage, ok := state.GetOk("image")
|
||||||
if !ok {
|
assert.True(t, ok, "State does not have resulting image.")
|
||||||
t.Fatal("should have image")
|
image, ok := uncastImage.(*Image)
|
||||||
}
|
assert.True(t, ok, "Image in state is not an Image.")
|
||||||
image, ok := uncastImage.(Image)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("image is not an Image")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify created Image results.
|
// Verify created Image results.
|
||||||
if image.Name != config.ImageName {
|
assert.Equal(t, image.Licenses, d.CreateImageResultLicenses, "Created image licenses don't match the licenses returned by the driver.")
|
||||||
t.Fatalf("Created image name, %s, does not match config name, %s.", image.Name, config.ImageName)
|
assert.Equal(t, image.Name, c.ImageName, "Created image does not match config name.")
|
||||||
}
|
assert.Equal(t, image.ProjectId, d.CreateImageResultProjectId, "Created image project does not match driver project.")
|
||||||
if driver.CreateImageProjectId != image.ProjectId {
|
assert.Equal(t, image.SizeGb, d.CreateImageResultSizeGb, "Created image size does not match the size returned by the driver.")
|
||||||
t.Fatalf("Created image project ID, %s, does not match driver project ID, %s.", image.ProjectId, driver.CreateImageProjectId)
|
|
||||||
}
|
|
||||||
if driver.CreateImageSizeGb != image.SizeGb {
|
|
||||||
t.Fatalf("Created image size, %d, does not match the expected test value, %d.", image.SizeGb, driver.CreateImageSizeGb)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify proper args passed to driver.CreateImage.
|
// Verify proper args passed to driver.CreateImage.
|
||||||
if driver.CreateImageName != config.ImageName {
|
assert.Equal(t, d.CreateImageName, c.ImageName, "Incorrect image name passed to driver.")
|
||||||
t.Fatalf("bad: %#v", driver.CreateImageName)
|
assert.Equal(t, d.CreateImageDesc, c.ImageDescription, "Incorrect image description passed to driver.")
|
||||||
}
|
assert.Equal(t, d.CreateImageFamily, c.ImageFamily, "Incorrect image family passed to driver.")
|
||||||
if driver.CreateImageDesc != config.ImageDescription {
|
assert.Equal(t, d.CreateImageZone, c.Zone, "Incorrect image zone passed to driver.")
|
||||||
t.Fatalf("bad: %#v", driver.CreateImageDesc)
|
assert.Equal(t, d.CreateImageDisk, c.DiskName, "Incorrect disk passed to driver.")
|
||||||
}
|
|
||||||
if driver.CreateImageFamily != config.ImageFamily {
|
|
||||||
t.Fatalf("bad: %#v", driver.CreateImageFamily)
|
|
||||||
}
|
|
||||||
if driver.CreateImageZone != config.Zone {
|
|
||||||
t.Fatalf("bad: %#v", driver.CreateImageZone)
|
|
||||||
}
|
|
||||||
if driver.CreateImageDisk != config.DiskName {
|
|
||||||
t.Fatalf("bad: %#v", driver.CreateImageDisk)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStepCreateImage_errorOnChannel(t *testing.T) {
|
func TestStepCreateImage_errorOnChannel(t *testing.T) {
|
||||||
|
@ -76,14 +60,10 @@ func TestStepCreateImage_errorOnChannel(t *testing.T) {
|
||||||
driver.CreateImageErrCh = errCh
|
driver.CreateImageErrCh = errCh
|
||||||
|
|
||||||
// run the step
|
// run the step
|
||||||
if action := step.Run(state); action != multistep.ActionHalt {
|
action := step.Run(state)
|
||||||
t.Fatalf("bad action: %#v", action)
|
assert.Equal(t, action, multistep.ActionHalt, "Step should not have passed.")
|
||||||
}
|
_, ok := state.GetOk("error")
|
||||||
|
assert.True(t, ok, "State should have an error.")
|
||||||
if _, ok := state.GetOk("error"); !ok {
|
_, ok = state.GetOk("image_name")
|
||||||
t.Fatal("should have error")
|
assert.False(t, ok, "State should not have a resulting image.")
|
||||||
}
|
|
||||||
if _, ok := state.GetOk("image_name"); ok {
|
|
||||||
t.Fatal("should NOT have image")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,82 +15,97 @@ type StepCreateInstance struct {
|
||||||
Debug bool
|
Debug bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *Config) getImage() Image {
|
func (c *Config) createInstanceMetadata(sourceImage *Image, sshPublicKey string) (map[string]string, error) {
|
||||||
project := config.ProjectId
|
|
||||||
if config.SourceImageProjectId != "" {
|
|
||||||
project = config.SourceImageProjectId
|
|
||||||
}
|
|
||||||
return Image{Name: config.SourceImage, ProjectId: project}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config *Config) getInstanceMetadata(sshPublicKey string) (map[string]string, error) {
|
|
||||||
instanceMetadata := make(map[string]string)
|
instanceMetadata := make(map[string]string)
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Copy metadata from config.
|
// Copy metadata from config.
|
||||||
for k, v := range config.Metadata {
|
for k, v := range c.Metadata {
|
||||||
instanceMetadata[k] = v
|
instanceMetadata[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge any existing ssh keys with our public key.
|
// Merge any existing ssh keys with our public key.
|
||||||
sshMetaKey := "sshKeys"
|
sshMetaKey := "sshKeys"
|
||||||
sshKeys := fmt.Sprintf("%s:%s", config.Comm.SSHUsername, sshPublicKey)
|
sshKeys := fmt.Sprintf("%s:%s", c.Comm.SSHUsername, sshPublicKey)
|
||||||
if confSshKeys, exists := instanceMetadata[sshMetaKey]; exists {
|
if confSshKeys, exists := instanceMetadata[sshMetaKey]; exists {
|
||||||
sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSshKeys)
|
sshKeys = fmt.Sprintf("%s\n%s", sshKeys, confSshKeys)
|
||||||
}
|
}
|
||||||
instanceMetadata[sshMetaKey] = sshKeys
|
instanceMetadata[sshMetaKey] = sshKeys
|
||||||
|
|
||||||
// Wrap any startup script with our own startup script.
|
// Wrap any startup script with our own startup script.
|
||||||
if config.StartupScriptFile != "" {
|
if c.StartupScriptFile != "" {
|
||||||
var content []byte
|
var content []byte
|
||||||
content, err = ioutil.ReadFile(config.StartupScriptFile)
|
content, err = ioutil.ReadFile(c.StartupScriptFile)
|
||||||
instanceMetadata[StartupWrappedScriptKey] = string(content)
|
instanceMetadata[StartupWrappedScriptKey] = string(content)
|
||||||
} else if wrappedStartupScript, exists := instanceMetadata[StartupScriptKey]; exists {
|
} else if wrappedStartupScript, exists := instanceMetadata[StartupScriptKey]; exists {
|
||||||
instanceMetadata[StartupWrappedScriptKey] = wrappedStartupScript
|
instanceMetadata[StartupWrappedScriptKey] = wrappedStartupScript
|
||||||
}
|
}
|
||||||
instanceMetadata[StartupScriptKey] = StartupScript
|
if sourceImage.IsWindows() {
|
||||||
|
// Windows startup script support is not yet implemented.
|
||||||
|
// Mark the startup script as done.
|
||||||
|
instanceMetadata[StartupScriptKey] = StartupScriptWindows
|
||||||
|
instanceMetadata[StartupScriptStatusKey] = StartupScriptStatusDone
|
||||||
|
} else {
|
||||||
|
instanceMetadata[StartupScriptKey] = StartupScriptLinux
|
||||||
|
instanceMetadata[StartupScriptStatusKey] = StartupScriptStatusNotDone
|
||||||
|
}
|
||||||
|
|
||||||
return instanceMetadata, err
|
return instanceMetadata, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getImage(c *Config, d Driver) (*Image, error) {
|
||||||
|
if c.SourceImageProjectId == "" {
|
||||||
|
return d.GetImage(c.SourceImage)
|
||||||
|
} else {
|
||||||
|
return d.GetImageFromProject(c.SourceImageProjectId, c.SourceImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Run executes the Packer build step that creates a GCE instance.
|
// Run executes the Packer build step that creates a GCE instance.
|
||||||
func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction {
|
func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
config := state.Get("config").(*Config)
|
c := state.Get("config").(*Config)
|
||||||
driver := state.Get("driver").(Driver)
|
d := state.Get("driver").(Driver)
|
||||||
sshPublicKey := state.Get("ssh_public_key").(string)
|
sshPublicKey := state.Get("ssh_public_key").(string)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
sourceImage, err := getImage(c, d)
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error getting source image for instance creation: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
ui.Say("Creating instance...")
|
ui.Say("Creating instance...")
|
||||||
name := config.InstanceName
|
name := c.InstanceName
|
||||||
|
|
||||||
var errCh <-chan error
|
var errCh <-chan error
|
||||||
var err error
|
|
||||||
var metadata map[string]string
|
var metadata map[string]string
|
||||||
metadata, err = config.getInstanceMetadata(sshPublicKey)
|
metadata, err = c.createInstanceMetadata(sourceImage, sshPublicKey)
|
||||||
errCh, err = driver.RunInstance(&InstanceConfig{
|
errCh, err = d.RunInstance(&InstanceConfig{
|
||||||
Address: config.Address,
|
Address: c.Address,
|
||||||
Description: "New instance created by Packer",
|
Description: "New instance created by Packer",
|
||||||
DiskSizeGb: config.DiskSizeGb,
|
DiskSizeGb: c.DiskSizeGb,
|
||||||
DiskType: config.DiskType,
|
DiskType: c.DiskType,
|
||||||
Image: config.getImage(),
|
Image: sourceImage,
|
||||||
MachineType: config.MachineType,
|
MachineType: c.MachineType,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
Name: name,
|
Name: name,
|
||||||
Network: config.Network,
|
Network: c.Network,
|
||||||
OmitExternalIP: config.OmitExternalIP,
|
OmitExternalIP: c.OmitExternalIP,
|
||||||
Preemptible: config.Preemptible,
|
Preemptible: c.Preemptible,
|
||||||
Region: config.Region,
|
Region: c.Region,
|
||||||
ServiceAccountEmail: config.Account.ClientEmail,
|
ServiceAccountEmail: c.Account.ClientEmail,
|
||||||
Subnetwork: config.Subnetwork,
|
Subnetwork: c.Subnetwork,
|
||||||
Tags: config.Tags,
|
Tags: c.Tags,
|
||||||
Zone: config.Zone,
|
Zone: c.Zone,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
ui.Message("Waiting for creation operation to complete...")
|
ui.Message("Waiting for creation operation to complete...")
|
||||||
select {
|
select {
|
||||||
case err = <-errCh:
|
case err = <-errCh:
|
||||||
case <-time.After(config.stateTimeout):
|
case <-time.After(c.stateTimeout):
|
||||||
err = errors.New("time out while waiting for instance to create")
|
err = errors.New("time out while waiting for instance to create")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,7 +121,7 @@ func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction
|
||||||
|
|
||||||
if s.Debug {
|
if s.Debug {
|
||||||
if name != "" {
|
if name != "" {
|
||||||
ui.Message(fmt.Sprintf("Instance: %s started in %s", name, config.Zone))
|
ui.Message(fmt.Sprintf("Instance: %s started in %s", name, c.Zone))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,11 @@ package googlecompute
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/mitchellh/multistep"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStepCreateInstance_impl(t *testing.T) {
|
func TestStepCreateInstance_impl(t *testing.T) {
|
||||||
|
@ -18,36 +20,25 @@ func TestStepCreateInstance(t *testing.T) {
|
||||||
|
|
||||||
state.Put("ssh_public_key", "key")
|
state.Put("ssh_public_key", "key")
|
||||||
|
|
||||||
config := state.Get("config").(*Config)
|
c := state.Get("config").(*Config)
|
||||||
driver := state.Get("driver").(*DriverMock)
|
d := state.Get("driver").(*DriverMock)
|
||||||
|
d.GetImageResult = StubImage("test-image", "test-project", []string{}, 100)
|
||||||
|
|
||||||
// run the step
|
// run the step
|
||||||
if action := step.Run(state); action != multistep.ActionContinue {
|
assert.Equal(t, step.Run(state), multistep.ActionContinue, "Step should have passed and continued.")
|
||||||
t.Fatalf("bad action: %#v", action)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify state
|
// Verify state
|
||||||
nameRaw, ok := state.GetOk("instance_name")
|
nameRaw, ok := state.GetOk("instance_name")
|
||||||
if !ok {
|
assert.True(t, ok, "State should have an instance name.")
|
||||||
t.Fatal("should have instance name")
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanup
|
// cleanup
|
||||||
step.Cleanup(state)
|
step.Cleanup(state)
|
||||||
|
|
||||||
if driver.DeleteInstanceName != nameRaw.(string) {
|
// Check args passed to the driver.
|
||||||
t.Fatal("should've deleted instance")
|
assert.Equal(t, d.DeleteInstanceName, nameRaw.(string), "Incorrect instance name passed to driver.")
|
||||||
}
|
assert.Equal(t, d.DeleteInstanceZone, c.Zone, "Incorrect instance zone passed to driver.")
|
||||||
if driver.DeleteInstanceZone != config.Zone {
|
assert.Equal(t, d.DeleteDiskName, c.InstanceName, "Incorrect disk name passed to driver.")
|
||||||
t.Fatalf("bad instance zone: %#v", driver.DeleteInstanceZone)
|
assert.Equal(t, d.DeleteDiskZone, c.Zone, "Incorrect disk zone passed to driver.")
|
||||||
}
|
|
||||||
|
|
||||||
if driver.DeleteDiskName != config.InstanceName {
|
|
||||||
t.Fatal("should've deleted disk")
|
|
||||||
}
|
|
||||||
if driver.DeleteDiskZone != config.Zone {
|
|
||||||
t.Fatalf("bad disk zone: %#v", driver.DeleteDiskZone)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStepCreateInstance_error(t *testing.T) {
|
func TestStepCreateInstance_error(t *testing.T) {
|
||||||
|
@ -57,21 +48,18 @@ func TestStepCreateInstance_error(t *testing.T) {
|
||||||
|
|
||||||
state.Put("ssh_public_key", "key")
|
state.Put("ssh_public_key", "key")
|
||||||
|
|
||||||
driver := state.Get("driver").(*DriverMock)
|
d := state.Get("driver").(*DriverMock)
|
||||||
driver.RunInstanceErr = errors.New("error")
|
d.RunInstanceErr = errors.New("error")
|
||||||
|
d.GetImageResult = StubImage("test-image", "test-project", []string{}, 100)
|
||||||
|
|
||||||
// run the step
|
// run the step
|
||||||
if action := step.Run(state); action != multistep.ActionHalt {
|
assert.Equal(t, step.Run(state), multistep.ActionHalt, "Step should have failed and halted.")
|
||||||
t.Fatalf("bad action: %#v", action)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify state
|
// Verify state
|
||||||
if _, ok := state.GetOk("error"); !ok {
|
_, ok := state.GetOk("error")
|
||||||
t.Fatal("should have error")
|
assert.True(t, ok, "State should have an error.")
|
||||||
}
|
_, ok = state.GetOk("instance_name")
|
||||||
if _, ok := state.GetOk("instance_name"); ok {
|
assert.False(t, ok, "State should not have an instance name.")
|
||||||
t.Fatal("should NOT have instance name")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStepCreateInstance_errorOnChannel(t *testing.T) {
|
func TestStepCreateInstance_errorOnChannel(t *testing.T) {
|
||||||
|
@ -79,26 +67,23 @@ func TestStepCreateInstance_errorOnChannel(t *testing.T) {
|
||||||
step := new(StepCreateInstance)
|
step := new(StepCreateInstance)
|
||||||
defer step.Cleanup(state)
|
defer step.Cleanup(state)
|
||||||
|
|
||||||
|
state.Put("ssh_public_key", "key")
|
||||||
|
|
||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
errCh <- errors.New("error")
|
errCh <- errors.New("error")
|
||||||
|
|
||||||
state.Put("ssh_public_key", "key")
|
d := state.Get("driver").(*DriverMock)
|
||||||
|
d.RunInstanceErrCh = errCh
|
||||||
driver := state.Get("driver").(*DriverMock)
|
d.GetImageResult = StubImage("test-image", "test-project", []string{}, 100)
|
||||||
driver.RunInstanceErrCh = errCh
|
|
||||||
|
|
||||||
// run the step
|
// run the step
|
||||||
if action := step.Run(state); action != multistep.ActionHalt {
|
assert.Equal(t, step.Run(state), multistep.ActionHalt, "Step should have failed and halted.")
|
||||||
t.Fatalf("bad action: %#v", action)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify state
|
// Verify state
|
||||||
if _, ok := state.GetOk("error"); !ok {
|
_, ok := state.GetOk("error")
|
||||||
t.Fatal("should have error")
|
assert.True(t, ok, "State should have an error.")
|
||||||
}
|
_, ok = state.GetOk("instance_name")
|
||||||
if _, ok := state.GetOk("instance_name"); ok {
|
assert.False(t, ok, "State should not have an instance name.")
|
||||||
t.Fatal("should NOT have instance name")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStepCreateInstance_errorTimeout(t *testing.T) {
|
func TestStepCreateInstance_errorTimeout(t *testing.T) {
|
||||||
|
@ -106,30 +91,27 @@ func TestStepCreateInstance_errorTimeout(t *testing.T) {
|
||||||
step := new(StepCreateInstance)
|
step := new(StepCreateInstance)
|
||||||
defer step.Cleanup(state)
|
defer step.Cleanup(state)
|
||||||
|
|
||||||
|
state.Put("ssh_public_key", "key")
|
||||||
|
|
||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
<-time.After(10 * time.Millisecond)
|
<-time.After(10 * time.Millisecond)
|
||||||
errCh <- nil
|
errCh <- nil
|
||||||
}()
|
}()
|
||||||
|
|
||||||
state.Put("ssh_public_key", "key")
|
|
||||||
|
|
||||||
config := state.Get("config").(*Config)
|
config := state.Get("config").(*Config)
|
||||||
config.stateTimeout = 1 * time.Microsecond
|
config.stateTimeout = 1 * time.Microsecond
|
||||||
|
|
||||||
driver := state.Get("driver").(*DriverMock)
|
d := state.Get("driver").(*DriverMock)
|
||||||
driver.RunInstanceErrCh = errCh
|
d.RunInstanceErrCh = errCh
|
||||||
|
d.GetImageResult = StubImage("test-image", "test-project", []string{}, 100)
|
||||||
|
|
||||||
// run the step
|
// run the step
|
||||||
if action := step.Run(state); action != multistep.ActionHalt {
|
assert.Equal(t, step.Run(state), multistep.ActionHalt, "Step should have failed and halted.")
|
||||||
t.Fatalf("bad action: %#v", action)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify state
|
// Verify state
|
||||||
if _, ok := state.GetOk("error"); !ok {
|
_, ok := state.GetOk("error")
|
||||||
t.Fatal("should have error")
|
assert.True(t, ok, "State should have an error.")
|
||||||
}
|
_, ok = state.GetOk("instance_name")
|
||||||
if _, ok := state.GetOk("instance_name"); ok {
|
assert.False(t, ok, "State should not have an instance name.")
|
||||||
t.Fatal("should NOT have instance name")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,10 @@ package googlecompute
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func testState(t *testing.T) multistep.StateBag {
|
func testState(t *testing.T) multistep.StateBag {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package googlecompute
|
package googlecompute
|
||||||
|
|
||||||
import(
|
import(
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
|
@ -10,7 +10,8 @@ import(
|
||||||
|
|
||||||
type StepWaitInstanceStartup int
|
type StepWaitInstanceStartup int
|
||||||
|
|
||||||
// Run reads the instance serial port output and looks for the log entry indicating the startup script finished.
|
// Run reads the instance metadata and looks for the log entry
|
||||||
|
// indicating the startup script finished.
|
||||||
func (s *StepWaitInstanceStartup) Run(state multistep.StateBag) multistep.StepAction {
|
func (s *StepWaitInstanceStartup) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
config := state.Get("config").(*Config)
|
config := state.Get("config").(*Config)
|
||||||
driver := state.Get("driver").(Driver)
|
driver := state.Get("driver").(Driver)
|
||||||
|
@ -21,14 +22,20 @@ func (s *StepWaitInstanceStartup) Run(state multistep.StateBag) multistep.StepAc
|
||||||
|
|
||||||
// Keep checking the serial port output to see if the startup script is done.
|
// Keep checking the serial port output to see if the startup script is done.
|
||||||
err := Retry(10, 60, 0, func() (bool, error) {
|
err := Retry(10, 60, 0, func() (bool, error) {
|
||||||
output, err := driver.GetSerialPortOutput(config.Zone, instanceName)
|
status, err := driver.GetInstanceMetadata(config.Zone,
|
||||||
|
instanceName, StartupScriptStatusKey)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf("Error getting serial port output: %s", err)
|
err := fmt.Errorf("Error getting startup script status: %s", err)
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
done := strings.Contains(output, StartupScriptDoneLog)
|
if status == StartupScriptStatusError {
|
||||||
|
err = errors.New("Startup script error.")
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
done := status == StartupScriptStatusDone
|
||||||
if !done {
|
if !done {
|
||||||
ui.Say("Startup script not finished yet. Waiting...")
|
ui.Say("Startup script not finished yet. Waiting...")
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,36 +3,28 @@ package googlecompute
|
||||||
import (
|
import (
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
"testing"
|
"testing"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStepWaitInstanceStartup(t *testing.T) {
|
func TestStepWaitInstanceStartup(t *testing.T) {
|
||||||
state := testState(t)
|
state := testState(t)
|
||||||
step := new(StepWaitInstanceStartup)
|
step := new(StepWaitInstanceStartup)
|
||||||
config := state.Get("config").(*Config)
|
c := state.Get("config").(*Config)
|
||||||
driver := state.Get("driver").(*DriverMock)
|
d := state.Get("driver").(*DriverMock)
|
||||||
|
|
||||||
testZone := "test-zone"
|
testZone := "test-zone"
|
||||||
testInstanceName := "test-instance-name"
|
testInstanceName := "test-instance-name"
|
||||||
|
|
||||||
config.Zone = testZone
|
c.Zone = testZone
|
||||||
state.Put("instance_name", testInstanceName)
|
state.Put("instance_name", testInstanceName)
|
||||||
// The done log triggers step completion.
|
|
||||||
driver.GetSerialPortOutputResult = StartupScriptDoneLog
|
// This step stops when it gets Done back from the metadata.
|
||||||
|
d.GetInstanceMetadataResult = StartupScriptStatusDone
|
||||||
|
|
||||||
// Run the step.
|
// Run the step.
|
||||||
if action := step.Run(state); action != multistep.ActionContinue {
|
assert.Equal(t, step.Run(state), multistep.ActionContinue, "Step should have passed and continued.")
|
||||||
t.Fatalf("StepWaitInstanceStartup did not return a Continue action: %#v", action)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that GetSerialPortOutput was called properly.
|
// Check that GetInstanceMetadata was called properly.
|
||||||
if driver.GetSerialPortOutputZone != testZone {
|
assert.Equal(t, d.GetInstanceMetadataZone, testZone, "Incorrect zone passed to GetInstanceMetadata.")
|
||||||
t.Fatalf(
|
assert.Equal(t, d.GetInstanceMetadataName, testInstanceName, "Incorrect instance name passed to GetInstanceMetadata.")
|
||||||
"GetSerialPortOutput wrong zone. Expected: %s, Actual: %s", driver.GetSerialPortOutputZone,
|
|
||||||
testZone)
|
|
||||||
}
|
|
||||||
if driver.GetSerialPortOutputName != testInstanceName {
|
|
||||||
t.Fatalf(
|
|
||||||
"GetSerialPortOutput wrong instance name. Expected: %s, Actual: %s", driver.GetSerialPortOutputName,
|
|
||||||
testInstanceName)
|
|
||||||
}
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue