Merge pull request #715 from mitchellh/f-gce-builder
Google Compute Builder
This commit is contained in:
commit
4f150dcc5c
39
builder/googlecompute/artifact.go
Normal file
39
builder/googlecompute/artifact.go
Normal file
@ -0,0 +1,39 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// Artifact represents a GCE image as the result of a Packer build.
|
||||
type Artifact struct {
|
||||
imageName string
|
||||
driver Driver
|
||||
}
|
||||
|
||||
// BuilderId returns the builder Id.
|
||||
func (*Artifact) BuilderId() string {
|
||||
return BuilderId
|
||||
}
|
||||
|
||||
// Destroy destroys the GCE image represented by the artifact.
|
||||
func (a *Artifact) Destroy() error {
|
||||
log.Printf("Destroying image: %s", a.imageName)
|
||||
errCh := a.driver.DeleteImage(a.imageName)
|
||||
return <-errCh
|
||||
}
|
||||
|
||||
// Files returns the files represented by the artifact.
|
||||
func (*Artifact) Files() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Id returns the GCE image name.
|
||||
func (a *Artifact) Id() string {
|
||||
return a.imageName
|
||||
}
|
||||
|
||||
// String returns the string representation of the artifact.
|
||||
func (a *Artifact) String() string {
|
||||
return fmt.Sprintf("A disk image was created: %v", a.imageName)
|
||||
}
|
10
builder/googlecompute/artifact_test.go
Normal file
10
builder/googlecompute/artifact_test.go
Normal file
@ -0,0 +1,10 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestArtifact_impl(t *testing.T) {
|
||||
var _ packer.Artifact = new(Artifact)
|
||||
}
|
100
builder/googlecompute/builder.go
Normal file
100
builder/googlecompute/builder.go
Normal file
@ -0,0 +1,100 @@
|
||||
// The googlecompute package contains a packer.Builder implementation that
|
||||
// builds images for Google Compute Engine.
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// The unique ID for this builder.
|
||||
const BuilderId = "packer.googlecompute"
|
||||
|
||||
// Builder represents a Packer Builder.
|
||||
type Builder struct {
|
||||
config *Config
|
||||
runner multistep.Runner
|
||||
}
|
||||
|
||||
// Prepare processes the build configuration parameters.
|
||||
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||
c, warnings, errs := NewConfig(raws...)
|
||||
if errs != nil {
|
||||
return warnings, errs
|
||||
}
|
||||
b.config = c
|
||||
|
||||
return warnings, nil
|
||||
}
|
||||
|
||||
// Run executes a googlecompute Packer build and returns a packer.Artifact
|
||||
// representing a GCE machine image.
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
driver, err := NewDriverGCE(
|
||||
ui, b.config.ProjectId, b.config.clientSecrets, b.config.privateKeyBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set up the state.
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("config", b.config)
|
||||
state.Put("driver", driver)
|
||||
state.Put("hook", hook)
|
||||
state.Put("ui", ui)
|
||||
|
||||
// Build the steps.
|
||||
steps := []multistep.Step{
|
||||
new(StepCreateSSHKey),
|
||||
new(StepCreateInstance),
|
||||
new(StepInstanceInfo),
|
||||
&common.StepConnectSSH{
|
||||
SSHAddress: sshAddress,
|
||||
SSHConfig: sshConfig,
|
||||
SSHWaitTimeout: 5 * time.Minute,
|
||||
},
|
||||
new(common.StepProvision),
|
||||
new(StepUpdateGsutil),
|
||||
new(StepCreateImage),
|
||||
new(StepUploadImage),
|
||||
new(StepRegisterImage),
|
||||
}
|
||||
|
||||
// Run the steps.
|
||||
if b.config.PackerDebug {
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: common.MultistepDebugFn(ui),
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
b.runner.Run(state)
|
||||
|
||||
// Report any errors.
|
||||
if rawErr, ok := state.GetOk("error"); ok {
|
||||
return nil, rawErr.(error)
|
||||
}
|
||||
if _, ok := state.GetOk("image_name"); !ok {
|
||||
log.Println("Failed to find image_name in state. Bug?")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
artifact := &Artifact{
|
||||
imageName: state.Get("image_name").(string),
|
||||
driver: driver,
|
||||
}
|
||||
return artifact, nil
|
||||
}
|
||||
|
||||
// Cancel.
|
||||
func (b *Builder) Cancel() {
|
||||
if b.runner != nil {
|
||||
log.Println("Cancelling the step runner...")
|
||||
b.runner.Cancel()
|
||||
}
|
||||
}
|
1
builder/googlecompute/builder_test.go
Normal file
1
builder/googlecompute/builder_test.go
Normal file
@ -0,0 +1 @@
|
||||
package googlecompute
|
32
builder/googlecompute/client_secrets.go
Normal file
32
builder/googlecompute/client_secrets.go
Normal file
@ -0,0 +1,32 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
// clientSecrets represents the client secrets of a GCE service account.
|
||||
type clientSecrets struct {
|
||||
Web struct {
|
||||
AuthURI string `json:"auth_uri"`
|
||||
ClientEmail string `json:"client_email"`
|
||||
ClientId string `json:"client_id"`
|
||||
TokenURI string `json:"token_uri"`
|
||||
}
|
||||
}
|
||||
|
||||
// loadClientSecrets loads the GCE client secrets file identified by path.
|
||||
func loadClientSecrets(path string) (*clientSecrets, error) {
|
||||
var cs *clientSecrets
|
||||
secretBytes, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(secretBytes, &cs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cs, nil
|
||||
}
|
31
builder/googlecompute/client_secrets_test.go
Normal file
31
builder/googlecompute/client_secrets_test.go
Normal file
@ -0,0 +1,31 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testClientSecretsFile(t *testing.T) string {
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
defer tf.Close()
|
||||
|
||||
if _, err := tf.Write([]byte(testClientSecretsContent)); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
return tf.Name()
|
||||
}
|
||||
|
||||
func TestLoadClientSecrets(t *testing.T) {
|
||||
_, err := loadClientSecrets(testClientSecretsFile(t))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// This is just some dummy data that doesn't actually work (it was revoked
|
||||
// a long time ago).
|
||||
const testClientSecretsContent = `{"web":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","client_email":"774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873@developer.gserviceaccount.com","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873@developer.gserviceaccount.com","client_id":"774313886706-eorlsj0r4eqkh5e7nvea5fuf59ifr873.apps.googleusercontent.com","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"}}`
|
187
builder/googlecompute/config.go
Normal file
187
builder/googlecompute/config.go
Normal file
@ -0,0 +1,187 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// Config is the configuration structure for the GCE builder. It stores
|
||||
// both the publicly settable state as well as the privately generated
|
||||
// state of the config object.
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
|
||||
BucketName string `mapstructure:"bucket_name"`
|
||||
ClientSecretsFile string `mapstructure:"client_secrets_file"`
|
||||
ImageName string `mapstructure:"image_name"`
|
||||
ImageDescription string `mapstructure:"image_description"`
|
||||
MachineType string `mapstructure:"machine_type"`
|
||||
Metadata map[string]string `mapstructure:"metadata"`
|
||||
Network string `mapstructure:"network"`
|
||||
Passphrase string `mapstructure:"passphrase"`
|
||||
PrivateKeyFile string `mapstructure:"private_key_file"`
|
||||
ProjectId string `mapstructure:"project_id"`
|
||||
SourceImage string `mapstructure:"source_image"`
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
RawStateTimeout string `mapstructure:"state_timeout"`
|
||||
Tags []string `mapstructure:"tags"`
|
||||
Zone string `mapstructure:"zone"`
|
||||
|
||||
clientSecrets *clientSecrets
|
||||
instanceName string
|
||||
privateKeyBytes []byte
|
||||
sshTimeout time.Duration
|
||||
stateTimeout time.Duration
|
||||
tpl *packer.ConfigTemplate
|
||||
}
|
||||
|
||||
func NewConfig(raws ...interface{}) (*Config, []string, error) {
|
||||
c := new(Config)
|
||||
md, err := common.DecodeConfig(c, raws...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
c.tpl, err = packer.NewConfigTemplate()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
c.tpl.UserVars = c.PackerUserVars
|
||||
|
||||
// Prepare the errors
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
|
||||
// Set defaults.
|
||||
if c.Network == "" {
|
||||
c.Network = "default"
|
||||
}
|
||||
|
||||
if c.ImageDescription == "" {
|
||||
c.ImageDescription = "Created by Packer"
|
||||
}
|
||||
|
||||
if c.ImageName == "" {
|
||||
c.ImageName = "packer-{{timestamp}}"
|
||||
}
|
||||
|
||||
if c.MachineType == "" {
|
||||
c.MachineType = "n1-standard-1"
|
||||
}
|
||||
|
||||
if c.RawSSHTimeout == "" {
|
||||
c.RawSSHTimeout = "5m"
|
||||
}
|
||||
|
||||
if c.RawStateTimeout == "" {
|
||||
c.RawStateTimeout = "5m"
|
||||
}
|
||||
|
||||
if c.SSHUsername == "" {
|
||||
c.SSHUsername = "root"
|
||||
}
|
||||
|
||||
if c.SSHPort == 0 {
|
||||
c.SSHPort = 22
|
||||
}
|
||||
|
||||
// Process Templates
|
||||
templates := map[string]*string{
|
||||
"bucket_name": &c.BucketName,
|
||||
"client_secrets_file": &c.ClientSecretsFile,
|
||||
"image_name": &c.ImageName,
|
||||
"image_description": &c.ImageDescription,
|
||||
"machine_type": &c.MachineType,
|
||||
"network": &c.Network,
|
||||
"passphrase": &c.Passphrase,
|
||||
"private_key_file": &c.PrivateKeyFile,
|
||||
"project_id": &c.ProjectId,
|
||||
"source_image": &c.SourceImage,
|
||||
"ssh_username": &c.SSHUsername,
|
||||
"ssh_timeout": &c.RawSSHTimeout,
|
||||
"state_timeout": &c.RawStateTimeout,
|
||||
"zone": &c.Zone,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = c.tpl.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
// Process required parameters.
|
||||
if c.BucketName == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("a bucket_name must be specified"))
|
||||
}
|
||||
|
||||
if c.ClientSecretsFile == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("a client_secrets_file must be specified"))
|
||||
}
|
||||
|
||||
if c.PrivateKeyFile == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("a private_key_file must be specified"))
|
||||
}
|
||||
|
||||
if c.ProjectId == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("a project_id must be specified"))
|
||||
}
|
||||
|
||||
if c.SourceImage == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("a source_image must be specified"))
|
||||
}
|
||||
|
||||
if c.Zone == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("a zone must be specified"))
|
||||
}
|
||||
|
||||
// Process timeout settings.
|
||||
sshTimeout, err := time.ParseDuration(c.RawSSHTimeout)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
|
||||
}
|
||||
c.sshTimeout = sshTimeout
|
||||
|
||||
stateTimeout, err := time.ParseDuration(c.RawStateTimeout)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed parsing state_timeout: %s", err))
|
||||
}
|
||||
c.stateTimeout = stateTimeout
|
||||
|
||||
// Load the client secrets file.
|
||||
cs, err := loadClientSecrets(c.ClientSecretsFile)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed parsing client secrets file: %s", err))
|
||||
}
|
||||
c.clientSecrets = cs
|
||||
|
||||
// Load the private key.
|
||||
c.privateKeyBytes, err = processPrivateKeyFile(c.PrivateKeyFile, c.Passphrase)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed loading private key file: %s", err))
|
||||
}
|
||||
|
||||
// Check for any errors.
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return nil, nil, errs
|
||||
}
|
||||
|
||||
return c, nil, nil
|
||||
}
|
176
builder/googlecompute/config_test.go
Normal file
176
builder/googlecompute/config_test.go
Normal file
@ -0,0 +1,176 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testConfig(t *testing.T) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"bucket_name": "foo",
|
||||
"client_secrets_file": testClientSecretsFile(t),
|
||||
"private_key_file": testPrivateKeyFile(t),
|
||||
"project_id": "hashicorp",
|
||||
"source_image": "foo",
|
||||
"zone": "us-east-1a",
|
||||
}
|
||||
}
|
||||
|
||||
func testConfigStruct(t *testing.T) *Config {
|
||||
c, warns, errs := NewConfig(testConfig(t))
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", len(warns))
|
||||
}
|
||||
if errs != nil {
|
||||
t.Fatalf("bad: %#v", errs)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func testConfigErr(t *testing.T, warns []string, err error, extra string) {
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatalf("should error: %s", extra)
|
||||
}
|
||||
}
|
||||
|
||||
func testConfigOk(t *testing.T, warns []string, err error) {
|
||||
if len(warns) > 0 {
|
||||
t.Fatalf("bad: %#v", warns)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigPrepare(t *testing.T) {
|
||||
cases := []struct {
|
||||
Key string
|
||||
Value interface{}
|
||||
Err bool
|
||||
}{
|
||||
{
|
||||
"unknown_key",
|
||||
"bad",
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
"bucket_name",
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"bucket_name",
|
||||
"good",
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"client_secrets_file",
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"client_secrets_file",
|
||||
testClientSecretsFile(t),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"client_secrets_file",
|
||||
"/tmp/i/should/not/exist",
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
"private_key_file",
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"private_key_file",
|
||||
testPrivateKeyFile(t),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"private_key_file",
|
||||
"/tmp/i/should/not/exist",
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
"project_id",
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"project_id",
|
||||
"foo",
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"source_image",
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"source_image",
|
||||
"foo",
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"zone",
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"zone",
|
||||
"foo",
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"ssh_timeout",
|
||||
"SO BAD",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"ssh_timeout",
|
||||
"5s",
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"state_timeout",
|
||||
"SO BAD",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"state_timeout",
|
||||
"5s",
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
raw := testConfig(t)
|
||||
|
||||
if tc.Value == nil {
|
||||
delete(raw, tc.Key)
|
||||
} else {
|
||||
raw[tc.Key] = tc.Value
|
||||
}
|
||||
|
||||
_, warns, errs := NewConfig(raw)
|
||||
|
||||
if tc.Err {
|
||||
testConfigErr(t, warns, errs, tc.Key)
|
||||
} else {
|
||||
testConfigOk(t, warns, errs)
|
||||
}
|
||||
}
|
||||
}
|
35
builder/googlecompute/driver.go
Normal file
35
builder/googlecompute/driver.go
Normal file
@ -0,0 +1,35 @@
|
||||
package googlecompute
|
||||
|
||||
// Driver is the interface that has to be implemented to communicate
|
||||
// with GCE. The Driver interface exists mostly to allow a mock implementation
|
||||
// to be used to test the steps.
|
||||
type Driver interface {
|
||||
// CreateImage creates an image with the given URL in Google Storage.
|
||||
CreateImage(name, description, url string) <-chan error
|
||||
|
||||
// DeleteImage deletes the image with the given name.
|
||||
DeleteImage(name string) <-chan error
|
||||
|
||||
// DeleteInstance deletes the given instance.
|
||||
DeleteInstance(zone, name string) (<-chan error, error)
|
||||
|
||||
// GetNatIP gets the NAT IP address for the instance.
|
||||
GetNatIP(zone, name string) (string, error)
|
||||
|
||||
// RunInstance takes the given config and launches an instance.
|
||||
RunInstance(*InstanceConfig) (<-chan error, error)
|
||||
|
||||
// WaitForInstance waits for an instance to reach the given state.
|
||||
WaitForInstance(state, zone, name string) <-chan error
|
||||
}
|
||||
|
||||
type InstanceConfig struct {
|
||||
Description string
|
||||
Image string
|
||||
MachineType string
|
||||
Metadata map[string]string
|
||||
Name string
|
||||
Network string
|
||||
Tags []string
|
||||
Zone string
|
||||
}
|
309
builder/googlecompute/driver_gce.go
Normal file
309
builder/googlecompute/driver_gce.go
Normal file
@ -0,0 +1,309 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"code.google.com/p/goauth2/oauth"
|
||||
"code.google.com/p/goauth2/oauth/jwt"
|
||||
"code.google.com/p/google-api-go-client/compute/v1beta16"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// driverGCE is a Driver implementation that actually talks to GCE.
|
||||
// Create an instance using NewDriverGCE.
|
||||
type driverGCE struct {
|
||||
projectId string
|
||||
service *compute.Service
|
||||
ui packer.Ui
|
||||
}
|
||||
|
||||
const DriverScopes string = "https://www.googleapis.com/auth/compute " +
|
||||
"https://www.googleapis.com/auth/devstorage.full_control"
|
||||
|
||||
func NewDriverGCE(ui packer.Ui, projectId string, c *clientSecrets, key []byte) (Driver, error) {
|
||||
log.Printf("[INFO] Requesting token...")
|
||||
log.Printf("[INFO] -- Email: %s", c.Web.ClientEmail)
|
||||
log.Printf("[INFO] -- Scopes: %s", DriverScopes)
|
||||
log.Printf("[INFO] -- Private Key Length: %d", len(key))
|
||||
log.Printf("[INFO] -- Token URL: %s", c.Web.TokenURI)
|
||||
jwtTok := jwt.NewToken(c.Web.ClientEmail, DriverScopes, key)
|
||||
jwtTok.ClaimSet.Aud = c.Web.TokenURI
|
||||
token, err := jwtTok.Assert(new(http.Client))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transport := &oauth.Transport{
|
||||
Config: &oauth.Config{
|
||||
ClientId: c.Web.ClientId,
|
||||
Scope: DriverScopes,
|
||||
TokenURL: c.Web.TokenURI,
|
||||
AuthURL: c.Web.AuthURI,
|
||||
},
|
||||
Token: token,
|
||||
}
|
||||
|
||||
log.Printf("[INFO] Instantiating client...")
|
||||
service, err := compute.New(transport.Client())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &driverGCE{
|
||||
projectId: projectId,
|
||||
service: service,
|
||||
ui: ui,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *driverGCE) CreateImage(name, description, url string) <-chan error {
|
||||
image := &compute.Image{
|
||||
Description: description,
|
||||
Name: name,
|
||||
RawDisk: &compute.ImageRawDisk{
|
||||
ContainerType: "TAR",
|
||||
Source: url,
|
||||
},
|
||||
SourceType: "RAW",
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
op, err := d.service.Images.Insert(d.projectId, image).Do()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
} else {
|
||||
go waitForState(errCh, "DONE", d.refreshGlobalOp(op))
|
||||
}
|
||||
|
||||
return errCh
|
||||
}
|
||||
|
||||
func (d *driverGCE) DeleteImage(name string) <-chan error {
|
||||
errCh := make(chan error, 1)
|
||||
op, err := d.service.Images.Delete(d.projectId, name).Do()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
} else {
|
||||
go waitForState(errCh, "DONE", d.refreshGlobalOp(op))
|
||||
}
|
||||
|
||||
return errCh
|
||||
}
|
||||
|
||||
func (d *driverGCE) DeleteInstance(zone, name string) (<-chan error, error) {
|
||||
op, err := d.service.Instances.Delete(d.projectId, zone, name).Do()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go waitForState(errCh, "DONE", d.refreshZoneOp(zone, op))
|
||||
return errCh, nil
|
||||
}
|
||||
|
||||
func (d *driverGCE) GetNatIP(zone, name string) (string, error) {
|
||||
instance, err := d.service.Instances.Get(d.projectId, zone, name).Do()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, ni := range instance.NetworkInterfaces {
|
||||
if ni.AccessConfigs == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ac := range ni.AccessConfigs {
|
||||
if ac.NatIP != "" {
|
||||
return ac.NatIP, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (d *driverGCE) RunInstance(c *InstanceConfig) (<-chan error, error) {
|
||||
// Get the zone
|
||||
d.ui.Message(fmt.Sprintf("Loading zone: %s", c.Zone))
|
||||
zone, err := d.service.Zones.Get(d.projectId, c.Zone).Do()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the image
|
||||
d.ui.Message(fmt.Sprintf("Loading image: %s", c.Image))
|
||||
image, err := d.getImage(c.Image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the machine type
|
||||
d.ui.Message(fmt.Sprintf("Loading machine type: %s", c.MachineType))
|
||||
machineType, err := d.service.MachineTypes.Get(
|
||||
d.projectId, zone.Name, c.MachineType).Do()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO(mitchellh): deprecation warnings
|
||||
|
||||
// Get the network
|
||||
d.ui.Message(fmt.Sprintf("Loading network: %s", c.Network))
|
||||
network, err := d.service.Networks.Get(d.projectId, c.Network).Do()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build up the metadata
|
||||
metadata := make([]*compute.MetadataItems, len(c.Metadata))
|
||||
for k, v := range c.Metadata {
|
||||
metadata = append(metadata, &compute.MetadataItems{
|
||||
Key: k,
|
||||
Value: v,
|
||||
})
|
||||
}
|
||||
|
||||
// Create the instance information
|
||||
instance := compute.Instance{
|
||||
Description: c.Description,
|
||||
Image: image.SelfLink,
|
||||
MachineType: machineType.SelfLink,
|
||||
Metadata: &compute.Metadata{
|
||||
Items: metadata,
|
||||
},
|
||||
Name: c.Name,
|
||||
NetworkInterfaces: []*compute.NetworkInterface{
|
||||
&compute.NetworkInterface{
|
||||
AccessConfigs: []*compute.AccessConfig{
|
||||
&compute.AccessConfig{
|
||||
Name: "AccessConfig created by Packer",
|
||||
Type: "ONE_TO_ONE_NAT",
|
||||
},
|
||||
},
|
||||
Network: network.SelfLink,
|
||||
},
|
||||
},
|
||||
ServiceAccounts: []*compute.ServiceAccount{
|
||||
&compute.ServiceAccount{
|
||||
Email: "default",
|
||||
Scopes: []string{
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
"https://www.googleapis.com/auth/compute",
|
||||
"https://www.googleapis.com/auth/devstorage.full_control",
|
||||
},
|
||||
},
|
||||
},
|
||||
Tags: &compute.Tags{
|
||||
Items: c.Tags,
|
||||
},
|
||||
}
|
||||
|
||||
d.ui.Message("Requesting instance creation...")
|
||||
op, err := d.service.Instances.Insert(d.projectId, zone.Name, &instance).Do()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go waitForState(errCh, "DONE", d.refreshZoneOp(zone.Name, op))
|
||||
return errCh, nil
|
||||
}
|
||||
|
||||
func (d *driverGCE) WaitForInstance(state, zone, name string) <-chan error {
|
||||
errCh := make(chan error, 1)
|
||||
go waitForState(errCh, state, d.refreshInstanceState(zone, name))
|
||||
return errCh
|
||||
}
|
||||
|
||||
func (d *driverGCE) getImage(name string) (image *compute.Image, err error) {
|
||||
projects := []string{d.projectId, "debian-cloud", "centos-cloud"}
|
||||
for _, project := range projects {
|
||||
image, err = d.service.Images.Get(project, name).Do()
|
||||
if err == nil && image != nil && image.SelfLink != "" {
|
||||
return
|
||||
}
|
||||
image = nil
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
err = fmt.Errorf("Image could not be found: %s", name)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *driverGCE) refreshInstanceState(zone, name string) stateRefreshFunc {
|
||||
return func() (string, error) {
|
||||
instance, err := d.service.Instances.Get(d.projectId, zone, name).Do()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return instance.Status, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *driverGCE) refreshGlobalOp(op *compute.Operation) stateRefreshFunc {
|
||||
return func() (string, error) {
|
||||
newOp, err := d.service.GlobalOperations.Get(d.projectId, op.Name).Do()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// If the op is done, check for errors
|
||||
err = nil
|
||||
if newOp.Status == "DONE" {
|
||||
if newOp.Error != nil {
|
||||
for _, e := range newOp.Error.Errors {
|
||||
err = packer.MultiErrorAppend(err, fmt.Errorf(e.Message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newOp.Status, err
|
||||
}
|
||||
}
|
||||
|
||||
func (d *driverGCE) refreshZoneOp(zone string, op *compute.Operation) stateRefreshFunc {
|
||||
return func() (string, error) {
|
||||
newOp, err := d.service.ZoneOperations.Get(d.projectId, zone, op.Name).Do()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// If the op is done, check for errors
|
||||
err = nil
|
||||
if newOp.Status == "DONE" {
|
||||
if newOp.Error != nil {
|
||||
for _, e := range newOp.Error.Errors {
|
||||
err = packer.MultiErrorAppend(err, fmt.Errorf(e.Message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newOp.Status, err
|
||||
}
|
||||
}
|
||||
|
||||
// stateRefreshFunc is used to refresh the state of a thing and is
|
||||
// used in conjunction with waitForState.
|
||||
type stateRefreshFunc func() (string, error)
|
||||
|
||||
// waitForState will spin in a loop forever waiting for state to
|
||||
// reach a certain target.
|
||||
func waitForState(errCh chan<- error, target string, refresh stateRefreshFunc) {
|
||||
for {
|
||||
state, err := refresh()
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
if state == target {
|
||||
errCh <- nil
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
108
builder/googlecompute/driver_mock.go
Normal file
108
builder/googlecompute/driver_mock.go
Normal file
@ -0,0 +1,108 @@
|
||||
package googlecompute
|
||||
|
||||
// DriverMock is a Driver implementation that is a mocked out so that
|
||||
// it can be used for tests.
|
||||
type DriverMock struct {
|
||||
CreateImageName string
|
||||
CreateImageDesc string
|
||||
CreateImageURL string
|
||||
CreateImageErrCh <-chan error
|
||||
|
||||
DeleteImageName string
|
||||
DeleteImageErrCh <-chan error
|
||||
|
||||
DeleteInstanceZone string
|
||||
DeleteInstanceName string
|
||||
DeleteInstanceErrCh <-chan error
|
||||
DeleteInstanceErr error
|
||||
|
||||
GetNatIPZone string
|
||||
GetNatIPName string
|
||||
GetNatIPResult string
|
||||
GetNatIPErr error
|
||||
|
||||
RunInstanceConfig *InstanceConfig
|
||||
RunInstanceErrCh <-chan error
|
||||
RunInstanceErr error
|
||||
|
||||
WaitForInstanceState string
|
||||
WaitForInstanceZone string
|
||||
WaitForInstanceName string
|
||||
WaitForInstanceErrCh <-chan error
|
||||
}
|
||||
|
||||
func (d *DriverMock) CreateImage(name, description, url string) <-chan error {
|
||||
d.CreateImageName = name
|
||||
d.CreateImageDesc = description
|
||||
d.CreateImageURL = url
|
||||
|
||||
resultCh := d.CreateImageErrCh
|
||||
if resultCh == nil {
|
||||
ch := make(chan error)
|
||||
close(ch)
|
||||
resultCh = ch
|
||||
}
|
||||
|
||||
return resultCh
|
||||
}
|
||||
|
||||
func (d *DriverMock) DeleteImage(name string) <-chan error {
|
||||
d.DeleteImageName = name
|
||||
|
||||
resultCh := d.DeleteImageErrCh
|
||||
if resultCh == nil {
|
||||
ch := make(chan error)
|
||||
close(ch)
|
||||
resultCh = ch
|
||||
}
|
||||
|
||||
return resultCh
|
||||
}
|
||||
|
||||
func (d *DriverMock) DeleteInstance(zone, name string) (<-chan error, error) {
|
||||
d.DeleteInstanceZone = zone
|
||||
d.DeleteInstanceName = name
|
||||
|
||||
resultCh := d.DeleteInstanceErrCh
|
||||
if resultCh == nil {
|
||||
ch := make(chan error)
|
||||
close(ch)
|
||||
resultCh = ch
|
||||
}
|
||||
|
||||
return resultCh, d.DeleteInstanceErr
|
||||
}
|
||||
|
||||
func (d *DriverMock) GetNatIP(zone, name string) (string, error) {
|
||||
d.GetNatIPZone = zone
|
||||
d.GetNatIPName = name
|
||||
return d.GetNatIPResult, d.GetNatIPErr
|
||||
}
|
||||
|
||||
func (d *DriverMock) RunInstance(c *InstanceConfig) (<-chan error, error) {
|
||||
d.RunInstanceConfig = c
|
||||
|
||||
resultCh := d.RunInstanceErrCh
|
||||
if resultCh == nil {
|
||||
ch := make(chan error)
|
||||
close(ch)
|
||||
resultCh = ch
|
||||
}
|
||||
|
||||
return resultCh, d.RunInstanceErr
|
||||
}
|
||||
|
||||
func (d *DriverMock) WaitForInstance(state, zone, name string) <-chan error {
|
||||
d.WaitForInstanceState = state
|
||||
d.WaitForInstanceZone = zone
|
||||
d.WaitForInstanceName = name
|
||||
|
||||
resultCh := d.WaitForInstanceErrCh
|
||||
if resultCh == nil {
|
||||
ch := make(chan error)
|
||||
close(ch)
|
||||
resultCh = ch
|
||||
}
|
||||
|
||||
return resultCh
|
||||
}
|
43
builder/googlecompute/private_key.go
Normal file
43
builder/googlecompute/private_key.go
Normal file
@ -0,0 +1,43 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
// processPrivateKeyFile takes a private key file and an optional passphrase
|
||||
// and decodes it to a byte slice.
|
||||
func processPrivateKeyFile(privateKeyFile, passphrase string) ([]byte, error) {
|
||||
rawPrivateKeyBytes, err := ioutil.ReadFile(privateKeyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed loading private key file: %s", err)
|
||||
}
|
||||
|
||||
PEMBlock, _ := pem.Decode(rawPrivateKeyBytes)
|
||||
if PEMBlock == nil {
|
||||
return nil, fmt.Errorf(
|
||||
"%s does not contain a vaild private key", privateKeyFile)
|
||||
}
|
||||
|
||||
if x509.IsEncryptedPEMBlock(PEMBlock) {
|
||||
if passphrase == "" {
|
||||
return nil, errors.New("a passphrase must be specified when using an encrypted private key")
|
||||
}
|
||||
|
||||
decryptedPrivateKeyBytes, err := x509.DecryptPEMBlock(PEMBlock, []byte(passphrase))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed decrypting private key: %s", err)
|
||||
}
|
||||
|
||||
b := &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: decryptedPrivateKeyBytes,
|
||||
}
|
||||
return pem.EncodeToMemory(b), nil
|
||||
}
|
||||
|
||||
return rawPrivateKeyBytes, nil
|
||||
}
|
78
builder/googlecompute/private_key_test.go
Normal file
78
builder/googlecompute/private_key_test.go
Normal file
@ -0,0 +1,78 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testPrivateKeyFile(t *testing.T) string {
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
defer tf.Close()
|
||||
|
||||
b := &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: []byte("what"),
|
||||
}
|
||||
|
||||
if err := pem.Encode(tf, b); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
return tf.Name()
|
||||
}
|
||||
|
||||
func TestProcesssPrivateKeyFile(t *testing.T) {
|
||||
path := testPrivateKeyFile(t)
|
||||
defer os.Remove(path)
|
||||
|
||||
data, err := processPrivateKeyFile(path, "")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if len(data) <= 0 {
|
||||
t.Fatalf("bad: %#v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessPrivateKeyFile_encrypted(t *testing.T) {
|
||||
// Encrypt the file
|
||||
b, err := x509.EncryptPEMBlock(rand.Reader,
|
||||
"RSA PRIVATE KEY",
|
||||
[]byte("what"),
|
||||
[]byte("password"),
|
||||
x509.PEMCipherAES128)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
defer os.Remove(tf.Name())
|
||||
|
||||
err = pem.Encode(tf, b)
|
||||
tf.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
path := tf.Name()
|
||||
|
||||
// Should have an error with a bad password
|
||||
if _, err := processPrivateKeyFile(path, "bad"); err == nil {
|
||||
t.Fatal("should error")
|
||||
}
|
||||
|
||||
if _, err := processPrivateKeyFile(path, "password"); err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
}
|
34
builder/googlecompute/ssh.go
Normal file
34
builder/googlecompute/ssh.go
Normal file
@ -0,0 +1,34 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
gossh "code.google.com/p/go.crypto/ssh"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/communicator/ssh"
|
||||
)
|
||||
|
||||
// sshAddress returns the ssh address.
|
||||
func sshAddress(state multistep.StateBag) (string, error) {
|
||||
config := state.Get("config").(*Config)
|
||||
ipAddress := state.Get("instance_ip").(string)
|
||||
return fmt.Sprintf("%s:%d", ipAddress, config.SSHPort), nil
|
||||
}
|
||||
|
||||
// sshConfig returns the ssh configuration.
|
||||
func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
|
||||
config := state.Get("config").(*Config)
|
||||
privateKey := state.Get("ssh_private_key").(string)
|
||||
|
||||
keyring := new(ssh.SimpleKeychain)
|
||||
if err := keyring.AddPEMKey(privateKey); err != nil {
|
||||
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
|
||||
}
|
||||
|
||||
sshConfig := &gossh.ClientConfig{
|
||||
User: config.SSHUsername,
|
||||
Auth: []gossh.ClientAuth{gossh.ClientAuthKeyring(keyring)},
|
||||
}
|
||||
|
||||
return sshConfig, nil
|
||||
}
|
52
builder/googlecompute/step_create_image.go
Normal file
52
builder/googlecompute/step_create_image.go
Normal file
@ -0,0 +1,52 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// StepCreateImage represents a Packer build step that creates GCE machine
|
||||
// images.
|
||||
type StepCreateImage int
|
||||
|
||||
// Run executes the Packer build step that creates a GCE machine image.
|
||||
//
|
||||
// Currently the only way to create a GCE image is to run the gcimagebundle
|
||||
// command on the running GCE instance.
|
||||
func (s *StepCreateImage) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
comm := state.Get("communicator").(packer.Communicator)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
sudoPrefix := ""
|
||||
if config.SSHUsername != "root" {
|
||||
sudoPrefix = "sudo "
|
||||
}
|
||||
|
||||
imageFilename := fmt.Sprintf("%s.tar.gz", config.ImageName)
|
||||
imageBundleCmd := "/usr/bin/gcimagebundle -d /dev/sda -o /tmp/"
|
||||
|
||||
ui.Say("Creating image...")
|
||||
cmd := new(packer.RemoteCmd)
|
||||
cmd.Command = fmt.Sprintf("%s%s --output_file_name %s",
|
||||
sudoPrefix, imageBundleCmd, imageFilename)
|
||||
err := cmd.StartWithUi(comm, ui)
|
||||
if err == nil && cmd.ExitStatus != 0 {
|
||||
err = fmt.Errorf(
|
||||
"gcimagebundle exited with non-zero exit status: %d", cmd.ExitStatus)
|
||||
}
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating image: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("image_file_name", filepath.Join("/tmp", imageFilename))
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepCreateImage) Cleanup(state multistep.StateBag) {}
|
96
builder/googlecompute/step_create_image_test.go
Normal file
96
builder/googlecompute/step_create_image_test.go
Normal file
@ -0,0 +1,96 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
func TestStepCreateImage_impl(t *testing.T) {
|
||||
var _ multistep.Step = new(StepCreateImage)
|
||||
}
|
||||
|
||||
func TestStepCreateImage(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepCreateImage)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
comm := new(packer.MockCommunicator)
|
||||
state.Put("communicator", comm)
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify
|
||||
if !comm.StartCalled {
|
||||
t.Fatal("start should be called")
|
||||
}
|
||||
if strings.HasPrefix(comm.StartCmd.Command, "sudo") {
|
||||
t.Fatal("should not sudo")
|
||||
}
|
||||
if !strings.Contains(comm.StartCmd.Command, "gcimagebundle") {
|
||||
t.Fatalf("bad command: %#v", comm.StartCmd.Command)
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("image_file_name"); !ok {
|
||||
t.Fatal("should have image")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepCreateImage_badExitStatus(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepCreateImage)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
comm := new(packer.MockCommunicator)
|
||||
comm.StartExitStatus = 12
|
||||
state.Put("communicator", comm)
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
if _, ok := state.GetOk("image_file_name"); ok {
|
||||
t.Fatal("should NOT have image")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepCreateImage_nonRoot(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepCreateImage)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
comm := new(packer.MockCommunicator)
|
||||
state.Put("communicator", comm)
|
||||
|
||||
config := state.Get("config").(*Config)
|
||||
config.SSHUsername = "bob"
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify
|
||||
if !comm.StartCalled {
|
||||
t.Fatal("start should be called")
|
||||
}
|
||||
if !strings.HasPrefix(comm.StartCmd.Command, "sudo") {
|
||||
t.Fatal("should sudo")
|
||||
}
|
||||
if !strings.Contains(comm.StartCmd.Command, "gcimagebundle") {
|
||||
t.Fatalf("bad command: %#v", comm.StartCmd.Command)
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("image_file_name"); !ok {
|
||||
t.Fatal("should have image")
|
||||
}
|
||||
}
|
95
builder/googlecompute/step_create_instance.go
Normal file
95
builder/googlecompute/step_create_instance.go
Normal file
@ -0,0 +1,95 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common/uuid"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// StepCreateInstance represents a Packer build step that creates GCE instances.
|
||||
type StepCreateInstance struct {
|
||||
instanceName string
|
||||
}
|
||||
|
||||
// Run executes the Packer build step that creates a GCE instance.
|
||||
func (s *StepCreateInstance) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
driver := state.Get("driver").(Driver)
|
||||
sshPublicKey := state.Get("ssh_public_key").(string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Creating instance...")
|
||||
name := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
|
||||
|
||||
errCh, err := driver.RunInstance(&InstanceConfig{
|
||||
Description: "New instance created by Packer",
|
||||
Image: config.SourceImage,
|
||||
MachineType: config.MachineType,
|
||||
Metadata: map[string]string{
|
||||
"sshKeys": fmt.Sprintf("%s:%s", config.SSHUsername, sshPublicKey),
|
||||
},
|
||||
Name: name,
|
||||
Network: config.Network,
|
||||
Tags: config.Tags,
|
||||
Zone: config.Zone,
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
ui.Message("Waiting for creation operation to complete...")
|
||||
select {
|
||||
case err = <-errCh:
|
||||
case <-time.After(config.stateTimeout):
|
||||
err = errors.New("time out while waiting for instance to create")
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating instance: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Message("Instance has been created!")
|
||||
|
||||
// Things succeeded, store the name so we can remove it later
|
||||
state.Put("instance_name", name)
|
||||
s.instanceName = name
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// Cleanup destroys the GCE instance created during the image creation process.
|
||||
func (s *StepCreateInstance) Cleanup(state multistep.StateBag) {
|
||||
if s.instanceName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
config := state.Get("config").(*Config)
|
||||
driver := state.Get("driver").(Driver)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Deleting instance...")
|
||||
errCh, err := driver.DeleteInstance(config.Zone, s.instanceName)
|
||||
if err == nil {
|
||||
select {
|
||||
case err = <-errCh:
|
||||
case <-time.After(config.stateTimeout):
|
||||
err = errors.New("time out while waiting for instance to delete")
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error deleting instance. Please delete it manually.\n\n"+
|
||||
"Name: %s\n"+
|
||||
"Error: %s", s.instanceName, err))
|
||||
}
|
||||
|
||||
s.instanceName = ""
|
||||
return
|
||||
}
|
128
builder/googlecompute/step_create_instance_test.go
Normal file
128
builder/googlecompute/step_create_instance_test.go
Normal file
@ -0,0 +1,128 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/mitchellh/multistep"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestStepCreateInstance_impl(t *testing.T) {
|
||||
var _ multistep.Step = new(StepCreateInstance)
|
||||
}
|
||||
|
||||
func TestStepCreateInstance(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepCreateInstance)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
state.Put("ssh_public_key", "key")
|
||||
|
||||
config := state.Get("config").(*Config)
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify state
|
||||
nameRaw, ok := state.GetOk("instance_name")
|
||||
if !ok {
|
||||
t.Fatal("should have instance name")
|
||||
}
|
||||
|
||||
// cleanup
|
||||
step.Cleanup(state)
|
||||
|
||||
if driver.DeleteInstanceName != nameRaw.(string) {
|
||||
t.Fatal("should've deleted instance")
|
||||
}
|
||||
if driver.DeleteInstanceZone != config.Zone {
|
||||
t.Fatal("bad zone: %#v", driver.DeleteInstanceZone)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepCreateInstance_error(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepCreateInstance)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
state.Put("ssh_public_key", "key")
|
||||
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
driver.RunInstanceErr = errors.New("error")
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify state
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
if _, ok := state.GetOk("instance_name"); ok {
|
||||
t.Fatal("should NOT have instance name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepCreateInstance_errorOnChannel(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepCreateInstance)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
errCh <- errors.New("error")
|
||||
|
||||
state.Put("ssh_public_key", "key")
|
||||
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
driver.RunInstanceErrCh = errCh
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify state
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
if _, ok := state.GetOk("instance_name"); ok {
|
||||
t.Fatal("should NOT have instance name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepCreateInstance_errorTimeout(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepCreateInstance)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
<-time.After(10 * time.Millisecond)
|
||||
errCh <- nil
|
||||
}()
|
||||
|
||||
state.Put("ssh_public_key", "key")
|
||||
|
||||
config := state.Get("config").(*Config)
|
||||
config.stateTimeout = 1 * time.Microsecond
|
||||
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
driver.RunInstanceErrCh = errCh
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify state
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
if _, ok := state.GetOk("instance_name"); ok {
|
||||
t.Fatal("should NOT have instance name")
|
||||
}
|
||||
}
|
51
builder/googlecompute/step_create_ssh_key.go
Normal file
51
builder/googlecompute/step_create_ssh_key.go
Normal file
@ -0,0 +1,51 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
|
||||
"code.google.com/p/go.crypto/ssh"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// StepCreateSSHKey represents a Packer build step that generates SSH key pairs.
|
||||
type StepCreateSSHKey int
|
||||
|
||||
// Run executes the Packer build step that generates SSH key pairs.
|
||||
func (s *StepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Creating temporary SSH key for instance...")
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating temporary ssh key: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
priv_blk := pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Headers: nil,
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(priv),
|
||||
}
|
||||
|
||||
pub, err := ssh.NewPublicKey(&priv.PublicKey)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating temporary ssh key: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("ssh_private_key", string(pem.EncodeToMemory(&priv_blk)))
|
||||
state.Put("ssh_public_key", string(ssh.MarshalAuthorizedKey(pub)))
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// Nothing to clean up. SSH keys are associated with a single GCE instance.
|
||||
func (s *StepCreateSSHKey) Cleanup(state multistep.StateBag) {}
|
29
builder/googlecompute/step_create_ssh_key_test.go
Normal file
29
builder/googlecompute/step_create_ssh_key_test.go
Normal file
@ -0,0 +1,29 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStepCreateSSHKey_impl(t *testing.T) {
|
||||
var _ multistep.Step = new(StepCreateSSHKey)
|
||||
}
|
||||
|
||||
func TestStepCreateSSHKey(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepCreateSSHKey)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify that we have a public/private key
|
||||
if _, ok := state.GetOk("ssh_private_key"); !ok {
|
||||
t.Fatal("should have key")
|
||||
}
|
||||
if _, ok := state.GetOk("ssh_public_key"); !ok {
|
||||
t.Fatal("should have key")
|
||||
}
|
||||
}
|
53
builder/googlecompute/step_instance_info.go
Normal file
53
builder/googlecompute/step_instance_info.go
Normal file
@ -0,0 +1,53 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// stepInstanceInfo represents a Packer build step that gathers GCE instance info.
|
||||
type StepInstanceInfo int
|
||||
|
||||
// Run executes the Packer build step that gathers GCE instance info.
|
||||
func (s *StepInstanceInfo) 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 the instance to become running...")
|
||||
errCh := driver.WaitForInstance("RUNNING", config.Zone, instanceName)
|
||||
var err error
|
||||
select {
|
||||
case err = <-errCh:
|
||||
case <-time.After(config.stateTimeout):
|
||||
err = errors.New("time out while waiting for instance to become running")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for instance: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ip, err := driver.GetNatIP(config.Zone, instanceName)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error retrieving instance nat ip address: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Message(fmt.Sprintf("IP: %s", ip))
|
||||
state.Put("instance_ip", ip)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// Cleanup.
|
||||
func (s *StepInstanceInfo) Cleanup(state multistep.StateBag) {}
|
134
builder/googlecompute/step_instance_info_test.go
Normal file
134
builder/googlecompute/step_instance_info_test.go
Normal file
@ -0,0 +1,134 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/mitchellh/multistep"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestStepInstanceInfo_impl(t *testing.T) {
|
||||
var _ multistep.Step = new(StepInstanceInfo)
|
||||
}
|
||||
|
||||
func TestStepInstanceInfo(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepInstanceInfo)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
state.Put("instance_name", "foo")
|
||||
|
||||
config := state.Get("config").(*Config)
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
driver.GetNatIPResult = "1.2.3.4"
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify state
|
||||
if driver.WaitForInstanceState != "RUNNING" {
|
||||
t.Fatalf("bad: %#v", driver.WaitForInstanceState)
|
||||
}
|
||||
if driver.WaitForInstanceZone != config.Zone {
|
||||
t.Fatalf("bad: %#v", driver.WaitForInstanceZone)
|
||||
}
|
||||
if driver.WaitForInstanceName != "foo" {
|
||||
t.Fatalf("bad: %#v", driver.WaitForInstanceName)
|
||||
}
|
||||
|
||||
ipRaw, ok := state.GetOk("instance_ip")
|
||||
if !ok {
|
||||
t.Fatal("should have ip")
|
||||
}
|
||||
if ip, ok := ipRaw.(string); !ok {
|
||||
t.Fatal("ip is not a string")
|
||||
} else if ip != "1.2.3.4" {
|
||||
t.Fatalf("bad ip: %s", ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepInstanceInfo_getNatIPError(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepInstanceInfo)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
state.Put("instance_name", "foo")
|
||||
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
driver.GetNatIPErr = errors.New("error")
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify state
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
if _, ok := state.GetOk("instance_ip"); ok {
|
||||
t.Fatal("should NOT have instance IP")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepInstanceInfo_waitError(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepInstanceInfo)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
state.Put("instance_name", "foo")
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
errCh <- errors.New("error")
|
||||
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
driver.WaitForInstanceErrCh = errCh
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify state
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
if _, ok := state.GetOk("instance_ip"); ok {
|
||||
t.Fatal("should NOT have instance IP")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepInstanceInfo_errorTimeout(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepInstanceInfo)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
<-time.After(10 * time.Millisecond)
|
||||
errCh <- nil
|
||||
}()
|
||||
|
||||
state.Put("instance_name", "foo")
|
||||
|
||||
config := state.Get("config").(*Config)
|
||||
config.stateTimeout = 1 * time.Microsecond
|
||||
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
driver.WaitForInstanceErrCh = errCh
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify state
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
if _, ok := state.GetOk("instance_ip"); ok {
|
||||
t.Fatal("should NOT have instance IP")
|
||||
}
|
||||
}
|
46
builder/googlecompute/step_register_image.go
Normal file
46
builder/googlecompute/step_register_image.go
Normal file
@ -0,0 +1,46 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// StepRegisterImage represents a Packer build step that registers GCE machine images.
|
||||
type StepRegisterImage int
|
||||
|
||||
// Run executes the Packer build step that registers a GCE machine image.
|
||||
func (s *StepRegisterImage) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
driver := state.Get("driver").(Driver)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
var err error
|
||||
imageURL := fmt.Sprintf(
|
||||
"https://storage.cloud.google.com/%s/%s.tar.gz",
|
||||
config.BucketName, config.ImageName)
|
||||
|
||||
ui.Say("Registering image...")
|
||||
errCh := driver.CreateImage(config.ImageName, config.ImageDescription, imageURL)
|
||||
select {
|
||||
case err = <-errCh:
|
||||
case <-time.After(config.stateTimeout):
|
||||
err = errors.New("time out while waiting for image to register")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for image: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("image_name", config.ImageName)
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// Cleanup.
|
||||
func (s *StepRegisterImage) Cleanup(state multistep.StateBag) {}
|
100
builder/googlecompute/step_register_image_test.go
Normal file
100
builder/googlecompute/step_register_image_test.go
Normal file
@ -0,0 +1,100 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/mitchellh/multistep"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestStepRegisterImage_impl(t *testing.T) {
|
||||
var _ multistep.Step = new(StepRegisterImage)
|
||||
}
|
||||
|
||||
func TestStepRegisterImage(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepRegisterImage)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
config := state.Get("config").(*Config)
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify state
|
||||
if driver.CreateImageName != config.ImageName {
|
||||
t.Fatalf("bad: %#v", driver.CreateImageName)
|
||||
}
|
||||
if driver.CreateImageDesc != config.ImageDescription {
|
||||
t.Fatalf("bad: %#v", driver.CreateImageDesc)
|
||||
}
|
||||
|
||||
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 TestStepRegisterImage_waitError(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepRegisterImage)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
errCh <- errors.New("error")
|
||||
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
driver.CreateImageErrCh = errCh
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify state
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
if _, ok := state.GetOk("image_name"); ok {
|
||||
t.Fatal("should NOT have image_name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepRegisterImage_errorTimeout(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepRegisterImage)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
<-time.After(10 * time.Millisecond)
|
||||
errCh <- nil
|
||||
}()
|
||||
|
||||
config := state.Get("config").(*Config)
|
||||
config.stateTimeout = 1 * time.Microsecond
|
||||
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
driver.CreateImageErrCh = errCh
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify state
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
if _, ok := state.GetOk("image_name"); ok {
|
||||
t.Fatal("should NOT have image name")
|
||||
}
|
||||
}
|
20
builder/googlecompute/step_test.go
Normal file
20
builder/googlecompute/step_test.go
Normal file
@ -0,0 +1,20 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testState(t *testing.T) multistep.StateBag {
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("config", testConfigStruct(t))
|
||||
state.Put("driver", &DriverMock{})
|
||||
state.Put("hook", &packer.MockHook{})
|
||||
state.Put("ui", &packer.BasicUi{
|
||||
Reader: new(bytes.Buffer),
|
||||
Writer: new(bytes.Buffer),
|
||||
})
|
||||
return state
|
||||
}
|
52
builder/googlecompute/step_update_gsutil.go
Normal file
52
builder/googlecompute/step_update_gsutil.go
Normal file
@ -0,0 +1,52 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// StepUpdateGsutil represents a Packer build step that updates the gsutil
|
||||
// utility to the latest version available.
|
||||
type StepUpdateGsutil int
|
||||
|
||||
// Run executes the Packer build step that updates the gsutil utility to the
|
||||
// latest version available.
|
||||
//
|
||||
// This step is required to prevent the image creation process from hanging;
|
||||
// the image creation process utilizes the gcimagebundle cli tool which will
|
||||
// prompt to update gsutil if a newer version is available.
|
||||
func (s *StepUpdateGsutil) Run(state multistep.StateBag) multistep.StepAction {
|
||||
comm := state.Get("communicator").(packer.Communicator)
|
||||
config := state.Get("config").(*Config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
sudoPrefix := ""
|
||||
|
||||
if config.SSHUsername != "root" {
|
||||
sudoPrefix = "sudo "
|
||||
}
|
||||
|
||||
gsutilUpdateCmd := "/usr/local/bin/gsutil update -n -f"
|
||||
cmd := new(packer.RemoteCmd)
|
||||
cmd.Command = fmt.Sprintf("%s%s", sudoPrefix, gsutilUpdateCmd)
|
||||
|
||||
ui.Say("Updating gsutil...")
|
||||
err := cmd.StartWithUi(comm, ui)
|
||||
if err == nil && cmd.ExitStatus != 0 {
|
||||
err = fmt.Errorf(
|
||||
"gsutil update exited with non-zero exit status: %d", cmd.ExitStatus)
|
||||
}
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error updating gsutil: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// Cleanup.
|
||||
func (s *StepUpdateGsutil) Cleanup(state multistep.StateBag) {}
|
85
builder/googlecompute/step_update_gsutil_test.go
Normal file
85
builder/googlecompute/step_update_gsutil_test.go
Normal file
@ -0,0 +1,85 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
func TestStepUpdateGsutil_impl(t *testing.T) {
|
||||
var _ multistep.Step = new(StepUpdateGsutil)
|
||||
}
|
||||
|
||||
func TestStepUpdateGsutil(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepUpdateGsutil)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
comm := new(packer.MockCommunicator)
|
||||
state.Put("communicator", comm)
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify
|
||||
if !comm.StartCalled {
|
||||
t.Fatal("start should be called")
|
||||
}
|
||||
if strings.HasPrefix(comm.StartCmd.Command, "sudo") {
|
||||
t.Fatal("should not sudo")
|
||||
}
|
||||
if !strings.Contains(comm.StartCmd.Command, "gsutil update") {
|
||||
t.Fatalf("bad command: %#v", comm.StartCmd.Command)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepUpdateGsutil_badExitStatus(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepUpdateGsutil)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
comm := new(packer.MockCommunicator)
|
||||
comm.StartExitStatus = 12
|
||||
state.Put("communicator", comm)
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepUpdateGsutil_nonRoot(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepUpdateGsutil)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
comm := new(packer.MockCommunicator)
|
||||
state.Put("communicator", comm)
|
||||
|
||||
config := state.Get("config").(*Config)
|
||||
config.SSHUsername = "bob"
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify
|
||||
if !comm.StartCalled {
|
||||
t.Fatal("start should be called")
|
||||
}
|
||||
if !strings.HasPrefix(comm.StartCmd.Command, "sudo") {
|
||||
t.Fatal("should sudo")
|
||||
}
|
||||
if !strings.Contains(comm.StartCmd.Command, "gsutil update") {
|
||||
t.Fatalf("bad command: %#v", comm.StartCmd.Command)
|
||||
}
|
||||
}
|
45
builder/googlecompute/step_upload_image.go
Normal file
45
builder/googlecompute/step_upload_image.go
Normal file
@ -0,0 +1,45 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// StepUploadImage represents a Packer build step that uploads GCE machine images.
|
||||
type StepUploadImage int
|
||||
|
||||
// Run executes the Packer build step that uploads a GCE machine image.
|
||||
func (s *StepUploadImage) Run(state multistep.StateBag) multistep.StepAction {
|
||||
comm := state.Get("communicator").(packer.Communicator)
|
||||
config := state.Get("config").(*Config)
|
||||
imageFilename := state.Get("image_file_name").(string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
sudoPrefix := ""
|
||||
if config.SSHUsername != "root" {
|
||||
sudoPrefix = "sudo "
|
||||
}
|
||||
|
||||
ui.Say("Uploading image...")
|
||||
cmd := new(packer.RemoteCmd)
|
||||
cmd.Command = fmt.Sprintf("%s/usr/local/bin/gsutil cp %s gs://%s",
|
||||
sudoPrefix, imageFilename, config.BucketName)
|
||||
err := cmd.StartWithUi(comm, ui)
|
||||
if err == nil && cmd.ExitStatus != 0 {
|
||||
err = fmt.Errorf(
|
||||
"gsutil exited with non-zero exit status: %d", cmd.ExitStatus)
|
||||
}
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error uploading image: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// Cleanup.
|
||||
func (s *StepUploadImage) Cleanup(state multistep.StateBag) {}
|
88
builder/googlecompute/step_upload_image_test.go
Normal file
88
builder/googlecompute/step_upload_image_test.go
Normal file
@ -0,0 +1,88 @@
|
||||
package googlecompute
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
func TestStepUploadImage_impl(t *testing.T) {
|
||||
var _ multistep.Step = new(StepUploadImage)
|
||||
}
|
||||
|
||||
func TestStepUploadImage(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepUploadImage)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
comm := new(packer.MockCommunicator)
|
||||
state.Put("communicator", comm)
|
||||
state.Put("image_file_name", "foo")
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify
|
||||
if !comm.StartCalled {
|
||||
t.Fatal("start should be called")
|
||||
}
|
||||
if strings.HasPrefix(comm.StartCmd.Command, "sudo") {
|
||||
t.Fatal("should not sudo")
|
||||
}
|
||||
if !strings.Contains(comm.StartCmd.Command, "gsutil cp") {
|
||||
t.Fatalf("bad command: %#v", comm.StartCmd.Command)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepUploadImage_badExitStatus(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepUploadImage)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
comm := new(packer.MockCommunicator)
|
||||
comm.StartExitStatus = 12
|
||||
state.Put("communicator", comm)
|
||||
state.Put("image_file_name", "foo")
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionHalt {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
if _, ok := state.GetOk("error"); !ok {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepUploadImage_nonRoot(t *testing.T) {
|
||||
state := testState(t)
|
||||
step := new(StepUploadImage)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
comm := new(packer.MockCommunicator)
|
||||
state.Put("communicator", comm)
|
||||
state.Put("image_file_name", "foo")
|
||||
|
||||
config := state.Get("config").(*Config)
|
||||
config.SSHUsername = "bob"
|
||||
|
||||
// run the step
|
||||
if action := step.Run(state); action != multistep.ActionContinue {
|
||||
t.Fatalf("bad action: %#v", action)
|
||||
}
|
||||
|
||||
// Verify
|
||||
if !comm.StartCalled {
|
||||
t.Fatal("start should be called")
|
||||
}
|
||||
if !strings.HasPrefix(comm.StartCmd.Command, "sudo") {
|
||||
t.Fatal("should sudo")
|
||||
}
|
||||
if !strings.Contains(comm.StartCmd.Command, "gsutil cp") {
|
||||
t.Fatalf("bad command: %#v", comm.StartCmd.Command)
|
||||
}
|
||||
}
|
@ -24,6 +24,7 @@ const defaultConfig = `
|
||||
"amazon-instance": "packer-builder-amazon-instance",
|
||||
"digitalocean": "packer-builder-digitalocean",
|
||||
"docker": "packer-builder-docker",
|
||||
"googlecompute": "packer-builder-googlecompute",
|
||||
"openstack": "packer-builder-openstack",
|
||||
"qemu": "packer-builder-qemu",
|
||||
"virtualbox": "packer-builder-virtualbox",
|
||||
|
15
plugin/builder-googlecompute/main.go
Normal file
15
plugin/builder-googlecompute/main.go
Normal file
@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/builder/googlecompute"
|
||||
"github.com/mitchellh/packer/packer/plugin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
server, err := plugin.Server()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
server.RegisterBuilder(new(googlecompute.Builder))
|
||||
server.Serve()
|
||||
}
|
1
plugin/builder-googlecompute/main_test.go
Normal file
1
plugin/builder-googlecompute/main_test.go
Normal file
@ -0,0 +1 @@
|
||||
package main
|
127
website/source/docs/builders/googlecompute.markdown
Normal file
127
website/source/docs/builders/googlecompute.markdown
Normal file
@ -0,0 +1,127 @@
|
||||
---
|
||||
layout: "docs"
|
||||
---
|
||||
|
||||
# Google Compute Builder
|
||||
|
||||
Type: `googlecompute`
|
||||
|
||||
The `googlecompute` builder is able to create
|
||||
[images](https://developers.google.com/compute/docs/images)
|
||||
for use with [Google Compute Engine](https://cloud.google.com/products/compute-engine)
|
||||
(GCE) based on existing images. Google Compute Engine doesn't allow the creation
|
||||
of images from scratch.
|
||||
|
||||
## Setting Up API Access
|
||||
|
||||
There is a small setup step required in order to obtain the credentials
|
||||
that Packer needs to use Google Compute Engine. This needs to be done only
|
||||
once if you intend to share the credentials.
|
||||
|
||||
In order for Packer to talk to Google Compute Engine, it will need
|
||||
a _client secrets_ JSON file and a _client private key_. Both of these are
|
||||
obtained from the [Google Cloud Console](https://cloud.google.com/console).
|
||||
|
||||
Follow the steps below:
|
||||
|
||||
1. Log into the [Google Cloud Console](https://cloud.google.com/console)
|
||||
2. Click on the project you want to use Packer with (or create one if you
|
||||
don't have one yet).
|
||||
3. Click "APIs & auth" in the left sidebar
|
||||
4. Click "Registered apps" in the left sidebar
|
||||
5. Click "Register App" and register a "Web Application". Choose any
|
||||
name you'd like.
|
||||
7. After creating the app, click "Certificate" (below the OAuth 2.0 Client
|
||||
ID section), and click "Download JSON". This is your _client secrets JSON_
|
||||
file. Make sure you didn't download the JSON from the "OAuth 2.0" section!
|
||||
This is a common mistake and will cause the builder to not work.
|
||||
8. Next, click "Generate Certificate". You should be prompted to download
|
||||
a private key. Note the password for the private key! This private key
|
||||
is your _client private key_.
|
||||
|
||||
Finally, one last step, you'll have to convert the `p12` file you
|
||||
got from Google into the PEM format. You can do this with OpenSSL, which
|
||||
is installed standard on most Unixes:
|
||||
|
||||
```
|
||||
$ openssl pkcs12 -in <path to .p12> -nocerts -passin pass:notasecret \
|
||||
-nodes -out private_key.pem
|
||||
```
|
||||
|
||||
The client secrets JSON you downloaded along with the new "private\_key.pem"
|
||||
file are the two files you need to configure Packer with to talk to GCE.
|
||||
|
||||
## Basic Example
|
||||
|
||||
Below is a fully functioning example. It doesn't do anything useful,
|
||||
since no provisioners are defined, but it will effectively repackage an
|
||||
existing GCE image. The client secrets file and private key file are the
|
||||
files obtained in the previous section.
|
||||
|
||||
<pre class="prettyprint">
|
||||
{
|
||||
"type": "googlecompute",
|
||||
"bucket_name": "packer-images",
|
||||
"client_secrets_file": "client_secret.json",
|
||||
"private_key_file": "XXXXXX-privatekey.p12",
|
||||
"project_id": "my-project",
|
||||
"source_image": "debian-7-wheezy-v20131014",
|
||||
"zone": "us-central1-a"
|
||||
}
|
||||
</pre>
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
Configuration options are organized below into two categories: required and optional. Within
|
||||
each category, the available options are alphabetized and described.
|
||||
|
||||
Required:
|
||||
|
||||
* `bucket_name` (string) - The Google Cloud Storage bucket to store the
|
||||
images that are created.
|
||||
|
||||
* `client_secrets_file` (string) - The client secrets JSON file that
|
||||
was set up in the section above.
|
||||
|
||||
* `private_key_file` (string) - The client private key file that was
|
||||
generated in the section above.
|
||||
|
||||
* `project_id` (string) - The project ID that will be used to launch instances
|
||||
and store images.
|
||||
|
||||
* `source_image` (string) - The source image to use to create the new image
|
||||
from. Example: "debian-7"
|
||||
|
||||
* `zone` (string) - The zone in which to launch the instance used to create
|
||||
the image. Example: "us-central1-a"
|
||||
|
||||
Optional:
|
||||
|
||||
* `image_name` (string) - The unique name of the resulting image.
|
||||
Defaults to `packer-{{timestamp}}`.
|
||||
|
||||
* `image_description` (string) - The description of the resulting image.
|
||||
|
||||
* `machine_type` (string) - The machine type. Defaults to `n1-standard-1`.
|
||||
|
||||
* `network` (string) - The Google Compute network to use for the launched
|
||||
instance. Defaults to `default`.
|
||||
|
||||
* `passphrase` (string) - The passphrase to use if the `private_key_file`
|
||||
is encrypted.
|
||||
|
||||
* `ssh_port` (int) - The SSH port. Defaults to 22.
|
||||
|
||||
* `ssh_timeout` (string) - The time to wait for SSH to become available.
|
||||
Defaults to "1m".
|
||||
|
||||
* `ssh_username` (string) - The SSH username. Defaults to "root".
|
||||
|
||||
* `state_timeout` (string) - The time to wait for instance state changes.
|
||||
Defaults to "5m".
|
||||
|
||||
## Gotchas
|
||||
|
||||
Centos images have root ssh access disabled by default. Set `ssh_username` to any user, which will be created by packer with sudo access.
|
||||
|
||||
The machine type must have a scratch disk, which means you can't use an `f1-micro` or `g1-small` to build images.
|
@ -33,6 +33,7 @@
|
||||
<li><a href="/docs/builders/amazon.html">Amazon EC2 (AMI)</a></li>
|
||||
<li><a href="/docs/builders/digitalocean.html">DigitalOcean</a></li>
|
||||
<li><a href="/docs/builders/docker.html">Docker</a></li>
|
||||
<li><a href="/docs/builders/googlecompute.html">Google Compute Engine</a></li>
|
||||
<li><a href="/docs/builders/openstack.html">OpenStack</a></li>
|
||||
<li><a href="/docs/builders/qemu.html">QEMU</a></li>
|
||||
<li><a href="/docs/builders/virtualbox.html">VirtualBox</a></li>
|
||||
|
@ -174,7 +174,6 @@ header .header {
|
||||
|
||||
ul,
|
||||
ol {
|
||||
list-style-type: circle;
|
||||
list-style-position: inside;
|
||||
margin-top: $baseline;
|
||||
margin-left: 20px;
|
||||
@ -188,6 +187,14 @@ header .header {
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: circle;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
div.alert {
|
||||
font-family: $serif;
|
||||
font-size: 17px;
|
||||
|
Loading…
x
Reference in New Issue
Block a user