Commit 87001dba authored by Eric Johnson's avatar Eric Johnson

Merge pull request #1679 from evandbrown/gce-service-accounts

Use golang/oauth2, no longer require client_secrets.json, and use
parents 289fdc62 ff149df3
...@@ -13,16 +13,6 @@ type accountFile struct { ...@@ -13,16 +13,6 @@ type accountFile struct {
ClientId string `json:"client_id"` 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 { func loadJSON(result interface{}, path string) error {
f, err := os.Open(path) f, err := os.Open(path)
if err != nil { if err != nil {
......
...@@ -35,7 +35,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -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.account, &b.config.clientSecrets) ui, b.config.ProjectId, &b.config.account)
if err != nil { if err != nil {
return nil, err return nil, err
} }
......
...@@ -17,7 +17,6 @@ type Config struct { ...@@ -17,7 +17,6 @@ type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
AccountFile string `mapstructure:"account_file"` AccountFile string `mapstructure:"account_file"`
ClientSecretsFile string `mapstructure:"client_secrets_file"`
ProjectId string `mapstructure:"project_id"` ProjectId string `mapstructure:"project_id"`
BucketName string `mapstructure:"bucket_name"` BucketName string `mapstructure:"bucket_name"`
...@@ -38,7 +37,6 @@ type Config struct { ...@@ -38,7 +37,6 @@ type Config struct {
Zone string `mapstructure:"zone"` Zone string `mapstructure:"zone"`
account accountFile account accountFile
clientSecrets clientSecretsFile
instanceName string instanceName string
privateKeyBytes []byte privateKeyBytes []byte
sshTimeout time.Duration sshTimeout time.Duration
...@@ -106,7 +104,6 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -106,7 +104,6 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
// Process Templates // Process Templates
templates := map[string]*string{ templates := map[string]*string{
"account_file": &c.AccountFile, "account_file": &c.AccountFile,
"client_secrets_file": &c.ClientSecretsFile,
"bucket_name": &c.BucketName, "bucket_name": &c.BucketName,
"image_name": &c.ImageName, "image_name": &c.ImageName,
...@@ -138,16 +135,6 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -138,16 +135,6 @@ 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 == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("a client_secrets_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"))
...@@ -185,13 +172,6 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { ...@@ -185,13 +172,6 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
} }
} }
if c.ClientSecretsFile != "" {
if err := loadJSON(&c.clientSecrets, c.ClientSecretsFile); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing client secrets file: %s", err))
}
}
// Check for any errors. // Check for any errors.
if errs != nil && len(errs.Errors) > 0 { if errs != nil && len(errs.Errors) > 0 {
return nil, nil, errs return nil, nil, errs
......
...@@ -9,7 +9,6 @@ func testConfig(t *testing.T) map[string]interface{} { ...@@ -9,7 +9,6 @@ func testConfig(t *testing.T) map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"account_file": testAccountFile(t), "account_file": testAccountFile(t),
"bucket_name": "foo", "bucket_name": "foo",
"client_secrets_file": testClientSecretsFile(t),
"project_id": "hashicorp", "project_id": "hashicorp",
"source_image": "foo", "source_image": "foo",
"zone": "us-east-1a", "zone": "us-east-1a",
...@@ -69,22 +68,6 @@ func TestConfigPrepare(t *testing.T) { ...@@ -69,22 +68,6 @@ func TestConfigPrepare(t *testing.T) {
false, false,
}, },
{
"client_secrets_file",
nil,
true,
},
{
"client_secrets_file",
testClientSecretsFile(t),
false,
},
{
"client_secrets_file",
"/tmp/i/should/not/exist",
true,
},
{ {
"private_key_file", "private_key_file",
"/tmp/i/should/not/exist", "/tmp/i/should/not/exist",
...@@ -180,22 +163,6 @@ func testAccountFile(t *testing.T) string { ...@@ -180,22 +163,6 @@ func testAccountFile(t *testing.T) string {
return tf.Name() 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 // This is just some dummy data that doesn't actually work (it was revoked
// a long time ago). // a long time ago).
const testAccountContent = `{}` 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"}}`
...@@ -6,9 +6,9 @@ import ( ...@@ -6,9 +6,9 @@ import (
"net/http" "net/http"
"time" "time"
"code.google.com/p/goauth2/oauth"
"code.google.com/p/goauth2/oauth/jwt"
"code.google.com/p/google-api-go-client/compute/v1" "code.google.com/p/google-api-go-client/compute/v1"
"github.com/golang/oauth2"
"github.com/golang/oauth2/google"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
...@@ -20,39 +20,35 @@ type driverGCE struct { ...@@ -20,39 +20,35 @@ type driverGCE struct {
ui packer.Ui ui packer.Ui
} }
const DriverScopes string = "https://www.googleapis.com/auth/compute " + var 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, p string, a *accountFile) (Driver, error) {
func NewDriverGCE(ui packer.Ui, p string, a *accountFile, c *clientSecretsFile) (Driver, error) { var f *oauth2.Flow
// Get the token for use in our requests var err error
log.Printf("[INFO] Requesting Google token...")
log.Printf("[INFO] -- Email: %s", a.ClientEmail) // Auth with AccountFile first if provided
log.Printf("[INFO] -- Scopes: %s", DriverScopes) if a.PrivateKey != "" {
log.Printf("[INFO] -- Private Key Length: %d", len(a.PrivateKey)) log.Printf("[INFO] Requesting Google token via AccountFile...")
log.Printf("[INFO] -- Token URL: %s", c.Web.TokenURI) log.Printf("[INFO] -- Email: %s", a.ClientEmail)
jwtTok := jwt.NewToken( log.Printf("[INFO] -- Scopes: %s", DriverScopes)
a.ClientEmail, log.Printf("[INFO] -- Private Key Length: %d", len(a.PrivateKey))
DriverScopes,
[]byte(a.PrivateKey)) f, err = oauth2.New(
jwtTok.ClaimSet.Aud = c.Web.TokenURI oauth2.JWTClient(a.ClientEmail, []byte(a.PrivateKey)),
token, err := jwtTok.Assert(new(http.Client)) oauth2.Scope(DriverScopes...),
if err != nil { google.JWTEndpoint())
return nil, fmt.Errorf("Error retrieving auth token: %s", err) } else {
log.Printf("[INFO] Requesting Google token via GCE Service Role...")
f, err = oauth2.New(google.ComputeEngineAccount(""))
} }
// Instantiate the transport to communicate to Google if err != nil {
transport := &oauth.Transport{ return nil, err
Config: &oauth.Config{
ClientId: a.ClientId,
Scope: DriverScopes,
TokenURL: c.Web.TokenURI,
AuthURL: c.Web.AuthURI,
},
Token: token,
} }
log.Printf("[INFO] Instantiating GCE client...") log.Printf("[INFO] Instantiating GCE client using...")
service, err := compute.New(transport.Client()) service, err := compute.New(&http.Client{Transport: f.NewTransport()})
if err != nil { if err != nil {
return nil, err return nil, err
} }
......
...@@ -41,8 +41,7 @@ Set the following self-explanatory environmental variables: ...@@ -41,8 +41,7 @@ Set the following self-explanatory environmental variables:
Set the following environmental variables: Set the following environmental variables:
* `GC_BUCKET_NAME` * `GC_BUCKET_NAME`
* `GC_CLIENT_SECRETS_FILE` * `GC_ACCOUNT_FILE`
* `GC_PRIVATE_KEY_FILE`
* `GC_PROJECT_ID` * `GC_PROJECT_ID`
### Running ### Running
......
...@@ -8,8 +8,7 @@ fixtures builder-googlecompute ...@@ -8,8 +8,7 @@ fixtures builder-googlecompute
# Required parameters # Required parameters
: ${GC_BUCKET_NAME:?} : ${GC_BUCKET_NAME:?}
: ${GC_CLIENT_SECRETS_FILE:?} : ${GC_ACCOUNT_FILE:?}
: ${GC_PRIVATE_KEY_FILE:?}
: ${GC_PROJECT_ID:?} : ${GC_PROJECT_ID:?}
command -v gcutil >/dev/null 2>&1 || { command -v gcutil >/dev/null 2>&1 || {
echo "'gcutil' must be installed" >&2 echo "'gcutil' must be installed" >&2
...@@ -17,8 +16,7 @@ command -v gcutil >/dev/null 2>&1 || { ...@@ -17,8 +16,7 @@ command -v gcutil >/dev/null 2>&1 || {
} }
USER_VARS="-var bucket_name=${GC_BUCKET_NAME}" USER_VARS="-var bucket_name=${GC_BUCKET_NAME}"
USER_VARS="${USER_VARS} -var client_secrets_file=${GC_CLIENT_SECRETS_FILE}" USER_VARS="${USER_VARS} -var account_file=${GC_ACCOUNT_FILE}"
USER_VARS="${USER_VARS} -var private_key_file=${GC_PRIVATE_KEY_FILE}"
USER_VARS="${USER_VARS} -var project_id=${GC_PROJECT_ID}" USER_VARS="${USER_VARS} -var project_id=${GC_PROJECT_ID}"
# This tests if GCE has an image that contains the given parameter. # This tests if GCE has an image that contains the given parameter.
...@@ -30,7 +28,7 @@ gc_has_image() { ...@@ -30,7 +28,7 @@ gc_has_image() {
teardown() { teardown() {
gcutil --format=names --project=${GC_PROJECT_ID} listimages \ gcutil --format=names --project=${GC_PROJECT_ID} listimages \
| grep packerbats \ | grep packerbats \
| xargs -n1 gcutil --project=${GC_PROJECT_ID} --force deleteimage | xargs -n1 gcutil --project=${GC_PROJECT_ID} deleteimage --force
} }
@test "googlecompute: build minimal.json" { @test "googlecompute: build minimal.json" {
......
{ {
"variables": { "variables": {
"bucket_name": null, "bucket_name": null,
"client_secrets_file": null, "account_file": null,
"private_key_file": null,
"project_id": null "project_id": null
}, },
"builders": [{ "builders": [{
"type": "googlecompute", "type": "googlecompute",
"bucket_name": "{{user `bucket_name`}}", "bucket_name": "{{user `bucket_name`}}",
"client_secrets_file": "{{user `client_secrets_file`}}", "account_file": "{{user `account_file`}}",
"private_key_file": "{{user `private_key_file`}}",
"project_id": "{{user `project_id`}}", "project_id": "{{user `project_id`}}",
"image_name": "packerbats-minimal-{{timestamp}}", "image_name": "packerbats-minimal-{{timestamp}}",
"source_image": "debian-7-wheezy-v20131120", "source_image": "debian-7-wheezy-v20141108",
"zone": "us-central1-a" "zone": "us-central1-a"
}] }]
} }
...@@ -9,19 +9,49 @@ description: |- ...@@ -9,19 +9,49 @@ description: |-
Type: `googlecompute` Type: `googlecompute`
The `googlecompute` Packer builder is able to create The `googlecompute` Packer builder is able to create [images](https://developers.google.com/compute/docs/images) for use with
[images](https://developers.google.com/compute/docs/images) [Google Compute Engine](https://cloud.google.com/products/compute-engine)(GCE) based on existing images. Google
for use with [Google Compute Engine](https://cloud.google.com/products/compute-engine) Compute Engine doesn't allow the creation of images from scratch.
(GCE) based on existing images. Google Compute Engine doesn't allow the creation
of images from scratch.
## Authentication ## Authentication
Authenticating with Google Cloud services requires two separate JSON Authenticating with Google Cloud services requires at most one JSON file,
files: one which we call the _account file_ and the _client secrets file_. called the _account file_. The _account file_ is **not** required if you are running
the `googlecompute` Packer builder from a GCE instance with a properly-configured
[Compute Engine Service Account](https://cloud.google.com/compute/docs/authentication.
Both of these files are downloaded directly from the ### Running With a Compute Engine Service Account
[Google Developers Console](https://console.developers.google.com). To make If you run the `googlecompute` Packer builder from a GCE instance, you can configure that
instance to use a [Compute Engine Service Account](https://cloud.google.com/compute/docs/authentication). This will allow Packer to authenticate
to Google Cloud without having to bake in a separate credential/authentication file.
To create a GCE instance that uses a service account, provide the required scopes when
launching the intance.
For `gcloud`, do this via the `--scopes` parameter:
```sh
gcloud compute --project YOUR_PROJECT instances create "INSTANCE-NAME" ... \
--scopes "https://www.googleapis.com/auth/compute" \
"https://www.googleapis.com/auth/devstorage.full_control" \
...
```
For the [Google Developers Console](https://console.developers.google.com):
1. Choose "Show advanced options"
2. Tick "Enable Compute Engine service account"
3. Choose "Read Write" for Compute
4. Chose "Full" for "Storage"
**The service account will be used automatically by Packer as long as there is
no _account file_ specified in the Packer configuration file.**
### Running Without a Compute Engine Service Account
The [Google Developers Console](https://console.developers.google.com) allows you to
create and download a credential file that will let you use the `googlecompute` Packer
builder anywhere. To make
the process more straightforwarded, it is documented here. the process more straightforwarded, it is documented here.
1. Log into the [Google Developers Console](https://console.developers.google.com) 1. Log into the [Google Developers Console](https://console.developers.google.com)
...@@ -29,27 +59,22 @@ the process more straightforwarded, it is documented here. ...@@ -29,27 +59,22 @@ the process more straightforwarded, it is documented here.
2. Under the "APIs & Auth" section, click "Credentials." 2. Under the "APIs & Auth" section, click "Credentials."
3. Click the "Download JSON" button under the "Compute Engine and App Engine" 3. Click the "Create new Client ID" button, select "Service account", and click "Create Client ID"
account in the OAuth section. The file should start with "client\_secrets".
This is your _client secrets file_.
4. Create a new OAuth client ID and select "Service Account" as the type 4. Click "Generate new JSON key" for the Service Account you just created. A JSON file will be downloaded automatically. This is your
of account. Once created, a JSON file should be downloaded. This is your
_account file_. _account file_.
## Basic Example ## Basic Example
Below is a fully functioning example. It doesn't do anything useful, Below is a fully functioning example. It doesn't do anything useful,
since no provisioners are defined, but it will effectively repackage an since no provisioners are defined, but it will effectively repackage an
existing GCE image. The client secrets file and private key file are the existing GCE image. The account file is obtained in the previous section.
files obtained in the previous section.
```javascript ```javascript
{ {
"type": "googlecompute", "type": "googlecompute",
"bucket_name": "my-project-packer-images", "bucket_name": "my-project-packer-images",
"account_file": "account.json", "account_file": "account.json",
"client_secrets_file": "client_secret.json",
"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"
...@@ -63,17 +88,8 @@ each category, the available options are alphabetized and described. ...@@ -63,17 +88,8 @@ 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
* `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 * `project_id` (string) - The project ID that will be used to launch instances
and store images. and store images.
...@@ -86,6 +102,10 @@ each category, the available options are alphabetized and described. ...@@ -86,6 +102,10 @@ each category, the available options are alphabetized and described.
### Optional: ### Optional:
* `account_file` (string) - The JSON file containing your account credentials.
Not required if you run Packer on a GCE instance with a service account.
Instructions for creating file or using service accounts are above.
* `disk_size` (integer) - The size of the disk in GB. * `disk_size` (integer) - The size of the disk in GB.
This defaults to 10, which is 10GB. This defaults to 10, which is 10GB.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment