Merge pull request #1464 from mitchellh/f-new-gce

builder/googlecompute: uses new auth style
This commit is contained in:
Mitchell Hashimoto 2014-09-05 09:50:56 -07:00
commit 7002b8f07e
8 changed files with 125 additions and 138 deletions

View File

@ -0,0 +1,35 @@
package googlecompute
import (
"encoding/json"
"os"
)
// accountFile represents the structure of the account file JSON file.
type accountFile struct {
PrivateKeyId string `json:"private_key_id"`
PrivateKey string `json:"private_key"`
ClientEmail string `json:"client_email"`
ClientId string `json:"client_id"`
}
// clientSecretsFile represents the structure of the client secrets JSON file.
type clientSecretsFile struct {
Web struct {
AuthURI string `json:"auth_uri"`
ClientEmail string `json:"client_email"`
ClientId string `json:"client_id"`
TokenURI string `json:"token_uri"`
}
}
func loadJSON(result interface{}, path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
dec := json.NewDecoder(f)
return dec.Decode(result)
}

View File

@ -35,7 +35,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
// representing a GCE machine image. // representing a GCE machine image.
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
driver, err := NewDriverGCE( driver, err := NewDriverGCE(
ui, b.config.ProjectId, b.config.clientSecrets, b.config.privateKeyBytes) ui, b.config.ProjectId, &b.config.account, &b.config.clientSecrets)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,32 +0,0 @@
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
}

View File

@ -1,31 +0,0 @@
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"}}`

View File

@ -16,8 +16,11 @@ import (
type Config struct { type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
BucketName string `mapstructure:"bucket_name"` AccountFile string `mapstructure:"account_file"`
ClientSecretsFile string `mapstructure:"client_secrets_file"` ClientSecretsFile string `mapstructure:"client_secrets_file"`
ProjectId string `mapstructure:"project_id"`
BucketName string `mapstructure:"bucket_name"`
DiskSizeGb int64 `mapstructure:"disk_size"` DiskSizeGb int64 `mapstructure:"disk_size"`
ImageName string `mapstructure:"image_name"` ImageName string `mapstructure:"image_name"`
ImageDescription string `mapstructure:"image_description"` ImageDescription string `mapstructure:"image_description"`
@ -25,9 +28,6 @@ type Config struct {
MachineType string `mapstructure:"machine_type"` MachineType string `mapstructure:"machine_type"`
Metadata map[string]string `mapstructure:"metadata"` Metadata map[string]string `mapstructure:"metadata"`
Network string `mapstructure:"network"` Network string `mapstructure:"network"`
Passphrase string `mapstructure:"passphrase"`
PrivateKeyFile string `mapstructure:"private_key_file"`
ProjectId string `mapstructure:"project_id"`
SourceImage string `mapstructure:"source_image"` SourceImage string `mapstructure:"source_image"`
SourceImageProjectId string `mapstructure:"source_image_project_id"` SourceImageProjectId string `mapstructure:"source_image_project_id"`
SSHUsername string `mapstructure:"ssh_username"` SSHUsername string `mapstructure:"ssh_username"`
@ -37,7 +37,8 @@ type Config struct {
Tags []string `mapstructure:"tags"` Tags []string `mapstructure:"tags"`
Zone string `mapstructure:"zone"` Zone string `mapstructure:"zone"`
clientSecrets *clientSecrets account accountFile
clientSecrets clientSecretsFile
instanceName string instanceName string
privateKeyBytes []byte privateKeyBytes []byte
sshTimeout time.Duration sshTimeout time.Duration
@ -104,15 +105,15 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
// Process Templates // Process Templates
templates := map[string]*string{ templates := map[string]*string{
"bucket_name": &c.BucketName, "account_file": &c.AccountFile,
"client_secrets_file": &c.ClientSecretsFile, "client_secrets_file": &c.ClientSecretsFile,
"bucket_name": &c.BucketName,
"image_name": &c.ImageName, "image_name": &c.ImageName,
"image_description": &c.ImageDescription, "image_description": &c.ImageDescription,
"instance_name": &c.InstanceName, "instance_name": &c.InstanceName,
"machine_type": &c.MachineType, "machine_type": &c.MachineType,
"network": &c.Network, "network": &c.Network,
"passphrase": &c.Passphrase,
"private_key_file": &c.PrivateKeyFile,
"project_id": &c.ProjectId, "project_id": &c.ProjectId,
"source_image": &c.SourceImage, "source_image": &c.SourceImage,
"source_image_project_id": &c.SourceImageProjectId, "source_image_project_id": &c.SourceImageProjectId,
@ -137,16 +138,16 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
errs, errors.New("a bucket_name must be specified")) errs, errors.New("a bucket_name must be specified"))
} }
if c.AccountFile == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("an account_file must be specified"))
}
if c.ClientSecretsFile == "" { if c.ClientSecretsFile == "" {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
errs, errors.New("a client_secrets_file must be specified")) 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 == "" { if c.ProjectId == "" {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
errs, errors.New("a project_id must be specified")) errs, errors.New("a project_id must be specified"))
@ -177,22 +178,17 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
} }
c.stateTimeout = stateTimeout c.stateTimeout = stateTimeout
if c.ClientSecretsFile != "" { if c.AccountFile != "" {
// Load the client secrets file. if err := loadJSON(&c.account, c.AccountFile); err != nil {
cs, err := loadClientSecrets(c.ClientSecretsFile)
if err != nil {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing client secrets file: %s", err)) errs, fmt.Errorf("Failed parsing account file: %s", err))
} }
c.clientSecrets = cs
} }
if c.PrivateKeyFile != "" { if c.ClientSecretsFile != "" {
// Load the private key. if err := loadJSON(&c.clientSecrets, c.ClientSecretsFile); err != nil {
c.privateKeyBytes, err = processPrivateKeyFile(c.PrivateKeyFile, c.Passphrase)
if err != nil {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed loading private key file: %s", err)) errs, fmt.Errorf("Failed parsing client secrets file: %s", err))
} }
} }

View File

@ -1,14 +1,15 @@
package googlecompute package googlecompute
import ( import (
"io/ioutil"
"testing" "testing"
) )
func testConfig(t *testing.T) map[string]interface{} { func testConfig(t *testing.T) map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"account_file": testAccountFile(t),
"bucket_name": "foo", "bucket_name": "foo",
"client_secrets_file": testClientSecretsFile(t), "client_secrets_file": testClientSecretsFile(t),
"private_key_file": testPrivateKeyFile(t),
"project_id": "hashicorp", "project_id": "hashicorp",
"source_image": "foo", "source_image": "foo",
"zone": "us-east-1a", "zone": "us-east-1a",
@ -84,16 +85,6 @@ func TestConfigPrepare(t *testing.T) {
true, true,
}, },
{
"private_key_file",
nil,
true,
},
{
"private_key_file",
testPrivateKeyFile(t),
false,
},
{ {
"private_key_file", "private_key_file",
"/tmp/i/should/not/exist", "/tmp/i/should/not/exist",
@ -174,3 +165,37 @@ func TestConfigPrepare(t *testing.T) {
} }
} }
} }
func testAccountFile(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(testAccountContent)); err != nil {
t.Fatalf("err: %s", err)
}
return tf.Name()
}
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()
}
// This is just some dummy data that doesn't actually work (it was revoked
// a long time ago).
const testAccountContent = `{}`
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"}}`

View File

@ -23,22 +23,27 @@ type driverGCE struct {
const DriverScopes string = "https://www.googleapis.com/auth/compute " + const DriverScopes string = "https://www.googleapis.com/auth/compute " +
"https://www.googleapis.com/auth/devstorage.full_control" "https://www.googleapis.com/auth/devstorage.full_control"
func NewDriverGCE(ui packer.Ui, projectId string, c *clientSecrets, key []byte) (Driver, error) { func NewDriverGCE(ui packer.Ui, p string, a *accountFile, c *clientSecretsFile) (Driver, error) {
log.Printf("[INFO] Requesting token...") // Get the token for use in our requests
log.Printf("[INFO] -- Email: %s", c.Web.ClientEmail) log.Printf("[INFO] Requesting Google token...")
log.Printf("[INFO] -- Email: %s", a.ClientEmail)
log.Printf("[INFO] -- Scopes: %s", DriverScopes) log.Printf("[INFO] -- Scopes: %s", DriverScopes)
log.Printf("[INFO] -- Private Key Length: %d", len(key)) log.Printf("[INFO] -- Private Key Length: %d", len(a.PrivateKey))
log.Printf("[INFO] -- Token URL: %s", c.Web.TokenURI) log.Printf("[INFO] -- Token URL: %s", c.Web.TokenURI)
jwtTok := jwt.NewToken(c.Web.ClientEmail, DriverScopes, key) jwtTok := jwt.NewToken(
a.ClientEmail,
DriverScopes,
[]byte(a.PrivateKey))
jwtTok.ClaimSet.Aud = c.Web.TokenURI jwtTok.ClaimSet.Aud = c.Web.TokenURI
token, err := jwtTok.Assert(new(http.Client)) token, err := jwtTok.Assert(new(http.Client))
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("Error retrieving auth token: %s", err)
} }
// Instantiate the transport to communicate to Google
transport := &oauth.Transport{ transport := &oauth.Transport{
Config: &oauth.Config{ Config: &oauth.Config{
ClientId: c.Web.ClientId, ClientId: a.ClientId,
Scope: DriverScopes, Scope: DriverScopes,
TokenURL: c.Web.TokenURI, TokenURL: c.Web.TokenURI,
AuthURL: c.Web.AuthURI, AuthURL: c.Web.AuthURI,
@ -46,14 +51,14 @@ func NewDriverGCE(ui packer.Ui, projectId string, c *clientSecrets, key []byte)
Token: token, Token: token,
} }
log.Printf("[INFO] Instantiating client...") log.Printf("[INFO] Instantiating GCE client...")
service, err := compute.New(transport.Client()) service, err := compute.New(transport.Client())
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &driverGCE{ return &driverGCE{
projectId: projectId, projectId: p,
service: service, service: service,
ui: ui, ui: ui,
}, nil }, nil

View File

@ -13,38 +13,27 @@ for use with [Google Compute Engine](https://cloud.google.com/products/compute-e
(GCE) based on existing images. Google Compute Engine doesn't allow the creation (GCE) based on existing images. Google Compute Engine doesn't allow the creation
of images from scratch. of images from scratch.
## Setting Up API Access ## Authentication
There is a small setup step required in order to obtain the credentials Authenticating with Google Cloud services requires two separate JSON
that Packer needs to use Google Compute Engine. This needs to be done only files: one which we call the _account file_ and the _client secrets file_.
once if you intend to share the credentials.
In order for Packer to talk to Google Compute Engine, it will need Both of these files are downloaded directly from the
a _client secrets_ JSON file and a _client private key_. Both of these are [Google Developers Console](https://console.developers.google.com). To make
obtained from the [Google Cloud Console](https://cloud.google.com/console). the process more straightforwarded, it is documented here.
Follow the steps below: 1. Log into the [Google Developers Console](https://console.developers.google.com)
and select a project.
1. Log into the [Google Cloud Console](https://cloud.google.com/console) 2. Under the "APIs & Auth" section, click "Credentials."
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 "Credentials" in the left sidebar
5. Click "Create New Client ID" and choose "Service Account"
6. A private key will be downloaded for you. Note the password for the private key! This private key is your _client private key_.
7. After creating the account, 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.
Finally, one last step, you'll have to convert the `p12` file you 3. Click the "Download JSON" button under the "Compute Engine and App Engine"
got from Google into the PEM format. You can do this with OpenSSL, which account in the OAuth section. The file should start with "client\_secrets".
is installed standard on most Unixes: This is your _client secrets file_.
``` 4. Create a new OAuth client ID and select "Service Account" as the type
$ openssl pkcs12 -in <path to .p12> -nocerts -passin pass:notasecret \ of account. Once created, a JSON file should be downloaded. This is your
-nodes -out private_key.pem _account file_.
```
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 ## Basic Example
@ -57,8 +46,8 @@ files obtained in the previous section.
{ {
"type": "googlecompute", "type": "googlecompute",
"bucket_name": "my-project-packer-images", "bucket_name": "my-project-packer-images",
"account_file": "account.json",
"client_secrets_file": "client_secret.json", "client_secrets_file": "client_secret.json",
"private_key_file": "XXXXXX-privatekey.p12",
"project_id": "my-project", "project_id": "my-project",
"source_image": "debian-7-wheezy-v20140718", "source_image": "debian-7-wheezy-v20140718",
"zone": "us-central1-a" "zone": "us-central1-a"
@ -72,6 +61,9 @@ each category, the available options are alphabetized and described.
### Required: ### Required:
* `account_file` (string) - The JSON file containing your account credentials.
Instructions for how to retrieve these are above.
* `bucket_name` (string) - The Google Cloud Storage bucket to store the * `bucket_name` (string) - The Google Cloud Storage bucket to store the
images that are created. The bucket must already exist in your project. images that are created. The bucket must already exist in your project.
@ -113,9 +105,6 @@ each category, the available options are alphabetized and described.
* `network` (string) - The Google Compute network to use for the launched * `network` (string) - The Google Compute network to use for the launched
instance. Defaults to `default`. instance. Defaults to `default`.
* `passphrase` (string) - The passphrase to use if the `private_key_file`
is encrypted.
* `ssh_port` (integer) - The SSH port. Defaults to 22. * `ssh_port` (integer) - The SSH port. Defaults to 22.
* `ssh_timeout` (string) - The time to wait for SSH to become available. * `ssh_timeout` (string) - The time to wait for SSH to become available.